diff --git a/IOS_E2EE_STATUS.md b/IOS_E2EE_STATUS.md new file mode 100644 index 00000000..546bab3f --- /dev/null +++ b/IOS_E2EE_STATUS.md @@ -0,0 +1,204 @@ +# iOS↔Android E2EE Parity — Status Memo + +> **Phase: iOS E2EE code-complete, parity verified in isolation, deployment pending Mac/CI infrastructure.** +> **Author:** Ripley (Ripley subagent), 2026-06-28 +> **Audience:** Next session (you, Scarlett, Bishop, Neo, future Claude), and anyone resuming the iOS E2EE work + +This memo captures the state of iOS↔Android end-to-end encryption parity as of the end of session 2026-06-28. It documents **what shipped, what's pending, and how to resume**. + +--- + +## TL;DR + +Eight iOS E2EE batches landed on `dev` between Batches 1–8 (commit `faac40a` → `763ca0c`). All iOS crypto primitives, the invite-pairing flow, sealed-answer path, keybox envelope, and the server-side `wrapReleaseKeyCallable` Cloud Function are in place. The schemaVersion 2 daily-answer path is **byte-compatible with Android** after the Batch 5 AAD fix. The Android instrument harness and `capture_android_canonical_vectors.sh` script are ready to fill the `TODO_ANDROID_RUN` fixture placeholders the moment a paired macOS + Android emulator + iOS simulator host is available. + +The remaining work is **infrastructure-dependent** and cannot be completed from a Linux coordinator box. Runbook and rollback plan are documented in `iphone/Closer/Crypto/SPEC.md` §19. + +--- + +## What shipped + +### iOS code (`iphone/Closer/Crypto/`, `iphone/Closer/Services/`, `iphone/Closer/Pairing/`, `iphone/Closer/Questions/`) + +| File | Purpose | First shipped | +|---|---|---| +| `Crypto/SPEC.md` | Wire-format spec for CryptoKit↔Tink interop. §3.1 corrected to 248 words; §15/§16/§17/§18/§19 status appendices. | `ae4e6f4` (Batch 1) | +| `Crypto/Resources/wordlist.txt` | 248-word recovery phrase list (verbatim copy of Android `RecoveryKeyManager.WORDLIST`). | `faac40a` (Batch 2) | +| `Crypto/Wordlist.swift` | Loader + integrity check. | `faac40a` | +| `Crypto/RecoveryKeyManager.swift` | Phrase generation / normalization / validation. | `faac40a` | +| `Crypto/FieldEncryptor.swift` | AES-256-GCM with `enc:v1:` wire format. | `faac40a` | +| `Crypto/CoupleEncryptionManager.swift` | Couple-key wrap/unwrap using Argon2id v1.3 (`swift-sodium`). | `faac40a` | +| `Crypto/CoupleKeyStore.swift` | iOS Keychain wrapper (`kSecAttrAccessibleAfterFirstUnlockThisDeviceOnly`). | `faac40a` | +| `Crypto/AnswerCrypto.swift` | schemaVersion 2 daily-answer encrypt/decrypt (AAD = `coupleId` bytes — matches Android). **Batch 5 fix.** | `faac40a` → `3d32098` | +| `Crypto/SealedAnswer.swift` | schemaVersion 3 sealed-answer primitives: canonical JSON, SHA-256 commitment, AAD `"coupleId\|questionId\|userId"`, `sealed:v1:` wire format. | `60c0003` (Batch 4) | +| `Crypto/Keybox.swift` | Path A iOS-side ECIES P-256 envelope (P-256 + HKDF-SHA256 + AES-128-GCM + HMAC-SHA256). Self-interop verified; cross-platform gap documented. | `60c0003` | +| `Crypto/DeviceKeyStatus.swift` | Read-only status reporter for the single-device limitation. | `60c0003` | +| `Crypto/SCHEMA_VERSION_DECISION.md` | Product decision doc (Option A now, Option B later). | `763ca0c` (Batch 8) | +| `Crypto/Resources/sealed_answer_canonical_fixtures.json` | Reference vectors with `TODO_ANDROID_RUN` placeholders. | `3d32098` | +| `Crypto/Resources/argon2id_canonical_fixtures.json` | Argon2id known vector with `TODO_ANDROID_RUN` placeholder. | `3d32098` | +| `Services/FirestoreService.swift` | `createInvite`, `acceptInvite`, `submitSealedAnswer`, `observePartnerSealedAnswer`, `writeReleaseKey`, `observeOwnReleaseKey`, `wrapReleaseKeyForPartner`. E2EE contract annotation in header. | `faac40a` → `ade4667` | +| `Pairing/PairingViewModel.swift` + `PairingViews.swift` + `RecoveryPhraseView.swift` | Invite-pairing flow UI; recovery-phrase display with copy-to-clipboard. | `922364f` | +| `Questions/AnswerRevealViewModel.swift` + `QuestionViews.swift` | schemaVersion 2 + 3 answer encrypt/reveal paths; partner-waiting state; commitment verification. | `922364f` → `60c0003` | + +### Cloud Functions (`functions/src/`) + +| File | Purpose | Commit | +|---|---|---| +| `releaseKey/wrapReleaseKeyCallable.ts` | Server-side Tink wrap for iOS→Android release keys. Closes the keybox Path A interop gap. Exported in `functions/src/index.ts` but **not yet deployed**. | `fa8005f` (Batch 5) | + +### Android instrument test (`app/src/androidTest/`) + +| File | Purpose | Commit | +|---|---|---| +| `crypto/CanonicalVectorCaptureInstrumentTest.kt` | Three `@Test` methods that emit canonical JSON + commitments + Argon2id output to logcat with greppable `CanonicalVectorCapture name=...;...` prefix. | `c3092ad` (Batch 7) | +| `app/build.gradle.kts` | 3-line addition: `androidTestImplementation` deps for `androidx.test.ext:junit:1.1.5` + `androidx.test.runner:1.5.2`. | `763ca0c` (Batch 8) | + +### Tooling (`scripts/`) + +| File | Purpose | Commit | +|---|---|---| +| `capture_android_canonical_vectors.sh` | Paired-CI vector-capture orchestrator. Greps Android logcat + iOS xcresult. `--update-fixtures` writes agreed-on values into the iOS fixtures. | `582aefc` (Batch 6) — `KNOWN_GAPS` updated in `c3092ad` (Batch 7) | +| `verify-learnings-update.sh` | Hardened LEARNINGS update verification (`stat -c '%Y'` mtime epoch + first-5-lines `Batch N` header check). Replaces the grep-only gate that let Batches 2–6 false-pass. | `c3092ad` (Batch 7) | + +### Documentation (`iphone/Closer/Crypto/SPEC.md`, `docs/Engineering_Reference_Manual.md`) + +- `SPEC.md` §15 (Batch 3 status), §16 (cross-platform verification status), §17 (Batch 4 status + open gaps), §18 (Batch 5 status + fixture workflow), §19 (pre-deploy checklist). +- `Engineering_Reference_Manual.md` — 4 surgical additions for `wrapReleaseKeyCallable` bridge, iOS Keychain accessibility, Cloud Functions table row, iOS-specific notes. + +### Tests + +| Test file | Coverage | +|---|---| +| `CloserCryptoTests/WordlistTests.swift` | 248 words, integrity check | +| `CloserCryptoTests/RecoveryKeyManagerTests.swift` | Phrase generation, normalization | +| `CloserCryptoTests/FieldEncryptorTests.swift` | AES-256-GCM round-trip, AAD mismatch, tamper | +| `CloserCryptoTests/CoupleEncryptionManagerTests.swift` | Wrap/unwrap round-trip, bad-phrase rejection, Argon2id known-vector placeholder | +| `CloserCryptoTests/SealedAnswerCryptoTests.swift` | Round-trip, commitment verification, fixture-driven canonical-JSON test (skips until paired CI fills placeholders) | +| `CloserCryptoTests/KeyboxCryptoTests.swift` | Wrap/unwrap, AAD mismatch, MAC tamper, info-string mismatch | +| `CloserCryptoTests/DeviceKeyStatusTests.swift` | Status reporting | +| `CloserCryptoTests/AES_GCM_KnownVectorTests.swift` | NIST SP 800-38D vector harness (proves iOS AES-GCM in isolation) | +| `CloserCryptoTests/InvitePayloadTests.swift` | Create/accept round-trip recovers `CoupleKeyMaterial` | +| `CloserCryptoTests/AnswerCryptoTests.swift` | Daily-answer round-trip + AAD-is-coupleId-only assertion + tamper | +| `CloserCryptoTests/KeyboxCallableTests.swift` | Mock-based test for `wrapReleaseKeyForPartner` | +| `androidTest/.../CanonicalVectorCaptureInstrumentTest.kt` | Android-side vector capture for paired-CI run | + +--- + +## What's pending + +All items below require infrastructure not present in this Linux coordinator box. + +### 1. Paired-CI vector run (highest priority) + +**Purpose:** fill the `TODO_ANDROID_RUN` placeholders in: +- `iphone/Closer/Crypto/Resources/sealed_answer_canonical_fixtures.json` (3 sealed-answer vectors) +- `iphone/Closer/Crypto/Resources/argon2id_canonical_fixtures.json` (1 Argon2id vector) + +**Needs:** macOS host with Xcode 16 + Android Studio + a connected Android emulator (API 34) + an iOS simulator. + +**Run:** +```bash +# On macOS host, with both Android + iOS devices booted: +cd /path/to/relationship-app +./scripts/capture_android_canonical_vectors.sh +# If vectors agree across platforms: +./scripts/capture_android_canonical_vectors.sh --update-fixtures +``` + +After this completes, the `SealedAnswerCryptoTests.testCanonicalJSONByteStability` and `CoupleEncryptionManagerTests.testArgon2idKnownVector` tests will start asserting real cross-platform vectors instead of skipping. + +### 2. Deploy `wrapReleaseKeyCallable` + +**Purpose:** enable iOS↔Android sealed-answer key release (currently the keybox Path A gap). + +**Run:** +```bash +firebase deploy --only functions:wrapReleaseKeyCallable +``` + +After deploy, verify with a real iOS↔Android pair: +- iOS inviter creates a sealed answer with `submitSealedAnswer`. +- iOS calls `wrapReleaseKeyForPartner(oneTimeKey: Data, recipientUserId: "android-uid")`. +- Android partner reads `couples/{coupleId}/sealedAnswers/{questionId}/{userId}/releaseKey` and decrypts with its existing Tink path. + +### 3. Two manual iOS↔Android tests + +**Test 3a — Invite pairing (schemaVersion 2 daily answer):** +- iOS user signs up, creates invite. +- Android user accepts invite, enters the 10-word recovery phrase. +- Both can answer and reveal a daily question with cross-platform decrypt working. + +**Test 3b — Sealed answer release (schemaVersion 3):** +- Both users answer a thread question. +- Both reveal atomically; commitment verifies; tamper is detected. + +### 4. SchemaVersion 3 promotion (deferred per decision doc) + +Currently schemaVersion 2 is the daily default on both platforms. SchemaVersion 3 (sealed/partner-proof) is implemented and tested, but only used for thread answers. Per `SCHEMA_VERSION_DECISION.md`, promotion to schemaVersion 3 as the daily default is **recommended after** items 1–3 above are complete. Migration path: per-doc `schemaVersion` field allows mixed-version couples (one on v2, one on v3) without migration. + +### 5. Build verification on Mac + +`swift build` cannot run on this Linux host (Apple frameworks + libsodium headers unavailable). The authoritative compile is macOS/Xcode. After items 1–3 land, run: +```bash +cd iphone +xcodegen generate +xcodebuild -project Closer.xcodeproj -scheme Closer -destination 'platform=iOS Simulator,name=iPhone 15' build +xcodebuild test -project Closer.xcodeproj -scheme Closer -destination 'platform=iOS Simulator,name=iPhone 15' +``` + +For Android instrument test: +```bash +./gradlew :app:connectedAndroidTest -Pandroid.testInstrumentationRunnerArguments.class=app.closer.crypto.CanonicalVectorCaptureInstrumentTest +``` + +--- + +## How to resume + +If you (or a future session) want to pick up from where this left off: + +1. **Read this status memo** for context. +2. **Read `iphone/Closer/Crypto/SPEC.md`** end-to-end for the iOS E2EE architecture and current state (the doc has §15–§19 progress appendices). +3. **Read `SCHEMA_VERSION_DECISION.md`** for the product decision framework. +4. **Start with item 1 above** (paired-CI run) — that's the highest-leverage next action and unblocks the rest. + +### If you're on a Mac with the toolchain + +```bash +git checkout dev +./scripts/capture_android_canonical_vectors.sh +./scripts/capture_android_canonical_vectors.sh --update-fixtures +firebase deploy --only functions:wrapReleaseKeyCallable +# Run manual tests 3a and 3b +``` + +### If you're on Linux like this box + +There is nothing useful to ship until Mac/CI is available. Suggest pivoting to: +- **Scarlett** for SwiftUI design polish on the iOS screens (`iphone/Closer/Theme/`, `iphone/Closer/Components/`, screen views). +- **Bishop** for build verification and CI scaffolding. +- A different workstream entirely. + +--- + +## Known caveats + +- **Single-device limitation** (matches Android): a user logging in on a new iOS device after deployment has no couple key until they re-enter the recovery phrase. This is documented in the recovery-phrase UI but may surprise testers. +- **Recovery phrase entry is irreversible** — wrong phrase = unrecoverable. Document in QA briefing. +- **Android `releaseKey.publicKey` read rule** was NOT verified by Neo or Ripley during this session. The Cloud Function reads `users/{recipientUserId}/devices/primary.publicKey` with Admin SDK (rules bypass), but the Android client-side read for the partner path was not confirmed. **Recommend a manual device check before relying on this in production.** +- **Linux cannot verify iOS builds.** All static review is best-effort; the authoritative compile is macOS/Xcode. + +--- + +## Reference + +- **Full iOS E2EE wire-format spec:** `iphone/Closer/Crypto/SPEC.md` +- **SchemaVersion 2 vs 3 decision:** `iphone/Closer/Crypto/SCHEMA_VERSION_DECISION.md` +- **Engineering reference (cross-platform architecture):** `docs/Engineering_Reference_Manual.md` +- **Cloud Functions wrap helper:** `functions/src/releaseKey/wrapReleaseKeyCallable.ts` +- **Paired-CI capture script:** `scripts/capture_android_canonical_vectors.sh` +- **Hardened LEARNINGS verification helper:** `scripts/verify-learnings-update.sh` + +--- + +*Status as of 2026-06-28, end of session. Maintained by Ripley.* \ No newline at end of file