Closer/functions/dist
null 2c44cc6ff2 feat(crypto): couple-key rotation, phase 1 — rotate forward (Future.md security #3)
A couple-key compromise currently exposes everything, forever, because the key
never changes. This adds the rotation ceremony: a fresh AES-256-GCM key becomes
the keyset's primary while the old keys stay for reads. The keyset is a Tink
keyring and every enc:v1: blob carries its key-id internally, so all history
keeps decrypting with zero wire-format changes — none of the 25 isCiphertext
rule sites move. Phase 1 protects FUTURE content only (a stolen keyset still
contains the old key); forward secrecy for history is phase 2, which builds on
the keyGeneration plumbing laid here. The Security-screen copy says so plainly.

The ceremony (CoupleRepositoryImpl.rotateCoupleKey): read the couple fresh so
concurrent rotations collide at the rules instead of overwriting each other →
prepareRotation builds the rotated keyset and re-wraps it under the SAME phrase
(fail-closed with a typed error when this device lacks keyset or phrase; nothing
persisted anywhere) → ONE merge write lands the new wrap + a strictly-increasing
keyGeneration atomically, so the partner can never observe a bumped generation
pointing at the old wrap → only then commitRotation stores locally. A failed
server write leaves the device coherent on the old key; a crash after it
self-heals through the same adoption path as the partner.

Adoption (CoupleEncryptionManager.adoptRotationIfNeeded, hooked into Home's
healing block, synchronously before the screen settles — until the rotated
keyset is stored, new content renders locked): couple.keyGeneration ahead of the
local generation → unwrap the published wrap with the locally-stored phrase →
replace the keyset. Replaced only on success, never deleted on failure, so old
content survives anything. No phrase on this device → needsRecovery, and both
recovery flows already deliver the rotated keyset for free (phrase entry unwraps
the current wrap; partner-assist exports the current keyset). Same phrase both
sides is the entire distribution trick — no new ceremony, no partner action.

Server: onCoupleKeyRotated (couples/{id} update, pure isKeyGenerationIncrease
edge guard so streak/rhythm/re-wrap updates never fire it, and a rules-forbidden
downgrade or redelivered stale event never alerts) sends both members the 🔑
security alert through the house pipeline, bypassing quiet hours like the
restore self-alerts. The push is also functional: the partner's closed app can't
read new-key content until it next loads Home — the tap takes them there.

Rules: isUpdatingRecoveryWrap admits keyGeneration, strictly increasing
(monotonic like encryptionVersion), untouched for plain phrase re-wraps.

Tests (real Tink, mocks stop at storage): history readable after rotation + NEW
writes unreadable by the old keyset — mutation-checked by dropping setPrimary,
which kills exactly that test (a rotation that forgets setPrimary passes
everything else while protecting nothing) — same-phrase unwrap reads both eras
(the partner's whole adoption, proven), prepare persists nothing until commit,
fail-closed without phrase/keyset, adoption state machine incl. corrupt-wrap.
Android suite green, assembleDebug clean, functions 105/105, tsc clean.

Deploy (scoped): firebase deploy --only firestore:rules and
--only functions:onCoupleKeyRotated. Live verify follows deploys.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-15 02:49:50 -05:00
..
backup chore(functions): rebuild dist for the restore cleanup + alert-precision batches 2026-07-15 02:39:50 -05:00
billing fix(billing): correct RevenueCat webhook auth to HMAC-SHA256 + enable export 2026-07-09 03:43:59 -05:00
couples feat(crypto): couple-key rotation, phase 1 — rotate forward (Future.md security #3) 2026-07-15 02:49:50 -05:00
dates refactor(functions): B1 migrate 13 Firestore triggers to v2 + harden 2026-07-07 23:46:17 -05:00
games refactor(functions): B6c split game part-finished trigger + reorder reads (#11) 2026-07-08 00:08:18 -05:00
notifications chore(functions): rebuild dist for the restore cleanup + alert-precision batches 2026-07-15 02:39:50 -05:00
questions chore(functions): rebuild dist for the restore cleanup + alert-precision batches 2026-07-15 02:39:50 -05:00
releaseKey refactor(functions): B3 migrate 12 callables to v2 + harden 2026-07-07 23:58:32 -05:00
security refactor(functions): B3 migrate 12 callables to v2 + harden 2026-07-07 23:58:32 -05:00
users refactor(functions): B4+B5 migrate webhook to v2 + pin onUserDelete on v1 2026-07-08 00:00:29 -05:00
index.js feat(crypto): couple-key rotation, phase 1 — rotate forward (Future.md security #3) 2026-07-15 02:49:50 -05:00
index.js.map feat(crypto): couple-key rotation, phase 1 — rotate forward (Future.md security #3) 2026-07-15 02:49:50 -05:00
log.js build(functions): B0 v2 foundation — global options, shared push helper, structured logging 2026-07-07 23:36:16 -05:00
log.js.map build(functions): B0 v2 foundation — global options, shared push helper, structured logging 2026-07-07 23:36:16 -05:00
options.js fix(functions): gcf_gen1 fractional CPU to fit the Cloud Run CPU quota 2026-07-08 03:04:26 -05:00
options.js.map fix(functions): gcf_gen1 fractional CPU to fit the Cloud Run CPU quota 2026-07-08 03:04:26 -05:00