From 2cac2e8d4fcc43f0a41eb16b2a4c6c35c574ac65 Mon Sep 17 00:00:00 2001 From: Anselm Date: Mon, 7 Aug 2023 19:54:07 +0100 Subject: [PATCH] Deal better with private transactions we don't have a key for --- src/coValue.ts | 67 ++++++++++++++++++++++++----------------- src/permissions.test.ts | 24 ++++++++------- src/permissions.ts | 15 ++++++++- src/sync.test.ts | 3 +- 4 files changed, 68 insertions(+), 41 deletions(-) diff --git a/src/coValue.ts b/src/coValue.ts index bc3c70b18..ed820a20c 100644 --- a/src/coValue.ts +++ b/src/coValue.ts @@ -231,6 +231,10 @@ export class CoValue { if (privacy === "private") { const { secret: keySecret, id: keyID } = this.getCurrentReadKey(); + if (!keySecret) { + throw new Error("Can't make transaction without read key secret"); + } + transaction = { privacy: "private", madeAt, @@ -298,26 +302,40 @@ export class CoValue { const allTransactions: DecryptedTransaction[] = validTransactions.map( ({ txID, tx }) => { - return { - txID, - madeAt: tx.madeAt, - changes: - tx.privacy === "private" - ? decryptForTransaction( - tx.encryptedChanges, - this.getReadKey(tx.keyUsed), - { - in: this.id, - tx: txID, - } - ) || - (() => { - throw new Error("Couldn't decrypt changes"); - })() - : tx.changes, - }; + if (tx.privacy === "trusting") { + return { + txID, + madeAt: tx.madeAt, + changes: tx.changes, + }; + } else { + const readKey = this.getReadKey(tx.keyUsed); + + if (!readKey) { + return undefined; + } else { + const decrytedChanges = decryptForTransaction( + tx.encryptedChanges, + readKey, + { + in: this.id, + tx: txID, + } + ); + + if (!decrytedChanges) { + console.error("Failed to decrypt transaction despite having key"); + return undefined; + } + return { + txID, + madeAt: tx.madeAt, + changes: decrytedChanges, + }; + } + } } - ); + ).filter((x): x is Exclude => !!x); allTransactions.sort( (a, b) => a.madeAt - b.madeAt || @@ -328,7 +346,7 @@ export class CoValue { return allTransactions; } - getCurrentReadKey(): { secret: KeySecret; id: KeyID } { + getCurrentReadKey(): { secret: KeySecret | undefined; id: KeyID } { if (this.header.ruleset.type === "team") { const content = expectTeamContent(this.getCurrentContent()); @@ -355,7 +373,7 @@ export class CoValue { } } - getReadKey(keyID: KeyID): KeySecret { + getReadKey(keyID: KeyID): KeySecret | undefined { if (this.header.ruleset.type === "team") { const content = expectTeamContent(this.getCurrentContent()); @@ -416,12 +434,7 @@ export class CoValue { } } - throw new Error( - "readKey " + - keyID + - " not revealed for " + - getAgentID(getAgent(this.node.agentCredential)) - ); + return undefined; } else if (this.header.ruleset.type === "ownedByTeam") { return this.node .expectCoValueLoaded(this.header.ruleset.team) diff --git a/src/permissions.test.ts b/src/permissions.test.ts index d5cf4f080..c52a260a9 100644 --- a/src/permissions.test.ts +++ b/src/permissions.test.ts @@ -1194,15 +1194,12 @@ test("Admins can set team read rey, make a private transaction in an owned objec newRandomSessionID(reader2ID) ); - expect(() => expectMap(childObjectAsReader.getCurrentContent())).toThrow( - /readKey (.+?) not revealed for (.+?)/ - ); + expect( + expectMap(childObjectAsReader.getCurrentContent()).get("foo2") + ).toBeUndefined(); expect( expectMap(childObjectAsReader2.getCurrentContent()).get("foo2") ).toEqual("bar2"); - expect(() => { - childObjectAsReader.getCurrentContent(); - }).toThrow(); }); test("Admins can set team read rey, make a private transaction in an owned object, rotate the read key, add two readers, rotate the read key again to kick out one reader, make another private transaction in the owned object, and only the remaining reader can read both transactions (high level)", () => { @@ -1259,9 +1256,14 @@ test("Admins can set team read rey, make a private transaction in an owned objec expect(childContentAsReader2.get("foo2")).toEqual("bar2"); expect(childContentAsReader2.get("foo3")).toEqual("bar3"); - expect(() => - childObject.coValue - .testWithDifferentCredentials(reader, newRandomSessionID(readerID)) - .getCurrentContent() - ).toThrow(/readKey (.+?) not revealed for (.+?)/); + expect( + expectMap( + childObject.coValue + .testWithDifferentCredentials( + reader, + newRandomSessionID(readerID) + ) + .getCurrentContent() + ).get("foo3") + ).toBeUndefined(); }); diff --git a/src/permissions.ts b/src/permissions.ts index 21084ce2a..3f15883c2 100644 --- a/src/permissions.ts +++ b/src/permissions.ts @@ -251,6 +251,10 @@ export class Team { const currentReadKey = this.teamMap.coValue.getCurrentReadKey(); + if (!currentReadKey.secret) { + throw new Error("Can't add member without read key secret"); + } + const revelation = seal( currentReadKey.secret, this.teamMap.coValue.node.agentCredential.recipientSecret, @@ -281,7 +285,16 @@ export class Team { } }) as AgentID[]; - const currentReadKey = this.teamMap.coValue.getCurrentReadKey(); + const maybeCurrentReadKey = this.teamMap.coValue.getCurrentReadKey(); + + if (!maybeCurrentReadKey.secret) { + throw new Error("Can't rotate read key secret we don't have access to"); + } + + const currentReadKey = { + id: maybeCurrentReadKey.id, + secret: maybeCurrentReadKey.secret, + }; const newReadKey = newRandomKeySecret(); diff --git a/src/sync.test.ts b/src/sync.test.ts index 248947761..20c14e788 100644 --- a/src/sync.test.ts +++ b/src/sync.test.ts @@ -909,7 +909,6 @@ test("Can sync a coValue through a server to another client", async () => { const server = new LocalNode(serverUser, newRandomSessionID(serverUserID)); const [serverAsPeer, client1AsPeer] = connectedPeers("server", "client1", { - trace: true, peer1role: "server", peer2role: "client", }); @@ -922,7 +921,7 @@ test("Can sync a coValue through a server to another client", async () => { const [serverAsOtherPeer, client2AsPeer] = connectedPeers( "server", "client2", - { trace: true, peer1role: "server", peer2role: "client" } + { peer1role: "server", peer2role: "client" } ); client2.sync.addPeer(serverAsOtherPeer);