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
This commit is contained in:
parent
85b4eb589b
commit
70bb0a346c
|
|
@ -9,15 +9,15 @@ import javax.inject.Inject
|
||||||
import javax.inject.Singleton
|
import javax.inject.Singleton
|
||||||
|
|
||||||
enum class EncryptionStatus {
|
enum class EncryptionStatus {
|
||||||
/** Local keyset present — ready to encrypt/decrypt. */
|
/** Local keyset present -- ready to encrypt/decrypt. */
|
||||||
UNLOCKED,
|
UNLOCKED,
|
||||||
/** Found keyset in the invite slot; moved to coupleId slot automatically. */
|
/** Found keyset in the invite slot; moved to coupleId slot automatically. */
|
||||||
RECONCILED_FROM_INVITE,
|
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,
|
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,
|
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
|
NEEDS_CONTENT_MIGRATION
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -71,7 +71,7 @@ class CoupleEncryptionManager @Inject constructor(
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Called on app launch / Home load after the couple doc is resolved.
|
* 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 {
|
fun checkStatus(couple: Couple): EncryptionStatus {
|
||||||
if (couple.encryptionVersion == 0) return EncryptionStatus.NEEDS_ENCRYPTION_UPGRADE
|
if (couple.encryptionVersion == 0) return EncryptionStatus.NEEDS_ENCRYPTION_UPGRADE
|
||||||
|
|
@ -111,7 +111,7 @@ class CoupleEncryptionManager @Inject constructor(
|
||||||
): Result<RecoveryKeyManager.WrappedKey> = withContext(Dispatchers.Default) {
|
): Result<RecoveryKeyManager.WrappedKey> = withContext(Dispatchers.Default) {
|
||||||
runCatching {
|
runCatching {
|
||||||
val handle = keyStore.loadKeyset(coupleId)
|
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)
|
keyManager.wrap(handle, newPhrase)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -11,7 +11,7 @@ import javax.inject.Singleton
|
||||||
* Wire format: "enc:v1:{base64(tinkCiphertext)}"
|
* Wire format: "enc:v1:{base64(tinkCiphertext)}"
|
||||||
* Plaintext values (no prefix) pass through unchanged so legacy data works.
|
* 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.
|
* copy-paste of one couple's ciphertext into another couple's document.
|
||||||
*/
|
*/
|
||||||
@Singleton
|
@Singleton
|
||||||
|
|
|
||||||
|
|
@ -11,7 +11,7 @@ import javax.inject.Inject
|
||||||
import javax.inject.Singleton
|
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.
|
* 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.
|
* 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.
|
* Wraps [keyset] with Argon2id(passphrase, salt) using AES-256-GCM.
|
||||||
* AAD is the fixed constant [WRAP_AAD] so the blob is portable across
|
* 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 {
|
fun wrap(keyset: KeysetHandle, phrase: String): WrappedKey {
|
||||||
val salt = ByteArray(SALT_BYTES).also { SecureRandom().nextBytes(it) }
|
val salt = ByteArray(SALT_BYTES).also { SecureRandom().nextBytes(it) }
|
||||||
|
|
@ -64,7 +64,7 @@ class RecoveryKeyManager @Inject constructor() {
|
||||||
return deserializeKeyset(keysetBytes)
|
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 {
|
fun serializeKeyset(handle: KeysetHandle): ByteArray {
|
||||||
val baos = java.io.ByteArrayOutputStream()
|
val baos = java.io.ByteArrayOutputStream()
|
||||||
com.google.crypto.tink.CleartextKeysetHandle.write(
|
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 const val PARAMS_TAG = "argon2id;v=19;m=47104;t=3;p=1"
|
||||||
private val WRAP_AAD = "closer_couple_key".toByteArray(Charsets.UTF_8)
|
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(
|
val WORDLIST = arrayOf(
|
||||||
"able","acid","acre","aged","aide","also","army","atom",
|
"able","acid","acre","aged","aide","also","army","atom",
|
||||||
"baby","back","bake","ball","bank","barn","base","bath",
|
"baby","back","bake","ball","bank","barn","base","bath",
|
||||||
|
|
|
||||||
|
|
@ -322,6 +322,7 @@ service cloud.firestore {
|
||||||
match /answers/{userId} {
|
match /answers/{userId} {
|
||||||
allow create, update: if isCouplesMember(coupleId)
|
allow create, update: if isCouplesMember(coupleId)
|
||||||
&& isOwner(userId)
|
&& isOwner(userId)
|
||||||
|
&& request.resource.data.keys().hasOnly(['userId', 'questionId', 'answerType', 'writtenText', 'selectedOptionIds', 'scaleValue', 'createdAt', 'updatedAt'])
|
||||||
&& coupleEncryptionEnabled(coupleId)
|
&& coupleEncryptionEnabled(coupleId)
|
||||||
&& isEncryptedAnswerPayload(request.resource.data);
|
&& isEncryptedAnswerPayload(request.resource.data);
|
||||||
allow delete: if isOwner(userId);
|
allow delete: if isOwner(userId);
|
||||||
|
|
@ -334,10 +335,12 @@ service cloud.firestore {
|
||||||
allow create: if isCouplesMember(coupleId)
|
allow create: if isCouplesMember(coupleId)
|
||||||
&& coupleEncryptionEnabled(coupleId)
|
&& coupleEncryptionEnabled(coupleId)
|
||||||
&& request.resource.data.authorUserId == request.auth.uid
|
&& request.resource.data.authorUserId == request.auth.uid
|
||||||
|
&& request.resource.data.keys().hasOnly(['authorUserId', 'text', 'createdAt'])
|
||||||
&& isCiphertext(request.resource.data.text);
|
&& isCiphertext(request.resource.data.text);
|
||||||
allow update: if isCouplesMember(coupleId)
|
allow update: if isCouplesMember(coupleId)
|
||||||
&& coupleEncryptionEnabled(coupleId)
|
&& coupleEncryptionEnabled(coupleId)
|
||||||
&& resource.data.authorUserId == request.auth.uid
|
&& resource.data.authorUserId == request.auth.uid
|
||||||
|
&& request.resource.data.keys().hasOnly(['text'])
|
||||||
&& isCiphertext(request.resource.data.text);
|
&& isCiphertext(request.resource.data.text);
|
||||||
allow delete: if isCouplesMember(coupleId)
|
allow delete: if isCouplesMember(coupleId)
|
||||||
&& resource.data.authorUserId == request.auth.uid;
|
&& resource.data.authorUserId == request.auth.uid;
|
||||||
|
|
@ -347,9 +350,11 @@ service cloud.firestore {
|
||||||
match /reactions/{reactionId} {
|
match /reactions/{reactionId} {
|
||||||
allow read: if isCouplesMember(coupleId);
|
allow read: if isCouplesMember(coupleId);
|
||||||
allow create: 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)
|
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)
|
allow delete: if isCouplesMember(coupleId)
|
||||||
&& resource.data.userId == request.auth.uid;
|
&& resource.data.userId == request.auth.uid;
|
||||||
}
|
}
|
||||||
|
|
@ -457,7 +462,7 @@ service cloud.firestore {
|
||||||
allow read: if isCouplesMember(coupleId);
|
allow read: if isCouplesMember(coupleId);
|
||||||
allow create: if isCouplesMember(coupleId)
|
allow create: if isCouplesMember(coupleId)
|
||||||
&& request.auth.uid == userId
|
&& 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.userId == request.auth.uid
|
||||||
&& request.resource.data.questionId is string
|
&& request.resource.data.questionId is string
|
||||||
&& request.resource.data.answerType 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.userId == resource.data.userId
|
||||||
&& request.resource.data.questionId == resource.data.questionId
|
&& request.resource.data.questionId == resource.data.questionId
|
||||||
&& request.resource.data.answerType == resource.data.answerType
|
&& request.resource.data.answerType == resource.data.answerType
|
||||||
|
&& request.resource.data.keys().hasOnly(['userId', 'questionId', 'answerType', 'writtenText', 'selectedOptionIds', 'scaleValue', 'createdAt', 'updatedAt'])
|
||||||
&& coupleEncryptionEnabled(coupleId)
|
&& coupleEncryptionEnabled(coupleId)
|
||||||
&& isEncryptedAnswerPayload(request.resource.data);
|
&& isEncryptedAnswerPayload(request.resource.data);
|
||||||
allow delete: if false;
|
allow delete: if false;
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue