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! 💻
0 comments:
Post a Comment