1package com.peachpasswords.ui.lock23import android.content.Context4import android.os.Build5import android.security.keystore.KeyGenParameterSpec6import android.security.keystore.KeyInfo7import android.security.keystore.KeyPermanentlyInvalidatedException8import android.security.keystore.KeyProperties9import android.security.keystore.StrongBoxUnavailableException10import java.security.KeyStore11import java.security.GeneralSecurityException12import java.util.Base6413import javax.crypto.Cipher14import javax.crypto.KeyGenerator15import javax.crypto.SecretKey16import javax.crypto.SecretKeyFactory17import javax.crypto.spec.GCMParameterSpec18192021222324252627282930313233object BiometricKeyStore {34 private const val KEY_ALIAS = "peach_bio_vault_key"35 private const val KEYSTORE = "AndroidKeyStore"36 private const val TRANSFORM = "AES/GCM/NoPadding"37 private const val PREFS = "peach_bio_prefs"38 private const val K_WRAPPED = "wrapped"39 private const val K_IV = "iv"40 private const val GCM_TAG_BITS = 1284142 private fun prefs(context: Context) =43 context.getSharedPreferences(PREFS, Context.MODE_PRIVATE)4445 fun hasWrappedKey(context: Context): Boolean =46 prefs(context).contains(K_WRAPPED) && prefs(context).contains(K_IV) &&47 (runCatching { existingKey()?.let(::isHardwareBacked) == true }.getOrDefault(false))4849 fun clear(context: Context) {50 prefs(context).edit().remove(K_WRAPPED).remove(K_IV).apply()51 runCatching {52 KeyStore.getInstance(KEYSTORE).apply { load(null) }.deleteEntry(KEY_ALIAS)53 }54 }5556 57 fun encryptCipher(): Cipher {58 val key = getOrCreateKey()59 return Cipher.getInstance(TRANSFORM).apply { init(Cipher.ENCRYPT_MODE, key) }60 }6162 6364656667 fun decryptCipher(context: Context): Cipher? {68 val iv = loadIv(context) ?: return null69 return try {70 val key = existingKey()?.takeIf(::isHardwareBacked) ?: return null71 Cipher.getInstance(TRANSFORM).apply { init(Cipher.DECRYPT_MODE, key, GCMParameterSpec(GCM_TAG_BITS, iv)) }72 } catch (_: KeyPermanentlyInvalidatedException) {73 clear(context)74 null75 } catch (_: Exception) {76 null77 }78 }7980 81 fun wrapAndStore(context: Context, cipher: Cipher, vaultKey: ByteArray) {82 val wrapped = cipher.doFinal(vaultKey)83 prefs(context).edit()84 .putString(K_WRAPPED, b64(wrapped))85 .putString(K_IV, b64(cipher.iv))86 .apply()87 }8889 90 fun unwrap(context: Context, cipher: Cipher): ByteArray? {91 val wrapped = prefs(context).getString(K_WRAPPED, null)?.let(::unb64) ?: return null92 return runCatching { cipher.doFinal(wrapped) }.getOrNull()93 }9495 private fun loadIv(context: Context): ByteArray? =96 prefs(context).getString(K_IV, null)?.let(::unb64)9798 @Suppress("NewApi")99 private fun getOrCreateKey(): SecretKey {100 existingKey()?.let { key ->101 if (isHardwareBacked(key)) return key102 deleteKey()103 }104 val builder = KeyGenParameterSpec.Builder(105 KEY_ALIAS,106 KeyProperties.PURPOSE_ENCRYPT or KeyProperties.PURPOSE_DECRYPT,107 )108 .setBlockModes(KeyProperties.BLOCK_MODE_GCM)109 .setEncryptionPaddings(KeyProperties.ENCRYPTION_PADDING_NONE)110 .setKeySize(256)111 .setUserAuthenticationRequired(true)112 .setInvalidatedByBiometricEnrollment(true)113 if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {114 builder.setUserAuthenticationParameters(0, KeyProperties.AUTH_BIOMETRIC_STRONG)115 }116 val key = try {117 if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) {118 builder.setIsStrongBoxBacked(true)119 }120 generateKey(builder)121 } catch (_: StrongBoxUnavailableException) {122 generateKey(builder, strongBox = false)123 }124 if (!isHardwareBacked(key)) {125 deleteKey()126 throw GeneralSecurityException("biometric_key_not_hardware_backed")127 }128 return key129 }130131 private fun generateKey(builder: KeyGenParameterSpec.Builder, strongBox: Boolean = true): SecretKey {132 if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P && !strongBox) {133 builder.setIsStrongBoxBacked(false)134 }135 val gen = KeyGenerator.getInstance(KeyProperties.KEY_ALGORITHM_AES, KEYSTORE)136 gen.init(builder.build())137 return gen.generateKey()138 }139140 private fun existingKey(): SecretKey? {141 val ks = KeyStore.getInstance(KEYSTORE).apply { load(null) }142 return (ks.getEntry(KEY_ALIAS, null) as? KeyStore.SecretKeyEntry)?.secretKey143 }144145 private fun isHardwareBacked(key: SecretKey): Boolean = runCatching {146 val info = SecretKeyFactory.getInstance(key.algorithm, KEYSTORE)147 .getKeySpec(key, KeyInfo::class.java) as KeyInfo148 if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {149 info.securityLevel == KeyProperties.SECURITY_LEVEL_TRUSTED_ENVIRONMENT ||150 info.securityLevel == KeyProperties.SECURITY_LEVEL_STRONGBOX151 } else {152 @Suppress("DEPRECATION")153 info.isInsideSecureHardware154 }155 }.getOrDefault(false)156157 private fun deleteKey() {158 runCatching { KeyStore.getInstance(KEYSTORE).apply { load(null) }.deleteEntry(KEY_ALIAS) }159 }160161 private fun b64(b: ByteArray) = Base64.getEncoder().encodeToString(b)162 private fun unb64(s: String) = Base64.getDecoder().decode(s)163}