ihm_client/src/services/credentials/webauthn.service.ts
NicolasCantu aa913ef930 feat: centralize database configuration and fix service worker blocking
**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
2025-10-26 02:19:00 +01:00

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;
}
}