@optimystic/quereus-plugin-crypto 0.13.5 → 0.16.2
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/CHANGELOG.md +91 -0
- package/README.md +259 -16
- package/dist/index.d.ts +224 -198
- package/dist/index.js +272 -272
- package/dist/index.js.map +1 -1
- package/dist/plugin.d.ts +17 -1
- package/dist/plugin.js +435 -23
- package/dist/plugin.js.map +1 -1
- package/package.json +32 -6
- package/src/cid.ts +200 -0
- package/src/crypto.ts +252 -34
- package/src/index.ts +30 -5
- package/src/plugin.ts +202 -5
- package/src/sd.ts +250 -0
- package/src/digest.ts +0 -173
- package/src/sign.ts +0 -235
- package/src/signature-valid.ts +0 -262
package/dist/plugin.js
CHANGED
|
@@ -1,12 +1,18 @@
|
|
|
1
1
|
import { FunctionFlags, TEXT_TYPE, BOOLEAN_TYPE, INTEGER_TYPE } from '@quereus/quereus';
|
|
2
|
+
import { toString, fromString } from 'uint8arrays';
|
|
2
3
|
import { sha512, sha256 } from '@noble/hashes/sha2.js';
|
|
3
4
|
import { blake3 } from '@noble/hashes/blake3.js';
|
|
4
|
-
import { randomBytes as randomBytes$1, utf8ToBytes } from '@noble/hashes/utils.js';
|
|
5
|
+
import { randomBytes as randomBytes$1, concatBytes, utf8ToBytes } from '@noble/hashes/utils.js';
|
|
5
6
|
import { secp256k1 } from '@noble/curves/secp256k1.js';
|
|
6
7
|
import { p256 } from '@noble/curves/nist.js';
|
|
7
8
|
import { ed25519 } from '@noble/curves/ed25519.js';
|
|
8
|
-
import {
|
|
9
|
-
import {
|
|
9
|
+
import { bytesToHex, hexToBytes } from '@noble/curves/utils.js';
|
|
10
|
+
import { CID } from 'multiformats/cid';
|
|
11
|
+
import * as Digest from 'multiformats/hashes/digest';
|
|
12
|
+
import { base32 } from 'multiformats/bases/base32';
|
|
13
|
+
import { base58btc } from 'multiformats/bases/base58';
|
|
14
|
+
import { base64url } from 'multiformats/bases/base64';
|
|
15
|
+
import { base16 } from 'multiformats/bases/base16';
|
|
10
16
|
|
|
11
17
|
// src/plugin.ts
|
|
12
18
|
function toBytes(input, encoding = "base64url") {
|
|
@@ -48,29 +54,133 @@ function fromBytes(bytes, encoding = "base64url") {
|
|
|
48
54
|
return toString(bytes, "base64url");
|
|
49
55
|
}
|
|
50
56
|
}
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
57
|
+
var HASHERS = {
|
|
58
|
+
sha256,
|
|
59
|
+
sha512,
|
|
60
|
+
blake3
|
|
61
|
+
};
|
|
62
|
+
var OUTPUT_ENCODERS = {
|
|
63
|
+
base64url: (bytes) => toString(bytes, "base64url"),
|
|
64
|
+
base64: (bytes) => toString(bytes, "base64"),
|
|
65
|
+
hex: (bytes) => bytesToHex(bytes),
|
|
66
|
+
bytes: (bytes) => bytes
|
|
67
|
+
};
|
|
68
|
+
function resolveHasher(algorithm) {
|
|
69
|
+
const hasher = HASHERS[algorithm];
|
|
70
|
+
if (!hasher) {
|
|
71
|
+
throw new Error(`Unsupported hash algorithm: ${algorithm}`);
|
|
72
|
+
}
|
|
73
|
+
return hasher;
|
|
74
|
+
}
|
|
75
|
+
function resolveOutputEncoder(encoding) {
|
|
76
|
+
const encoder = OUTPUT_ENCODERS[encoding];
|
|
77
|
+
if (!encoder) {
|
|
78
|
+
throw new Error(`Unsupported output encoding: ${encoding}`);
|
|
79
|
+
}
|
|
80
|
+
return encoder;
|
|
81
|
+
}
|
|
82
|
+
var DIGEST_FORMAT_V1 = 1;
|
|
83
|
+
var TAG_NULL = 0;
|
|
84
|
+
var TAG_INT = 1;
|
|
85
|
+
var TAG_REAL = 2;
|
|
86
|
+
var TAG_TEXT = 3;
|
|
87
|
+
var TAG_BOOL = 4;
|
|
88
|
+
var TAG_BLOB = 5;
|
|
89
|
+
var TAG_JSON = 6;
|
|
90
|
+
function writeVarint(out, value) {
|
|
91
|
+
if (!Number.isInteger(value) || value < 0) {
|
|
92
|
+
throw new Error(`varint expects a non-negative integer, got ${value}`);
|
|
93
|
+
}
|
|
94
|
+
let v = value;
|
|
95
|
+
while (v >= 128) {
|
|
96
|
+
out.push(v & 127 | 128);
|
|
97
|
+
v = Math.floor(v / 128);
|
|
98
|
+
}
|
|
99
|
+
out.push(v);
|
|
100
|
+
}
|
|
101
|
+
function framed(tag, payload) {
|
|
102
|
+
const header = [tag];
|
|
103
|
+
writeVarint(header, payload.length);
|
|
104
|
+
return concatBytes(Uint8Array.from(header), payload);
|
|
105
|
+
}
|
|
106
|
+
function canonicalJson(value) {
|
|
107
|
+
if (value === null) return "null";
|
|
108
|
+
const t = typeof value;
|
|
109
|
+
if (t === "string") return JSON.stringify(value);
|
|
110
|
+
if (t === "boolean") return value ? "true" : "false";
|
|
111
|
+
if (t === "number") {
|
|
112
|
+
if (!Number.isFinite(value)) {
|
|
113
|
+
throw new Error("digest: cannot encode a non-finite number inside a JSON field");
|
|
114
|
+
}
|
|
115
|
+
return JSON.stringify(value);
|
|
116
|
+
}
|
|
117
|
+
if (t === "bigint") {
|
|
118
|
+
throw new Error("digest: bigint is not representable inside a JSON field");
|
|
119
|
+
}
|
|
120
|
+
if (Array.isArray(value)) {
|
|
121
|
+
return `[${value.map((el) => {
|
|
122
|
+
if (el === void 0) {
|
|
123
|
+
throw new Error("digest: undefined / sparse element inside a JSON field");
|
|
124
|
+
}
|
|
125
|
+
return canonicalJson(el);
|
|
126
|
+
}).join(",")}]`;
|
|
127
|
+
}
|
|
128
|
+
if (t === "object") {
|
|
129
|
+
const proto = Object.getPrototypeOf(value);
|
|
130
|
+
if (proto !== Object.prototype && proto !== null) {
|
|
131
|
+
throw new Error("digest: only plain objects are allowed inside a JSON field");
|
|
132
|
+
}
|
|
133
|
+
const obj = value;
|
|
134
|
+
const keys = Object.keys(obj).sort();
|
|
135
|
+
return `{${keys.map((k) => {
|
|
136
|
+
if (obj[k] === void 0) {
|
|
137
|
+
throw new Error(`digest: undefined value for JSON key '${k}'`);
|
|
138
|
+
}
|
|
139
|
+
return `${JSON.stringify(k)}:${canonicalJson(obj[k])}`;
|
|
140
|
+
}).join(",")}}`;
|
|
141
|
+
}
|
|
142
|
+
throw new Error(`digest: unsupported value of type '${t}' inside a JSON field`);
|
|
143
|
+
}
|
|
144
|
+
function encodeField(field) {
|
|
145
|
+
if (field === null || field === void 0) {
|
|
146
|
+
return Uint8Array.of(TAG_NULL);
|
|
147
|
+
}
|
|
148
|
+
switch (typeof field) {
|
|
149
|
+
case "boolean":
|
|
150
|
+
return Uint8Array.of(TAG_BOOL, field ? 1 : 0);
|
|
151
|
+
case "bigint":
|
|
152
|
+
return framed(TAG_INT, utf8ToBytes(field.toString()));
|
|
153
|
+
case "number":
|
|
154
|
+
if (!Number.isFinite(field)) {
|
|
155
|
+
throw new Error("digest: cannot encode a non-finite number");
|
|
156
|
+
}
|
|
157
|
+
return Number.isInteger(field) ? framed(TAG_INT, utf8ToBytes(BigInt(field).toString())) : framed(TAG_REAL, utf8ToBytes(field.toString()));
|
|
158
|
+
case "string":
|
|
159
|
+
return framed(TAG_TEXT, utf8ToBytes(field));
|
|
160
|
+
case "object":
|
|
161
|
+
if (field instanceof Uint8Array) {
|
|
162
|
+
return framed(TAG_BLOB, field);
|
|
163
|
+
}
|
|
164
|
+
return framed(TAG_JSON, utf8ToBytes(canonicalJson(field)));
|
|
64
165
|
default:
|
|
65
|
-
throw new Error(`
|
|
166
|
+
throw new Error(`digest: unsupported field type '${typeof field}'`);
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
function encodeFields(fields) {
|
|
170
|
+
const chunks = [Uint8Array.of(DIGEST_FORMAT_V1)];
|
|
171
|
+
for (const field of fields) {
|
|
172
|
+
chunks.push(encodeField(field));
|
|
66
173
|
}
|
|
67
|
-
return
|
|
174
|
+
return concatBytes(...chunks);
|
|
175
|
+
}
|
|
176
|
+
function digestFields(fields, hasher, encode) {
|
|
177
|
+
return encode(hasher(encodeFields(fields)));
|
|
68
178
|
}
|
|
69
179
|
function hashMod(data, bits, algorithm = "sha256", inputEncoding = "base64url") {
|
|
70
180
|
if (bits <= 0 || bits > 53) {
|
|
71
181
|
throw new Error("Bits must be between 1 and 53 (JavaScript safe integer limit)");
|
|
72
182
|
}
|
|
73
|
-
const hashBytes = toBytes(
|
|
183
|
+
const hashBytes = resolveHasher(algorithm)(toBytes(data, inputEncoding));
|
|
74
184
|
const view = new DataView(hashBytes.buffer, hashBytes.byteOffset, Math.min(8, hashBytes.length));
|
|
75
185
|
const fullHash = view.getBigUint64(0, false);
|
|
76
186
|
const modulus = BigInt(2) ** BigInt(bits);
|
|
@@ -123,23 +233,316 @@ function randomBytes(bits = 256, encoding = "base64url") {
|
|
|
123
233
|
const randomBytesArray = randomBytes$1(bytes);
|
|
124
234
|
return fromBytes(randomBytesArray, encoding);
|
|
125
235
|
}
|
|
236
|
+
var MULTICODEC_CODES = {
|
|
237
|
+
"raw": 85,
|
|
238
|
+
"dag-cbor": 113
|
|
239
|
+
};
|
|
240
|
+
var MULTIHASH_CODES = {
|
|
241
|
+
"sha2-256": 18,
|
|
242
|
+
"sha2-512": 19,
|
|
243
|
+
"blake3": 30
|
|
244
|
+
};
|
|
245
|
+
var MULTIHASH_TO_ALGORITHM = {
|
|
246
|
+
"sha2-256": "sha256",
|
|
247
|
+
"sha2-512": "sha512",
|
|
248
|
+
"blake3": "blake3"
|
|
249
|
+
};
|
|
250
|
+
var MULTIHASH_DIGEST_LENGTHS = {
|
|
251
|
+
"sha2-256": 32,
|
|
252
|
+
"sha2-512": 64,
|
|
253
|
+
"blake3": 32
|
|
254
|
+
};
|
|
255
|
+
var MULTICODEC_NAMES = new Map(
|
|
256
|
+
Object.entries(MULTICODEC_CODES).map(([name, code]) => [code, name])
|
|
257
|
+
);
|
|
258
|
+
var MULTIHASH_NAMES = new Map(
|
|
259
|
+
Object.entries(MULTIHASH_CODES).map(([name, code]) => [code, name])
|
|
260
|
+
);
|
|
261
|
+
var MULTIBASE_ENCODERS = {
|
|
262
|
+
"base32": base32,
|
|
263
|
+
"base58btc": base58btc,
|
|
264
|
+
"base64url": base64url,
|
|
265
|
+
"base16": base16
|
|
266
|
+
};
|
|
267
|
+
var MULTIBASE_DECODER = base32.decoder.or(base58btc.decoder).or(base64url.decoder).or(base16.decoder);
|
|
268
|
+
function resolveCodecCode(codec) {
|
|
269
|
+
const code = MULTICODEC_CODES[codec];
|
|
270
|
+
if (code === void 0) {
|
|
271
|
+
throw new Error(`cid: unsupported multicodec '${codec}' (expected one of ${Object.keys(MULTICODEC_CODES).join(", ")})`);
|
|
272
|
+
}
|
|
273
|
+
return code;
|
|
274
|
+
}
|
|
275
|
+
function resolveBaseEncoder(base) {
|
|
276
|
+
const encoder = MULTIBASE_ENCODERS[base];
|
|
277
|
+
if (!encoder) {
|
|
278
|
+
throw new Error(`cid: unsupported multibase '${base}' (expected one of ${Object.keys(MULTIBASE_ENCODERS).join(", ")})`);
|
|
279
|
+
}
|
|
280
|
+
return encoder;
|
|
281
|
+
}
|
|
282
|
+
function cidV1(digest, hash, codec = "raw", base = "base32") {
|
|
283
|
+
const hashCode = MULTIHASH_CODES[hash];
|
|
284
|
+
if (hashCode === void 0) {
|
|
285
|
+
throw new Error(`cid: unsupported multihash code '${hash}' (expected one of ${Object.keys(MULTIHASH_CODES).join(", ")})`);
|
|
286
|
+
}
|
|
287
|
+
const expectedLength = MULTIHASH_DIGEST_LENGTHS[hash];
|
|
288
|
+
if (digest.length !== expectedLength) {
|
|
289
|
+
throw new Error(`cid: digest length ${digest.length} does not match asserted hash '${hash}' (expected ${expectedLength} bytes)`);
|
|
290
|
+
}
|
|
291
|
+
const codecCode = resolveCodecCode(codec);
|
|
292
|
+
const encoder = resolveBaseEncoder(base);
|
|
293
|
+
const multihash = Digest.create(hashCode, digest);
|
|
294
|
+
return CID.createV1(codecCode, multihash).toString(encoder);
|
|
295
|
+
}
|
|
296
|
+
function cid(data, codec = "raw", hash = "sha2-256", base = "base32") {
|
|
297
|
+
const algorithm = MULTIHASH_TO_ALGORITHM[hash];
|
|
298
|
+
if (!algorithm) {
|
|
299
|
+
throw new Error(`cid: unsupported multihash code '${hash}' (expected one of ${Object.keys(MULTIHASH_CODES).join(", ")})`);
|
|
300
|
+
}
|
|
301
|
+
const digest = resolveHasher(algorithm)(data);
|
|
302
|
+
return cidV1(digest, hash, codec, base);
|
|
303
|
+
}
|
|
304
|
+
function cidDecode(value) {
|
|
305
|
+
const parsed = CID.parse(value, MULTIBASE_DECODER);
|
|
306
|
+
return {
|
|
307
|
+
version: parsed.version,
|
|
308
|
+
codec: MULTICODEC_NAMES.get(parsed.code) ?? parsed.code,
|
|
309
|
+
hashCode: MULTIHASH_NAMES.get(parsed.multihash.code) ?? parsed.multihash.code,
|
|
310
|
+
digest: parsed.multihash.digest
|
|
311
|
+
};
|
|
312
|
+
}
|
|
313
|
+
var SD_LEAF_DOMAIN_V1 = "optimystic/sd-leaf/v1";
|
|
314
|
+
var SD_SET_DOMAIN_V1 = "optimystic/sd-set/v1";
|
|
315
|
+
var HIDDEN_ENCODING = "base64url";
|
|
316
|
+
function compareBytes(a, b) {
|
|
317
|
+
const len = Math.min(a.length, b.length);
|
|
318
|
+
for (let i = 0; i < len; i++) {
|
|
319
|
+
const d = a[i] - b[i];
|
|
320
|
+
if (d !== 0) return d;
|
|
321
|
+
}
|
|
322
|
+
return a.length - b.length;
|
|
323
|
+
}
|
|
324
|
+
function bytesEqual(a, b) {
|
|
325
|
+
if (a.length !== b.length) return false;
|
|
326
|
+
for (let i = 0; i < a.length; i++) {
|
|
327
|
+
if (a[i] !== b[i]) return false;
|
|
328
|
+
}
|
|
329
|
+
return true;
|
|
330
|
+
}
|
|
331
|
+
function requireSaltBytes(leaf) {
|
|
332
|
+
const { salt } = leaf;
|
|
333
|
+
if (salt == null) {
|
|
334
|
+
throw new Error(`set commitment: leaf '${leaf.name}' is missing a salt (an unsalted leaf is brute-forceable)`);
|
|
335
|
+
}
|
|
336
|
+
const bytes = salt instanceof Uint8Array ? salt : fromString(salt, HIDDEN_ENCODING);
|
|
337
|
+
if (bytes.length === 0) {
|
|
338
|
+
throw new Error(`set commitment: leaf '${leaf.name}' has an empty salt (an unsalted leaf is brute-forceable)`);
|
|
339
|
+
}
|
|
340
|
+
return bytes;
|
|
341
|
+
}
|
|
342
|
+
function assertUniqueNames(leaves) {
|
|
343
|
+
const seen = /* @__PURE__ */ new Set();
|
|
344
|
+
for (const leaf of leaves) {
|
|
345
|
+
if (seen.has(leaf.name)) {
|
|
346
|
+
throw new Error(`set commitment: duplicate leaf name '${leaf.name}'`);
|
|
347
|
+
}
|
|
348
|
+
seen.add(leaf.name);
|
|
349
|
+
}
|
|
350
|
+
}
|
|
351
|
+
function leafDigest(leaf, hasher) {
|
|
352
|
+
const saltBytes = requireSaltBytes(leaf);
|
|
353
|
+
return hasher(encodeFields([SD_LEAF_DOMAIN_V1, leaf.name, leaf.value, saltBytes]));
|
|
354
|
+
}
|
|
355
|
+
function setCommit(leaves, hasher = resolveHasher("sha256"), encode = resolveOutputEncoder("base64url")) {
|
|
356
|
+
assertUniqueNames(leaves);
|
|
357
|
+
const leafDigests = leaves.map((leaf) => leafDigest(leaf, hasher));
|
|
358
|
+
leafDigests.sort(compareBytes);
|
|
359
|
+
return encode(hasher(encodeFields([SD_SET_DOMAIN_V1, ...leafDigests])));
|
|
360
|
+
}
|
|
361
|
+
function setVerify(root, disclosure, hasher = resolveHasher("sha256"), encode = resolveOutputEncoder("base64url")) {
|
|
362
|
+
try {
|
|
363
|
+
const { disclosed, hidden } = disclosure;
|
|
364
|
+
const digests = [];
|
|
365
|
+
for (const leaf of disclosed) {
|
|
366
|
+
digests.push(leafDigest(leaf, hasher));
|
|
367
|
+
}
|
|
368
|
+
for (const h of hidden) {
|
|
369
|
+
digests.push(fromString(h, HIDDEN_ENCODING));
|
|
370
|
+
}
|
|
371
|
+
digests.sort(compareBytes);
|
|
372
|
+
const recomputed = hasher(encodeFields([SD_SET_DOMAIN_V1, ...digests]));
|
|
373
|
+
if (root instanceof Uint8Array) {
|
|
374
|
+
return bytesEqual(recomputed, root);
|
|
375
|
+
}
|
|
376
|
+
const encoded = encode(recomputed);
|
|
377
|
+
return typeof encoded === "string" && encoded === root;
|
|
378
|
+
} catch {
|
|
379
|
+
return false;
|
|
380
|
+
}
|
|
381
|
+
}
|
|
126
382
|
|
|
127
383
|
// src/plugin.ts
|
|
384
|
+
var DIGEST_ALGORITHMS = ["sha256", "sha512", "blake3"];
|
|
385
|
+
var DIGEST_TEXT_ENCODINGS = ["base64url", "base64", "hex"];
|
|
386
|
+
function configAlgorithm(config) {
|
|
387
|
+
const value = config.algorithm == null ? "sha256" : String(config.algorithm);
|
|
388
|
+
if (!DIGEST_ALGORITHMS.includes(value)) {
|
|
389
|
+
throw new Error(`crypto plugin: unsupported digest algorithm '${value}' (expected one of ${DIGEST_ALGORITHMS.join(", ")})`);
|
|
390
|
+
}
|
|
391
|
+
return value;
|
|
392
|
+
}
|
|
393
|
+
function configEncoding(config) {
|
|
394
|
+
const value = config.encoding == null ? "base64url" : String(config.encoding);
|
|
395
|
+
if (!DIGEST_TEXT_ENCODINGS.includes(value)) {
|
|
396
|
+
throw new Error(`crypto plugin: unsupported digest encoding '${value}' (expected one of ${DIGEST_TEXT_ENCODINGS.join(", ")})`);
|
|
397
|
+
}
|
|
398
|
+
return value;
|
|
399
|
+
}
|
|
400
|
+
function toContentBytes(value, fnName) {
|
|
401
|
+
if (value instanceof Uint8Array) {
|
|
402
|
+
return value;
|
|
403
|
+
}
|
|
404
|
+
if (typeof value === "string") {
|
|
405
|
+
return fromString(value, "base64url");
|
|
406
|
+
}
|
|
407
|
+
throw new Error(`${fnName}: expected a BLOB or base64url TEXT argument, got ${value == null ? "NULL" : typeof value}`);
|
|
408
|
+
}
|
|
409
|
+
function leafFromJson(entry, fnName) {
|
|
410
|
+
if (Array.isArray(entry)) {
|
|
411
|
+
if (entry.length < 3) {
|
|
412
|
+
throw new Error(`${fnName}: a leaf array must be [name, value, salt]`);
|
|
413
|
+
}
|
|
414
|
+
const [name, value, salt] = entry;
|
|
415
|
+
if (typeof name !== "string") {
|
|
416
|
+
throw new Error(`${fnName}: leaf name must be a string`);
|
|
417
|
+
}
|
|
418
|
+
return { name, value, salt };
|
|
419
|
+
}
|
|
420
|
+
if (entry !== null && typeof entry === "object") {
|
|
421
|
+
const o = entry;
|
|
422
|
+
if (typeof o.name !== "string") {
|
|
423
|
+
throw new Error(`${fnName}: leaf name must be a string`);
|
|
424
|
+
}
|
|
425
|
+
if (!("value" in o)) {
|
|
426
|
+
throw new Error(`${fnName}: leaf '${o.name}' is missing a value (pass value: null for a null-valued attribute)`);
|
|
427
|
+
}
|
|
428
|
+
return { name: o.name, value: o.value, salt: o.salt };
|
|
429
|
+
}
|
|
430
|
+
throw new Error(`${fnName}: each leaf must be a [name, value, salt] array or { name, value, salt } object`);
|
|
431
|
+
}
|
|
432
|
+
function parseLeaves(json, fnName) {
|
|
433
|
+
const parsed = JSON.parse(json);
|
|
434
|
+
if (!Array.isArray(parsed)) {
|
|
435
|
+
throw new Error(`${fnName}: expected a JSON array of leaves`);
|
|
436
|
+
}
|
|
437
|
+
return parsed.map((entry) => leafFromJson(entry, fnName));
|
|
438
|
+
}
|
|
128
439
|
var DETERMINISTIC_FLAGS = FunctionFlags.UTF8 | FunctionFlags.DETERMINISTIC;
|
|
129
440
|
var NON_DETERMINISTIC_FLAGS = FunctionFlags.UTF8;
|
|
130
|
-
function register(_db,
|
|
441
|
+
function register(_db, config = {}) {
|
|
442
|
+
const digestHasher = resolveHasher(configAlgorithm(config));
|
|
443
|
+
const digestEncoder = resolveOutputEncoder(configEncoding(config));
|
|
131
444
|
const functions = [
|
|
132
445
|
{
|
|
133
446
|
schema: {
|
|
134
447
|
name: "digest",
|
|
135
448
|
numArgs: -1,
|
|
136
|
-
//
|
|
449
|
+
// Variadic over data fields: digest(f1, f2, ..., fN)
|
|
137
450
|
flags: DETERMINISTIC_FLAGS,
|
|
138
451
|
// digest is deterministic
|
|
452
|
+
// Bit-identical across peers/platforms — these digests are signed and persisted.
|
|
453
|
+
replicable: true,
|
|
454
|
+
returnType: { typeClass: "scalar", logicalType: TEXT_TYPE, nullable: false },
|
|
455
|
+
implementation: (...fields) => digestFields(fields, digestHasher, digestEncoder)
|
|
456
|
+
}
|
|
457
|
+
},
|
|
458
|
+
{
|
|
459
|
+
schema: {
|
|
460
|
+
name: "cid",
|
|
461
|
+
numArgs: -1,
|
|
462
|
+
// cid(data, codec?, hash?, base?) — trailing args optional
|
|
463
|
+
flags: DETERMINISTIC_FLAGS,
|
|
464
|
+
// Self-describing content address; signed/persisted, so byte-identical across peers.
|
|
465
|
+
replicable: true,
|
|
139
466
|
returnType: { typeClass: "scalar", logicalType: TEXT_TYPE, nullable: false },
|
|
140
467
|
implementation: (...args) => {
|
|
141
|
-
const [data,
|
|
142
|
-
return
|
|
468
|
+
const [data, codec = "raw", hash = "sha2-256", base = "base32"] = args;
|
|
469
|
+
return cid(toContentBytes(data, "cid"), codec, hash, base);
|
|
470
|
+
}
|
|
471
|
+
}
|
|
472
|
+
},
|
|
473
|
+
{
|
|
474
|
+
schema: {
|
|
475
|
+
name: "cid_v1",
|
|
476
|
+
numArgs: -1,
|
|
477
|
+
// cid_v1(digest, hash, codec?, base?) — hash required, trailing args optional
|
|
478
|
+
flags: DETERMINISTIC_FLAGS,
|
|
479
|
+
replicable: true,
|
|
480
|
+
returnType: { typeClass: "scalar", logicalType: TEXT_TYPE, nullable: false },
|
|
481
|
+
implementation: (...args) => {
|
|
482
|
+
const [digest, hash, codec = "raw", base = "base32"] = args;
|
|
483
|
+
if (hash == null) {
|
|
484
|
+
throw new Error("cid_v1: 'hash' argument is required (the multihash code asserting which algorithm produced the digest)");
|
|
485
|
+
}
|
|
486
|
+
return cidV1(toContentBytes(digest, "cid_v1"), hash, codec, base);
|
|
487
|
+
}
|
|
488
|
+
}
|
|
489
|
+
},
|
|
490
|
+
{
|
|
491
|
+
schema: {
|
|
492
|
+
name: "cid_decode",
|
|
493
|
+
numArgs: 1,
|
|
494
|
+
// cid_decode(cid) -> JSON text { version, codec, hashCode, digest }
|
|
495
|
+
flags: DETERMINISTIC_FLAGS,
|
|
496
|
+
replicable: true,
|
|
497
|
+
returnType: { typeClass: "scalar", logicalType: TEXT_TYPE, nullable: false },
|
|
498
|
+
implementation: (value) => {
|
|
499
|
+
const parts = cidDecode(value);
|
|
500
|
+
return JSON.stringify({
|
|
501
|
+
version: parts.version,
|
|
502
|
+
codec: parts.codec,
|
|
503
|
+
hashCode: parts.hashCode,
|
|
504
|
+
digest: toString(parts.digest, "base64url")
|
|
505
|
+
});
|
|
506
|
+
}
|
|
507
|
+
}
|
|
508
|
+
},
|
|
509
|
+
{
|
|
510
|
+
schema: {
|
|
511
|
+
name: "set_commit",
|
|
512
|
+
numArgs: 1,
|
|
513
|
+
// set_commit(leaves_json) -> root over a JSON array of [name, value, salt] leaves
|
|
514
|
+
flags: DETERMINISTIC_FLAGS,
|
|
515
|
+
// The root is signed and persisted as a commitment, same bar as `digest`.
|
|
516
|
+
replicable: true,
|
|
517
|
+
returnType: { typeClass: "scalar", logicalType: TEXT_TYPE, nullable: false },
|
|
518
|
+
implementation: (leavesJson) => {
|
|
519
|
+
if (typeof leavesJson !== "string") {
|
|
520
|
+
throw new Error("set_commit: expected a JSON TEXT array of [name, value, salt] leaves");
|
|
521
|
+
}
|
|
522
|
+
const leaves = parseLeaves(leavesJson, "set_commit");
|
|
523
|
+
return setCommit(leaves, digestHasher, digestEncoder);
|
|
524
|
+
}
|
|
525
|
+
}
|
|
526
|
+
},
|
|
527
|
+
{
|
|
528
|
+
schema: {
|
|
529
|
+
name: "set_verify",
|
|
530
|
+
numArgs: 3,
|
|
531
|
+
// set_verify(root, disclosed_json, hidden_json) -> BOOLEAN
|
|
532
|
+
flags: DETERMINISTIC_FLAGS,
|
|
533
|
+
// pure, not persisted — matches `verify` (no `replicable`)
|
|
534
|
+
returnType: { typeClass: "scalar", logicalType: BOOLEAN_TYPE, nullable: false },
|
|
535
|
+
implementation: (root, disclosedJson, hiddenJson) => {
|
|
536
|
+
try {
|
|
537
|
+
if (typeof root !== "string" && !(root instanceof Uint8Array)) return false;
|
|
538
|
+
if (typeof disclosedJson !== "string" || typeof hiddenJson !== "string") return false;
|
|
539
|
+
const disclosed = parseLeaves(disclosedJson, "set_verify");
|
|
540
|
+
const hidden = JSON.parse(hiddenJson);
|
|
541
|
+
if (!Array.isArray(hidden) || !hidden.every((h) => typeof h === "string")) return false;
|
|
542
|
+
return setVerify(root, { disclosed, hidden }, digestHasher, digestEncoder);
|
|
543
|
+
} catch {
|
|
544
|
+
return false;
|
|
545
|
+
}
|
|
143
546
|
}
|
|
144
547
|
}
|
|
145
548
|
},
|
|
@@ -150,6 +553,15 @@ function register(_db, _config = {}) {
|
|
|
150
553
|
// Variable arguments: data, privateKey, curve?, inputEncoding?, keyEncoding?, outputEncoding?
|
|
151
554
|
flags: DETERMINISTIC_FLAGS,
|
|
152
555
|
// sign is deterministic (same key + data = same signature)
|
|
556
|
+
// Security: passing a private key as the second argument (literal or bound
|
|
557
|
+
// parameter) is SAFE with respect to replication. The Quereus engine rebuilds
|
|
558
|
+
// the replicated statement from evaluated column values, not from source SQL
|
|
559
|
+
// text, so the key argument is discarded before the record is written — peers
|
|
560
|
+
// re-execute `INSERT ... VALUES (<signature>)`, never `sign(..., key)`. What
|
|
561
|
+
// to avoid: storing a raw private key AS a column value, since any persisted
|
|
562
|
+
// column value is replicated. See docs/transactions.md § "Secrets and the
|
|
563
|
+
// replicated statement record" and the regression guard in
|
|
564
|
+
// quereus-plugin-optimystic/test/statement-secret-arg-redaction.spec.ts.
|
|
153
565
|
returnType: { typeClass: "scalar", logicalType: TEXT_TYPE, nullable: false },
|
|
154
566
|
implementation: (...args) => {
|
|
155
567
|
const [data, privateKey, curve = "secp256k1", inputEncoding = "base64url", keyEncoding = "base64url", outputEncoding = "base64url"] = args;
|