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
|
<activity
|
||||||
android:name="com.stevesoltys.backup.activity.MainActivity"
|
android:name="com.stevesoltys.backup.activity.MainActivity"
|
||||||
android:label="@string/app_name">
|
android:label="@string/app_name"/>
|
||||||
<intent-filter>
|
|
||||||
<action android:name="android.intent.action.MAIN" />
|
|
||||||
<category android:name="android.intent.category.LAUNCHER" />
|
|
||||||
</intent-filter>
|
|
||||||
</activity>
|
|
||||||
|
|
||||||
<activity
|
<activity
|
||||||
android:name="com.stevesoltys.backup.activity.backup.CreateBackupActivity"
|
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
|
package com.stevesoltys.backup.settings
|
||||||
|
|
||||||
import android.app.Application
|
import android.app.Application
|
||||||
import android.util.ByteStringUtils
|
|
||||||
import androidx.lifecycle.AndroidViewModel
|
import androidx.lifecycle.AndroidViewModel
|
||||||
import com.stevesoltys.backup.LiveEvent
|
import com.stevesoltys.backup.LiveEvent
|
||||||
import com.stevesoltys.backup.MutableLiveEvent
|
import com.stevesoltys.backup.MutableLiveEvent
|
||||||
|
import com.stevesoltys.backup.security.KeyManager
|
||||||
import io.github.novacrypto.bip39.*
|
import io.github.novacrypto.bip39.*
|
||||||
import io.github.novacrypto.bip39.Validation.InvalidChecksumException
|
import io.github.novacrypto.bip39.Validation.InvalidChecksumException
|
||||||
import io.github.novacrypto.bip39.Validation.InvalidWordCountException
|
import io.github.novacrypto.bip39.Validation.InvalidWordCountException
|
||||||
|
@ -46,8 +46,7 @@ class RecoveryCodeViewModel(application: Application) : AndroidViewModel(applica
|
||||||
}
|
}
|
||||||
val mnemonic = input.joinToString(" ")
|
val mnemonic = input.joinToString(" ")
|
||||||
val seed = SeedCalculator(JavaxPBKDF2WithHmacSHA512.INSTANCE).calculateSeed(mnemonic, "")
|
val seed = SeedCalculator(JavaxPBKDF2WithHmacSHA512.INSTANCE).calculateSeed(mnemonic, "")
|
||||||
// TODO use KeyManager to store secret
|
KeyManager.storeBackupKey(seed)
|
||||||
setBackupPassword(getApplication(), ByteStringUtils.toHexString(seed))
|
|
||||||
recoveryCodeSaved.setEvent(true)
|
recoveryCodeSaved.setEvent(true)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -25,6 +25,7 @@ fun getBackupFolderUri(context: Context): Uri? {
|
||||||
* This is insecure and not supposed to be part of a release,
|
* This is insecure and not supposed to be part of a release,
|
||||||
* but rather an intermediate step towards a generated passphrase.
|
* but rather an intermediate step towards a generated passphrase.
|
||||||
*/
|
*/
|
||||||
|
@Deprecated("Replaced by KeyManager#storeBackupKey()")
|
||||||
fun setBackupPassword(context: Context, password: String) {
|
fun setBackupPassword(context: Context, password: String) {
|
||||||
getDefaultSharedPreferences(context)
|
getDefaultSharedPreferences(context)
|
||||||
.edit()
|
.edit()
|
||||||
|
@ -32,6 +33,7 @@ fun setBackupPassword(context: Context, password: String) {
|
||||||
.apply()
|
.apply()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Deprecated("Replaced by KeyManager#getBackupKey()")
|
||||||
fun getBackupPassword(context: Context): String? {
|
fun getBackupPassword(context: Context): String? {
|
||||||
return getDefaultSharedPreferences(context).getString(PREF_KEY_BACKUP_PASSWORD, null)
|
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_READ_URI_PERMISSION
|
||||||
import android.content.Intent.FLAG_GRANT_WRITE_URI_PERMISSION
|
import android.content.Intent.FLAG_GRANT_WRITE_URI_PERMISSION
|
||||||
import androidx.lifecycle.AndroidViewModel
|
import androidx.lifecycle.AndroidViewModel
|
||||||
|
import com.stevesoltys.backup.security.KeyManager
|
||||||
|
|
||||||
class SettingsViewModel(application: Application) : AndroidViewModel(application) {
|
class SettingsViewModel(application: Application) : AndroidViewModel(application) {
|
||||||
|
|
||||||
private val app = application
|
private val app = application
|
||||||
|
|
||||||
fun recoveryCodeIsSet() = getBackupPassword(getApplication()) != null
|
fun recoveryCodeIsSet() = KeyManager.hasBackupKey()
|
||||||
fun locationIsSet() = getBackupFolderUri(getApplication()) != null
|
fun locationIsSet() = getBackupFolderUri(getApplication()) != null
|
||||||
|
|
||||||
fun handleChooseFolderResult(result: Intent?) {
|
fun handleChooseFolderResult(result: Intent?) {
|
||||||
|
|
Loading…
Reference in a new issue