rootcard 0.0.0 → 0.1.0

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/package.json CHANGED
@@ -1,21 +1,44 @@
1
1
  {
2
2
  "name": "rootcard",
3
- "version": "0.0.0",
3
+ "version": "0.1.0",
4
4
  "description": "A portable, self-certifying identity for online logins (core library, zero dependencies, WebCrypto)",
5
- "keywords": ["identity", "authentication", "login", "ed25519", "webcrypto", "self-certifying"],
5
+ "keywords": [
6
+ "identity",
7
+ "authentication",
8
+ "login",
9
+ "ed25519",
10
+ "webcrypto",
11
+ "self-certifying"
12
+ ],
6
13
  "license": "MIT",
7
14
  "homepage": "https://github.com/Hexagon/rootcard",
8
- "repository": { "type": "git", "url": "git+https://github.com/Hexagon/rootcard.git", "directory": "packages/rootcard" },
15
+ "repository": {
16
+ "type": "git",
17
+ "url": "git+https://github.com/Hexagon/rootcard.git",
18
+ "directory": "packages/rootcard"
19
+ },
9
20
  "bugs": "https://github.com/Hexagon/rootcard/issues",
10
21
  "type": "module",
11
22
  "exports": {
12
23
  ".": "./src/index.js",
24
+ "./vault": "./src/vault-index.js",
25
+ "./testing": "./src/testing.js",
26
+ "./keytool.html": "./keytool.html",
13
27
  "./package.json": "./package.json"
14
28
  },
15
- "files": ["src/", "README.md", "CHANGELOG.md", "LICENSE"],
29
+ "files": [
30
+ "src/",
31
+ "keytool.html",
32
+ "README.md",
33
+ "CHANGELOG.md",
34
+ "LICENSE"
35
+ ],
16
36
  "sideEffects": false,
17
- "engines": { "node": ">=20" },
37
+ "engines": {
38
+ "node": ">=20"
39
+ },
18
40
  "scripts": {
19
- "test": "node --test"
41
+ "test": "node --test",
42
+ "build": "node tools/build-keytool.mjs"
20
43
  }
21
44
  }
