M4: Android core — crypto port, protocol models, API client, secure store (ENG-248) #10

Merged demi.dev
targeting main from stoopkidddd/eng-248-m4-android-core-crypto-port-protocol-models-api-client
Merged opened by and assigned to demi.dev
targeting main from stoopkidddd/eng-248-m4-android-core-crypto-port-protocol-models-api-client

Linear: https://linear.app/engineereddev/issue/ENG-248

What's in this PR:

  • dev.pneumo.app.crypto (javax.crypto only): MasterKey (NFC, PBKDF2WithHmacSHA256), RFC 5869 HKDF, deriveKeys and keyCheck, PushCrypto (§6.5), streaming FileCrypto with the §6.6 lookahead plus ciphertextSize, and PairingUri (§6.7).
  • dev.pneumo.app.protocol: kotlinx.serialization models for every DTO and frame (sealed frames on type, ignoreUnknownKeys, required fields stay required, refinements validated).
  • dev.pneumo.app.net.ApiClient (OkHttp) maps ErrorBody to a typed ApiError.
  • dev.pneumo.app.data.SecureStore wraps secrets with an Android Keystore AES-GCM key (pneumo_wrap) and stores them in DataStore, behind a KeyWrapper interface. Uuidv7 helper.
  • The syncSharedFixtures Gradle task copies the frozen vectors and fixtures into the unit-test resources under build/.

Cross-language result: every vector in packages/crypto/vectors/v1.json (contract locked at 323bca9) and all 90 valid / 111 invalid protocol fixtures pass in Kotlin. Extra Kotlin tests cover the gate's vector gaps: wrong version byte, final block under 16 bytes, and empty last chunk when n > 1.

Verify (lead, forced re-runs):

./gradlew spotlessCheck detekt lint testDebugUnitTest verifyRoborazziDebug --rerun-tasks → exit 0 · Lint found no errors or warnings · 46 executed · JVM tests=54 failures=0
./gradlew pixel6Api34DebugAndroidTest --rerun → exit 0 · managed device tests pass (incl. real Keystore SecureStore round trip)
git diff origin/main...HEAD -- packages → empty (frozen contract untouched)

Deviations:

  • The INTERNET permission is added to the manifest (spec §14.2), and the serialization plugin is registered in the root build.gradle.kts. Both are outside the listed Touches.
  • PBKDF2 rejects iteration counts above Int.MAX_VALUE, the JCA PBEKeySpec limit; this is documented in the Android README.
Commits 2

M4: Accept browser-compatible HTTP URLs on Android (ENG-248)
M4: Add Android crypto, protocol, API client and secure store (ENG-248)