cojson 0.17.8 → 0.17.10
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.
- package/.turbo/turbo-build.log +1 -1
- package/CHANGELOG.md +13 -0
- package/dist/coValueCore/SessionMap.d.ts +44 -0
- package/dist/coValueCore/SessionMap.d.ts.map +1 -0
- package/dist/coValueCore/SessionMap.js +118 -0
- package/dist/coValueCore/SessionMap.js.map +1 -0
- package/dist/coValueCore/coValueCore.d.ts +1 -0
- package/dist/coValueCore/coValueCore.d.ts.map +1 -1
- package/dist/coValueCore/coValueCore.js +40 -61
- package/dist/coValueCore/coValueCore.js.map +1 -1
- package/dist/coValueCore/verifiedState.d.ts +14 -18
- package/dist/coValueCore/verifiedState.d.ts.map +1 -1
- package/dist/coValueCore/verifiedState.js +23 -86
- package/dist/coValueCore/verifiedState.js.map +1 -1
- package/dist/coValues/account.d.ts +4 -0
- package/dist/coValues/account.d.ts.map +1 -1
- package/dist/coValues/account.js +24 -4
- package/dist/coValues/account.js.map +1 -1
- package/dist/crypto/PureJSCrypto.d.ts +31 -3
- package/dist/crypto/PureJSCrypto.d.ts.map +1 -1
- package/dist/crypto/PureJSCrypto.js +112 -0
- package/dist/crypto/PureJSCrypto.js.map +1 -1
- package/dist/crypto/WasmCrypto.d.ts +23 -4
- package/dist/crypto/WasmCrypto.d.ts.map +1 -1
- package/dist/crypto/WasmCrypto.js +44 -2
- package/dist/crypto/WasmCrypto.js.map +1 -1
- package/dist/crypto/crypto.d.ts +17 -1
- package/dist/crypto/crypto.d.ts.map +1 -1
- package/dist/crypto/crypto.js.map +1 -1
- package/dist/localNode.d.ts +1 -0
- package/dist/localNode.d.ts.map +1 -1
- package/dist/localNode.js +10 -5
- package/dist/localNode.js.map +1 -1
- package/dist/sync.d.ts +2 -0
- package/dist/sync.d.ts.map +1 -1
- package/dist/sync.js +8 -1
- package/dist/sync.js.map +1 -1
- package/dist/tests/PureJSCrypto.test.d.ts +2 -0
- package/dist/tests/PureJSCrypto.test.d.ts.map +1 -0
- package/dist/tests/PureJSCrypto.test.js +88 -0
- package/dist/tests/PureJSCrypto.test.js.map +1 -0
- package/dist/tests/WasmCrypto.test.d.ts +2 -0
- package/dist/tests/WasmCrypto.test.d.ts.map +1 -0
- package/dist/tests/WasmCrypto.test.js +88 -0
- package/dist/tests/WasmCrypto.test.js.map +1 -0
- package/dist/tests/coValueCore.test.js +62 -187
- package/dist/tests/coValueCore.test.js.map +1 -1
- package/dist/tests/coreWasm.test.d.ts +2 -0
- package/dist/tests/coreWasm.test.d.ts.map +1 -0
- package/dist/tests/coreWasm.test.js +80 -0
- package/dist/tests/coreWasm.test.js.map +1 -0
- package/dist/tests/sync.test.js +19 -1
- package/dist/tests/sync.test.js.map +1 -1
- package/dist/tests/testUtils.d.ts +3 -0
- package/dist/tests/testUtils.d.ts.map +1 -1
- package/dist/tests/testUtils.js +4 -1
- package/dist/tests/testUtils.js.map +1 -1
- package/package.json +3 -3
- package/src/coValueCore/SessionMap.ts +230 -0
- package/src/coValueCore/coValueCore.ts +66 -91
- package/src/coValueCore/verifiedState.ts +60 -129
- package/src/coValues/account.ts +28 -4
- package/src/crypto/PureJSCrypto.ts +202 -2
- package/src/crypto/WasmCrypto.ts +95 -4
- package/src/crypto/crypto.ts +38 -1
- package/src/localNode.ts +18 -10
- package/src/sync.ts +10 -0
- package/src/tests/PureJSCrypto.test.ts +130 -0
- package/src/tests/WasmCrypto.test.ts +130 -0
- package/src/tests/coValueCore.test.ts +84 -292
- package/src/tests/coreWasm.test.ts +142 -0
- package/src/tests/sync.test.ts +32 -0
- package/src/tests/testUtils.ts +9 -1
|
@@ -3,23 +3,32 @@ import { ed25519, x25519 } from "@noble/curves/ed25519";
|
|
|
3
3
|
import { blake3 } from "@noble/hashes/blake3";
|
|
4
4
|
import { base58 } from "@scure/base";
|
|
5
5
|
import { base64URLtoBytes, bytesToBase64url } from "../base64url.js";
|
|
6
|
-
import {
|
|
6
|
+
import {
|
|
7
|
+
PrivateTransaction,
|
|
8
|
+
Transaction,
|
|
9
|
+
TrustingTransaction,
|
|
10
|
+
} from "../coValueCore/verifiedState.js";
|
|
11
|
+
import { RawCoID, SessionID, TransactionID } from "../ids.js";
|
|
7
12
|
import { Stringified, stableStringify } from "../jsonStringify.js";
|
|
8
13
|
import { JsonValue } from "../jsonValue.js";
|
|
9
14
|
import { logger } from "../logger.js";
|
|
10
15
|
import {
|
|
11
16
|
CryptoProvider,
|
|
12
17
|
Encrypted,
|
|
18
|
+
KeyID,
|
|
13
19
|
KeySecret,
|
|
14
20
|
Sealed,
|
|
15
21
|
SealerID,
|
|
16
22
|
SealerSecret,
|
|
23
|
+
SessionLogImpl,
|
|
17
24
|
Signature,
|
|
18
25
|
SignerID,
|
|
19
26
|
SignerSecret,
|
|
27
|
+
StreamingHash,
|
|
20
28
|
textDecoder,
|
|
21
29
|
textEncoder,
|
|
22
30
|
} from "./crypto.js";
|
|
31
|
+
import { ControlledAccountOrAgent } from "../coValues/account.js";
|
|
23
32
|
|
|
24
33
|
type Blake3State = ReturnType<typeof blake3.create>;
|
|
25
34
|
|
|
@@ -67,7 +76,7 @@ export class PureJSCrypto extends CryptoProvider<Blake3State> {
|
|
|
67
76
|
return this.blake3HashOnce(input).slice(0, 24);
|
|
68
77
|
}
|
|
69
78
|
|
|
70
|
-
|
|
79
|
+
generateJsonNonce(material: JsonValue): Uint8Array {
|
|
71
80
|
return this.generateNonce(textEncoder.encode(stableStringify(material)));
|
|
72
81
|
}
|
|
73
82
|
|
|
@@ -199,4 +208,195 @@ export class PureJSCrypto extends CryptoProvider<Blake3State> {
|
|
|
199
208
|
return undefined;
|
|
200
209
|
}
|
|
201
210
|
}
|
|
211
|
+
|
|
212
|
+
createSessionLog(
|
|
213
|
+
coID: RawCoID,
|
|
214
|
+
sessionID: SessionID,
|
|
215
|
+
signerID: SignerID,
|
|
216
|
+
): SessionLogImpl {
|
|
217
|
+
return new PureJSSessionLog(coID, sessionID, signerID, this);
|
|
218
|
+
}
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
export class PureJSSessionLog implements SessionLogImpl {
|
|
222
|
+
transactions: string[] = [];
|
|
223
|
+
lastSignature: Signature | undefined;
|
|
224
|
+
streamingHash: Blake3State;
|
|
225
|
+
|
|
226
|
+
constructor(
|
|
227
|
+
private readonly coID: RawCoID,
|
|
228
|
+
private readonly sessionID: SessionID,
|
|
229
|
+
private readonly signerID: SignerID,
|
|
230
|
+
private readonly crypto: PureJSCrypto,
|
|
231
|
+
) {
|
|
232
|
+
this.streamingHash = this.crypto.emptyBlake3State();
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
clone(): SessionLogImpl {
|
|
236
|
+
const newLog = new PureJSSessionLog(
|
|
237
|
+
this.coID,
|
|
238
|
+
this.sessionID,
|
|
239
|
+
this.signerID,
|
|
240
|
+
this.crypto,
|
|
241
|
+
);
|
|
242
|
+
newLog.transactions = this.transactions.slice();
|
|
243
|
+
newLog.lastSignature = this.lastSignature;
|
|
244
|
+
newLog.streamingHash = this.crypto.cloneBlake3State(this.streamingHash);
|
|
245
|
+
return newLog;
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
tryAdd(
|
|
249
|
+
transactions: Transaction[],
|
|
250
|
+
newSignature: Signature,
|
|
251
|
+
skipVerify: boolean,
|
|
252
|
+
): void {
|
|
253
|
+
this.internalTryAdd(
|
|
254
|
+
transactions.map((tx) => stableStringify(tx)),
|
|
255
|
+
newSignature,
|
|
256
|
+
skipVerify,
|
|
257
|
+
);
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
internalTryAdd(
|
|
261
|
+
transactions: string[],
|
|
262
|
+
newSignature: Signature,
|
|
263
|
+
skipVerify: boolean,
|
|
264
|
+
) {
|
|
265
|
+
if (!skipVerify) {
|
|
266
|
+
const checkHasher = this.crypto.cloneBlake3State(this.streamingHash);
|
|
267
|
+
|
|
268
|
+
for (const tx of transactions) {
|
|
269
|
+
checkHasher.update(textEncoder.encode(tx));
|
|
270
|
+
}
|
|
271
|
+
const newHash = checkHasher.digest();
|
|
272
|
+
const newHashEncoded = `hash_z${base58.encode(newHash)}`;
|
|
273
|
+
|
|
274
|
+
if (!this.crypto.verify(newSignature, newHashEncoded, this.signerID)) {
|
|
275
|
+
throw new Error("Signature verification failed");
|
|
276
|
+
}
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
for (const tx of transactions) {
|
|
280
|
+
this.crypto.blake3IncrementalUpdate(
|
|
281
|
+
this.streamingHash,
|
|
282
|
+
textEncoder.encode(tx),
|
|
283
|
+
);
|
|
284
|
+
this.transactions.push(tx);
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
this.lastSignature = newSignature;
|
|
288
|
+
|
|
289
|
+
return newSignature;
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
expectedHashAfter(transactionsJson: string[]): string {
|
|
293
|
+
const hasher = this.crypto.cloneBlake3State(this.streamingHash);
|
|
294
|
+
for (const tx of transactionsJson) {
|
|
295
|
+
hasher.update(textEncoder.encode(tx));
|
|
296
|
+
}
|
|
297
|
+
const newHash = hasher.digest();
|
|
298
|
+
return `hash_z${base58.encode(newHash)}`;
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
internalAddNewTransaction(
|
|
302
|
+
transaction: string,
|
|
303
|
+
signerAgent: ControlledAccountOrAgent,
|
|
304
|
+
) {
|
|
305
|
+
this.crypto.blake3IncrementalUpdate(
|
|
306
|
+
this.streamingHash,
|
|
307
|
+
textEncoder.encode(transaction),
|
|
308
|
+
);
|
|
309
|
+
const newHash = this.crypto.blake3DigestForState(this.streamingHash);
|
|
310
|
+
const newHashEncoded = `hash_z${base58.encode(newHash)}`;
|
|
311
|
+
const signature = this.crypto.sign(
|
|
312
|
+
signerAgent.currentSignerSecret(),
|
|
313
|
+
newHashEncoded,
|
|
314
|
+
);
|
|
315
|
+
this.transactions.push(transaction);
|
|
316
|
+
this.lastSignature = signature;
|
|
317
|
+
|
|
318
|
+
return signature;
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
addNewPrivateTransaction(
|
|
322
|
+
signerAgent: ControlledAccountOrAgent,
|
|
323
|
+
changes: JsonValue[],
|
|
324
|
+
keyID: KeyID,
|
|
325
|
+
keySecret: KeySecret,
|
|
326
|
+
madeAt: number,
|
|
327
|
+
): { signature: Signature; transaction: PrivateTransaction } {
|
|
328
|
+
const encryptedChanges = this.crypto.encrypt(changes, keySecret, {
|
|
329
|
+
in: this.coID,
|
|
330
|
+
tx: { sessionID: this.sessionID, txIndex: this.transactions.length },
|
|
331
|
+
});
|
|
332
|
+
const tx = {
|
|
333
|
+
encryptedChanges: encryptedChanges,
|
|
334
|
+
madeAt: madeAt,
|
|
335
|
+
privacy: "private",
|
|
336
|
+
keyUsed: keyID,
|
|
337
|
+
} satisfies Transaction;
|
|
338
|
+
const signature = this.internalAddNewTransaction(
|
|
339
|
+
stableStringify(tx),
|
|
340
|
+
signerAgent,
|
|
341
|
+
);
|
|
342
|
+
return {
|
|
343
|
+
signature: signature as Signature,
|
|
344
|
+
transaction: tx,
|
|
345
|
+
};
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
addNewTrustingTransaction(
|
|
349
|
+
signerAgent: ControlledAccountOrAgent,
|
|
350
|
+
changes: JsonValue[],
|
|
351
|
+
madeAt: number,
|
|
352
|
+
): { signature: Signature; transaction: TrustingTransaction } {
|
|
353
|
+
const tx = {
|
|
354
|
+
changes: stableStringify(changes),
|
|
355
|
+
madeAt: madeAt,
|
|
356
|
+
privacy: "trusting",
|
|
357
|
+
} satisfies Transaction;
|
|
358
|
+
const signature = this.internalAddNewTransaction(
|
|
359
|
+
stableStringify(tx),
|
|
360
|
+
signerAgent,
|
|
361
|
+
);
|
|
362
|
+
return {
|
|
363
|
+
signature: signature as Signature,
|
|
364
|
+
transaction: tx,
|
|
365
|
+
};
|
|
366
|
+
}
|
|
367
|
+
|
|
368
|
+
decryptNextTransactionChangesJson(
|
|
369
|
+
txIndex: number,
|
|
370
|
+
keySecret: KeySecret,
|
|
371
|
+
): string {
|
|
372
|
+
const txJson = this.transactions[txIndex];
|
|
373
|
+
if (!txJson) {
|
|
374
|
+
throw new Error("Transaction not found");
|
|
375
|
+
}
|
|
376
|
+
const tx = JSON.parse(txJson) as Transaction;
|
|
377
|
+
if (tx.privacy === "private") {
|
|
378
|
+
const nOnceMaterial = {
|
|
379
|
+
in: this.coID,
|
|
380
|
+
tx: { sessionID: this.sessionID, txIndex: txIndex },
|
|
381
|
+
};
|
|
382
|
+
|
|
383
|
+
const nOnce = this.crypto.generateJsonNonce(nOnceMaterial);
|
|
384
|
+
|
|
385
|
+
const ciphertext = base64URLtoBytes(
|
|
386
|
+
tx.encryptedChanges.substring("encrypted_U".length),
|
|
387
|
+
);
|
|
388
|
+
const keySecretBytes = base58.decode(
|
|
389
|
+
keySecret.substring("keySecret_z".length),
|
|
390
|
+
);
|
|
391
|
+
const plaintext = xsalsa20(keySecretBytes, nOnce, ciphertext);
|
|
392
|
+
|
|
393
|
+
return textDecoder.decode(plaintext);
|
|
394
|
+
} else {
|
|
395
|
+
return tx.changes;
|
|
396
|
+
}
|
|
397
|
+
}
|
|
398
|
+
|
|
399
|
+
free(): void {
|
|
400
|
+
// no-op
|
|
401
|
+
}
|
|
202
402
|
}
|
package/src/crypto/WasmCrypto.ts
CHANGED
|
@@ -1,4 +1,6 @@
|
|
|
1
1
|
import {
|
|
2
|
+
SessionLog,
|
|
3
|
+
initialize,
|
|
2
4
|
Blake3Hasher,
|
|
3
5
|
blake3_empty_state,
|
|
4
6
|
blake3_hash_once,
|
|
@@ -7,16 +9,15 @@ import {
|
|
|
7
9
|
encrypt,
|
|
8
10
|
get_sealer_id,
|
|
9
11
|
get_signer_id,
|
|
10
|
-
initialize,
|
|
11
12
|
new_ed25519_signing_key,
|
|
12
13
|
new_x25519_private_key,
|
|
13
14
|
seal,
|
|
14
15
|
sign,
|
|
15
16
|
unseal,
|
|
16
17
|
verify,
|
|
17
|
-
} from "
|
|
18
|
+
} from "cojson-core-wasm";
|
|
18
19
|
import { base64URLtoBytes, bytesToBase64url } from "../base64url.js";
|
|
19
|
-
import { RawCoID, TransactionID } from "../ids.js";
|
|
20
|
+
import { RawCoID, SessionID, TransactionID } from "../ids.js";
|
|
20
21
|
import { Stringified, stableStringify } from "../jsonStringify.js";
|
|
21
22
|
import { JsonValue } from "../jsonValue.js";
|
|
22
23
|
import { logger } from "../logger.js";
|
|
@@ -24,6 +25,8 @@ import { PureJSCrypto } from "./PureJSCrypto.js";
|
|
|
24
25
|
import {
|
|
25
26
|
CryptoProvider,
|
|
26
27
|
Encrypted,
|
|
28
|
+
Hash,
|
|
29
|
+
KeyID,
|
|
27
30
|
KeySecret,
|
|
28
31
|
Sealed,
|
|
29
32
|
SealerID,
|
|
@@ -34,11 +37,17 @@ import {
|
|
|
34
37
|
textDecoder,
|
|
35
38
|
textEncoder,
|
|
36
39
|
} from "./crypto.js";
|
|
40
|
+
import { ControlledAccountOrAgent } from "../coValues/account.js";
|
|
41
|
+
import {
|
|
42
|
+
PrivateTransaction,
|
|
43
|
+
Transaction,
|
|
44
|
+
TrustingTransaction,
|
|
45
|
+
} from "../coValueCore/verifiedState.js";
|
|
37
46
|
|
|
38
47
|
type Blake3State = Blake3Hasher;
|
|
39
48
|
|
|
40
49
|
/**
|
|
41
|
-
* WebAssembly implementation of the CryptoProvider interface using
|
|
50
|
+
* WebAssembly implementation of the CryptoProvider interface using cojson-core-wasm.
|
|
42
51
|
* This provides the primary implementation using WebAssembly for optimal performance, offering:
|
|
43
52
|
* - Signing/verifying (Ed25519)
|
|
44
53
|
* - Encryption/decryption (XSalsa20)
|
|
@@ -195,4 +204,86 @@ export class WasmCrypto extends CryptoProvider<Blake3State> {
|
|
|
195
204
|
return undefined;
|
|
196
205
|
}
|
|
197
206
|
}
|
|
207
|
+
|
|
208
|
+
createSessionLog(coID: RawCoID, sessionID: SessionID, signerID: SignerID) {
|
|
209
|
+
return new SessionLogAdapter(new SessionLog(coID, sessionID, signerID));
|
|
210
|
+
}
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
class SessionLogAdapter {
|
|
214
|
+
constructor(private readonly sessionLog: SessionLog) {}
|
|
215
|
+
|
|
216
|
+
tryAdd(
|
|
217
|
+
transactions: Transaction[],
|
|
218
|
+
newSignature: Signature,
|
|
219
|
+
skipVerify: boolean,
|
|
220
|
+
): void {
|
|
221
|
+
this.sessionLog.tryAdd(
|
|
222
|
+
transactions.map((tx) => stableStringify(tx)),
|
|
223
|
+
newSignature,
|
|
224
|
+
skipVerify,
|
|
225
|
+
);
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
addNewPrivateTransaction(
|
|
229
|
+
signerAgent: ControlledAccountOrAgent,
|
|
230
|
+
changes: JsonValue[],
|
|
231
|
+
keyID: KeyID,
|
|
232
|
+
keySecret: KeySecret,
|
|
233
|
+
madeAt: number,
|
|
234
|
+
): { signature: Signature; transaction: PrivateTransaction } {
|
|
235
|
+
const output = this.sessionLog.addNewPrivateTransaction(
|
|
236
|
+
stableStringify(changes),
|
|
237
|
+
signerAgent.currentSignerSecret(),
|
|
238
|
+
keySecret,
|
|
239
|
+
keyID,
|
|
240
|
+
madeAt,
|
|
241
|
+
);
|
|
242
|
+
const parsedOutput = JSON.parse(output);
|
|
243
|
+
const transaction: PrivateTransaction = {
|
|
244
|
+
privacy: "private",
|
|
245
|
+
madeAt,
|
|
246
|
+
encryptedChanges: parsedOutput.encrypted_changes,
|
|
247
|
+
keyUsed: keyID,
|
|
248
|
+
};
|
|
249
|
+
return { signature: parsedOutput.signature, transaction };
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
addNewTrustingTransaction(
|
|
253
|
+
signerAgent: ControlledAccountOrAgent,
|
|
254
|
+
changes: JsonValue[],
|
|
255
|
+
madeAt: number,
|
|
256
|
+
): { signature: Signature; transaction: TrustingTransaction } {
|
|
257
|
+
const stringifiedChanges = stableStringify(changes);
|
|
258
|
+
const output = this.sessionLog.addNewTrustingTransaction(
|
|
259
|
+
stringifiedChanges,
|
|
260
|
+
signerAgent.currentSignerSecret(),
|
|
261
|
+
madeAt,
|
|
262
|
+
);
|
|
263
|
+
const transaction: TrustingTransaction = {
|
|
264
|
+
privacy: "trusting",
|
|
265
|
+
madeAt,
|
|
266
|
+
changes: stringifiedChanges,
|
|
267
|
+
};
|
|
268
|
+
return { signature: output as Signature, transaction };
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
decryptNextTransactionChangesJson(
|
|
272
|
+
txIndex: number,
|
|
273
|
+
keySecret: KeySecret,
|
|
274
|
+
): string {
|
|
275
|
+
const output = this.sessionLog.decryptNextTransactionChangesJson(
|
|
276
|
+
txIndex,
|
|
277
|
+
keySecret,
|
|
278
|
+
);
|
|
279
|
+
return output;
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
free() {
|
|
283
|
+
this.sessionLog.free();
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
clone(): SessionLogAdapter {
|
|
287
|
+
return new SessionLogAdapter(this.sessionLog.clone());
|
|
288
|
+
}
|
|
198
289
|
}
|
package/src/crypto/crypto.ts
CHANGED
|
@@ -1,10 +1,15 @@
|
|
|
1
1
|
import { base58 } from "@scure/base";
|
|
2
|
-
import { RawAccountID } from "../coValues/account.js";
|
|
2
|
+
import { ControlledAccountOrAgent, RawAccountID } from "../coValues/account.js";
|
|
3
3
|
import { AgentID, RawCoID, TransactionID } from "../ids.js";
|
|
4
4
|
import { SessionID } from "../ids.js";
|
|
5
5
|
import { Stringified, parseJSON, stableStringify } from "../jsonStringify.js";
|
|
6
6
|
import { JsonValue } from "../jsonValue.js";
|
|
7
7
|
import { logger } from "../logger.js";
|
|
8
|
+
import {
|
|
9
|
+
PrivateTransaction,
|
|
10
|
+
Transaction,
|
|
11
|
+
TrustingTransaction,
|
|
12
|
+
} from "../coValueCore/verifiedState.js";
|
|
8
13
|
|
|
9
14
|
function randomBytes(bytesLength = 32): Uint8Array {
|
|
10
15
|
return crypto.getRandomValues(new Uint8Array(bytesLength));
|
|
@@ -297,6 +302,12 @@ export abstract class CryptoProvider<Blake3State = any> {
|
|
|
297
302
|
newRandomSessionID(accountID: RawAccountID | AgentID): SessionID {
|
|
298
303
|
return `${accountID}_session_z${base58.encode(this.randomBytes(8))}`;
|
|
299
304
|
}
|
|
305
|
+
|
|
306
|
+
abstract createSessionLog(
|
|
307
|
+
coID: RawCoID,
|
|
308
|
+
sessionID: SessionID,
|
|
309
|
+
signerID: SignerID,
|
|
310
|
+
): SessionLogImpl;
|
|
300
311
|
}
|
|
301
312
|
|
|
302
313
|
export type Hash = `hash_z${string}`;
|
|
@@ -341,3 +352,29 @@ export type KeySecret = `keySecret_z${string}`;
|
|
|
341
352
|
export type KeyID = `key_z${string}`;
|
|
342
353
|
|
|
343
354
|
export const secretSeedLength = 32;
|
|
355
|
+
|
|
356
|
+
export interface SessionLogImpl {
|
|
357
|
+
clone(): SessionLogImpl;
|
|
358
|
+
tryAdd(
|
|
359
|
+
transactions: Transaction[],
|
|
360
|
+
newSignature: Signature,
|
|
361
|
+
skipVerify: boolean,
|
|
362
|
+
): void;
|
|
363
|
+
addNewPrivateTransaction(
|
|
364
|
+
signerAgent: ControlledAccountOrAgent,
|
|
365
|
+
changes: JsonValue[],
|
|
366
|
+
keyID: KeyID,
|
|
367
|
+
keySecret: KeySecret,
|
|
368
|
+
madeAt: number,
|
|
369
|
+
): { signature: Signature; transaction: PrivateTransaction };
|
|
370
|
+
addNewTrustingTransaction(
|
|
371
|
+
signerAgent: ControlledAccountOrAgent,
|
|
372
|
+
changes: JsonValue[],
|
|
373
|
+
madeAt: number,
|
|
374
|
+
): { signature: Signature; transaction: TrustingTransaction };
|
|
375
|
+
decryptNextTransactionChangesJson(
|
|
376
|
+
tx_index: number,
|
|
377
|
+
key_secret: KeySecret,
|
|
378
|
+
): string;
|
|
379
|
+
free(): void;
|
|
380
|
+
}
|
package/src/localNode.ts
CHANGED
|
@@ -132,17 +132,25 @@ export class LocalNode {
|
|
|
132
132
|
return accountOrAgentIDfromSessionID(this.currentSessionID);
|
|
133
133
|
}
|
|
134
134
|
|
|
135
|
+
_cachedCurrentAgent: ControlledAccountOrAgent | undefined;
|
|
135
136
|
getCurrentAgent(): ControlledAccountOrAgent {
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
137
|
+
if (!this._cachedCurrentAgent) {
|
|
138
|
+
const accountOrAgent = this.getCurrentAccountOrAgentID();
|
|
139
|
+
if (isAgentID(accountOrAgent)) {
|
|
140
|
+
this._cachedCurrentAgent = new ControlledAgent(
|
|
141
|
+
this.agentSecret,
|
|
142
|
+
this.crypto,
|
|
143
|
+
);
|
|
144
|
+
} else {
|
|
145
|
+
this._cachedCurrentAgent = new ControlledAccount(
|
|
146
|
+
expectAccount(
|
|
147
|
+
this.expectCoValueLoaded(accountOrAgent).getCurrentContent(),
|
|
148
|
+
),
|
|
149
|
+
this.agentSecret,
|
|
150
|
+
);
|
|
151
|
+
}
|
|
139
152
|
}
|
|
140
|
-
return
|
|
141
|
-
expectAccount(
|
|
142
|
-
this.expectCoValueLoaded(accountOrAgent).getCurrentContent(),
|
|
143
|
-
),
|
|
144
|
-
this.agentSecret,
|
|
145
|
-
);
|
|
153
|
+
return this._cachedCurrentAgent;
|
|
146
154
|
}
|
|
147
155
|
|
|
148
156
|
expectCurrentAccountID(reason: string): RawAccountID {
|
|
@@ -360,7 +368,7 @@ export class LocalNode {
|
|
|
360
368
|
|
|
361
369
|
const coValue = this.putCoValue(
|
|
362
370
|
id,
|
|
363
|
-
new VerifiedState(id, this.crypto, header
|
|
371
|
+
new VerifiedState(id, this.crypto, header),
|
|
364
372
|
);
|
|
365
373
|
|
|
366
374
|
this.garbageCollector?.trackCoValueAccess(coValue);
|
package/src/sync.ts
CHANGED
|
@@ -132,6 +132,11 @@ export class SyncManager {
|
|
|
132
132
|
peers: { [key: PeerID]: PeerState } = {};
|
|
133
133
|
local: LocalNode;
|
|
134
134
|
|
|
135
|
+
// When true, transactions will not be verified.
|
|
136
|
+
// This is useful when syncing only for storage purposes, with the expectation that
|
|
137
|
+
// the transactions have already been verified by the [trusted] peer that sent them.
|
|
138
|
+
private skipVerify: boolean = false;
|
|
139
|
+
|
|
135
140
|
peersCounter = metrics.getMeter("cojson").createUpDownCounter("jazz.peers", {
|
|
136
141
|
description: "Amount of connected peers",
|
|
137
142
|
valueType: ValueType.INT,
|
|
@@ -154,6 +159,10 @@ export class SyncManager {
|
|
|
154
159
|
|
|
155
160
|
syncState: SyncStateManager;
|
|
156
161
|
|
|
162
|
+
disableTransactionVerification() {
|
|
163
|
+
this.skipVerify = true;
|
|
164
|
+
}
|
|
165
|
+
|
|
157
166
|
peersInPriorityOrder(): PeerState[] {
|
|
158
167
|
return Object.values(this.peers).sort((a, b) => {
|
|
159
168
|
const aPriority = a.priority || 0;
|
|
@@ -637,6 +646,7 @@ export class SyncManager {
|
|
|
637
646
|
undefined,
|
|
638
647
|
newContentForSession.lastSignature,
|
|
639
648
|
"immediate",
|
|
649
|
+
this.skipVerify,
|
|
640
650
|
);
|
|
641
651
|
|
|
642
652
|
if (result.isErr()) {
|
|
@@ -0,0 +1,130 @@
|
|
|
1
|
+
import { assert, beforeEach, describe, expect, it } from "vitest";
|
|
2
|
+
import {
|
|
3
|
+
loadCoValueOrFail,
|
|
4
|
+
setCurrentTestCryptoProvider,
|
|
5
|
+
setupTestNode,
|
|
6
|
+
setupTestAccount,
|
|
7
|
+
} from "./testUtils";
|
|
8
|
+
import { PureJSCrypto } from "../crypto/PureJSCrypto";
|
|
9
|
+
import { stableStringify } from "../jsonStringify";
|
|
10
|
+
|
|
11
|
+
const jsCrypto = await PureJSCrypto.create();
|
|
12
|
+
setCurrentTestCryptoProvider(jsCrypto);
|
|
13
|
+
|
|
14
|
+
let syncServer: ReturnType<typeof setupTestNode>;
|
|
15
|
+
|
|
16
|
+
beforeEach(() => {
|
|
17
|
+
syncServer = setupTestNode({ isSyncServer: true });
|
|
18
|
+
});
|
|
19
|
+
|
|
20
|
+
// A suite of tests focused on high-level tests that verify:
|
|
21
|
+
// - Keys creation and unsealing
|
|
22
|
+
// - Signature creation and verification
|
|
23
|
+
// - Encryption and decryption of values
|
|
24
|
+
describe("PureJSCrypto", () => {
|
|
25
|
+
it("successfully creates a private CoValue and reads it in another session", async () => {
|
|
26
|
+
const client = setupTestNode({
|
|
27
|
+
connected: true,
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
const group = client.node.createGroup();
|
|
31
|
+
const map = group.createMap();
|
|
32
|
+
map.set("count", 0, "private");
|
|
33
|
+
map.set("count", 1, "private");
|
|
34
|
+
map.set("count", 2, "private");
|
|
35
|
+
|
|
36
|
+
const client2 = client.spawnNewSession();
|
|
37
|
+
|
|
38
|
+
const mapInTheOtherSession = await loadCoValueOrFail(client2.node, map.id);
|
|
39
|
+
|
|
40
|
+
expect(mapInTheOtherSession.get("count")).toEqual(2);
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
it("successfully updates a private CoValue and reads it in another session", async () => {
|
|
44
|
+
const client = setupTestNode({
|
|
45
|
+
connected: true,
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
const group = client.node.createGroup();
|
|
49
|
+
const map = group.createMap();
|
|
50
|
+
map.set("count", 0, "private");
|
|
51
|
+
map.set("count", 1, "private");
|
|
52
|
+
map.set("count", 2, "private");
|
|
53
|
+
|
|
54
|
+
const client2 = client.spawnNewSession();
|
|
55
|
+
|
|
56
|
+
const mapInTheOtherSession = await loadCoValueOrFail(client2.node, map.id);
|
|
57
|
+
mapInTheOtherSession.set("count", 3, "private");
|
|
58
|
+
|
|
59
|
+
await mapInTheOtherSession.core.waitForSync();
|
|
60
|
+
|
|
61
|
+
expect(mapInTheOtherSession.get("count")).toEqual(3);
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
it("can invite another account to a group and share a private CoValue", async () => {
|
|
65
|
+
const client = setupTestNode({
|
|
66
|
+
connected: true,
|
|
67
|
+
});
|
|
68
|
+
const account = await setupTestAccount({
|
|
69
|
+
connected: true,
|
|
70
|
+
});
|
|
71
|
+
|
|
72
|
+
const group = client.node.createGroup();
|
|
73
|
+
const invite = group.createInvite("admin");
|
|
74
|
+
|
|
75
|
+
await account.node.acceptInvite(group.id, invite);
|
|
76
|
+
|
|
77
|
+
const map = group.createMap();
|
|
78
|
+
map.set("secret", "private-data", "private");
|
|
79
|
+
|
|
80
|
+
// The other account should be able to read the private value
|
|
81
|
+
const mapInOtherSession = await loadCoValueOrFail(account.node, map.id);
|
|
82
|
+
expect(mapInOtherSession.get("secret")).toEqual("private-data");
|
|
83
|
+
|
|
84
|
+
mapInOtherSession.set("secret", "modified", "private");
|
|
85
|
+
|
|
86
|
+
await mapInOtherSession.core.waitForSync();
|
|
87
|
+
|
|
88
|
+
expect(map.get("secret")).toEqual("modified");
|
|
89
|
+
});
|
|
90
|
+
|
|
91
|
+
it("rejects sessions with invalid signatures", async () => {
|
|
92
|
+
const client = setupTestNode({
|
|
93
|
+
connected: true,
|
|
94
|
+
});
|
|
95
|
+
|
|
96
|
+
const group = client.node.createGroup();
|
|
97
|
+
const map = group.createMap();
|
|
98
|
+
map.set("count", 0, "trusting");
|
|
99
|
+
|
|
100
|
+
// Create a new session with the same agent
|
|
101
|
+
const client2 = client.spawnNewSession();
|
|
102
|
+
|
|
103
|
+
// This should work normally
|
|
104
|
+
const mapInOtherSession = await loadCoValueOrFail(client2.node, map.id);
|
|
105
|
+
expect(mapInOtherSession.get("count")).toEqual(0);
|
|
106
|
+
|
|
107
|
+
mapInOtherSession.core.tryAddTransactions(
|
|
108
|
+
client2.node.currentSessionID,
|
|
109
|
+
[
|
|
110
|
+
{
|
|
111
|
+
privacy: "trusting",
|
|
112
|
+
changes: stableStringify([{ op: "set", key: "count", value: 1 }]),
|
|
113
|
+
madeAt: Date.now(),
|
|
114
|
+
},
|
|
115
|
+
],
|
|
116
|
+
"hash_z12345678",
|
|
117
|
+
"signature_z12345678",
|
|
118
|
+
"immediate",
|
|
119
|
+
true,
|
|
120
|
+
);
|
|
121
|
+
|
|
122
|
+
const content =
|
|
123
|
+
mapInOtherSession.core.verified.newContentSince(undefined)?.[0];
|
|
124
|
+
assert(content);
|
|
125
|
+
|
|
126
|
+
client.node.syncManager.handleNewContent(content, "storage");
|
|
127
|
+
|
|
128
|
+
expect(map.get("count")).toEqual(0);
|
|
129
|
+
});
|
|
130
|
+
});
|