refactor: centralize encryption/decryption in unified service

This commit is contained in:
NicolasCantu 2025-10-26 02:47:20 +01:00
parent ab31901a20
commit f4b80f1d93
2 changed files with 207 additions and 9 deletions

View File

@ -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
);

View 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('')
};
}
}