From 1b36eba692fc2f7825748f1d79be8e1f05da03f6 Mon Sep 17 00:00:00 2001 From: null Date: Wed, 8 Jul 2026 23:20:16 -0500 Subject: [PATCH] =?UTF-8?q?docs(manual):=20batch=202=20review=20=E2=80=94?= =?UTF-8?q?=20recovery=20phrase=20'256-word=20list'=20was=20wrong=20(it's?= =?UTF-8?q?=20248)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The Recovery phrase flow said RecoveryKeyManager.generateRecoveryPhrase() draws from a 256-word list. Verified the actual list size in RecoveryKeyManager.kt: the hard-coded WORDLIST array has 248 entries (python re.findall over the array literal), and the iOS wordlist file iphone/Closer/Crypto/Resources/wordlist.txt is 247 lines (last word 'real' with no trailing newline, so 248 entries). The '256' is also wrong in the inline comment in RecoveryKeyManager.kt - a pre-existing comment bug from before the R24 iOS port (IOS_E2EE_STATUS.md notes the iOS SPEC.md originally said 256 too, corrected in 922364f). The Android side never got the same comment fix. Manual now says 248 with the source-of-truth pointers, and computes the entropy as 248^10 (a quick sanity check) so the next reader doesn't trust the wrong number on either side. Other Batch 2 claims verified clean: - All 5 Android files in the 'Key Android files' list exist. - All 2 Cloud Functions files in the 'Key Cloud Functions' list exist. - Rate limit: 1h window, 10 max, 25h TTL on invite_attempts - all match ACCEPT_RATE_LIMIT_WINDOW_MS / ACCEPT_RATE_LIMIT_MAX / ACCEPT_ATTEMPT_TTL_MS in acceptInviteCallable.ts and the fieldOverrides entry in firestore.indexes.json. - Couples doc model fields (id, userIds, inviteCode, createdAt, streakCount, lastAnsweredAt, currentQuestionId, activePackId, encryptionVersion, wrappedCoupleKey, kdfSalt, kdfParams) all match the create() payload in acceptInviteCallable.ts. createdAt uses FieldValue.serverTimestamp() (manual says 'server-side' - correct). - EncryptionVersion.STRICT = 2 in EncryptionVersion.kt. --- docs/Engineering_Reference_Manual.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/Engineering_Reference_Manual.md b/docs/Engineering_Reference_Manual.md index 36ccb6cf..cb99c593 100644 --- a/docs/Engineering_Reference_Manual.md +++ b/docs/Engineering_Reference_Manual.md @@ -350,7 +350,7 @@ This prevents brute-forcing the 6-character invite code space. The recovery phrase is the only human-readable secret in the system. It is never sent to the server in plaintext. -1. When an Android inviter creates a couple, `RecoveryKeyManager.generateRecoveryPhrase()` produces a 10-word phrase from a 256-word list. The phrase has roughly 80 bits of raw entropy; Argon2id makes brute-force infeasible. +1. When an Android inviter creates a couple, `RecoveryKeyManager.generateRecoveryPhrase()` produces a 10-word phrase from a 248-word list (the list is hard-coded as `WORDLIST` in `RecoveryKeyManager.kt`; the iOS `iphone/Closer/Crypto/Resources/wordlist.txt` is a verbatim copy with 248 entries). The phrase has roughly 80 bits of raw entropy (`248^10`); Argon2id makes brute-force infeasible. 2. The inviter encrypts the phrase with the invite code using `encryptPhraseWithCode` and stores the blob on the invite document. 3. The acceptor receives the encrypted blob, decrypts it with the same code, and stores the phrase locally. 4. The phrase is used to unwrap the couple keyset from `wrappedCoupleKey`.