refactor: centralize encryption/decryption in unified service
This commit is contained in:
parent
ab31901a20
commit
f4b80f1d93
@ -172,9 +172,7 @@ document.addEventListener('DOMContentLoaded', async () => {
|
||||
console.log('🔐 Using security mode for wallet encryption:', currentMode);
|
||||
|
||||
// Générer un wallet temporaire avec état birthday_waiting
|
||||
const { StorageService } = await import('../../services/credentials/storage.service');
|
||||
const { EncryptionService } = await import('../../services/credentials/encryption.service');
|
||||
const storageService = StorageService.getInstance();
|
||||
const { EncryptionService } = await import('../../services/encryption.service');
|
||||
const encryptionService = EncryptionService.getInstance();
|
||||
|
||||
// Générer des clés temporaires pour le wallet
|
||||
@ -201,7 +199,7 @@ document.addEventListener('DOMContentLoaded', async () => {
|
||||
console.log('🔐 PBKDF2 key retrieved for wallet encryption');
|
||||
|
||||
// Chiffrer le wallet avec la clé PBKDF2
|
||||
const encryptedWallet = await encryptionService.encryptWithPassword(
|
||||
const encryptedWallet = await encryptionService.encrypt(
|
||||
JSON.stringify(walletData),
|
||||
pbkdf2Key
|
||||
);
|
||||
@ -274,7 +272,7 @@ document.addEventListener('DOMContentLoaded', async () => {
|
||||
// Le device contient des données sensibles (sp_wallet) qui ne doivent JAMAIS être en clair
|
||||
console.log('🔐 Encrypting device data before storage...');
|
||||
const deviceString = JSON.stringify(device);
|
||||
const encryptedDevice = await encryptionService.encryptWithPassword(deviceString, pbkdf2Key);
|
||||
const encryptedDevice = await encryptionService.encrypt(deviceString, pbkdf2Key);
|
||||
console.log('🔐 Device encrypted successfully');
|
||||
|
||||
await new Promise<void>((resolve, reject) => {
|
||||
@ -408,8 +406,8 @@ document.addEventListener('DOMContentLoaded', async () => {
|
||||
} else {
|
||||
console.log('✅ TEST: PBKDF2 key retrieved for decryption test');
|
||||
|
||||
// Déchiffrer le wallet chiffré (format base64)
|
||||
const decryptedWallet = await encryptionService.decryptWithPasswordBase64(
|
||||
// Déchiffrer le wallet chiffré
|
||||
const decryptedWallet = await encryptionService.decrypt(
|
||||
finalVerification.encrypted_wallet,
|
||||
pbkdf2KeyTest
|
||||
);
|
||||
@ -422,8 +420,8 @@ document.addEventListener('DOMContentLoaded', async () => {
|
||||
created_at: parsedWallet.created_at
|
||||
});
|
||||
|
||||
// Déchiffrer le device chiffré (format base64)
|
||||
const decryptedDevice = await encryptionService.decryptWithPasswordBase64(
|
||||
// Déchiffrer le device chiffré
|
||||
const decryptedDevice = await encryptionService.decrypt(
|
||||
finalVerification.encrypted_device,
|
||||
pbkdf2KeyTest
|
||||
);
|
||||
|
||||
200
src/services/encryption.service.ts
Normal file
200
src/services/encryption.service.ts
Normal file
@ -0,0 +1,200 @@
|
||||
/**
|
||||
* EncryptionService - Centralized encryption/decryption service
|
||||
*
|
||||
* This service is the single source of truth for all encryption operations in the application.
|
||||
* All encryption uses AES-GCM with PBKDF2 key derivation.
|
||||
*/
|
||||
|
||||
import { secureLogger } from './secure-logger';
|
||||
|
||||
export interface EncryptionConfig {
|
||||
iterations?: number;
|
||||
saltLength?: number;
|
||||
ivLength?: number;
|
||||
}
|
||||
|
||||
export class EncryptionService {
|
||||
private static instance: EncryptionService;
|
||||
|
||||
private readonly defaultConfig: Required<EncryptionConfig> = {
|
||||
iterations: 100000,
|
||||
saltLength: 16,
|
||||
ivLength: 12
|
||||
};
|
||||
|
||||
private constructor() {}
|
||||
|
||||
public static getInstance(): EncryptionService {
|
||||
if (!EncryptionService.instance) {
|
||||
EncryptionService.instance = new EncryptionService();
|
||||
}
|
||||
return EncryptionService.instance;
|
||||
}
|
||||
|
||||
/**
|
||||
* Encrypts data using AES-GCM with PBKDF2 key derivation
|
||||
* Returns base64-encoded string containing: salt (16 bytes) + IV (12 bytes) + encrypted data
|
||||
*
|
||||
* @param data - Plain text data to encrypt
|
||||
* @param password - Password/passphrase for encryption
|
||||
* @param config - Optional configuration (iterations, saltLength, ivLength)
|
||||
* @returns Base64-encoded encrypted data (salt + IV + ciphertext)
|
||||
*/
|
||||
async encrypt(data: string, password: string, config: EncryptionConfig = {}): Promise<string> {
|
||||
try {
|
||||
const cfg = { ...this.defaultConfig, ...config };
|
||||
|
||||
// Generate random salt and IV
|
||||
const salt = crypto.getRandomValues(new Uint8Array(cfg.saltLength));
|
||||
const iv = crypto.getRandomValues(new Uint8Array(cfg.ivLength));
|
||||
|
||||
// Derive key using PBKDF2
|
||||
const keyMaterial = await crypto.subtle.importKey(
|
||||
'raw',
|
||||
new TextEncoder().encode(password),
|
||||
'PBKDF2',
|
||||
false,
|
||||
['deriveBits']
|
||||
);
|
||||
|
||||
const derivedKey = await crypto.subtle.deriveBits(
|
||||
{
|
||||
name: 'PBKDF2',
|
||||
salt: salt,
|
||||
iterations: cfg.iterations,
|
||||
hash: 'SHA-256'
|
||||
},
|
||||
keyMaterial,
|
||||
256
|
||||
);
|
||||
|
||||
// Import as AES-GCM key
|
||||
const cryptoKey = await crypto.subtle.importKey(
|
||||
'raw',
|
||||
derivedKey,
|
||||
{ name: 'AES-GCM' },
|
||||
false,
|
||||
['encrypt']
|
||||
);
|
||||
|
||||
// Encrypt
|
||||
const encrypted = await crypto.subtle.encrypt(
|
||||
{ name: 'AES-GCM', iv: iv },
|
||||
cryptoKey,
|
||||
new TextEncoder().encode(data)
|
||||
);
|
||||
|
||||
// Combine salt + IV + encrypted data
|
||||
const combined = new Uint8Array(salt.length + iv.length + encrypted.byteLength);
|
||||
combined.set(salt, 0);
|
||||
combined.set(iv, salt.length);
|
||||
combined.set(new Uint8Array(encrypted), salt.length + iv.length);
|
||||
|
||||
// Return base64-encoded
|
||||
return btoa(String.fromCharCode(...combined));
|
||||
} catch (error) {
|
||||
secureLogger.error('Failed to encrypt data', error as Error, {
|
||||
component: 'EncryptionService',
|
||||
operation: 'encrypt'
|
||||
});
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Decrypts data encrypted by the encrypt() method
|
||||
* Expects base64-encoded string containing: salt (16 bytes) + IV (12 bytes) + encrypted data
|
||||
*
|
||||
* @param encryptedData - Base64-encoded encrypted data
|
||||
* @param password - Password/passphrase used for encryption
|
||||
* @param config - Optional configuration (iterations, saltLength, ivLength)
|
||||
* @returns Decrypted plain text data
|
||||
*/
|
||||
async decrypt(encryptedData: string, password: string, config: EncryptionConfig = {}): Promise<string> {
|
||||
try {
|
||||
const cfg = { ...this.defaultConfig, ...config };
|
||||
|
||||
// Decode base64
|
||||
const encrypted = atob(encryptedData);
|
||||
const combined = new Uint8Array(encrypted.length);
|
||||
for (let i = 0; i < encrypted.length; i++) {
|
||||
combined[i] = encrypted.charCodeAt(i);
|
||||
}
|
||||
|
||||
// Extract salt, IV, and encrypted data
|
||||
const salt = combined.slice(0, cfg.saltLength);
|
||||
const iv = combined.slice(cfg.saltLength, cfg.saltLength + cfg.ivLength);
|
||||
const ciphertext = combined.slice(cfg.saltLength + cfg.ivLength);
|
||||
|
||||
// Derive key using PBKDF2
|
||||
const keyMaterial = await crypto.subtle.importKey(
|
||||
'raw',
|
||||
new TextEncoder().encode(password),
|
||||
'PBKDF2',
|
||||
false,
|
||||
['deriveBits']
|
||||
);
|
||||
|
||||
const derivedKey = await crypto.subtle.deriveBits(
|
||||
{
|
||||
name: 'PBKDF2',
|
||||
salt: salt,
|
||||
iterations: cfg.iterations,
|
||||
hash: 'SHA-256'
|
||||
},
|
||||
keyMaterial,
|
||||
256
|
||||
);
|
||||
|
||||
// Import as AES-GCM key
|
||||
const cryptoKey = await crypto.subtle.importKey(
|
||||
'raw',
|
||||
derivedKey,
|
||||
{ name: 'AES-GCM' },
|
||||
false,
|
||||
['decrypt']
|
||||
);
|
||||
|
||||
// Decrypt
|
||||
const decrypted = await crypto.subtle.decrypt(
|
||||
{ name: 'AES-GCM', iv: iv },
|
||||
cryptoKey,
|
||||
ciphertext
|
||||
);
|
||||
|
||||
return new TextDecoder().decode(decrypted);
|
||||
} catch (error) {
|
||||
secureLogger.error('Failed to decrypt data', error as Error, {
|
||||
component: 'EncryptionService',
|
||||
operation: 'decrypt'
|
||||
});
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate a random key
|
||||
*
|
||||
* @param length - Key length in bytes (default: 32)
|
||||
* @returns Hex-encoded random key
|
||||
*/
|
||||
generateRandomKey(length: number = 32): string {
|
||||
const keyBytes = crypto.getRandomValues(new Uint8Array(length));
|
||||
return Array.from(keyBytes).map(b => b.toString(16).padStart(2, '0')).join('');
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate random spend and scan keys
|
||||
*
|
||||
* @returns Object with spendKey and scanKey (hex-encoded)
|
||||
*/
|
||||
generateRandomKeys(): { spendKey: string; scanKey: string } {
|
||||
const spendKey = crypto.getRandomValues(new Uint8Array(32));
|
||||
const scanKey = crypto.getRandomValues(new Uint8Array(32));
|
||||
|
||||
return {
|
||||
spendKey: Array.from(spendKey).map(b => b.toString(16).padStart(2, '0')).join(''),
|
||||
scanKey: Array.from(scanKey).map(b => b.toString(16).padStart(2, '0')).join('')
|
||||
};
|
||||
}
|
||||
}
|
||||
Loading…
x
Reference in New Issue
Block a user