cojson 0.8.44 → 0.8.48

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -3,178 +3,187 @@ import { x25519 } from "@noble/curves/ed25519";
3
3
  import { blake3 } from "@noble/hashes/blake3";
4
4
  import { base58, base64url } from "@scure/base";
5
5
  import { expect, test } from "vitest";
6
+ import { PureJSCrypto } from "../crypto/PureJSCrypto.js";
6
7
  import { WasmCrypto } from "../crypto/WasmCrypto.js";
7
8
  import { SessionID } from "../ids.js";
8
9
  import { stableStringify } from "../jsonStringify.js";
9
10
 
10
- const Crypto = await WasmCrypto.create();
11
-
12
- test("Signatures round-trip and use stable stringify", () => {
13
- const data = { b: "world", a: "hello" };
14
- const signer = Crypto.newRandomSigner();
15
- const signature = Crypto.sign(signer, data);
16
-
17
- expect(signature).toMatch(/^signature_z/);
18
- expect(
19
- Crypto.verify(
20
- signature,
21
- { a: "hello", b: "world" },
22
- Crypto.getSignerID(signer),
23
- ),
24
- ).toBe(true);
25
- });
26
-
27
- test("Invalid signatures don't verify", () => {
28
- const data = { b: "world", a: "hello" };
29
- const signer = Crypto.newRandomSigner();
30
- const signer2 = Crypto.newRandomSigner();
31
- const wrongSignature = Crypto.sign(signer2, data);
11
+ const wasmCrypto = await WasmCrypto.create();
12
+ const pureJSCrypto = await PureJSCrypto.create();
13
+
14
+ [wasmCrypto, pureJSCrypto].forEach((crypto) => {
15
+ const name = crypto.constructor.name;
16
+
17
+ test(`Signatures round-trip and use stable stringify [${name}]`, () => {
18
+ const data = { b: "world", a: "hello" };
19
+ const signer = crypto.newRandomSigner();
20
+ const signature = crypto.sign(signer, data);
21
+
22
+ expect(signature).toMatch(/^signature_z/);
23
+ expect(
24
+ crypto.verify(
25
+ signature,
26
+ { a: "hello", b: "world" },
27
+ crypto.getSignerID(signer),
28
+ ),
29
+ ).toBe(true);
30
+ });
32
31
 
33
- expect(Crypto.verify(wrongSignature, data, Crypto.getSignerID(signer))).toBe(
34
- false,
35
- );
36
- });
32
+ test(`Invalid signatures don't verify [${name}]`, () => {
33
+ const data = { b: "world", a: "hello" };
34
+ const signer = crypto.newRandomSigner();
35
+ const signer2 = crypto.newRandomSigner();
36
+ const wrongSignature = crypto.sign(signer2, data);
37
37
 
38
- test("encrypting round-trips, but invalid receiver can't unseal", () => {
39
- const data = { b: "world", a: "hello" };
40
- const sender = Crypto.newRandomSealer();
41
- const sealer = Crypto.newRandomSealer();
42
- const wrongSealer = Crypto.newRandomSealer();
43
-
44
- const nOnceMaterial = {
45
- in: "co_zTEST",
46
- tx: { sessionID: "co_zTEST_session_zTEST" as SessionID, txIndex: 0 },
47
- } as const;
48
-
49
- const sealed = Crypto.seal({
50
- message: data,
51
- from: sender,
52
- to: Crypto.getSealerID(sealer),
53
- nOnceMaterial,
38
+ expect(
39
+ crypto.verify(wrongSignature, data, crypto.getSignerID(signer)),
40
+ ).toBe(false);
54
41
  });
55
42
 
56
- expect(
57
- Crypto.unseal(sealed, sealer, Crypto.getSealerID(sender), nOnceMaterial),
58
- ).toEqual(data);
59
- expect(() =>
60
- Crypto.unseal(
61
- sealed,
62
- wrongSealer,
63
- Crypto.getSealerID(sender),
43
+ test(`encrypting round-trips, but invalid receiver can't unseal [${name}]`, () => {
44
+ const data = { b: "world", a: "hello" };
45
+ const sender = crypto.newRandomSealer();
46
+ const sealer = crypto.newRandomSealer();
47
+ const wrongSealer = crypto.newRandomSealer();
48
+
49
+ const nOnceMaterial = {
50
+ in: "co_zTEST",
51
+ tx: { sessionID: "co_zTEST_session_zTEST" as SessionID, txIndex: 0 },
52
+ } as const;
53
+
54
+ const sealed = crypto.seal({
55
+ message: data,
56
+ from: sender,
57
+ to: crypto.getSealerID(sealer),
64
58
  nOnceMaterial,
65
- ),
66
- ).toThrow(/Wrong tag/);
67
-
68
- // trying with wrong sealer secret, by hand
69
- const nOnce = blake3(
70
- new TextEncoder().encode(stableStringify(nOnceMaterial)),
71
- ).slice(0, 24);
72
- const sealer3priv = base58.decode(
73
- wrongSealer.substring("sealerSecret_z".length),
74
- );
75
- const senderPub = base58.decode(
76
- Crypto.getSealerID(sender).substring("sealer_z".length),
77
- );
78
- const sealedBytes = base64url.decode(sealed.substring("sealed_U".length));
79
- const sharedSecret = x25519.getSharedSecret(sealer3priv, senderPub);
80
-
81
- expect(() => {
82
- const _ = xsalsa20_poly1305(sharedSecret, nOnce).decrypt(sealedBytes);
83
- }).toThrow("Wrong tag");
84
- });
59
+ });
60
+
61
+ expect(
62
+ crypto.unseal(sealed, sealer, crypto.getSealerID(sender), nOnceMaterial),
63
+ ).toEqual(data);
64
+ expect(() =>
65
+ crypto.unseal(
66
+ sealed,
67
+ wrongSealer,
68
+ crypto.getSealerID(sender),
69
+ nOnceMaterial,
70
+ ),
71
+ ).toThrow(/Wrong tag/);
72
+
73
+ // trying with wrong sealer secret, by hand
74
+ const nOnce = blake3(
75
+ new TextEncoder().encode(stableStringify(nOnceMaterial)),
76
+ ).slice(0, 24);
77
+ const sealer3priv = base58.decode(
78
+ wrongSealer.substring("sealerSecret_z".length),
79
+ );
80
+ const senderPub = base58.decode(
81
+ crypto.getSealerID(sender).substring("sealer_z".length),
82
+ );
83
+ const sealedBytes = base64url.decode(sealed.substring("sealed_U".length));
84
+ const sharedSecret = x25519.getSharedSecret(sealer3priv, senderPub);
85
+
86
+ expect(() => {
87
+ const _ = xsalsa20_poly1305(sharedSecret, nOnce).decrypt(sealedBytes);
88
+ }).toThrow("Wrong tag");
89
+ });
85
90
 
86
- test("Hashing is deterministic", () => {
87
- expect(Crypto.secureHash({ b: "world", a: "hello" })).toEqual(
88
- Crypto.secureHash({ a: "hello", b: "world" }),
89
- );
91
+ test(`Hashing is deterministic [${name}]`, () => {
92
+ expect(crypto.secureHash({ b: "world", a: "hello" })).toEqual(
93
+ crypto.secureHash({ a: "hello", b: "world" }),
94
+ );
90
95
 
91
- expect(Crypto.shortHash({ b: "world", a: "hello" })).toEqual(
92
- Crypto.shortHash({ a: "hello", b: "world" }),
93
- );
94
- });
96
+ expect(crypto.shortHash({ b: "world", a: "hello" })).toEqual(
97
+ crypto.shortHash({ a: "hello", b: "world" }),
98
+ );
99
+ });
95
100
 
96
- test("Encryption for transactions round-trips", () => {
97
- const { secret } = Crypto.newRandomKeySecret();
101
+ test(`Encryption for transactions round-trips [${name}]`, () => {
102
+ const { secret } = crypto.newRandomKeySecret();
98
103
 
99
- const encrypted1 = Crypto.encryptForTransaction({ a: "hello" }, secret, {
100
- in: "co_zTEST",
101
- tx: { sessionID: "co_zTEST_session_zTEST" as SessionID, txIndex: 0 },
102
- });
104
+ const encrypted1 = crypto.encryptForTransaction({ a: "hello" }, secret, {
105
+ in: "co_zTEST",
106
+ tx: { sessionID: "co_zTEST_session_zTEST" as SessionID, txIndex: 0 },
107
+ });
103
108
 
104
- const encrypted2 = Crypto.encryptForTransaction({ b: "world" }, secret, {
105
- in: "co_zTEST",
106
- tx: { sessionID: "co_zTEST_session_zTEST" as SessionID, txIndex: 1 },
107
- });
109
+ const encrypted2 = crypto.encryptForTransaction({ b: "world" }, secret, {
110
+ in: "co_zTEST",
111
+ tx: { sessionID: "co_zTEST_session_zTEST" as SessionID, txIndex: 1 },
112
+ });
108
113
 
109
- const decrypted1 = Crypto.decryptForTransaction(encrypted1, secret, {
110
- in: "co_zTEST",
111
- tx: { sessionID: "co_zTEST_session_zTEST" as SessionID, txIndex: 0 },
112
- });
114
+ const decrypted1 = crypto.decryptForTransaction(encrypted1, secret, {
115
+ in: "co_zTEST",
116
+ tx: { sessionID: "co_zTEST_session_zTEST" as SessionID, txIndex: 0 },
117
+ });
113
118
 
114
- const decrypted2 = Crypto.decryptForTransaction(encrypted2, secret, {
115
- in: "co_zTEST",
116
- tx: { sessionID: "co_zTEST_session_zTEST" as SessionID, txIndex: 1 },
119
+ const decrypted2 = crypto.decryptForTransaction(encrypted2, secret, {
120
+ in: "co_zTEST",
121
+ tx: { sessionID: "co_zTEST_session_zTEST" as SessionID, txIndex: 1 },
122
+ });
123
+
124
+ expect([decrypted1, decrypted2]).toEqual([{ a: "hello" }, { b: "world" }]);
117
125
  });
118
126
 
119
- expect([decrypted1, decrypted2]).toEqual([{ a: "hello" }, { b: "world" }]);
120
- });
127
+ test(`Encryption for transactions doesn't decrypt with a wrong key [${name}]`, () => {
128
+ const { secret } = crypto.newRandomKeySecret();
129
+ const { secret: secret2 } = crypto.newRandomKeySecret();
121
130
 
122
- test("Encryption for transactions doesn't decrypt with a wrong key", () => {
123
- const { secret } = Crypto.newRandomKeySecret();
124
- const { secret: secret2 } = Crypto.newRandomKeySecret();
131
+ const encrypted1 = crypto.encryptForTransaction({ a: "hello" }, secret, {
132
+ in: "co_zTEST",
133
+ tx: { sessionID: "co_zTEST_session_zTEST" as SessionID, txIndex: 0 },
134
+ });
125
135
 
126
- const encrypted1 = Crypto.encryptForTransaction({ a: "hello" }, secret, {
127
- in: "co_zTEST",
128
- tx: { sessionID: "co_zTEST_session_zTEST" as SessionID, txIndex: 0 },
129
- });
136
+ const encrypted2 = crypto.encryptForTransaction({ b: "world" }, secret, {
137
+ in: "co_zTEST",
138
+ tx: { sessionID: "co_zTEST_session_zTEST" as SessionID, txIndex: 1 },
139
+ });
130
140
 
131
- const encrypted2 = Crypto.encryptForTransaction({ b: "world" }, secret, {
132
- in: "co_zTEST",
133
- tx: { sessionID: "co_zTEST_session_zTEST" as SessionID, txIndex: 1 },
134
- });
141
+ const decrypted1 = crypto.decryptForTransaction(encrypted1, secret2, {
142
+ in: "co_zTEST",
143
+ tx: { sessionID: "co_zTEST_session_zTEST" as SessionID, txIndex: 0 },
144
+ });
135
145
 
136
- const decrypted1 = Crypto.decryptForTransaction(encrypted1, secret2, {
137
- in: "co_zTEST",
138
- tx: { sessionID: "co_zTEST_session_zTEST" as SessionID, txIndex: 0 },
139
- });
146
+ const decrypted2 = crypto.decryptForTransaction(encrypted2, secret2, {
147
+ in: "co_zTEST",
148
+ tx: { sessionID: "co_zTEST_session_zTEST" as SessionID, txIndex: 1 },
149
+ });
140
150
 
141
- const decrypted2 = Crypto.decryptForTransaction(encrypted2, secret2, {
142
- in: "co_zTEST",
143
- tx: { sessionID: "co_zTEST_session_zTEST" as SessionID, txIndex: 1 },
151
+ expect([decrypted1, decrypted2]).toEqual([undefined, undefined]);
144
152
  });
145
153
 
146
- expect([decrypted1, decrypted2]).toEqual([undefined, undefined]);
147
- });
148
-
149
- test("Encryption of keySecrets round-trips", () => {
150
- const toEncrypt = Crypto.newRandomKeySecret();
151
- const encrypting = Crypto.newRandomKeySecret();
154
+ test(`Encryption of keySecrets round-trips [${name}]`, () => {
155
+ const toEncrypt = crypto.newRandomKeySecret();
156
+ const encrypting = crypto.newRandomKeySecret();
152
157
 
153
- const keys = {
154
- toEncrypt,
155
- encrypting,
156
- };
158
+ const keys = {
159
+ toEncrypt,
160
+ encrypting,
161
+ };
157
162
 
158
- const encrypted = Crypto.encryptKeySecret(keys);
163
+ const encrypted = crypto.encryptKeySecret(keys);
159
164
 
160
- const decrypted = Crypto.decryptKeySecret(encrypted, encrypting.secret);
165
+ const decrypted = crypto.decryptKeySecret(encrypted, encrypting.secret);
161
166
 
162
- expect(decrypted).toEqual(toEncrypt.secret);
163
- });
167
+ expect(decrypted).toEqual(toEncrypt.secret);
168
+ });
164
169
 
165
- test("Encryption of keySecrets doesn't decrypt with a wrong key", () => {
166
- const toEncrypt = Crypto.newRandomKeySecret();
167
- const encrypting = Crypto.newRandomKeySecret();
168
- const encryptingWrong = Crypto.newRandomKeySecret();
170
+ test(`Encryption of keySecrets doesn't decrypt with a wrong key [${name}]`, () => {
171
+ const toEncrypt = crypto.newRandomKeySecret();
172
+ const encrypting = crypto.newRandomKeySecret();
173
+ const encryptingWrong = crypto.newRandomKeySecret();
169
174
 
170
- const keys = {
171
- toEncrypt,
172
- encrypting,
173
- };
175
+ const keys = {
176
+ toEncrypt,
177
+ encrypting,
178
+ };
174
179
 
175
- const encrypted = Crypto.encryptKeySecret(keys);
180
+ const encrypted = crypto.encryptKeySecret(keys);
176
181
 
177
- const decrypted = Crypto.decryptKeySecret(encrypted, encryptingWrong.secret);
182
+ const decrypted = crypto.decryptKeySecret(
183
+ encrypted,
184
+ encryptingWrong.secret,
185
+ );
178
186
 
179
- expect(decrypted).toBeUndefined();
187
+ expect(decrypted).toBeUndefined();
188
+ });
180
189
  });
@@ -1,4 +1,5 @@
1
1
  import { describe, expect, test } from "vitest";
2
+ import { CoValueState } from "../coValueState.js";
2
3
  import { RawCoList } from "../coValues/coList.js";
3
4
  import { RawCoMap } from "../coValues/coMap.js";
4
5
  import { RawCoStream } from "../coValues/coStream.js";
@@ -492,4 +493,120 @@ describe("writeOnly", () => {
492
493
  // The writer role should be able to see the edits from the admin
493
494
  expect(mapOnNode2.get("test")).toEqual("Written from the admin");
494
495
  });
496
+
497
+ test("upgrade to writer roles should work correctly", async () => {
498
+ const { node1, node2 } = await createTwoConnectedNodes("server", "server");
499
+
500
+ const group = node1.node.createGroup();
501
+ group.addMember(
502
+ await loadCoValueOrFail(node1.node, node2.accountID),
503
+ "writeOnly",
504
+ );
505
+
506
+ await group.core.waitForSync();
507
+
508
+ const groupOnNode2 = await loadCoValueOrFail(node2.node, group.id);
509
+ const map = groupOnNode2.createMap();
510
+ map.set("test", "Written from the writeOnly member");
511
+
512
+ await map.core.waitForSync();
513
+
514
+ group.addMember(
515
+ await loadCoValueOrFail(node1.node, node2.accountID),
516
+ "writer",
517
+ );
518
+
519
+ group.core.waitForSync();
520
+
521
+ node2.node.coValuesStore.coValues.delete(map.id);
522
+ expect(node2.node.coValuesStore.get(map.id)).toEqual(
523
+ CoValueState.Unknown(map.id),
524
+ );
525
+
526
+ const mapOnNode2 = await loadCoValueOrFail(node2.node, map.id);
527
+
528
+ // The writer role should be able to see the edits from the admin
529
+ expect(mapOnNode2.get("test")).toEqual("Written from the writeOnly member");
530
+ });
531
+
532
+ test("a user should be able to extend a group when his role on the parent group is writer", async () => {
533
+ const { node1, node2 } = await createTwoConnectedNodes("server", "server");
534
+
535
+ const group = node1.node.createGroup();
536
+ group.addMember(
537
+ await loadCoValueOrFail(node1.node, node2.accountID),
538
+ "writer",
539
+ );
540
+
541
+ await group.core.waitForSync();
542
+
543
+ const groupOnNode2 = await loadCoValueOrFail(node2.node, group.id);
544
+
545
+ const childGroup = node2.node.createGroup();
546
+ childGroup.extend(groupOnNode2);
547
+
548
+ const map = childGroup.createMap();
549
+ map.set("test", "Written from node2");
550
+
551
+ await map.core.waitForSync();
552
+ await childGroup.core.waitForSync();
553
+
554
+ const mapOnNode2 = await loadCoValueOrFail(node2.node, map.id);
555
+
556
+ expect(mapOnNode2.get("test")).toEqual("Written from node2");
557
+ });
558
+
559
+ test("a user should be able to extend a group when his role on the parent group is reader", async () => {
560
+ const { node1, node2 } = await createTwoConnectedNodes("server", "server");
561
+
562
+ const group = node1.node.createGroup();
563
+ group.addMember(
564
+ await loadCoValueOrFail(node1.node, node2.accountID),
565
+ "reader",
566
+ );
567
+
568
+ await group.core.waitForSync();
569
+
570
+ const groupOnNode2 = await loadCoValueOrFail(node2.node, group.id);
571
+
572
+ const childGroup = node2.node.createGroup();
573
+ childGroup.extend(groupOnNode2);
574
+
575
+ const map = childGroup.createMap();
576
+ map.set("test", "Written from node2");
577
+
578
+ await map.core.waitForSync();
579
+ await childGroup.core.waitForSync();
580
+
581
+ const mapOnNode2 = await loadCoValueOrFail(node2.node, map.id);
582
+
583
+ expect(mapOnNode2.get("test")).toEqual("Written from node2");
584
+ });
585
+
586
+ test("a user should be able to extend a group when his role on the parent group is writeOnly", async () => {
587
+ const { node1, node2 } = await createTwoConnectedNodes("server", "server");
588
+
589
+ const group = node1.node.createGroup();
590
+ group.addMember(
591
+ await loadCoValueOrFail(node1.node, node2.accountID),
592
+ "writeOnly",
593
+ );
594
+
595
+ await group.core.waitForSync();
596
+
597
+ const groupOnNode2 = await loadCoValueOrFail(node2.node, group.id);
598
+
599
+ const childGroup = node2.node.createGroup();
600
+ childGroup.extend(groupOnNode2);
601
+
602
+ const map = childGroup.createMap();
603
+ map.set("test", "Written from node2");
604
+
605
+ await map.core.waitForSync();
606
+ await childGroup.core.waitForSync();
607
+
608
+ const mapOnNode2 = await loadCoValueOrFail(node2.node, map.id);
609
+
610
+ expect(mapOnNode2.get("test")).toEqual("Written from node2");
611
+ });
495
612
  });
@@ -2157,21 +2157,17 @@ test("Admins can set child extensions when the admin role is inherited", async (
2157
2157
  );
2158
2158
  });
2159
2159
 
2160
- test("Writers, readers and invitees can not set child extensions", () => {
2160
+ test("Writers, readers and writeOnly can set child extensions", () => {
2161
2161
  const { group, node } = newGroupHighLevel();
2162
2162
  const childGroup = node.createGroup();
2163
2163
 
2164
2164
  const writer = node.createAccount();
2165
2165
  const reader = node.createAccount();
2166
- const adminInvite = node.createAccount();
2167
- const writerInvite = node.createAccount();
2168
- const readerInvite = node.createAccount();
2166
+ const writeOnly = node.createAccount();
2169
2167
 
2170
2168
  group.addMember(writer, "writer");
2171
2169
  group.addMember(reader, "reader");
2172
- group.addMember(adminInvite, "adminInvite");
2173
- group.addMember(writerInvite, "writerInvite");
2174
- group.addMember(readerInvite, "readerInvite");
2170
+ group.addMember(writeOnly, "writeOnly");
2175
2171
 
2176
2172
  const groupAsWriter = expectGroup(
2177
2173
  group.core
@@ -2180,7 +2176,7 @@ test("Writers, readers and invitees can not set child extensions", () => {
2180
2176
  );
2181
2177
 
2182
2178
  groupAsWriter.set(`child_${childGroup.id}`, "extend", "trusting");
2183
- expect(groupAsWriter.get(`child_${childGroup.id}`)).toBeUndefined();
2179
+ expect(groupAsWriter.get(`child_${childGroup.id}`)).toEqual("extend");
2184
2180
 
2185
2181
  const groupAsReader = expectGroup(
2186
2182
  group.core
@@ -2189,7 +2185,20 @@ test("Writers, readers and invitees can not set child extensions", () => {
2189
2185
  );
2190
2186
 
2191
2187
  groupAsReader.set(`child_${childGroup.id}`, "extend", "trusting");
2192
- expect(groupAsReader.get(`child_${childGroup.id}`)).toBeUndefined();
2188
+ expect(groupAsReader.get(`child_${childGroup.id}`)).toEqual("extend");
2189
+ });
2190
+
2191
+ test("Invitees can not set child extensions", () => {
2192
+ const { group, node } = newGroupHighLevel();
2193
+ const childGroup = node.createGroup();
2194
+
2195
+ const adminInvite = node.createAccount();
2196
+ const writerInvite = node.createAccount();
2197
+ const readerInvite = node.createAccount();
2198
+
2199
+ group.addMember(adminInvite, "adminInvite");
2200
+ group.addMember(writerInvite, "writerInvite");
2201
+ group.addMember(readerInvite, "readerInvite");
2193
2202
 
2194
2203
  const groupAsAdminInvite = expectGroup(
2195
2204
  group.core