Implement simpler team logic without history

This commit is contained in:
Anselm
2023-08-14 17:34:00 +01:00
parent 7f0cdf1be8
commit c406f8ba2c
8 changed files with 418 additions and 313 deletions

View File

@@ -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 };
}

View File

@@ -116,6 +116,21 @@ export class CoMap<
return lastEntry.txID;
}
getLastEntry<KK extends K>(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<KK extends K>(key: KK): { at: number; txID: TransactionID; value: M[KK] | undefined; }[] {
const ops = this.ops[key];
if (!ops) {

View File

@@ -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();
});

View File

@@ -127,53 +127,40 @@ export function getAgentRecipientSecret(agentSecret: AgentSecret): RecipientSecr
return agentSecret.split("/")[0] as RecipientSecret;
}
export type SealedSet<T> = {
[recipient: RecipientID]: Sealed<T>;
};
export function seal<T extends JsonValue>(
message: T,
from: RecipientSecret,
to: Set<RecipientID>,
to: RecipientID,
nOnceMaterial: { in: RawCoValueID; tx: TransactionID }
): SealedSet<T> {
): Sealed<T> {
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<T> = {};
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<T>;
}
return sealedSet;
return `sealed_U${base64url.encode(
sealedBytes
)}` as Sealed<T>
}
export function openAs<T extends JsonValue>(
sealedSet: SealedSet<T>,
export function unseal<T extends JsonValue>(
sealed: Sealed<T>,
recipient: RecipientSecret,
from: RecipientID,
nOnceMaterial: { in: RawCoValueID; tx: TransactionID }
@@ -188,12 +175,6 @@ export function openAs<T extends JsonValue>(
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<T extends JsonValue>(
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<KeySecret, { sealed: KeyID; sealing: KeyID }>;
encryptedID: KeyID;
encryptingID: KeyID;
encrypted: Encrypted<KeySecret, { encryptedID: KeyID; encryptingID: KeyID }>;
} {
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<T extends JsonValue>(
return decrypt(encrypted, keySecret, nOnceMaterial);
}
export function unsealKeySecret(
sealedInfo: {
sealed: KeyID;
sealing: KeyID;
encrypted: Encrypted<KeySecret, { sealed: KeyID; sealing: KeyID }>;
export function decryptKeySecret(
encryptedInfo: {
encryptedID: KeyID;
encryptingID: KeyID;
encrypted: Encrypted<KeySecret, { encryptedID: KeyID; encryptingID: KeyID }>;
},
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}` {

View File

@@ -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);

View File

@@ -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");

View File

@@ -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<KeySecret>;
previousKeys?: {
[key: KeyID]: Encrypted<
KeySecret,
{ sealed: KeyID; sealing: KeyID }
>;
};
};
export type TeamContent = {
[key: AccountIDOrAgentID]: Role;
readKey: KeyID;
[revelationFor: `${KeyID}_for_${AccountIDOrAgentID}`]: Sealed<KeySecret>;
[oldKeyForNewKey: `${KeyID}_for_${KeyID}`]: Encrypted<
KeySecret,
{ encryptedID: KeyID; encryptingID: KeyID }
>;
};
export function expectTeamContent(content: ContentType): CoMap<TeamContent, {}> {
export function expectTeamContent(
content: ContentType
): CoMap<TeamContent, {}> {
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"));
}

View File

@@ -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();