Showing posts with label Hardware-Backed Encryption. Show all posts
Showing posts with label Hardware-Backed Encryption. Show all posts

Part 2: Hardware-Backed Encrypted DataStore in Android

 Alright — let’s make Part 2 of this blog: a full hardware-backed Encrypted DataStore implementation for production use.

We’ll combine:

  • Jetpack DataStore (Preferences) for safe, async storage

  • Hardware-backed AES key in Android Keystore (StrongBox when available)

  • AES/GCM/NoPadding encryption with IV handling

  • Easy API for saving/reading/deleting sensitive strings



Part 2: Hardware-Backed Encrypted DataStore in Android


1. Why Combine Hardware-Backed Keys with DataStore?

From Part 1, we learned that hardware-backed encryption ensures that encryption keys never leave secure hardware and can’t be extracted.

DataStore is the modern alternative to SharedPreferences:

  • Asynchronous (no ANRs)

  • Type-safe

  • Corruption-handling

  • Flow-based API

By encrypting all values before storing them in DataStore — with a hardware-backed AES key — we get:

  • Encryption at rest + secure key storage

  • Resilience against root and file dump attacks

  • Modern, maintainable API


2. Dependencies

Add to your build.gradle:

dependencies {
    implementation "androidx.datastore:datastore-preferences:1.1.1"
}

No extra crypto libraries are needed — we’ll use Android’s built-in Keystore and javax.crypto.


3. Crypto Helper (Hardware-Backed)

import android.security.keystore.KeyGenParameterSpec
import android.security.keystore.KeyProperties
import java.security.KeyStore
import javax.crypto.Cipher
import javax.crypto.KeyGenerator
import javax.crypto.SecretKey
import javax.crypto.spec.GCMParameterSpec
import android.util.Base64
import java.security.SecureRandom

object HardwareCrypto {
    private const val KEY_ALIAS = "app_secure_datastore_key"
    private const val ANDROID_KEYSTORE = "AndroidKeyStore"
    private const val TRANSFORMATION = "AES/GCM/NoPadding"
    private const val IV_SIZE_BYTES = 12
    private const val TAG_LENGTH_BITS = 128

    fun getOrCreateKey(): SecretKey {
        val keyStore = KeyStore.getInstance(ANDROID_KEYSTORE).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, ANDROID_KEYSTORE)
        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 encrypt(secretKey: SecretKey, plainText: String): String {
        val cipher = Cipher.getInstance(TRANSFORMATION)
        val iv = ByteArray(IV_SIZE_BYTES).also { SecureRandom().nextBytes(it) }
        cipher.init(Cipher.ENCRYPT_MODE, secretKey, GCMParameterSpec(TAG_LENGTH_BITS, iv))
        val cipherBytes = cipher.doFinal(plainText.toByteArray(Charsets.UTF_8))
        return Base64.encodeToString(iv + cipherBytes, Base64.NO_WRAP)
    }

    fun decrypt(secretKey: SecretKey, encryptedBase64: String): String {
        val decoded = Base64.decode(encryptedBase64, Base64.NO_WRAP)
        val iv = decoded.copyOfRange(0, IV_SIZE_BYTES)
        val cipherBytes = decoded.copyOfRange(IV_SIZE_BYTES, decoded.size)
        val cipher = Cipher.getInstance(TRANSFORMATION)
        cipher.init(Cipher.DECRYPT_MODE, secretKey, GCMParameterSpec(TAG_LENGTH_BITS, iv))
        return String(cipher.doFinal(cipherBytes), Charsets.UTF_8)
    }
}

4. Encrypted DataStore Manager

import android.content.Context
import androidx.datastore.preferences.core.edit
import androidx.datastore.preferences.core.preferencesKey
import androidx.datastore.preferences.preferencesDataStore
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.map
import javax.crypto.SecretKey

private val Context.secureDataStore by preferencesDataStore(name = "secure_prefs")

class EncryptedDataStoreManager(private val context: Context) {

    private val secretKey: SecretKey by lazy { HardwareCrypto.getOrCreateKey() }

    suspend fun saveString(key: String, value: String) {
        val encrypted = HardwareCrypto.encrypt(secretKey, value)
        context.secureDataStore.edit { prefs ->
            prefs[preferencesKey<String>(key)] = encrypted
        }
    }

    fun readString(key: String): Flow<String?> {
        return context.secureDataStore.data.map { prefs ->
            prefs[preferencesKey<String>(key)]?.let {
                try { HardwareCrypto.decrypt(secretKey, it) } catch (e: Exception) { null }
            }
        }
    }

    suspend fun removeKey(key: String) {
        context.secureDataStore.edit { prefs ->
            prefs.remove(preferencesKey<String>(key))
        }
    }

    suspend fun clearAll() {
        context.secureDataStore.edit { it.clear() }
    }
}

5. Example Usage

val secureStore = EncryptedDataStoreManager(context)

// Saving
lifecycleScope.launch {
    secureStore.saveString("auth_token", "super_secret_token_123")
}

// Reading
lifecycleScope.launch {
    secureStore.readString("auth_token").collect { token ->
        println("Decrypted token: $token")
    }
}

6. Benefits of This Approach

  • Hardware-backed keys protect encryption keys at the hardware level

  • Asynchronous DataStore prevents ANRs

  • AES-256 GCM provides confidentiality + integrity verification

  • StrongBox support ensures even higher security on compatible devices

  • Simple API for engineers to integrate


7. Final Thoughts

If your app handles any sensitive data — authentication tokens, API secrets, offline cached PII — you should never store it in plain text. Combining hardware-backed keys with modern DataStore gives you an end-to-end secure storage layer that’s:

  • Modern

  • Maintainable

  • Resistant to common mobile security threats

In a security audit, this design will be a strong point in your architecture.


πŸ“’ 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! πŸ’»

Why Hardware-Backed Encryption Matters in Android Security (with Example)

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

  1. 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.

  2. Key usage:

    • The AES key never leaves secure hardware.

    • Encryption/decryption operations happen inside the hardware security module.

  3. 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! πŸ’»