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
- 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
- Android Getting Started - Jetpack Compose integration