When storing sensitive information in Android apps — such as authentication tokens, API keys, or personally identifiable information (PII) — it’s not enough to “just encrypt it.”
Where and how encryption keys are stored is as important as encrypting the data itself. If your encryption keys can be extracted from the device, your encryption is essentially useless.
That’s where hardware-backed encryption in Android comes into play.
2. What is Hardware-Backed Encryption?
Android devices often include a Trusted Execution Environment (TEE) or Secure Element (SE) — an isolated, secure chip separate from the main CPU.
When you generate encryption keys with the Android Keystore and request them to be hardware-backed, the keys:
-
Are generated inside secure hardware
-
Never leave the hardware in plaintext form
-
Are used directly for cryptographic operations inside the TEE/SE
If an attacker gains root access or dumps the device’s memory, the encryption key is still safe because it physically cannot be extracted.
3. Why It’s Important
Without hardware-backed encryption:
-
Keys are stored in software, protected only by file system permissions
-
A rooted device or sophisticated malware can steal them
With hardware-backed encryption:
-
Keys are tied to the device hardware
-
Even if your app's data is exfiltrated, the attacker cannot decrypt it without the physical device and access credentials
-
Optionally, you can require user authentication (PIN, password, or biometric) before the key can be used
Real-World Scenarios
-
Banking apps protecting stored session tokens
-
Healthcare apps storing patient records offline
-
Messaging apps protecting encryption keys for end-to-end chats
-
IoT control apps where device commands must be authenticated
4. How to Use Hardware-Backed Keystore in Android
Here’s how to generate and use a hardware-backed AES key in Android:
import android.security.keystore.KeyGenParameterSpec
import android.security.keystore.KeyProperties
import javax.crypto.Cipher
import javax.crypto.KeyGenerator
import javax.crypto.SecretKey
import javax.crypto.spec.GCMParameterSpec
import android.util.Base64
private const val KEY_ALIAS = "my_hardware_backed_key"
private const val TRANSFORMATION = "AES/GCM/NoPadding"
fun getOrCreateHardwareKey(): SecretKey {
val keyStore = java.security.KeyStore.getInstance("AndroidKeyStore").apply { load(null) }
val existingKey = keyStore.getKey(KEY_ALIAS, null) as? SecretKey
if (existingKey != null) return existingKey
val keyGenerator = KeyGenerator.getInstance(KeyProperties.KEY_ALGORITHM_AES, "AndroidKeyStore")
val spec = KeyGenParameterSpec.Builder(
KEY_ALIAS,
KeyProperties.PURPOSE_ENCRYPT or KeyProperties.PURPOSE_DECRYPT
)
.setBlockModes(KeyProperties.BLOCK_MODE_GCM)
.setEncryptionPaddings(KeyProperties.ENCRYPTION_PADDING_NONE)
.setKeySize(256)
.setIsStrongBoxBacked(true) // Use StrongBox if available
.build()
keyGenerator.init(spec)
return keyGenerator.generateKey()
}
fun encryptData(secretKey: SecretKey, plainText: String): String {
val cipher = Cipher.getInstance(TRANSFORMATION)
cipher.init(Cipher.ENCRYPT_MODE, secretKey)
val iv = cipher.iv
val encryptedBytes = cipher.doFinal(plainText.toByteArray(Charsets.UTF_8))
val combined = iv + encryptedBytes
return Base64.encodeToString(combined, Base64.NO_WRAP)
}
fun decryptData(secretKey: SecretKey, encryptedBase64: String): String {
val decoded = Base64.decode(encryptedBase64, Base64.NO_WRAP)
val iv = decoded.copyOfRange(0, 12)
val cipherData = decoded.copyOfRange(12, decoded.size)
val cipher = Cipher.getInstance(TRANSFORMATION)
cipher.init(Cipher.DECRYPT_MODE, secretKey, GCMParameterSpec(128, iv))
return String(cipher.doFinal(cipherData), Charsets.UTF_8)
}
5. How This Works
-
Key generation:
-
setIsStrongBoxBacked(true)
attempts to store the key in the StrongBox secure hardware if the device supports it (Pixel devices, some Samsung models). -
If StrongBox isn’t available, it falls back to TEE-backed storage.
-
-
Key usage:
-
The AES key never leaves secure hardware.
-
Encryption/decryption operations happen inside the hardware security module.
-
-
IV handling:
-
We prepend the IV to the ciphertext so it’s available during decryption.
-
The IV is not secret, but must be unique for each encryption.
-
6. Checking Hardware Support
You can verify if the key is hardware-backed:
val keyStore = java.security.KeyStore.getInstance("AndroidKeyStore").apply { load(null) }
val key = keyStore.getKey(KEY_ALIAS, null)
val cert = keyStore.getCertificate(KEY_ALIAS)
val isHardwareBacked = cert.publicKey?.format == "X.509" // Basic check
println("Hardware-backed: $isHardwareBacked")
For detailed attestation, use KeyInfo
from KeyFactory.getKeySpec(...)
to check isInsideSecureHardware
.
7. Best Practices
-
Always use hardware-backed keys for sensitive data if the device supports it.
-
Fail gracefully — if hardware-backed storage is unavailable, fall back to software-keystore encryption but with user notification or reduced functionality.
-
Use
setUserAuthenticationRequired(true)
for extra protection so that the user must authenticate before the key can be used. -
Rotate keys periodically and securely delete old keys.
-
Never log plaintext or keys.
8. My thoughts
Using Android’s hardware-backed keystore isn’t just a “nice to have” — it’s a necessity for any app that deals with sensitive user data.
By ensuring your keys never leave secure hardware, you protect against a whole class of attacks that target extracted or leaked keys. For banking, fintech, healthcare, and enterprise apps, this can be the difference between a minor breach and a catastrophic data leak.
💡 Next step: In a future article, I’ll show how to combine hardware-backed keys with Jetpack Encrypted DataStore so your stored data remains encrypted even if your app’s data directory is compromised.
📢 Feedback: Did you find this article helpful? Let me know your thoughts or suggestions for improvements! Please leave a comment below. I’d love to hear from you! 👇
Happy coding! 💻
0 comments:
Post a Comment