feat(rules): add read-gated secure subdoc for couple-key encrypted answers (schemaVersion 2)
This commit is contained in:
parent
f7418df700
commit
e5c9c43317
|
|
@ -117,6 +117,25 @@ service cloud.firestore {
|
||||||
.hasOnly(['isRevealed', 'answerKeyReleased', 'updatedAt']);
|
.hasOnly(['isRevealed', 'answerKeyReleased', 'updatedAt']);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Couple-key daily answer (schemaVersion 2): the answer doc is metadata only (no content).
|
||||||
|
// The encrypted content lives in the read-gated `secure` subdoc, so this doc can stay
|
||||||
|
// readable (drives the partner's "your turn" indicator) without leaking the answer.
|
||||||
|
function isCoupleKeyAnswerCreate(data) {
|
||||||
|
return data.keys().hasOnly([
|
||||||
|
'userId', 'questionId', 'answerType',
|
||||||
|
'schemaVersion', 'answerDate', 'createdAt', 'updatedAt', 'isRevealed'
|
||||||
|
])
|
||||||
|
&& data.schemaVersion == 2
|
||||||
|
&& data.isRevealed == false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// After a couple-key answer is created, only the reveal flag may flip (drives the opened push).
|
||||||
|
function isCoupleKeyAnswerUpdate() {
|
||||||
|
return resource.data.schemaVersion == 2
|
||||||
|
&& request.resource.data.diff(resource.data).affectedKeys()
|
||||||
|
.hasOnly(['isRevealed', 'updatedAt']);
|
||||||
|
}
|
||||||
|
|
||||||
// Thread sealed answers differ from daily answers: no answerDate (threads use threadId
|
// Thread sealed answers differ from daily answers: no answerDate (threads use threadId
|
||||||
// as context), no isRevealed field (reveal state is tracked by the thread VM).
|
// as context), no isRevealed field (reveal state is tracked by the thread VM).
|
||||||
function isSealedThreadAnswerCreate(data) {
|
function isSealedThreadAnswerCreate(data) {
|
||||||
|
|
@ -670,6 +689,9 @@ service cloud.firestore {
|
||||||
|
|
||||||
// Daily question answers: each user writes their own; both members read.
|
// Daily question answers: each user writes their own; both members read.
|
||||||
match /daily_question/{date}/answers/{userId} {
|
match /daily_question/{date}/answers/{userId} {
|
||||||
|
// The answer doc holds only metadata (no content) so the partner can see THAT you
|
||||||
|
// answered ("your turn / waiting for you"); the encrypted content lives in the
|
||||||
|
// read-gated `secure` subdoc below. Both members may read metadata.
|
||||||
allow read: if isCouplesMember(coupleId);
|
allow read: if isCouplesMember(coupleId);
|
||||||
|
|
||||||
allow create: if isCouplesMember(coupleId)
|
allow create: if isCouplesMember(coupleId)
|
||||||
|
|
@ -681,16 +703,17 @@ service cloud.firestore {
|
||||||
// whose metadata disagrees with the path it lands in.
|
// whose metadata disagrees with the path it lands in.
|
||||||
&& request.resource.data.answerDate is string
|
&& request.resource.data.answerDate is string
|
||||||
&& request.resource.data.answerDate == date
|
&& request.resource.data.answerDate == date
|
||||||
// schemaVersion 3: partner-proof sealed answer (the only accepted shape).
|
// schemaVersion 2 = couple-key (current); 3 = legacy sealed partner-proof.
|
||||||
&& isSealedAnswerCreate(request.resource.data);
|
&& (isCoupleKeyAnswerCreate(request.resource.data)
|
||||||
|
|| isSealedAnswerCreate(request.resource.data));
|
||||||
|
|
||||||
allow update: if isCouplesMember(coupleId)
|
allow update: if isCouplesMember(coupleId)
|
||||||
&& request.auth.uid == userId
|
&& request.auth.uid == userId
|
||||||
&& 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
|
||||||
// Sealed answers: only reveal metadata may change; payload is immutable.
|
// Only reveal metadata may change; the encrypted payload is immutable.
|
||||||
&& isSealedAnswerUpdate();
|
&& (isCoupleKeyAnswerUpdate() || isSealedAnswerUpdate());
|
||||||
|
|
||||||
allow delete: if false;
|
allow delete: if false;
|
||||||
|
|
||||||
|
|
@ -718,6 +741,20 @@ service cloud.firestore {
|
||||||
allow update: if false;
|
allow update: if false;
|
||||||
allow delete: if false;
|
allow delete: if false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Couple-key encrypted answer content (schemaVersion 2). Read-gated: you can read your
|
||||||
|
// PARTNER's content only once YOU have also answered — the cryptographic "private until
|
||||||
|
// both answered" gate. Your own content is always readable.
|
||||||
|
match /secure/{doc} {
|
||||||
|
allow read: if isCouplesMember(coupleId)
|
||||||
|
&& (request.auth.uid == userId
|
||||||
|
|| exists(/databases/$(database)/documents/couples/$(coupleId)/daily_question/$(date)/answers/$(request.auth.uid)));
|
||||||
|
allow create: if isCouplesMember(coupleId)
|
||||||
|
&& request.auth.uid == userId
|
||||||
|
&& isCiphertext(request.resource.data.encryptedPayload)
|
||||||
|
&& request.resource.data.keys().hasOnly(['encryptedPayload']);
|
||||||
|
allow update, delete: if false;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Games use enc:v1: (schemaVersion 2 / shared couple key).
|
// Games use enc:v1: (schemaVersion 2 / shared couple key).
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue