@pasosdejesus/m 0.23.0 → 0.24.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/dist/CHANGELOG.md +47 -0
- package/dist/e2e/env.test.js +13 -0
- package/dist/e2e/env.test.js.map +1 -1
- package/dist/e2e/full-wallet.d.ts +41 -0
- package/dist/e2e/full-wallet.d.ts.map +1 -0
- package/dist/e2e/full-wallet.js +138 -0
- package/dist/e2e/full-wallet.js.map +1 -0
- package/dist/e2e/full-wallet.test.d.ts +2 -0
- package/dist/e2e/full-wallet.test.d.ts.map +1 -0
- package/dist/e2e/full-wallet.test.js +37 -0
- package/dist/e2e/full-wallet.test.js.map +1 -0
- package/dist/e2e/index.d.ts +5 -1
- package/dist/e2e/index.d.ts.map +1 -1
- package/dist/e2e/index.js +2 -0
- package/dist/e2e/index.js.map +1 -1
- package/dist/e2e/local-node.d.ts +22 -0
- package/dist/e2e/local-node.d.ts.map +1 -0
- package/dist/e2e/local-node.js +144 -0
- package/dist/e2e/local-node.js.map +1 -0
- package/dist/e2e/local-node.test.d.ts +2 -0
- package/dist/e2e/local-node.test.d.ts.map +1 -0
- package/dist/e2e/local-node.test.js +43 -0
- package/dist/e2e/local-node.test.js.map +1 -0
- package/dist/e2e/signer.test.d.ts +2 -0
- package/dist/e2e/signer.test.d.ts.map +1 -0
- package/dist/e2e/signer.test.js +57 -0
- package/dist/e2e/signer.test.js.map +1 -0
- package/dist/e2e/wallet-mock.d.ts +18 -3
- package/dist/e2e/wallet-mock.d.ts.map +1 -1
- package/dist/e2e/wallet-mock.js +22 -10
- package/dist/e2e/wallet-mock.js.map +1 -1
- package/dist/e2e/wallet-mock.test.js +7 -7
- package/dist/e2e/wallet-mock.test.js.map +1 -1
- package/dist/tasks/wallet.d.ts.map +1 -1
- package/dist/tasks/wallet.js +14 -25
- package/dist/tasks/wallet.js.map +1 -1
- package/dist/test-utils/index.d.ts +1 -0
- package/dist/test-utils/index.d.ts.map +1 -1
- package/dist/test-utils/index.js +1 -0
- package/dist/test-utils/index.js.map +1 -1
- package/dist/test-utils/wallet-mocks.d.ts +44 -0
- package/dist/test-utils/wallet-mocks.d.ts.map +1 -0
- package/dist/test-utils/wallet-mocks.js +114 -0
- package/dist/test-utils/wallet-mocks.js.map +1 -0
- package/dist/test-utils/wallet-mocks.test.d.ts +2 -0
- package/dist/test-utils/wallet-mocks.test.d.ts.map +1 -0
- package/dist/test-utils/wallet-mocks.test.js +55 -0
- package/dist/test-utils/wallet-mocks.test.js.map +1 -0
- package/dist/test-utils/wallet-setup.d.ts +2 -0
- package/dist/test-utils/wallet-setup.d.ts.map +1 -0
- package/dist/test-utils/wallet-setup.js +23 -0
- package/dist/test-utils/wallet-setup.js.map +1 -0
- package/dist/test-utils/wallet-setup.test.d.ts +2 -0
- package/dist/test-utils/wallet-setup.test.d.ts.map +1 -0
- package/dist/test-utils/wallet-setup.test.js +18 -0
- package/dist/test-utils/wallet-setup.test.js.map +1 -0
- package/dist/wallet/biometric.d.ts +33 -0
- package/dist/wallet/biometric.d.ts.map +1 -0
- package/dist/wallet/biometric.js +88 -0
- package/dist/wallet/biometric.js.map +1 -0
- package/dist/wallet/biometric.test.d.ts +2 -0
- package/dist/wallet/biometric.test.d.ts.map +1 -0
- package/dist/wallet/biometric.test.js +44 -0
- package/dist/wallet/biometric.test.js.map +1 -0
- package/dist/wallet/crypto.d.ts +42 -0
- package/dist/wallet/crypto.d.ts.map +1 -0
- package/dist/wallet/crypto.js +127 -0
- package/dist/wallet/crypto.js.map +1 -0
- package/dist/wallet/crypto.test.d.ts +2 -0
- package/dist/wallet/crypto.test.d.ts.map +1 -0
- package/dist/wallet/crypto.test.js +34 -0
- package/dist/wallet/crypto.test.js.map +1 -0
- package/dist/wallet/destinations.d.ts +20 -0
- package/dist/wallet/destinations.d.ts.map +1 -0
- package/dist/wallet/destinations.js +98 -0
- package/dist/wallet/destinations.js.map +1 -0
- package/dist/wallet/destinations.test.d.ts +2 -0
- package/dist/wallet/destinations.test.d.ts.map +1 -0
- package/dist/wallet/destinations.test.js +34 -0
- package/dist/wallet/destinations.test.js.map +1 -0
- package/dist/wallet/external-provider.d.ts +33 -0
- package/dist/wallet/external-provider.d.ts.map +1 -0
- package/dist/wallet/external-provider.js +108 -0
- package/dist/wallet/external-provider.js.map +1 -0
- package/dist/wallet/external-provider.test.d.ts +2 -0
- package/dist/wallet/external-provider.test.d.ts.map +1 -0
- package/dist/wallet/external-provider.test.js +44 -0
- package/dist/wallet/external-provider.test.js.map +1 -0
- package/dist/wallet/index.d.ts +14 -0
- package/dist/wallet/index.d.ts.map +1 -0
- package/dist/wallet/index.js +24 -0
- package/dist/wallet/index.js.map +1 -0
- package/dist/wallet/indexeddb.test.d.ts +2 -0
- package/dist/wallet/indexeddb.test.d.ts.map +1 -0
- package/dist/wallet/indexeddb.test.js +28 -0
- package/dist/wallet/indexeddb.test.js.map +1 -0
- package/dist/wallet/kdf.test.d.ts +2 -0
- package/dist/wallet/kdf.test.d.ts.map +1 -0
- package/dist/wallet/kdf.test.js +54 -0
- package/dist/wallet/kdf.test.js.map +1 -0
- package/dist/wallet/provider.d.ts +38 -0
- package/dist/wallet/provider.d.ts.map +1 -0
- package/dist/wallet/provider.js +285 -0
- package/dist/wallet/provider.js.map +1 -0
- package/dist/wallet/provider.test.d.ts +2 -0
- package/dist/wallet/provider.test.d.ts.map +1 -0
- package/dist/wallet/provider.test.js +170 -0
- package/dist/wallet/provider.test.js.map +1 -0
- package/dist/wallet/signer.d.ts +55 -0
- package/dist/wallet/signer.d.ts.map +1 -0
- package/dist/wallet/signer.js +91 -0
- package/dist/wallet/signer.js.map +1 -0
- package/dist/wallet/signer.test.d.ts +2 -0
- package/dist/wallet/signer.test.d.ts.map +1 -0
- package/dist/wallet/signer.test.js +62 -0
- package/dist/wallet/signer.test.js.map +1 -0
- package/dist/wallet/siwe.d.ts +2 -0
- package/dist/wallet/siwe.d.ts.map +1 -0
- package/dist/wallet/siwe.js +5 -0
- package/dist/wallet/siwe.js.map +1 -0
- package/dist/wallet/storage/file.d.ts +19 -0
- package/dist/wallet/storage/file.d.ts.map +1 -0
- package/dist/wallet/storage/file.js +48 -0
- package/dist/wallet/storage/file.js.map +1 -0
- package/dist/wallet/storage/idb.d.ts +25 -0
- package/dist/wallet/storage/idb.d.ts.map +1 -0
- package/dist/wallet/storage/idb.js +58 -0
- package/dist/wallet/storage/idb.js.map +1 -0
- package/dist/wallet/storage/index.d.ts +5 -0
- package/dist/wallet/storage/index.d.ts.map +1 -0
- package/dist/wallet/storage/index.js +4 -0
- package/dist/wallet/storage/index.js.map +1 -0
- package/dist/wallet/storage/indexeddb.d.ts +8 -0
- package/dist/wallet/storage/indexeddb.d.ts.map +1 -0
- package/dist/wallet/storage/indexeddb.js +18 -0
- package/dist/wallet/storage/indexeddb.js.map +1 -0
- package/dist/wallet/storage/memory.d.ts +10 -0
- package/dist/wallet/storage/memory.d.ts.map +1 -0
- package/dist/wallet/storage/memory.js +19 -0
- package/dist/wallet/storage/memory.js.map +1 -0
- package/dist/wallet/storage.test.d.ts +2 -0
- package/dist/wallet/storage.test.d.ts.map +1 -0
- package/dist/wallet/storage.test.js +103 -0
- package/dist/wallet/storage.test.js.map +1 -0
- package/dist/wallet/types.d.ts +69 -0
- package/dist/wallet/types.d.ts.map +1 -0
- package/dist/wallet/types.js +6 -0
- package/dist/wallet/types.js.map +1 -0
- package/dist/wallet/wallet.d.ts +81 -0
- package/dist/wallet/wallet.d.ts.map +1 -0
- package/dist/wallet/wallet.js +358 -0
- package/dist/wallet/wallet.js.map +1 -0
- package/dist/wallet/wallet.test.d.ts +2 -0
- package/dist/wallet/wallet.test.d.ts.map +1 -0
- package/dist/wallet/wallet.test.js +150 -0
- package/dist/wallet/wallet.test.js.map +1 -0
- package/dist/wallet/web-authn.d.ts +78 -0
- package/dist/wallet/web-authn.d.ts.map +1 -0
- package/dist/wallet/web-authn.js +250 -0
- package/dist/wallet/web-authn.js.map +1 -0
- package/package.json +14 -4
- package/src/e2e/README.md +51 -0
- package/src/wallet/README.md +96 -0
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Mocks to test the wallet core in Node (`m/REQ/14` Fase 4).
|
|
3
|
+
*
|
|
4
|
+
* The wallet core needs three browser-only surfaces to be testable outside a
|
|
5
|
+
* browser: `localStorage` (destinations), `crypto.subtle` (Node 20+ already has it
|
|
6
|
+
* as a global) and `WebAuthn` (`navigator.credentials`, `PublicKeyCredential`).
|
|
7
|
+
* `IndexedDB` is not emulated here: the core tests use `MemoryStorage`, and the
|
|
8
|
+
* `IndexedDBStorage` tests need the optional `fake-indexeddb` devDependency
|
|
9
|
+
* (`installIndexedDB`).
|
|
10
|
+
*
|
|
11
|
+
* "Y todo lo que hagáis, hacedlo de corazón, como para el Señor y no para los hombres" (Colosenses 3:23)
|
|
12
|
+
*/
|
|
13
|
+
import { toBase64 } from '../wallet/crypto.js';
|
|
14
|
+
const RAW_ID = new Uint8Array([1, 2, 3, 4]);
|
|
15
|
+
function defineGlobal(key, value) {
|
|
16
|
+
Object.defineProperty(globalThis, key, { value, configurable: true, writable: true });
|
|
17
|
+
}
|
|
18
|
+
/** Minimal in-memory `localStorage` (Node has none) and installs it on `globalThis`. */
|
|
19
|
+
export function installLocalStorage() {
|
|
20
|
+
const store = new Map();
|
|
21
|
+
const storage = {
|
|
22
|
+
get length() {
|
|
23
|
+
return store.size;
|
|
24
|
+
},
|
|
25
|
+
clear: () => store.clear(),
|
|
26
|
+
getItem: (key) => (store.has(key) ? store.get(key) : null),
|
|
27
|
+
key: (index) => [...store.keys()][index] ?? null,
|
|
28
|
+
removeItem: (key) => {
|
|
29
|
+
store.delete(key);
|
|
30
|
+
},
|
|
31
|
+
setItem: (key, value) => {
|
|
32
|
+
store.set(key, String(value));
|
|
33
|
+
},
|
|
34
|
+
};
|
|
35
|
+
defineGlobal('localStorage', storage);
|
|
36
|
+
return storage;
|
|
37
|
+
}
|
|
38
|
+
/**
|
|
39
|
+
* Installs a fake IndexedDB from the optional `fake-indexeddb` devDependency.
|
|
40
|
+
* Throws a clear message when it is not installed so `IndexedDBStorage` tests are
|
|
41
|
+
* skipped instead of failing obscurely.
|
|
42
|
+
*/
|
|
43
|
+
export async function installIndexedDB() {
|
|
44
|
+
const specifier = 'fake-indexeddb/auto';
|
|
45
|
+
try {
|
|
46
|
+
await import(/* @vite-ignore */ specifier);
|
|
47
|
+
}
|
|
48
|
+
catch {
|
|
49
|
+
throw new Error('installIndexedDB requires the optional devDependency `fake-indexeddb` (pnpm add -D fake-indexeddb)');
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
/**
|
|
53
|
+
* Deterministic WebAuthn mock for `web-authn.ts`/`biometric.ts`: installs
|
|
54
|
+
* `globalThis.PublicKeyCredential` and `globalThis.navigator.credentials` so the
|
|
55
|
+
* L1/L2 layers can be exercised without hardware.
|
|
56
|
+
*/
|
|
57
|
+
export function mockWebAuthn(opts = {}) {
|
|
58
|
+
const prfSecret = opts.prfSecret ?? new Uint8Array(32).fill(7);
|
|
59
|
+
const credentialId = toBase64(RAW_ID);
|
|
60
|
+
const calls = { create: 0, get: 0 };
|
|
61
|
+
let cancelNext = opts.cancelNext ?? false;
|
|
62
|
+
const prfResult = () => prfSecret.buffer.slice(prfSecret.byteOffset, prfSecret.byteOffset + prfSecret.byteLength);
|
|
63
|
+
const credentials = {
|
|
64
|
+
create: async () => {
|
|
65
|
+
calls.create += 1;
|
|
66
|
+
return {
|
|
67
|
+
rawId: RAW_ID.buffer.slice(0),
|
|
68
|
+
getClientExtensionResults: () => ({ prf: { enabled: opts.prfSupported !== false } }),
|
|
69
|
+
};
|
|
70
|
+
},
|
|
71
|
+
get: async () => {
|
|
72
|
+
calls.get += 1;
|
|
73
|
+
if (cancelNext) {
|
|
74
|
+
cancelNext = false;
|
|
75
|
+
throw Object.assign(new Error('The operation either timed out or was not allowed.'), {
|
|
76
|
+
name: 'NotAllowedError',
|
|
77
|
+
});
|
|
78
|
+
}
|
|
79
|
+
return {
|
|
80
|
+
rawId: RAW_ID.buffer.slice(0),
|
|
81
|
+
getClientExtensionResults: () => ({ prf: { results: { first: prfResult() } } }),
|
|
82
|
+
};
|
|
83
|
+
},
|
|
84
|
+
};
|
|
85
|
+
defineGlobal('PublicKeyCredential', {
|
|
86
|
+
isUserVerifyingPlatformAuthenticatorAvailable: async () => opts.userVerifying !== false,
|
|
87
|
+
getClientCapabilities: async () => ({
|
|
88
|
+
'extension:prf': opts.prfSupported !== false,
|
|
89
|
+
}),
|
|
90
|
+
signalUnknownCredential: async () => { },
|
|
91
|
+
});
|
|
92
|
+
defineGlobal('navigator', { credentials });
|
|
93
|
+
return {
|
|
94
|
+
credentialId,
|
|
95
|
+
calls,
|
|
96
|
+
reset() {
|
|
97
|
+
calls.create = 0;
|
|
98
|
+
calls.get = 0;
|
|
99
|
+
cancelNext = false;
|
|
100
|
+
},
|
|
101
|
+
};
|
|
102
|
+
}
|
|
103
|
+
/**
|
|
104
|
+
* One-call setup for wallet tests: installs `localStorage`, tries `fake-indexeddb`
|
|
105
|
+
* and installs a default WebAuthn mock. Returns the WebAuthn mock so a test can
|
|
106
|
+
* inspect or reset it. Used by the `wallet-setup` vitest file.
|
|
107
|
+
*/
|
|
108
|
+
export function setupWalletTests(options = {}) {
|
|
109
|
+
installLocalStorage();
|
|
110
|
+
if (options.indexedDB !== false)
|
|
111
|
+
void installIndexedDB().catch(() => undefined);
|
|
112
|
+
return mockWebAuthn(options);
|
|
113
|
+
}
|
|
114
|
+
//# sourceMappingURL=wallet-mocks.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"wallet-mocks.js","sourceRoot":"","sources":["../../src/test-utils/wallet-mocks.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"wallet-mocks.test.d.ts","sourceRoot":"","sources":["../../src/test-utils/wallet-mocks.test.ts"],"names":[],"mappings":""}
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
import { afterEach, describe, expect, it } from 'vitest';
|
|
2
|
+
import { assertUserVerification, createPrfCredential, detectPlatformSupport, evaluatePrf, hasRecentUserVerification, } from '../wallet/web-authn.js';
|
|
3
|
+
import { clearDestinations, isKnownDestination, rememberDestination, setDestinationsPrefix, } from '../wallet/destinations.js';
|
|
4
|
+
import { installLocalStorage, mockWebAuthn } from './wallet-mocks.js';
|
|
5
|
+
afterEach(() => {
|
|
6
|
+
clearDestinations();
|
|
7
|
+
});
|
|
8
|
+
describe('installLocalStorage', () => {
|
|
9
|
+
it('provides a working in-memory localStorage on globalThis', () => {
|
|
10
|
+
const storage = installLocalStorage();
|
|
11
|
+
storage.setItem('a', '1');
|
|
12
|
+
expect(globalThis.localStorage.getItem('a')).toBe('1');
|
|
13
|
+
expect(storage.length).toBe(1);
|
|
14
|
+
expect(storage.key(0)).toBe('a');
|
|
15
|
+
storage.removeItem('a');
|
|
16
|
+
expect(storage.getItem('a')).toBeNull();
|
|
17
|
+
});
|
|
18
|
+
});
|
|
19
|
+
describe('destinations with mocked localStorage', () => {
|
|
20
|
+
it('remembers and reports known destinations', () => {
|
|
21
|
+
installLocalStorage();
|
|
22
|
+
setDestinationsPrefix('test.destinations.');
|
|
23
|
+
const wallet = '0xABC';
|
|
24
|
+
const dest = '0xDeF';
|
|
25
|
+
expect(isKnownDestination(wallet, dest)).toBe(false);
|
|
26
|
+
rememberDestination(wallet, dest);
|
|
27
|
+
expect(isKnownDestination(wallet, dest)).toBe(true);
|
|
28
|
+
expect(globalThis.localStorage.getItem('test.destinations.0xabc')).toContain('0xdef');
|
|
29
|
+
});
|
|
30
|
+
});
|
|
31
|
+
describe('mockWebAuthn', () => {
|
|
32
|
+
it('reports platform support and creates a PRF credential', async () => {
|
|
33
|
+
const mock = mockWebAuthn();
|
|
34
|
+
const support = await detectPlatformSupport();
|
|
35
|
+
expect(support).toEqual({ webauthn: true, userVerifying: true, prf: true });
|
|
36
|
+
const credential = await createPrfCredential('alice');
|
|
37
|
+
expect(credential.prfEnabled).toBe(true);
|
|
38
|
+
expect(credential.credentialId).toBe(mock.credentialId);
|
|
39
|
+
});
|
|
40
|
+
it('evaluates the PRF and marks the user verified', async () => {
|
|
41
|
+
const secret = new Uint8Array(32).fill(9);
|
|
42
|
+
mockWebAuthn({ prfSecret: secret });
|
|
43
|
+
const result = await evaluatePrf('AQIDBA==', new Uint8Array(32));
|
|
44
|
+
expect(Array.from(result ?? [])).toEqual(Array.from(secret));
|
|
45
|
+
await assertUserVerification('AQIDBA==');
|
|
46
|
+
expect(hasRecentUserVerification()).toBe(true);
|
|
47
|
+
});
|
|
48
|
+
it('surfaces a cancellation as NotAllowedError', async () => {
|
|
49
|
+
mockWebAuthn({ cancelNext: true });
|
|
50
|
+
await expect(evaluatePrf('AQIDBA==', new Uint8Array(32))).rejects.toMatchObject({
|
|
51
|
+
name: 'NotAllowedError',
|
|
52
|
+
});
|
|
53
|
+
});
|
|
54
|
+
});
|
|
55
|
+
//# sourceMappingURL=wallet-mocks.test.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"wallet-mocks.test.js","sourceRoot":"","sources":["../../src/test-utils/wallet-mocks.test.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"wallet-setup.d.ts","sourceRoot":"","sources":["../../src/test-utils/wallet-setup.ts"],"names":[],"mappings":""}
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Vitest setup file for wallet tests (`m/REQ/14` Fase 4).
|
|
3
|
+
*
|
|
4
|
+
* Use it as a `setupFiles` entry together with the jsdom environment so the wallet
|
|
5
|
+
* core tests can run outside a browser without reinventing the mocks:
|
|
6
|
+
*
|
|
7
|
+
* ```ts
|
|
8
|
+
* // vitest.config.ts
|
|
9
|
+
* export default {
|
|
10
|
+
* test: {
|
|
11
|
+
* environment: 'jsdom',
|
|
12
|
+
* setupFiles: ['@pasosdejesus/m/test-utils/wallet-setup'],
|
|
13
|
+
* },
|
|
14
|
+
* }
|
|
15
|
+
* ```
|
|
16
|
+
*
|
|
17
|
+
* It installs a `localStorage`, tries to load `fake-indexeddb` (optional devDependency)
|
|
18
|
+
* and installs a WebAuthn mock. For finer control, import `setupWalletTests` from
|
|
19
|
+
* `@pasosdejesus/m/test-utils` and pass options.
|
|
20
|
+
*/
|
|
21
|
+
import { setupWalletTests } from './wallet-mocks.js';
|
|
22
|
+
setupWalletTests();
|
|
23
|
+
//# sourceMappingURL=wallet-setup.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"wallet-setup.js","sourceRoot":"","sources":["../../src/test-utils/wallet-setup.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;GAmBG;AACH,OAAO,EAAE,gBAAgB,EAAE,MAAM,mBAAmB,CAAA;AAEpD,gBAAgB,EAAE,CAAA"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"wallet-setup.test.d.ts","sourceRoot":"","sources":["../../src/test-utils/wallet-setup.test.ts"],"names":[],"mappings":"AAGA,OAAO,mBAAmB,CAAA"}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
// @vitest-environment jsdom
|
|
2
|
+
import { describe, expect, it } from 'vitest';
|
|
3
|
+
// Side effect: installs localStorage, fake-indexeddb and a WebAuthn mock.
|
|
4
|
+
import './wallet-setup.js';
|
|
5
|
+
import { createPrfCredential, detectPlatformSupport } from '../wallet/web-authn.js';
|
|
6
|
+
describe('wallet-setup preset (jsdom)', () => {
|
|
7
|
+
it('provides a DOM, a localStorage and a WebAuthn mock', async () => {
|
|
8
|
+
expect(typeof document).toBe('object');
|
|
9
|
+
expect(document.createElement('div')).toBeTruthy();
|
|
10
|
+
globalThis.localStorage.setItem('k', 'v');
|
|
11
|
+
expect(globalThis.localStorage.getItem('k')).toBe('v');
|
|
12
|
+
const support = await detectPlatformSupport();
|
|
13
|
+
expect(support).toEqual({ webauthn: true, userVerifying: true, prf: true });
|
|
14
|
+
const credential = await createPrfCredential('alice');
|
|
15
|
+
expect(credential.prfEnabled).toBe(true);
|
|
16
|
+
});
|
|
17
|
+
});
|
|
18
|
+
//# sourceMappingURL=wallet-setup.test.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"wallet-setup.test.js","sourceRoot":"","sources":["../../src/test-utils/wallet-setup.test.ts"],"names":[],"mappings":"AAAA,4BAA4B;AAC5B,OAAO,EAAE,QAAQ,EAAE,MAAM,EAAE,EAAE,EAAE,MAAM,QAAQ,CAAA;AAC7C,0EAA0E;AAC1E,OAAO,mBAAmB,CAAA;AAC1B,OAAO,EAAE,mBAAmB,EAAE,qBAAqB,EAAE,MAAM,wBAAwB,CAAA;AAEnF,QAAQ,CAAC,6BAA6B,EAAE,GAAG,EAAE;IAC3C,EAAE,CAAC,oDAAoD,EAAE,KAAK,IAAI,EAAE;QAClE,MAAM,CAAC,OAAO,QAAQ,CAAC,CAAC,IAAI,CAAC,QAAQ,CAAC,CAAA;QACtC,MAAM,CAAC,QAAQ,CAAC,aAAa,CAAC,KAAK,CAAC,CAAC,CAAC,UAAU,EAAE,CAAA;QAElD,UAAU,CAAC,YAAY,CAAC,OAAO,CAAC,GAAG,EAAE,GAAG,CAAC,CAAA;QACzC,MAAM,CAAC,UAAU,CAAC,YAAY,CAAC,OAAO,CAAC,GAAG,CAAC,CAAC,CAAC,IAAI,CAAC,GAAG,CAAC,CAAA;QAEtD,MAAM,OAAO,GAAG,MAAM,qBAAqB,EAAE,CAAA;QAC7C,MAAM,CAAC,OAAO,CAAC,CAAC,OAAO,CAAC,EAAE,QAAQ,EAAE,IAAI,EAAE,aAAa,EAAE,IAAI,EAAE,GAAG,EAAE,IAAI,EAAE,CAAC,CAAA;QAE3E,MAAM,UAAU,GAAG,MAAM,mBAAmB,CAAC,OAAO,CAAC,CAAA;QACrD,MAAM,CAAC,UAAU,CAAC,UAAU,CAAC,CAAC,IAAI,CAAC,IAAI,CAAC,CAAA;IAC1C,CAAC,CAAC,CAAA;AACJ,CAAC,CAAC,CAAA"}
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
export declare const PRF_SALT_BYTES = 32;
|
|
2
|
+
export declare const HKDF_SALT_BYTES = 16;
|
|
3
|
+
/** Stored (non-secret) material needed to re-derive the wrapping key. */
|
|
4
|
+
export interface BiometricRecord {
|
|
5
|
+
v: 1;
|
|
6
|
+
address: string;
|
|
7
|
+
credentialId: string;
|
|
8
|
+
prfSalt: string;
|
|
9
|
+
hkdfSalt: string;
|
|
10
|
+
iv: string;
|
|
11
|
+
data: string;
|
|
12
|
+
createdAt: number;
|
|
13
|
+
}
|
|
14
|
+
export declare function readBiometricRecord(): Promise<BiometricRecord | null>;
|
|
15
|
+
export declare function writeBiometricRecord(record: BiometricRecord): Promise<void>;
|
|
16
|
+
export declare function deleteBiometricRecord(): Promise<void>;
|
|
17
|
+
/**
|
|
18
|
+
* Drops the sealed copy **and** tells the authenticator the passkey is gone
|
|
19
|
+
* (R-#246 §14 item 1). Used when the user turns the gesture off or deletes the
|
|
20
|
+
* wallet: before this, the orphan passkey stayed in the OS list.
|
|
21
|
+
*/
|
|
22
|
+
export declare function forgetBiometricCredential(): Promise<void>;
|
|
23
|
+
/** Encrypts `privateKey` with the key derived from the PRF secret of `prfSalt`. */
|
|
24
|
+
export declare function sealWithPrfSecret(prfSecret: Uint8Array, prfSalt: Uint8Array, privateKey: `0x${string}`, { address, credentialId }: {
|
|
25
|
+
address: string;
|
|
26
|
+
credentialId: string;
|
|
27
|
+
}): Promise<BiometricRecord>;
|
|
28
|
+
/** Decrypts the sealed key with the PRF secret. Throws `auth-failed` on failure. */
|
|
29
|
+
export declare function unsealWithPrfSecret(prfSecret: Uint8Array, record: BiometricRecord): Promise<`0x${string}`>;
|
|
30
|
+
export declare function wipePrfSecret(secret: Uint8Array | null | undefined): void;
|
|
31
|
+
/** Fresh salt for a `prf.eval` call; stored next to the sealed record. */
|
|
32
|
+
export declare function newPrfSalt(): Uint8Array;
|
|
33
|
+
//# sourceMappingURL=biometric.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"biometric.d.ts","sourceRoot":"","sources":["../../src/wallet/biometric.ts"],"names":[],"mappings":"AAgBA,eAAO,MAAM,cAAc,KAAK,CAAA;AAChC,eAAO,MAAM,eAAe,KAAK,CAAA;AAEjC,yEAAyE;AACzE,MAAM,WAAW,eAAe;IAC9B,CAAC,EAAE,CAAC,CAAA;IACJ,OAAO,EAAE,MAAM,CAAA;IACf,YAAY,EAAE,MAAM,CAAA;IACpB,OAAO,EAAE,MAAM,CAAA;IACf,QAAQ,EAAE,MAAM,CAAA;IAChB,EAAE,EAAE,MAAM,CAAA;IACV,IAAI,EAAE,MAAM,CAAA;IACZ,SAAS,EAAE,MAAM,CAAA;CAClB;AAgBD,wBAAsB,mBAAmB,IAAI,OAAO,CAAC,eAAe,GAAG,IAAI,CAAC,CAK3E;AAED,wBAAsB,oBAAoB,CAAC,MAAM,EAAE,eAAe,GAAG,OAAO,CAAC,IAAI,CAAC,CAEjF;AAED,wBAAsB,qBAAqB,IAAI,OAAO,CAAC,IAAI,CAAC,CAE3D;AAED;;;;GAIG;AACH,wBAAsB,yBAAyB,IAAI,OAAO,CAAC,IAAI,CAAC,CAI/D;AAED,mFAAmF;AACnF,wBAAsB,iBAAiB,CACrC,SAAS,EAAE,UAAU,EACrB,OAAO,EAAE,UAAU,EACnB,UAAU,EAAE,KAAK,MAAM,EAAE,EACzB,EAAE,OAAO,EAAE,YAAY,EAAE,EAAE;IAAE,OAAO,EAAE,MAAM,CAAC;IAAC,YAAY,EAAE,MAAM,CAAA;CAAE,GACnE,OAAO,CAAC,eAAe,CAAC,CAmB1B;AAED,oFAAoF;AACpF,wBAAsB,mBAAmB,CACvC,SAAS,EAAE,UAAU,EACrB,MAAM,EAAE,eAAe,GACtB,OAAO,CAAC,KAAK,MAAM,EAAE,CAAC,CAYxB;AAED,wBAAgB,aAAa,CAAC,MAAM,EAAE,UAAU,GAAG,IAAI,GAAG,SAAS,GAAG,IAAI,CAEzE;AAED,0EAA0E;AAC1E,wBAAgB,UAAU,IAAI,UAAU,CAEvC"}
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Layer L2 of https://github.com/pasosdeJesus/learn.tg/issues/246: the wallet key
|
|
3
|
+
* sealed with the WebAuthn **PRF** secret.
|
|
4
|
+
*
|
|
5
|
+
* The private key is never written in plaintext. The stored record only holds the
|
|
6
|
+
* ciphertext plus the (non-secret) salts and the credential id; the key that opens
|
|
7
|
+
* it can only be produced by the authenticator, after the user verifies (Face ID /
|
|
8
|
+
* fingerprint / device password). The password-encrypted record of `wallet.ts` is untouched
|
|
9
|
+
* and remains the recovery path.
|
|
10
|
+
*/
|
|
11
|
+
import { fromBase64, toBase64, wipe } from './crypto.js';
|
|
12
|
+
import { runInDb, STORE_BIOMETRIC } from './storage/idb.js';
|
|
13
|
+
import { deriveWrappingKey, signalUnknownCredential, toArrayBuffer } from './web-authn.js';
|
|
14
|
+
const KEY = 'current';
|
|
15
|
+
const IV_BYTES = 12;
|
|
16
|
+
export const PRF_SALT_BYTES = 32;
|
|
17
|
+
export const HKDF_SALT_BYTES = 16;
|
|
18
|
+
async function subtle() {
|
|
19
|
+
const c = globalThis.crypto?.subtle;
|
|
20
|
+
if (!c)
|
|
21
|
+
throw new Error('Web Crypto (crypto.subtle) is not available');
|
|
22
|
+
return c;
|
|
23
|
+
}
|
|
24
|
+
function randomBytes(length) {
|
|
25
|
+
const c = globalThis.crypto;
|
|
26
|
+
if (!c?.getRandomValues)
|
|
27
|
+
throw new Error('crypto.getRandomValues is not available');
|
|
28
|
+
const bytes = new Uint8Array(length);
|
|
29
|
+
c.getRandomValues(bytes);
|
|
30
|
+
return bytes;
|
|
31
|
+
}
|
|
32
|
+
export async function readBiometricRecord() {
|
|
33
|
+
const value = await runInDb(STORE_BIOMETRIC, 'readonly', (store) => store.get(KEY));
|
|
34
|
+
return value ?? null;
|
|
35
|
+
}
|
|
36
|
+
export async function writeBiometricRecord(record) {
|
|
37
|
+
await runInDb(STORE_BIOMETRIC, 'readwrite', (store) => store.put(record, KEY));
|
|
38
|
+
}
|
|
39
|
+
export async function deleteBiometricRecord() {
|
|
40
|
+
await runInDb(STORE_BIOMETRIC, 'readwrite', (store) => store.delete(KEY));
|
|
41
|
+
}
|
|
42
|
+
/**
|
|
43
|
+
* Drops the sealed copy **and** tells the authenticator the passkey is gone
|
|
44
|
+
* (R-#246 §14 item 1). Used when the user turns the gesture off or deletes the
|
|
45
|
+
* wallet: before this, the orphan passkey stayed in the OS list.
|
|
46
|
+
*/
|
|
47
|
+
export async function forgetBiometricCredential() {
|
|
48
|
+
const record = await readBiometricRecord().catch(() => null);
|
|
49
|
+
if (record?.credentialId)
|
|
50
|
+
await signalUnknownCredential(record.credentialId);
|
|
51
|
+
await deleteBiometricRecord().catch(() => undefined);
|
|
52
|
+
}
|
|
53
|
+
/** Encrypts `privateKey` with the key derived from the PRF secret of `prfSalt`. */
|
|
54
|
+
export async function sealWithPrfSecret(prfSecret, prfSalt, privateKey, { address, credentialId }) {
|
|
55
|
+
const hkdfSalt = randomBytes(HKDF_SALT_BYTES);
|
|
56
|
+
const iv = randomBytes(IV_BYTES);
|
|
57
|
+
const key = await deriveWrappingKey(prfSecret, hkdfSalt);
|
|
58
|
+
const data = await (await subtle()).encrypt({ name: 'AES-GCM', iv: toArrayBuffer(iv) }, key, toArrayBuffer(new TextEncoder().encode(privateKey)));
|
|
59
|
+
return {
|
|
60
|
+
v: 1,
|
|
61
|
+
address,
|
|
62
|
+
credentialId,
|
|
63
|
+
prfSalt: toBase64(prfSalt),
|
|
64
|
+
hkdfSalt: toBase64(hkdfSalt),
|
|
65
|
+
iv: toBase64(iv),
|
|
66
|
+
data: toBase64(new Uint8Array(data)),
|
|
67
|
+
createdAt: Date.now(),
|
|
68
|
+
};
|
|
69
|
+
}
|
|
70
|
+
/** Decrypts the sealed key with the PRF secret. Throws `auth-failed` on failure. */
|
|
71
|
+
export async function unsealWithPrfSecret(prfSecret, record) {
|
|
72
|
+
const key = await deriveWrappingKey(prfSecret, fromBase64(record.hkdfSalt));
|
|
73
|
+
try {
|
|
74
|
+
const plain = await (await subtle()).decrypt({ name: 'AES-GCM', iv: toArrayBuffer(fromBase64(record.iv)) }, key, toArrayBuffer(fromBase64(record.data)));
|
|
75
|
+
return new TextDecoder().decode(plain);
|
|
76
|
+
}
|
|
77
|
+
catch {
|
|
78
|
+
throw new Error('auth-failed');
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
export function wipePrfSecret(secret) {
|
|
82
|
+
wipe(secret);
|
|
83
|
+
}
|
|
84
|
+
/** Fresh salt for a `prf.eval` call; stored next to the sealed record. */
|
|
85
|
+
export function newPrfSalt() {
|
|
86
|
+
return randomBytes(PRF_SALT_BYTES);
|
|
87
|
+
}
|
|
88
|
+
//# sourceMappingURL=biometric.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"biometric.js","sourceRoot":"","sources":["../../src/wallet/biometric.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"biometric.test.d.ts","sourceRoot":"","sources":["../../src/wallet/biometric.test.ts"],"names":[],"mappings":""}
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
import { beforeAll, beforeEach, describe, expect, it } from 'vitest';
|
|
2
|
+
import { installIndexedDB, mockWebAuthn } from '../test-utils/wallet-mocks.js';
|
|
3
|
+
import { MemoryStorage } from './storage/memory.js';
|
|
4
|
+
import { createWallet, deleteWallet, hasBiometricUnlock, lockWallet, unlockWithBiometric, enableBiometricUnlock, disableBiometricUnlock, } from './wallet.js';
|
|
5
|
+
import { deleteBiometricRecord, readBiometricRecord } from './biometric.js';
|
|
6
|
+
const password = '12345678';
|
|
7
|
+
beforeAll(async () => {
|
|
8
|
+
await installIndexedDB();
|
|
9
|
+
});
|
|
10
|
+
describe('biometric unlock (L2) with mocked WebAuthn', () => {
|
|
11
|
+
let storage;
|
|
12
|
+
beforeEach(async () => {
|
|
13
|
+
storage = new MemoryStorage();
|
|
14
|
+
await lockWallet();
|
|
15
|
+
await deleteBiometricRecord().catch(() => undefined);
|
|
16
|
+
mockWebAuthn();
|
|
17
|
+
});
|
|
18
|
+
it('seals the key with the PRF secret and re-opens it with one gesture', async () => {
|
|
19
|
+
const { walletInfo } = await createWallet({ password, storage });
|
|
20
|
+
const { address } = await enableBiometricUnlock(password, storage);
|
|
21
|
+
expect(address).toBe(walletInfo.address);
|
|
22
|
+
expect(await hasBiometricUnlock(storage)).toBe(true);
|
|
23
|
+
const sealed = await readBiometricRecord();
|
|
24
|
+
expect(sealed?.address).toBe(walletInfo.address);
|
|
25
|
+
// El secreto no queda en claro en el registro.
|
|
26
|
+
expect(JSON.stringify(sealed)).not.toContain('privateKey');
|
|
27
|
+
await lockWallet();
|
|
28
|
+
const unlocked = await unlockWithBiometric(storage);
|
|
29
|
+
expect(unlocked.address).toBe(walletInfo.address);
|
|
30
|
+
});
|
|
31
|
+
it('forgets the sealed copy when the biometric unlock is disabled', async () => {
|
|
32
|
+
await createWallet({ password, storage });
|
|
33
|
+
await enableBiometricUnlock(password, storage);
|
|
34
|
+
await disableBiometricUnlock();
|
|
35
|
+
expect(await hasBiometricUnlock(storage)).toBe(false);
|
|
36
|
+
});
|
|
37
|
+
it('cleans the sealed copy when the wallet is deleted', async () => {
|
|
38
|
+
await createWallet({ password, storage });
|
|
39
|
+
await enableBiometricUnlock(password, storage);
|
|
40
|
+
await deleteWallet(storage);
|
|
41
|
+
expect(await readBiometricRecord().catch(() => null)).toBeNull();
|
|
42
|
+
});
|
|
43
|
+
});
|
|
44
|
+
//# sourceMappingURL=biometric.test.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"biometric.test.js","sourceRoot":"","sources":["../../src/wallet/biometric.test.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,SAAS,EAAE,UAAU,EAAE,QAAQ,EAAE,MAAM,EAAE,EAAE,EAAE,MAAM,QAAQ,CAAA;AACpE,OAAO,EAAE,gBAAgB,EAAE,YAAY,EAAE,MAAM,+BAA+B,CAAA;AAC9E,OAAO,EAAE,aAAa,EAAE,MAAM,qBAAqB,CAAA;AACnD,OAAO,EACL,YAAY,EACZ,YAAY,EACZ,kBAAkB,EAClB,UAAU,EACV,mBAAmB,EACnB,qBAAqB,EACrB,sBAAsB,GACvB,MAAM,aAAa,CAAA;AACpB,OAAO,EAAE,qBAAqB,EAAE,mBAAmB,EAAE,MAAM,gBAAgB,CAAA;AAE3E,MAAM,QAAQ,GAAG,UAAU,CAAA;AAE3B,SAAS,CAAC,KAAK,IAAI,EAAE;IACnB,MAAM,gBAAgB,EAAE,CAAA;AAC1B,CAAC,CAAC,CAAA;AAEF,QAAQ,CAAC,4CAA4C,EAAE,GAAG,EAAE;IAC1D,IAAI,OAAsB,CAAA;IAE1B,UAAU,CAAC,KAAK,IAAI,EAAE;QACpB,OAAO,GAAG,IAAI,aAAa,EAAE,CAAA;QAC7B,MAAM,UAAU,EAAE,CAAA;QAClB,MAAM,qBAAqB,EAAE,CAAC,KAAK,CAAC,GAAG,EAAE,CAAC,SAAS,CAAC,CAAA;QACpD,YAAY,EAAE,CAAA;IAChB,CAAC,CAAC,CAAA;IAEF,EAAE,CAAC,oEAAoE,EAAE,KAAK,IAAI,EAAE;QAClF,MAAM,EAAE,UAAU,EAAE,GAAG,MAAM,YAAY,CAAC,EAAE,QAAQ,EAAE,OAAO,EAAE,CAAC,CAAA;QAEhE,MAAM,EAAE,OAAO,EAAE,GAAG,MAAM,qBAAqB,CAAC,QAAQ,EAAE,OAAO,CAAC,CAAA;QAClE,MAAM,CAAC,OAAO,CAAC,CAAC,IAAI,CAAC,UAAU,CAAC,OAAO,CAAC,CAAA;QACxC,MAAM,CAAC,MAAM,kBAAkB,CAAC,OAAO,CAAC,CAAC,CAAC,IAAI,CAAC,IAAI,CAAC,CAAA;QAEpD,MAAM,MAAM,GAAG,MAAM,mBAAmB,EAAE,CAAA;QAC1C,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC,IAAI,CAAC,UAAU,CAAC,OAAO,CAAC,CAAA;QAChD,+CAA+C;QAC/C,MAAM,CAAC,IAAI,CAAC,SAAS,CAAC,MAAM,CAAC,CAAC,CAAC,GAAG,CAAC,SAAS,CAAC,YAAY,CAAC,CAAA;QAE1D,MAAM,UAAU,EAAE,CAAA;QAClB,MAAM,QAAQ,GAAG,MAAM,mBAAmB,CAAC,OAAO,CAAC,CAAA;QACnD,MAAM,CAAC,QAAQ,CAAC,OAAO,CAAC,CAAC,IAAI,CAAC,UAAU,CAAC,OAAO,CAAC,CAAA;IACnD,CAAC,CAAC,CAAA;IAEF,EAAE,CAAC,+DAA+D,EAAE,KAAK,IAAI,EAAE;QAC7E,MAAM,YAAY,CAAC,EAAE,QAAQ,EAAE,OAAO,EAAE,CAAC,CAAA;QACzC,MAAM,qBAAqB,CAAC,QAAQ,EAAE,OAAO,CAAC,CAAA;QAC9C,MAAM,sBAAsB,EAAE,CAAA;QAC9B,MAAM,CAAC,MAAM,kBAAkB,CAAC,OAAO,CAAC,CAAC,CAAC,IAAI,CAAC,KAAK,CAAC,CAAA;IACvD,CAAC,CAAC,CAAA;IAEF,EAAE,CAAC,mDAAmD,EAAE,KAAK,IAAI,EAAE;QACjE,MAAM,YAAY,CAAC,EAAE,QAAQ,EAAE,OAAO,EAAE,CAAC,CAAA;QACzC,MAAM,qBAAqB,CAAC,QAAQ,EAAE,OAAO,CAAC,CAAA;QAC9C,MAAM,YAAY,CAAC,OAAO,CAAC,CAAA;QAC3B,MAAM,CAAC,MAAM,mBAAmB,EAAE,CAAC,KAAK,CAAC,GAAG,EAAE,CAAC,IAAI,CAAC,CAAC,CAAC,QAAQ,EAAE,CAAA;IAClE,CAAC,CAAC,CAAA;AACJ,CAAC,CAAC,CAAA"}
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
/** Default work factor (R-#251). */
|
|
2
|
+
export declare const KDF_ITERATIONS = 600000;
|
|
3
|
+
/** Never below the historical value: an old record must not get weaker. */
|
|
4
|
+
export declare const KDF_ITERATIONS_FLOOR = 600000;
|
|
5
|
+
/** Ceiling so a slow phone cannot hang when calibrating (R-#251). */
|
|
6
|
+
export declare const KDF_ITERATIONS_CEILING = 5000000;
|
|
7
|
+
/** Target cost of one derivation on the device (R-#251). */
|
|
8
|
+
export declare const KDF_BUDGET_MS = 400;
|
|
9
|
+
/**
|
|
10
|
+
* Accepted secret (R-#251): NFKD + trim, so a leading/trailing space or a composed
|
|
11
|
+
* accent does not lock the user out of their own wallet. Idempotent.
|
|
12
|
+
*/
|
|
13
|
+
export declare function normalizePassword(password: string): string;
|
|
14
|
+
export declare function toBase64(bytes: Uint8Array): string;
|
|
15
|
+
export declare function fromBase64(value: string): Uint8Array;
|
|
16
|
+
export interface EncryptedSecret {
|
|
17
|
+
kdf: {
|
|
18
|
+
name: 'PBKDF2';
|
|
19
|
+
hash: 'SHA-256';
|
|
20
|
+
iterations: number;
|
|
21
|
+
salt: string;
|
|
22
|
+
};
|
|
23
|
+
cipher: {
|
|
24
|
+
name: 'AES-GCM';
|
|
25
|
+
iv: string;
|
|
26
|
+
data: string;
|
|
27
|
+
};
|
|
28
|
+
}
|
|
29
|
+
export declare function encryptSecret(plaintext: string, password: string, iterations?: number): Promise<EncryptedSecret>;
|
|
30
|
+
/**
|
|
31
|
+
* Picks the PBKDF2 work factor for this device (R-#251): measures the floor and
|
|
32
|
+
* scales it to `budgetMs`, clamped to [floor, ceiling]. The count lives in the
|
|
33
|
+
* record, so a device that chose 2 M and another that chose 600 k keep working.
|
|
34
|
+
*
|
|
35
|
+
* `measure` is injectable so tests do not depend on the machine's speed.
|
|
36
|
+
*/
|
|
37
|
+
export declare function calibrateIterations(measure?: (iterations: number) => Promise<number>, budgetMs?: number): Promise<number>;
|
|
38
|
+
export declare function decryptSecret(secret: EncryptedSecret, password: string): Promise<string>;
|
|
39
|
+
export declare function wipe(bytes: Uint8Array | null | undefined): void;
|
|
40
|
+
/** SHA-256 over bytes. Used by the BIP39 checksum of `isValidMnemonic`. */
|
|
41
|
+
export declare function sha256(bytes: Uint8Array): Promise<Uint8Array>;
|
|
42
|
+
//# sourceMappingURL=crypto.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"crypto.d.ts","sourceRoot":"","sources":["../../src/wallet/crypto.ts"],"names":[],"mappings":"AAIA,oCAAoC;AACpC,eAAO,MAAM,cAAc,SAAoB,CAAA;AAC/C,2EAA2E;AAC3E,eAAO,MAAM,oBAAoB,SAAoB,CAAA;AACrD,qEAAqE;AACrE,eAAO,MAAM,sBAAsB,UAAY,CAAA;AAC/C,4DAA4D;AAC5D,eAAO,MAAM,aAAa,MAAM,CAAA;AAEhC;;;GAGG;AACH,wBAAgB,iBAAiB,CAAC,QAAQ,EAAE,MAAM,GAAG,MAAM,CAE1D;AAsBD,wBAAgB,QAAQ,CAAC,KAAK,EAAE,UAAU,GAAG,MAAM,CAIlD;AAED,wBAAgB,UAAU,CAAC,KAAK,EAAE,MAAM,GAAG,UAAU,CAKpD;AAwBD,MAAM,WAAW,eAAe;IAC9B,GAAG,EAAE;QAAE,IAAI,EAAE,QAAQ,CAAC;QAAC,IAAI,EAAE,SAAS,CAAC;QAAC,UAAU,EAAE,MAAM,CAAC;QAAC,IAAI,EAAE,MAAM,CAAA;KAAE,CAAA;IAC1E,MAAM,EAAE;QAAE,IAAI,EAAE,SAAS,CAAC;QAAC,EAAE,EAAE,MAAM,CAAC;QAAC,IAAI,EAAE,MAAM,CAAA;KAAE,CAAA;CACtD;AAED,wBAAsB,aAAa,CACjC,SAAS,EAAE,MAAM,EACjB,QAAQ,EAAE,MAAM,EAChB,UAAU,GAAE,MAA0B,GACrC,OAAO,CAAC,eAAe,CAAC,CAe1B;AAUD;;;;;;GAMG;AACH,wBAAsB,mBAAmB,CACvC,OAAO,GAAE,CAAC,UAAU,EAAE,MAAM,KAAK,OAAO,CAAC,MAAM,CAAqB,EACpE,QAAQ,GAAE,MAAsB,GAC/B,OAAO,CAAC,MAAM,CAAC,CAajB;AAED,wBAAsB,aAAa,CAAC,MAAM,EAAE,eAAe,EAAE,QAAQ,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,CAAC,CAsB9F;AAED,wBAAgB,IAAI,CAAC,KAAK,EAAE,UAAU,GAAG,IAAI,GAAG,SAAS,GAAG,IAAI,CAE/D;AAED,2EAA2E;AAC3E,wBAAsB,MAAM,CAAC,KAAK,EAAE,UAAU,GAAG,OAAO,CAAC,UAAU,CAAC,CAGnE"}
|
|
@@ -0,0 +1,127 @@
|
|
|
1
|
+
const PBKDF2_ITERATIONS = 600_000;
|
|
2
|
+
const SALT_BYTES = 16;
|
|
3
|
+
const IV_BYTES = 12;
|
|
4
|
+
/** Default work factor (R-#251). */
|
|
5
|
+
export const KDF_ITERATIONS = PBKDF2_ITERATIONS;
|
|
6
|
+
/** Never below the historical value: an old record must not get weaker. */
|
|
7
|
+
export const KDF_ITERATIONS_FLOOR = PBKDF2_ITERATIONS;
|
|
8
|
+
/** Ceiling so a slow phone cannot hang when calibrating (R-#251). */
|
|
9
|
+
export const KDF_ITERATIONS_CEILING = 5_000_000;
|
|
10
|
+
/** Target cost of one derivation on the device (R-#251). */
|
|
11
|
+
export const KDF_BUDGET_MS = 400;
|
|
12
|
+
/**
|
|
13
|
+
* Accepted secret (R-#251): NFKD + trim, so a leading/trailing space or a composed
|
|
14
|
+
* accent does not lock the user out of their own wallet. Idempotent.
|
|
15
|
+
*/
|
|
16
|
+
export function normalizePassword(password) {
|
|
17
|
+
return typeof password === 'string' ? password.normalize('NFKD').trim() : '';
|
|
18
|
+
}
|
|
19
|
+
function subtle() {
|
|
20
|
+
const c = globalThis.crypto;
|
|
21
|
+
if (!c?.subtle) {
|
|
22
|
+
throw new Error('Web Crypto (crypto.subtle) is not available; Node 18+ or a secure browser context is required');
|
|
23
|
+
}
|
|
24
|
+
return c.subtle;
|
|
25
|
+
}
|
|
26
|
+
function randomBytes(length) {
|
|
27
|
+
const c = globalThis.crypto;
|
|
28
|
+
if (!c?.getRandomValues) {
|
|
29
|
+
throw new Error('crypto.getRandomValues is not available');
|
|
30
|
+
}
|
|
31
|
+
const bytes = new Uint8Array(length);
|
|
32
|
+
c.getRandomValues(bytes);
|
|
33
|
+
return bytes;
|
|
34
|
+
}
|
|
35
|
+
export function toBase64(bytes) {
|
|
36
|
+
let binary = '';
|
|
37
|
+
for (const b of bytes)
|
|
38
|
+
binary += String.fromCharCode(b);
|
|
39
|
+
return btoa(binary);
|
|
40
|
+
}
|
|
41
|
+
export function fromBase64(value) {
|
|
42
|
+
const binary = atob(value);
|
|
43
|
+
const bytes = new Uint8Array(binary.length);
|
|
44
|
+
for (let i = 0; i < binary.length; i++)
|
|
45
|
+
bytes[i] = binary.charCodeAt(i);
|
|
46
|
+
return bytes;
|
|
47
|
+
}
|
|
48
|
+
async function deriveKey(password, salt, iterations) {
|
|
49
|
+
const material = await subtle().importKey('raw', new TextEncoder().encode(password), 'PBKDF2', false, ['deriveKey']);
|
|
50
|
+
return subtle().deriveKey({
|
|
51
|
+
name: 'PBKDF2',
|
|
52
|
+
salt: salt,
|
|
53
|
+
iterations,
|
|
54
|
+
hash: 'SHA-256',
|
|
55
|
+
}, material, { name: 'AES-GCM', length: 256 }, false, ['encrypt', 'decrypt']);
|
|
56
|
+
}
|
|
57
|
+
export async function encryptSecret(plaintext, password, iterations = PBKDF2_ITERATIONS) {
|
|
58
|
+
const salt = randomBytes(SALT_BYTES);
|
|
59
|
+
const iv = randomBytes(IV_BYTES);
|
|
60
|
+
const count = Number.isFinite(iterations) && iterations > 0 ? Math.floor(iterations) : PBKDF2_ITERATIONS;
|
|
61
|
+
const key = await deriveKey(normalizePassword(password), salt, count);
|
|
62
|
+
const data = await subtle().encrypt({ name: 'AES-GCM', iv: iv }, key, new TextEncoder().encode(plaintext));
|
|
63
|
+
return {
|
|
64
|
+
kdf: { name: 'PBKDF2', hash: 'SHA-256', iterations: count, salt: toBase64(salt) },
|
|
65
|
+
cipher: { name: 'AES-GCM', iv: toBase64(iv), data: toBase64(new Uint8Array(data)) },
|
|
66
|
+
};
|
|
67
|
+
}
|
|
68
|
+
/** One derivation with a throwaway salt, used to measure the device cost. */
|
|
69
|
+
async function measureDerivation(iterations) {
|
|
70
|
+
const salt = randomBytes(SALT_BYTES);
|
|
71
|
+
const started = Date.now();
|
|
72
|
+
await deriveKey('calibration-only', salt, iterations);
|
|
73
|
+
return Date.now() - started;
|
|
74
|
+
}
|
|
75
|
+
/**
|
|
76
|
+
* Picks the PBKDF2 work factor for this device (R-#251): measures the floor and
|
|
77
|
+
* scales it to `budgetMs`, clamped to [floor, ceiling]. The count lives in the
|
|
78
|
+
* record, so a device that chose 2 M and another that chose 600 k keep working.
|
|
79
|
+
*
|
|
80
|
+
* `measure` is injectable so tests do not depend on the machine's speed.
|
|
81
|
+
*/
|
|
82
|
+
export async function calibrateIterations(measure = measureDerivation, budgetMs = KDF_BUDGET_MS) {
|
|
83
|
+
let floorMs = 0;
|
|
84
|
+
try {
|
|
85
|
+
floorMs = await measure(KDF_ITERATIONS_FLOOR);
|
|
86
|
+
}
|
|
87
|
+
catch {
|
|
88
|
+
return KDF_ITERATIONS_FLOOR;
|
|
89
|
+
}
|
|
90
|
+
if (!Number.isFinite(floorMs) || floorMs <= 0)
|
|
91
|
+
return KDF_ITERATIONS_FLOOR;
|
|
92
|
+
const projected = Math.floor((KDF_ITERATIONS_FLOOR * budgetMs) / floorMs);
|
|
93
|
+
if (!Number.isFinite(projected) || projected < KDF_ITERATIONS_FLOOR) {
|
|
94
|
+
return KDF_ITERATIONS_FLOOR;
|
|
95
|
+
}
|
|
96
|
+
return Math.min(KDF_ITERATIONS_CEILING, projected);
|
|
97
|
+
}
|
|
98
|
+
export async function decryptSecret(secret, password) {
|
|
99
|
+
const salt = fromBase64(secret.kdf.salt);
|
|
100
|
+
const iv = fromBase64(secret.cipher.iv);
|
|
101
|
+
// R-#251: primero con la clave normalizada (NFKD + trim) y, si el registro se
|
|
102
|
+
// cifró con la clave tal cual la escribió el usuario, con esa misma. Así una clave
|
|
103
|
+
// con un espacio o un acento compuesto sigue abriendo la billetera.
|
|
104
|
+
const normalized = normalizePassword(password);
|
|
105
|
+
const candidates = normalized === password ? [password] : [normalized, password];
|
|
106
|
+
for (const candidate of candidates) {
|
|
107
|
+
const key = await deriveKey(candidate, salt, secret.kdf.iterations);
|
|
108
|
+
try {
|
|
109
|
+
const plain = await subtle().decrypt({ name: 'AES-GCM', iv: iv }, key, fromBase64(secret.cipher.data));
|
|
110
|
+
return new TextDecoder().decode(plain);
|
|
111
|
+
}
|
|
112
|
+
catch {
|
|
113
|
+
// se prueba la siguiente variante
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
throw new Error('Wrong password or corrupted wallet data');
|
|
117
|
+
}
|
|
118
|
+
export function wipe(bytes) {
|
|
119
|
+
if (bytes)
|
|
120
|
+
bytes.fill(0);
|
|
121
|
+
}
|
|
122
|
+
/** SHA-256 over bytes. Used by the BIP39 checksum of `isValidMnemonic`. */
|
|
123
|
+
export async function sha256(bytes) {
|
|
124
|
+
const digest = await subtle().digest('SHA-256', bytes);
|
|
125
|
+
return new Uint8Array(digest);
|
|
126
|
+
}
|
|
127
|
+
//# sourceMappingURL=crypto.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"crypto.js","sourceRoot":"","sources":["../../src/wallet/crypto.ts"],"names":[],"mappings":"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"}
|