Pure-JVM additions to shade-android (no Android SDK needed): - V4.9 blob primitives: BlobKdf (HKDF deriveBlobSlotId/Key/SigningSeed), BlobAead (nonce||ct||tag with shade-profile-aad-v1:<slot> AAD), BlobClient (java.net.http with hand-written canonical JSON signing matching TS signPayload output), Profile high-level namespace. - V4.10 approval helpers: CanonicalProfileBlob schema with denormalized trustedApproverFingerprints, build/sign/verify proxy approvals via length-prefixed u16 BE UTF-8 canonical signing payload. - Password KDFs: scrypt + argon2id via Bouncy Castle, NFKC-normalized. - SessionStateJson at-rest serializer for persistence layer. Cross-platform vectors (test-vectors/blob.json, approval.json) gate byte-identical output between TS and Kotlin, including a TS-signed Ed25519 signature the Kotlin port verifies and reproduces (Ed25519 is deterministic). New shade-android-keystore sibling Gradle module (Android-specific): - KeystoreMasterKey: hardware-backed AES-256-GCM with BIOMETRIC_STRONG gating, StrongBox-backed when available, invalidated on enrollment. - BiometricUnlock: coroutine wrapper around BiometricPrompt with tagged cancellation/failure exceptions. - KeystoreStorage: StorageProvider over biometric-gated AES-encrypted SharedPreferences with AAD-bound row keys. All 25 SDK packages typecheck clean; 104 SDK tests + 24 new Kotlin tests + 11 cross-platform vector tests all green. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
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 aroundBiometricPromptfor unlocking aCipherinstance bound to the keystore key. ThrowsBiometricCancelledException/BiometricFailedExceptionso callers can handle the auth flow without writing custom callbacks.KeystoreStorage—StorageProviderimplementation that persists session/identity/prekey state toSharedPreferences, each row encrypted under the keystore key with the row's preference key bound as AAD.
Usage
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)excludesDEVICE_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 withProfile.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.
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).