Your encryption is only as strong as where you keep the key
The E2E encryption post in this series built a complete encryption pipeline: key generation, key exchange, symmetric encryption with libsodium. The FFI series showed how to integrate a real cryptography library. Both posts ended with the same caveat: the key storage problem is not solved by the encryption itself.
An AES-256 key sitting in SharedPreferences is like a vault door with the key taped to the frame. The encryption is sound. The key management is catastrophic. Anyone who reads the app's data directory — via a rooted device, an ADB backup, a forensic extraction tool — gets the key and can decrypt everything.
Hardware-backed key storage solves this by moving the key off the main processor entirely. The key lives inside a physically separate chip — one that's hardened against extraction, that performs cryptographic operations internally, and that never exposes the raw key material to the app or the operating system.
This post covers how that hardware works on Android and iOS, what Flutter developers can access, and how to build key management that survives device compromise.
The hardware: what's actually in your phone
ARM TrustZone (Android)
Every modern ARM processor — which means every Android phone — has TrustZone, a hardware security feature built into the CPU itself. TrustZone divides the processor into two worlds:
┌─────────────────────────────────────────────────┐
│ ARM Processor │
│ │
│ Normal World Secure World │
│ ┌──────────────┐ ┌──────────────┐ │
│ │ Android OS │ │ Trusted OS │ │
│ │ Your app │ │ (Trusty, │ │
│ │ Other apps │ │ QSEE, etc.) │ │
│ │ Linux kernel │ │ │ │
│ └──────────────┘ │ Keymaster │ │
│ │ Gatekeeper │ │
│ │ Fingerprint │ │
│ └──────────────┘ │
│ │
│ Normal World CANNOT │
│ read Secure World memory │
└─────────────────────────────────────────────────┘The two worlds share the same physical CPU but have hardware-enforced memory isolation. Code running in the Normal World (Android, your app, even the Linux kernel) cannot access memory belonging to the Secure World. A vulnerability in Android — even a kernel exploit — cannot read TrustZone memory.
The Keymaster module runs inside TrustZone. When your app asks the Android Keystore to generate a key, Keymaster generates it inside TrustZone. The key never exists in Normal World memory. When your app asks to encrypt data with that key, the plaintext goes into TrustZone, the encryption happens there, and only the ciphertext comes back.
StrongBox (Android, newer devices)
Some Android devices go further with StrongBox — a separate, physically distinct security chip (like a smart card soldered to the motherboard). StrongBox provides the same API as the Keystore but with stronger guarantees:
- Separate processor (not shared with the application CPU)
- Its own secure storage
- Tamper-resistant packaging
- Immune to side-channel attacks on the main CPU
Google's Pixel phones use the Titan M chip as their StrongBox implementation. Samsung uses Knox Vault. Not all Android devices have StrongBox, but the Keystore API lets you query for it and fall back gracefully.
Secure Enclave (iOS)
Apple's Secure Enclave is conceptually similar to StrongBox — a dedicated security coprocessor, physically separate from the application processor:
┌──────────────────────┐ ┌──────────────────────┐
│ Application │ │ Secure Enclave │
│ Processor (A-series)│ │ Processor │
│ │ │ │
│ iOS │ │ Encrypted memory │
│ Your Flutter app │◄───►│ Key storage │
│ All other apps │ │ Crypto operations │
│ │ │ Biometric matching │
│ │ │ │
│ Shared memory: NO │ │ UID key (fused at │
│ │ │ manufacture, never │
│ │ │ readable) │
└──────────────────────┘ └──────────────────────┘The Secure Enclave has its own boot ROM, its own AES engine, and a unique ID (UID) key that's fused into the silicon during manufacturing. This UID key is never readable — not by iOS, not by Apple, not by anyone. It's used to derive encryption keys that are unique to that specific physical device.
When your app creates a key in the Keychain with Secure Enclave protection, the key is generated inside the Secure Enclave and wrapped with the UID-derived key. Even if an attacker dumps the entire flash storage of the device, the wrapped key is useless without the UID key — which can only be accessed by the Secure Enclave hardware.
Android Keystore: practical implementation
The Android Keystore is the developer API for hardware-backed key management. From Flutter, you access it via platform channels (Kotlin/Java on the Android side).
Generating a hardware-backed key
// android/app/src/main/kotlin/com/yourapp/KeystoreService.kt
import android.security.keystore.KeyGenParameterSpec
import android.security.keystore.KeyProperties
import java.security.KeyStore
import javax.crypto.KeyGenerator
class KeystoreService {
companion object {
private const val KEYSTORE_PROVIDER = "AndroidKeyStore"
}
/// Generate an AES-256 key backed by hardware.
/// The key material NEVER leaves the Keystore / TrustZone.
fun generateKey(alias: String, requireBiometric: Boolean): Boolean {
val keyGenerator = KeyGenerator.getInstance(
KeyProperties.KEY_ALGORITHM_AES,
KEYSTORE_PROVIDER
)
val builder = KeyGenParameterSpec.Builder(
alias,
KeyProperties.PURPOSE_ENCRYPT or KeyProperties.PURPOSE_DECRYPT
)
.setBlockModes(KeyProperties.BLOCK_MODE_GCM)
.setEncryptionPaddings(KeyProperties.ENCRYPTION_PADDING_NONE)
.setKeySize(256)
// Key is bound to this device — cannot be exported
.setRandomizedEncryptionRequired(true)
if (requireBiometric) {
builder
// Key can only be used after biometric authentication
.setUserAuthenticationRequired(true)
// Auth is valid for 0 seconds — require biometric for EVERY use
.setUserAuthenticationParameters(
0, KeyProperties.AUTH_BIOMETRIC_STRONG
)
// Invalidate the key if biometrics change
// (e.g., new fingerprint enrolled)
.setInvalidatedByBiometricEnrollment(true)
}
// Request StrongBox if available — falls back to TEE if not
if (android.os.Build.VERSION.SDK_INT >= 28) {
builder.setIsStrongBoxBacked(true)
}
try {
keyGenerator.init(builder.build())
keyGenerator.generateKey()
return true
} catch (e: Exception) {
// StrongBox not available — retry without it
if (e.message?.contains("StrongBox") == true) {
builder.setIsStrongBoxBacked(false)
keyGenerator.init(builder.build())
keyGenerator.generateKey()
return true
}
throw e
}
}
/// Encrypt data using a hardware-backed key.
/// The key never leaves the secure hardware — the encryption
/// happens inside TrustZone/StrongBox.
fun encrypt(alias: String, plaintext: ByteArray): EncryptedData {
val keyStore = KeyStore.getInstance(KEYSTORE_PROVIDER)
keyStore.load(null)
val key = keyStore.getKey(alias, null)
?: throw IllegalStateException("Key '$alias' not found in Keystore")
val cipher = javax.crypto.Cipher.getInstance("AES/GCM/NoPadding")
cipher.init(javax.crypto.Cipher.ENCRYPT_MODE, key)
val ciphertext = cipher.doFinal(plaintext)
val iv = cipher.iv // GCM generates a random IV
return EncryptedData(ciphertext = ciphertext, iv = iv)
}
/// Decrypt data using a hardware-backed key.
fun decrypt(alias: String, ciphertext: ByteArray, iv: ByteArray): ByteArray {
val keyStore = KeyStore.getInstance(KEYSTORE_PROVIDER)
keyStore.load(null)
val key = keyStore.getKey(alias, null)
?: throw IllegalStateException("Key '$alias' not found in Keystore")
val cipher = javax.crypto.Cipher.getInstance("AES/GCM/NoPadding")
val spec = javax.crypto.spec.GCMParameterSpec(128, iv)
cipher.init(javax.crypto.Cipher.DECRYPT_MODE, key, spec)
return cipher.doFinal(ciphertext)
}
/// Check if the device supports StrongBox
fun hasStrongBox(): Boolean {
return if (android.os.Build.VERSION.SDK_INT >= 28) {
try {
val pm = android.app.Application().packageManager
pm.hasSystemFeature(android.content.pm.PackageManager.FEATURE_STRONGBOX_KEYSTORE)
} catch (e: Exception) {
false
}
} else {
false
}
}
}
data class EncryptedData(
val ciphertext: ByteArray,
val iv: ByteArray
)The Flutter platform channel bridge
// android/app/src/main/kotlin/com/yourapp/SecureKeysPlugin.kt
import io.flutter.embedding.engine.plugins.FlutterPlugin
import io.flutter.plugin.common.MethodCall
import io.flutter.plugin.common.MethodChannel
class SecureKeysPlugin : FlutterPlugin, MethodChannel.MethodCallHandler {
private lateinit var channel: MethodChannel
private val keystoreService = KeystoreService()
override fun onAttachedToEngine(binding: FlutterPlugin.FlutterPluginBinding) {
channel = MethodChannel(binding.binaryMessenger, "com.yourapp/secure_keys")
channel.setMethodCallHandler(this)
}
override fun onMethodCall(call: MethodCall, result: MethodChannel.Result) {
when (call.method) {
"generateKey" -> {
val alias = call.argument<String>("alias")!!
val requireBiometric = call.argument<Boolean>("requireBiometric") ?: false
try {
keystoreService.generateKey(alias, requireBiometric)
result.success(true)
} catch (e: Exception) {
result.error("KEY_GEN_FAILED", e.message, null)
}
}
"encrypt" -> {
val alias = call.argument<String>("alias")!!
val plaintext = call.argument<ByteArray>("plaintext")!!
try {
val encrypted = keystoreService.encrypt(alias, plaintext)
result.success(mapOf(
"ciphertext" to encrypted.ciphertext,
"iv" to encrypted.iv,
))
} catch (e: Exception) {
result.error("ENCRYPT_FAILED", e.message, null)
}
}
"decrypt" -> {
val alias = call.argument<String>("alias")!!
val ciphertext = call.argument<ByteArray>("ciphertext")!!
val iv = call.argument<ByteArray>("iv")!!
try {
val plaintext = keystoreService.decrypt(alias, ciphertext, iv)
result.success(plaintext)
} catch (e: Exception) {
result.error("DECRYPT_FAILED", e.message, null)
}
}
else -> result.notImplemented()
}
}
override fun onDetachedFromEngine(binding: FlutterPlugin.FlutterPluginBinding) {
channel.setMethodCallHandler(null)
}
}The Dart API
// lib/services/secure_keys.dart
class SecureKeys {
static const _channel = MethodChannel('com.yourapp/secure_keys');
/// Generate a hardware-backed encryption key.
/// The key material never leaves the secure hardware.
static Future<void> generateKey(
String alias, {
bool requireBiometric = false,
}) async {
await _channel.invokeMethod('generateKey', {
'alias': alias,
'requireBiometric': requireBiometric,
});
}
/// Encrypt data using a hardware-backed key.
/// Returns the ciphertext and IV needed for decryption.
static Future<EncryptedPayload> encrypt(
String alias,
Uint8List plaintext,
) async {
final result = await _channel.invokeMapMethod<String, dynamic>(
'encrypt',
{'alias': alias, 'plaintext': plaintext},
);
return EncryptedPayload(
ciphertext: Uint8List.fromList(result!['ciphertext']),
iv: Uint8List.fromList(result['iv']),
);
}
/// Decrypt data using a hardware-backed key.
/// If the key requires biometric auth, the system prompt appears automatically.
static Future<Uint8List> decrypt(
String alias,
Uint8List ciphertext,
Uint8List iv,
) async {
final result = await _channel.invokeMethod<Uint8List>(
'decrypt',
{'alias': alias, 'ciphertext': ciphertext, 'iv': iv},
);
return result!;
}
}
class EncryptedPayload {
final Uint8List ciphertext;
final Uint8List iv;
EncryptedPayload({required this.ciphertext, required this.iv});
}iOS Keychain with Secure Enclave protection
On iOS, the equivalent is the Keychain with kSecAttrAccessibleWhenPasscodeSetThisDeviceOnly protection, optionally backed by the Secure Enclave for key generation.
// ios/Runner/SecureKeysPlugin.swift
import Flutter
import Security
import LocalAuthentication
class SecureKeysPlugin: NSObject, FlutterPlugin {
static func register(with registrar: FlutterPluginRegistrar) {
let channel = FlutterMethodChannel(
name: "com.yourapp/secure_keys",
binaryMessenger: registrar.messenger()
)
let instance = SecureKeysPlugin()
registrar.addMethodCallDelegate(instance, channel: channel)
}
func handle(_ call: FlutterMethodCall, result: @escaping FlutterResult) {
switch call.method {
case "generateKey":
guard let args = call.arguments as? [String: Any],
let alias = args["alias"] as? String else {
result(FlutterError(code: "INVALID_ARGS", message: nil, details: nil))
return
}
let requireBiometric = args["requireBiometric"] as? Bool ?? false
generateKey(alias: alias, requireBiometric: requireBiometric, result: result)
case "encrypt":
guard let args = call.arguments as? [String: Any],
let alias = args["alias"] as? String,
let plaintext = args["plaintext"] as? FlutterStandardTypedData else {
result(FlutterError(code: "INVALID_ARGS", message: nil, details: nil))
return
}
encrypt(alias: alias, plaintext: plaintext.data, result: result)
case "decrypt":
guard let args = call.arguments as? [String: Any],
let alias = args["alias"] as? String,
let ciphertext = args["ciphertext"] as? FlutterStandardTypedData,
let iv = args["iv"] as? FlutterStandardTypedData else {
result(FlutterError(code: "INVALID_ARGS", message: nil, details: nil))
return
}
decrypt(alias: alias, ciphertext: ciphertext.data, iv: iv.data, result: result)
default:
result(FlutterMethodNotImplemented)
}
}
private func generateKey(alias: String, requireBiometric: Bool, result: @escaping FlutterResult) {
// Generate a key inside the Secure Enclave
var accessControl: SecAccessControl?
if requireBiometric {
accessControl = SecAccessControlCreateWithFlags(
kCFAllocatorDefault,
kSecAttrAccessibleWhenPasscodeSetThisDeviceOnly,
[.privateKeyUsage, .biometryCurrentSet],
nil
)
} else {
accessControl = SecAccessControlCreateWithFlags(
kCFAllocatorDefault,
kSecAttrAccessibleWhenPasscodeSetThisDeviceOnly,
.privateKeyUsage,
nil
)
}
let attributes: [String: Any] = [
kSecAttrKeyType as String: kSecAttrKeyTypeECSECPrimeRandom,
kSecAttrKeySizeInBits as String: 256,
kSecAttrTokenID as String: kSecAttrTokenIDSecureEnclave,
kSecPrivateKeyAttrs as String: [
kSecAttrIsPermanent as String: true,
kSecAttrApplicationTag as String: alias.data(using: .utf8)!,
kSecAttrAccessControl as String: accessControl as Any,
],
]
var error: Unmanaged<CFError>?
guard SecKeyCreateRandomKey(attributes as CFDictionary, &error) != nil else {
result(FlutterError(
code: "KEY_GEN_FAILED",
message: error?.takeRetainedValue().localizedDescription,
details: nil
))
return
}
result(true)
}
// iOS Secure Enclave supports asymmetric operations (ECDH, ECDSA)
// For symmetric encryption, we generate an EC key pair in the Secure Enclave
// and use ECDH to derive a symmetric key, or we use the Keychain directly
// for AES key storage with Secure Enclave access control
private func encrypt(alias: String, plaintext: Data, result: @escaping FlutterResult) {
guard let key = getKey(alias: alias) else {
result(FlutterError(code: "KEY_NOT_FOUND", message: nil, details: nil))
return
}
var error: Unmanaged<CFError>?
guard let ciphertext = SecKeyCreateEncryptedData(
key,
.eciesEncryptionCofactorVariableIVX963SHA256AESGCM,
plaintext as CFData,
&error
) else {
result(FlutterError(
code: "ENCRYPT_FAILED",
message: error?.takeRetainedValue().localizedDescription,
details: nil
))
return
}
// For ECIES, the IV is embedded in the ciphertext
result([
"ciphertext": FlutterStandardTypedData(bytes: ciphertext as Data),
"iv": FlutterStandardTypedData(bytes: Data()), // embedded in ECIES output
])
}
private func decrypt(alias: String, ciphertext: Data, iv: Data, result: @escaping FlutterResult) {
guard let key = getKey(alias: alias) else {
result(FlutterError(code: "KEY_NOT_FOUND", message: nil, details: nil))
return
}
var error: Unmanaged<CFError>?
guard let plaintext = SecKeyCreateDecryptedData(
key,
.eciesEncryptionCofactorVariableIVX963SHA256AESGCM,
ciphertext as CFData,
&error
) else {
result(FlutterError(
code: "DECRYPT_FAILED",
message: error?.takeRetainedValue().localizedDescription,
details: nil
))
return
}
result(FlutterStandardTypedData(bytes: plaintext as Data))
}
private func getKey(alias: String) -> SecKey? {
let query: [String: Any] = [
kSecClass as String: kSecClassKey,
kSecAttrApplicationTag as String: alias.data(using: .utf8)!,
kSecAttrKeyType as String: kSecAttrKeyTypeECSECPrimeRandom,
kSecReturnRef as String: true,
]
var item: CFTypeRef?
let status = SecItemCopyMatching(query as CFDictionary, &item)
guard status == errSecSuccess else { return nil }
return (item as! SecKey)
}
}Platform differences: what each gives you
| Capability | Android Keystore (TEE) | Android StrongBox | iOS Secure Enclave |
|---|---|---|---|
| Key generation in hardware | Yes | Yes | Yes |
| Key never in app memory | Yes | Yes | Yes |
| Separate physical chip | No (shared CPU) | Yes | Yes |
| Biometric binding | Yes | Yes | Yes |
| Key invalidation on biometric change | Yes | Yes | Yes |
| Symmetric keys (AES) | Yes | Yes | No (EC only, derive symmetric) |
| Asymmetric keys (EC/RSA) | Yes | Yes | Yes (EC P-256 only) |
| Key attestation | Yes | Yes | Yes (DeviceCheck) |
| Tamper-resistant | Partially (TEE) | Yes | Yes |
| Side-channel resistant | Depends on SoC | Yes | Yes |
The key difference: Android Keystore's TEE shares the CPU with your app (hardware isolation via TrustZone but same chip), while StrongBox and Secure Enclave are physically separate processors. For most threat models, TEE is sufficient. For high-assurance applications (banking, government), StrongBox/Secure Enclave provides stronger guarantees.
Biometric-bound keys: the strongest mobile auth
The most powerful feature of hardware-backed keys is biometric binding. You can create a key that's only usable after the user authenticates with their fingerprint or face. Not "show a biometric prompt and then access the key" — the key literally cannot perform cryptographic operations until the biometric hardware confirms identity.
This is different from what most Flutter biometric packages do. Most packages authenticate the user, get a boolean result, and then proceed. The key material is accessible regardless — the biometric is just a UI gate.
With hardware-bound biometric keys, the flow is:
1. App requests encryption with key "vault_key"
2. Android Keystore / iOS Keychain checks key's access control
3. Key requires biometric → OS shows biometric prompt
4. User authenticates → biometric hardware releases the key
5. Cryptographic operation happens inside secure hardware
6. Result (ciphertext) returned to app
7. Key is re-locked — next operation requires biometric againIf step 4 fails (wrong fingerprint, timeout), the key is never released. There is no fallback, no bypass, no way for the app to access the key without the biometric. This is enforced by hardware, not software.
// Using biometric-bound keys for vault access
class BiometricVault {
static const _keyAlias = 'vault_master_key';
/// Set up the vault — generates a biometric-bound key
static Future<void> initialize() async {
await SecureKeys.generateKey(
_keyAlias,
requireBiometric: true, // key bound to biometric auth
);
}
/// Encrypt vault data — triggers biometric prompt automatically
static Future<EncryptedPayload> lockVault(Uint8List data) async {
// This call will trigger the system biometric prompt
// If biometric fails, the method throws — no data is encrypted
return SecureKeys.encrypt(_keyAlias, data);
}
/// Decrypt vault data — triggers biometric prompt automatically
static Future<Uint8List> unlockVault(
Uint8List ciphertext,
Uint8List iv,
) async {
// This call will trigger the system biometric prompt
// If biometric fails, the method throws — no data is decrypted
return SecureKeys.decrypt(_keyAlias, ciphertext, iv);
}
}The beauty of this approach: the biometric authentication and the key access are atomic. There's no window where the key is accessible but the biometric hasn't been verified. There's no way for malware to intercept the key between authentication and use — because they happen in the same hardware operation.
Key attestation: proving the key is in hardware
Enterprise clients sometimes need proof that a key is genuinely hardware-backed — not generated in software pretending to be hardware. Key attestation provides this proof.
On Android, the Keystore can generate an attestation certificate chain that proves:
- The key was generated inside the TEE or StrongBox
- The device's security patch level
- Whether the bootloader is locked
- The key's properties (algorithm, size, access controls)
// Android key attestation
fun getKeyAttestation(alias: String, challenge: ByteArray): List<ByteArray> {
val keyStore = KeyStore.getInstance("AndroidKeyStore")
keyStore.load(null)
// The attestation certificate chain proves the key is hardware-backed
val certificates = keyStore.getCertificateChain(alias)
?: throw IllegalStateException("No certificate chain for key '$alias'")
return certificates.map { it.encoded }
}The server can verify this attestation chain against Google's root certificates. If the chain validates, the server has cryptographic proof that the key exists inside genuine, unmodified hardware. This is used in high-security scenarios: banking apps verifying they're running on uncompromised devices, enterprise MDM solutions verifying device integrity.
On iOS, the equivalent is DeviceCheck and App Attest, which provide device-level and app-level integrity assertions.
What flutter_secure_storage actually does
Many Flutter developers use flutter_secure_storage and assume it's "secure." Let's look at what it actually does under the hood:
On Android: It uses EncryptedSharedPreferences, which encrypts values using AES-256-SIV with a master key stored in the Android Keystore. The master key is hardware-backed (TEE), so the encryption key is protected. The encrypted values are stored in a SharedPreferences XML file on disk.
On iOS: It uses the Keychain with kSecAttrAccessibleWhenUnlocked by default. Data is encrypted at rest by the Keychain's data protection system.
For most apps, this is sufficient. But understand the limitations:
- No biometric binding by default. The data is accessible whenever the device is unlocked. You need to configure access control flags for biometric binding.
- Not Secure Enclave. The values are encrypted with a Keystore/Keychain key, but the data itself goes through the app's memory. The Secure Enclave approach keeps the key operations inside hardware.
- Backup concerns. On Android,
EncryptedSharedPreferencesfiles can appear in backups. On iOS, Keychain items with certain attributes can sync to iCloud Keychain.
For enterprise apps that need to demonstrate hardware-backed security to auditors, the custom platform channel approach shown above gives you explicit control and clear documentation of exactly what's hardware-backed.
Practical architecture: combining software and hardware security
Here's how these pieces fit together in a real enterprise Flutter app:
App startup:
1. Check if hardware key exists → if not, generate it
2. Biometric prompt → unlocks hardware key
3. Hardware key decrypts the "vault key" (stored in Keychain/Keystore)
4. Vault key decrypts the local database (SQLCipher)
5. App is ready — database is accessible
API calls:
1. Auth token stored encrypted with hardware key
2. Each API call retrieves and decrypts the token
3. Token sent in request header
4. Token re-encrypted after use (optional, for paranoid mode)
Data at rest:
├── SQLCipher database (encrypted with vault key)
├── Vault key (encrypted with hardware-backed key)
├── Hardware-backed key (in TEE/StrongBox/Secure Enclave)
│ └── Never leaves hardware
│ └── Requires biometric to use
├── Auth tokens (in flutter_secure_storage → Keystore/Keychain)
└── Cache files: NO sensitive dataThis layered approach means:
- Lost/stolen device (locked): Attacker can't access any data. Hardware key requires biometric.
- Lost/stolen device (unlocked, no biometric): Data protected by hardware key's biometric binding.
- Rooted device: Hardware keys are still protected — TrustZone/Secure Enclave isolation survives root.
- Device backup extracted: Encrypted data without the hardware key is useless.
- App binary reverse-engineered: No keys in the binary — they're generated at runtime in hardware.
When hardware security isn't enough
Hardware security has limits:
Device compromise by a state actor. Intelligence agencies have been known to exploit TrustZone vulnerabilities. The Secure Enclave has had vulnerabilities (checkm8 affected the boot ROM on older iPhones). Hardware security raises the bar enormously, but it's not impenetrable against a sufficiently motivated and resourced attacker.
User coercion. If someone forces the user to place their finger on the sensor, biometric binding doesn't help. Consider implementing a "duress mode" — a secondary biometric (different finger) that appears to unlock but actually triggers a wipe or sends a silent alert.
Device without hardware support. Low-end Android devices may not have TrustZone implementations that properly protect Keymaster. Very old devices may not support the Keystore at all. Your app needs a graceful fallback — and a clear understanding of what security guarantees are lost.
Key lifecycle. What happens when the user gets a new phone? Hardware keys can't be exported (that's the point). You need a key migration strategy — typically re-encrypting data with a key derived from the user's password, transferring the encrypted data, and re-encrypting with a new hardware key on the new device.
The compliance connection
Hardware-backed key management directly satisfies requirements in every major compliance framework:
- HIPAA: "Implement a mechanism to encrypt and decrypt electronic protected health information" — with Keystore/Keychain, you can demonstrate the key is hardware-protected
- PCI-DSS Requirement 3: "Protect stored cardholder data" — hardware-backed encryption with non-extractable keys
- SOC 2 CC6.1: "Logical and physical access controls" — biometric-bound hardware keys are both logical and physical access controls
- GDPR Article 32: "Implement appropriate technical measures" — hardware security is about as appropriate as it gets
In the compliance frameworks post, several requirements call for encryption with proper key management. This post is the implementation of "proper key management."
This post implements the key management referenced in E2E Encryption and Compliance Frameworks. For testing these implementations, see Security Audit Artifacts.