Use Android's hardware-backed keystore to store backup key
This commit also disables the old UI as it does not work with the new key
This commit is contained in:
parent
66c0919eb5
commit
3e64c3686f
5 changed files with 55 additions and 10 deletions
|
@ -35,12 +35,7 @@
|
|||
|
||||
<activity
|
||||
android:name="com.stevesoltys.backup.activity.MainActivity"
|
||||
android:label="@string/app_name">
|
||||
<intent-filter>
|
||||
<action android:name="android.intent.action.MAIN" />
|
||||
<category android:name="android.intent.category.LAUNCHER" />
|
||||
</intent-filter>
|
||||
</activity>
|
||||
android:label="@string/app_name"/>
|
||||
|
||||
<activity
|
||||
android:name="com.stevesoltys.backup.activity.backup.CreateBackupActivity"
|
||||
|
|
|
@ -0,0 +1,48 @@
|
|||
package com.stevesoltys.backup.security
|
||||
|
||||
import android.os.Build.VERSION.SDK_INT
|
||||
import android.security.keystore.KeyProperties.*
|
||||
import android.security.keystore.KeyProtection
|
||||
import java.security.KeyStore
|
||||
import java.security.KeyStore.SecretKeyEntry
|
||||
import javax.crypto.SecretKey
|
||||
import javax.crypto.spec.SecretKeySpec
|
||||
|
||||
private const val KEY_SIZE = 256
|
||||
private const val KEY_ALIAS = "com.stevesoltys.backup"
|
||||
private const val ANDROID_KEY_STORE = "AndroidKeyStore"
|
||||
|
||||
object KeyManager {
|
||||
|
||||
private val keyStore by lazy {
|
||||
KeyStore.getInstance(ANDROID_KEY_STORE).apply {
|
||||
load(null)
|
||||
}
|
||||
}
|
||||
|
||||
fun storeBackupKey(seed: ByteArray) {
|
||||
if (seed.size < KEY_SIZE / 8) throw IllegalArgumentException()
|
||||
// TODO check if using first 256 of 512 bytes produced by PBKDF2WithHmacSHA512 is safe!
|
||||
val secretKeySpec = SecretKeySpec(seed, 0, KEY_SIZE / 8, "AES")
|
||||
val ksEntry = SecretKeyEntry(secretKeySpec)
|
||||
keyStore.setEntry(KEY_ALIAS, ksEntry, getKeyProtection())
|
||||
}
|
||||
|
||||
fun hasBackupKey() = keyStore.containsAlias(KEY_ALIAS) &&
|
||||
keyStore.entryInstanceOf(KEY_ALIAS, SecretKeyEntry::class.java)
|
||||
|
||||
fun getBackupKey(): SecretKey {
|
||||
val ksEntry = keyStore.getEntry(KEY_ALIAS, null) as SecretKeyEntry
|
||||
return ksEntry.secretKey
|
||||
}
|
||||
|
||||
private fun getKeyProtection(): KeyProtection {
|
||||
val builder = KeyProtection.Builder(PURPOSE_ENCRYPT or PURPOSE_DECRYPT)
|
||||
.setBlockModes(BLOCK_MODE_GCM)
|
||||
.setEncryptionPaddings(ENCRYPTION_PADDING_NONE)
|
||||
.setRandomizedEncryptionRequired(true)
|
||||
if (SDK_INT >= 28) builder.setUnlockedDeviceRequired(true)
|
||||
return builder.build()
|
||||
}
|
||||
|
||||
}
|
|
@ -1,10 +1,10 @@
|
|||
package com.stevesoltys.backup.settings
|
||||
|
||||
import android.app.Application
|
||||
import android.util.ByteStringUtils
|
||||
import androidx.lifecycle.AndroidViewModel
|
||||
import com.stevesoltys.backup.LiveEvent
|
||||
import com.stevesoltys.backup.MutableLiveEvent
|
||||
import com.stevesoltys.backup.security.KeyManager
|
||||
import io.github.novacrypto.bip39.*
|
||||
import io.github.novacrypto.bip39.Validation.InvalidChecksumException
|
||||
import io.github.novacrypto.bip39.Validation.InvalidWordCountException
|
||||
|
@ -46,8 +46,7 @@ class RecoveryCodeViewModel(application: Application) : AndroidViewModel(applica
|
|||
}
|
||||
val mnemonic = input.joinToString(" ")
|
||||
val seed = SeedCalculator(JavaxPBKDF2WithHmacSHA512.INSTANCE).calculateSeed(mnemonic, "")
|
||||
// TODO use KeyManager to store secret
|
||||
setBackupPassword(getApplication(), ByteStringUtils.toHexString(seed))
|
||||
KeyManager.storeBackupKey(seed)
|
||||
recoveryCodeSaved.setEvent(true)
|
||||
}
|
||||
|
||||
|
|
|
@ -25,6 +25,7 @@ fun getBackupFolderUri(context: Context): Uri? {
|
|||
* This is insecure and not supposed to be part of a release,
|
||||
* but rather an intermediate step towards a generated passphrase.
|
||||
*/
|
||||
@Deprecated("Replaced by KeyManager#storeBackupKey()")
|
||||
fun setBackupPassword(context: Context, password: String) {
|
||||
getDefaultSharedPreferences(context)
|
||||
.edit()
|
||||
|
@ -32,6 +33,7 @@ fun setBackupPassword(context: Context, password: String) {
|
|||
.apply()
|
||||
}
|
||||
|
||||
@Deprecated("Replaced by KeyManager#getBackupKey()")
|
||||
fun getBackupPassword(context: Context): String? {
|
||||
return getDefaultSharedPreferences(context).getString(PREF_KEY_BACKUP_PASSWORD, null)
|
||||
}
|
||||
|
|
|
@ -5,12 +5,13 @@ import android.content.Intent
|
|||
import android.content.Intent.FLAG_GRANT_READ_URI_PERMISSION
|
||||
import android.content.Intent.FLAG_GRANT_WRITE_URI_PERMISSION
|
||||
import androidx.lifecycle.AndroidViewModel
|
||||
import com.stevesoltys.backup.security.KeyManager
|
||||
|
||||
class SettingsViewModel(application: Application) : AndroidViewModel(application) {
|
||||
|
||||
private val app = application
|
||||
|
||||
fun recoveryCodeIsSet() = getBackupPassword(getApplication()) != null
|
||||
fun recoveryCodeIsSet() = KeyManager.hasBackupKey()
|
||||
fun locationIsSet() = getBackupFolderUri(getApplication()) != null
|
||||
|
||||
fun handleChooseFolderResult(result: Intent?) {
|
||||
|
|
Loading…
Reference in a new issue