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);
|
console.log('🔐 Using security mode for wallet encryption:', currentMode);
|
||||||
|
|
||||||
// Générer un wallet temporaire avec état birthday_waiting
|
// Générer un wallet temporaire avec état birthday_waiting
|
||||||
const { StorageService } = await import('../../services/credentials/storage.service');
|
const { EncryptionService } = await import('../../services/encryption.service');
|
||||||
const { EncryptionService } = await import('../../services/credentials/encryption.service');
|
|
||||||
const storageService = StorageService.getInstance();
|
|
||||||
const encryptionService = EncryptionService.getInstance();
|
const encryptionService = EncryptionService.getInstance();
|
||||||
|
|
||||||
// Générer des clés temporaires pour le wallet
|
// 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');
|
console.log('🔐 PBKDF2 key retrieved for wallet encryption');
|
||||||
|
|
||||||
// Chiffrer le wallet avec la clé PBKDF2
|
// Chiffrer le wallet avec la clé PBKDF2
|
||||||
const encryptedWallet = await encryptionService.encryptWithPassword(
|
const encryptedWallet = await encryptionService.encrypt(
|
||||||
JSON.stringify(walletData),
|
JSON.stringify(walletData),
|
||||||
pbkdf2Key
|
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
|
// Le device contient des données sensibles (sp_wallet) qui ne doivent JAMAIS être en clair
|
||||||
console.log('🔐 Encrypting device data before storage...');
|
console.log('🔐 Encrypting device data before storage...');
|
||||||
const deviceString = JSON.stringify(device);
|
const deviceString = JSON.stringify(device);
|
||||||
const encryptedDevice = await encryptionService.encryptWithPassword(deviceString, pbkdf2Key);
|
const encryptedDevice = await encryptionService.encrypt(deviceString, pbkdf2Key);
|
||||||
console.log('🔐 Device encrypted successfully');
|
console.log('🔐 Device encrypted successfully');
|
||||||
|
|
||||||
await new Promise<void>((resolve, reject) => {
|
await new Promise<void>((resolve, reject) => {
|
||||||
@ -408,8 +406,8 @@ document.addEventListener('DOMContentLoaded', async () => {
|
|||||||
} else {
|
} else {
|
||||||
console.log('✅ TEST: PBKDF2 key retrieved for decryption test');
|
console.log('✅ TEST: PBKDF2 key retrieved for decryption test');
|
||||||
|
|
||||||
// Déchiffrer le wallet chiffré (format base64)
|
// Déchiffrer le wallet chiffré
|
||||||
const decryptedWallet = await encryptionService.decryptWithPasswordBase64(
|
const decryptedWallet = await encryptionService.decrypt(
|
||||||
finalVerification.encrypted_wallet,
|
finalVerification.encrypted_wallet,
|
||||||
pbkdf2KeyTest
|
pbkdf2KeyTest
|
||||||
);
|
);
|
||||||
@ -422,8 +420,8 @@ document.addEventListener('DOMContentLoaded', async () => {
|
|||||||
created_at: parsedWallet.created_at
|
created_at: parsedWallet.created_at
|
||||||
});
|
});
|
||||||
|
|
||||||
// Déchiffrer le device chiffré (format base64)
|
// Déchiffrer le device chiffré
|
||||||
const decryptedDevice = await encryptionService.decryptWithPasswordBase64(
|
const decryptedDevice = await encryptionService.decrypt(
|
||||||
finalVerification.encrypted_device,
|
finalVerification.encrypted_device,
|
||||||
pbkdf2KeyTest
|
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