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)