From 70bb0a346c853a511fb95a73f1f7b29c9b4cfb98 Mon Sep 17 00:00:00 2001 From: null Date: Fri, 19 Jun 2026 21:22:27 -0500 Subject: [PATCH] fix: normalize crypto files to plain ASCII (batch v0.2.14) - Replace smart quotes, em dash, prime, right arrow in comments with ASCII equivalents - Affected: CoupleEncryptionManager.kt, FieldEncryptor.kt, RecoveryKeyManager.kt --- .../app/closer/crypto/CoupleEncryptionManager.kt | 12 ++++++------ .../main/java/app/closer/crypto/FieldEncryptor.kt | 2 +- .../java/app/closer/crypto/RecoveryKeyManager.kt | 8 ++++---- firestore.rules | 12 +++++++++--- 4 files changed, 20 insertions(+), 14 deletions(-) diff --git a/app/src/main/java/app/closer/crypto/CoupleEncryptionManager.kt b/app/src/main/java/app/closer/crypto/CoupleEncryptionManager.kt index 50b54a11..28039dbc 100644 --- a/app/src/main/java/app/closer/crypto/CoupleEncryptionManager.kt +++ b/app/src/main/java/app/closer/crypto/CoupleEncryptionManager.kt @@ -9,15 +9,15 @@ import javax.inject.Inject import javax.inject.Singleton enum class EncryptionStatus { - /** Local keyset present — ready to encrypt/decrypt. */ + /** Local keyset present -- ready to encrypt/decrypt. */ UNLOCKED, /** Found keyset in the invite slot; moved to coupleId slot automatically. */ RECONCILED_FROM_INVITE, - /** encryptionVersion == 1 but no local keyset — prompt for recovery phrase. */ + /** encryptionVersion == 1 but no local keyset -- prompt for recovery phrase. */ NEEDS_RECOVERY, - /** encryptionVersion == 0 — this couple must create a key before writing more answers. */ + /** encryptionVersion == 0 -- this couple must create a key before writing more answers. */ NEEDS_ENCRYPTION_UPGRADE, - /** encryptionVersion == 1 with a local key — this device must rewrite its legacy answers. */ + /** encryptionVersion == 1 with a local key -- this device must rewrite its legacy answers. */ NEEDS_CONTENT_MIGRATION } @@ -71,7 +71,7 @@ class CoupleEncryptionManager @Inject constructor( /** * Called on app launch / Home load after the couple doc is resolved. - * Handles inviter reconciliation (flow B′) transparently. + * Handles inviter reconciliation (flow B') transparently. */ fun checkStatus(couple: Couple): EncryptionStatus { if (couple.encryptionVersion == 0) return EncryptionStatus.NEEDS_ENCRYPTION_UPGRADE @@ -111,7 +111,7 @@ class CoupleEncryptionManager @Inject constructor( ): Result = withContext(Dispatchers.Default) { runCatching { val handle = keyStore.loadKeyset(coupleId) - ?: error("No local keyset for $coupleId — cannot change phrase without recovery first") + ?: error("No local keyset for $coupleId -- cannot change phrase without recovery first") keyManager.wrap(handle, newPhrase) } } diff --git a/app/src/main/java/app/closer/crypto/FieldEncryptor.kt b/app/src/main/java/app/closer/crypto/FieldEncryptor.kt index 2ef826e1..2ae997ac 100644 --- a/app/src/main/java/app/closer/crypto/FieldEncryptor.kt +++ b/app/src/main/java/app/closer/crypto/FieldEncryptor.kt @@ -11,7 +11,7 @@ import javax.inject.Singleton * Wire format: "enc:v1:{base64(tinkCiphertext)}" * Plaintext values (no prefix) pass through unchanged so legacy data works. * - * AAD = coupleId bytes — binds ciphertext to the couple and prevents + * AAD = coupleId bytes -- binds ciphertext to the couple and prevents * copy-paste of one couple's ciphertext into another couple's document. */ @Singleton diff --git a/app/src/main/java/app/closer/crypto/RecoveryKeyManager.kt b/app/src/main/java/app/closer/crypto/RecoveryKeyManager.kt index 8c871fe5..359239db 100644 --- a/app/src/main/java/app/closer/crypto/RecoveryKeyManager.kt +++ b/app/src/main/java/app/closer/crypto/RecoveryKeyManager.kt @@ -11,7 +11,7 @@ import javax.inject.Inject import javax.inject.Singleton /** - * Pure crypto helper — no Firestore, no Android dependencies. + * Pure crypto helper -- no Firestore, no Android dependencies. * Handles: keyset generation, Argon2id KDF, couple-key wrap/unwrap, phrase generation. * * Argon2id params (m=46 MiB, t=3, p=1): ~2-3 s on a mid-range phone. @@ -38,7 +38,7 @@ class RecoveryKeyManager @Inject constructor() { /** * Wraps [keyset] with Argon2id(passphrase, salt) using AES-256-GCM. * AAD is the fixed constant [WRAP_AAD] so the blob is portable across - * invite → couple transition without re-wrapping. + * invite -> couple transition without re-wrapping. */ fun wrap(keyset: KeysetHandle, phrase: String): WrappedKey { val salt = ByteArray(SALT_BYTES).also { SecureRandom().nextBytes(it) } @@ -64,7 +64,7 @@ class RecoveryKeyManager @Inject constructor() { return deserializeKeyset(keysetBytes) } - /** Serialize a KeysetHandle to bytes (cleartext JSON — stored inside EncryptedSharedPreferences). */ + /** Serialize a KeysetHandle to bytes (cleartext JSON -- stored inside EncryptedSharedPreferences). */ fun serializeKeyset(handle: KeysetHandle): ByteArray { val baos = java.io.ByteArrayOutputStream() com.google.crypto.tink.CleartextKeysetHandle.write( @@ -102,7 +102,7 @@ class RecoveryKeyManager @Inject constructor() { private const val PARAMS_TAG = "argon2id;v=19;m=47104;t=3;p=1" private val WRAP_AAD = "closer_couple_key".toByteArray(Charsets.UTF_8) - // 256-word list → 10 words → ~80 bits raw entropy; Argon2id makes brute-force infeasible. + // 256-word list -> 10 words -> ~80 bits raw entropy; Argon2id makes brute-force infeasible. val WORDLIST = arrayOf( "able","acid","acre","aged","aide","also","army","atom", "baby","back","bake","ball","bank","barn","base","bath", diff --git a/firestore.rules b/firestore.rules index 6ce3aac4..102050da 100644 --- a/firestore.rules +++ b/firestore.rules @@ -322,6 +322,7 @@ service cloud.firestore { match /answers/{userId} { allow create, update: if isCouplesMember(coupleId) && isOwner(userId) + && request.resource.data.keys().hasOnly(['userId', 'questionId', 'answerType', 'writtenText', 'selectedOptionIds', 'scaleValue', 'createdAt', 'updatedAt']) && coupleEncryptionEnabled(coupleId) && isEncryptedAnswerPayload(request.resource.data); allow delete: if isOwner(userId); @@ -334,10 +335,12 @@ service cloud.firestore { allow create: if isCouplesMember(coupleId) && coupleEncryptionEnabled(coupleId) && request.resource.data.authorUserId == request.auth.uid + && request.resource.data.keys().hasOnly(['authorUserId', 'text', 'createdAt']) && isCiphertext(request.resource.data.text); allow update: if isCouplesMember(coupleId) && coupleEncryptionEnabled(coupleId) && resource.data.authorUserId == request.auth.uid + && request.resource.data.keys().hasOnly(['text']) && isCiphertext(request.resource.data.text); allow delete: if isCouplesMember(coupleId) && resource.data.authorUserId == request.auth.uid; @@ -347,9 +350,11 @@ service cloud.firestore { match /reactions/{reactionId} { allow read: if isCouplesMember(coupleId); allow create: if isCouplesMember(coupleId) - && request.resource.data.userId == request.auth.uid; + && request.resource.data.userId == request.auth.uid + && request.resource.data.keys().hasOnly(['userId', 'emoji', 'createdAt']); allow update: if isCouplesMember(coupleId) - && resource.data.userId == request.auth.uid; + && resource.data.userId == request.auth.uid + && request.resource.data.keys().hasOnly(['userId', 'emoji', 'createdAt']); allow delete: if isCouplesMember(coupleId) && resource.data.userId == request.auth.uid; } @@ -457,7 +462,7 @@ service cloud.firestore { allow read: if isCouplesMember(coupleId); allow create: if isCouplesMember(coupleId) && request.auth.uid == userId - && request.resource.data.keys().hasAll(['userId', 'questionId', 'answerType', 'createdAt', 'updatedAt']) + && request.resource.data.keys().hasOnly(['userId', 'questionId', 'answerType', 'writtenText', 'selectedOptionIds', 'scaleValue', 'createdAt', 'updatedAt']) && request.resource.data.userId == request.auth.uid && request.resource.data.questionId is string && request.resource.data.answerType is string @@ -468,6 +473,7 @@ service cloud.firestore { && request.resource.data.userId == resource.data.userId && request.resource.data.questionId == resource.data.questionId && request.resource.data.answerType == resource.data.answerType + && request.resource.data.keys().hasOnly(['userId', 'questionId', 'answerType', 'writtenText', 'selectedOptionIds', 'scaleValue', 'createdAt', 'updatedAt']) && coupleEncryptionEnabled(coupleId) && isEncryptedAnswerPayload(request.resource.data); allow delete: if false;