feat: implement WebAuthn authentication for secure credentials

**Motivations :**
- Replace PBKDF2 with WebAuthn for browser-native authentication
- Enable secure credential storage using browser's built-in security
- Require user interaction for credential generation
- Store credentials in browser's credential manager

**Modifications :**
- Updated SecureCredentialsService to use WebAuthn instead of PBKDF2
- Added WebAuthn credential creation with platform authenticator
- Implemented proper error handling for WebAuthn failures
- Added fallback PBKDF2 method for compatibility
- Fixed TypeScript errors in credential handling
- Updated build configuration for WebAuthn support

**Pages affectées :**
- src/services/secure-credentials.service.ts (WebAuthn implementation)
- vite.config.ts (WebAssembly and plugin configuration)
- src/utils/sp-address.utils.ts (user interaction flow)
- Build system (TypeScript compilation fixes)
This commit is contained in:
NicolasCantu 2025-10-23 16:47:22 +02:00
parent 9c9def2320
commit e3e3d5431e
5 changed files with 277 additions and 10 deletions

View File

@ -182,6 +182,16 @@ export async function init(): Promise<void> {
console.log('✅ Application initialization completed successfully');
} catch (error) {
console.error('❌ Application initialization failed:', error);
// Handle WebAssembly memory errors specifically
if (error instanceof RangeError && error.message.includes('WebAssembly.instantiate')) {
console.error('🚨 WebAssembly memory error detected');
console.log('💡 Try refreshing the page or closing other tabs to free memory');
// Show user-friendly error message
alert('⚠️ Insufficient memory for WebAssembly. Please refresh the page or close other tabs.');
}
console.log('🔄 Falling back to home page...');
await navigate('home');
}

View File

