feat(tools+docs): add androidTest deps for paired-CI vector harness; add SCHEMA_VERSION_DECISION + SPEC §19 pre-deploy checklist (iOS E2EE Batch 8)
This commit is contained in:
parent
2aaeef3d45
commit
763ca0c7cb
|
|
@ -216,4 +216,8 @@ dependencies {
|
|||
testImplementation("junit:junit:4.13.2")
|
||||
testImplementation("org.jetbrains.kotlinx:kotlinx-coroutines-test:1.9.0")
|
||||
testImplementation("io.mockk:mockk:1.13.14")
|
||||
|
||||
// Canonical-vector capture harness (paired-CI for iOS↔Android E2EE fixture fill)
|
||||
androidTestImplementation("androidx.test.ext:junit:1.1.5")
|
||||
androidTestImplementation("androidx.test:runner:1.5.2")
|
||||
}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,86 @@
|
|||
# SchemaVersion Decision Document — Closer E2EE Answers
|
||||
|
||||
> Scope: daily-question answers (`answers/{userId}` under `couples/{coupleId}/daily_question/{date}`).
|
||||
> Status: decision record. No source-code changes in this batch.
|
||||
|
||||
---
|
||||
|
||||
## 1. Status quo (current behavior)
|
||||
|
||||
SchemaVersion 2 is the daily-answer default on both Android and iOS today. The payload is encrypted with the shared couple key and written to the read-gated `secure/payload` subdoc as `enc:v1:<base64( IV || ciphertext || tag )>`. The AAD is the `coupleId` UTF-8 bytes (Android `FieldEncryptor.kt`). Both partners already hold the couple key, so decryption is immediate once both have answered; the "do not reveal until both answer" rule is enforced by the Firestore read rule on the `secure` subdoc, not by cryptography.
|
||||
|
||||
SchemaVersion 3 (sealed / partner-proof) is fully implemented in code on both platforms but is used only for thread answers and the legacy sealed-answer path. It uses a per-answer one-time AES-256-GCM key, a SHA-256 commitment over canonical JSON, and AAD `"coupleId|questionId|userId"`. The one-time key is wrapped for the partner via a `keybox:v1:` envelope and released only after both partners have submitted.
|
||||
|
||||
iOS-side parity: schemaVersion 2 works end-to-end after Batch 5's AAD fix (`AnswerCrypto.swift` now uses `coupleId` as AAD). SchemaVersion 3 primitives are implemented in `SealedAnswer.swift` (Batch 4) and the Cloud Function `wrapReleaseKeyCallable` exists to let iOS release a Tink-compatible keybox to Android, but schemaVersion 3 is not wired into the iOS daily-answer path.
|
||||
|
||||
---
|
||||
|
||||
## 2. What schemaVersion 3 adds over schemaVersion 2
|
||||
|
||||
- **Partner-proof cryptography**: neither partner can decrypt the other's answer until both have submitted and released their one-time keys. With schemaVersion 2, either partner could read the other's answer at any time because both hold the couple key; the "until both answer" rule is a Firestore rule, not a cryptographic guarantee.
|
||||
- **Cryptographic commitment**: the SHA-256 hash binds the answer plaintext to its content before reveal. SchemaVersion 2 has no such commitment — a malicious server with write access could substitute ciphertext without detection. On reveal, `SealedAnswerCrypto.verifyCommitment` can surface a tamper warning if the decrypted plaintext does not match the stored commitment.
|
||||
- **Audit trail on reveal**: the commitment is recomputed from the decrypted plaintext and compared to the stored value. A mismatch is a hard signal of tampering or cross-platform canonical-JSON drift.
|
||||
- **Cost**: one extra round-trip per daily answer (`commitment → wait for partner → release key → decrypt`), plus one more key exchange. The one-time AES-256 key must be wrapped for the partner via `wrapReleaseKeyCallable` (Cloud Function) or a local ECIES keybox exchange.
|
||||
|
||||
---
|
||||
|
||||
## 3. Decision options
|
||||
|
||||
| Option | Daily default | Sealed path | Migration cost | Recommendation |
|
||||
|---|---|---|---|---|
|
||||
| **A. Stay at schemaVersion 2** | yes | threads / legacy only | none | **Ship with this.** Keeps daily answers simple, fast, and cross-platform today. |
|
||||
| **B. Promote schemaVersion 3 to daily** | yes, for new couples only | threads / legacy + daily | medium | Best long-term privacy. Requires `PendingAnswerKeyStore` parity, two-sided release wiring in `AnswerRevealViewModel`, `wrapReleaseKeyCallable` deployed, and paired-CI vector validation before promotion. |
|
||||
| **C. Add a user toggle** | user choice | per-question opt-in | high — UI + state migration | Not recommended. Couples should operate under the same privacy model; mixed per-question modes are confusing and error-prone. |
|
||||
|
||||
---
|
||||
|
||||
## 4. Recommendation
|
||||
|
||||
**Ship Batch 1 with Option A: stay at schemaVersion 2 as the daily-answer default.**
|
||||
|
||||
SchemaVersion 2 is code-complete, cross-platform, and requires no new Cloud Function deployment. It gives couples the same day-to-day experience as the Android app today. The single-device limitation (new iOS device needs the recovery phrase) is the only meaningful operational change, and it is already surfaced in the UI.
|
||||
|
||||
**Plan Batch 2 for Option B: promote schemaVersion 3 to the daily default for new couples only.**
|
||||
|
||||
Promotion should happen only after:
|
||||
1. The paired-CI vector run fills `TODO_ANDROID_RUN` placeholders in `sealed_answer_canonical_fixtures.json` and `argon2id_canonical_fixtures.json`.
|
||||
2. `wrapReleaseKeyCallable` is deployed and verified end-to-end (iOS inviter → Android acceptor and vice versa).
|
||||
3. `PendingAnswerKeyStore` parity is audited between iOS and Android.
|
||||
4. `AnswerRevealViewModel` is extended to handle the full two-sided release flow for daily questions.
|
||||
|
||||
**Migration path for mixed-version couples:** keep `schemaVersion` in every answer doc. An older answer (schemaVersion 2) decrypts with the couple key; a newer answer (schemaVersion 3) decrypts with its one-time released key. The read path must branch on `schemaVersion`. No per-couple migration is required because the version is per-document.
|
||||
|
||||
---
|
||||
|
||||
## 5. Risks of promotion
|
||||
|
||||
- **Cloud Function dependency**: `wrapReleaseKeyCallable` must be deployed before schemaVersion 3 can ship. Today it is exported from `functions/src/index.ts` but not deployed. Without it, iOS cannot release a Tink-compatible keybox to an Android partner.
|
||||
- **Key-store parity**: the `PendingAnswerKeyStore` is referenced in iOS code and implemented on Android (`PendingAnswerKeyStore.kt`). If the iOS equivalent is not wired to survive app restart, partner answers cannot be released across sessions.
|
||||
- **Latency increase**: daily-answer reveal moves from "encrypt + write" to "commit + write + wait for partner + release key + decrypt". Expect 1-3 extra Firestore round-trips per daily answer, depending on partner timing.
|
||||
- **Canonical-JSON byte-stability**: the Batch 5 fixture test (`SealedAnswerCryptoTests.testCanonicalJSONByteStability`) MUST pass cross-platform before promotion. If iOS and Android canonical JSON diverge by even one byte, commitment hashes diverge and reveals fail with tamper warnings.
|
||||
- **Rollback surface**: if schemaVersion 3 is promoted and then rolled back, old schemaVersion 3 docs still need a release-key path. Rolling back to schemaVersion 2-only daily answers is safe only for new docs going forward; existing schemaVersion 3 docs retain their old decryption requirements.
|
||||
|
||||
---
|
||||
|
||||
## 6. Next actions to unblock Option B promotion
|
||||
|
||||
1. Run `scripts/capture_android_canonical_vectors.sh` on a paired macOS host with an Android emulator and iOS simulator to fill `TODO_ANDROID_RUN` placeholders in the fixture JSON files.
|
||||
2. Deploy `wrapReleaseKeyCallable` via `firebase deploy --only functions:wrapReleaseKeyCallable` to staging and production.
|
||||
3. Audit `PendingAnswerKeyStore` parity between iOS and Android; ensure the iOS one-time key store survives app restart and is keyed consistently.
|
||||
4. Add a schemaVersion-aware answer-read path in both clients that handles mixed-version couples without silent fallback.
|
||||
5. Extend `AnswerRevealViewModel` (iOS) and `AnswerRevealViewModel` (Android) to perform the two-sided release handshake for daily schemaVersion 3 answers.
|
||||
6. Document the user-visible latency change and the "lost local key = unrecoverable answer" behavior for QA.
|
||||
|
||||
---
|
||||
|
||||
## 7. Open questions
|
||||
|
||||
1. **Scope of schemaVersion choice**: per-couple default, or per-question override? Recommendation: per-couple default first; add per-question override only if product strongly wants it.
|
||||
2. **Partner-answer timeout**: what is the wait timeout before a schemaVersion 3 answer is considered "stuck"? No explicit timeout is currently implemented in iOS code.
|
||||
3. **Commitment input scope**: should the commitment hash include metadata (e.g. timestamp) or only the plaintext answer content? Currently it includes only `v1|coupleId|questionId|userId|canonicalJson`.
|
||||
4. **Keybox format for iOS↔iOS**: should iOS continue to use its native Path A envelope, or should all iOS releases also go through `wrapReleaseKeyCallable` for consistency? Native Path A is simpler; server wrapping is needed only for Android recipients.
|
||||
5. **Release-key deletion policy**: after both partners reveal, should the `releaseKeys/{recipientId}` subdoc be deleted to reduce Firestore storage? If deleted, re-reveal on another device would fail.
|
||||
|
||||
---
|
||||
|
||||
*Document written by Neo (subagent) — 2026-06-28. Sources: `iphone/Closer/Crypto/SPEC.md` §10, `SealedAnswer.swift`, `AnswerCrypto.swift`, Android `FirestoreAnswerDataSource.kt`, `SealedAnswerEncryptor.kt`, `PendingAnswerKeyStore.kt`, `AnswerRevealViewModel.kt`.*
|
||||
|
|
@ -554,6 +554,45 @@ The most likely silent break is the canonical JSON contract. `SealedAnswerCrypto
|
|||
|
||||
Correction 2026-06-28: actual Android wordlist size is 248, not 256. Bundled resource reflects the live Android source. Cross-platform recovery remains byte-identical because we copy the list verbatim.
|
||||
|
||||
---
|
||||
|
||||
## 19. Pre-deploy checklist for iOS E2EE parity
|
||||
|
||||
### 19.1 Before deploy
|
||||
|
||||
Before merging the iOS E2EE branch to `main`, all of the following must be true:
|
||||
|
||||
- [ ] All Android↔iOS unit tests pass on a macOS host. (Currently blocked in this Linux environment; run on a Mac or in CI.)
|
||||
- [ ] Paired-CI vector run fills `TODO_ANDROID_RUN` placeholders in:
|
||||
- `iphone/Closer/Crypto/Resources/sealed_answer_canonical_fixtures.json`
|
||||
- `iphone/Closer/Crypto/Resources/argon2id_canonical_fixtures.json`
|
||||
- [ ] `wrapReleaseKeyCallable` is deployed to the staging Firebase project.
|
||||
- [ ] `wrapReleaseKeyCallable` is verified end-to-end with iOS as inviter and Android as acceptor (or vice versa).
|
||||
- [ ] Manual test: iOS user creates an invite, Android user accepts, and both partners can answer and reveal a daily question.
|
||||
- [ ] Manual test: iOS user creates a thread answer (schemaVersion 3) and an Android user receives and decrypts it.
|
||||
- [ ] `app/build.gradle.kts` contains the `androidTestImplementation` deps required for `connectedAndroidTest`.
|
||||
- [ ] Schema-version promotion decision is recorded in `SCHEMA_VERSION_DECISION.md` and signed off by product.
|
||||
|
||||
### 19.2 Risks during deploy
|
||||
|
||||
- **Multi-device limitation**: 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 UI but may surprise testers during QA.
|
||||
- **Recovery phrase irreversibility**: entering the wrong recovery phrase permanently locks the couple key. Make this explicit in the QA briefing and any public-facing copy review.
|
||||
- **Cloud Function dependency**: iOS→Android sealed-answer release depends on `wrapReleaseKeyCallable`. If deploy order is wrong, iOS users cannot reveal schemaVersion 3 answers to Android partners.
|
||||
- **Canonical-JSON drift**: any last-minute change to the sealed-answer JSON shape or escape rules will break commitment hashes cross-platform. Treat sealed-answer JSON as a wire contract.
|
||||
|
||||
### 19.3 Rollback plan
|
||||
|
||||
If iOS E2EE needs to be reverted after merge or release:
|
||||
|
||||
1. Delete the Cloud Function `wrapReleaseKeyCallable` via `firebase functions:delete wrapReleaseKeyCallable` if it is causing errors.
|
||||
2. iOS falls back to schemaVersion 2 by rolling back calls to `FirestoreService.wrapReleaseKeyForPartner` in the daily-answer path. SchemaVersion 2 answers already decrypt end-to-end today.
|
||||
3. No data migration is required: `schemaVersion` is stored per answer document, so older schemaVersion 2 answers continue to decrypt with the couple key regardless of the default for new answers.
|
||||
|
||||
---
|
||||
|
||||
*Pre-deploy checklist written by Neo (subagent) — 2026-06-28.*
|
||||
|
||||
|
||||
---
|
||||
|
||||
## 18. Batch 5 implementation status + cross-platform fixture workflow
|
||||
|
|
|
|||
Loading…
Reference in New Issue