Skip to main content

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:
dependencies {
    implementation("androidx.security:security-crypto:1.1.0-alpha06")
}

Implementation

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:
dependencies {
    implementation("androidx.biometric:biometric:1.1.0")
}

BiometricSessionStorage

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

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

@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

  1. Use BIOMETRIC_STRONG - Requires Class 3 biometrics
  2. Set invalidatedByBiometricEnrollment - Invalidate key if new biometric enrolled
  3. No timeout - Require auth every time for sensitive data
  4. Handle key invalidation - Clear session if key is invalidated
  5. Don’t store unencrypted - Always encrypt sensitive data

Next Steps