@pasosdejesus/m 0.23.1 → 0.25.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 +45 -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 +4 -0
- 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/i18n/locales/cli.d.ts.map +1 -1
- package/dist/i18n/locales/cli.js +2 -0
- package/dist/i18n/locales/cli.js.map +1 -1
- package/dist/m.js +7 -1
- package/dist/m.js.map +1 -1
- package/dist/tasks/e2e-summary.d.ts +13 -0
- package/dist/tasks/e2e-summary.d.ts.map +1 -0
- package/dist/tasks/e2e-summary.js +117 -0
- package/dist/tasks/e2e-summary.js.map +1 -0
- package/dist/tasks/e2e.d.ts +29 -0
- package/dist/tasks/e2e.d.ts.map +1 -1
- package/dist/tasks/e2e.js +130 -36
- package/dist/tasks/e2e.js.map +1 -1
- package/dist/tasks/e2e.test.d.ts +2 -0
- package/dist/tasks/e2e.test.d.ts.map +1 -0
- package/dist/tasks/e2e.test.js +108 -0
- package/dist/tasks/e2e.test.js.map +1 -0
- 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/next/components/InAppWalletSetup.d.ts +11 -0
- package/dist/wallet/next/components/InAppWalletSetup.d.ts.map +1 -0
- package/dist/wallet/next/components/InAppWalletSetup.js +55 -0
- package/dist/wallet/next/components/InAppWalletSetup.js.map +1 -0
- package/dist/wallet/next/components/InAppWalletUnlock.d.ts +9 -0
- package/dist/wallet/next/components/InAppWalletUnlock.d.ts.map +1 -0
- package/dist/wallet/next/components/InAppWalletUnlock.js +29 -0
- package/dist/wallet/next/components/InAppWalletUnlock.js.map +1 -0
- package/dist/wallet/next/components.test.d.ts +2 -0
- package/dist/wallet/next/components.test.d.ts.map +1 -0
- package/dist/wallet/next/components.test.js +122 -0
- package/dist/wallet/next/components.test.js.map +1 -0
- package/dist/wallet/next/i18n.d.ts +63 -0
- package/dist/wallet/next/i18n.d.ts.map +1 -0
- package/dist/wallet/next/i18n.js +64 -0
- package/dist/wallet/next/i18n.js.map +1 -0
- package/dist/wallet/next/index.d.ts +7 -0
- package/dist/wallet/next/index.d.ts.map +1 -0
- package/dist/wallet/next/index.js +12 -0
- package/dist/wallet/next/index.js.map +1 -0
- package/dist/wallet/next/namespaces.d.ts +17 -0
- package/dist/wallet/next/namespaces.d.ts.map +1 -0
- package/dist/wallet/next/namespaces.js +17 -0
- package/dist/wallet/next/namespaces.js.map +1 -0
- package/dist/wallet/next/useInAppWallet.d.ts +55 -0
- package/dist/wallet/next/useInAppWallet.d.ts.map +1 -0
- package/dist/wallet/next/useInAppWallet.js +261 -0
- package/dist/wallet/next/useInAppWallet.js.map +1 -0
- package/dist/wallet/next/useInAppWallet.test.d.ts +2 -0
- package/dist/wallet/next/useInAppWallet.test.d.ts.map +1 -0
- package/dist/wallet/next/useInAppWallet.test.js +261 -0
- package/dist/wallet/next/useInAppWallet.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 +18 -4
- package/src/e2e/README.md +67 -0
- package/src/wallet/README.md +119 -0
|
@@ -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":"AAAA;;;;;;;;;GASG;AACH,OAAO,EAAE,UAAU,EAAE,QAAQ,EAAE,IAAI,EAAE,MAAM,aAAa,CAAA;AACxD,OAAO,EAAE,OAAO,EAAE,eAAe,EAAE,MAAM,kBAAkB,CAAA;AAC3D,OAAO,EAAE,iBAAiB,EAAE,uBAAuB,EAAE,aAAa,EAAE,MAAM,gBAAgB,CAAA;AAE1F,MAAM,GAAG,GAAG,SAAS,CAAA;AACrB,MAAM,QAAQ,GAAG,EAAE,CAAA;AACnB,MAAM,CAAC,MAAM,cAAc,GAAG,EAAE,CAAA;AAChC,MAAM,CAAC,MAAM,eAAe,GAAG,EAAE,CAAA;AAcjC,KAAK,UAAU,MAAM;IACnB,MAAM,CAAC,GAAI,UAAkC,CAAC,MAAM,EAAE,MAAM,CAAA;IAC5D,IAAI,CAAC,CAAC;QAAE,MAAM,IAAI,KAAK,CAAC,6CAA6C,CAAC,CAAA;IACtE,OAAO,CAAC,CAAA;AACV,CAAC;AAED,SAAS,WAAW,CAAC,MAAc;IACjC,MAAM,CAAC,GAAI,UAAkC,CAAC,MAAM,CAAA;IACpD,IAAI,CAAC,CAAC,EAAE,eAAe;QAAE,MAAM,IAAI,KAAK,CAAC,yCAAyC,CAAC,CAAA;IACnF,MAAM,KAAK,GAAG,IAAI,UAAU,CAAC,MAAM,CAAC,CAAA;IACpC,CAAC,CAAC,eAAe,CAAC,KAAK,CAAC,CAAA;IACxB,OAAO,KAAK,CAAA;AACd,CAAC;AAED,MAAM,CAAC,KAAK,UAAU,mBAAmB;IACvC,MAAM,KAAK,GAAG,MAAM,OAAO,CAA8B,eAAe,EAAE,UAAU,EAAE,CAAC,KAAK,EAAE,EAAE,CAC9F,KAAK,CAAC,GAAG,CAAC,GAAG,CAAC,CACf,CAAA;IACD,OAAO,KAAK,IAAI,IAAI,CAAA;AACtB,CAAC;AAED,MAAM,CAAC,KAAK,UAAU,oBAAoB,CAAC,MAAuB;IAChE,MAAM,OAAO,CAAc,eAAe,EAAE,WAAW,EAAE,CAAC,KAAK,EAAE,EAAE,CAAC,KAAK,CAAC,GAAG,CAAC,MAAM,EAAE,GAAG,CAAC,CAAC,CAAA;AAC7F,CAAC;AAED,MAAM,CAAC,KAAK,UAAU,qBAAqB;IACzC,MAAM,OAAO,CAAY,eAAe,EAAE,WAAW,EAAE,CAAC,KAAK,EAAE,EAAE,CAAC,KAAK,CAAC,MAAM,CAAC,GAAG,CAAC,CAAC,CAAA;AACtF,CAAC;AAED;;;;GAIG;AACH,MAAM,CAAC,KAAK,UAAU,yBAAyB;IAC7C,MAAM,MAAM,GAAG,MAAM,mBAAmB,EAAE,CAAC,KAAK,CAAC,GAAG,EAAE,CAAC,IAAI,CAAC,CAAA;IAC5D,IAAI,MAAM,EAAE,YAAY;QAAE,MAAM,uBAAuB,CAAC,MAAM,CAAC,YAAY,CAAC,CAAA;IAC5E,MAAM,qBAAqB,EAAE,CAAC,KAAK,CAAC,GAAG,EAAE,CAAC,SAAS,CAAC,CAAA;AACtD,CAAC;AAED,mFAAmF;AACnF,MAAM,CAAC,KAAK,UAAU,iBAAiB,CACrC,SAAqB,EACrB,OAAmB,EACnB,UAAyB,EACzB,EAAE,OAAO,EAAE,YAAY,EAA6C;IAEpE,MAAM,QAAQ,GAAG,WAAW,CAAC,eAAe,CAAC,CAAA;IAC7C,MAAM,EAAE,GAAG,WAAW,CAAC,QAAQ,CAAC,CAAA;IAChC,MAAM,GAAG,GAAG,MAAM,iBAAiB,CAAC,SAAS,EAAE,QAAQ,CAAC,CAAA;IACxD,MAAM,IAAI,GAAG,MAAM,CAAC,MAAM,MAAM,EAAE,CAAC,CAAC,OAAO,CACzC,EAAE,IAAI,EAAE,SAAS,EAAE,EAAE,EAAE,aAAa,CAAC,EAAE,CAAC,EAAE,EAC1C,GAAG,EACH,aAAa,CAAC,IAAI,WAAW,EAAE,CAAC,MAAM,CAAC,UAAU,CAAC,CAAC,CACpD,CAAA;IACD,OAAO;QACL,CAAC,EAAE,CAAC;QACJ,OAAO;QACP,YAAY;QACZ,OAAO,EAAE,QAAQ,CAAC,OAAO,CAAC;QAC1B,QAAQ,EAAE,QAAQ,CAAC,QAAQ,CAAC;QAC5B,EAAE,EAAE,QAAQ,CAAC,EAAE,CAAC;QAChB,IAAI,EAAE,QAAQ,CAAC,IAAI,UAAU,CAAC,IAAI,CAAC,CAAC;QACpC,SAAS,EAAE,IAAI,CAAC,GAAG,EAAE;KACtB,CAAA;AACH,CAAC;AAED,oFAAoF;AACpF,MAAM,CAAC,KAAK,UAAU,mBAAmB,CACvC,SAAqB,EACrB,MAAuB;IAEvB,MAAM,GAAG,GAAG,MAAM,iBAAiB,CAAC,SAAS,EAAE,UAAU,CAAC,MAAM,CAAC,QAAQ,CAAC,CAAC,CAAA;IAC3E,IAAI,CAAC;QACH,MAAM,KAAK,GAAG,MAAM,CAAC,MAAM,MAAM,EAAE,CAAC,CAAC,OAAO,CAC1C,EAAE,IAAI,EAAE,SAAS,EAAE,EAAE,EAAE,aAAa,CAAC,UAAU,CAAC,MAAM,CAAC,EAAE,CAAC,CAAC,EAAE,EAC7D,GAAG,EACH,aAAa,CAAC,UAAU,CAAC,MAAM,CAAC,IAAI,CAAC,CAAC,CACvC,CAAA;QACD,OAAO,IAAI,WAAW,EAAE,CAAC,MAAM,CAAC,KAAK,CAAkB,CAAA;IACzD,CAAC;IAAC,MAAM,CAAC;QACP,MAAM,IAAI,KAAK,CAAC,aAAa,CAAC,CAAA;IAChC,CAAC;AACH,CAAC;AAED,MAAM,UAAU,aAAa,CAAC,MAAqC;IACjE,IAAI,CAAC,MAAM,CAAC,CAAA;AACd,CAAC;AAED,0EAA0E;AAC1E,MAAM,UAAU,UAAU;IACxB,OAAO,WAAW,CAAC,cAAc,CAAC,CAAA;AACpC,CAAC"}
|
|
@@ -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":"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"}
|
|
@@ -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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"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"crypto.test.d.ts","sourceRoot":"","sources":["../../src/wallet/crypto.test.ts"],"names":[],"mappings":""}
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import { describe, expect, it } from 'vitest';
|
|
2
|
+
import { KDF_ITERATIONS, decryptSecret, encryptSecret, fromBase64, toBase64 } from './crypto.js';
|
|
3
|
+
describe('crypto', () => {
|
|
4
|
+
it('round-trips a secret with the password', async () => {
|
|
5
|
+
const secret = await encryptSecret('0x' + 'ab'.repeat(32), '123456');
|
|
6
|
+
expect(secret.kdf.name).toBe('PBKDF2');
|
|
7
|
+
expect(secret.kdf.hash).toBe('SHA-256');
|
|
8
|
+
expect(secret.kdf.iterations).toBe(KDF_ITERATIONS);
|
|
9
|
+
expect(secret.cipher.name).toBe('AES-GCM');
|
|
10
|
+
expect(secret.kdf.salt).not.toBe('');
|
|
11
|
+
expect(secret.cipher.iv).not.toBe('');
|
|
12
|
+
expect(secret.cipher.data).not.toContain('ab'.repeat(32));
|
|
13
|
+
await expect(decryptSecret(secret, '123456')).resolves.toBe('0x' + 'ab'.repeat(32));
|
|
14
|
+
});
|
|
15
|
+
it('uses 600,000 PBKDF2 iterations', () => {
|
|
16
|
+
expect(KDF_ITERATIONS).toBe(600_000);
|
|
17
|
+
});
|
|
18
|
+
it('uses a fresh salt and IV per encryption', async () => {
|
|
19
|
+
const first = await encryptSecret('secret', '123456');
|
|
20
|
+
const second = await encryptSecret('secret', '123456');
|
|
21
|
+
expect(first.kdf.salt).not.toBe(second.kdf.salt);
|
|
22
|
+
expect(first.cipher.iv).not.toBe(second.cipher.iv);
|
|
23
|
+
expect(first.cipher.data).not.toBe(second.cipher.data);
|
|
24
|
+
});
|
|
25
|
+
it('rejects a wrong password', async () => {
|
|
26
|
+
const secret = await encryptSecret('secret', '123456');
|
|
27
|
+
await expect(decryptSecret(secret, '654321')).rejects.toThrow(/Wrong password/i);
|
|
28
|
+
});
|
|
29
|
+
it('round-trips base64 helpers', () => {
|
|
30
|
+
const bytes = new Uint8Array([0, 1, 2, 250, 255]);
|
|
31
|
+
expect(Array.from(fromBase64(toBase64(bytes)))).toEqual([0, 1, 2, 250, 255]);
|
|
32
|
+
});
|
|
33
|
+
});
|
|
34
|
+
//# sourceMappingURL=crypto.test.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"crypto.test.js","sourceRoot":"","sources":["../../src/wallet/crypto.test.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Destinations this wallet has already sent funds to (R-#253).
|
|
3
|
+
*
|
|
4
|
+
* A **new** destination always requires the device gesture, even inside the grace
|
|
5
|
+
* window of `USER_VERIFICATION_GRACE_MS`: that stops an automated or hostile flow
|
|
6
|
+
* from draining the wallet to an address the user never used. Repeated transfers to
|
|
7
|
+
* a known address stay silent within the window.
|
|
8
|
+
*
|
|
9
|
+
* Stored per wallet address in `localStorage` (not a secret; only addresses), with
|
|
10
|
+
* an in-memory fallback when `localStorage` is unavailable (private mode, Node).
|
|
11
|
+
*/
|
|
12
|
+
/** Overrides the storage-key prefix (a consumer keeps its own namespace). */
|
|
13
|
+
export declare function setDestinationsPrefix(value: string): void;
|
|
14
|
+
/** Whether the wallet has already sent funds to `destination`. */
|
|
15
|
+
export declare function isKnownDestination(wallet: string, destination: string): boolean;
|
|
16
|
+
/** Records `destination` for `wallet` after a successful transfer. */
|
|
17
|
+
export declare function rememberDestination(wallet: string, destination: string): void;
|
|
18
|
+
/** Forgets every destination (tests, and when the wallet is deleted). */
|
|
19
|
+
export declare function clearDestinations(wallet?: string): void;
|
|
20
|
+
//# sourceMappingURL=destinations.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"destinations.d.ts","sourceRoot":"","sources":["../../src/wallet/destinations.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;GAUG;AAIH,6EAA6E;AAC7E,wBAAgB,qBAAqB,CAAC,KAAK,EAAE,MAAM,GAAG,IAAI,CAEzD;AA+BD,kEAAkE;AAClE,wBAAgB,kBAAkB,CAAC,MAAM,EAAE,MAAM,EAAE,WAAW,EAAE,MAAM,GAAG,OAAO,CAG/E;AAED,sEAAsE;AACtE,wBAAgB,mBAAmB,CAAC,MAAM,EAAE,MAAM,EAAE,WAAW,EAAE,MAAM,GAAG,IAAI,CAc7E;AAED,yEAAyE;AACzE,wBAAgB,iBAAiB,CAAC,MAAM,CAAC,EAAE,MAAM,GAAG,IAAI,CAiBvD"}
|
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Destinations this wallet has already sent funds to (R-#253).
|
|
3
|
+
*
|
|
4
|
+
* A **new** destination always requires the device gesture, even inside the grace
|
|
5
|
+
* window of `USER_VERIFICATION_GRACE_MS`: that stops an automated or hostile flow
|
|
6
|
+
* from draining the wallet to an address the user never used. Repeated transfers to
|
|
7
|
+
* a known address stay silent within the window.
|
|
8
|
+
*
|
|
9
|
+
* Stored per wallet address in `localStorage` (not a secret; only addresses), with
|
|
10
|
+
* an in-memory fallback when `localStorage` is unavailable (private mode, Node).
|
|
11
|
+
*/
|
|
12
|
+
/** Namespace of the `localStorage` keys (a consumer sets its own with `setDestinationsPrefix`). */
|
|
13
|
+
let prefix = 'pasosdejesus-m-wallet.destinations.';
|
|
14
|
+
/** Overrides the storage-key prefix (a consumer keeps its own namespace). */
|
|
15
|
+
export function setDestinationsPrefix(value) {
|
|
16
|
+
if (value)
|
|
17
|
+
prefix = value;
|
|
18
|
+
}
|
|
19
|
+
/** Cap so the list cannot grow without bound. */
|
|
20
|
+
const MAX_DESTINATIONS = 200;
|
|
21
|
+
const memory = new Map();
|
|
22
|
+
function storage() {
|
|
23
|
+
try {
|
|
24
|
+
return globalThis.localStorage ?? null;
|
|
25
|
+
}
|
|
26
|
+
catch {
|
|
27
|
+
return null;
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
function load(wallet) {
|
|
31
|
+
const key = wallet.toLowerCase();
|
|
32
|
+
const cached = memory.get(key);
|
|
33
|
+
if (cached)
|
|
34
|
+
return cached;
|
|
35
|
+
const set = new Set();
|
|
36
|
+
const raw = storage()?.getItem(prefix + key);
|
|
37
|
+
if (raw) {
|
|
38
|
+
try {
|
|
39
|
+
for (const item of JSON.parse(raw))
|
|
40
|
+
set.add(String(item).toLowerCase());
|
|
41
|
+
}
|
|
42
|
+
catch {
|
|
43
|
+
// Lista corrupta: se empieza de cero (sólo provoca pedir el gesto de nuevo).
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
memory.set(key, set);
|
|
47
|
+
return set;
|
|
48
|
+
}
|
|
49
|
+
/** Whether the wallet has already sent funds to `destination`. */
|
|
50
|
+
export function isKnownDestination(wallet, destination) {
|
|
51
|
+
if (!wallet || !destination)
|
|
52
|
+
return false;
|
|
53
|
+
return load(wallet).has(destination.toLowerCase());
|
|
54
|
+
}
|
|
55
|
+
/** Records `destination` for `wallet` after a successful transfer. */
|
|
56
|
+
export function rememberDestination(wallet, destination) {
|
|
57
|
+
if (!wallet || !destination)
|
|
58
|
+
return;
|
|
59
|
+
const set = load(wallet);
|
|
60
|
+
set.add(destination.toLowerCase());
|
|
61
|
+
while (set.size > MAX_DESTINATIONS) {
|
|
62
|
+
const first = set.values().next().value;
|
|
63
|
+
if (first === undefined)
|
|
64
|
+
break;
|
|
65
|
+
set.delete(first);
|
|
66
|
+
}
|
|
67
|
+
try {
|
|
68
|
+
storage()?.setItem(prefix + wallet.toLowerCase(), JSON.stringify([...set]));
|
|
69
|
+
}
|
|
70
|
+
catch {
|
|
71
|
+
// Sin persistencia: queda sólo en memoria (se pedirá el gesto tras recargar).
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
/** Forgets every destination (tests, and when the wallet is deleted). */
|
|
75
|
+
export function clearDestinations(wallet) {
|
|
76
|
+
if (wallet) {
|
|
77
|
+
memory.delete(wallet.toLowerCase());
|
|
78
|
+
}
|
|
79
|
+
else {
|
|
80
|
+
memory.clear();
|
|
81
|
+
}
|
|
82
|
+
const s = storage();
|
|
83
|
+
if (!s)
|
|
84
|
+
return;
|
|
85
|
+
try {
|
|
86
|
+
for (let i = s.length - 1; i >= 0; i -= 1) {
|
|
87
|
+
const key = s.key(i);
|
|
88
|
+
if (!key?.startsWith(prefix))
|
|
89
|
+
continue;
|
|
90
|
+
if (!wallet || key === prefix + wallet.toLowerCase())
|
|
91
|
+
s.removeItem(key);
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
catch {
|
|
95
|
+
// Nada que limpiar.
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
//# sourceMappingURL=destinations.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"destinations.js","sourceRoot":"","sources":["../../src/wallet/destinations.ts"],"names":[],"mappings":"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"}
|