rootcard 0.0.0 → 0.2.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/CHANGELOG.md +19 -1
- package/LICENSE-APACHE +202 -0
- package/README.md +36 -3
- package/keytool.html +3123 -0
- package/package.json +31 -7
- package/src/account.js +89 -0
- package/src/argon2.js +239 -0
- package/src/binding.js +49 -0
- package/src/blake2b.js +120 -0
- package/src/crypto.js +186 -0
- package/src/encoding.js +145 -0
- package/src/errors.js +38 -0
- package/src/file.js +118 -0
- package/src/id.js +107 -0
- package/src/index.js +29 -17
- package/src/json.js +280 -0
- package/src/keys.js +134 -0
- package/src/labels.js +17 -0
- package/src/log.js +316 -0
- package/src/messages.js +82 -0
- package/src/profile.js +89 -0
- package/src/proof.js +66 -0
- package/src/recovery.js +49 -0
- package/src/testing.js +34 -0
- package/src/vault-index.js +9 -0
- package/src/vault.js +294 -0
- package/src/vaultcheck.js +180 -0
- /package/{LICENSE → LICENSE-MIT} +0 -0
package/package.json
CHANGED
|
@@ -1,21 +1,45 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "rootcard",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.2.0",
|
|
4
4
|
"description": "A portable, self-certifying identity for online logins (core library, zero dependencies, WebCrypto)",
|
|
5
|
-
"keywords": [
|
|
6
|
-
|
|
5
|
+
"keywords": [
|
|
6
|
+
"identity",
|
|
7
|
+
"authentication",
|
|
8
|
+
"login",
|
|
9
|
+
"ed25519",
|
|
10
|
+
"webcrypto",
|
|
11
|
+
"self-certifying"
|
|
12
|
+
],
|
|
13
|
+
"license": "(MIT OR Apache-2.0)",
|
|
7
14
|
"homepage": "https://github.com/Hexagon/rootcard",
|
|
8
|
-
"repository": {
|
|
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": [
|
|
29
|
+
"files": [
|
|
30
|
+
"src/",
|
|
31
|
+
"keytool.html",
|
|
32
|
+
"README.md",
|
|
33
|
+
"CHANGELOG.md",
|
|
34
|
+
"LICENSE-MIT",
|
|
35
|
+
"LICENSE-APACHE"
|
|
36
|
+
],
|
|
16
37
|
"sideEffects": false,
|
|
17
|
-
"engines": {
|
|
38
|
+
"engines": {
|
|
39
|
+
"node": ">=20"
|
|
40
|
+
},
|
|
18
41
|
"scripts": {
|
|
19
|
-
"test": "node --test"
|
|
42
|
+
"test": "node --test",
|
|
43
|
+
"build": "node tools/build-keytool.mjs"
|
|
20
44
|
}
|
|
21
45
|
}
|
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/binding.js
ADDED
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
// The HTTP binding's version and base URLs (HOST_PROTOCOL.md §1, §1.3; SPEC [HST-22]). The core makes no requests;
|
|
2
|
+
// these let hosts, clients and the key tool agree on where endpoints live. Mirrors `rootcard::binding`.
|
|
3
|
+
|
|
4
|
+
import { fail } from "./errors.js";
|
|
5
|
+
import { checkUrl } from "./log.js";
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* The version segment of the HTTP binding: every endpoint is under `<base URL>/v0` until 1.0.0, then under `/v1`.
|
|
9
|
+
* It is never part of a base URL, `home.url` or anything signed.
|
|
10
|
+
*/
|
|
11
|
+
export const API_VERSION = "v0";
|
|
12
|
+
|
|
13
|
+
/** Whether a path segment has the form of an API version (`v0`, `V12`). */
|
|
14
|
+
export function isVersionSegment(segment) {
|
|
15
|
+
return /^[vV][0-9]+$/.test(segment);
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
function authorityHost(rest) {
|
|
19
|
+
const authority = rest.split("/")[0];
|
|
20
|
+
if (authority.startsWith("[")) {
|
|
21
|
+
const i = authority.indexOf("]");
|
|
22
|
+
return i < 0 ? "" : authority.slice(0, i + 1);
|
|
23
|
+
}
|
|
24
|
+
return authority.split(":")[0];
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* Check a base URL ([HST-22]): `https://` and a host, an optional port and an optional path without a trailing slash,
|
|
29
|
+
* query, fragment or user info; plain `http://` only for `localhost`, `127.0.0.1` and `[::1]`; and its last path
|
|
30
|
+
* segment is not an API version segment. → `bad_field`
|
|
31
|
+
*/
|
|
32
|
+
export function checkBaseUrl(url) {
|
|
33
|
+
checkUrl(url);
|
|
34
|
+
let rest;
|
|
35
|
+
if (url.startsWith("https://")) rest = url.slice(8);
|
|
36
|
+
else if (url.startsWith("http://")) {
|
|
37
|
+
rest = url.slice(7);
|
|
38
|
+
if (!["localhost", "127.0.0.1", "[::1]"].includes(authorityHost(rest))) fail("bad_field");
|
|
39
|
+
} else fail("bad_field");
|
|
40
|
+
if (/[?#@\\]/.test(rest)) fail("bad_field");
|
|
41
|
+
const slash = rest.indexOf("/");
|
|
42
|
+
const authority = slash < 0 ? rest : rest.slice(0, slash);
|
|
43
|
+
if (authorityHost(authority) === "" || !/^[A-Za-z0-9.\-:[\]]+$/.test(authority)) fail("bad_field");
|
|
44
|
+
if (slash >= 0) {
|
|
45
|
+
const segments = rest.slice(slash + 1).split("/");
|
|
46
|
+
if (segments.some((x) => x === "")) fail("bad_field");
|
|
47
|
+
if (isVersionSegment(segments[segments.length - 1])) fail("bad_field");
|
|
48
|
+
}
|
|
49
|
+
}
|
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
|
+
}
|