**Motivations :** - Centralize database configuration to prevent version inconsistencies - Fix service worker blocking during wallet setup - Ensure all database stores are created at initialization **Modifications :** - Created database-config.ts with centralized DATABASE_CONFIG (name, version, stores) - Updated storage.service.ts to use DATABASE_CONFIG and create all stores on upgrade - Updated security-setup.ts to initialize database with complete configuration - Updated wallet-setup.ts to call SDK directly and bypass service worker blocking - Updated database.service.ts, webauthn.service.ts, and database.worker.js to use DATABASE_CONFIG - Removed service worker dependency for wallet setup page **Pages affected :** - security-setup.html: Initializes database with all stores on page load - wallet-setup.html: Saves wallet directly to IndexedDB without service worker dependency
222 lines
6.4 KiB
TypeScript
222 lines
6.4 KiB
TypeScript
/**
|
|
* SecureKeyManager - Gestion sécurisée des clés privées
|
|
* Chiffre les clés privées avant stockage et les déchiffre à la demande
|
|
*/
|
|
export class SecureKeyManager {
|
|
private keyStore: CryptoKey | null = null;
|
|
private salt: Uint8Array;
|
|
private isInitialized: boolean = false;
|
|
|
|
constructor() {
|
|
this.salt = crypto.getRandomValues(new Uint8Array(16));
|
|
}
|
|
|
|
/**
|
|
* Initialise le gestionnaire de clés avec un mot de passe
|
|
*/
|
|
async initialize(password: string): Promise<void> {
|
|
try {
|
|
const derivedKey = await this.deriveKey(password);
|
|
this.keyStore = derivedKey;
|
|
this.isInitialized = true;
|
|
console.log('🔐 SecureKeyManager initialized');
|
|
} catch (error) {
|
|
console.error('❌ Failed to initialize SecureKeyManager:', error);
|
|
throw new Error('Failed to initialize secure key manager');
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Stocke une clé privée de manière chiffrée
|
|
*/
|
|
async storePrivateKey(key: string, password: string): Promise<void> {
|
|
if (!this.isInitialized) {
|
|
await this.initialize(password);
|
|
}
|
|
|
|
try {
|
|
const derivedKey = await this.deriveKey(password);
|
|
const iv = crypto.getRandomValues(new Uint8Array(12));
|
|
|
|
const encryptedKey = await crypto.subtle.encrypt(
|
|
{ name: 'AES-GCM', iv },
|
|
derivedKey,
|
|
new TextEncoder().encode(key)
|
|
);
|
|
|
|
// Stocker la clé chiffrée avec l'IV
|
|
const encryptedData = new Uint8Array(iv.length + encryptedKey.byteLength);
|
|
encryptedData.set(iv);
|
|
encryptedData.set(new Uint8Array(encryptedKey), iv.length);
|
|
|
|
// Stocker dans IndexedDB de manière sécurisée
|
|
await this.storeEncryptedData(encryptedData);
|
|
|
|
console.log('🔐 Private key stored securely');
|
|
} catch (error) {
|
|
console.error('❌ Failed to store private key:', error);
|
|
throw new Error('Failed to store private key securely');
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Récupère et déchiffre une clé privée
|
|
*/
|
|
async getPrivateKey(password: string): Promise<string | null> {
|
|
try {
|
|
const encryptedData = await this.getEncryptedData();
|
|
if (!encryptedData) {return null;}
|
|
|
|
const derivedKey = await this.deriveKey(password);
|
|
const iv = encryptedData.slice(0, 12);
|
|
const encryptedKey = encryptedData.slice(12);
|
|
|
|
const decrypted = await crypto.subtle.decrypt(
|
|
{ name: 'AES-GCM', iv },
|
|
derivedKey,
|
|
encryptedKey
|
|
);
|
|
|
|
return new TextDecoder().decode(decrypted);
|
|
} catch (error) {
|
|
console.error('❌ Failed to retrieve private key:', error);
|
|
return null;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Supprime toutes les clés stockées
|
|
*/
|
|
async clearKeys(): Promise<void> {
|
|
try {
|
|
await this.clearEncryptedData();
|
|
this.keyStore = null;
|
|
this.isInitialized = false;
|
|
console.log('🔐 All keys cleared');
|
|
} catch (error) {
|
|
console.error('❌ Failed to clear keys:', error);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Vérifie si une clé est stockée
|
|
*/
|
|
async hasStoredKey(): Promise<boolean> {
|
|
try {
|
|
const encryptedData = await this.getEncryptedData();
|
|
return encryptedData !== null;
|
|
} catch {
|
|
return false;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Dérive une clé de chiffrement à partir du mot de passe
|
|
*/
|
|
private async deriveKey(password: string): Promise<CryptoKey> {
|
|
const keyMaterial = await crypto.subtle.importKey(
|
|
'raw',
|
|
new TextEncoder().encode(password),
|
|
'PBKDF2',
|
|
false,
|
|
['deriveKey']
|
|
);
|
|
|
|
return crypto.subtle.deriveKey(
|
|
{
|
|
name: 'PBKDF2',
|
|
salt: new Uint8Array(this.salt),
|
|
iterations: 100000,
|
|
hash: 'SHA-256'
|
|
},
|
|
keyMaterial,
|
|
{ name: 'AES-GCM', length: 256 },
|
|
false,
|
|
['encrypt', 'decrypt']
|
|
);
|
|
}
|
|
|
|
/**
|
|
* Stocke les données chiffrées dans IndexedDB
|
|
*/
|
|
private async storeEncryptedData(data: Uint8Array): Promise<void> {
|
|
return new Promise((resolve, reject) => {
|
|
const request = indexedDB.open('SecureKeyStore', 1);
|
|
|
|
request.onerror = () => reject(new Error('Failed to open IndexedDB'));
|
|
|
|
request.onsuccess = () => {
|
|
const db = request.result;
|
|
const transaction = db.transaction(['keys'], 'readwrite');
|
|
const store = transaction.objectStore('keys');
|
|
|
|
const putRequest = store.put(data, 'encryptedKey');
|
|
putRequest.onsuccess = () => resolve();
|
|
putRequest.onerror = () => reject(new Error('Failed to store encrypted data'));
|
|
};
|
|
|
|
request.onupgradeneeded = () => {
|
|
const db = request.result;
|
|
if (!db.objectStoreNames.contains('keys')) {
|
|
db.createObjectStore('keys');
|
|
}
|
|
};
|
|
});
|
|
}
|
|
|
|
/**
|
|
* Récupère les données chiffrées depuis IndexedDB
|
|
*/
|
|
private async getEncryptedData(): Promise<Uint8Array | null> {
|
|
return new Promise((resolve, reject) => {
|
|
const request = indexedDB.open('SecureKeyStore', 1);
|
|
|
|
request.onerror = () => reject(new Error('Failed to open IndexedDB'));
|
|
|
|
request.onsuccess = () => {
|
|
const db = request.result;
|
|
const transaction = db.transaction(['keys'], 'readonly');
|
|
const store = transaction.objectStore('keys');
|
|
|
|
const getRequest = store.get('encryptedKey');
|
|
getRequest.onsuccess = () => {
|
|
const result = getRequest.result;
|
|
resolve(result ? new Uint8Array(result) : null);
|
|
};
|
|
getRequest.onerror = () => reject(new Error('Failed to retrieve encrypted data'));
|
|
};
|
|
|
|
request.onupgradeneeded = () => {
|
|
const db = request.result;
|
|
if (!db.objectStoreNames.contains('keys')) {
|
|
db.createObjectStore('keys');
|
|
}
|
|
};
|
|
});
|
|
}
|
|
|
|
/**
|
|
* Supprime les données chiffrées
|
|
*/
|
|
private async clearEncryptedData(): Promise<void> {
|
|
return new Promise((resolve, reject) => {
|
|
const request = indexedDB.open('SecureKeyStore', 1);
|
|
|
|
request.onerror = () => reject(new Error('Failed to open IndexedDB'));
|
|
|
|
request.onsuccess = () => {
|
|
const db = request.result;
|
|
const transaction = db.transaction(['keys'], 'readwrite');
|
|
const store = transaction.objectStore('keys');
|
|
|
|
const deleteRequest = store.delete('encryptedKey');
|
|
deleteRequest.onsuccess = () => resolve();
|
|
deleteRequest.onerror = () => reject(new Error('Failed to clear encrypted data'));
|
|
};
|
|
});
|
|
}
|
|
}
|
|
|
|
// Instance singleton pour l'application
|
|
export const secureKeyManager = new SecureKeyManager();
|