The Android Keystore generates and holds keys for you. You get a handle, never the key bytes: the cryptography runs in a system process and, on most phones, in a Trusted Execution Environment (TEE), a separate secure OS on the main processor. Android 9 added StrongBox, a dedicated security chip (setIsStrongBoxBacked(true)). A key's allowed uses are fixed at creation and enforced there. BookNest creates one AES-256 key for its token:
object KeystoreCipher {
private const val ALIAS = "booknest_token_key"
private const val TRANSFORMATION = "AES/GCM/NoPadding"
private val keyStore = KeyStore.getInstance("AndroidKeyStore").apply { load(null) }
... // key() returns the stored key, or create()s it once
private fun create(): SecretKey {
val spec = KeyGenParameterSpec.Builder(ALIAS,
KeyProperties.PURPOSE_ENCRYPT or KeyProperties.PURPOSE_DECRYPT)
.setBlockModes(KeyProperties.BLOCK_MODE_GCM)
.setEncryptionPaddings(KeyProperties.ENCRYPTION_PADDING_NONE)
.setKeySize(256)
.build()
return KeyGenerator.getInstance(KeyProperties.KEY_ALGORITHM_AES, "AndroidKeyStore")
.apply { init(spec) }
.generateKey()
}
fun encrypt(plain: ByteArray): ByteArray {
val cipher = Cipher.getInstance(TRANSFORMATION).apply { init(Cipher.ENCRYPT_MODE, key()) }
return cipher.iv + cipher.doFinal(plain) // the Keystore picks a fresh IV
}
... // decrypt() reads the IV back with GCMParameterSpec(128, blob, 0, 12)
}The result is the 12-byte IV, the ciphertext and a 16-byte GCM tag, which makes decryption throw AEADBadTagException if a byte was altered. KeyInfo.securityLevel (API 31) reports where the key lives:
D/BookNestSec( 4257): booknest_token_key: AES-256, software, key.encoded=null
The emulator's KeyMint runs in software; most phones report TEE or StrongBox, which a server can verify through key attestation. key.encoded is null everywhere, and keys die with the app and are never backed up, so treat what they encrypt as a cache you can rebuild by signing in again.