package/src/account.js ADDED
@@ -0,0 +1,89 @@
1
+ // High-level flows (SPEC §18): registration ([CLT-1]), new instances ([KT-3], [KT-4], [KT-8]) and recovery-key
2
+ // rotation ([KT-5]). The random draws are in the same order as the Rust crate's `account` module.
3
+
4
+ import { fail } from "./errors.js";
5
+ import { base64urlEncode } from "./encoding.js";
6
+ import { inception } from "./id.js";
7
+ import { checkRecoveryKey, eventHash, pendingInstance, rotateRecoveryEvent, verifyLog } from "./log.js";
8
+ import { signPublic } from "./profile.js";
9
+ import { deriveRecoveryKeys } from "./recovery.js";
10
+ import { recoveryCard } from "./file.js";
11
+ import { carriedLocks, checkPassword, instanceVault, regrant } from "./vault.js";
12
+
13
+ /**
14
+ * Build a registration ([CLT-1]) from a recovery key the caller generated ([RK-3]). `home` is `{ key, url }` with
15
+ * a key string. Draws: the account seed (32), then instanceVault with one new slot.
16
+ * @returns {Promise<{ log, vault, public, card, account }>}
17
+ */
18
+ export async function register(recoveryKey, realm, username, password, slotLabel, home, time, kdf, random) {
19
+ checkPassword(password, username);
20
+ const keys = await deriveRecoveryKeys(recoveryKey, realm, username);
21
+ const inc = await inception(keys, realm, username);
22
+ const state = await verifyLog([inc], { realms: [realm] });
23
+ const seed = random(32);
24
+ const pending = await pendingInstance(state, keys.signSeed, seed, home, "register", time);
25
+ const instance = base64urlEncode(await eventHash(pending));
26
+ const vault = await instanceVault(
27
+ {
28
+ account: state.account,
29
+ instance,
30
+ recovery: keys,
31
+ contents: { accountSeed: seed, data: {} },
32
+ keep: [],
33
+ add: { label: slotLabel, password, kdf },
34
+ },
35
+ random,
36
+ );
37
+ const pub = await signPublic(state.account, instance, 1, {}, seed);
38
+ const card = await recoveryCard(realm, username, state.account, recoveryKey);
39
+ seed.fill(0);
40
+ keys.signSeed.fill(0);
41
+ keys.boxSecret.fill(0);
42
+ return { log: [inc, pending], vault, public: pub, card, account: state.account };
43
+ }
44
+
45
+ /**
46
+ * Build a new instance on top of `state` ([KT-3]). `mode` is `{ restore: true }`, `{ revoke: [slot ids] }`,
47
+ * `{ reset: slot }` or `{ fresh: slot }`, where a slot is `{ label, password, kdf }`. `old` is the current vault
48
+ * unlocked with the recovery lock (not needed for fresh). Draws: the account seed (32), then instanceVault.
49
+ * @returns {Promise<{ event, vault, public }>}
50
+ */
51
+ export async function newInstance(state, recovery, old, oldPublicData, mode, home, time, random) {
52
+ await checkRecoveryKey(state, recovery.signSeed);
53
+ const seed = random(32);
54
+ const reason = mode.restore ? "restore" : mode.fresh ? "fresh" : "revoke";
55
+ const event = await pendingInstance(state, recovery.signSeed, seed, home, reason, time);
56
+ const instance = base64urlEncode(await eventHash(event));
57
+ let data = {};
58
+ let known;
59
+ let keep = [];
60
+ let add;
61
+ let publicData = {};
62
+ if (!mode.fresh) {
63
+ if (!old) fail("bad_locks");
64
+ data = old.contents.data;
65
+ known = old.contents.known;
66
+ publicData = oldPublicData ?? {};
67
+ if (mode.restore) keep = carriedLocks(old, []);
68
+ else if (mode.revoke) keep = carriedLocks(old, mode.revoke);
69
+ else add = mode.reset;
70
+ } else add = mode.fresh;
71
+ const contents = { accountSeed: seed, data };
72
+ if (known !== undefined) contents.known = known;
73
+ const vault = await instanceVault({ account: state.account, instance, recovery, contents, keep, add }, random);
74
+ const pub = await signPublic(state.account, instance, 1, publicData, seed);
75
+ seed.fill(0);
76
+ return { event, vault, public: pub };
77
+ }
78
+
79
+ /**
80
+ * Rotate the recovery key ([KT-5]) to `newKey`. `unlocked` is the current vault unlocked. Draws: regrant.
81
+ * @returns {Promise<{ event, vault, card }>}
82
+ */
83
+ export async function rotateRecovery(state, log, oldKeys, newKey, oldVault, unlocked, time, random) {
84
+ const next = await deriveRecoveryKeys(newKey, state.realm, state.username);
85
+ const event = await rotateRecoveryEvent(state, oldKeys.signSeed, next.signSeed, time);
86
+ const vault = await regrant(unlocked, oldVault, next, random);
87
+ const card = await recoveryCard(state.realm, state.username, state.account, newKey, [...log, event]);
88
+ return { event, vault, card };
89
+ }
package/src/argon2.js ADDED
@@ -0,0 +1,239 @@
1
+ // Argon2id, version 0x13 (RFC 9106), the one primitive this package implements itself ([CRY-1]). It supports the
2
+ // general parameters of the RFC (lanes, secret, associated data) so that it can be checked against the RFC vectors;
3
+ // rootcard uses p = 1, no secret and no associated data ([CRY-6]).
4
+
5
+ import { blake2b } from "./blake2b.js";
6
+
7
+ function le32(n) {
8
+ return new Uint8Array([n & 255, (n >>> 8) & 255, (n >>> 16) & 255, (n >>> 24) & 255]);
9
+ }
10
+
11
+ /** H' (RFC 9106 §3.3): variable-length hash of `parts` to `T` bytes. */
12
+ function hPrime(T, ...parts) {
13
+ if (T <= 64) return blake2b(T, le32(T), ...parts);
14
+ const out = new Uint8Array(T);
15
+ const r = Math.ceil(T / 32) - 2;
16
+ let v = blake2b(64, le32(T), ...parts);
17
+ out.set(v.subarray(0, 32), 0);
18
+ for (let i = 1; i < r; i++) {
19
+ v = blake2b(64, v);
20
+ out.set(v.subarray(0, 32), i * 32);
21
+ }
22
+ out.set(blake2b(T - 32 * r, v), r * 32);
23
+ return out;
24
+ }
25
+
26
+ // Scratch space for the compression function: R (256 words) and Q (256 words).
27
+ const R = new Uint32Array(256);
28
+ const Q = new Uint32Array(256);
29
+
30
+ // a = a + b + 2 * lo(a) * lo(b) on 64-bit words at word offsets a and b of Q (in u32 units: 2a, 2a+1).
31
+ function fBlaMka(a, b) {
32
+ const al = Q[a];
33
+ const bl = Q[b];
34
+ // 64-bit product of the two low halves.
35
+ const a1 = al >>> 16;
36
+ const a0 = al & 0xffff;
37
+ const b1 = bl >>> 16;
38
+ const b0 = bl & 0xffff;
39
+ const p00 = a0 * b0;
40
+ const mid = a1 * b0 + a0 * b1 + (p00 >>> 16);
41
+ let ph = a1 * b1 + Math.floor(mid / 65536);
42
+ let pl = (Math.imul(al, bl) >>> 0);
43
+ // times two
44
+ ph = ((ph << 1) | (pl >>> 31)) >>> 0;
45
+ pl = (pl << 1) >>> 0;
46
+ // a + b
47
+ let lo = Q[a] + Q[b];
48
+ let hi = Q[a + 1] + Q[b + 1] + (lo >= 0x100000000 ? 1 : 0);
49
+ lo >>>= 0;
50
+ // + product
51
+ const lo2 = lo + pl;
52
+ hi = hi + ph + (lo2 >= 0x100000000 ? 1 : 0);
53
+ Q[a] = lo2;
54
+ Q[a + 1] = hi;
55
+ }
56
+
57
+ function rotr(d, n) {
58
+ const l = Q[d];
59
+ const h = Q[d + 1];
60
+ if (n === 32) {
61
+ Q[d] = h;
62
+ Q[d + 1] = l;
63
+ } else if (n < 32) {
64
+ Q[d] = (l >>> n) | (h << (32 - n));
65
+ Q[d + 1] = (h >>> n) | (l << (32 - n));
66
+ } else {
67
+ const k = n - 32;
68
+ Q[d] = (h >>> k) | (l << (32 - k));
69
+ Q[d + 1] = (l >>> k) | (h << (32 - k));
70
+ }
71
+ }
72
+
73
+ function xor(d, a) {
74
+ Q[d] ^= Q[a];
75
+ Q[d + 1] ^= Q[a + 1];
76
+ }
77
+
78
+ // GB on 64-bit words with u32 offsets a, b, c, d into Q.
79
+ function gb(a, b, c, d) {
80
+ fBlaMka(a, b);
81
+ xor(d, a);
82
+ rotr(d, 32);
83
+ fBlaMka(c, d);
84
+ xor(b, c);
85
+ rotr(b, 24);
86
+ fBlaMka(a, b);
87
+ xor(d, a);
88
+ rotr(d, 16);
89
+ fBlaMka(c, d);
90
+ xor(b, c);
91
+ rotr(b, 63);
92
+ }
93
+
94
+ // P on 16 64-bit words whose u32 offsets are in `w`.
95
+ function p(w) {
96
+ gb(w[0], w[4], w[8], w[12]);
97
+ gb(w[1], w[5], w[9], w[13]);
98
+ gb(w[2], w[6], w[10], w[14]);
99
+ gb(w[3], w[7], w[11], w[15]);
100
+ gb(w[0], w[5], w[10], w[15]);
101
+ gb(w[1], w[6], w[11], w[12]);
102
+ gb(w[2], w[7], w[8], w[13]);
103
+ gb(w[3], w[4], w[9], w[14]);
104
+ }
105
+
106
+ const ROWS = [];
107
+ const COLS = [];
108
+ for (let i = 0; i < 8; i++) {
109
+ ROWS.push(Array.from({ length: 16 }, (_, k) => 2 * (16 * i + k)));
110
+ COLS.push(Array.from({ length: 16 }, (_, k) => 2 * (2 * i + 16 * (k >> 1) + (k & 1))));
111
+ }
112
+
113
+ /**
114
+ * G(X, Y) of RFC 9106 §3.5 into `out` (all Uint32Array views of 256 words). With `xorOut`, the result is XORed into
115
+ * the existing contents of `out` (passes after the first, version 0x13).
116
+ */
117
+ function compress(out, outOff, x, xOff, y, yOff, xorOut) {
118
+ for (let i = 0; i < 256; i++) R[i] = x[xOff + i] ^ y[yOff + i];
119
+ Q.set(R);
120
+ for (let i = 0; i < 8; i++) p(ROWS[i]);
121
+ for (let i = 0; i < 8; i++) p(COLS[i]);
122
+ if (xorOut) for (let i = 0; i < 256; i++) out[outOff + i] ^= Q[i] ^ R[i];
123
+ else for (let i = 0; i < 256; i++) out[outOff + i] = Q[i] ^ R[i];
124
+ }
125
+
126
+ function blockToWords(bytes, mem, off) {
127
+ for (let i = 0; i < 256; i++) {
128
+ mem[off + i] = bytes[4 * i] | (bytes[4 * i + 1] << 8) | (bytes[4 * i + 2] << 16) | (bytes[4 * i + 3] << 24);
129
+ }
130
+ }
131
+
132
+ /**
133
+ * Argon2id (RFC 9106) with a `tagLength`-byte tag. `m` is in KiB.
134
+ * @param {Uint8Array} password
135
+ * @param {Uint8Array} salt
136
+ * @param {{ m: number, t: number, p: number, tagLength?: number, secret?: Uint8Array, ad?: Uint8Array }} params
137
+ */
138
+ export function argon2id(password, salt, params) {
139
+ const { m, t, p: lanes } = params;
140
+ const T = params.tagLength ?? 32;
141
+ const K = params.secret ?? new Uint8Array(0);
142
+ const X = params.ad ?? new Uint8Array(0);
143
+ if (!(lanes >= 1 && t >= 1 && m >= 8 * lanes && T >= 4)) throw new RangeError("argon2id parameters");
144
+ const h0 = blake2b(
145
+ 64,
146
+ le32(lanes), le32(T), le32(m), le32(t), le32(0x13), le32(2),
147
+ le32(password.length), password, le32(salt.length), salt, le32(K.length), K, le32(X.length), X,
148
+ );
149
+ const mPrime = 4 * lanes * Math.floor(m / (4 * lanes));
150
+ const q = mPrime / lanes;
151
+ const segment = q / 4;
152
+ const mem = new Uint32Array(mPrime * 256);
153
+ const at = (lane, col) => (lane * q + col) * 256;
154
+ for (let l = 0; l < lanes; l++) {
155
+ blockToWords(hPrime(1024, h0, le32(0), le32(l)), mem, at(l, 0));
156
+ blockToWords(hPrime(1024, h0, le32(1), le32(l)), mem, at(l, 1));
157
+ }
158
+ const zero = new Uint32Array(256);
159
+ const input = new Uint32Array(256);
160
+ const address = new Uint32Array(256);
161
+ for (let pass = 0; pass < t; pass++) {
162
+ for (let slice = 0; slice < 4; slice++) {
163
+ for (let lane = 0; lane < lanes; lane++) {
164
+ const independent = pass === 0 && slice < 2;
165
+ if (independent) {
166
+ input.fill(0);
167
+ input[0] = pass;
168
+ input[2] = lane;
169
+ input[4] = slice;
170
+ input[6] = mPrime;
171
+ input[8] = t;
172
+ input[10] = 2;
173
+ input[12] = 0; // counter
174
+ }
175
+ const nextAddresses = () => {
176
+ input[12]++;
177
+ address.fill(0);
178
+ compress(address, 0, zero, 0, input, 0, false);
179
+ compress(address, 0, zero, 0, address.slice(), 0, false);
180
+ };
181
+ let start = 0;
182
+ if (pass === 0 && slice === 0) {
183
+ start = 2;
184
+ if (independent) nextAddresses();
185
+ }
186
+ for (let index = start; index < segment; index++) {
187
+ const col = slice * segment + index;
188
+ const prevCol = col === 0 ? q - 1 : col - 1;
189
+ let j1;
190
+ let j2;
191
+ if (independent) {
192
+ if (index % 128 === 0) nextAddresses();
193
+ const k = 2 * (index % 128);
194
+ j1 = address[k];
195
+ j2 = address[k + 1];
196
+ } else {
197
+ const o = at(lane, prevCol);
198
+ j1 = mem[o];
199
+ j2 = mem[o + 1];
200
+ }
201
+ let refLane = j2 % lanes;
202
+ if (pass === 0 && slice === 0) refLane = lane;
203
+ const same = refLane === lane;
204
+ let area;
205
+ if (pass === 0) {
206
+ if (slice === 0) area = index - 1;
207
+ else if (same) area = slice * segment + index - 1;
208
+ else area = slice * segment + (index === 0 ? -1 : 0);
209
+ } else if (same) area = q - segment + index - 1;
210
+ else area = q - segment + (index === 0 ? -1 : 0);
211
+ // x = j1^2 >> 32; y = area * x >> 32; z = area - 1 - y
212
+ const x = Number((BigInt(j1) * BigInt(j1)) >> 32n);
213
+ const y = Number((BigInt(area) * BigInt(x)) >> 32n);
214
+ const z = area - 1 - y;
215
+ const startPos = pass === 0 ? 0 : slice === 3 ? 0 : (slice + 1) * segment;
216
+ const refCol = (startPos + z) % q;
217
+ compress(mem, at(lane, col), mem, at(lane, prevCol), mem, at(refLane, refCol), pass > 0);
218
+ }
219
+ }
220
+ }
221
+ }
222
+ const c = new Uint32Array(256);
223
+ for (let l = 0; l < lanes; l++) {
224
+ const o = at(l, q - 1);
225
+ for (let i = 0; i < 256; i++) c[i] ^= mem[o + i];
226
+ }
227
+ const cb = new Uint8Array(1024);
228
+ for (let i = 0; i < 256; i++) {
229
+ cb[4 * i] = c[i];
230
+ cb[4 * i + 1] = c[i] >>> 8;
231
+ cb[4 * i + 2] = c[i] >>> 16;
232
+ cb[4 * i + 3] = c[i] >>> 24;
233
+ }
234
+ const tag = hPrime(T, cb);
235
+ mem.fill(0);
236
+ cb.fill(0);
237
+ c.fill(0);
238
+ return tag;
239
+ }
package/src/blake2b.js ADDED
@@ -0,0 +1,120 @@
1
+ // BLAKE2b (RFC 7693), needed only inside Argon2id ([CRY-1]): WebCrypto has neither. 64-bit words are pairs of 32-bit
2
+ // integers (low, high) in a Uint32Array.
3
+
4
+ const IV = new Uint32Array([
5
+ 0xf3bcc908, 0x6a09e667, 0x84caa73b, 0xbb67ae85, 0xfe94f82b, 0x3c6ef372, 0x5f1d36f1, 0xa54ff53a, 0xade682d1,
6
+ 0x510e527f, 0x2b3e6c1f, 0x9b05688c, 0xfb41bd6b, 0x1f83d9ab, 0x137e2179, 0x5be0cd19,
7
+ ]);
8
+
9
+ const SIGMA = [
10
+ [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15],
11
+ [14, 10, 4, 8, 9, 15, 13, 6, 1, 12, 0, 2, 11, 7, 5, 3],
12
+ [11, 8, 12, 0, 5, 2, 15, 13, 10, 14, 3, 6, 7, 1, 9, 4],
13
+ [7, 9, 3, 1, 13, 12, 11, 14, 2, 6, 5, 10, 4, 0, 15, 8],
14
+ [9, 0, 5, 7, 2, 4, 10, 15, 14, 1, 11, 12, 6, 8, 3, 13],
15
+ [2, 12, 6, 10, 0, 11, 8, 3, 4, 13, 7, 5, 15, 14, 1, 9],
16
+ [12, 5, 1, 15, 14, 13, 4, 10, 0, 7, 6, 3, 9, 2, 8, 11],
17
+ [13, 11, 7, 14, 12, 1, 3, 9, 5, 0, 15, 4, 8, 6, 2, 10],
18
+ [6, 15, 14, 9, 11, 3, 0, 8, 12, 2, 13, 7, 1, 4, 10, 5],
19
+ [10, 2, 8, 4, 7, 6, 1, 5, 15, 11, 9, 14, 3, 12, 13, 0],
20
+ ].map((r) => r.map((x) => x * 2));
21
+
22
+ const v = new Uint32Array(32);
23
+ const m = new Uint32Array(32);
24
+
25
+ function add64(a, b) {
26
+ const lo = v[a] + v[b];
27
+ let hi = v[a + 1] + v[b + 1];
28
+ if (lo >= 0x100000000) hi++;
29
+ v[a] = lo;
30
+ v[a + 1] = hi;
31
+ }
32
+
33
+ function add64m(a, i) {
34
+ const lo = v[a] + m[i];
35
+ let hi = v[a + 1] + m[i + 1];
36
+ if (lo >= 0x100000000) hi++;
37
+ v[a] = lo;
38
+ v[a + 1] = hi;
39
+ }
40
+
41
+ function g(a, b, c, d, x, y) {
42
+ add64(a, b);
43
+ add64m(a, x);
44
+ let xl = v[d] ^ v[a];
45
+ let xh = v[d + 1] ^ v[a + 1];
46
+ v[d] = xh;
47
+ v[d + 1] = xl;
48
+ add64(c, d);
49
+ xl = v[b] ^ v[c];
50
+ xh = v[b + 1] ^ v[c + 1];
51
+ v[b] = (xl >>> 24) ^ (xh << 8);
52
+ v[b + 1] = (xh >>> 24) ^ (xl << 8);
53
+ add64(a, b);
54
+ add64m(a, y);
55
+ xl = v[d] ^ v[a];
56
+ xh = v[d + 1] ^ v[a + 1];
57
+ v[d] = (xl >>> 16) ^ (xh << 16);
58
+ v[d + 1] = (xh >>> 16) ^ (xl << 16);
59
+ add64(c, d);
60
+ xl = v[b] ^ v[c];
61
+ xh = v[b + 1] ^ v[c + 1];
62
+ v[b] = (xh >>> 31) ^ (xl << 1);
63
+ v[b + 1] = (xl >>> 31) ^ (xh << 1);
64
+ }
65
+
66
+ function compress(h, block, t, last) {
67
+ for (let i = 0; i < 16; i++) {
68
+ v[i] = h[i];
69
+ v[i + 16] = IV[i];
70
+ }
71
+ v[24] ^= t;
72
+ v[25] ^= t / 0x100000000;
73
+ if (last) {
74
+ v[28] = ~v[28];
75
+ v[29] = ~v[29];
76
+ }
77
+ for (let i = 0; i < 32; i++) {
78
+ m[i] = block[4 * i] | (block[4 * i + 1] << 8) | (block[4 * i + 2] << 16) | (block[4 * i + 3] << 24);
79
+ }
80
+ for (let r = 0; r < 12; r++) {
81
+ const s = SIGMA[r % 10];
82
+ g(0, 8, 16, 24, s[0], s[1]);
83
+ g(2, 10, 18, 26, s[2], s[3]);
84
+ g(4, 12, 20, 28, s[4], s[5]);
85
+ g(6, 14, 22, 30, s[6], s[7]);
86
+ g(0, 10, 20, 30, s[8], s[9]);
87
+ g(2, 12, 22, 24, s[10], s[11]);
88
+ g(4, 14, 16, 26, s[12], s[13]);
89
+ g(6, 8, 18, 28, s[14], s[15]);
90
+ }
91
+ for (let i = 0; i < 16; i++) h[i] ^= v[i] ^ v[i + 16];
92
+ }
93
+
94
+ /** BLAKE2b without a key, with an output of `outlen` bytes (1 to 64), over the concatenation of `parts`. */
95
+ export function blake2b(outlen, ...parts) {
96
+ const h = new Uint32Array(16);
97
+ h.set(IV);
98
+ h[0] ^= 0x01010000 ^ outlen;
99
+ const block = new Uint8Array(128);
100
+ let fill = 0;
101
+ let t = 0;
102
+ for (const p of parts) {
103
+ for (let i = 0; i < p.length; i++) {
104
+ if (fill === 128) {
105
+ t += 128;
106
+ compress(h, block, t, false);
107
+ fill = 0;
108
+ }
109
+ block[fill++] = p[i];
110
+ }
111
+ }
112
+ t += fill;
113
+ block.fill(0, fill);
114
+ compress(h, block, t, true);
115
+ const out = new Uint8Array(outlen);
116
+ for (let i = 0; i < outlen; i++) out[i] = h[i >> 2] >>> (8 * (i & 3));
117
+ block.fill(0);
118
+ h.fill(0);
119
+ return out;
120
+ }
package/src/crypto.js ADDED
@@ -0,0 +1,186 @@
1
+ // Cryptographic primitives (SPEC §4) over WebCrypto, and domain separation (SPEC §5).
2
+ // Private keys are imported non-extractable for signing and agreement; raw secret buffers are overwritten by the
3
+ // callers that own them ([IMP-2]).
4
+
5
+ import { fail } from "./errors.js";
6
+ import { canonical } from "./json.js";
7
+ import { concat, utf8 } from "./encoding.js";
8
+
9
+ const subtle = globalThis.crypto?.subtle;
10
+
11
+ let support;
12
+ /** Check that WebCrypto has Ed25519 and X25519 ([IMP-3]). → unsupported_browser */
13
+ export async function checkSupport() {
14
+ if (support === undefined) {
15
+ support = (async () => {
16
+ if (!subtle) return false;
17
+ try {
18
+ await subtle.importKey("raw", new Uint8Array(32).fill(9), { name: "X25519" }, true, []);
19
+ const k = await subtle.importKey("raw", ED_TEST_KEY, { name: "Ed25519" }, true, ["verify"]);
20
+ return !!k;
21
+ } catch {
22
+ return false;
23
+ }
24
+ })();
25
+ }
26
+ if (!(await support)) fail("unsupported_browser");
27
+ }
28
+
29
+ // The Ed25519 public key of RFC 8032 test 1, used only to probe for support.
30
+ const ED_TEST_KEY = new Uint8Array([
31
+ 0xd7, 0x5a, 0x98, 0x01, 0x82, 0xb1, 0x0a, 0xb7, 0xd5, 0x4b, 0xfe, 0xd3, 0xc9, 0x64, 0x07, 0x3a, 0x0e, 0xe1, 0x72,
32
+ 0xf3, 0xda, 0xa6, 0x23, 0x25, 0xaf, 0x02, 0x1a, 0x68, 0xf7, 0x07, 0x51, 0x1a,
33
+ ]);
34
+
35
+ /** `frame(label, m) = utf8(label) ‖ 0x00 ‖ m` ([DS-1]). */
36
+ export function frame(label, m) {
37
+ return concat(utf8(label), new Uint8Array([0]), m);
38
+ }
39
+
40
+ /** `frame(label, canonical(x))`. */
41
+ export function frameObject(label, x) {
42
+ return frame(label, canonical(x));
43
+ }
44
+
45
+ /** SHA-256. */
46
+ export async function sha256(m) {
47
+ return new Uint8Array(await subtle.digest("SHA-256", m));
48
+ }
49
+
50
+ /** HKDF-SHA256 with a 32-byte output ([CRY-5]). */
51
+ export async function hkdf(ikm, salt, info) {
52
+ const key = await subtle.importKey("raw", ikm, "HKDF", false, ["deriveBits"]);
53
+ return new Uint8Array(await subtle.deriveBits({ name: "HKDF", hash: "SHA-256", salt, info }, key, 256));
54
+ }
55
+
56
+ const ED_PKCS8 = new Uint8Array([0x30, 0x2e, 0x02, 0x01, 0x00, 0x30, 0x05, 0x06, 0x03, 0x2b, 0x65, 0x70, 0x04, 0x22, 0x04, 0x20]);
57
+ const X_PKCS8 = new Uint8Array([0x30, 0x2e, 0x02, 0x01, 0x00, 0x30, 0x05, 0x06, 0x03, 0x2b, 0x65, 0x6e, 0x04, 0x22, 0x04, 0x20]);
58
+
59
+ function b64uToBytes(s) {
60
+ const b = atob(s.replace(/-/g, "+").replace(/_/g, "/") + "=".repeat((4 - (s.length % 4)) % 4));
61
+ return Uint8Array.from(b, (c) => c.charCodeAt(0));
62
+ }
63
+
64
+ async function importPrivate(prefix, alg, secret, extractable, usages) {
65
+ const der = concat(prefix, secret);
66
+ try {
67
+ return await subtle.importKey("pkcs8", der, { name: alg }, extractable, usages);
68
+ } finally {
69
+ der.fill(0);
70
+ }
71
+ }
72
+
73
+ async function publicOf(prefix, alg, secret, usages) {
74
+ const k = await importPrivate(prefix, alg, secret, true, usages);
75
+ const jwk = await subtle.exportKey("jwk", k);
76
+ const x = b64uToBytes(jwk.x);
77
+ jwk.d = "";
78
+ return x;
79
+ }
80
+
81
+ /** The Ed25519 public key of a 32-byte seed ([CRY-2]). */
82
+ export async function ed25519Public(seed) {
83
+ return publicOf(ED_PKCS8, "Ed25519", seed, ["sign"]);
84
+ }
85
+
86
+ /** Import an Ed25519 seed as a non-extractable signing key ([IMP-2]). */
87
+ export async function ed25519SigningKey(seed) {
88
+ return importPrivate(ED_PKCS8, "Ed25519", seed, false, ["sign"]);
89
+ }
90
+
91
+ /** A deterministic Ed25519 signature ([CRY-8]). `key` is a seed or a key from ed25519SigningKey. */
92
+ export async function ed25519Sign(key, m) {
93
+ const k = key instanceof Uint8Array ? await ed25519SigningKey(key) : key;
94
+ return new Uint8Array(await subtle.sign("Ed25519", k, m));
95
+ }
96
+
97
+ // L, p and the small-order encodings of [CRY-3].
98
+ const L = 2n ** 252n + 27742317777372353535851937790883648493n;
99
+ const P = 2n ** 255n - 19n;
100
+ const SMALL_ORDER = [
101
+ "0100000000000000000000000000000000000000000000000000000000000000",
102
+ "ecffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff7f",
103
+ "0000000000000000000000000000000000000000000000000000000000000000",
104
+ "0000000000000000000000000000000000000000000000000000000000000080",
105
+ "26e8958fc2b227b045c3f489f2ef98f0d5dfac05d3c63339b13802886d53fc05",
106
+ "26e8958fc2b227b045c3f489f2ef98f0d5dfac05d3c63339b13802886d53fc85",
107
+ "c7176a703d4dd84fba3c0b760d10670f2a2053fa2c39ccc64ec7fd7792ac037a",
108
+ "c7176a703d4dd84fba3c0b760d10670f2a2053fa2c39ccc64ec7fd7792ac03fa",
109
+ ];
110
+ const NEG_ZERO = [
111
+ "0100000000000000000000000000000000000000000000000000000000000080",
112
+ "ecffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff",
113
+ ];
114
+
115
+ function le(bytes) {
116
+ let n = 0n;
117
+ for (let i = bytes.length - 1; i >= 0; i--) n = (n << 8n) | BigInt(bytes[i]);
118
+ return n;
119
+ }
120
+
121
+ function hex(b) {
122
+ let s = "";
123
+ for (const x of b) s += x.toString(16).padStart(2, "0");
124
+ return s;
125
+ }
126
+
127
+ /** [CRY-3] steps 3 and 4 for an encoded point: canonical and not of small order (also [KEY-3]). */
128
+ export function ed25519PointOk(e) {
129
+ if (e.length !== 32) return false;
130
+ const y = e.slice();
131
+ y[31] &= 0x7f;
132
+ if (le(y) >= P) return false;
133
+ const h = hex(e);
134
+ return !NEG_ZERO.includes(h) && !SMALL_ORDER.includes(h);
135
+ }
136
+
137
+ /** Strict Ed25519 verification ([CRY-3]). → bad_signature */
138
+ export async function ed25519Verify(publicKey, m, sig) {
139
+ if (publicKey.length !== 32 || sig.length !== 64) fail("bad_signature");
140
+ const r = sig.subarray(0, 32);
141
+ const s = sig.subarray(32);
142
+ if (le(s) >= L || !ed25519PointOk(publicKey) || !ed25519PointOk(r)) fail("bad_signature");
143
+ let ok = false;
144
+ try {
145
+ const k = await subtle.importKey("raw", publicKey, { name: "Ed25519" }, false, ["verify"]);
146
+ ok = await subtle.verify("Ed25519", k, sig, m);
147
+ } catch {
148
+ ok = false;
149
+ }
150
+ if (!ok) fail("bad_signature");
151
+ }
152
+
153
+ /** The X25519 public key of a 32-byte secret ([CRY-2]). */
154
+ export async function x25519Public(secret) {
155
+ return publicOf(X_PKCS8, "X25519", secret, ["deriveBits"]);
156
+ }
157
+
158
+ /** X25519 key agreement; an all-zero result fails ([CRY-4]). → bad_key */
159
+ export async function x25519(secret, publicKey) {
160
+ let bits;
161
+ try {
162
+ const priv = await importPrivate(X_PKCS8, "X25519", secret, false, ["deriveBits"]);
163
+ const pub = await subtle.importKey("raw", publicKey, { name: "X25519" }, false, []);
164
+ bits = new Uint8Array(await subtle.deriveBits({ name: "X25519", public: pub }, priv, 256));
165
+ } catch {
166
+ fail("bad_key");
167
+ }
168
+ if (bits.every((b) => b === 0)) fail("bad_key");
169
+ return bits;
170
+ }
171
+
172
+ /** AES-256-GCM encryption: ciphertext ‖ tag ([CRY-7]). */
173
+ export async function aesGcmEncrypt(key, nonce, plaintext, aad) {
174
+ const k = await subtle.importKey("raw", key, "AES-GCM", false, ["encrypt"]);
175
+ return new Uint8Array(await subtle.encrypt({ name: "AES-GCM", iv: nonce, additionalData: aad, tagLength: 128 }, k, plaintext));
176
+ }
177
+
178
+ /** AES-256-GCM decryption. → unlock_failed */
179
+ export async function aesGcmDecrypt(key, nonce, ct, aad) {
180
+ const k = await subtle.importKey("raw", key, "AES-GCM", false, ["decrypt"]);
181
+ try {
182
+ return new Uint8Array(await subtle.decrypt({ name: "AES-GCM", iv: nonce, additionalData: aad, tagLength: 128 }, k, ct));
183
+ } catch {
184
+ fail("unlock_failed");
185
+ }
186
+ }