android: StrongBox for new keystore keys, show key storage level (#7637)

* android: create new keystore keys in StrongBox

* android: show keystore key storage level

* ui: save keystore key before marking it stored

* android: show keystore storage level in the footer text instead of a separate row

* android: return keystore storage label, drop enum

---------

Co-authored-by: Narasimha-sc <166327228+Narasimha-sc@users.noreply.github.com>
This commit is contained in:
sh
2026-10-04 10:47:46 +01:00
committed by GitHub
co-authored by Narasimha-sc
parent 1a969a6cb9
commit be05a46628
42 changed files with 92 additions and 44 deletions
@@ -1,8 +1,11 @@
package chat.simplex.common.platform
import android.annotation.SuppressLint
import android.os.Build
import android.security.keystore.KeyGenParameterSpec
import android.security.keystore.KeyInfo
import android.security.keystore.KeyProperties
import android.security.keystore.StrongBoxUnavailableException
import chat.simplex.common.views.helpers.AlertManager
import chat.simplex.common.views.helpers.generalGetString
import chat.simplex.res.MR
@@ -68,15 +71,44 @@ internal class Cryptor: CryptorInterface {
keyStore.deleteEntry(alias)
}
override fun keyStorage(alias: String): String? {
val secretKey = getSecretKey(alias) ?: return null
val keyInfo = SecretKeyFactory.getInstance(secretKey.algorithm, "AndroidKeyStore").getKeySpec(secretKey, KeyInfo::class.java) as KeyInfo
val storage = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
when (keyInfo.securityLevel) {
KeyProperties.SECURITY_LEVEL_STRONGBOX -> MR.strings.keystore_key_storage_strongbox
KeyProperties.SECURITY_LEVEL_TRUSTED_ENVIRONMENT -> MR.strings.keystore_key_storage_tee
KeyProperties.SECURITY_LEVEL_SOFTWARE -> MR.strings.keystore_key_storage_software
else -> return null
}
} else {
// isInsideSecureHardware does not distinguish StrongBox, which createSecretKey only requests on API 31+
@Suppress("DEPRECATION")
if (keyInfo.isInsideSecureHardware) MR.strings.keystore_key_storage_tee else MR.strings.keystore_key_storage_software
}
return generalGetString(storage)
}
private fun createSecretKey(alias: String): SecretKey? {
if (keyStore.containsAlias(alias)) return getSecretKey(alias)
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
try {
return generateKey(keySpec(alias).setIsStrongBoxBacked(true))
} catch (e: StrongBoxUnavailableException) {
Log.i(TAG, "StrongBox is unavailable, using default keystore: ${e.message}")
}
}
return generateKey(keySpec(alias))
}
private fun keySpec(alias: String): KeyGenParameterSpec.Builder =
KeyGenParameterSpec.Builder(alias, KeyProperties.PURPOSE_ENCRYPT or KeyProperties.PURPOSE_DECRYPT)
.setBlockModes(BLOCK_MODE)
.setEncryptionPaddings(KeyProperties.ENCRYPTION_PADDING_NONE)
private fun generateKey(spec: KeyGenParameterSpec.Builder): SecretKey {
val keyGenerator: KeyGenerator = KeyGenerator.getInstance(KEY_ALGORITHM, "AndroidKeyStore")
keyGenerator.init(
KeyGenParameterSpec.Builder(alias, KeyProperties.PURPOSE_ENCRYPT or KeyProperties.PURPOSE_DECRYPT)
.setBlockModes(BLOCK_MODE)
.setEncryptionPaddings(KeyProperties.ENCRYPTION_PADDING_NONE)
.build()
)
keyGenerator.init(spec.build())
return keyGenerator.generateKey()
}
@@ -59,6 +59,7 @@ actual fun DatabaseEncryptionFooter(
useKeychain: MutableState<Boolean>,
chatDbEncrypted: Boolean?,
storedKey: MutableState<Boolean>,
keyStorage: String?,
initialRandomDBPassphrase: MutableState<Boolean>,
migration: Boolean,
) {
@@ -66,7 +67,7 @@ actual fun DatabaseEncryptionFooter(
SectionTextFooter(generalGetString(MR.strings.database_is_not_encrypted))
} else if (useKeychain.value) {
if (storedKey.value) {
SectionTextFooter(generalGetString(MR.strings.keychain_is_storing_securely))
SectionTextFooter(String.format(generalGetString(MR.strings.keychain_is_storing_securely), keyStorage?.let { " ($it)" } ?: ""))
if (initialRandomDBPassphrase.value && !migration) {
SectionTextFooter(generalGetString(MR.strings.encrypted_with_random_passphrase))
} else {