204 lines
12 KiB
Markdown
204 lines
12 KiB
Markdown
# 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:<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 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.* |