471 lines
24 KiB
TypeScript
471 lines
24 KiB
TypeScript
/**
|
||
* Page de génération du wallet
|
||
* Deuxième étape du processus d'initialisation
|
||
*/
|
||
|
||
import { DATABASE_CONFIG } from '../../services/database-config';
|
||
|
||
document.addEventListener('DOMContentLoaded', async () => {
|
||
console.log('💰 Wallet setup page loaded');
|
||
|
||
const status = document.getElementById('status') as HTMLDivElement;
|
||
const progressBar = document.getElementById('progressBar') as HTMLDivElement;
|
||
const continueBtn = document.getElementById('continueBtn') as HTMLButtonElement;
|
||
|
||
function updateStatus(message: string, type: 'loading' | 'success' | 'error') {
|
||
status.textContent = message;
|
||
status.className = `status ${type}`;
|
||
}
|
||
|
||
function updateProgress(percent: number) {
|
||
progressBar.style.width = `${percent}%`;
|
||
}
|
||
|
||
// Méthode pour sauvegarder directement en IndexedDB dans la base 4nk
|
||
async function saveCredentialsDirectly(credentials: any): Promise<void> {
|
||
return new Promise((resolve, reject) => {
|
||
const request = indexedDB.open(DATABASE_CONFIG.name, DATABASE_CONFIG.version);
|
||
|
||
request.onerror = () => reject(request.error);
|
||
request.onsuccess = () => {
|
||
const db = request.result;
|
||
const transaction = db.transaction([DATABASE_CONFIG.stores.wallet.name], 'readwrite');
|
||
const store = transaction.objectStore(DATABASE_CONFIG.stores.wallet.name);
|
||
|
||
const putRequest = store.put(credentials, '/4nk/credentials');
|
||
putRequest.onsuccess = () => resolve();
|
||
putRequest.onerror = () => reject(putRequest.error);
|
||
};
|
||
|
||
request.onupgradeneeded = () => {
|
||
const db = request.result;
|
||
if (!db.objectStoreNames.contains(DATABASE_CONFIG.stores.wallet.name)) {
|
||
db.createObjectStore(DATABASE_CONFIG.stores.wallet.name, { keyPath: DATABASE_CONFIG.stores.wallet.keyPath as string });
|
||
}
|
||
};
|
||
});
|
||
}
|
||
|
||
try {
|
||
// Étape 1: Initialisation des services
|
||
updateStatus('🔄 Initialisation des services...', 'loading');
|
||
updateProgress(10);
|
||
|
||
let services: any; // Déclarer services au niveau supérieur
|
||
|
||
try {
|
||
console.log('🔄 Importing services...');
|
||
const serviceModule = await import('../../services/service');
|
||
console.log('✅ Service module imported:', Object.keys(serviceModule));
|
||
|
||
// La classe Services est exportée par défaut
|
||
const Services = serviceModule.default;
|
||
|
||
if (!Services) {
|
||
throw new Error('Services class not found in default export');
|
||
}
|
||
console.log('🔄 Waiting for services to be ready...');
|
||
|
||
// Attendre que les services soient initialisés avec plus de patience
|
||
let attempts = 0;
|
||
const maxAttempts = 30; // Plus de tentatives
|
||
const delayMs = 2000; // Délai plus long entre les tentatives
|
||
|
||
while (attempts < maxAttempts) {
|
||
try {
|
||
console.log(`🔄 Attempting to get services (attempt ${attempts + 1}/${maxAttempts})...`);
|
||
services = await Services.getInstance();
|
||
console.log('✅ Services initialized successfully');
|
||
break;
|
||
} catch (error) {
|
||
console.log(`⏳ Services not ready yet (attempt ${attempts + 1}/${maxAttempts}):`, error instanceof Error ? error.message : String(error));
|
||
|
||
// Diagnostic plus détaillé
|
||
if (attempts === 5) {
|
||
console.log('🔍 Diagnostic: Checking memory usage...');
|
||
if ((performance as any).memory) {
|
||
const memory = (performance as any).memory;
|
||
console.log(`📊 Memory usage: ${Math.round(memory.usedJSHeapSize / 1024 / 1024)}MB / ${Math.round(memory.totalJSHeapSize / 1024 / 1024)}MB`);
|
||
}
|
||
}
|
||
|
||
attempts++;
|
||
if (attempts >= maxAttempts) {
|
||
throw new Error(`Services failed to initialize after ${maxAttempts} attempts. This may be due to high memory usage or WebAssembly initialization issues.`);
|
||
}
|
||
await new Promise(resolve => setTimeout(resolve, delayMs));
|
||
}
|
||
}
|
||
} catch (error) {
|
||
console.error('❌ Services not available:', error);
|
||
throw error;
|
||
}
|
||
|
||
// Étape 2: Génération des credentials sécurisés
|
||
updateStatus('🔐 Génération des clés de sécurité...', 'loading');
|
||
updateProgress(30);
|
||
|
||
try {
|
||
const { SecureCredentialsService } = await import('../../services/secure-credentials.service');
|
||
const secureCredentialsService = SecureCredentialsService.getInstance();
|
||
|
||
// Vérifier si des credentials existent déjà
|
||
// CRITICAL: wallet-setup ne doit PAS générer de credentials
|
||
// Les credentials et clé PBKDF2 doivent être créés dans security-setup
|
||
// On vérifie juste que la clé PBKDF2 existe
|
||
console.log('🔐 Checking for existing PBKDF2 key...');
|
||
} catch (error) {
|
||
// Erreur lors de la vérification, on continuera pour tester les clés PBKDF2
|
||
console.log('ℹ️ Could not check credentials status, will test PBKDF2 keys directly');
|
||
}
|
||
|
||
// Étape 3: Sauvegarde du wallet avec état birthday_waiting
|
||
updateStatus('💰 Sauvegarde du portefeuille...', 'loading');
|
||
updateProgress(60);
|
||
|
||
try {
|
||
console.log('🔐 Sauvegarde du wallet avec état birthday_waiting...');
|
||
|
||
// DÉTECTER le mode de sécurité via la clé PBKDF2 disponible
|
||
// Le mode de sécurité est identifié par la clé PBKDF2 qui fonctionne
|
||
const { SecureCredentialsService } = await import('../../services/secure-credentials.service');
|
||
const secureCredentialsService = SecureCredentialsService.getInstance();
|
||
|
||
// TEST: Log credentialId from sessionStorage
|
||
const storedCredentialId = sessionStorage.getItem('webauthn_credential_id');
|
||
if (storedCredentialId) {
|
||
console.log('🔐 TEST: credentialId from sessionStorage in wallet-setup:', storedCredentialId);
|
||
} else {
|
||
console.log('🔐 TEST: No credentialId found in sessionStorage');
|
||
}
|
||
|
||
console.log('🔍 Testing all security modes to find a PBKDF2 key...');
|
||
|
||
// Tester tous les modes de sécurité pour trouver une clé PBKDF2 valide
|
||
const allSecurityModes: Array<'browser' | 'otp' | 'password' | 'none' | 'os' | 'proton-pass'> =
|
||
['browser', 'otp', 'password', 'none', 'os', 'proton-pass'];
|
||
|
||
let currentMode: string | null = null;
|
||
|
||
for (const mode of allSecurityModes) {
|
||
console.log(`🔍 Testing security mode: ${mode}`);
|
||
try {
|
||
const key = await secureCredentialsService.retrievePBKDF2Key(mode);
|
||
if (key) {
|
||
currentMode = mode;
|
||
console.log(`✅ PBKDF2 key found for security mode: ${mode}`);
|
||
break;
|
||
}
|
||
} catch (error) {
|
||
console.log(`⚠️ No PBKDF2 key found for mode ${mode}`);
|
||
}
|
||
}
|
||
|
||
// CRITICAL: Si aucune clé PBKDF2 n'est disponible, arrêter immédiatement
|
||
if (!currentMode) {
|
||
console.error('❌ CRITICAL: No PBKDF2 key found for any security mode.');
|
||
console.error('❌ Cannot proceed with wallet creation without encryption key.');
|
||
updateStatus('❌ Erreur: Aucune clé de chiffrement trouvée. Veuillez configurer la sécurité.', 'error');
|
||
throw new Error('CRITICAL: No PBKDF2 key found. Cannot create wallet without encryption key.');
|
||
}
|
||
|
||
console.log('🔐 Using security mode for wallet encryption:', currentMode);
|
||
|
||
// Générer un wallet temporaire avec état birthday_waiting
|
||
const { EncryptionService } = await import('../../services/encryption.service');
|
||
const encryptionService = EncryptionService.getInstance();
|
||
|
||
// Générer des clés temporaires pour le wallet
|
||
// Générer un wallet temporaire avec l'état birthday_waiting
|
||
const walletData = {
|
||
scan_sk: encryptionService.generateRandomKey(),
|
||
spend_key: encryptionService.generateRandomKey(),
|
||
network: 'signet',
|
||
state: 'birthday_waiting',
|
||
created_at: new Date().toISOString()
|
||
};
|
||
|
||
console.log('🔐 Wallet data generated:', walletData);
|
||
|
||
// Récupérer la clé PBKDF2 existante pour le mode détecté
|
||
// IMPORTANT: Ne PAS générer de nouvelle clé, utiliser celle qui existe
|
||
console.log('🔐 Retrieving existing PBKDF2 key for security mode:', currentMode);
|
||
const pbkdf2Key = await secureCredentialsService.retrievePBKDF2Key(currentMode as any);
|
||
if (!pbkdf2Key) {
|
||
console.error('❌ CRITICAL: Failed to retrieve PBKDF2 key for mode:', currentMode);
|
||
updateStatus('❌ Erreur: Impossible de récupérer la clé de chiffrement.', 'error');
|
||
throw new Error('CRITICAL: Failed to retrieve PBKDF2 key');
|
||
}
|
||
console.log('🔐 PBKDF2 key retrieved for wallet encryption');
|
||
|
||
// Chiffrer le wallet avec la clé PBKDF2
|
||
const encryptedWallet = await encryptionService.encrypt(
|
||
JSON.stringify(walletData),
|
||
pbkdf2Key
|
||
);
|
||
console.log('🔐 Wallet encrypted with PBKDF2 key');
|
||
console.log('🔐 Encrypted wallet data:', encryptedWallet);
|
||
|
||
// Ouvrir la base de données 4nk existante sans la modifier
|
||
console.log(`🔍 Opening IndexedDB database "${DATABASE_CONFIG.name}" version ${DATABASE_CONFIG.version}...`);
|
||
const db = await new Promise<IDBDatabase>((resolve, reject) => {
|
||
const request = indexedDB.open(DATABASE_CONFIG.name, DATABASE_CONFIG.version); // Utiliser la version centralisée
|
||
request.onerror = () => {
|
||
console.error('❌ Failed to open IndexedDB:', request.error);
|
||
reject(request.error);
|
||
};
|
||
request.onsuccess = () => {
|
||
console.log('✅ IndexedDB opened successfully');
|
||
console.log('🔍 Database name:', request.result.name);
|
||
console.log('🔍 Database version:', request.result.version);
|
||
console.log('🔍 Available stores:', Array.from(request.result.objectStoreNames));
|
||
resolve(request.result);
|
||
};
|
||
request.onupgradeneeded = () => {
|
||
const db = request.result;
|
||
console.log('🔄 IndexedDB upgrade needed, checking wallet store...');
|
||
|
||
// Créer le store wallet seulement s'il n'existe pas
|
||
if (!db.objectStoreNames.contains(DATABASE_CONFIG.stores.wallet.name)) {
|
||
const store = db.createObjectStore(DATABASE_CONFIG.stores.wallet.name, { keyPath: DATABASE_CONFIG.stores.wallet.keyPath as string });
|
||
console.log(`✅ Wallet store created with keyPath: ${DATABASE_CONFIG.stores.wallet.keyPath}`);
|
||
} else {
|
||
console.log('✅ Wallet store already exists');
|
||
}
|
||
};
|
||
});
|
||
|
||
// Étape 1: Sauvegarder le wallet dans le format attendu par getDeviceFromDatabase
|
||
// Utiliser le SDK directement pour éviter le service worker qui bloque
|
||
console.log('🔧 Creating device using SDK...');
|
||
|
||
// Créer le device directement avec le SDK sans passer par saveDeviceInDatabase
|
||
if (!services.sdkClient) {
|
||
throw new Error('WebAssembly SDK not initialized');
|
||
}
|
||
|
||
// We set birthday later when we have the chain tip from relay
|
||
console.log('🔧 Creating new device with birthday 0...');
|
||
const spAddress = await services.sdkClient.create_new_device(0, 'signet');
|
||
console.log('✅ Device created with address:', spAddress);
|
||
|
||
// Force wallet generation to ensure keys are created
|
||
console.log('🔧 Forcing wallet generation...');
|
||
try {
|
||
const wallet = await services.sdkClient.dump_wallet();
|
||
console.log('✅ Wallet generated:', JSON.stringify(wallet));
|
||
} catch (walletError) {
|
||
console.warn('⚠️ Wallet generation failed:', walletError);
|
||
}
|
||
|
||
// Récupérer le device créé par le SDK
|
||
const device = services.dumpDeviceFromMemory();
|
||
console.log('🔍 Device structure from SDK:', {
|
||
hasSpWallet: !!device.sp_wallet,
|
||
hasSpClient: !!device.sp_client,
|
||
spClientKeys: device.sp_client ? Object.keys(device.sp_client) : 'none'
|
||
});
|
||
|
||
console.log(`🔍 Opening transaction for ${DATABASE_CONFIG.stores.wallet.name} store...`);
|
||
|
||
// CRITICAL: Chiffrer TOUS les données du wallet avant stockage
|
||
// Le device contient des données sensibles (sp_wallet) qui ne doivent JAMAIS être en clair
|
||
console.log('🔐 Encrypting device data before storage...');
|
||
const deviceString = JSON.stringify(device);
|
||
const encryptedDevice = await encryptionService.encrypt(deviceString, pbkdf2Key);
|
||
console.log('🔐 Device encrypted successfully');
|
||
|
||
await new Promise<void>((resolve, reject) => {
|
||
const transaction = db.transaction([DATABASE_CONFIG.stores.wallet.name], 'readwrite');
|
||
const store = transaction.objectStore(DATABASE_CONFIG.stores.wallet.name);
|
||
console.log('🔍 Store opened:', store.name);
|
||
|
||
// Stocker UNIQUEMENT des données chiffrées - aucun wallet en clair
|
||
const walletObject = {
|
||
pre_id: '1',
|
||
encrypted_device: encryptedDevice, // Device complètement chiffré
|
||
encrypted_wallet: encryptedWallet, // Wallet chiffré
|
||
security_mode: currentMode // Mode de sécurité utilisé
|
||
};
|
||
|
||
console.log('🔍 Attempting to save encrypted wallet object');
|
||
console.log('🔐 Object contains only encrypted data:', {
|
||
hasEncryptedDevice: !!walletObject.encrypted_device,
|
||
hasEncryptedWallet: !!walletObject.encrypted_wallet,
|
||
securityMode: walletObject.security_mode,
|
||
// Ne pas logger le contenu chiffré
|
||
});
|
||
|
||
// Le store utilise des clés in-line (keyPath: 'pre_id'), ne pas fournir de clé explicite
|
||
const request = store.put(walletObject);
|
||
request.onsuccess = () => {
|
||
console.log('✅ Wallet saved in IndexedDB with correct format');
|
||
console.log('🔍 Saved wallet object:', walletObject);
|
||
resolve();
|
||
};
|
||
request.onerror = () => {
|
||
console.error('❌ Failed to save wallet in IndexedDB:', request.error);
|
||
reject(request.error);
|
||
};
|
||
|
||
transaction.oncomplete = () => {
|
||
console.log('✅ Transaction completed successfully');
|
||
console.log('🔍 Transaction completed for store:', store.name);
|
||
};
|
||
transaction.onerror = () => {
|
||
console.error('❌ Transaction failed:', transaction.error);
|
||
console.error('🔍 Transaction error details:', {
|
||
error: transaction.error,
|
||
store: store.name,
|
||
mode: transaction.mode
|
||
});
|
||
reject(transaction.error);
|
||
};
|
||
});
|
||
|
||
// Étape 2: Vérifier que le wallet est bien stocké (nouvelle transaction)
|
||
await new Promise<void>((resolve, reject) => {
|
||
const transaction = db.transaction([DATABASE_CONFIG.stores.wallet.name], 'readonly');
|
||
const store = transaction.objectStore(DATABASE_CONFIG.stores.wallet.name);
|
||
|
||
const verificationRequest = store.get('1');
|
||
verificationRequest.onsuccess = () => {
|
||
console.log('🔍 Verification result structure:', verificationRequest.result ? {
|
||
hasPreId: !!verificationRequest.result.pre_id,
|
||
hasEncryptedDevice: !!verificationRequest.result.encrypted_device,
|
||
hasEncryptedWallet: !!verificationRequest.result.encrypted_wallet,
|
||
hasSecurityMode: !!verificationRequest.result.security_mode,
|
||
hasDeviceInClear: !!verificationRequest.result.device // DEVRAIT ÊTRE NULL
|
||
} : 'null');
|
||
|
||
if (verificationRequest.result) {
|
||
console.log('✅ Wallet verification: Found in IndexedDB with key "1"');
|
||
|
||
// Vérifier qu'il n'y a AUCUN wallet en clair
|
||
if (verificationRequest.result.device) {
|
||
console.error('❌ CRITICAL: Device in clear found in database! This should be encrypted.');
|
||
reject(new Error('Security violation: wallet stored in clear'));
|
||
return;
|
||
}
|
||
|
||
// Vérifier que les données chiffrées sont présentes
|
||
if (!verificationRequest.result.encrypted_device || !verificationRequest.result.encrypted_wallet) {
|
||
console.error('❌ Wallet verification failed: encrypted data missing');
|
||
reject(new Error('Encrypted data missing'));
|
||
return;
|
||
}
|
||
|
||
console.log('✅ Wallet stored correctly: only encrypted data present');
|
||
resolve();
|
||
} else {
|
||
console.error('❌ Wallet verification: Not found in IndexedDB with key "1"');
|
||
reject(new Error('Wallet not found after save'));
|
||
}
|
||
};
|
||
verificationRequest.onerror = () => {
|
||
console.error('❌ Wallet verification failed:', verificationRequest.error);
|
||
reject(verificationRequest.error);
|
||
};
|
||
});
|
||
|
||
// Le mode de sécurité est déjà stocké via la clé PBKDF2 dans le store pbkdf2keys
|
||
// Pas besoin de le stocker séparément
|
||
console.log('🔐 Security mode is implicitly stored via PBKDF2 key:', currentMode);
|
||
|
||
// Vérification finale : s'assurer que le wallet est bien dans IndexedDB
|
||
console.log('🔍 Final verification: checking wallet in IndexedDB...');
|
||
const finalVerification = await new Promise<any>((resolve, reject) => {
|
||
const transaction = db.transaction([DATABASE_CONFIG.stores.wallet.name], 'readonly');
|
||
const store = transaction.objectStore(DATABASE_CONFIG.stores.wallet.name);
|
||
const request = store.get('1');
|
||
request.onsuccess = () => resolve(request.result);
|
||
request.onerror = () => reject(request.error);
|
||
});
|
||
|
||
if (finalVerification) {
|
||
// Vérifier qu'il n'y a PAS de device en clair
|
||
if (finalVerification.device) {
|
||
console.error('❌ CRITICAL: Final verification - Device in clear found!');
|
||
throw new Error('Security violation: wallet stored in clear');
|
||
}
|
||
|
||
console.log('✅ Wallet saved exclusively in IndexedDB and verified');
|
||
console.log('🔍 Wallet contains only encrypted data:', {
|
||
hasEncryptedDevice: !!finalVerification.encrypted_device,
|
||
hasEncryptedWallet: !!finalVerification.encrypted_wallet,
|
||
securityMode: finalVerification.security_mode,
|
||
hasDeviceInClear: !!finalVerification.device // DEVRAIT ÊTRE FALSE
|
||
});
|
||
|
||
// TEST: Déchiffrer le wallet pour valider que ça fonctionne
|
||
console.log('🔐 TEST: Attempting to decrypt wallet to validate encryption...');
|
||
try {
|
||
const pbkdf2KeyTest = await secureCredentialsService.retrievePBKDF2Key(currentMode as any);
|
||
if (!pbkdf2KeyTest) {
|
||
console.error('❌ TEST: Failed to retrieve PBKDF2 key for decryption test');
|
||
} else {
|
||
console.log('✅ TEST: PBKDF2 key retrieved for decryption test');
|
||
|
||
// Déchiffrer le wallet chiffré
|
||
const decryptedWallet = await encryptionService.decrypt(
|
||
finalVerification.encrypted_wallet,
|
||
pbkdf2KeyTest
|
||
);
|
||
const parsedWallet = JSON.parse(decryptedWallet);
|
||
console.log('✅ TEST: Wallet decrypted successfully:', {
|
||
hasScanSk: !!parsedWallet.scan_sk,
|
||
hasSpendKey: !!parsedWallet.spend_key,
|
||
network: parsedWallet.network,
|
||
state: parsedWallet.state,
|
||
created_at: parsedWallet.created_at
|
||
});
|
||
|
||
// Déchiffrer le device chiffré
|
||
const decryptedDevice = await encryptionService.decrypt(
|
||
finalVerification.encrypted_device,
|
||
pbkdf2KeyTest
|
||
);
|
||
const parsedDevice = JSON.parse(decryptedDevice);
|
||
console.log('✅ TEST: Device decrypted successfully:', {
|
||
hasSpWallet: !!parsedDevice.sp_wallet,
|
||
network: parsedDevice.network
|
||
});
|
||
|
||
console.log('✅ TEST: Full decryption test passed - wallet and device decrypt correctly');
|
||
}
|
||
} catch (decryptError) {
|
||
console.error('❌ TEST: Decryption test failed:', decryptError);
|
||
console.error('❌ This indicates an issue with encryption/decryption logic');
|
||
}
|
||
} else {
|
||
console.error('❌ Final wallet verification failed - wallet not found in IndexedDB');
|
||
throw new Error('Wallet verification failed - wallet not found');
|
||
}
|
||
|
||
} catch (error) {
|
||
console.error('❌ Error during wallet save:', error);
|
||
updateStatus('❌ Erreur: Échec de la sauvegarde du wallet', 'error');
|
||
throw error;
|
||
}
|
||
|
||
// Étape 4: Finalisation
|
||
updateStatus('✅ Wallet sauvegardé avec succès!', 'success');
|
||
updateProgress(100);
|
||
|
||
console.log('🎉 Wallet setup completed successfully - wallet saved with birthday_waiting state');
|
||
console.log('🔗 Ready to proceed to network connection and birthday setup');
|
||
|
||
// Activer le bouton continuer
|
||
continueBtn.disabled = false;
|
||
|
||
} catch (error) {
|
||
console.error('❌ Error during wallet setup:', error);
|
||
updateStatus('❌ Erreur lors de la génération du wallet', 'error');
|
||
}
|
||
|
||
// Gestion du bouton continuer
|
||
continueBtn.addEventListener('click', () => {
|
||
console.log('🔗 Redirecting to pairing page...');
|
||
window.location.href = '/src/pages/birthday-setup/birthday-setup.html';
|
||
});
|
||
}); |