Closer/IOS_E2EE_STATUS.md

12 KiB
Raw Blame History

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 faac40a763ca0c). 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. faac40a3d32098
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. faac40aade4667
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. 922364f60c0003

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:

# 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:

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:

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:

./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

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.