**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
456 lines
15 KiB
TypeScript
456 lines
15 KiB
TypeScript
/**
|
|
* WebAuthnService - Gestion des opérations WebAuthn
|
|
*/
|
|
import { secureLogger } from '../secure-logger';
|
|
import { SecurityMode } from '../security-mode.service';
|
|
import { WebAuthnCredential, EncryptionResult } from './types';
|
|
import { DATABASE_CONFIG } from '../database-config';
|
|
|
|
export class WebAuthnService {
|
|
private static instance: WebAuthnService;
|
|
|
|
private constructor() {}
|
|
|
|
public static getInstance(): WebAuthnService {
|
|
if (!WebAuthnService.instance) {
|
|
WebAuthnService.instance = new WebAuthnService();
|
|
}
|
|
return WebAuthnService.instance;
|
|
}
|
|
|
|
/**
|
|
* Stocke une clé PBKDF2 avec WebAuthn
|
|
* WebAuthn est utilisé uniquement pour l'authentification, pas pour le chiffrement
|
|
*/
|
|
async storeKeyWithWebAuthn(key: string, securityMode: SecurityMode): Promise<void> {
|
|
try {
|
|
secureLogger.info('Storing PBKDF2 key with WebAuthn encryption', {
|
|
component: 'WebAuthnService',
|
|
operation: 'storeKeyWithWebAuthn',
|
|
securityMode
|
|
});
|
|
|
|
// Créer des credentials WebAuthn pour l'authentification
|
|
const credential = await this.createCredentials('4nk-pbkdf2-key', securityMode);
|
|
|
|
// Chiffrer la clé PBKDF2 avec la réponse WebAuthn
|
|
const encryptedKey = await this.encryptKeyWithWebAuthn(key, credential);
|
|
|
|
// Stocker la clé PBKDF2 chiffrée dans IndexedDB
|
|
// La clé est stockée avec le securityMode comme clé
|
|
await this.savePBKDF2Key(encryptedKey, credential.id, securityMode);
|
|
|
|
secureLogger.info('PBKDF2 key encrypted and stored with WebAuthn successfully', {
|
|
component: 'WebAuthnService',
|
|
operation: 'storeKeyWithWebAuthn'
|
|
});
|
|
|
|
} catch (error) {
|
|
secureLogger.error('Failed to store PBKDF2 key with WebAuthn', error as Error, {
|
|
component: 'WebAuthnService',
|
|
operation: 'storeKeyWithWebAuthn'
|
|
});
|
|
throw error;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Détecte les authentificateurs disponibles
|
|
*/
|
|
async detectAvailableAuthenticators(): Promise<boolean> {
|
|
try {
|
|
if (!navigator.credentials || !navigator.credentials.create) {
|
|
return false;
|
|
}
|
|
|
|
if (typeof PublicKeyCredential === 'undefined') {
|
|
return false;
|
|
}
|
|
|
|
// Vérifier la disponibilité sans faire d'appel réel à WebAuthn
|
|
// Juste vérifier que les APIs sont disponibles
|
|
if (!navigator.credentials.get) {
|
|
return false;
|
|
}
|
|
|
|
// Vérifier si on est dans un contexte sécurisé (requis pour WebAuthn)
|
|
if (location.protocol !== 'https:' && location.hostname !== 'localhost') {
|
|
return false;
|
|
}
|
|
|
|
return true;
|
|
} catch (error) {
|
|
secureLogger.error('Error detecting authenticators', error as Error, {
|
|
component: 'WebAuthnService',
|
|
operation: 'detectAvailableAuthenticators'
|
|
});
|
|
return false;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Détecte spécifiquement Proton Pass
|
|
*/
|
|
async detectProtonPass(): Promise<boolean> {
|
|
try {
|
|
console.log('🔍 Detecting Proton Pass availability...');
|
|
|
|
const available = await this.detectAvailableAuthenticators();
|
|
if (!available) {
|
|
console.log('❌ WebAuthn not available');
|
|
return false;
|
|
}
|
|
|
|
console.log('✅ WebAuthn is available, checking Proton Pass support...');
|
|
|
|
// Vérifier la disponibilité sans faire d'appel réel à WebAuthn
|
|
// Juste vérifier que les APIs sont disponibles
|
|
if (!navigator.credentials || !navigator.credentials.create) {
|
|
console.log('❌ WebAuthn credentials API not available');
|
|
return false;
|
|
}
|
|
|
|
// Vérifier si on est dans un contexte sécurisé (requis pour WebAuthn)
|
|
if (location.protocol !== 'https:' && location.hostname !== 'localhost') {
|
|
console.log('❌ WebAuthn requires HTTPS or localhost');
|
|
return false;
|
|
}
|
|
|
|
console.log('✅ Proton Pass should be available (basic checks passed)');
|
|
return true;
|
|
} catch (error) {
|
|
console.log('❌ Error detecting Proton Pass:', error);
|
|
return false;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Crée des credentials WebAuthn
|
|
*/
|
|
async createCredentials(
|
|
password: string,
|
|
mode: SecurityMode
|
|
): Promise<WebAuthnCredential> {
|
|
console.log('🔐 WebAuthnService.createCredentials called with mode:', mode);
|
|
|
|
// Vérifier la disponibilité de Proton Pass si c'est le mode sélectionné
|
|
if (mode === 'proton-pass') {
|
|
console.log('🔍 Checking Proton Pass availability...');
|
|
const protonPassAvailable = await this.detectProtonPass();
|
|
if (!protonPassAvailable) {
|
|
console.log('❌ Proton Pass not available, falling back to platform authenticator');
|
|
// Ne pas échouer, mais utiliser un mode de fallback
|
|
} else {
|
|
console.log('✅ Proton Pass is available and ready');
|
|
}
|
|
}
|
|
|
|
const challenge = crypto.getRandomValues(new Uint8Array(32));
|
|
|
|
const authenticatorSelection: AuthenticatorSelectionCriteria = {
|
|
userVerification: "required",
|
|
residentKey: "required"
|
|
};
|
|
|
|
// Configuration spécifique selon le mode
|
|
if (mode === 'proton-pass') {
|
|
authenticatorSelection.authenticatorAttachment = "platform";
|
|
console.log('🔐 Configuring for Proton Pass (platform authenticator)');
|
|
} else if (mode === 'os') {
|
|
authenticatorSelection.authenticatorAttachment = "platform";
|
|
console.log('🔐 Configuring for OS authenticator (platform)');
|
|
}
|
|
|
|
const publicKeyCredentialCreationOptions: PublicKeyCredentialCreationOptions = {
|
|
challenge: challenge,
|
|
rp: {
|
|
name: "4NK Secure Storage",
|
|
id: window.location.hostname
|
|
},
|
|
user: {
|
|
id: new TextEncoder().encode(password),
|
|
name: "4nk-user",
|
|
displayName: "4NK User"
|
|
},
|
|
pubKeyCredParams: [
|
|
{ alg: -7, type: "public-key" },
|
|
{ alg: -257, type: "public-key" }
|
|
],
|
|
authenticatorSelection,
|
|
timeout: 60000,
|
|
attestation: "none"
|
|
};
|
|
|
|
// Options spécifiques pour Proton Pass
|
|
if (mode === 'proton-pass') {
|
|
publicKeyCredentialCreationOptions.extensions = {
|
|
largeBlob: { support: "preferred" }
|
|
};
|
|
console.log('🔐 Added largeBlob extension for Proton Pass');
|
|
}
|
|
|
|
console.log('🔐 Calling navigator.credentials.create with options:', publicKeyCredentialCreationOptions);
|
|
console.log('🔐 This should trigger Proton Pass window...');
|
|
|
|
try {
|
|
const credential = await navigator.credentials.create({
|
|
publicKey: publicKeyCredentialCreationOptions
|
|
}) as PublicKeyCredential;
|
|
|
|
console.log('🔐 WebAuthn credential created successfully:', credential);
|
|
|
|
const response = credential.response as AuthenticatorAttestationResponse;
|
|
return {
|
|
id: Array.from(new Uint8Array(credential.rawId)).map(b => b.toString(16).padStart(2, '0')).join(''),
|
|
publicKey: Array.from(new Uint8Array(response.getPublicKey()!))
|
|
};
|
|
} catch (error) {
|
|
console.error('❌ WebAuthn credential creation failed:', error);
|
|
|
|
// Message d'erreur spécifique pour Proton Pass
|
|
if (mode === 'proton-pass') {
|
|
const errorMessage = error instanceof Error ? error.message : String(error);
|
|
throw new Error(`Proton Pass authentication failed: ${errorMessage}. Please ensure Proton Pass is installed and enabled.`);
|
|
}
|
|
|
|
throw error;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Chiffre une clé avec WebAuthn
|
|
*/
|
|
private async encryptKeyWithWebAuthn(key: string, credential: WebAuthnCredential): Promise<string> {
|
|
try {
|
|
// Utiliser la clé publique WebAuthn pour chiffrer la clé PBKDF2
|
|
// Pour l'instant, on utilise un chiffrement AES-GCM avec une clé dérivée
|
|
const { EncryptionService } = await import('./encryption.service');
|
|
const encryptionService = EncryptionService.getInstance();
|
|
|
|
// Utiliser l'ID de la credential WebAuthn comme mot de passe pour chiffrer la clé PBKDF2
|
|
const encryptedKey = await encryptionService.encryptWithPassword(key, credential.id);
|
|
|
|
console.log('🔐 Key encrypted with WebAuthn credential');
|
|
return encryptedKey;
|
|
} catch (error) {
|
|
secureLogger.error('Failed to encrypt key with WebAuthn', error as Error, {
|
|
component: 'WebAuthnService',
|
|
operation: 'encryptKeyWithWebAuthn'
|
|
});
|
|
throw error;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Sauvegarde une clé chiffrée dans IndexedDB
|
|
*/
|
|
/**
|
|
* Sauvegarde la clé PBKDF2 chiffrée dans IndexedDB
|
|
* NE PAS stocker credentialId avec la clé chiffrée
|
|
*/
|
|
private async savePBKDF2Key(encryptedKey: string, _credentialId: string, securityMode: SecurityMode): Promise<void> {
|
|
try {
|
|
const db = await this.openDatabase();
|
|
console.log(`🔍 Available stores in ${DATABASE_CONFIG.name}:`, Array.from(db.objectStoreNames));
|
|
const transaction = db.transaction([DATABASE_CONFIG.stores.pbkdf2keys.name], 'readwrite');
|
|
const store = transaction.objectStore(DATABASE_CONFIG.stores.pbkdf2keys.name);
|
|
|
|
await new Promise<void>((resolve, reject) => {
|
|
// Utiliser le securityMode comme clé d'enregistrement
|
|
// NE PAS stocker credentialId avec la clé chiffrée
|
|
const request = store.put({
|
|
encryptedKey, // Clé PBKDF2 chiffrée avec WebAuthn
|
|
timestamp: Date.now()
|
|
}, securityMode);
|
|
request.onsuccess = () => resolve();
|
|
request.onerror = () => reject(request.error);
|
|
});
|
|
|
|
console.log(`🔐 PBKDF2 key stored with security mode: ${securityMode}`);
|
|
|
|
} catch (error) {
|
|
secureLogger.error('Failed to save PBKDF2 key', error as Error, {
|
|
component: 'WebAuthnService',
|
|
operation: 'savePBKDF2Key'
|
|
});
|
|
throw error;
|
|
}
|
|
}
|
|
|
|
|
|
/**
|
|
* Récupère et déchiffre la clé PBKDF2 avec WebAuthn
|
|
*/
|
|
async retrieveKeyWithWebAuthn(securityMode: SecurityMode): Promise<string | null> {
|
|
try {
|
|
// Récupérer la clé PBKDF2 chiffrée depuis IndexedDB avec le securityMode comme clé
|
|
const db = await this.openDatabase();
|
|
const transaction = db.transaction([DATABASE_CONFIG.stores.pbkdf2keys.name], 'readonly');
|
|
const store = transaction.objectStore(DATABASE_CONFIG.stores.pbkdf2keys.name);
|
|
|
|
const result = await new Promise<any>((resolve, reject) => {
|
|
const request = store.get(securityMode);
|
|
request.onsuccess = () => resolve(request.result);
|
|
request.onerror = () => reject(request.error);
|
|
});
|
|
|
|
if (!result || !result.encryptedKey) {
|
|
console.log(`🔍 No PBKDF2 key found for security mode: ${securityMode}`);
|
|
return null;
|
|
}
|
|
|
|
// Récupérer le credentialId dynamiquement via WebAuthn
|
|
const credentialId = await this.getCurrentCredentialId();
|
|
if (!credentialId) {
|
|
console.log('🔍 No WebAuthn credential available');
|
|
return null;
|
|
}
|
|
|
|
// Déchiffrer la clé avec le credentialId WebAuthn
|
|
const encrypted = atob(result.encryptedKey);
|
|
const combined = new Uint8Array(encrypted.length);
|
|
for (let i = 0; i < encrypted.length; i++) {
|
|
combined[i] = encrypted.charCodeAt(i);
|
|
}
|
|
|
|
// Extraire salt (16 bytes), iv (12 bytes) et données chiffrées
|
|
const salt = combined.slice(0, 16);
|
|
const iv = combined.slice(16, 28);
|
|
const encryptedData = combined.slice(28);
|
|
|
|
// Dériver la clé avec PBKDF2
|
|
const keyMaterial = await crypto.subtle.importKey(
|
|
'raw',
|
|
new TextEncoder().encode(credentialId),
|
|
'PBKDF2',
|
|
false,
|
|
['deriveBits']
|
|
);
|
|
|
|
const derivedKey = await crypto.subtle.deriveBits(
|
|
{
|
|
name: 'PBKDF2',
|
|
salt: salt,
|
|
iterations: 100000,
|
|
hash: 'SHA-256'
|
|
},
|
|
keyMaterial,
|
|
256
|
|
);
|
|
|
|
const cryptoKey = await crypto.subtle.importKey(
|
|
'raw',
|
|
derivedKey,
|
|
{ name: 'AES-GCM' },
|
|
false,
|
|
['decrypt']
|
|
);
|
|
|
|
// Déchiffrer
|
|
const decrypted = await crypto.subtle.decrypt(
|
|
{ name: 'AES-GCM', iv: iv },
|
|
cryptoKey,
|
|
encryptedData
|
|
);
|
|
|
|
const decryptedKey = new TextDecoder().decode(decrypted);
|
|
console.log('🔐 PBKDF2 key decrypted with WebAuthn');
|
|
return decryptedKey;
|
|
|
|
} catch (error) {
|
|
secureLogger.error('Failed to retrieve PBKDF2 key with WebAuthn', error as Error, {
|
|
component: 'WebAuthnService',
|
|
operation: 'retrieveKeyWithWebAuthn'
|
|
});
|
|
return null;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Récupère l'ID de la credential WebAuthn actuelle
|
|
*/
|
|
private async getCurrentCredentialId(): Promise<string | null> {
|
|
try {
|
|
// Utiliser WebAuthn pour récupérer l'ID de la credential
|
|
const credential = await navigator.credentials.get({
|
|
publicKey: {
|
|
challenge: new Uint8Array(32),
|
|
allowCredentials: [],
|
|
userVerification: 'preferred'
|
|
}
|
|
});
|
|
|
|
if (credential && credential.id) {
|
|
return credential.id;
|
|
}
|
|
|
|
return null;
|
|
} catch (error) {
|
|
console.log('🔍 No WebAuthn credential available:', error);
|
|
return null;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Ouvre la base de données IndexedDB
|
|
*/
|
|
private async openDatabase(): Promise<IDBDatabase> {
|
|
return new Promise((resolve, reject) => {
|
|
const request = indexedDB.open(DATABASE_CONFIG.name, DATABASE_CONFIG.version);
|
|
|
|
request.onerror = () => {
|
|
secureLogger.error('Failed to open database', new Error('Database open failed'), {
|
|
component: 'WebAuthnService',
|
|
operation: 'openDatabase'
|
|
});
|
|
reject(request.error);
|
|
};
|
|
|
|
request.onsuccess = () => {
|
|
resolve(request.result);
|
|
};
|
|
|
|
request.onupgradeneeded = (event) => {
|
|
const db = (event.target as IDBOpenDBRequest).result;
|
|
console.log(`🔄 ${DATABASE_CONFIG.name} database upgrade needed, creating stores...`);
|
|
if (!db.objectStoreNames.contains(DATABASE_CONFIG.stores.pbkdf2keys.name)) {
|
|
db.createObjectStore(DATABASE_CONFIG.stores.pbkdf2keys.name);
|
|
console.log(`✅ ${DATABASE_CONFIG.stores.pbkdf2keys.name} store created`);
|
|
} else {
|
|
console.log(`✅ ${DATABASE_CONFIG.stores.pbkdf2keys.name} store already exists`);
|
|
}
|
|
};
|
|
});
|
|
}
|
|
|
|
/**
|
|
* Utilise des credentials WebAuthn existants
|
|
*/
|
|
async useCredentials(
|
|
credentialId: string,
|
|
mode: SecurityMode
|
|
): Promise<PublicKeyCredential> {
|
|
const getOptions: PublicKeyCredentialRequestOptions = {
|
|
challenge: crypto.getRandomValues(new Uint8Array(32)),
|
|
allowCredentials: [{
|
|
id: new TextEncoder().encode(credentialId),
|
|
type: 'public-key'
|
|
}],
|
|
userVerification: 'required',
|
|
timeout: 60000,
|
|
rpId: window.location.hostname
|
|
};
|
|
|
|
// Configuration spécifique selon le mode
|
|
if (mode === 'proton-pass') {
|
|
getOptions.extensions = {
|
|
largeBlob: { support: "preferred" }
|
|
};
|
|
}
|
|
|
|
return await navigator.credentials.get({
|
|
publicKey: getOptions
|
|
}) as PublicKeyCredential;
|
|
}
|
|
}
|