* to get rid of global state * to have a testable architecture * to allow for authenticated encryption * to have a backup format version * to potentially allow for other storage plugins
72 lines
2.5 KiB
Kotlin
72 lines
2.5 KiB
Kotlin
package com.stevesoltys.backup.crypto
|
|
|
|
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
|
|
|
|
internal const val KEY_SIZE = 256
|
|
private const val KEY_SIZE_BYTES = KEY_SIZE / 8
|
|
private const val KEY_ALIAS = "com.stevesoltys.backup"
|
|
private const val ANDROID_KEY_STORE = "AndroidKeyStore"
|
|
|
|
interface KeyManager {
|
|
/**
|
|
* Store a new backup key derived from the given [seed].
|
|
*
|
|
* The seed needs to be larger or equal to [KEY_SIZE_BYTES].
|
|
*/
|
|
fun storeBackupKey(seed: ByteArray)
|
|
|
|
/**
|
|
* @return true if a backup key already exists in the [KeyStore].
|
|
*/
|
|
fun hasBackupKey(): Boolean
|
|
|
|
/**
|
|
* Returns the backup key, so it can be used for encryption or decryption.
|
|
*
|
|
* Note that any attempt to export the key will return null or an empty [ByteArray],
|
|
* because the key can not leave the [KeyStore]'s hardware security module.
|
|
*/
|
|
fun getBackupKey(): SecretKey
|
|
}
|
|
|
|
class KeyManagerImpl : KeyManager {
|
|
|
|
private val keyStore by lazy {
|
|
KeyStore.getInstance(ANDROID_KEY_STORE).apply {
|
|
load(null)
|
|
}
|
|
}
|
|
|
|
override fun storeBackupKey(seed: ByteArray) {
|
|
if (seed.size < KEY_SIZE_BYTES) throw IllegalArgumentException()
|
|
// TODO check if using first 256 of 512 bytes produced by PBKDF2WithHmacSHA512 is safe!
|
|
val secretKeySpec = SecretKeySpec(seed, 0, KEY_SIZE_BYTES, "AES")
|
|
val ksEntry = SecretKeyEntry(secretKeySpec)
|
|
keyStore.setEntry(KEY_ALIAS, ksEntry, getKeyProtection())
|
|
}
|
|
|
|
override fun hasBackupKey() = keyStore.containsAlias(KEY_ALIAS) &&
|
|
keyStore.entryInstanceOf(KEY_ALIAS, SecretKeyEntry::class.java)
|
|
|
|
override 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)
|
|
// unlocking is required only for decryption, so when restoring from backup
|
|
if (SDK_INT >= 28) builder.setUnlockedDeviceRequired(true)
|
|
return builder.build()
|
|
}
|
|
|
|
}
|