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>
This commit is contained in:
78
CHANGELOG.md
78
CHANGELOG.md
@@ -5,6 +5,84 @@ All notable changes to Shade are documented in this file.
|
||||
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
|
||||
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
||||
|
||||
## [Unreleased — 2026-05-09] — Android: V4.9/V4.10 ports + KeystoreStorage adapter
|
||||
|
||||
The Kotlin side of the v4.10 cross-host approval routing FR. With this
|
||||
release every primitive Prism Plan 04 needs on the phone has a Kotlin
|
||||
implementation that produces byte-identical output to the TS reference.
|
||||
|
||||
**`shade-android` (pure-JVM, no Android SDK needed)**
|
||||
|
||||
- V4.9 blob primitives ported: `deriveBlobSlotId / deriveBlobKey /
|
||||
deriveBlobSigningSeed`, `aeadSeal / aeadOpen` (`nonce(12) || ct||tag`
|
||||
format with `shade-profile-aad-v1:<slotIdHex>` AAD),
|
||||
`ed25519PublicKeyFromSeed`, `slotIdToHex`. Lives under `no.zyon.shade.blob`.
|
||||
- `BlobClient` HTTP wrapper for `/v1/blob/<slotId>` (java.net.http) —
|
||||
GET/PUT/DELETE with the same canonical-JSON-sorted-keys signing form
|
||||
the TS server expects. Hand-written JSON canonicalizer keeps Ed25519
|
||||
signing-input bytes identical to TS `signPayload` output.
|
||||
- `Profile` high-level namespace (`createProfileNamespace`) — bundles
|
||||
KDF + AEAD seal/open + BlobClient calls into the same shape as
|
||||
`@shade/sdk`'s `createProfileNamespace`.
|
||||
- V4.10 approval helpers ported: canonical profile schema
|
||||
(`CanonicalProfileBlob`, `ProfileHostEntry`, `ProfileClientEntry`)
|
||||
with parse/serialize/upsert/setTrustedApprover mutators that
|
||||
re-derive `trustedApproverFingerprints[]` invariantly.
|
||||
`buildApprovalRequest`, `signProxyApproval`, `verifyProxyApproval`,
|
||||
and the load-bearing `canonicalApprovalSigningBytes` (length-prefixed
|
||||
u16 BE UTF-8). All under `no.zyon.shade.approval`.
|
||||
- Password KDFs: `deriveMasterKey` (scrypt) + `deriveMasterKeyArgon2id`
|
||||
via Bouncy Castle. NFKC-normalize string inputs to match TS.
|
||||
- New `SessionStateJson` serializer for at-rest persistence of
|
||||
`IdentityKeyPair` / `SignedPreKey` / `OneTimePreKey` / `SessionState`.
|
||||
|
||||
**Cross-platform vectors**
|
||||
|
||||
- `test-vectors/blob.json` — V4.9 blob KDF + AEAD round-trip. Three
|
||||
(master, app) cases plus two pinned AEAD seal/open round-trips.
|
||||
- `test-vectors/approval.json` — V4.10 approval signing-payload bytes
|
||||
+ a TS-signed Ed25519 signature the Kotlin port verifies. Ed25519 is
|
||||
deterministic, so the Kotlin sign-with-the-same-seed produces the
|
||||
same 64 bytes back — that's checked too.
|
||||
- Both wired into `CrossPlatformVectorTest` so any byte-divergence
|
||||
fails Gradle within the existing 60-second parity gate.
|
||||
|
||||
**`shade-android-keystore` (new sibling module — Android-specific)**
|
||||
|
||||
- `KeystoreMasterKey` — hardware-backed AES-256-GCM master key.
|
||||
- `BIOMETRIC_STRONG` gating only (Class 3 assurance) — explicitly
|
||||
excludes `DEVICE_CREDENTIAL` so a stolen-device-with-known-PIN
|
||||
can't unlock Shade.
|
||||
- StrongBox-backed when available; transparent fallback to TEE.
|
||||
- `setInvalidatedByBiometricEnrollment(true)`: a newly enrolled
|
||||
fingerprint/face wipes the key, forcing credential rebootstrap.
|
||||
- `BiometricUnlock` — coroutine wrapper around `BiometricPrompt`.
|
||||
Tagged exceptions (`BiometricCancelledException` /
|
||||
`BiometricFailedException`) so callers handle UX without writing
|
||||
callback boilerplate.
|
||||
- `KeystoreStorage` — `StorageProvider` impl over `SharedPreferences`
|
||||
with each row AES-GCM-encrypted under the keystore key. AAD = the
|
||||
pref key string so a substituted-prefs swap fails to open. Exposes
|
||||
`unlock(BiometricUnlock)` / `lock()` / `forgetEverything()` for
|
||||
app-lifetime gating (single biometric prompt at start, in-memory
|
||||
unlock thereafter).
|
||||
- Builds as a standard AAR (`com.android.library`, AGP 8.7.3), depends
|
||||
transitively on `:shade-android` for protocol types and on
|
||||
`androidx.biometric:biometric:1.2.0-alpha05`.
|
||||
|
||||
**Threat model**
|
||||
|
||||
The keystore key never leaves the secure environment — encrypt/decrypt
|
||||
operations happen in the TEE/StrongBox. A compromised app process can
|
||||
ask the TEE to use the cipher only after biometric authentication
|
||||
within the same `Cipher` instance. Combined with `Profile.delete()`
|
||||
+ `forgetEverything()`, this gives a credible erase path: zero
|
||||
recoverable plaintext after a rebootstrap.
|
||||
|
||||
Instrumented tests for `KeystoreStorage` are deferred (need an
|
||||
emulator/device); the pure-JVM `SessionStateJson` round-trip is unit-
|
||||
tested in `:shade-android`.
|
||||
|
||||
## [4.10.0] — 2026-05-09 — cross-host approval routing primitives
|
||||
|
||||
Prism filed a follow-up feature request
|
||||
|
||||
Reference in New Issue
Block a user