# shade-android-keystore Android-specific bindings for `shade-android`. Lives as a sibling Gradle module so the JVM-only protocol code can keep running in CI without an Android SDK install. Provides: - **`KeystoreMasterKey`** — hardware-backed AES-256-GCM master key in the Android Keystore. Optionally biometric-gated (BIOMETRIC_STRONG only — Class 3 assurance), StrongBox-backed when available, invalidated on new biometric enrollment. - **`BiometricUnlock`** — coroutine wrapper around `BiometricPrompt` for unlocking a `Cipher` instance bound to the keystore key. Throws `BiometricCancelledException` / `BiometricFailedException` so callers can handle the auth flow without writing custom callbacks. - **`KeystoreStorage`** — `StorageProvider` implementation that persists session/identity/prekey state to `SharedPreferences`, each row encrypted under the keystore key with the row's preference key bound as AAD. ## Usage ```kotlin import androidx.fragment.app.FragmentActivity import no.zyon.shade.ShadeSessionManager import no.zyon.shade.crypto.TinkProvider import no.zyon.shade.keystore.BiometricUnlock import no.zyon.shade.keystore.KeystoreStorage class MyActivity : FragmentActivity() { private val crypto = TinkProvider() private lateinit var storage: KeystoreStorage private lateinit var manager: ShadeSessionManager override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) storage = KeystoreStorage(this, crypto) lifecycleScope.launch { val unlock = BiometricUnlock( activity = this@MyActivity, title = "Unlock Shade", subtitle = "Tap your fingerprint to access your messages", ) try { storage.unlock(unlock) } catch (e: BiometricCancelledException) { // user backed out — show a "tap to retry" UI return@launch } manager = ShadeSessionManager(crypto, storage) manager.initialize() // ... use manager normally } } } ``` For credential-driven bootstrap (V4.9 profile + V4.10 approval), pair this with `no.zyon.shade.blob.createProfileNamespace` and `no.zyon.shade.approval.signProxyApproval` — both pure-JVM (in `:shade-android`). ## Threat model - **Compromised app process**: cannot read the AES key (it's in the secure environment). Can attempt to use the cipher only after the user has authenticated; biometric re-prompts are required after each biometric event. - **Stolen device with known PIN**: cannot unlock — `setUserAuthenticationParameters(0, BIOMETRIC_STRONG)` excludes `DEVICE_CREDENTIAL`. - **Attacker enrolls own biometric**: `setInvalidatedByBiometricEnrollment(true)` invalidates the key on enrollment, forcing a credential rebootstrap (which would need username + password + PIN). - **Catastrophic recovery**: `forgetEverything()` deletes the master key and clears the SharedPreferences. Pair with `Profile.delete()` for full account erasure. ## Build Requires an Android SDK. The Gradle build uses Android Gradle Plugin 8.7+, AGP minSdk 28 (Pie+ for BiometricPrompt baseline), targetSdk 35. ```bash JAVA_HOME=/path/to/jdk-21 ./gradlew :shade-android-keystore:assembleDebug ``` Unit tests: none yet — `KeystoreStorage` requires Android runtime. Robolectric or instrumented tests against an emulator are tracked as a follow-up. The pure-JVM `SessionStateJson` round-trip serializer is tested in `:shade-android` (`SessionStateJsonTest`).