diff --git a/src/coValue.ts b/src/coValue.ts index e1967e9ac..8d2e24db9 100644 --- a/src/coValue.ts +++ b/src/coValue.ts @@ -9,14 +9,14 @@ import { KeySecret, Signature, StreamingHash, - openAs, + unseal, shortHash, sign, verify, encryptForTransaction, decryptForTransaction, KeyID, - unsealKeySecret, + decryptKeySecret, getAgentSignatoryID, getAgentRecipientID, } from "./crypto.js"; @@ -27,6 +27,7 @@ import { Team, determineValidTransactions, expectTeamContent, + isKeyForKeyField, } from "./permissions.js"; import { LocalNode } from "./node.js"; import { CoValueKnownState, NewContentMessage } from "./sync.js"; @@ -158,7 +159,7 @@ export class CoValue { newSignature: Signature ): boolean { const signatoryID = getAgentSignatoryID( - this.node.resolveAccount( + this.node.resolveAccountAgent( accountOrAgentIDfromSessionID(sessionID), "Expected to know signatory of transaction" ) @@ -376,7 +377,7 @@ export class CoValue { if (this.header.ruleset.type === "team") { const content = expectTeamContent(this.getCurrentContent()); - const currentKeyId = content.get("readKey")?.keyID; + const currentKeyId = content.get("readKey"); if (!currentKeyId) { throw new Error("No readKey set"); @@ -403,63 +404,63 @@ export class CoValue { if (this.header.ruleset.type === "team") { const content = expectTeamContent(this.getCurrentContent()); - const readKeyHistory = content.getHistory("readKey"); + // Try to find key revelation for us - // Try to find direct relevation of key for us + const readKeyEntry = content.getLastEntry(`${keyID}_for_${this.node.account.id}`); - for (const entry of readKeyHistory) { - if (entry.value?.keyID === keyID) { - const revealer = accountOrAgentIDfromSessionID( - entry.txID.sessionID - ); - const revealerAgent = this.node.resolveAccount( - revealer, - "Expected to know revealer" - ); + if (readKeyEntry) { + const revealer = accountOrAgentIDfromSessionID( + readKeyEntry.txID.sessionID + ); + const revealerAgent = this.node.resolveAccountAgent( + revealer, + "Expected to know revealer" + ); - const secret = openAs( - entry.value.revelation, - this.node.account.currentRecipientSecret(), - getAgentRecipientID(revealerAgent), - { - in: this.id, - tx: entry.txID, - } - ); + const secret = unseal( + readKeyEntry.value, + this.node.account.currentRecipientSecret(), + getAgentRecipientID(revealerAgent), + { + in: this.id, + tx: readKeyEntry.txID, + } + ); - if (secret) return secret as KeySecret; - } + if (secret) return secret as KeySecret; } // Try to find indirect revelation through previousKeys - for (const entry of readKeyHistory) { - const encryptedPreviousKey = entry.value?.previousKeys?.[keyID]; - if (entry.value && encryptedPreviousKey) { - const sealingKeyID = entry.value.keyID; - const sealingKeySecret = this.getReadKey(sealingKeyID); + for (const field of content.keys()) { + if (isKeyForKeyField(field) && field.startsWith(keyID)) { + const encryptingKeyID = field.split("_for_")[1] as KeyID; + const encryptingKeySecret = this.getReadKey(encryptingKeyID); - if (!sealingKeySecret) { + if (!encryptingKeySecret) { continue; } - const secret = unsealKeySecret( + const encryptedPreviousKey = content.get(field)!; + + const secret = decryptKeySecret( { - sealed: keyID, - sealing: sealingKeyID, + encryptedID: keyID, + encryptingID: encryptingKeyID, encrypted: encryptedPreviousKey, }, - sealingKeySecret + encryptingKeySecret ); if (secret) { return secret; } else { console.error( - `Sealing ${sealingKeyID} key didn't unseal ${keyID}` + `Encrypting ${encryptingKeyID} key didn't decrypt ${keyID}` ); } } + } return undefined; @@ -551,6 +552,4 @@ export class CoValue { ? [this.header.ruleset.team] : []; } -} - -export { SessionID }; +} \ No newline at end of file diff --git a/src/contentTypes/coMap.ts b/src/contentTypes/coMap.ts index a25def337..2454d0213 100644 --- a/src/contentTypes/coMap.ts +++ b/src/contentTypes/coMap.ts @@ -116,6 +116,21 @@ export class CoMap< return lastEntry.txID; } + getLastEntry(key: KK): { at: number; txID: TransactionID; value: M[KK]; } | undefined { + const ops = this.ops[key]; + if (!ops) { + return undefined; + } + + const lastEntry = ops[ops.length - 1]!; + + if (lastEntry.op === "delete") { + return undefined; + } else { + return { at: lastEntry.madeAt, txID: lastEntry.txID, value: lastEntry.value }; + } + } + getHistory(key: KK): { at: number; txID: TransactionID; value: M[KK] | undefined; }[] { const ops = this.ops[key]; if (!ops) { diff --git a/src/crypto.test.ts b/src/crypto.test.ts index f4a4bdac1..56bf61aff 100644 --- a/src/crypto.test.ts +++ b/src/crypto.test.ts @@ -6,14 +6,14 @@ import { newRandomSignatory, seal, sign, - openAs, + unseal, verify, shortHash, newRandomKeySecret, encryptForTransaction, decryptForTransaction, - sealKeySecret, - unsealKeySecret, + encryptKeySecret, + decryptKeySecret, } from './crypto.js'; import { base58, base64url } from "@scure/base"; import { x25519 } from "@noble/curves/ed25519"; @@ -41,12 +41,11 @@ test("Invalid signatures don't verify", () => { expect(verify(wrongSignature, data, getSignatoryID(signatory))).toBe(false); }); -test("Sealing round-trips, but invalid receiver can't unseal", () => { +test("encrypting round-trips, but invalid receiver can't unseal", () => { const data = { b: "world", a: "hello" }; const sender = newRandomRecipient(); - const recipient1 = newRandomRecipient(); - const recipient2 = newRandomRecipient(); - const recipient3 = newRandomRecipient(); + const recipient = newRandomRecipient(); + const wrongRecipient = newRandomRecipient(); const nOnceMaterial = { in: "co_zTEST", @@ -56,34 +55,29 @@ test("Sealing round-trips, but invalid receiver can't unseal", () => { const sealed = seal( data, sender, - new Set([getRecipientID(recipient1), getRecipientID(recipient2)]), + getRecipientID(recipient), nOnceMaterial ); - expect(sealed[getRecipientID(recipient1)]).toMatch(/^sealed_U/); - expect(sealed[getRecipientID(recipient2)]).toMatch(/^sealed_U/); expect( - openAs(sealed, recipient1, getRecipientID(sender), nOnceMaterial) + unseal(sealed, recipient, getRecipientID(sender), nOnceMaterial) ).toEqual(data); expect( - openAs(sealed, recipient2, getRecipientID(sender), nOnceMaterial) - ).toEqual(data); - expect( - openAs(sealed, recipient3, getRecipientID(sender), nOnceMaterial) - ).toBeUndefined(); + () => unseal(sealed, wrongRecipient, getRecipientID(sender), nOnceMaterial) + ).toThrow(/Wrong tag/); // trying with wrong recipient secret, by hand const nOnce = blake3( new TextEncoder().encode(stableStringify(nOnceMaterial)) ).slice(0, 24); const recipient3priv = base58.decode( - recipient3.substring("recipientSecret_z".length) + wrongRecipient.substring("recipientSecret_z".length) ); const senderPub = base58.decode( getRecipientID(sender).substring("recipient_z".length) ); const sealedBytes = base64url.decode( - sealed[getRecipientID(recipient1)]!.substring("sealed_U".length) + sealed.substring("sealed_U".length) ); const sharedSecret = x25519.getSharedSecret(recipient3priv, senderPub); @@ -156,34 +150,34 @@ test("Encryption for transactions doesn't decrypt with a wrong key", () => { }); test("Encryption of keySecrets round-trips", () => { - const toSeal = newRandomKeySecret(); - const sealing = newRandomKeySecret(); + const toEncrypt = newRandomKeySecret(); + const encrypting = newRandomKeySecret(); const keys = { - toSeal, - sealing, + toEncrypt, + encrypting, }; - const sealed = sealKeySecret(keys); + const encrypted = encryptKeySecret(keys); - const unsealed = unsealKeySecret(sealed, sealing.secret); + const decrypted = decryptKeySecret(encrypted, encrypting.secret); - expect(unsealed).toEqual(toSeal.secret); + expect(decrypted).toEqual(toEncrypt.secret); }); -test("Encryption of keySecrets doesn't unseal with a wrong key", () => { - const toSeal = newRandomKeySecret(); - const sealing = newRandomKeySecret(); - const sealingWrong = newRandomKeySecret(); +test("Encryption of keySecrets doesn't decrypt with a wrong key", () => { + const toEncrypt = newRandomKeySecret(); + const encrypting = newRandomKeySecret(); + const encryptingWrong = newRandomKeySecret(); const keys = { - toSeal, - sealing, + toEncrypt, + encrypting, }; - const sealed = sealKeySecret(keys); + const encrypted = encryptKeySecret(keys); - const unsealed = unsealKeySecret(sealed, sealingWrong.secret); + const decrypted = decryptKeySecret(encrypted, encryptingWrong.secret); - expect(unsealed).toBeUndefined(); + expect(decrypted).toBeUndefined(); }); diff --git a/src/crypto.ts b/src/crypto.ts index 97624a0ac..0c0c3a9db 100644 --- a/src/crypto.ts +++ b/src/crypto.ts @@ -127,53 +127,40 @@ export function getAgentRecipientSecret(agentSecret: AgentSecret): RecipientSecr return agentSecret.split("/")[0] as RecipientSecret; } -export type SealedSet = { - [recipient: RecipientID]: Sealed; -}; - export function seal( message: T, from: RecipientSecret, - to: Set, + to: RecipientID, nOnceMaterial: { in: RawCoValueID; tx: TransactionID } -): SealedSet { +): Sealed { const nOnce = blake3( textEncoder.encode(stableStringify(nOnceMaterial)) ).slice(0, 24); - const recipientsSorted = Array.from(to).sort(); - const recipientPubs = recipientsSorted.map((recipient) => { - return base58.decode(recipient.substring("recipient_z".length)); - }); + const recipientPub = base58.decode(to.substring("recipient_z".length)); + const senderPriv = base58.decode( from.substring("recipientSecret_z".length) ); const plaintext = textEncoder.encode(stableStringify(message)); - const sealedSet: SealedSet = {}; + const sharedSecret = x25519.getSharedSecret( + senderPriv, + recipientPub + ); - for (let i = 0; i < recipientsSorted.length; i++) { - const recipient = recipientsSorted[i]!; - const sharedSecret = x25519.getSharedSecret( - senderPriv, - recipientPubs[i]! - ); + const sealedBytes = xsalsa20_poly1305(sharedSecret, nOnce).encrypt( + plaintext + ); - const sealedBytes = xsalsa20_poly1305(sharedSecret, nOnce).encrypt( - plaintext - ); - - sealedSet[recipient] = `sealed_U${base64url.encode( - sealedBytes - )}` as Sealed; - } - - return sealedSet; + return `sealed_U${base64url.encode( + sealedBytes + )}` as Sealed } -export function openAs( - sealedSet: SealedSet, +export function unseal( + sealed: Sealed, recipient: RecipientSecret, from: RecipientID, nOnceMaterial: { in: RawCoValueID; tx: TransactionID } @@ -188,12 +175,6 @@ export function openAs( const senderPub = base58.decode(from.substring("recipient_z".length)); - const sealed = sealedSet[getRecipientID(recipient)]; - - if (!sealed) { - return undefined; - } - const sealedBytes = base64url.decode(sealed.substring("sealed_U".length)); const sharedSecret = x25519.getSharedSecret(recipientPriv, senderPub); @@ -287,25 +268,25 @@ export function encryptForTransaction( return encrypt(value, keySecret, nOnceMaterial); } -export function sealKeySecret(keys: { - toSeal: { id: KeyID; secret: KeySecret }; - sealing: { id: KeyID; secret: KeySecret }; +export function encryptKeySecret(keys: { + toEncrypt: { id: KeyID; secret: KeySecret }; + encrypting: { id: KeyID; secret: KeySecret }; }): { - sealed: KeyID; - sealing: KeyID; - encrypted: Encrypted; + encryptedID: KeyID; + encryptingID: KeyID; + encrypted: Encrypted; } { const nOnceMaterial = { - sealed: keys.toSeal.id, - sealing: keys.sealing.id, + encryptedID: keys.toEncrypt.id, + encryptingID: keys.encrypting.id, }; return { - sealed: keys.toSeal.id, - sealing: keys.sealing.id, + encryptedID: keys.toEncrypt.id, + encryptingID: keys.encrypting.id, encrypted: encrypt( - keys.toSeal.secret, - keys.sealing.secret, + keys.toEncrypt.secret, + keys.encrypting.secret, nOnceMaterial ), }; @@ -343,20 +324,20 @@ export function decryptForTransaction( return decrypt(encrypted, keySecret, nOnceMaterial); } -export function unsealKeySecret( - sealedInfo: { - sealed: KeyID; - sealing: KeyID; - encrypted: Encrypted; +export function decryptKeySecret( + encryptedInfo: { + encryptedID: KeyID; + encryptingID: KeyID; + encrypted: Encrypted; }, sealingSecret: KeySecret ): KeySecret | undefined { const nOnceMaterial = { - sealed: sealedInfo.sealed, - sealing: sealedInfo.sealing, + encryptedID: encryptedInfo.encryptedID, + encryptingID: encryptedInfo.encryptingID, }; - return decrypt(sealedInfo.encrypted, sealingSecret, nOnceMaterial); + return decrypt(encryptedInfo.encrypted, sealingSecret, nOnceMaterial); } export function uniquenessForHeader(): `z${string}` { diff --git a/src/node.ts b/src/node.ts index 2d4161574..538af74da 100644 --- a/src/node.ts +++ b/src/node.ts @@ -87,27 +87,31 @@ export class LocalNode { const account = this.createCoValue( accountHeaderForInitialAgentSecret(agentSecret) - ).testWithDifferentAccount(new AnonymousControlledAccount(agentSecret), newRandomSessionID(getAgentID(agentSecret))); + ).testWithDifferentAccount( + new AnonymousControlledAccount(agentSecret), + newRandomSessionID(getAgentID(agentSecret)) + ); expectTeamContent(account.getCurrentContent()).edit((editable) => { editable.set(getAgentID(agentSecret), "admin", "trusting"); const readKey = newRandomKeySecret(); - const revelation = seal( - readKey.secret, - getAgentRecipientSecret(agentSecret), - new Set([getAgentRecipientID(getAgentID(agentSecret))]), - { - in: account.id, - tx: account.nextTransactionID(), - } - ); editable.set( - "readKey", - { keyID: readKey.id, revelation }, + `${readKey.id}_for_${getAgentID(agentSecret)}`, + seal( + readKey.secret, + getAgentRecipientSecret(agentSecret), + getAgentRecipientID(getAgentID(agentSecret)), + { + in: account.id, + tx: account.nextTransactionID(), + } + ), "trusting" ); + + editable.set('readKey', readKey.id, "trusting"); }); return new ControlledAccount( @@ -117,7 +121,7 @@ export class LocalNode { ); } - resolveAccount(id: AccountIDOrAgentID, expectation?: string): AgentID { + resolveAccountAgent(id: AccountIDOrAgentID, expectation?: string): AgentID { if (isAgentID(id)) { return id; } @@ -159,21 +163,22 @@ export class LocalNode { editable.set(this.account.id, "admin", "trusting"); const readKey = newRandomKeySecret(); - const revelation = seal( - readKey.secret, - this.account.currentRecipientSecret(), - new Set([this.account.currentRecipientID()]), - { - in: teamCoValue.id, - tx: teamCoValue.nextTransactionID(), - } - ); editable.set( - "readKey", - { keyID: readKey.id, revelation }, + `${readKey.id}_for_${this.account.id}`, + seal( + readKey.secret, + this.account.currentRecipientSecret(), + this.account.currentRecipientID(), + { + in: teamCoValue.id, + tx: teamCoValue.nextTransactionID(), + } + ), "trusting" ); + + editable.set('readKey', readKey.id, "trusting"); }); return new Team(teamContent, this); diff --git a/src/permissions.test.ts b/src/permissions.test.ts index 07beca442..ed7c830aa 100644 --- a/src/permissions.test.ts +++ b/src/permissions.test.ts @@ -7,7 +7,7 @@ import { getRecipientID, newRandomKeySecret, seal, - sealKeySecret, + encryptKeySecret, } from "./crypto.js"; import { newTeam, @@ -424,17 +424,23 @@ test("Admins can set team read key and then use it to create and read private tr const revelation = seal( readKey, admin.currentRecipientSecret(), - new Set([admin.currentRecipientID()]), + admin.currentRecipientID(), { in: team.id, tx: team.nextTransactionID(), } ); - editable.set("readKey", { keyID: readKeyID, revelation }, "trusting"); - expect(editable.get("readKey")).toEqual({ - keyID: readKeyID, - revelation, - }); + + editable.set(`${readKeyID}_for_${admin.id}`, revelation, "trusting"); + + expect(editable.get(`${readKeyID}_for_${admin.id}`)).toEqual( + revelation + ); + + editable.set("readKey", readKeyID, "trusting"); + + expect(editable.get("readKey")).toEqual(readKeyID); + expect(team.getCurrentReadKey().secret).toEqual(readKey); }); @@ -483,16 +489,31 @@ test("Admins can set team read key and then writers can use it to create and rea editable.set(writer.id, "writer", "trusting"); expect(editable.get(writer.id)).toEqual("writer"); - const revelation = seal( + const revelation1 = seal( readKey, admin.currentRecipientSecret(), - new Set([admin.currentRecipientID(), writer.currentRecipientID()]), + admin.currentRecipientID(), { in: team.id, tx: team.nextTransactionID(), } ); - editable.set("readKey", { keyID: readKeyID, revelation }, "trusting"); + + editable.set(`${readKeyID}_for_${admin.id}`, revelation1, "trusting"); + + const revelation2 = seal( + readKey, + admin.currentRecipientSecret(), + writer.currentRecipientID(), + { + in: team.id, + tx: team.nextTransactionID(), + } + ); + + editable.set(`${readKeyID}_for_${writer.id}`, revelation2, "trusting"); + + editable.set("readKey", readKeyID, "trusting"); }); const childObject = node.createCoValue({ @@ -560,16 +581,31 @@ test("Admins can set team read key and then use it to create private transaction editable.set(reader.id, "reader", "trusting"); expect(editable.get(reader.id)).toEqual("reader"); - const revelation = seal( + const revelation1 = seal( readKey, admin.currentRecipientSecret(), - new Set([admin.currentRecipientID(), reader.currentRecipientID()]), + admin.currentRecipientID(), { in: team.id, tx: team.nextTransactionID(), } ); - editable.set("readKey", { keyID: readKeyID, revelation }, "trusting"); + + editable.set(`${readKeyID}_for_${admin.id}`, revelation1, "trusting"); + + const revelation2 = seal( + readKey, + admin.currentRecipientSecret(), + reader.currentRecipientID(), + { + in: team.id, + tx: team.nextTransactionID(), + } + ); + + editable.set(`${readKeyID}_for_${reader.id}`, revelation2, "trusting"); + + editable.set("readKey", readKeyID, "trusting"); }); const childObject = node.createCoValue({ @@ -640,32 +676,28 @@ test("Admins can set team read key and then use it to create private transaction const revelation1 = seal( readKey, admin.currentRecipientSecret(), - new Set([admin.currentRecipientID(), reader1.currentRecipientID()]), + admin.currentRecipientID(), { in: team.id, tx: team.nextTransactionID(), } ); - editable.set( - "readKey", - { keyID: readKeyID, revelation: revelation1 }, - "trusting" - ); + + editable.set(`${readKeyID}_for_${admin.id}`, revelation1, "trusting"); const revelation2 = seal( readKey, admin.currentRecipientSecret(), - new Set([reader2.currentRecipientID()]), + reader1.currentRecipientID(), { in: team.id, tx: team.nextTransactionID(), } ); - editable.set( - "readKey", - { keyID: readKeyID, revelation: revelation2 }, - "trusting" - ); + + editable.set(`${readKeyID}_for_${reader1.id}`, revelation2, "trusting"); + + editable.set("readKey", readKeyID, "trusting"); }); const childObject = node.createCoValue({ @@ -694,6 +726,20 @@ test("Admins can set team read key and then use it to create private transaction expect(childContentAsReader1.get("foo")).toEqual("bar"); + teamContent.edit((editable) => { + const revelation3 = seal( + readKey, + admin.currentRecipientSecret(), + reader2.currentRecipientID(), + { + in: team.id, + tx: team.nextTransactionID(), + } + ); + + editable.set(`${readKeyID}_for_${reader2.id}`, revelation3, "trusting"); + }); + const childObjectAsReader2 = childObject.testWithDifferentAccount( reader2, newRandomSessionID(reader2.id) @@ -753,17 +799,17 @@ test("Admins can set team read key, make a private transaction in an owned objec const revelation = seal( readKey, admin.currentRecipientSecret(), - new Set([admin.currentRecipientID()]), + admin.currentRecipientID(), { in: team.id, tx: team.nextTransactionID(), } ); - editable.set("readKey", { keyID: readKeyID, revelation }, "trusting"); - expect(editable.get("readKey")).toEqual({ - keyID: readKeyID, - revelation, - }); + + editable.set(`${readKeyID}_for_${admin.id}`, revelation, "trusting"); + + editable.set("readKey", readKeyID, "trusting"); + expect(editable.get("readKey")).toEqual(readKeyID); expect(team.getCurrentReadKey().secret).toEqual(readKey); }); @@ -791,18 +837,17 @@ test("Admins can set team read key, make a private transaction in an owned objec const revelation = seal( readKey2, admin.currentRecipientSecret(), - new Set([admin.currentRecipientID()]), + admin.currentRecipientID(), { in: team.id, tx: team.nextTransactionID(), } ); - editable.set("readKey", { keyID: readKeyID2, revelation }, "trusting"); - expect(editable.get("readKey")).toEqual({ - keyID: readKeyID2, - revelation, - }); + editable.set(`${readKeyID2}_for_${admin.id}`, revelation, "trusting"); + + editable.set("readKey", readKeyID2, "trusting"); + expect(editable.get("readKey")).toEqual(readKeyID2); expect(team.getCurrentReadKey().secret).toEqual(readKey2); }); @@ -863,17 +908,17 @@ test("Admins can set team read key, make a private transaction in an owned objec const revelation = seal( readKey, admin.currentRecipientSecret(), - new Set([admin.currentRecipientID()]), + admin.currentRecipientID(), { in: team.id, tx: team.nextTransactionID(), } ); - editable.set("readKey", { keyID: readKeyID, revelation }, "trusting"); - expect(editable.get("readKey")).toEqual({ - keyID: readKeyID, - revelation, - }); + + editable.set(`${readKeyID}_for_${admin.id}`, revelation, "trusting"); + + editable.set("readKey", readKeyID, "trusting"); + expect(editable.get("readKey")).toEqual(readKeyID); expect(team.getCurrentReadKey().secret).toEqual(readKey); }); @@ -892,34 +937,42 @@ test("Admins can set team read key, make a private transaction in an owned objec const { secret: readKey2, id: readKeyID2 } = newRandomKeySecret(); teamContent.edit((editable) => { - const revelation = seal( + const revelation2 = seal( readKey2, admin.currentRecipientSecret(), - new Set([admin.currentRecipientID(), reader.currentRecipientID()]), + admin.currentRecipientID(), { in: team.id, tx: team.nextTransactionID(), } ); - editable.set( - "readKey", + editable.set(`${readKeyID2}_for_${admin.id}`, revelation2, "trusting"); + + const revelation3 = seal( + readKey2, + admin.currentRecipientSecret(), + reader.currentRecipientID(), { - keyID: readKeyID2, - revelation, - previousKeys: { - [readKeyID]: sealKeySecret({ - toSeal: { id: readKeyID, secret: readKey }, - sealing: { id: readKeyID2, secret: readKey2 }, - }).encrypted, - }, - }, + in: team.id, + tx: team.nextTransactionID(), + } + ); + + editable.set(`${readKeyID2}_for_${reader.id}`, revelation3, "trusting"); + + editable.set( + `${readKeyID}_for_${readKeyID2}`, + encryptKeySecret({ + toEncrypt: { id: readKeyID, secret: readKey }, + encrypting: { id: readKeyID2, secret: readKey2 }, + }).encrypted, "trusting" ); - expect(editable.get("readKey")).toMatchObject({ - keyID: readKeyID2, - revelation, - }); + + editable.set("readKey", readKeyID2, "trusting"); + + expect(editable.get("readKey")).toEqual(readKeyID2); expect(team.getCurrentReadKey().secret).toEqual(readKey2); editable.set(reader.id, "reader", "trusting"); @@ -1001,24 +1054,44 @@ test("Admins can set team read rey, make a private transaction in an owned objec const reader2 = node.createAccount("reader2"); teamContent.edit((editable) => { - const revelation = seal( + const revelation1 = seal( readKey, admin.currentRecipientSecret(), - new Set([ - admin.currentRecipientID(), - reader.currentRecipientID(), - reader2.currentRecipientID(), - ]), + admin.currentRecipientID(), { in: team.id, tx: team.nextTransactionID(), } ); - editable.set("readKey", { keyID: readKeyID, revelation }, "trusting"); - expect(editable.get("readKey")).toEqual({ - keyID: readKeyID, - revelation, - }); + + editable.set(`${readKeyID}_for_${admin.id}`, revelation1, "trusting"); + + const revelation2 = seal( + readKey, + admin.currentRecipientSecret(), + reader.currentRecipientID(), + { + in: team.id, + tx: team.nextTransactionID(), + } + ); + + editable.set(`${readKeyID}_for_${reader.id}`, revelation2, "trusting"); + + const revelation3 = seal( + readKey, + admin.currentRecipientSecret(), + reader2.currentRecipientID(), + { + in: team.id, + tx: team.nextTransactionID(), + } + ); + + editable.set(`${readKeyID}_for_${reader2.id}`, revelation3, "trusting"); + + editable.set("readKey", readKeyID, "trusting"); + expect(editable.get("readKey")).toEqual(readKeyID); expect(team.getCurrentReadKey().secret).toEqual(readKey); editable.set(reader.id, "reader", "trusting"); @@ -1058,20 +1131,40 @@ test("Admins can set team read rey, make a private transaction in an owned objec const { secret: readKey2, id: readKeyID2 } = newRandomKeySecret(); teamContent.edit((editable) => { - const revelation = seal( + const newRevelation1 = seal( readKey2, admin.currentRecipientSecret(), - new Set([admin.currentRecipientID(), reader2.currentRecipientID()]), + admin.currentRecipientID(), { in: team.id, tx: team.nextTransactionID(), } ); - editable.set("readKey", { keyID: readKeyID2, revelation }, "trusting"); - expect(editable.get("readKey")).toEqual({ - keyID: readKeyID2, - revelation, - }); + + editable.set( + `${readKeyID2}_for_${admin.id}`, + newRevelation1, + "trusting" + ); + + const newRevelation2 = seal( + readKey2, + admin.currentRecipientSecret(), + reader2.currentRecipientID(), + { + in: team.id, + tx: team.nextTransactionID(), + } + ); + + editable.set( + `${readKeyID2}_for_${reader2.id}`, + newRevelation2, + "trusting" + ); + + editable.set("readKey", readKeyID2, "trusting"); + expect(editable.get("readKey")).toEqual(readKeyID2); expect(team.getCurrentReadKey().secret).toEqual(readKey2); editable.set(reader.id, "revoked", "trusting"); diff --git a/src/permissions.ts b/src/permissions.ts index b1d71f4b3..e36c0a101 100644 --- a/src/permissions.ts +++ b/src/permissions.ts @@ -1,29 +1,29 @@ -import { CoValueID, ContentType } from './contentType.js'; -import { CoMap, MapOpPayload } from './contentTypes/coMap.js'; -import { JsonValue } from './jsonValue.js'; +import { CoValueID, ContentType } from "./contentType.js"; +import { CoMap, MapOpPayload } from "./contentTypes/coMap.js"; +import { JsonValue } from "./jsonValue.js"; import { Encrypted, KeyID, KeySecret, - SealedSet, createdNowUnique, newRandomKeySecret, seal, - sealKeySecret, - getAgentRecipientID -} from './crypto.js'; + encryptKeySecret, + getAgentRecipientID, + Sealed, +} from "./crypto.js"; import { CoValue, Transaction, TrustingTransaction, accountOrAgentIDfromSessionID, -} from './coValue.js'; +} from "./coValue.js"; import { LocalNode } from "./node.js"; -import { RawCoValueID, SessionID, TransactionID, isAgentID } from './ids.js'; -import { AccountIDOrAgentID, GeneralizedControlledAccount } from './account.js'; +import { RawCoValueID, SessionID, TransactionID, isAgentID } from "./ids.js"; +import { AccountIDOrAgentID, GeneralizedControlledAccount } from "./account.js"; export type PermissionsDef = - | { type: "team"; initialAdmin: AccountIDOrAgentID; } + | { type: "team"; initialAdmin: AccountIDOrAgentID } | { type: "ownedByTeam"; team: RawCoValueID } | { type: "unsafeAllowAll" }; @@ -94,6 +94,14 @@ export function determineValidTransactions( continue; } + validTransactions.push({ txID: { sessionID, txIndex }, tx }); + continue; + } else if (isKeyForKeyField(change.key) || isKeyForAccountField(change.key)) { + if (memberState[transactor] !== "admin") { + console.warn("Only admins can reveal keys"); + continue; + } + // TODO: check validity of agents who the key is revealed to? validTransactions.push({ txID: { sessionID, txIndex }, tx }); @@ -146,11 +154,12 @@ export function determineValidTransactions( return validTransactions; } else if (coValue.header.ruleset.type === "ownedByTeam") { - const teamContent = - coValue.node.expectCoValueLoaded( + const teamContent = coValue.node + .expectCoValueLoaded( coValue.header.ruleset.team, "Determining valid transaction in owned object but its team wasn't loaded" - ).getCurrentContent(); + ) + .getCurrentContent(); if (teamContent.type !== "comap") { throw new Error("Team must be a map"); @@ -158,7 +167,9 @@ export function determineValidTransactions( return Object.entries(coValue.sessions).flatMap( ([sessionID, sessionLog]) => { - const transactor = accountOrAgentIDfromSessionID(sessionID as SessionID); + const transactor = accountOrAgentIDfromSessionID( + sessionID as SessionID + ); return sessionLog.transactions .filter((tx) => { const transactorRoleAtTxTime = teamContent.getAtTime( @@ -187,24 +198,25 @@ export function determineValidTransactions( } ); } else { - throw new Error("Unknown ruleset type " + (coValue.header.ruleset as any).type); + throw new Error( + "Unknown ruleset type " + (coValue.header.ruleset as any).type + ); } } -export type TeamContent = { [key: AccountIDOrAgentID]: Role } & { - readKey: { - keyID: KeyID; - revelation: SealedSet; - previousKeys?: { - [key: KeyID]: Encrypted< - KeySecret, - { sealed: KeyID; sealing: KeyID } - >; - }; - }; +export type TeamContent = { + [key: AccountIDOrAgentID]: Role; + readKey: KeyID; + [revelationFor: `${KeyID}_for_${AccountIDOrAgentID}`]: Sealed; + [oldKeyForNewKey: `${KeyID}_for_${KeyID}`]: Encrypted< + KeySecret, + { encryptedID: KeyID; encryptingID: KeyID } + >; }; -export function expectTeamContent(content: ContentType): CoMap { +export function expectTeamContent( + content: ContentType +): CoMap { if (content.type !== "comap") { throw new Error("Expected map"); } @@ -227,36 +239,34 @@ export class Team { addMember(accountID: AccountIDOrAgentID, role: Role) { this.teamMap = this.teamMap.edit((map) => { - const agent = this.node.resolveAccount(accountID, "Expected to know agent to add them to team"); - - if (!agent) { - throw new Error("Unknown account/agent " + accountID); - } - - map.set(accountID, role, "trusting"); - if (map.get(accountID) !== role) { - throw new Error("Failed to set role"); - } - 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.account.currentRecipientSecret(), - new Set([getAgentRecipientID(agent)]), - { - in: this.teamMap.coValue.id, - tx: this.teamMap.coValue.nextTransactionID(), - } + const agent = this.node.resolveAccountAgent( + accountID, + "Expected to know agent to add them to team" ); + map.set(accountID, role, "trusting"); + + if (map.get(accountID) !== role) { + throw new Error("Failed to set role"); + } + map.set( - "readKey", - { keyID: currentReadKey.id, revelation }, + `${currentReadKey.id}_for_${accountID}`, + seal( + currentReadKey.secret, + this.teamMap.coValue.node.account.currentRecipientSecret(), + getAgentRecipientID(agent), + { + in: this.teamMap.coValue.id, + tx: this.teamMap.coValue.nextTransactionID(), + } + ), "trusting" ); }); @@ -277,7 +287,9 @@ export class Team { const maybeCurrentReadKey = this.teamMap.coValue.getCurrentReadKey(); if (!maybeCurrentReadKey.secret) { - throw new Error("Can't rotate read key secret we don't have access to"); + throw new Error( + "Can't rotate read key secret we don't have access to" + ); } const currentReadKey = { @@ -287,41 +299,38 @@ export class Team { const newReadKey = newRandomKeySecret(); - const newReadKeyRevelation = seal( - newReadKey.secret, - this.teamMap.coValue.node.account.currentRecipientSecret(), - new Set( - currentlyPermittedReaders.map( - (reader) => { - const readerAgent = this.node.resolveAccount(reader, "Expected to know currently permitted reader"); - if (!readerAgent) { - throw new Error("Unknown agent " + reader); - } - return getAgentRecipientID(readerAgent) - } - ) - ), - { - in: this.teamMap.coValue.id, - tx: this.teamMap.coValue.nextTransactionID(), - } - ); - this.teamMap = this.teamMap.edit((map) => { + for (const readerID of currentlyPermittedReaders) { + const reader = this.node.resolveAccountAgent( + readerID, + "Expected to know currently permitted reader" + ); + + map.set( + `${newReadKey.id}_for_${readerID}`, + seal( + newReadKey.secret, + this.teamMap.coValue.node.account.currentRecipientSecret(), + getAgentRecipientID(reader), + { + in: this.teamMap.coValue.id, + tx: this.teamMap.coValue.nextTransactionID(), + } + ), + "trusting" + ); + } + map.set( - "readKey", - { - keyID: newReadKey.id, - revelation: newReadKeyRevelation, - previousKeys: { - [currentReadKey.id]: sealKeySecret({ - sealing: newReadKey, - toSeal: currentReadKey, - }).encrypted, - }, - }, + `${currentReadKey.id}_for_${newReadKey.id}`, + encryptKeySecret({ + encrypting: newReadKey, + toEncrypt: currentReadKey, + }).encrypted, "trusting" ); + + map.set("readKey", newReadKey.id, "trusting"); }); } @@ -364,3 +373,11 @@ export class Team { ); } } + +export function isKeyForKeyField(field: string): field is `${KeyID}_for_${KeyID}` { + return field.startsWith("key_") && field.includes("_for_key"); +} + +export function isKeyForAccountField(field: string): field is `${KeyID}_for_${AccountIDOrAgentID}` { + return field.startsWith("key_") && (field.includes("_for_recipient") || field.includes("_for_co")); +} \ No newline at end of file diff --git a/src/testUtils.ts b/src/testUtils.ts index ce6a8a860..9102e1b58 100644 --- a/src/testUtils.ts +++ b/src/testUtils.ts @@ -1,8 +1,9 @@ import { AgentSecret, createdNowUnique, getAgentID, newRandomAgentSecret } from "./crypto.js"; -import { SessionID, newRandomSessionID } from "./coValue.js"; +import { newRandomSessionID } from "./coValue.js"; import { LocalNode } from "./node.js"; import { expectTeamContent } from "./permissions.js"; import { AnonymousControlledAccount } from "./account.js"; +import { SessionID } from "./ids.js"; export function randomAnonymousAccountAndSessionID(): [AnonymousControlledAccount, SessionID] { const agentSecret = newRandomAgentSecret();