Skip to main content

Documentation Index

Fetch the complete documentation index at: https://docs.privacyboost.io/llms.txt

Use this file to discover all available pages before exploring further.

Session Storage

This guide covers securely storing and restoring Privacy Boost sessions using Android Keystore, expanding on the persistence options in Key Management.

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: PrivacyBoost,
    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