mirror of
https://github.com/tompro/sattle.git
synced 2026-10-26 01:49:58 +00:00
fix(wallet): activate complete wallet material
This commit is contained in:
@@ -1,9 +1,19 @@
|
||||
import { bytesToHex } from '@noble/hashes/utils.js';
|
||||
import { createPinia, setActivePinia } from 'pinia';
|
||||
import { beforeEach, vi } from 'vitest';
|
||||
|
||||
import { encryptSecretParts, linkingPubKeyHex } from '@/lnurlcash/keys';
|
||||
import {
|
||||
deriveWalletMaterial,
|
||||
encryptSecretParts,
|
||||
linkingPubKeyHex,
|
||||
serializeWalletMaterial,
|
||||
walletMaterialHash,
|
||||
} from '@/lnurlcash/keys';
|
||||
import type { WalletMaterialV2 } from '@/lnurlcash/keys';
|
||||
import type * as NwcExports from '@/lnurlcash/nwc';
|
||||
import type * as NostrBackupExports from '@/lnurlcash/nostrBackup';
|
||||
import type * as PasskeysExports from '@/lnurlcash/passkeys';
|
||||
import { WALLET_MATERIAL_STORAGE_KEY } from '@/lnurlcash/storage/walletOwnerEvents';
|
||||
import { stubLocalStorage } from '@/lnurlcash/test-utils';
|
||||
import { lifecycleMocks } from './wallet.lifecycle.testMocks';
|
||||
|
||||
@@ -13,7 +23,16 @@ vi.mock('@/capabilities/biometricUnlock', async () => {
|
||||
const { lifecycleMocks } = await import('./wallet.lifecycle.testMocks');
|
||||
return {
|
||||
disableBiometricUnlock: lifecycleMocks.disableBiometricUnlock,
|
||||
unlockWithBiometrics: vi.fn(),
|
||||
unlockWalletMaterialWithBiometrics: lifecycleMocks.unlockWalletMaterialWithBiometrics,
|
||||
};
|
||||
});
|
||||
|
||||
vi.mock('@/lnurlcash/passkeys', async (importOriginal) => {
|
||||
const { lifecycleMocks } = await import('./wallet.lifecycle.testMocks');
|
||||
const actual = await importOriginal<typeof PasskeysExports>();
|
||||
return {
|
||||
...actual,
|
||||
unlockWalletMaterialWithPasskey: lifecycleMocks.unlockWalletMaterialWithPasskey,
|
||||
};
|
||||
});
|
||||
|
||||
@@ -31,19 +50,71 @@ vi.mock('@/lnurlcash/nostrBackup', async (importOriginal) => {
|
||||
|
||||
export const LINKING_KEY = new Uint8Array(32).fill(7);
|
||||
export const OTHER_LINKING_KEY = new Uint8Array(32).fill(9);
|
||||
export const MATERIAL: WalletMaterialV2 = {
|
||||
...deriveWalletMaterial(
|
||||
'abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon about',
|
||||
),
|
||||
linkingKeyHex: bytesToHex(LINKING_KEY),
|
||||
};
|
||||
export const OTHER_MATERIAL: WalletMaterialV2 = {
|
||||
...MATERIAL,
|
||||
linkingKeyHex: bytesToHex(OTHER_LINKING_KEY),
|
||||
};
|
||||
export const OWNER_ID = linkingPubKeyHex(LINKING_KEY);
|
||||
export const OTHER_OWNER_ID = linkingPubKeyHex(OTHER_LINKING_KEY);
|
||||
export const PASSWORD = 'correct horse battery staple';
|
||||
export const MINT_KEY = '02' + 'aa'.repeat(32);
|
||||
|
||||
export const encryptedLinkingKeyRecord = async () => {
|
||||
const parts = await encryptSecretParts(
|
||||
Array.from(LINKING_KEY, (byte) => byte.toString(16).padStart(2, '0')).join(''),
|
||||
PASSWORD,
|
||||
export const WALLET_MATERIAL_KEY = WALLET_MATERIAL_STORAGE_KEY;
|
||||
export const LEGACY_LINKING_KEY = 'sattle_linking_key';
|
||||
// the frozen pre-v2 bearer namespace: current storage never writes it, so
|
||||
// its presence means an alpha install
|
||||
export const LEGACY_BEARERS_KEY = 'sattle_bearers';
|
||||
|
||||
export const encryptedWalletMaterialRecord = async (
|
||||
material: WalletMaterialV2 = MATERIAL,
|
||||
password: string = PASSWORD,
|
||||
) => {
|
||||
const parts = await encryptSecretParts(serializeWalletMaterial(material), password);
|
||||
return { enc: true as const, ...parts, materialHash: walletMaterialHash(material) };
|
||||
};
|
||||
|
||||
// a v2 install whose owner marker is already proven (the steady state after
|
||||
// any successful unlock)
|
||||
export const installEncryptedWalletMaterial = async (): Promise<void> => {
|
||||
const record = await encryptedWalletMaterialRecord();
|
||||
localStorage.setItem(
|
||||
WALLET_MATERIAL_KEY,
|
||||
JSON.stringify({ ...record, ownerId: OWNER_ID, version: 2 }),
|
||||
);
|
||||
};
|
||||
|
||||
// a v2 install whose owner marker was stripped on restore - usable but not
|
||||
// yet proven, the state a backup/relay restore leaves behind
|
||||
export const installOwnerlessEncryptedWalletMaterial = async (): Promise<void> => {
|
||||
localStorage.setItem(
|
||||
WALLET_MATERIAL_KEY,
|
||||
JSON.stringify(await encryptedWalletMaterialRecord()),
|
||||
);
|
||||
};
|
||||
|
||||
// the pre-v2 record shape a backup file can still carry - the lifecycle must
|
||||
// never activate it, only clear it
|
||||
export const encryptedLinkingKeyRecord = async () => {
|
||||
const parts = await encryptSecretParts(bytesToHex(LINKING_KEY), PASSWORD);
|
||||
return { enc: true as const, ...parts };
|
||||
};
|
||||
|
||||
// unsupported alpha state: the linking-key-only record and the legacy bearer
|
||||
// namespace, never written by the v2 lifecycle
|
||||
export const installLegacyWalletState = (): void => {
|
||||
localStorage.setItem(
|
||||
LEGACY_LINKING_KEY,
|
||||
JSON.stringify({ enc: false, value: bytesToHex(LINKING_KEY), ownerId: OWNER_ID, version: 1 }),
|
||||
);
|
||||
localStorage.setItem(LEGACY_BEARERS_KEY, JSON.stringify([]));
|
||||
};
|
||||
|
||||
type Deferred = {
|
||||
readonly promise: Promise<void>;
|
||||
readonly resolve: () => void;
|
||||
@@ -60,6 +131,38 @@ export const deferred = (): Deferred => {
|
||||
};
|
||||
};
|
||||
|
||||
type LockRequest = {
|
||||
readonly callback: () => unknown;
|
||||
readonly resolve: (value: unknown) => void;
|
||||
readonly reject: (error: unknown) => void;
|
||||
};
|
||||
|
||||
// a navigator.locks stand-in whose requests queue up until the test releases
|
||||
// them one at a time - the deferral hook for mid-activation interruptions.
|
||||
// A callback failure rejects the lock promise (the real LockManager settles
|
||||
// the caller's promise with the callback outcome)
|
||||
export const deferredLocks = () => {
|
||||
const requests: LockRequest[] = [];
|
||||
return {
|
||||
requests,
|
||||
locks: {
|
||||
request: (_name: string, callback: () => unknown): Promise<unknown> =>
|
||||
new Promise((resolve, reject) => {
|
||||
requests.push({ callback, resolve, reject });
|
||||
}),
|
||||
},
|
||||
releaseNext: async (): Promise<void> => {
|
||||
const request = requests.shift();
|
||||
if (!request) throw new Error('Expected a queued lock request.');
|
||||
try {
|
||||
request.resolve(await request.callback());
|
||||
} catch (error) {
|
||||
request.reject(error);
|
||||
}
|
||||
},
|
||||
};
|
||||
};
|
||||
|
||||
export const installLegacyOwnerlessResidue = (): void => {
|
||||
localStorage.setItem(
|
||||
'sattle_passkey_slots',
|
||||
@@ -68,7 +171,8 @@ export const installLegacyOwnerlessResidue = (): void => {
|
||||
credentialId: '11'.repeat(16),
|
||||
hkdfSalt: '22'.repeat(16),
|
||||
iv: '33'.repeat(12),
|
||||
wrappedKey: '44'.repeat(48),
|
||||
materialHash: walletMaterialHash(MATERIAL),
|
||||
wrappedMaterial: '44'.repeat(48),
|
||||
createdAt: 1,
|
||||
},
|
||||
]),
|
||||
@@ -99,16 +203,18 @@ export const installLegacyOwnerlessResidue = (): void => {
|
||||
);
|
||||
};
|
||||
|
||||
export const installLegacyEncryptedWallet = async (): Promise<void> => {
|
||||
localStorage.setItem('sattle_linking_key', JSON.stringify(await encryptedLinkingKeyRecord()));
|
||||
};
|
||||
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks();
|
||||
vi.unstubAllGlobals();
|
||||
stubLocalStorage();
|
||||
setActivePinia(createPinia());
|
||||
lifecycleMocks.disableBiometricUnlock.mockResolvedValue();
|
||||
lifecycleMocks.unlockWalletMaterialWithBiometrics.mockRejectedValue(
|
||||
new Error('Biometric unlock is not set up on this device.'),
|
||||
);
|
||||
lifecycleMocks.unlockWalletMaterialWithPasskey.mockRejectedValue(
|
||||
new Error('No passkeys registered on this device.'),
|
||||
);
|
||||
lifecycleMocks.restoreFromNostr.mockResolvedValue({
|
||||
added: 0,
|
||||
skipped: 0,
|
||||
|
||||
Reference in New Issue
Block a user