Files
Sterister 188c3db56a
Some checks failed
Cross-platform vectors / TypeScript vectors (bun) (push) Has been cancelled
Cross-platform vectors / Kotlin vectors (gradle) (push) Has been cancelled
Test / test (push) Has been cancelled
android: V4.9 + V4.10 Kotlin ports + KeystoreStorage adapter
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>
2026-05-09 17:38:15 +02:00
..

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.
  • KeystoreStorageStorageProvider 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

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.

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).