Android Keystore Integration
This guide covers securely storing Privacy Boost session data using Android Keystore.Why Keystore?
Session data includes sensitive cryptographic keys. Android Keystore provides:- Hardware-backed encryption (TEE/Secure Element)
- Key material never leaves secure hardware
- Biometric authentication support
- Protection against extraction
EncryptedSharedPreferences
The simplest approach using AndroidX Security:Setup
Add dependency:Copy
dependencies {
implementation("androidx.security:security-crypto:1.1.0-alpha06")
}
Implementation
Copy
import android.content.Context
import androidx.security.crypto.EncryptedSharedPreferences
import androidx.security.crypto.MasterKey
import com.google.gson.Gson
class SessionStorage(context: Context) {
private val masterKey = MasterKey.Builder(context)
.setKeyScheme(MasterKey.KeyScheme.AES256_GCM)
.build()
private val sharedPrefs = EncryptedSharedPreferences.create(
context,
"privacy_boost_session",
masterKey,
EncryptedSharedPreferences.PrefKeyEncryptionScheme.AES256_SIV,
EncryptedSharedPreferences.PrefValueEncryptionScheme.AES256_GCM
)
private val gson = Gson()
fun saveSession(session: ExportedSession) {
val json = gson.toJson(session)
sharedPrefs.edit()
.putString(KEY_SESSION, json)
.apply()
}
fun loadSession(): ExportedSession? {
val json = sharedPrefs.getString(KEY_SESSION, null) ?: return null
return try {
gson.fromJson(json, ExportedSession::class.java)
} catch (e: Exception) {
null
}
}
fun deleteSession() {
sharedPrefs.edit()
.remove(KEY_SESSION)
.apply()
}
fun hasSession(): Boolean {
return sharedPrefs.contains(KEY_SESSION)
}
companion object {
private const val KEY_SESSION = "session_data"
}
}
Biometric Authentication
Add biometric protection for session access:Setup
Add dependencies:Copy
dependencies {
implementation("androidx.biometric:biometric:1.1.0")
}
BiometricSessionStorage
Copy
import android.content.Context
import android.security.keystore.KeyGenParameterSpec
import android.security.keystore.KeyProperties
import androidx.biometric.BiometricManager
import androidx.biometric.BiometricPrompt
import androidx.core.content.ContextCompat
import androidx.fragment.app.FragmentActivity
import java.security.KeyStore
import javax.crypto.Cipher
import javax.crypto.KeyGenerator
import javax.crypto.SecretKey
import javax.crypto.spec.GCMParameterSpec
class BiometricSessionStorage(private val context: Context) {
private val keyStore = KeyStore.getInstance("AndroidKeyStore").apply { load(null) }
private val gson = Gson()
// MARK: - Biometric Availability
fun isBiometricAvailable(): Boolean {
val biometricManager = BiometricManager.from(context)
return when (biometricManager.canAuthenticate(BiometricManager.Authenticators.BIOMETRIC_STRONG)) {
BiometricManager.BIOMETRIC_SUCCESS -> true
else -> false
}
}
// MARK: - Save with Biometrics
fun saveSession(
activity: FragmentActivity,
session: ExportedSession,
onSuccess: () -> Unit,
onError: (String) -> Unit
) {
val executor = ContextCompat.getMainExecutor(context)
val callback = object : BiometricPrompt.AuthenticationCallback() {
override fun onAuthenticationSucceeded(result: BiometricPrompt.AuthenticationResult) {
try {
val cipher = result.cryptoObject?.cipher
?: throw Exception("No cipher available")
val json = gson.toJson(session)
val encrypted = cipher.doFinal(json.toByteArray())
val iv = cipher.iv
// Store encrypted data and IV
getPrefs().edit()
.putString(KEY_ENCRYPTED_SESSION, encrypted.toBase64())
.putString(KEY_IV, iv.toBase64())
.apply()
onSuccess()
} catch (e: Exception) {
onError(e.message ?: "Encryption failed")
}
}
override fun onAuthenticationError(errorCode: Int, errString: CharSequence) {
onError(errString.toString())
}
override fun onAuthenticationFailed() {
// Called when biometric is valid but not recognized
}
}
val prompt = BiometricPrompt(activity, executor, callback)
val promptInfo = BiometricPrompt.PromptInfo.Builder()
.setTitle("Save Session")
.setSubtitle("Authenticate to save your Privacy Boost session")
.setNegativeButtonText("Cancel")
.setAllowedAuthenticators(BiometricManager.Authenticators.BIOMETRIC_STRONG)
.build()
// Get cipher for encryption
val cipher = getCipherForEncryption()
prompt.authenticate(promptInfo, BiometricPrompt.CryptoObject(cipher))
}
// MARK: - Load with Biometrics
fun loadSession(
activity: FragmentActivity,
onSuccess: (ExportedSession) -> Unit,
onError: (String) -> Unit
) {
val prefs = getPrefs()
val encryptedBase64 = prefs.getString(KEY_ENCRYPTED_SESSION, null)
val ivBase64 = prefs.getString(KEY_IV, null)
if (encryptedBase64 == null || ivBase64 == null) {
onError("No session found")
return
}
val executor = ContextCompat.getMainExecutor(context)
val callback = object : BiometricPrompt.AuthenticationCallback() {
override fun onAuthenticationSucceeded(result: BiometricPrompt.AuthenticationResult) {
try {
val cipher = result.cryptoObject?.cipher
?: throw Exception("No cipher available")
val encrypted = encryptedBase64.fromBase64()
val decrypted = cipher.doFinal(encrypted)
val json = String(decrypted)
val session = gson.fromJson(json, ExportedSession::class.java)
onSuccess(session)
} catch (e: Exception) {
onError(e.message ?: "Decryption failed")
}
}
override fun onAuthenticationError(errorCode: Int, errString: CharSequence) {
onError(errString.toString())
}
}
val prompt = BiometricPrompt(activity, executor, callback)
val promptInfo = BiometricPrompt.PromptInfo.Builder()
.setTitle("Access Session")
.setSubtitle("Authenticate to access your Privacy Boost session")
.setNegativeButtonText("Cancel")
.setAllowedAuthenticators(BiometricManager.Authenticators.BIOMETRIC_STRONG)
.build()
// Get cipher for decryption
val iv = ivBase64.fromBase64()
val cipher = getCipherForDecryption(iv)
prompt.authenticate(promptInfo, BiometricPrompt.CryptoObject(cipher))
}
// MARK: - Key Management
private fun getOrCreateKey(): SecretKey {
keyStore.getKey(KEY_ALIAS, null)?.let {
return it as SecretKey
}
val keyGenerator = KeyGenerator.getInstance(
KeyProperties.KEY_ALGORITHM_AES,
"AndroidKeyStore"
)
val keySpec = KeyGenParameterSpec.Builder(
KEY_ALIAS,
KeyProperties.PURPOSE_ENCRYPT or KeyProperties.PURPOSE_DECRYPT
)
.setBlockModes(KeyProperties.BLOCK_MODE_GCM)
.setEncryptionPaddings(KeyProperties.ENCRYPTION_PADDING_NONE)
.setKeySize(256)
.setUserAuthenticationRequired(true)
.setUserAuthenticationParameters(
0, // Timeout (0 = every use)
KeyProperties.AUTH_BIOMETRIC_STRONG
)
.setInvalidatedByBiometricEnrollment(true)
.build()
keyGenerator.init(keySpec)
return keyGenerator.generateKey()
}
private fun getCipherForEncryption(): Cipher {
val cipher = Cipher.getInstance("AES/GCM/NoPadding")
cipher.init(Cipher.ENCRYPT_MODE, getOrCreateKey())
return cipher
}
private fun getCipherForDecryption(iv: ByteArray): Cipher {
val cipher = Cipher.getInstance("AES/GCM/NoPadding")
val spec = GCMParameterSpec(128, iv)
cipher.init(Cipher.DECRYPT_MODE, getOrCreateKey(), spec)
return cipher
}
private fun getPrefs() = context.getSharedPreferences(
"privacy_boost_biometric",
Context.MODE_PRIVATE
)
fun deleteSession() {
getPrefs().edit().clear().apply()
keyStore.deleteEntry(KEY_ALIAS)
}
fun hasSession(): Boolean {
return getPrefs().contains(KEY_ENCRYPTED_SESSION)
}
companion object {
private const val KEY_ALIAS = "privacy_boost_session_key"
private const val KEY_ENCRYPTED_SESSION = "encrypted_session"
private const val KEY_IV = "iv"
}
}
// Extension functions
private fun ByteArray.toBase64(): String =
android.util.Base64.encodeToString(this, android.util.Base64.NO_WRAP)
private fun String.fromBase64(): ByteArray =
android.util.Base64.decode(this, android.util.Base64.NO_WRAP)
Usage in Repository
Copy
class PrivacyBoostRepository(
private val sdk: PrivacyBoostSdk,
private val sessionStorage: SessionStorage,
private val biometricStorage: BiometricSessionStorage
) {
suspend fun restoreSession(): Boolean = withContext(Dispatchers.IO) {
// Try regular storage first
sessionStorage.loadSession()?.let { session ->
return@withContext try {
sdk.importSession(session)
} catch (e: Exception) {
sessionStorage.deleteSession()
false
}
}
false
}
fun restoreSessionWithBiometrics(
activity: FragmentActivity,
onSuccess: () -> Unit,
onError: (String) -> Unit
) {
if (!biometricStorage.isBiometricAvailable()) {
onError("Biometrics not available")
return
}
biometricStorage.loadSession(
activity = activity,
onSuccess = { session ->
try {
val success = sdk.importSession(session)
if (success) onSuccess() else onError("Session expired")
} catch (e: Exception) {
onError(e.message ?: "Import failed")
}
},
onError = onError
)
}
fun saveCurrentSession(useBiometrics: Boolean = false, activity: FragmentActivity? = null) {
val session = sdk.exportSession() ?: return
if (useBiometrics && activity != null && biometricStorage.isBiometricAvailable()) {
biometricStorage.saveSession(
activity = activity,
session = session,
onSuccess = { /* Saved */ },
onError = { /* Fall back to regular storage */
sessionStorage.saveSession(session)
}
)
} else {
sessionStorage.saveSession(session)
}
}
fun clearSession() {
sessionStorage.deleteSession()
biometricStorage.deleteSession()
}
}
ViewModel Integration
Copy
@HiltViewModel
class AuthViewModel @Inject constructor(
private val repository: PrivacyBoostRepository
) : ViewModel() {
private val _sessionRestored = MutableStateFlow(false)
val sessionRestored: StateFlow<Boolean> = _sessionRestored
fun tryRestoreSession() {
viewModelScope.launch {
_sessionRestored.value = repository.restoreSession()
}
}
fun restoreWithBiometrics(activity: FragmentActivity) {
repository.restoreSessionWithBiometrics(
activity = activity,
onSuccess = { _sessionRestored.value = true },
onError = { /* Show error */ }
)
}
fun saveSession(useBiometrics: Boolean, activity: FragmentActivity?) {
repository.saveCurrentSession(useBiometrics, activity)
}
}
Security Best Practices
- Use BIOMETRIC_STRONG - Requires Class 3 biometrics
- Set invalidatedByBiometricEnrollment - Invalidate key if new biometric enrolled
- No timeout - Require auth every time for sensitive data
- Handle key invalidation - Clear session if key is invalidated
- Don’t store unencrypted - Always encrypt sensitive data
Next Steps
- API Reference - Complete API documentation
- Compose Guide - Jetpack Compose integration