Most apps today deal with sensitive data in some form. Tokens, user credentials, payment info, encryption keys. If all of that lives only in app memory, it’s easier to extract than you might think.
That’s why hardware-backed security matters.
Instead of trusting software alone, you let dedicated hardware handle key storage and cryptographic operations. On Android and other devices, that’s often TPM 2.0 (or similar hardware). On Apple devices, it’s the Secure Enclave.
If you’re using Kotlin Multiplatform, you can design this cleanly without duplicating logic across platforms.
Let’s walk through how it actually fits together.
What “Hardware-Backed Security” Really Means
Software-only protection is useful, but it has limits. If malware, root access, or a compromised OS gets in, software-held keys can be exposed more easily.
Hardware-backed systems reduce that risk by keeping keys inside a protected chip or secure execution area. The main app can ask for a cryptographic operation, but it should never see the raw secret.
This is why TPM 2.0 and Secure Enclave are so valuable. They are built to protect keys, verify device state, and make attacks harder even when the surrounding system is not fully trusted.
At a practical level, it means:
- Keys are generated inside secure hardware
- They never leave that environment
- Your app can use them, but can’t extract them
So even if someone reverse engineers your app or dumps memory, the critical material isn’t there.
TPM 2.0 in Practice (Android and Beyond)
TPM 2.0 stands for Trusted Platform Module 2.0. It is a hardware root of trust commonly found on PCs and laptops (on Windows/Linux), and it is used for secure key storage, platform integrity checks, and device attestation.
A TPM can generate keys, store them securely, and perform operations without exposing the private material to normal application memory. It is especially useful for boot integrity, device authentication, and encryption workflows tied to system trust.
Think of TPM 2.0 as a locked vault inside the machine. The app can request a signature or decryption, but it cannot simply open the vault and copy the key.
You usually don’t talk to TPM 2.0 directly on Android. Instead, you go through the Android Keystore system, which uses secure hardware when available.
What you get:
- Hardware-isolated key storage
- Built-in enforcement (like requiring biometrics)
- Protection against key export
From your app’s point of view, you’re just asking the system to generate and use keys. The hardware layer is handled underneath.
Secure Enclave on iOS and macOS
Secure Enclave is Apple’s isolated security subsystem used on Apple devices for protecting sensitive operations. It is commonly used for biometrics, key protection, and secure cryptographic actions.
Like TPM 2.0, it keeps secrets away from normal app memory and the main operating system. The difference is that Secure Enclave is more tightly integrated into Apple’s hardware and software stack, which makes it feel more seamless for iOS and macOS developers.
In practice, Secure Enclave is often the best place to anchor sensitive app secrets on Apple platforms. For user-facing apps, this can support safer authentication, credential storage, and cryptographic signing.
Apple’s Secure Enclave works similarly, but it’s more tightly integrated.
- Keys are created inside the enclave
- Biometric checks happen there
- The OS never exposes raw key material
If you’ve used Face ID or Touch ID to unlock something securely, you’ve already used it.
Where Kotlin Multiplatform Helps
Kotlin Multiplatform is a great choice when you want shared business logic but still need access to platform-specific security features. You can keep your common encryption flow, data models, and validation logic in shared code, then call Android and Apple native APIs for hardware-backed key handling.
This gives you the best of both worlds:
- Shared security logic in common code.
- Platform-native key storage on Android and Apple.
- Less duplicated code across apps.
- A cleaner path to consistent behavior.
For many teams, Kotlin Multiplatform is the right balance between reuse and platform control.
Recommended architecture
A good design separates responsibilities clearly.
- Common module: serialization, policy checks, encryption orchestration.
- Android module: Android Keystore or TPM-backed flows where available.
- Apple module: Keychain and Secure Enclave-backed APIs where available.
- Shared interface: a small API that hides platform differences.
This approach keeps your common code simple and testable while allowing each platform to use its strongest security primitive.
Instead of writing separate security flows for Android and iOS, you define a shared contract and implement it per platform.
You’re not trying to abstract the hardware itself. You’re abstracting how your app uses it.
Setting Up the Multiplatform Architecture
To keep our project clean, we use the expect/actual mechanism. We define a common “blueprint” in our shared module and then provide the “real” implementation for each platform.
Note: For simplicity, only Android and iOS are discussed here, but this is not limited to those platforms — we can implement it on other platforms and desktops as well (see the bonus section below).
Define a Common Interface
Start with a simple interface in shared code:
interface SecureKeyManager {
fun generateKey(alias: String)
fun encrypt(data: ByteArray): ByteArray
fun decrypt(data: ByteArray): ByteArray
}This keeps your business logic independent of platform details.
Android Implementation (Keystore / TPM 2.0-backed)
On Android, this typically goes through the Keystore:
class AndroidSecureKeyManager : SecureKeyManager {
override fun generateKey(alias: String) {
val keyGenerator = KeyGenerator.getInstance(
KeyProperties.KEY_ALGORITHM_AES, "AndroidKeyStore"
)
val spec = KeyGenParameterSpec.Builder(
alias,
KeyProperties.PURPOSE_ENCRYPT or KeyProperties.PURPOSE_DECRYPT
)
.setBlockModes(KeyProperties.BLOCK_MODE_GCM)
.setEncryptionPaddings(KeyProperties.ENCRYPTION_PADDING_NONE)
.setUserAuthenticationRequired(true)
.build()
keyGenerator.init(spec)
keyGenerator.generateKey()
}
override fun encrypt(data: ByteArray): ByteArray {
// Real implementation would use Cipher with the stored key
return data
}
override fun decrypt(data: ByteArray): ByteArray {
return data
}
}A few important details:
- The key is generated inside secure hardware when available
- You can require biometric auth before use
- The raw key is never exposed to your code
iOS Implementation (Secure Enclave)
On iOS, you’d use Keychain + Secure Enclave-backed keys.
import platform.Security.*
import platform.Foundation.*
class IOSSecureKeyManager : SecureKeyManager {
override fun generateKey(alias: String) {
// Backed by Secure Enclave via iOS Security framework or kotlin native (here we used kotlin native)
val flags = kSecAccessControlTouchIDAny or kSecAccessControlPrivateKeyUsage
val accessControl = SecAccessControlCreateWithFlags(
null,
kSecAttrAccessibleWhenUnlockedThisDeviceOnly,
flags,
null
)
val query = mutableMapOf<Any?, Any?>(
kSecAttrKeyType to kSecAttrKeyTypeECSECPrimeRandom,
kSecAttrKeySizeInBits to 256,
kSecAttrTokenID to kSecAttrTokenIDSecureEnclave, // Forces Secure Enclave
kSecPrivateKeyAttrs to mapOf(
kSecAttrIsPermanent to true,
kSecAttrApplicationTag to alias,
kSecAttrAccessControl to accessControl
)
)
val key = SecKeyCreateRandomKey(query as CFDictionaryRef, null)
println("Key generated securely: $key")
}
override fun encrypt(data: ByteArray): ByteArray {
return data
}
override fun decrypt(data: ByteArray): ByteArray {
return data
}
}The key here is kSecAttrTokenIDSecureEnclave. This tells iOS: “Don’t just store this in a database; burn this key into the hardware.
In a real app, this bridges into Swift/Objective-C APIs. Kotlin/Native calls into those under the hood.
Wiring It Together with expect/actual
Kotlin Multiplatform lets you plug in platform-specific implementations cleanly.
Shared code:
expect class PlatformSecureKeyManager() : SecureKeyManagerAndroid:
actual class PlatformSecureKeyManager actual constructor() :
AndroidSecureKeyManager()iOS:
actual class PlatformSecureKeyManager actual constructor() :
IOSSecureKeyManager()Now the rest of your app just depends on SecureKeyManager.
Bonus: Implementing TPM 2.0 (Windows/Desktop)
For Windows or Linux desktop targets, Kotlin Multiplatform uses Kotlin/Native to talk to system C-libraries. On Windows, we typically interact with the NCrypt (Next Generation Cryptography) library to access the TPM 2.0.
The Windows Implementation
In your desktopMain or mingwMain, you would use cinterop to call the Windows CNG (Cryptography Next Generation) API:
import kotlinx.cinterop.*
import platform.windows.*
class WindowsTPMProvider : SecureKeyManager {
// Using the Microsoft Platform Crypto Provider specifically targets the TPM
private val MS_PLATFORM_CRYPTO_PROVIDER = "Microsoft Platform Crypto Provider"
override fun generateKey(alias: String) {
memScoped {
val hProvider = alloc<NCRYPT_PROV_HANDLEVar>()
val hKey = alloc<NCRYPT_KEY_HANDLEVar>()
// 1. Open the TPM Storage Provider
NCryptOpenStorageProvider(hProvider.ptr, MS_PLATFORM_CRYPTO_PROVIDER, 0)
// 2. Create a new RSA or ECC key persisted in hardware
NCryptCreatePersistedKey(
hProvider.value,
hKey.ptr,
BCRYPT_RSA_ALGORITHM, // You can also use BCRYPT_ECDSA_P256_ALGORITHM
alias,
0,
0
)
// 3. Finalize the key to "burn" it into the TPM
NCryptFinalizeKey(hKey.value, 0)
// Clean up handles
NCryptFreeObject(hKey.value)
NCryptFreeObject(hProvider.value)
}
}
override fun encrypt(data: ByteArray): ByteArray {
// Implementation would involve NCryptOpenKey using the alias
// followed by NCryptEncrypt
// For hardware-backed keys, the TPM handles the actual math
return todo("NCryptEncrypt implementation")
}
override fun decrypt(data: ByteArray): ByteArray {
// Implementation would involve NCryptOpenKey using the alias
// followed by NCryptDecrypt
return todo("NCryptDecrypt implementation")
}
}- The Provider: By using
MS_PLATFORM_CRYPTO_PROVIDER, you are explicitly telling Windows to bypass the software-based providers and use the TPM 2.0 chip. If the device lacks a TPM, this call will fail, allowing you to handle the error gracefully. - NCryptFinalizeKey: In the Windows CNG (Cryptography Next Generation) API, a key isn’t “real” until you finalize it. This is the moment the TPM 2.0 generates the key material internally.
- Memory Management: Since this is Kotlin Multiplatform targeting Windows (Native), we use
memScopedandalloc. This ensures that pointers used for Windows C-headers are cleaned up properly, preventing memory leaks in your security layer.
Where This Is Actually Useful
This setup shows up in a few common places:
- Storing auth tokens securely
- Encrypting local database values
- Managing private keys for end-to-end encryption
- Adding biometric protection to sensitive actions
You don’t need to over-engineer it. Even using hardware-backed storage for one critical key is a big improvement.
Things That Trip People Up
Some common mistakes:
- Assuming all devices have hardware-backed storage
- Forgetting to handle fallback paths
- Treating encryption as useful without secure key storage
- Not testing biometric-required flows properly
Also worth noting: emulators don’t behave the same as real devices here.
A Few Practical Tips
- Always prefer hardware-backed keys when available
- Require user authentication for sensitive operations
- Don’t cache decrypted data longer than needed
- Keep your abstraction small and focused
You don’t need a huge framework. Just a clean boundary and correct usage.
Conclusion
You don’t interact with TPM 2.0 or Secure Enclave directly most of the time. The platform APIs handle that. Your job is to use them correctly and structure your code so it stays maintainable.
That’s where Kotlin Multiplatform helps. You define the contract once, plug in the platform specifics, and keep the rest of your app clean.
If you’re already sharing business logic across platforms, adding this layer is a natural next step.
