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:
Torsten Grote 2019-07-08 12:32:47 +02:00
parent 66c0919eb5
commit 3e64c3686f
No known key found for this signature in database
GPG key ID: 3E5F77D92CF891FF
5 changed files with 55 additions and 10 deletions

View file

@ -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"

View file

@ -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()
}
}

View file

@ -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)
} }

View file

@ -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)
} }

View file

@ -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?) {