Closer/IOS_E2EE_STATUS.md

204 lines
12 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# 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 18 (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:<base64>` 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 26 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 13 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 13 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.*