@quantsafe/mcp 0.0.0-stage → 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.
@@ -0,0 +1,107 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.wrapKeyfile = wrapKeyfile;
4
+ exports.unwrapKeyfile = unwrapKeyfile;
5
+ const argon2_1 = require("@noble/hashes/argon2");
6
+ const utils_1 = require("@noble/hashes/utils");
7
+ const aead_1 = require("./aead");
8
+ const constants_1 = require("./constants");
9
+ const hkdf_1 = require("./hkdf");
10
+ const types_1 = require("./types");
11
+ const utf8Encoder = new TextEncoder();
12
+ const utf8Decoder = new TextDecoder();
13
+ function base64urlEncode(bytes) {
14
+ let bin = '';
15
+ for (let i = 0; i < bytes.length; i++) {
16
+ bin += String.fromCharCode(bytes[i]);
17
+ }
18
+ return btoa(bin).replaceAll('+', '-').replaceAll('/', '_').replace(/=+$/, '');
19
+ }
20
+ function base64urlDecode(s) {
21
+ const pad = s.length % 4 === 0 ? '' : '='.repeat(4 - (s.length % 4));
22
+ const std = s.replaceAll('-', '+').replaceAll('_', '/') + pad;
23
+ const bin = atob(std);
24
+ const out = new Uint8Array(bin.length);
25
+ for (let i = 0; i < bin.length; i++) {
26
+ out[i] = bin.charCodeAt(i);
27
+ }
28
+ return out;
29
+ }
30
+ function serializeKeypair(kp) {
31
+ const json = JSON.stringify({
32
+ pk: {
33
+ mlkem: base64urlEncode(kp.publicKey.mlkem),
34
+ x25519: base64urlEncode(kp.publicKey.x25519),
35
+ mldsa: base64urlEncode(kp.publicKey.mldsa),
36
+ },
37
+ sk: {
38
+ mlkem: base64urlEncode(kp.secretKey.mlkem),
39
+ x25519: base64urlEncode(kp.secretKey.x25519),
40
+ mldsa: base64urlEncode(kp.secretKey.mldsa),
41
+ },
42
+ });
43
+ return utf8Encoder.encode(json);
44
+ }
45
+ function deserializeKeypair(bytes) {
46
+ const obj = JSON.parse(utf8Decoder.decode(bytes));
47
+ return {
48
+ publicKey: {
49
+ mlkem: base64urlDecode(obj.pk.mlkem),
50
+ x25519: base64urlDecode(obj.pk.x25519),
51
+ mldsa: base64urlDecode(obj.pk.mldsa),
52
+ },
53
+ secretKey: {
54
+ mlkem: base64urlDecode(obj.sk.mlkem),
55
+ x25519: base64urlDecode(obj.sk.x25519),
56
+ mldsa: base64urlDecode(obj.sk.mldsa),
57
+ },
58
+ };
59
+ }
60
+ async function deriveWrapKey(passphrase, salt, params) {
61
+ const passBytes = utf8Encoder.encode(passphrase);
62
+ const argonOut = await (0, argon2_1.argon2idAsync)(passBytes, salt, {
63
+ t: params.t,
64
+ m: params.m,
65
+ p: params.p,
66
+ dkLen: 32,
67
+ });
68
+ return (0, hkdf_1.hkdfDerive)(argonOut, constants_1.HKDF_SALT, constants_1.HKDF_INFO_KEYFILE_WRAP, constants_1.AES_KEY_LEN);
69
+ }
70
+ async function wrapKeyfile(kp, passphrase, params = constants_1.ARGON2ID_PARAMS_PROD) {
71
+ if (!passphrase || passphrase.length === 0) {
72
+ throw new Error('Passphrase must be non-empty');
73
+ }
74
+ const salt = (0, utils_1.randomBytes)(constants_1.ARGON2ID_SALT_LEN);
75
+ const nonce = (0, utils_1.randomBytes)(constants_1.NONCE_LEN);
76
+ const wrapKey = await deriveWrapKey(passphrase, salt, params);
77
+ const plaintext = serializeKeypair(kp);
78
+ const wrapped = await (0, aead_1.aeadEncrypt)(wrapKey, nonce, plaintext, utf8Encoder.encode(constants_1.KEYFILE_AAD));
79
+ return {
80
+ v: 1,
81
+ kdf: 'argon2id',
82
+ kdfParams: { t: params.t, m: params.m, p: params.p },
83
+ salt: base64urlEncode(salt),
84
+ nonce: base64urlEncode(nonce),
85
+ wrapped: base64urlEncode(wrapped),
86
+ };
87
+ }
88
+ async function unwrapKeyfile(kf, passphrase) {
89
+ if (kf.v !== 1) {
90
+ throw new Error(`Unsupported keyfile version ${kf.v}`);
91
+ }
92
+ if (kf.kdf !== 'argon2id') {
93
+ throw new Error(`Unsupported keyfile KDF ${kf.kdf}`);
94
+ }
95
+ const salt = base64urlDecode(kf.salt);
96
+ const nonce = base64urlDecode(kf.nonce);
97
+ const wrapped = base64urlDecode(kf.wrapped);
98
+ const wrapKey = await deriveWrapKey(passphrase, salt, kf.kdfParams);
99
+ let plaintext;
100
+ try {
101
+ plaintext = await (0, aead_1.aeadDecrypt)(wrapKey, nonce, wrapped, utf8Encoder.encode(constants_1.KEYFILE_AAD));
102
+ }
103
+ catch {
104
+ throw new types_1.InvalidPassphraseError();
105
+ }
106
+ return deserializeKeypair(plaintext);
107
+ }
@@ -0,0 +1,23 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.keysFromSeeds = keysFromSeeds;
4
+ const ml_dsa_1 = require("@noble/post-quantum/ml-dsa");
5
+ const ml_kem_1 = require("@noble/post-quantum/ml-kem");
6
+ const ed25519_1 = require("@noble/curves/ed25519");
7
+ function keysFromSeeds(seeds) {
8
+ const mlkem = ml_kem_1.ml_kem768.keygen(seeds.mlkem);
9
+ const mldsa = ml_dsa_1.ml_dsa65.keygen(seeds.mldsa);
10
+ const x25519Pub = ed25519_1.x25519.getPublicKey(seeds.x25519);
11
+ return {
12
+ publicKey: {
13
+ mlkem: mlkem.publicKey,
14
+ x25519: x25519Pub,
15
+ mldsa: mldsa.publicKey,
16
+ },
17
+ secretKey: {
18
+ mlkem: mlkem.secretKey,
19
+ x25519: seeds.x25519,
20
+ mldsa: mldsa.secretKey,
21
+ },
22
+ };
23
+ }
@@ -0,0 +1,33 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.SignatureError = exports.CorruptFileError = exports.InvalidPassphraseError = exports.QsafeError = void 0;
4
+ class QsafeError extends Error {
5
+ code;
6
+ constructor(message, code) {
7
+ super(message);
8
+ this.code = code;
9
+ this.name = 'QsafeError';
10
+ }
11
+ }
12
+ exports.QsafeError = QsafeError;
13
+ class InvalidPassphraseError extends QsafeError {
14
+ constructor() {
15
+ super('Wrong passphrase or corrupted key file', 'INVALID_PASSPHRASE');
16
+ this.name = 'InvalidPassphraseError';
17
+ }
18
+ }
19
+ exports.InvalidPassphraseError = InvalidPassphraseError;
20
+ class CorruptFileError extends QsafeError {
21
+ constructor(detail) {
22
+ super(`Corrupt or invalid .qsafe file: ${detail}`, 'CORRUPT_FILE');
23
+ this.name = 'CorruptFileError';
24
+ }
25
+ }
26
+ exports.CorruptFileError = CorruptFileError;
27
+ class SignatureError extends QsafeError {
28
+ constructor() {
29
+ super('Signature verification failed', 'SIGNATURE_FAILED');
30
+ this.name = 'SignatureError';
31
+ }
32
+ }
33
+ exports.SignatureError = SignatureError;
@@ -0,0 +1,6 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.SEAL_ALGO = exports.SEAL_VERSION = void 0;
4
+ // Kept free of DOM code so the MCP build (Node, no canvas) stamps the same values as the web Seal.
5
+ exports.SEAL_VERSION = '1';
6
+ exports.SEAL_ALGO = 'ML-KEM-768+X25519/AES-256-GCM/ML-DSA-65';
@@ -0,0 +1,312 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.parsePngTextChunks = parsePngTextChunks;
4
+ exports.parseSealTextChunks = parseSealTextChunks;
5
+ exports.injectSealTextChunks = injectSealTextChunks;
6
+ exports.buildMinimalSealPng = buildMinimalSealPng;
7
+ const PNG_SIGNATURE = new Uint8Array([0x89, 0x50, 0x4e, 0x47, 0x0d, 0x0a, 0x1a, 0x0a]);
8
+ function readU32BE(b, o) {
9
+ return ((((b[o] ?? 0) << 24) | ((b[o + 1] ?? 0) << 16) | ((b[o + 2] ?? 0) << 8) | (b[o + 3] ?? 0)) >>> 0);
10
+ }
11
+ function bytesToAscii(b) {
12
+ let s = '';
13
+ for (let i = 0; i < b.length; i++) {
14
+ s += String.fromCharCode(b[i]);
15
+ }
16
+ return s;
17
+ }
18
+ const CRC_TABLE = (() => {
19
+ const t = new Array(256);
20
+ for (let n = 0; n < 256; n++) {
21
+ let c = n;
22
+ for (let k = 0; k < 8; k++) {
23
+ c = c & 1 ? 0xedb88320 ^ (c >>> 1) : c >>> 1;
24
+ }
25
+ t[n] = c >>> 0;
26
+ }
27
+ return t;
28
+ })();
29
+ function crc32(bytes) {
30
+ let c = 0xffffffff;
31
+ for (let i = 0; i < bytes.length; i++) {
32
+ c = (CRC_TABLE[(c ^ bytes[i]) & 0xff] ^ (c >>> 8)) >>> 0;
33
+ }
34
+ return (c ^ 0xffffffff) >>> 0;
35
+ }
36
+ function isPng(b) {
37
+ if (b.length < 8)
38
+ return false;
39
+ for (let i = 0; i < 8; i++) {
40
+ if (b[i] !== PNG_SIGNATURE[i])
41
+ return false;
42
+ }
43
+ return true;
44
+ }
45
+ function parsePngTextChunks(png) {
46
+ if (!isPng(png))
47
+ throw new Error('Not a PNG: bad signature');
48
+ const out = [];
49
+ let o = 8;
50
+ const utf8 = new TextDecoder('utf-8', { fatal: false });
51
+ while (o + 8 <= png.length) {
52
+ const len = readU32BE(png, o);
53
+ o += 4;
54
+ if (o + 4 > png.length)
55
+ throw new Error('Truncated PNG chunk type');
56
+ const type = bytesToAscii(png.slice(o, o + 4));
57
+ o += 4;
58
+ if (o + len + 4 > png.length)
59
+ throw new Error('Truncated PNG chunk data');
60
+ const data = png.slice(o, o + len);
61
+ o += len;
62
+ const declaredCrc = readU32BE(png, o);
63
+ o += 4;
64
+ const computedCrc = crc32(png.slice(o - len - 8, o - 4));
65
+ if (declaredCrc !== computedCrc) {
66
+ // Skip chunks with a bad CRC. Do not push their text payload.
67
+ if (type === 'IEND')
68
+ break;
69
+ continue;
70
+ }
71
+ if (type === 'tEXt') {
72
+ const nul = data.indexOf(0);
73
+ if (nul > 0) {
74
+ const keyword = bytesToAscii(data.slice(0, nul));
75
+ const text = bytesToAscii(data.slice(nul + 1));
76
+ out.push({ keyword, text });
77
+ }
78
+ }
79
+ else if (type === 'iTXt') {
80
+ const nul = data.indexOf(0);
81
+ if (nul > 0) {
82
+ const keyword = bytesToAscii(data.slice(0, nul));
83
+ if (data.length >= nul + 5) {
84
+ let p = nul + 3;
85
+ while (p < data.length && data[p] !== 0)
86
+ p++;
87
+ p++; // skip language tag NUL
88
+ while (p < data.length && data[p] !== 0)
89
+ p++;
90
+ p++; // skip translated keyword NUL
91
+ if (p <= data.length) {
92
+ const text = utf8.decode(data.slice(p));
93
+ out.push({ keyword, text });
94
+ }
95
+ }
96
+ }
97
+ }
98
+ if (type === 'IEND')
99
+ break;
100
+ }
101
+ return out;
102
+ }
103
+ const SEAL_TEXT_KEYS = [
104
+ 'qsafe_version',
105
+ 'qsafe_timestamp',
106
+ 'qsafe_algo',
107
+ 'qsafe_file_hash',
108
+ 'qsafe_wallet',
109
+ ];
110
+ const SEAL_X402_KEYS = {
111
+ qsafe_x402_endpoint: 'endpoint',
112
+ qsafe_x402_amount: 'amount',
113
+ qsafe_x402_token: 'token',
114
+ qsafe_x402_tx_hash: 'tx_hash',
115
+ };
116
+ const SEAL_ANCHOR_KEYS = {
117
+ qsafe_anchor_chain: 'chain_id',
118
+ qsafe_anchor_registry: 'registry',
119
+ qsafe_anchor_tx: 'tx_hash',
120
+ qsafe_anchor_account: 'account',
121
+ qsafe_anchor_time: 'time',
122
+ };
123
+ function parseSealTextChunks(png) {
124
+ let entries;
125
+ try {
126
+ entries = parsePngTextChunks(png);
127
+ }
128
+ catch {
129
+ return { valid: false, fields: {} };
130
+ }
131
+ const map = new Map();
132
+ for (const e of entries)
133
+ map.set(e.keyword, e.text);
134
+ const fields = {};
135
+ if (map.has('qsafe_version'))
136
+ fields.version = map.get('qsafe_version');
137
+ if (map.has('qsafe_timestamp'))
138
+ fields.timestamp = map.get('qsafe_timestamp');
139
+ if (map.has('qsafe_algo'))
140
+ fields.algo = map.get('qsafe_algo');
141
+ if (map.has('qsafe_file_hash'))
142
+ fields.file_hash = map.get('qsafe_file_hash');
143
+ if (map.has('qsafe_wallet'))
144
+ fields.wallet = map.get('qsafe_wallet');
145
+ const x402 = {};
146
+ let hasX402 = false;
147
+ for (const [k, field] of Object.entries(SEAL_X402_KEYS)) {
148
+ if (map.has(k)) {
149
+ x402[field] = map.get(k);
150
+ hasX402 = true;
151
+ }
152
+ }
153
+ if (hasX402)
154
+ fields.x402 = x402;
155
+ const anchor = {};
156
+ let hasAnchor = false;
157
+ for (const [k, field] of Object.entries(SEAL_ANCHOR_KEYS)) {
158
+ if (map.has(k)) {
159
+ anchor[field] = map.get(k);
160
+ hasAnchor = true;
161
+ }
162
+ }
163
+ if (hasAnchor)
164
+ fields.anchor = anchor;
165
+ const valid = SEAL_TEXT_KEYS.some((k) => map.has(k)) || hasX402;
166
+ return { valid, fields };
167
+ }
168
+ function buildChunk(type, data) {
169
+ const out = new Uint8Array(12 + data.length);
170
+ const dv = new DataView(out.buffer);
171
+ dv.setUint32(0, data.length, false);
172
+ out[4] = type.charCodeAt(0);
173
+ out[5] = type.charCodeAt(1);
174
+ out[6] = type.charCodeAt(2);
175
+ out[7] = type.charCodeAt(3);
176
+ out.set(data, 8);
177
+ const crc = crc32(out.subarray(4, 8 + data.length));
178
+ dv.setUint32(8 + data.length, crc, false);
179
+ return out;
180
+ }
181
+ function buildTextChunk(keyword, text) {
182
+ const kw = new TextEncoder().encode(keyword);
183
+ const tx = new TextEncoder().encode(text);
184
+ const data = new Uint8Array(kw.length + 1 + tx.length);
185
+ data.set(kw, 0);
186
+ data[kw.length] = 0;
187
+ data.set(tx, kw.length + 1);
188
+ return buildChunk('tEXt', data);
189
+ }
190
+ const QSAFE_KEYS_SET = new Set([
191
+ 'qsafe_version',
192
+ 'qsafe_timestamp',
193
+ 'qsafe_algo',
194
+ 'qsafe_file_hash',
195
+ 'qsafe_wallet',
196
+ 'qsafe_x402_endpoint',
197
+ 'qsafe_x402_amount',
198
+ 'qsafe_x402_token',
199
+ 'qsafe_x402_tx_hash',
200
+ ...Object.keys(SEAL_ANCHOR_KEYS),
201
+ ]);
202
+ function injectSealTextChunks(png, fields) {
203
+ if (!isPng(png))
204
+ throw new Error('Not a PNG: bad signature');
205
+ // Pass 1: walk chunks, drop any existing qsafe_* tEXt chunks (idempotency),
206
+ // remember IEND offset.
207
+ const segments = [];
208
+ segments.push(png.slice(0, 8));
209
+ let o = 8;
210
+ let iendChunk = null;
211
+ while (o + 8 <= png.length) {
212
+ const len = readU32BE(png, o);
213
+ if (o + 8 + len + 4 > png.length)
214
+ throw new Error('Truncated PNG chunk');
215
+ const type = bytesToAscii(png.slice(o + 4, o + 8));
216
+ const chunkEnd = o + 8 + len + 4;
217
+ const chunk = png.slice(o, chunkEnd);
218
+ if (type === 'IEND') {
219
+ iendChunk = chunk;
220
+ break;
221
+ }
222
+ if (type === 'tEXt') {
223
+ const data = chunk.slice(8, 8 + len);
224
+ const nul = data.indexOf(0);
225
+ if (nul > 0) {
226
+ const keyword = bytesToAscii(data.slice(0, nul));
227
+ if (QSAFE_KEYS_SET.has(keyword)) {
228
+ o = chunkEnd;
229
+ continue;
230
+ }
231
+ }
232
+ }
233
+ segments.push(chunk);
234
+ o = chunkEnd;
235
+ }
236
+ if (!iendChunk)
237
+ throw new Error('PNG missing IEND chunk');
238
+ // Pass 2: append new qsafe_* tEXt chunks, then IEND.
239
+ const newChunks = [];
240
+ if (fields.version !== undefined)
241
+ newChunks.push(buildTextChunk('qsafe_version', fields.version));
242
+ if (fields.timestamp !== undefined)
243
+ newChunks.push(buildTextChunk('qsafe_timestamp', fields.timestamp));
244
+ if (fields.algo !== undefined)
245
+ newChunks.push(buildTextChunk('qsafe_algo', fields.algo));
246
+ if (fields.file_hash !== undefined)
247
+ newChunks.push(buildTextChunk('qsafe_file_hash', fields.file_hash));
248
+ if (fields.wallet !== undefined)
249
+ newChunks.push(buildTextChunk('qsafe_wallet', fields.wallet));
250
+ if (fields.x402) {
251
+ if (fields.x402.endpoint !== undefined)
252
+ newChunks.push(buildTextChunk('qsafe_x402_endpoint', fields.x402.endpoint));
253
+ if (fields.x402.amount !== undefined)
254
+ newChunks.push(buildTextChunk('qsafe_x402_amount', fields.x402.amount));
255
+ if (fields.x402.token !== undefined)
256
+ newChunks.push(buildTextChunk('qsafe_x402_token', fields.x402.token));
257
+ if (fields.x402.tx_hash !== undefined)
258
+ newChunks.push(buildTextChunk('qsafe_x402_tx_hash', fields.x402.tx_hash));
259
+ }
260
+ if (fields.anchor) {
261
+ for (const [keyword, field] of Object.entries(SEAL_ANCHOR_KEYS)) {
262
+ const value = fields.anchor[field];
263
+ if (value !== undefined)
264
+ newChunks.push(buildTextChunk(keyword, value));
265
+ }
266
+ }
267
+ segments.push(...newChunks);
268
+ segments.push(iendChunk);
269
+ let total = 0;
270
+ for (const s of segments)
271
+ total += s.length;
272
+ const out = new Uint8Array(total);
273
+ let p = 0;
274
+ for (const s of segments) {
275
+ out.set(s, p);
276
+ p += s.length;
277
+ }
278
+ return out;
279
+ }
280
+ function buildMinimalSealPng(fields) {
281
+ // Minimal 1x1 transparent PNG carrying QuantSafe tEXt chunks.
282
+ // Used by the MCP server (no canvas) and for tests that need a seal without a renderer.
283
+ const sig = PNG_SIGNATURE;
284
+ const ihdr = new Uint8Array(13);
285
+ const ihdrView = new DataView(ihdr.buffer);
286
+ ihdrView.setUint32(0, 1, false); // width
287
+ ihdrView.setUint32(4, 1, false); // height
288
+ ihdr[8] = 8; // bit depth
289
+ ihdr[9] = 6; // color type RGBA
290
+ ihdr[10] = 0;
291
+ ihdr[11] = 0;
292
+ ihdr[12] = 0;
293
+ const ihdrChunk = buildChunk('IHDR', ihdr);
294
+ // zlib deflate of filter byte + 4 zero bytes (1x1 RGBA transparent), Adler-32 included.
295
+ // Image viewers inflate this stream, so it must be valid zlib, not only well-framed PNG.
296
+ const idatData = new Uint8Array([
297
+ 0x78, 0x9c, 0x63, 0x60, 0x00, 0x02, 0x00, 0x00, 0x05, 0x00, 0x01,
298
+ ]);
299
+ const idatChunk = buildChunk('IDAT', idatData);
300
+ const iendChunk = buildChunk('IEND', new Uint8Array(0));
301
+ const segs = [sig, ihdrChunk, idatChunk, iendChunk];
302
+ let total = 0;
303
+ for (const s of segs)
304
+ total += s.length;
305
+ const skeleton = new Uint8Array(total);
306
+ let p = 0;
307
+ for (const s of segs) {
308
+ skeleton.set(s, p);
309
+ p += s.length;
310
+ }
311
+ return injectSealTextChunks(skeleton, fields);
312
+ }
@@ -0,0 +1,90 @@
1
+ "use strict";
2
+ // Wallet-derived keys are only as stable as the wallet's signature. A local-key EOA signs with
3
+ // RFC 6979 nonces, so the same message always yields the same signature. MPC and threshold
4
+ // signers draw fresh nonces, so each signature is valid, recovers to the address, and is still
5
+ // different: keys derived from it would change on the next session and lock the user out of
6
+ // every envelope sealed to them. Asking for the signature twice catches this before any file is
7
+ // encrypted. Only the outcome is remembered, never a signature.
8
+ Object.defineProperty(exports, "__esModule", { value: true });
9
+ exports.NonDeterministicSignerError = exports.SIGNER_CHECK_STORAGE_PREFIX = void 0;
10
+ exports.localDeterminismStore = localDeterminismStore;
11
+ exports.signaturesMatch = signaturesMatch;
12
+ exports.signForDerivation = signForDerivation;
13
+ exports.SIGNER_CHECK_STORAGE_PREFIX = 'qsafe:signer-deterministic:v1:';
14
+ class NonDeterministicSignerError extends Error {
15
+ constructor(message = 'Your wallet returned two different signatures for the same message. ' +
16
+ 'Keys derived from it would change between sessions, so files sealed to them could not be ' +
17
+ 'opened later. MPC and threshold signers often sign this way. Use an EOA wallet with a ' +
18
+ 'local private key (MetaMask, Rabby, Phantom) or a key file.') {
19
+ super(message);
20
+ this.name = 'NonDeterministicSignerError';
21
+ }
22
+ }
23
+ exports.NonDeterministicSignerError = NonDeterministicSignerError;
24
+ function storageKey(address) {
25
+ return `${exports.SIGNER_CHECK_STORAGE_PREFIX}${address.toLowerCase()}`;
26
+ }
27
+ // Storage can be missing or throw (private windows, blocked site data). Failing closed means the
28
+ // check simply runs again next time, which costs one extra signature and nothing else.
29
+ function localDeterminismStore(storage) {
30
+ const resolve = () => {
31
+ if (storage)
32
+ return storage;
33
+ try {
34
+ return typeof window === 'undefined' ? null : window.localStorage;
35
+ }
36
+ catch {
37
+ return null;
38
+ }
39
+ };
40
+ return {
41
+ isVerified(address) {
42
+ try {
43
+ return resolve()?.getItem(storageKey(address)) === '1';
44
+ }
45
+ catch {
46
+ return false;
47
+ }
48
+ },
49
+ markVerified(address) {
50
+ try {
51
+ resolve()?.setItem(storageKey(address), '1');
52
+ }
53
+ catch {
54
+ // Not remembered: the next derivation on this device asks for two signatures again.
55
+ }
56
+ },
57
+ };
58
+ }
59
+ // Compared as the bytes that feed the KDF, so hex letter case does not matter while any other
60
+ // difference, the recovery byte included, does.
61
+ function signaturesMatch(a, b) {
62
+ const left = a.toLowerCase();
63
+ const right = b.toLowerCase();
64
+ if (left.length !== right.length)
65
+ return false;
66
+ let diff = 0;
67
+ for (let i = 0; i < left.length; i++)
68
+ diff |= left.charCodeAt(i) ^ right.charCodeAt(i);
69
+ return diff === 0;
70
+ }
71
+ /**
72
+ * Returns the derivation signature once it is known to be reproducible. The first time an
73
+ * address derives keys on this device it is asked to sign twice and both answers must match.
74
+ */
75
+ async function signForDerivation(options) {
76
+ const { address, sign, verify, store, onStep } = options;
77
+ const needsCheck = !store.isVerified(address);
78
+ const total = needsCheck ? 2 : 1;
79
+ onStep?.({ current: 1, total });
80
+ const first = await sign();
81
+ await verify(first);
82
+ if (!needsCheck)
83
+ return first;
84
+ onStep?.({ current: 2, total });
85
+ const second = await sign();
86
+ if (!signaturesMatch(first, second))
87
+ throw new NonDeterministicSignerError();
88
+ store.markVerified(address);
89
+ return first;
90
+ }
@@ -0,0 +1,38 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.base64Encode = base64Encode;
4
+ exports.base64Decode = base64Decode;
5
+ exports.base64urlEncode = base64urlEncode;
6
+ exports.base64urlDecode = base64urlDecode;
7
+ const BASE64_RE = /^(?:[A-Za-z0-9+/]{4})*(?:[A-Za-z0-9+/]{2}==|[A-Za-z0-9+/]{3}=)?$/;
8
+ const BASE64URL_RE = /^[A-Za-z0-9_-]*$/;
9
+ function base64Encode(bytes) {
10
+ return Buffer.from(bytes).toString('base64');
11
+ }
12
+ // Buffer.from(s, 'base64') skips every character outside the alphabet and never throws, so raw
13
+ // text or a string with stray characters decodes to other bytes and still encrypts into a valid
14
+ // envelope that can never give the original back. Only the one canonical spelling of a byte
15
+ // string is accepted; field names the input in the error.
16
+ function base64Decode(s, field) {
17
+ if (typeof s !== 'string' || !BASE64_RE.test(s)) {
18
+ throw new Error(`${field} is not base64: use the standard alphabet (A-Z, a-z, 0-9, +, /) with = padding, no whitespace or line breaks.`);
19
+ }
20
+ const bytes = new Uint8Array(Buffer.from(s, 'base64'));
21
+ if (base64Encode(bytes) !== s) {
22
+ throw new Error(`${field} is not canonical base64: its last character carries stray bits.`);
23
+ }
24
+ return bytes;
25
+ }
26
+ function base64urlEncode(bytes) {
27
+ return base64Encode(bytes).replace(/\+/g, '-').replace(/\//g, '_').replace(/=+$/g, '');
28
+ }
29
+ function base64urlDecode(s, field) {
30
+ if (typeof s !== 'string' || !BASE64URL_RE.test(s) || s.length % 4 === 1) {
31
+ throw new Error(`${field} is not unpadded base64url.`);
32
+ }
33
+ const bytes = new Uint8Array(Buffer.from(s.replace(/-/g, '+').replace(/_/g, '/'), 'base64'));
34
+ if (base64urlEncode(bytes) !== s) {
35
+ throw new Error(`${field} is not canonical base64url: its last character carries stray bits.`);
36
+ }
37
+ return bytes;
38
+ }
@@ -0,0 +1,28 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.isJsonObject = isJsonObject;
4
+ exports.canonicalize = canonicalize;
5
+ function isJsonObject(v) {
6
+ return typeof v === 'object' && v !== null && !Array.isArray(v);
7
+ }
8
+ function canonicalize(value) {
9
+ if (value === null || typeof value === 'boolean' || typeof value === 'number') {
10
+ if (typeof value === 'number' && !Number.isFinite(value)) {
11
+ throw new Error('Non-finite numbers are not canonicalizable');
12
+ }
13
+ return JSON.stringify(value);
14
+ }
15
+ if (typeof value === 'string') {
16
+ return JSON.stringify(value);
17
+ }
18
+ if (Array.isArray(value)) {
19
+ return '[' + value.map((v) => canonicalize(v)).join(',') + ']';
20
+ }
21
+ if (isJsonObject(value)) {
22
+ const keys = Object.keys(value).sort();
23
+ return ('{' +
24
+ keys.map((k) => JSON.stringify(k) + ':' + canonicalize(value[k])).join(',') +
25
+ '}');
26
+ }
27
+ throw new Error(`Unsupported JSON value: ${typeof value}`);
28
+ }