@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/CHANGELOG.md
ADDED
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
# Changelog
|
|
2
|
+
|
|
3
|
+
## 0.14.0 — BREAKING: `digest` API rework
|
|
4
|
+
|
|
5
|
+
### What changed
|
|
6
|
+
|
|
7
|
+
The exported `digest()` function signature changed from:
|
|
8
|
+
|
|
9
|
+
```ts
|
|
10
|
+
// OLD (≤ 0.13.x)
|
|
11
|
+
digest(data: string | Uint8Array, algorithm?, inputEncoding?, outputEncoding?)
|
|
12
|
+
```
|
|
13
|
+
|
|
14
|
+
to:
|
|
15
|
+
|
|
16
|
+
```ts
|
|
17
|
+
// NEW (≥ 0.14.0)
|
|
18
|
+
digest(fields: readonly DigestField[], algorithm?, encoding?)
|
|
19
|
+
```
|
|
20
|
+
|
|
21
|
+
Key differences:
|
|
22
|
+
|
|
23
|
+
| | Old | New |
|
|
24
|
+
|---|---|---|
|
|
25
|
+
| First argument | A single scalar value (`string` \| `Uint8Array`) | An **array** of values |
|
|
26
|
+
| `inputEncoding` | 3rd positional arg | **Removed** (the new API frames values by type, no string-decoding step) |
|
|
27
|
+
| Output encoding | 4th positional arg | 2nd `encoding` arg (shifted left by one) |
|
|
28
|
+
| Algorithm | 2nd positional arg | 2nd `algorithm` arg (unchanged position) |
|
|
29
|
+
| Result | Bare hash of the decoded bytes | **Framed** injective digest — `digest(['hello'])` ≠ `sha256("hello")` |
|
|
30
|
+
| Algorithm + encoding | Per-call | **Bound at plugin load time** for the SQL function |
|
|
31
|
+
|
|
32
|
+
### Migration: JS/TypeScript callers
|
|
33
|
+
|
|
34
|
+
```ts
|
|
35
|
+
// OLD
|
|
36
|
+
const hashBytes = digest(payload, 'sha256', 'utf8', 'bytes') as Uint8Array;
|
|
37
|
+
const payloadDigest = digest(payload, 'sha256', 'utf8', 'base64url') as string;
|
|
38
|
+
|
|
39
|
+
// NEW — wrap the value in an array, drop inputEncoding
|
|
40
|
+
const hashBytes = digest([payload], 'sha256', 'bytes') as Uint8Array;
|
|
41
|
+
const payloadDigest = digest([payload], 'sha256', 'base64url') as string;
|
|
42
|
+
```
|
|
43
|
+
|
|
44
|
+
> **The result value changes.** The new digest is *framed* (version byte + type tag +
|
|
45
|
+
> length-prefixed payload per field), so `digest(['hello'])` is **not** the same bytes
|
|
46
|
+
> as `sha256(utf8("hello"))`. If you need to match an externally-computed bare hash,
|
|
47
|
+
> see the open question below.
|
|
48
|
+
|
|
49
|
+
### Migration: SQL callers
|
|
50
|
+
|
|
51
|
+
The SQL `digest(field1, field2, ...)` function is **variadic over data fields** — every
|
|
52
|
+
argument is a field to hash, not a config option — so the signature is unchanged from the
|
|
53
|
+
SQL perspective.
|
|
54
|
+
|
|
55
|
+
However, if you were passing extra positional arguments to mimic `algo`/`inputEncoding`/`outputEncoding`
|
|
56
|
+
(e.g. `digest(data, 'sha256', 'utf8', 'bytes')`), those are now treated as **additional
|
|
57
|
+
data fields** and hashed into the result silently rather than interpreted as config. There
|
|
58
|
+
is **no error** on the SQL path for this; it just hashes more fields. Check any SQL call
|
|
59
|
+
sites that pass more than pure data arguments.
|
|
60
|
+
|
|
61
|
+
Algorithm and encoding are now set via the plugin config at load time (see the
|
|
62
|
+
[Digest configuration](README.md#digest-configuration) section of the README).
|
|
63
|
+
|
|
64
|
+
### Why no compatibility shim?
|
|
65
|
+
|
|
66
|
+
The old and new calling conventions cannot be cleanly disambiguated: the new first argument
|
|
67
|
+
is always an array; the old's was always a scalar. Adding a scalar → old-API detection
|
|
68
|
+
shim would silently re-enable the broken `inputEncoding` positional footgun and make the
|
|
69
|
+
result value unpredictable. The clean break stays; instead, old-style JS calls now throw
|
|
70
|
+
a clear, actionable error message naming this migration note.
|
|
71
|
+
|
|
72
|
+
### Error message for old-style calls
|
|
73
|
+
|
|
74
|
+
If you pass a non-array as the first argument to `digest()` in JS/TypeScript, you will now
|
|
75
|
+
see:
|
|
76
|
+
|
|
77
|
+
```
|
|
78
|
+
digest(fields, algorithm?, encoding?): 'fields' must be an array of values.
|
|
79
|
+
The digest API changed in v0.14: it is now variadic/injective over fields,
|
|
80
|
+
the per-call inputEncoding was removed, and algorithm + output encoding are bound at plugin load time.
|
|
81
|
+
Migrate digest(value, algo, inputEncoding, outputEncoding) → digest([value], algo, outputEncoding) —
|
|
82
|
+
note the result is now a *framed* digest, not a bare hash of the bytes.
|
|
83
|
+
```
|
|
84
|
+
|
|
85
|
+
### Open question: bare hash helper
|
|
86
|
+
|
|
87
|
+
The new `digest` has no function that returns a bare (un-framed) hash of a single value's
|
|
88
|
+
bytes in a chosen encoding — what the old `digest(x, algo, inEnc, outEnc)` did. If you
|
|
89
|
+
need bare-hash semantics (e.g. to match a hash stored before v0.14, or computed by an
|
|
90
|
+
external system), there is currently no drop-in. File a separate issue/ticket if a
|
|
91
|
+
`hash(data, algorithm, inputEncoding, outputEncoding)` helper is needed.
|
package/README.md
CHANGED
|
@@ -5,6 +5,8 @@ Quereus plugin providing cryptographic functions for SQL queries with base64url
|
|
|
5
5
|
## Features
|
|
6
6
|
|
|
7
7
|
- **Hash Functions**: SHA-256, SHA-512, BLAKE3 hashing with base64url output
|
|
8
|
+
- **Content Identifiers (CIDv1)**: Self-describing, interoperable IPFS/IPLD content addresses
|
|
9
|
+
- **Selective Disclosure**: Salted-leaf set commitment — commit to a whole attribute set with one value, later reveal only a chosen subset with proof of authenticity
|
|
8
10
|
- **Hash Modulo**: Fixed-size hash values (16-bit, 32-bit, etc.) for sharding and partitioning
|
|
9
11
|
- **Random Bytes**: Generate cryptographically secure random bytes (default: 256 bits)
|
|
10
12
|
- **Signature Functions**: secp256k1, P-256, Ed25519 signing with base64url encoding
|
|
@@ -28,6 +30,13 @@ import { loadPlugin } from '@quereus/quereus/util/plugin-loader.js';
|
|
|
28
30
|
|
|
29
31
|
const db = new Database();
|
|
30
32
|
await loadPlugin('npm:@optimystic/quereus-plugin-crypto', db);
|
|
33
|
+
|
|
34
|
+
// Or configure the digest algorithm / output encoding once, at load time
|
|
35
|
+
// (see "Digest configuration" below):
|
|
36
|
+
await loadPlugin('npm:@optimystic/quereus-plugin-crypto', db, {
|
|
37
|
+
algorithm: 'sha256', // 'sha256' (default) | 'sha512' | 'blake3'
|
|
38
|
+
encoding: 'base64url', // 'base64url' (default) | 'base64' | 'hex'
|
|
39
|
+
});
|
|
31
40
|
```
|
|
32
41
|
|
|
33
42
|
### Direct Import (for JavaScript/TypeScript code)
|
|
@@ -45,8 +54,8 @@ import {
|
|
|
45
54
|
getPublicKey
|
|
46
55
|
} from '@optimystic/quereus-plugin-crypto';
|
|
47
56
|
|
|
48
|
-
// Hash
|
|
49
|
-
const hash = digest('hello world',
|
|
57
|
+
// Hash an ordered tuple of fields (injective — distinct tuples never collide)
|
|
58
|
+
const hash = digest(['hello world', 42, null], 'sha256', 'base64url');
|
|
50
59
|
|
|
51
60
|
// Get a 16-bit hash for sharding
|
|
52
61
|
const shard = hashMod('user@example.com', 16, 'sha256', 'utf8');
|
|
@@ -68,24 +77,164 @@ const isValid = verify(hash, signature, publicKey, 'secp256k1', 'base64url', 'ba
|
|
|
68
77
|
|
|
69
78
|
All SQL functions use **base64url encoding by default** for inputs and outputs. This is URL-safe and SQL-friendly.
|
|
70
79
|
|
|
71
|
-
### digest(
|
|
80
|
+
### digest(field1, field2, ..., fieldN)
|
|
81
|
+
|
|
82
|
+
Hash an **ordered tuple of fields** into a single digest. `digest` is variadic over
|
|
83
|
+
*data* — every argument is a field, not a configuration option. The algorithm and
|
|
84
|
+
output encoding are chosen once, at load time (see [Digest configuration](#digest-configuration)),
|
|
85
|
+
so they never have to be passed per call.
|
|
86
|
+
|
|
87
|
+
```sql
|
|
88
|
+
-- Hash a single value
|
|
89
|
+
SELECT digest(Id) as hash;
|
|
90
|
+
|
|
91
|
+
-- Hash a tuple of columns — distinct tuples never collide,
|
|
92
|
+
-- NULL is distinguished from '', and 123 from '123'
|
|
93
|
+
SELECT digest(Tid, Name, ImageRef, NumberRequiredTSAs) as commitment;
|
|
94
|
+
```
|
|
95
|
+
|
|
96
|
+
**Why variadic + injective?** Hashing several fields by joining them
|
|
97
|
+
(`a || '|' || b`) or by `String()`-concatenation is not injective: `('a|b','c')`
|
|
98
|
+
collides with `('a','b|c')`, `NULL` collides with `''`, and `123` collides with
|
|
99
|
+
`'123'`. `digest` instead applies a canonical, length-prefixed, type-tagged framing
|
|
100
|
+
to each field, so distinct tuples always produce distinct digests. This matters when
|
|
101
|
+
the digest is signed or persisted as a commitment.
|
|
102
|
+
|
|
103
|
+
The framing is also why `digest(x)` is **not** a bare `sha256(x)` — it is a framed
|
|
104
|
+
digest of a one-element tuple. For sharding a single value, use `hash_mod`.
|
|
105
|
+
|
|
106
|
+
### cid(data, codec?, hash?, base?) / cid_v1(digest, hash, codec?, base?) / cid_decode(cid)
|
|
72
107
|
|
|
73
|
-
|
|
108
|
+
`digest` returns a **bare** hash — raw digest bytes in some text encoding, with no
|
|
109
|
+
record of which base, content type, or hash algorithm produced it. `cid` instead
|
|
110
|
+
produces a **self-describing CIDv1**, the same interoperable content address an
|
|
111
|
+
IPFS/IPLD store computes for the same bytes:
|
|
112
|
+
|
|
113
|
+
```
|
|
114
|
+
CIDv1 = multibase( version ‖ multicodec(content-type) ‖ multihash )
|
|
115
|
+
multihash = hashFnCode ‖ digestLength ‖ digestBytes
|
|
116
|
+
```
|
|
117
|
+
|
|
118
|
+
Because the multibase, content codec, and hash code all travel *inside* the value,
|
|
119
|
+
a consumer can validate it without out-of-band knowledge, and an algorithm
|
|
120
|
+
migration (e.g. sha2-256 → another hash) is unambiguous rather than a silent
|
|
121
|
+
reinterpretation. The framing comes entirely from the audited
|
|
122
|
+
[`multiformats`](https://github.com/multiformats/js-multiformats) library.
|
|
74
123
|
|
|
75
124
|
```sql
|
|
76
|
-
-- Hash
|
|
77
|
-
|
|
125
|
+
-- Hash a blob and frame it as the canonical raw/sha2-256/base32 CID
|
|
126
|
+
-- (== the CID IPFS shows for the same bytes)
|
|
127
|
+
SELECT cid(SomeBlob) AS Cid;
|
|
78
128
|
|
|
79
|
-
--
|
|
80
|
-
SELECT
|
|
129
|
+
-- Pick content codec / hash / base
|
|
130
|
+
SELECT cid(SomeBlob, 'dag-cbor', 'sha2-512', 'base58btc') AS Cid;
|
|
81
131
|
|
|
82
|
-
--
|
|
83
|
-
|
|
132
|
+
-- A self-describing content address over a field tuple: digest() canonically
|
|
133
|
+
-- frames + hashes the fields; cid_v1() wraps that exact digest as a CIDv1
|
|
134
|
+
-- (no double-hash). The asserted hash must match digest()'s configured algorithm.
|
|
135
|
+
SELECT cid_v1(digest(ColA, ColB, ColC), 'sha2-256') AS Cid;
|
|
84
136
|
|
|
85
|
-
--
|
|
86
|
-
SELECT
|
|
137
|
+
-- Validate / inspect a stored CID (returns JSON: { version, codec, hashCode, digest })
|
|
138
|
+
SELECT cid_decode(Cid) ->> 'codec' AS codec FROM T;
|
|
87
139
|
```
|
|
88
140
|
|
|
141
|
+
- **`cid(data, codec?, hash?, base?)`** — hash `data` then frame. `data` is a BLOB,
|
|
142
|
+
or a base64url-encoded TEXT digest/blob (the plugin's canonical text encoding).
|
|
143
|
+
Defaults: `codec='raw'`, `hash='sha2-256'`, `base='base32'`.
|
|
144
|
+
- **`cid_v1(digest, hash, codec?, base?)`** — wrap an **already-computed** digest
|
|
145
|
+
without re-hashing. `hash` is required and asserts which algorithm produced the
|
|
146
|
+
digest; the digest length is checked against it (sha2-256/blake3 = 32 bytes,
|
|
147
|
+
sha2-512 = 64) and a mismatch is rejected.
|
|
148
|
+
- **`cid_decode(cid) → JSON`** — parse a CID back to `{ version, codec, hashCode,
|
|
149
|
+
digest }` (digest as base64url). Throws cleanly on malformed input.
|
|
150
|
+
|
|
151
|
+
Selectable values: `codec` ∈ `raw`, `dag-cbor`; `hash` ∈ `sha2-256`, `sha2-512`,
|
|
152
|
+
`blake3`; `base` ∈ `base32` (default), `base58btc`, `base64url`, `base16`.
|
|
153
|
+
|
|
154
|
+
**Why `base32` by default?** A CID's whole purpose is to match what an external
|
|
155
|
+
content-addressed store computes, and IPFS renders CIDv1 canonically in base32 (the
|
|
156
|
+
`b…` prefix). This is deliberately different from the plugin's `digest`/`random_bytes`
|
|
157
|
+
default of base64url: base64url is compact for *internal* values that live in memory,
|
|
158
|
+
on the wire, or in JSON, whereas base32 is case-insensitive and DNS/URL/filename-safe
|
|
159
|
+
where interoperable addresses are copied and read. Two audiences, two defaults — pass
|
|
160
|
+
`'base64url'` explicitly if you want the compact form.
|
|
161
|
+
|
|
162
|
+
### set_commit(leaves_json) / set_verify(root, disclosed_json, hidden_json)
|
|
163
|
+
|
|
164
|
+
**Per-attribute selective disclosure.** An authority commits to a registrant's whole
|
|
165
|
+
attribute set as a single root (which it signs / persists), then later reveals only a
|
|
166
|
+
chosen *subset* to a recipient — with a proof the revealed values are genuinely the
|
|
167
|
+
committed ones — **without** leaking the withheld attribute values. A flat
|
|
168
|
+
`digest(whole set)` can't do this (verifying one field needs the whole pre-image, so
|
|
169
|
+
it's all-or-nothing); `set_commit` supports *partial opening*.
|
|
170
|
+
|
|
171
|
+
Each attribute becomes a salted leaf, and the root is the digest of all leaf digests in
|
|
172
|
+
canonical (sort-by-leaf-digest-bytes) order:
|
|
173
|
+
|
|
174
|
+
```
|
|
175
|
+
leafDigest = digest([SD_LEAF_DOMAIN_V1, name, value, salt]) -- raw digest bytes
|
|
176
|
+
root = digest([SD_SET_DOMAIN_V1, sortedLeaf_0, sortedLeaf_1, ...])
|
|
177
|
+
```
|
|
178
|
+
|
|
179
|
+
This is the same flat salted-hash shape the IETF SD-JWT standard
|
|
180
|
+
(`draft-ietf-oauth-selective-disclosure-jwt`) settled on rather than a Merkle tree —
|
|
181
|
+
cited as conceptual precedent only; the framing here is Optimystic's own `digest`
|
|
182
|
+
`encodeFields` (not SD-JWT wire-compatible). The `name` is hashed into the leaf so a
|
|
183
|
+
`(value, salt)` proof can't be replayed under another attribute; the `salt` is per-leaf
|
|
184
|
+
and **mandatory** (low-entropy values like a DOB or a boolean are brute-forceable from a
|
|
185
|
+
bare hash, and independent salts defeat cross-registrant correlation).
|
|
186
|
+
|
|
187
|
+
```sql
|
|
188
|
+
-- Commit to a JSON array of [name, value, salt] leaves -> single root.
|
|
189
|
+
-- Pair with cid() for the self-describing persisted/signed column representation:
|
|
190
|
+
SELECT cid(set_commit(SelectiveDetails)) AS SelectiveCid;
|
|
191
|
+
|
|
192
|
+
-- A schema CHECK makes a forged root impossible to store: the root is recomputed from
|
|
193
|
+
-- the stored attribute triples, so SelectiveCid must equal the genuine commitment.
|
|
194
|
+
CREATE TABLE Registrant (
|
|
195
|
+
...,
|
|
196
|
+
SelectiveDetails TEXT, -- JSON array of [name, value, salt] triples
|
|
197
|
+
SelectiveCid TEXT,
|
|
198
|
+
CHECK (SelectiveCid = cid(set_commit(SelectiveDetails)))
|
|
199
|
+
);
|
|
200
|
+
|
|
201
|
+
-- A recipient verifies a disclosure: the disclosed [name, value, salt] triples plus
|
|
202
|
+
-- the opaque leaf digests of the withheld leaves reconstruct the entire root.
|
|
203
|
+
SELECT set_verify(root, disclosed_json, hidden_json) AS ok;
|
|
204
|
+
```
|
|
205
|
+
|
|
206
|
+
- **`set_commit(leaves_json) → TEXT`** — `leaves_json` is a JSON array; each element is a
|
|
207
|
+
leaf `[name, value, salt]` *or* `{ "name", "value", "salt" }`. Values follow `digest`'s
|
|
208
|
+
rules as parsed from JSON (INTEGER vs REAL by JS value, TEXT, BOOL, null, nested
|
|
209
|
+
object/array). Each leaf must carry all three fields — a missing `value` (object form) or
|
|
210
|
+
a `[name, value]` array (under three elements) throws; pass `value: null` for a null-valued
|
|
211
|
+
attribute. The salt is base64url TEXT (e.g. from `random_bytes`). **THROWS** on a duplicate
|
|
212
|
+
name, a missing value, a missing/empty salt, or unparseable/non-array JSON (invalid states
|
|
213
|
+
made impossible). `replicable` — the root is signed and persisted, same bar as `digest`.
|
|
214
|
+
- **`set_verify(root, disclosed_json, hidden_json) → BOOLEAN`** — `disclosed_json` is the
|
|
215
|
+
opened triples (same leaf shape), `hidden_json` a JSON array of the withheld leaves'
|
|
216
|
+
opaque base64url digests. Reconstructs the **entire** root and compares — so the holder
|
|
217
|
+
cannot add, drop, or swap a leaf. Returns `false` on any mismatch or malformed input
|
|
218
|
+
(forgiving, like `verify`).
|
|
219
|
+
|
|
220
|
+
> **Disclosure generation is JS-only** (`setDisclose`, below) — there's no SQL
|
|
221
|
+
> `set_disclose`, because building a disclosure requires the full attribute set including
|
|
222
|
+
> the secret salts, which lives engine-side, not in a query.
|
|
223
|
+
|
|
224
|
+
**BLOB-valued attributes via SQL:** JSON has no blob type, so a blob attribute passed
|
|
225
|
+
through `set_commit` is committed as its base64url TEXT. Callers needing a *true* BLOB
|
|
226
|
+
value (committed as a BLOB field, `TAG_BLOB`) must use the JS `setCommit` with a
|
|
227
|
+
`Uint8Array` value.
|
|
228
|
+
|
|
229
|
+
**Privacy note:** a fixed commitment disclosed to two audiences exposes the same hidden
|
|
230
|
+
digests and field count to both, so they can correlate that it's the same record.
|
|
231
|
+
Re-randomizing per disclosure would require fresh salts → a new root → a new signature
|
|
232
|
+
(out of scope here).
|
|
233
|
+
|
|
234
|
+
**Framing coupling:** leaf and root reuse `digest`'s `encodeFields`, so a future digest
|
|
235
|
+
framing-version bump changes `set_commit` output too — intentional (one canonical
|
|
236
|
+
framing), but it means the pinned set-commitment vectors move with the digest vectors.
|
|
237
|
+
|
|
89
238
|
### hash_mod(data, bits, algorithm?, inputEncoding?)
|
|
90
239
|
|
|
91
240
|
Hash data and return modulo 2^bits for fixed-size hash values.
|
|
@@ -121,6 +270,16 @@ SELECT random_bytes(64) as random_id;
|
|
|
121
270
|
|
|
122
271
|
### sign(data, privateKey, curve?, inputEncoding?, keyEncoding?, outputEncoding?)
|
|
123
272
|
|
|
273
|
+
**Security note (replication):** passing a private key to `sign()` as a literal or
|
|
274
|
+
bound parameter is **safe** with respect to Optimystic replication. The Quereus engine
|
|
275
|
+
rebuilds the replicated statement from evaluated column values — not from source SQL —
|
|
276
|
+
so the key argument is evaluated away and never reaches the record. Peers re-execute
|
|
277
|
+
`INSERT ... VALUES (<signature>)`, never the original `sign(..., key)` call. The one
|
|
278
|
+
thing to avoid is storing a raw private key **as a column value** in an
|
|
279
|
+
optimystic-backed table, since any persisted column value is replicated. Sign or derive
|
|
280
|
+
and store only the public result (signature, public key, commitment). See
|
|
281
|
+
`docs/transactions.md` § "Secrets and the replicated statement record" for full detail.
|
|
282
|
+
|
|
124
283
|
Sign data using secp256k1 (default), P-256, or Ed25519.
|
|
125
284
|
|
|
126
285
|
```sql
|
|
@@ -152,6 +311,40 @@ SELECT verify('hello', 'c2lnbmF0dXJl', 'cHVibGljS2V5', 'secp256k1', 'utf8') as i
|
|
|
152
311
|
SELECT verify('data', 'c2lnbmF0dXJl', 'cHVibGljS2V5', 'p256', 'utf8') as is_valid;
|
|
153
312
|
```
|
|
154
313
|
|
|
314
|
+
## Migration (v0.14 breaking change)
|
|
315
|
+
|
|
316
|
+
The `digest()` JS/TypeScript API changed significantly in v0.14. See [CHANGELOG.md](CHANGELOG.md) for the
|
|
317
|
+
full migration guide, including old → new call-site examples, what changed in the SQL function, and
|
|
318
|
+
the open bare-hash-helper question.
|
|
319
|
+
|
|
320
|
+
## Digest configuration
|
|
321
|
+
|
|
322
|
+
Because `digest` is variadic over data, its **algorithm** and **output encoding** are
|
|
323
|
+
not call arguments — they are bound once when the plugin is loaded, via the plugin
|
|
324
|
+
config object:
|
|
325
|
+
|
|
326
|
+
```ts
|
|
327
|
+
import { registerPlugin } from '@quereus/quereus';
|
|
328
|
+
import cryptoPlugin from '@optimystic/quereus-plugin-crypto/plugin';
|
|
329
|
+
|
|
330
|
+
await registerPlugin(db, cryptoPlugin, {
|
|
331
|
+
algorithm: 'sha256', // 'sha256' (default) | 'sha512' | 'blake3'
|
|
332
|
+
encoding: 'base64url', // 'base64url' (default) | 'base64' | 'hex'
|
|
333
|
+
});
|
|
334
|
+
```
|
|
335
|
+
|
|
336
|
+
An unknown `algorithm` or a non-text `encoding` throws at registration (fail fast),
|
|
337
|
+
and the algorithm/encoding are resolved once so the per-call path does no branching.
|
|
338
|
+
|
|
339
|
+
**Why load-time and not per-connection?** The SQL `digest` is registered as
|
|
340
|
+
`replicable` — its output must be bit-identical across peers, platforms, and app
|
|
341
|
+
versions, because these digests are signed and persisted as commitments. That holds
|
|
342
|
+
only if the configuration is fixed for every peer. *Mutable* per-connection
|
|
343
|
+
configuration (e.g. a runtime `SET`/PRAGMA) would let two peers disagree and silently
|
|
344
|
+
break signature validation, so it is intentionally not offered for this function. If a
|
|
345
|
+
single database genuinely needs two digest configurations, register the plugin twice
|
|
346
|
+
(or expose named variants) rather than flipping mutable session state.
|
|
347
|
+
|
|
155
348
|
## Supported Algorithms
|
|
156
349
|
|
|
157
350
|
### Hash Algorithms
|
|
@@ -216,7 +409,7 @@ const privateKey = generatePrivateKey('secp256k1', 'base64url');
|
|
|
216
409
|
const publicKey = getPublicKey(privateKey);
|
|
217
410
|
|
|
218
411
|
const message = 'Hello, World!';
|
|
219
|
-
const hash = digest(message, 'sha256', '
|
|
412
|
+
const hash = digest([message], 'sha256', 'base64url') as string;
|
|
220
413
|
const signature = sign(hash, privateKey);
|
|
221
414
|
const isValid = verify(hash, signature, publicKey);
|
|
222
415
|
|
|
@@ -229,9 +422,59 @@ console.log('Random nonce:', nonce);
|
|
|
229
422
|
|
|
230
423
|
## JavaScript API Reference
|
|
231
424
|
|
|
232
|
-
### digest(
|
|
233
|
-
|
|
234
|
-
|
|
425
|
+
### digest(fields, algorithm?, encoding?)
|
|
426
|
+
Injective digest over an ordered tuple of fields. `fields` is an array of values
|
|
427
|
+
(any SQL value type). `algorithm` defaults to `'sha256'`, `encoding` to `'base64url'`.
|
|
428
|
+
- **Returns**: Hash as string (or Uint8Array if encoding is `'bytes'`)
|
|
429
|
+
- **Related**: `encodeFields(fields)` returns the canonical pre-hash byte framing;
|
|
430
|
+
`digestFields(fields, hasher, encode)` / `resolveHasher` / `resolveOutputEncoder`
|
|
431
|
+
are the building blocks the SQL function composes (resolve once, no per-call branching).
|
|
432
|
+
|
|
433
|
+
### cid(data, codec?, hash?, base?)
|
|
434
|
+
Hash `data` (a `Uint8Array`) and frame it as a self-describing CIDv1 string. Defaults:
|
|
435
|
+
`codec='raw'`, `hash='sha2-256'`, `base='base32'`. Byte-identical to the CID an IPFS/IPLD
|
|
436
|
+
store computes for the same bytes.
|
|
437
|
+
- **Returns**: CIDv1 string
|
|
438
|
+
|
|
439
|
+
### cidV1(digest, hash, codec?, base?)
|
|
440
|
+
Frame an **already-computed** `digest` (a `Uint8Array`) as a CIDv1 without re-hashing.
|
|
441
|
+
`hash` asserts which algorithm produced the digest; the digest length is validated
|
|
442
|
+
against it. Use to turn a `digest(...)` result into a CID: `cidV1(digest(fields, 'sha256', 'bytes'), 'sha2-256')`.
|
|
443
|
+
- **Returns**: CIDv1 string
|
|
444
|
+
|
|
445
|
+
### cidDecode(cid)
|
|
446
|
+
Parse a CID string into `{ version, codec, hashCode, digest }` for validation/migration.
|
|
447
|
+
Recognized codec/hash codes are returned as names, otherwise as numbers; `digest` is a
|
|
448
|
+
`Uint8Array`. Throws on malformed input.
|
|
449
|
+
- **Returns**: `{ version: number, codec: Multicodec | number, hashCode: MultihashCode | number, digest: Uint8Array }`
|
|
450
|
+
|
|
451
|
+
### setCommit(leaves, hasher?, encode?) / setDisclose(leaves, revealNames, hasher?) / setVerify(root, disclosure, hasher?, encode?)
|
|
452
|
+
Salted-leaf set commitment for selective disclosure. `leaves` is an array of
|
|
453
|
+
`{ name, value, salt }` (`salt` a base64url string or `Uint8Array`).
|
|
454
|
+
- **`setCommit`** → the root (`string`, or `Uint8Array` with a bytes encoder). Throws on a
|
|
455
|
+
duplicate name or a missing/empty salt; the empty set is well-defined, not an error.
|
|
456
|
+
- **`setDisclose`** → `{ disclosed, hidden }`: the revealed `{ name, value, salt }` triples
|
|
457
|
+
plus the opaque base64url leaf digests of the withheld leaves (withheld values/salts never
|
|
458
|
+
appear). This is the engine-side generator with no SQL equivalent.
|
|
459
|
+
- **`setVerify`** → `boolean`: reconstructs the entire root from `disclosure` and compares to
|
|
460
|
+
`root`. `false` on mismatch or malformed input. `encode` is how the signed root is rendered
|
|
461
|
+
(default base64url); a `Uint8Array` root is compared by raw bytes.
|
|
462
|
+
- **`leafDigest(leaf, hasher)`** → raw leaf digest bytes (the low-level building block).
|
|
463
|
+
|
|
464
|
+
```typescript
|
|
465
|
+
import { setCommit, setDisclose, setVerify, randomBytes } from '@optimystic/quereus-plugin-crypto';
|
|
466
|
+
|
|
467
|
+
const leaves = [
|
|
468
|
+
{ name: 'name', value: 'Alice', salt: randomBytes(256) as string },
|
|
469
|
+
{ name: 'over18', value: true, salt: randomBytes(256) as string },
|
|
470
|
+
{ name: 'zip', value: '90210', salt: randomBytes(256) as string },
|
|
471
|
+
];
|
|
472
|
+
const root = setCommit(leaves); // sign / persist this (often as cid(root))
|
|
473
|
+
|
|
474
|
+
// Recipient gets only `over18`, with proof it belongs to the committed set:
|
|
475
|
+
const disclosure = setDisclose(leaves, ['over18']);
|
|
476
|
+
const ok = setVerify(root, disclosure); // true — withheld values never left the engine
|
|
477
|
+
```
|
|
235
478
|
|
|
236
479
|
### hashMod(data, bits, algorithm?, inputEncoding?)
|
|
237
480
|
Hash data and return modulo 2^bits for fixed-size hash values.
|