**Motivations:** - Implement BIP39 mnemonic import for identity creation - Add password-based key protection for enhanced security - Improve pairing workflow with QR code and URL display - Migrate hash cache from LocalStorage to IndexedDB for better scalability - Update signet-dashboard and mempool components **Root causes:** - N/A (feature implementations) **Correctifs:** - N/A (no bug fixes in this commit) **Evolutions:** - BIP39 mnemonic import: Support for 12/24 word English mnemonics with BIP32 derivation path m/44'/0'/0'/0/0 - Key protection: Password-based encryption of private keys at rest with unlock/lock functionality - Pairing workflow: QR code and URL display for device pairing, form-based word exchange between devices - IndexedDB migration: Hash cache moved from LocalStorage to IndexedDB to avoid size limitations - Global action bar: URL parameter support for navigation - Pairing connection: Enhanced pairing status management **Pages affectées:** - userwallet/src/utils/identity.ts - userwallet/src/utils/keyProtection.ts - userwallet/src/utils/sessionUnlockedKey.ts - userwallet/src/utils/indexedDbStorage.ts - userwallet/src/utils/cache.ts - userwallet/src/utils/pairing.ts - userwallet/src/components/UnlockScreen.tsx - userwallet/src/components/PairingDisplayScreen.tsx - userwallet/src/components/PairingSetupBlock.tsx - userwallet/src/components/GlobalActionBar.tsx - userwallet/src/components/HomeScreen.tsx - userwallet/src/components/ImportIdentityScreen.tsx - userwallet/src/components/DataExportImportScreen.tsx - userwallet/src/hooks/useIdentity.ts - userwallet/src/hooks/usePairingConnected.ts - userwallet/src/services/syncService.ts - userwallet/src/services/pairingConfirm.ts - userwallet/src/App.tsx - userwallet/package.json - userwallet/docs/specs.md - userwallet/docs/storage.md - userwallet/docs/synthese.md - signet-dashboard/public/*.html - signet-dashboard/public/app.js - signet-dashboard/public/styles.css - mempool (submodule updates) - hash_list.txt, hash_list_cache.txt, utxo_list.txt, utxo_list_cache.txt, fees_list.txt - features/*.md (documentation files)
102 lines
2.4 KiB
TypeScript
102 lines
2.4 KiB
TypeScript
const PBKDF2_ITERATIONS = 100_000;
|
|
const SALT_LENGTH = 16;
|
|
const IV_LENGTH = 12;
|
|
const KEY_LENGTH = 256;
|
|
|
|
export interface EncryptedPayload {
|
|
ciphertext: string;
|
|
iv: string;
|
|
salt: string;
|
|
}
|
|
|
|
/**
|
|
* Derive AES-GCM key from password using PBKDF2-HMAC-SHA256.
|
|
*/
|
|
async function deriveKey(
|
|
password: string,
|
|
salt: Uint8Array,
|
|
): Promise<CryptoKey> {
|
|
const enc = new TextEncoder();
|
|
const keyMaterial = await crypto.subtle.importKey(
|
|
'raw',
|
|
enc.encode(password),
|
|
'PBKDF2',
|
|
false,
|
|
['deriveBits', 'deriveKey'],
|
|
);
|
|
return crypto.subtle.deriveKey(
|
|
{
|
|
name: 'PBKDF2',
|
|
salt: salt as BufferSource,
|
|
iterations: PBKDF2_ITERATIONS,
|
|
hash: 'SHA-256',
|
|
},
|
|
keyMaterial,
|
|
{ name: 'AES-GCM', length: KEY_LENGTH },
|
|
false,
|
|
['encrypt', 'decrypt'],
|
|
);
|
|
}
|
|
|
|
/**
|
|
* Encrypt private key (hex string) with password.
|
|
* Returns base64-encoded ciphertext, iv, and salt.
|
|
*/
|
|
export async function encryptPrivateKey(
|
|
privateKeyHex: string,
|
|
password: string,
|
|
): Promise<EncryptedPayload> {
|
|
const salt = crypto.getRandomValues(new Uint8Array(SALT_LENGTH));
|
|
const iv = crypto.getRandomValues(new Uint8Array(IV_LENGTH));
|
|
const key = await deriveKey(password, salt);
|
|
const enc = new TextEncoder();
|
|
const plaintext = enc.encode(privateKeyHex);
|
|
const ciphertext = await crypto.subtle.encrypt(
|
|
{ name: 'AES-GCM', iv },
|
|
key,
|
|
plaintext,
|
|
);
|
|
return {
|
|
ciphertext: b64Encode(new Uint8Array(ciphertext)),
|
|
iv: b64Encode(iv),
|
|
salt: b64Encode(salt),
|
|
};
|
|
}
|
|
|
|
/**
|
|
* Decrypt private key from payload using password.
|
|
* Returns hex string or throws.
|
|
*/
|
|
export async function decryptPrivateKey(
|
|
payload: EncryptedPayload,
|
|
password: string,
|
|
): Promise<string> {
|
|
const salt = b64Decode(payload.salt);
|
|
const iv = b64Decode(payload.iv);
|
|
const ciphertext = b64Decode(payload.ciphertext);
|
|
const key = await deriveKey(password, salt);
|
|
const decrypted = await crypto.subtle.decrypt(
|
|
{ name: 'AES-GCM', iv: iv as BufferSource },
|
|
key,
|
|
ciphertext as BufferSource,
|
|
);
|
|
return new TextDecoder().decode(decrypted);
|
|
}
|
|
|
|
function b64Encode(bytes: Uint8Array): string {
|
|
let binary = '';
|
|
for (let i = 0; i < bytes.length; i++) {
|
|
binary += String.fromCharCode(bytes[i] ?? 0);
|
|
}
|
|
return btoa(binary);
|
|
}
|
|
|
|
function b64Decode(str: string): Uint8Array {
|
|
const bin = atob(str);
|
|
const out = new Uint8Array(bin.length);
|
|
for (let i = 0; i < bin.length; i++) {
|
|
out[i] = bin.charCodeAt(i);
|
|
}
|
|
return out;
|
|
}
|