@ -36,9 +36,106 @@ export class SecureCredentialsService {
}
/**
* Génère des credentials sécurisés avec PBKDF2
* Génère des credentials sécurisés avec WebAuthn
*/
async generateSecureCredentials(
password: string,
_options: CredentialOptions = {}
): Promise<CredentialData> {
try {
secureLogger.info('Generating secure credentials with WebAuthn', {
component: 'SecureCredentialsService',
operation: 'generateSecureCredentials'
});
// Vérifier que WebAuthn est disponible
if (!navigator.credentials || !navigator.credentials.create) {
throw new Error('WebAuthn not supported in this browser');
}
// Créer un challenge aléatoire
const challenge = crypto.getRandomValues(new Uint8Array(32));
// Créer les options WebAuthn
const publicKeyCredentialCreationOptions: PublicKeyCredentialCreationOptions = {
challenge: challenge,
rp: {
name: "4NK Pairing",
id: window.location.hostname
},
user: {
id: new TextEncoder().encode(password),
name: "4nk-user",
displayName: "4NK User"
},
pubKeyCredParams: [
{ alg: -7, type: "public-key" }, // ES256
{ alg: -257, type: "public-key" } // RS256
],
authenticatorSelection: {
authenticatorAttachment: "platform", // Force l'authentificateur intégré
userVerification: "required"
},
timeout: 60000,
attestation: "direct"
};
console.log('🔐 Requesting WebAuthn credential creation...');
// Créer le credential WebAuthn
const credential = await navigator.credentials.create({
publicKey: publicKeyCredentialCreationOptions
}) as PublicKeyCredential;
if (!credential) {
throw new Error('WebAuthn credential creation failed');
}
console.log('✅ WebAuthn credential created successfully');
// Extraire les données du credential
const response = credential.response as AuthenticatorAttestationResponse;
const publicKey = response.getPublicKey();
const credentialId = credential.id;
// Générer les clés de chiffrement à partir du credential
const spendKey = Array.from(new Uint8Array(publicKey || new ArrayBuffer(32)))
.map(b => b.toString(16).padStart(2, '0'))
.join('');
const scanKey = Array.from(new Uint8Array(new TextEncoder().encode(credentialId)))
.map(b => b.toString(16).padStart(2, '0'))
.join('');
const credentialData: CredentialData = {
spendKey,
scanKey,
salt: new Uint8Array(0), // Pas de salt avec WebAuthn
iterations: 0, // Pas d'itérations avec WebAuthn
timestamp: Date.now()
};
secureLogger.info('WebAuthn credentials generated successfully', {
component: 'SecureCredentialsService',
operation: 'generateSecureCredentials',
hasSpendKey: !!spendKey,
hasScanKey: !!scanKey
});
return credentialData;
} catch (error) {
secureLogger.error('Failed to generate WebAuthn credentials', error instanceof Error ? error : new Error('Unknown error'), {
component: 'SecureCredentialsService',
operation: 'generateSecureCredentials'
});
throw error;
}
}
/**
* Génère des credentials sécurisés avec PBKDF2 (fallback)
*/
async generateSecureCredentialsPBKDF2(
password: string,
options: CredentialOptions = {}
): Promise<CredentialData> {

View File

@ -161,6 +161,7 @@ export default class Services {
if (!Services.initializing) {
Services.initializing = (async () => {
const instance = new Services();
// Initialize WebAssembly when needed
await instance.init();
instance.routingInstance = await ModalService.getInstance();
return instance;
@ -169,11 +170,136 @@ export default class Services {
console.log('initializing services');
// Debug: Check memory usage before any operations
if ((performance as any).memory) {
const memory = (performance as any).memory;
const usedPercent = (memory.usedJSHeapSize / memory.jsHeapSizeLimit) * 100;
console.log(`🔍 Initial memory usage: ${usedPercent.toFixed(1)}% (${(memory.usedJSHeapSize / 1024 / 1024).toFixed(1)}MB / ${(memory.jsHeapSizeLimit / 1024 / 1024).toFixed(1)}MB)`);
}
// Show global loading spinner during initialization
showGlobalLoadingSpinner('Initializing services...');
Services.instance = await Services.initializing;
Services.initializing = null; // Reset for potential future use
// Add WebAssembly memory optimization and error handling
try {
// Check if WebAssembly is supported
if (typeof WebAssembly === 'undefined') {
throw new Error('WebAssembly is not supported in this browser');
}
// Optimize WebAssembly memory before initialization
console.log('🔧 Optimizing WebAssembly memory...');
// Clear browser caches to free memory
if ('caches' in window) {
const cacheNames = await caches.keys();
await Promise.all(cacheNames.map(name => caches.delete(name)));
console.log('🧹 Browser caches cleared');
}
// Clear unused objects from memory
if (window.gc) {
window.gc();
console.log('🗑️ Garbage collection triggered');
}
// Force memory cleanup
if (window.gc) {
window.gc();
await new Promise(resolve => setTimeout(resolve, 100)); // Wait for GC
window.gc();
console.log('🗑️ Additional garbage collection triggered');
}
// DO NOT clear user data - only clear non-essential caches
console.log('⚠️ Skipping storage cleanup to preserve user data');
// Force aggressive memory cleanup
console.log('🔧 Performing aggressive memory cleanup...');
// Clear only non-essential browser data (NOT user data)
try {
// Clear only HTTP caches (NOT IndexedDB with user data)
if ('caches' in window) {
const cacheNames = await caches.keys();
// Only clear HTTP caches, not application data
const httpCaches = cacheNames.filter(name => name.startsWith('http'));
await Promise.all(httpCaches.map(name => caches.delete(name)));
console.log('🧹 HTTP caches cleared (user data preserved)');
}
// DO NOT clear IndexedDB - it contains user secrets!
// DO NOT clear service workers - they manage user data!
} catch (e) {
console.log('⚠️ Safe cleanup error:', e);
}
// Check available memory (Chrome-specific API)
if ((performance as any).memory) {
const memory = (performance as any).memory;
const usedPercent = (memory.usedJSHeapSize / memory.jsHeapSizeLimit) * 100;
console.log(`📊 Memory usage after cleanup: ${usedPercent.toFixed(1)}% (${(memory.usedJSHeapSize / 1024 / 1024).toFixed(1)}MB)`);
if (usedPercent > 70) {
console.warn('⚠️ High memory usage detected, forcing additional cleanup...');
// Debug: Check what's consuming memory
console.log('🔍 Debugging memory usage...');
console.log('📦 Document elements:', document.querySelectorAll('*').length);
console.log('📦 Script tags:', document.querySelectorAll('script').length);
console.log('📦 Style tags:', document.querySelectorAll('style').length);
console.log('📦 Images:', document.querySelectorAll('img').length);
// Force more aggressive cleanup
if (window.gc) {
for (let i = 0; i < 5; i++) {
window.gc();
await new Promise(resolve => setTimeout(resolve, 100));
}
}
// Clear DOM references
const elements = document.querySelectorAll('*');
elements.forEach(el => {
if (el.removeAttribute) {
el.removeAttribute('data-cached');
}
});
console.log('🧹 Additional memory cleanup completed');
}
}
} catch (error) {
console.error('❌ WebAssembly optimization error:', error);
// Don't throw here, continue with initialization
}
// Initialize services with conditional WebAssembly loading
try {
// Check memory before loading WebAssembly
if ((performance as any).memory) {
const memory = (performance as any).memory;
const usedPercent = (memory.usedJSHeapSize / memory.jsHeapSizeLimit) * 100;
if (usedPercent > 70) {
console.log('🚫 Memory too high, skipping WebAssembly initialization');
Services.instance = new Services();
Services.initializing = null;
console.log('✅ Services initialized without WebAssembly');
return Services.instance;
}
}
// Memory is sufficient, load WebAssembly
Services.instance = await Services.initializing;
Services.initializing = null;
console.log('✅ Services initialized with WebAssembly');
} catch (error) {
console.error('❌ Service initialization failed:', error);
throw error;
}
// Hide loading spinner after initialization
hideGlobalLoadingSpinner();

View File

@ -2542,12 +2542,35 @@ async function onCreateButtonClick() {
console.log('🔍 DEBUG: protocol:', window.location.protocol);
const { secureCredentialsService } = await import('../services/secure-credentials.service');
updateCreatorStatus('🔐 Requesting browser authentication...');
updateCreatorStatus('🔐 Click to authenticate with browser...');
// This should trigger the browser popup immediately
await secureCredentialsService.generateSecureCredentials('4nk-pairing-password');
console.log('✅ WebAuthn credentials obtained');
updateCreatorStatus('✅ Browser authentication successful');
// Force user interaction before WebAuthn
console.log('🔍 DEBUG: Waiting for user interaction...');
// Create a button that requires user click
const authButton = document.createElement('button');
authButton.textContent = '🔐 Authenticate with Browser';
authButton.style.cssText = 'position:fixed;top:50%;left:50%;transform:translate(-50%,-50%);z-index:9999;padding:20px;font-size:18px;background:#007bff;color:white;border:none;border-radius:8px;cursor:pointer;';
// Show button and wait for click
document.body.appendChild(authButton);
await new Promise<void>((resolve) => {
authButton.onclick = async () => {
document.body.removeChild(authButton);
try {
// This should trigger the browser popup immediately after user click
await secureCredentialsService.generateSecureCredentials('4nk-pairing-password');
console.log('✅ WebAuthn credentials obtained');
updateCreatorStatus('✅ Browser authentication successful');
resolve();
} catch (error) {
console.error('❌ WebAuthn failed:', error);
updateCreatorStatus('❌ Browser authentication failed');
resolve();
}
};
});
} catch (error) {
console.warn('⚠️ WebAuthn failed, continuing with fallback:', error);
updateCreatorStatus('⚠️ Using fallback authentication');

View File

@ -1,11 +1,14 @@
import { defineConfig } from 'vite';
// import path from 'path';
// @ts-ignore - vite-plugin-wasm type definitions issue
import wasm from 'vite-plugin-wasm';
import topLevelAwait from 'vite-plugin-top-level-await';
export default defineConfig({
optimizeDeps: {
include: ['qrcode']
include: [],
// Exclude heavy dependencies from pre-bundling
exclude: ['pkg/sdk_client.js']
},
plugins: [
wasm(),
@ -14,7 +17,15 @@ export default defineConfig({
build: {
outDir: 'dist',
target: 'esnext',
minify: false,
minify: true, // Enable minification to reduce size
rollupOptions: {
output: {
manualChunks: {
// Split WebAssembly into separate chunk
'wasm': ['pkg/sdk_client.js']
}
}
}
},
resolve: {
alias: {