NicolasCantu 653c7f32ca ci: docker_tag=dev-test
**Motivations :**
- Implémentation du système de sélection de mode de sécurité
- Séparation claire entre les données de sécurité et les données du wallet
- Suppression des duplications entre 'encrypted-pbkdf2-key' et 'pbkdf2-key'
- Architecture modulaire pour la gestion des credentials

**Modifications :**
- Ajout du composant security-mode-selector pour la sélection du mode de sécurité
- Création des pages séquentielles : security-setup, wallet-setup, birthday-setup
- Implémentation des services de credentials (encryption, storage, webauthn)
- Ajout du service security-mode pour la gestion des modes de sécurité
- Correction du stockage des clés PBKDF2 avec le securityMode dynamique
- Suppression des méthodes redondantes dans StorageService
- Nettoyage des appels redondants dans secure-credentials.service.ts

**Pages affectées :**
- src/components/security-mode-selector/ (nouveau composant)
- src/pages/security-setup/ (nouvelle page)
- src/pages/wallet-setup/ (nouvelle page)
- src/pages/birthday-setup/ (nouvelle page)
- src/services/credentials/ (nouveaux services)
- src/services/security-mode.service.ts (nouveau service)
- src/services/secure-credentials.service.ts (modifié)
- src/services/database.service.ts (modifié)
- src/router.ts (modifié)
- src/pages/home/home.ts (modifié)
2025-10-24 18:29:31 +02:00

1161 lines
43 KiB
TypeScript
Executable File

import Services from '../../services/service';
import { addSubscription } from '../../utils/subscription.utils';
import { displayEmojis, generateCreateBtn, addressToEmoji, prepareAndSendPairingTx } from '../../utils/sp-address.utils';
import { getCorrectDOM } from '../../utils/html.utils';
import { IframePairingComponent } from '../../components/iframe-pairing/iframe-pairing';
// Extend WindowEventMap to include custom events
declare global {
interface WindowEventMap {
'pairing-words-generated': CustomEvent;
'pairing-status-update': CustomEvent;
'pairing-success': CustomEvent;
'pairing-error': CustomEvent;
}
}
let isInitializing = false;
export async function initHomePage(): Promise<void> {
if (isInitializing) {
console.log('⚠️ Home page already initializing, skipping...');
return;
}
isInitializing = true;
console.log('INIT-HOME');
// No loading spinner - let the interface load naturally
// Initialize iframe pairing, content menu, and communication only if in iframe
if (window.parent !== window) {
initIframePairing();
initContentMenu();
initIframeCommunication();
}
// Set up iframe pairing button listeners
setupIframePairingButtons();
// Set up main pairing interface (avec protection contre les appels multiples)
if (!isMainPairingSetup) {
setupMainPairing();
}
// Set up account actions
setupAccountActions();
const container = getCorrectDOM('login-4nk-component') as HTMLElement;
container.querySelectorAll('.tab').forEach(tab => {
addSubscription(tab, 'click', () => {
container.querySelectorAll('.tab').forEach(t => t.classList.remove('active'));
tab.classList.add('active');
container
.querySelectorAll('.tab-content')
.forEach(content => content.classList.remove('active'));
container
.querySelector(`#${tab.getAttribute('data-tab') as string}`)
?.classList.add('active');
});
});
try {
console.log('🔧 Getting services instance...');
const service = await Services.getInstance();
// D'abord vérifier la sécurité avant de créer le wallet
console.log('🔐 Checking security configuration...');
const { SecureCredentialsService } = await import('../../services/secure-credentials.service');
const secureCredentialsService = SecureCredentialsService.getInstance();
const hasCredentials = await secureCredentialsService.hasCredentials();
if (!hasCredentials) {
console.log('🔐 No security credentials found, user must configure security first...');
// Afficher le sélecteur de mode de sécurité
await handleMainPairing();
return;
}
// Check if wallet exists, create if not
console.log('🔍 Checking for existing wallet...');
const existingDevice = await service.getDeviceFromDatabase();
if (!existingDevice) {
console.log('📱 No wallet found, creating new device...');
const spAddress = await service.createNewDevice();
console.log('✅ New device created with address:', spAddress);
// Verify wallet was created successfully
const verifyDevice = await service.getDeviceFromDatabase();
if (!verifyDevice) {
throw new Error('Failed to create wallet - device not found after creation');
}
console.log('✅ Wallet creation verified');
} else {
console.log('📱 Existing wallet found');
console.log('🔍 Wallet details:', {
hasSpendKey: !!existingDevice.sp_wallet?.spend_key,
hasScanKey: !!existingDevice.sp_wallet?.scan_key,
birthday: existingDevice.sp_wallet?.birthday
});
}
// Trigger WebAuthn authentication
console.log('🔐 Triggering WebAuthn authentication...');
await handleMainPairing();
// Attendre que les credentials soient réellement disponibles avant de continuer
console.log('⏳ Waiting for credentials to be fully available...');
await waitForCredentialsAvailability();
console.log('✅ Credentials confirmed as available, proceeding...');
// After WebAuthn, get device address and setup UI
console.log('🔧 Getting device address...');
try {
const spAddress = await service.getDeviceAddress();
console.log('🔧 Generating create button...');
generateCreateBtn();
console.log('🔧 Displaying emojis...');
displayEmojis(spAddress);
} catch (error) {
console.error('❌ Failed to get device address:', error);
if (error.message.includes('Wallet keys not available')) {
console.error('❌ Wallet keys not available - authentication failed');
throw new Error('Authentication failed - wallet keys not available');
}
throw error;
}
console.log('✅ Home page initialization completed');
} catch (error) {
console.error('❌ Error initializing home page:', error);
throw error;
} finally {
isInitializing = false;
}
}
// Initialize iframe pairing component
let iframePairing: IframePairingComponent | null = null;
export function initIframePairing() {
if (!iframePairing) {
iframePairing = new IframePairingComponent();
iframePairing.createHiddenIframe();
// Listen for pairing events
window.addEventListener('pairing-words-generated', (event: Event) => {
const customEvent = event as CustomEvent;
console.log('✅ 4 words generated via iframe:', customEvent.detail.words);
// Update the UI with the generated words
const creatorWordsElement = document.querySelector('#creator-words');
if (creatorWordsElement) {
creatorWordsElement.textContent = customEvent.detail.words;
creatorWordsElement.className = 'words-content active';
}
// Send message to parent
if (window.parent !== window) {
window.parent.postMessage(
{
type: 'PAIRING_4WORDS_WORDS_GENERATED',
data: { words: customEvent.detail.words },
},
'*'
);
}
});
window.addEventListener('pairing-status-update', (event: Event) => {
const customEvent = event as CustomEvent;
console.log('📊 Pairing status update:', customEvent.detail.status);
// Update status indicators
const statusElement = document.querySelector(`#${customEvent.detail.type}-status span`);
if (statusElement) {
statusElement.textContent = customEvent.detail.status;
}
// Send message to parent
if (window.parent !== window) {
window.parent.postMessage(
{
type: 'PAIRING_4WORDS_STATUS_UPDATE',
data: { status: customEvent.detail.status, type: customEvent.detail.type },
},
'*'
);
}
});
window.addEventListener('pairing-success', (event: Event) => {
const customEvent = event as CustomEvent;
console.log('✅ Pairing successful:', customEvent.detail.message);
// Send message to parent
if (window.parent !== window) {
window.parent.postMessage(
{
type: 'PAIRING_4WORDS_SUCCESS',
data: { message: customEvent.detail.message },
},
'*'
);
}
// Handle successful pairing
setTimeout(() => {
window.location.href = '/account';
}, 2000);
});
window.addEventListener('pairing-error', (event: Event) => {
const customEvent = event as CustomEvent;
console.error('❌ Pairing error:', customEvent.detail.error);
// Send message to parent
if (window.parent !== window) {
window.parent.postMessage(
{
type: 'PAIRING_4WORDS_ERROR',
data: { error: customEvent.detail.error },
},
'*'
);
}
// Handle pairing error
alert(`Pairing error: ${customEvent.detail.error}`);
});
}
}
// Initialize content menu (only in iframe mode)
export function initContentMenu() {
// Only add menu buttons if we're in an iframe
if (window.parent !== window) {
// Add iframe mode class to body
document.body.classList.add('iframe-mode');
// Add menu buttons to title container
const titleContainer = document.querySelector('.title-container');
if (titleContainer) {
const menuHtml = `
<div class="content-menu">
<button class="menu-btn active" data-page="home">🏠 Home</button>
<button class="menu-btn" data-page="account">👤 Account</button>
<button class="menu-btn" data-page="settings">⚙️ Settings</button>
<button class="menu-btn" data-page="help">❓ Help</button>
</div>
`;
titleContainer.insertAdjacentHTML('beforeend', menuHtml);
}
const menuButtons = document.querySelectorAll('.menu-btn');
menuButtons.forEach(button => {
button.addEventListener('click', () => {
// Remove active class from all buttons
menuButtons.forEach(btn => btn.classList.remove('active'));
// Add active class to clicked button
button.classList.add('active');
const page = button.getAttribute('data-page');
console.log(`Menu clicked: ${page}`);
// Send message to parent window
window.parent.postMessage(
{
type: 'MENU_NAVIGATION',
data: { page },
},
'*'
);
});
});
}
}
// Initialize iframe communication
export function initIframeCommunication() {
// Listen for messages from parent window
window.addEventListener('message', event => {
// Filter out browser extension messages first
if (
event.data.source === 'react-devtools-content-script' ||
event.data.hello === true ||
!event.data.type ||
event.data.type.startsWith('Pass::') ||
event.data.type === 'PassClientScriptReady'
) {
return; // Ignore browser extension messages
}
// Security check - in production, verify event.origin
console.log('📨 Received message from parent:', event.data);
const { type, data } = event.data;
switch (type) {
case 'TEST_MESSAGE':
console.log('🧪 Test message received:', data.message);
// Send response back to parent
if (window.parent !== window) {
window.parent.postMessage(
{
type: 'TEST_RESPONSE',
data: { response: 'Hello from 4NK iframe!' },
},
'*'
);
}
break;
case 'PAIRING_4WORDS_CREATE':
console.log('🔐 Parent requested pairing creation');
createPairingViaIframe();
break;
case 'PAIRING_4WORDS_JOIN':
console.log('🔗 Parent requested pairing join with words:', data.words);
joinPairingViaIframe(data.words);
break;
case 'LISTENING':
console.log('👂 Parent is listening for messages');
break;
case 'IFRAME_READY':
console.log('✅ Iframe is ready and initialized');
break;
default:
console.log('❓ Unknown message type from parent:', type);
}
});
// Notify parent that iframe is ready
if (window.parent !== window) {
window.parent.postMessage(
{
type: 'IFRAME_READY',
data: { service: '4nk-pairing' },
},
'*'
);
console.log('📡 Notified parent that iframe is ready');
}
}
// Enhanced pairing functions using iframe
export async function createPairingViaIframe() {
if (!iframePairing) {
initIframePairing();
}
try {
await iframePairing!.createPairing();
} catch (error) {
console.error('Error creating pairing via iframe:', error);
alert(`Error creating pairing: ${(error as Error).message}`);
}
}
export async function joinPairingViaIframe(words: string) {
if (!iframePairing) {
initIframePairing();
}
try {
await iframePairing!.joinPairing(words);
} catch (error) {
console.error('Error joining pairing via iframe:', error);
alert(`Error joining pairing: ${(error as Error).message}`);
}
}
// Set up button listeners for iframe pairing
export function setupIframePairingButtons() {
// Create button listener
const createButton = document.getElementById('createButton');
if (createButton) {
createButton.addEventListener('click', async () => {
console.log('🔐 Create button clicked - using iframe pairing');
await createPairingViaIframe();
});
}
// Join button listener
const joinButton = document.getElementById('joinButton');
const wordsInput = document.getElementById('wordsInput') as HTMLInputElement;
if (joinButton && wordsInput) {
// Enable join button when words are entered
wordsInput.addEventListener('input', () => {
const words = wordsInput.value.trim();
(joinButton as HTMLButtonElement).disabled = !words;
});
joinButton.addEventListener('click', async () => {
const words = wordsInput.value.trim();
if (words) {
console.log('🔗 Join button clicked - using iframe pairing with words:', words);
await joinPairingViaIframe(words);
}
});
}
// Copy words button listener
const copyWordsBtn = document.getElementById('copyWordsBtn');
if (copyWordsBtn) {
copyWordsBtn.addEventListener('click', () => {
const creatorWordsElement = document.querySelector('#creator-words');
if (creatorWordsElement && creatorWordsElement.textContent) {
navigator.clipboard
.writeText(creatorWordsElement.textContent)
.then(() => {
console.log('✅ Words copied to clipboard');
// Show feedback
const originalText = copyWordsBtn.textContent;
copyWordsBtn.textContent = '✅ Copied!';
setTimeout(() => {
copyWordsBtn.textContent = originalText;
}, 2000);
})
.catch(err => {
console.error('Failed to copy words:', err);
});
}
});
}
}
// Variable pour éviter les appels multiples à setupMainPairing
let isMainPairingSetup = false;
// Main Pairing Interface - Automatic WebAuthn trigger
export function setupMainPairing(): void {
// Protection contre les appels multiples
if (isMainPairingSetup) {
console.log('🔐 Main pairing already setup, skipping...');
return;
}
isMainPairingSetup = true;
const container = getCorrectDOM('login-4nk-component') as HTMLElement;
const mainStatus = container.querySelector('#main-status') as HTMLElement;
if (mainStatus) {
mainStatus.innerHTML = '<span style="color: var(--info-color)">⏳ Waiting for user to validate secure key access...</span>';
}
console.log('🔐 Main pairing setup - waiting for user interaction');
}
function setupUserInteractionListener(): void {
let hasTriggered = false;
const triggerWebAuthn = async (event: Event) => {
if (hasTriggered) return;
hasTriggered = true;
console.log('🔐 User interaction detected:', event.type, 'triggering WebAuthn...');
await handleMainPairing();
};
// Listen for any user interaction with more specific events
document.addEventListener('click', triggerWebAuthn, { once: true, passive: true });
document.addEventListener('keydown', triggerWebAuthn, { once: true, passive: true });
document.addEventListener('touchstart', triggerWebAuthn, { once: true, passive: true });
document.addEventListener('mousedown', triggerWebAuthn, { once: true, passive: true });
console.log('🔐 User interaction listeners set up');
}
/**
* Affiche le sélecteur de mode de sécurisation
*/
async function showSecurityModeSelector(): Promise<void> {
return new Promise((resolve) => {
// Créer le conteneur pour le sélecteur
const selectorContainer = document.createElement('div');
selectorContainer.id = 'security-mode-selector-container';
selectorContainer.style.cssText = `
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 100%;
background: rgba(0, 0, 0, 0.8);
display: flex;
justify-content: center;
align-items: center;
z-index: 10000;
backdrop-filter: blur(5px);
`;
// Créer le contenu du sélecteur
const selectorContent = document.createElement('div');
selectorContent.style.cssText = `
background: white;
border-radius: 12px;
padding: 30px;
max-width: 600px;
width: 90%;
max-height: 80vh;
overflow-y: auto;
box-shadow: 0 8px 32px rgba(0, 0, 0, 0.3);
`;
selectorContent.innerHTML = `
<div style="text-align: center; margin-bottom: 30px;">
<h2 style="color: #2c3e50; margin-bottom: 10px;">🔐 Mode de Sécurisation</h2>
<p style="color: #7f8c8d;">Choisissez comment vous souhaitez sécuriser vos clés privées :</p>
</div>
<div style="display: grid; gap: 15px; margin-bottom: 30px;">
<div class="security-option" data-mode="proton-pass" style="border: 2px solid #e1e8ed; border-radius: 8px; padding: 15px; cursor: pointer; transition: all 0.3s ease;">
<div style="display: flex; align-items: center; margin-bottom: 8px;">
<span style="font-size: 20px; margin-right: 10px;">🔒</span>
<span style="font-weight: 600; color: #2c3e50;">Proton Pass</span>
<span style="background: #d4edda; color: #155724; padding: 2px 8px; border-radius: 12px; font-size: 12px; font-weight: 600; margin-left: auto;">Sécurisé</span>
</div>
<div style="color: #5a6c7d; font-size: 14px;">Utilise Proton Pass pour l'authentification biométrique</div>
</div>
<div class="security-option" data-mode="os" style="border: 2px solid #e1e8ed; border-radius: 8px; padding: 15px; cursor: pointer; transition: all 0.3s ease;">
<div style="display: flex; align-items: center; margin-bottom: 8px;">
<span style="font-size: 20px; margin-right: 10px;">🖥️</span>
<span style="font-weight: 600; color: #2c3e50;">Authentificateur OS</span>
<span style="background: #d4edda; color: #155724; padding: 2px 8px; border-radius: 12px; font-size: 12px; font-weight: 600; margin-left: auto;">Sécurisé</span>
</div>
<div style="color: #5a6c7d; font-size: 14px;">Utilise l'authentificateur intégré de votre système</div>
</div>
<div class="security-option" data-mode="browser" style="border: 2px solid #e1e8ed; border-radius: 8px; padding: 15px; cursor: pointer; transition: all 0.3s ease;">
<div style="display: flex; align-items: center; margin-bottom: 8px;">
<span style="font-size: 20px; margin-right: 10px;">🌐</span>
<span style="font-weight: 600; color: #2c3e50;">Navigateur</span>
<span style="background: #fff3cd; color: #856404; padding: 2px 8px; border-radius: 12px; font-size: 12px; font-weight: 600; margin-left: auto;">Moyennement sécurisé</span>
</div>
<div style="color: #5a6c7d; font-size: 14px;">Utilise les fonctionnalités de sécurité du navigateur</div>
</div>
<div class="security-option" data-mode="2fa" style="border: 2px solid #e1e8ed; border-radius: 8px; padding: 15px; cursor: pointer; transition: all 0.3s ease;">
<div style="display: flex; align-items: center; margin-bottom: 8px;">
<span style="font-size: 20px; margin-right: 10px;">📱</span>
<span style="font-weight: 600; color: #2c3e50;">Application 2FA</span>
<span style="background: #f8d7da; color: #721c24; padding: 2px 8px; border-radius: 12px; font-size: 12px; font-weight: 600; margin-left: auto;">⚠️ Non sécurisé</span>
</div>
<div style="color: #5a6c7d; font-size: 14px;">Stockage en clair avec authentification 2FA</div>
</div>
<div class="security-option" data-mode="password" style="border: 2px solid #e1e8ed; border-radius: 8px; padding: 15px; cursor: pointer; transition: all 0.3s ease;">
<div style="display: flex; align-items: center; margin-bottom: 8px;">
<span style="font-size: 20px; margin-right: 10px;">🔑</span>
<span style="font-weight: 600; color: #2c3e50;">Mot de Passe</span>
<span style="background: #fff3cd; color: #856404; padding: 2px 8px; border-radius: 12px; font-size: 12px; font-weight: 600; margin-left: auto;">⚠️ Non sauvegardé</span>
</div>
<div style="color: #5a6c7d; font-size: 14px;">Chiffrement par mot de passe (non sauvegardé, non récupérable)</div>
</div>
<div class="security-option" data-mode="none" style="border: 2px solid #e1e8ed; border-radius: 8px; padding: 15px; cursor: pointer; transition: all 0.3s ease;">
<div style="display: flex; align-items: center; margin-bottom: 8px;">
<span style="font-size: 20px; margin-right: 10px;">🚨</span>
<span style="font-weight: 600; color: #2c3e50;">Aucune Sécurité</span>
<span style="background: #f5c6cb; color: #721c24; padding: 2px 8px; border-radius: 12px; font-size: 12px; font-weight: 600; margin-left: auto;">DANGEREUX</span>
</div>
<div style="color: #5a6c7d; font-size: 14px;">Stockage en clair sans aucune protection</div>
</div>
</div>
<div style="text-align: center;">
<button id="confirm-security-mode" style="background: #27ae60; color: white; border: none; padding: 12px 24px; border-radius: 6px; cursor: pointer; font-size: 16px; margin: 5px; opacity: 0.5;" disabled>
Confirmer le Mode de Sécurisation
</button>
<button id="cancel-security-mode" style="background: #95a5a6; color: white; border: none; padding: 12px 24px; border-radius: 6px; cursor: pointer; font-size: 16px; margin: 5px;">
Annuler
</button>
</div>
`;
selectorContainer.appendChild(selectorContent);
document.body.appendChild(selectorContainer);
let selectedMode: string | null = null;
// Gestion des événements
const options = selectorContent.querySelectorAll('.security-option');
const confirmBtn = selectorContent.querySelector('#confirm-security-mode') as HTMLButtonElement;
const cancelBtn = selectorContent.querySelector('#cancel-security-mode') as HTMLButtonElement;
// Sélection d'un mode
options.forEach(option => {
option.addEventListener('click', () => {
options.forEach(opt => opt.style.borderColor = '#e1e8ed');
option.style.borderColor = '#27ae60';
option.style.background = '#f8fff8';
selectedMode = option.getAttribute('data-mode');
confirmBtn.disabled = false;
confirmBtn.style.opacity = '1';
});
// Effet hover
option.addEventListener('mouseenter', () => {
if (option.style.borderColor !== '#27ae60') {
option.style.borderColor = '#3498db';
}
});
option.addEventListener('mouseleave', () => {
if (option.style.borderColor !== '#27ae60') {
option.style.borderColor = '#e1e8ed';
}
});
});
// Confirmation
confirmBtn.addEventListener('click', async () => {
if (selectedMode) {
console.log(`🔐 Security mode selected: ${selectedMode}`);
// Vérifier si le mode nécessite une confirmation
const { SecurityModeService } = await import('../../services/security-mode.service');
const securityModeService = SecurityModeService.getInstance();
const modeConfig = securityModeService.getSecurityModeConfig(selectedMode as any);
if (modeConfig.requiresConfirmation) {
// Afficher une alerte de sécurité
const confirmed = await showSecurityWarning(modeConfig);
if (!confirmed) {
return; // L'utilisateur a annulé
}
}
// Définir le mode de sécurisation
await securityModeService.setSecurityMode(selectedMode as any);
// Fermer le sélecteur
document.body.removeChild(selectorContainer);
// Réinitialiser les flags pour permettre la relance
isPairingInProgress = false;
pairingAttempts = 0;
// Relancer l'authentification avec le mode sélectionné
await handleMainPairing();
resolve();
}
});
// Annulation
cancelBtn.addEventListener('click', () => {
console.log('❌ Security mode selection cancelled');
document.body.removeChild(selectorContainer);
resolve();
});
// Fermer avec Escape
const handleEscape = (e: KeyboardEvent) => {
if (e.key === 'Escape') {
document.body.removeChild(selectorContainer);
document.removeEventListener('keydown', handleEscape);
resolve();
}
};
document.addEventListener('keydown', handleEscape);
});
}
/**
* Affiche une alerte de sécurité pour les modes non recommandés
*/
async function showSecurityWarning(modeConfig: any): Promise<boolean> {
return new Promise((resolve) => {
const warningContainer = document.createElement('div');
warningContainer.style.cssText = `
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 100%;
background: rgba(0, 0, 0, 0.8);
display: flex;
justify-content: center;
align-items: center;
z-index: 10001;
backdrop-filter: blur(5px);
`;
const warningContent = document.createElement('div');
warningContent.style.cssText = `
background: white;
border-radius: 12px;
padding: 30px;
max-width: 500px;
width: 90%;
box-shadow: 0 8px 32px rgba(0, 0, 0, 0.3);
`;
warningContent.innerHTML = `
<div style="text-align: center; margin-bottom: 20px;">
<h3 style="color: #e74c3c; margin-bottom: 10px;">⚠️ Attention - Mode de Sécurisation Non Recommandé</h3>
</div>
<div style="margin-bottom: 20px;">
<h4>Vous avez choisi : <strong>${modeConfig.name}</strong></h4>
<p>${modeConfig.description}</p>
</div>
<div style="background: #f8d7da; padding: 15px; border-radius: 8px; border-left: 4px solid #dc3545; margin-bottom: 20px;">
<h5 style="color: #721c24; margin-top: 0;">⚠️ Risques identifiés :</h5>
<ul style="color: #721c24; margin-bottom: 0;">
${modeConfig.warnings.map((warning: string) => `<li>${warning}</li>`).join('')}
</ul>
</div>
<div style="background: #fff3cd; padding: 15px; border-radius: 8px; border-left: 4px solid #ffc107; margin-bottom: 20px;">
<p style="color: #856404; margin: 0;">
<strong>Recommandation :</strong>
${modeConfig.securityLevel === 'low'
? 'Nous vous recommandons fortement de choisir un mode plus sécurisé comme Proton Pass ou l\'authentificateur OS.'
: 'Ce mode présente des risques de sécurité élevés. Assurez-vous de comprendre les implications.'
}
</p>
</div>
<div style="background: #f8f9fa; padding: 15px; border-radius: 8px; margin-bottom: 20px;">
<label style="display: flex; align-items: center; cursor: pointer; font-weight: 500;">
<input type="checkbox" id="understand-risks" style="margin-right: 10px; transform: scale(1.2);">
Je comprends les risques et souhaite continuer
</label>
</div>
<div style="text-align: center;">
<button id="confirm-risky-mode" style="background: #e74c3c; color: white; border: none; padding: 12px 24px; border-radius: 6px; cursor: pointer; font-size: 16px; margin: 5px; opacity: 0.5;" disabled>
Continuer Malgré les Risques
</button>
<button id="cancel-risky-mode" style="background: #95a5a6; color: white; border: none; padding: 12px 24px; border-radius: 6px; cursor: pointer; font-size: 16px; margin: 5px;">
Choisir un Autre Mode
</button>
</div>
`;
warningContainer.appendChild(warningContent);
document.body.appendChild(warningContainer);
const understandCheckbox = warningContent.querySelector('#understand-risks') as HTMLInputElement;
const confirmBtn = warningContent.querySelector('#confirm-risky-mode') as HTMLButtonElement;
const cancelBtn = warningContent.querySelector('#cancel-risky-mode') as HTMLButtonElement;
// Gestion de la checkbox
understandCheckbox.addEventListener('change', () => {
confirmBtn.disabled = !understandCheckbox.checked;
confirmBtn.style.opacity = understandCheckbox.checked ? '1' : '0.5';
});
// Confirmation
confirmBtn.addEventListener('click', () => {
document.body.removeChild(warningContainer);
resolve(true);
});
// Annulation
cancelBtn.addEventListener('click', () => {
document.body.removeChild(warningContainer);
resolve(false);
});
// Fermer avec Escape
const handleEscape = (e: KeyboardEvent) => {
if (e.key === 'Escape') {
document.body.removeChild(warningContainer);
document.removeEventListener('keydown', handleEscape);
resolve(false);
}
};
document.addEventListener('keydown', handleEscape);
});
}
/**
* Attend que les credentials soient réellement disponibles
*/
// Variable pour éviter les appels multiples à waitForCredentialsAvailability
let isWaitingForCredentials = false;
async function waitForCredentialsAvailability(): Promise<void> {
// Protection contre les appels multiples
if (isWaitingForCredentials) {
console.log('🔍 Already waiting for credentials, skipping...');
return;
}
isWaitingForCredentials = true;
try {
const { secureCredentialsService } = await import('../../services/secure-credentials.service');
let attempts = 0;
const maxAttempts = 20;
const delayMs = 1000;
while (attempts < maxAttempts) {
attempts++;
console.log(`🔍 Vérification de la disponibilité des credentials (tentative ${attempts}/${maxAttempts})...`);
try {
// Vérifier que les credentials sont réellement disponibles et accessibles
const credentials = await secureCredentialsService.retrieveCredentials('');
if (credentials && credentials.spendKey && credentials.scanKey) {
console.log('✅ Credentials confirmés comme disponibles');
return;
} else {
throw new Error('Credentials incomplets');
}
} catch (error) {
console.warn(`⚠️ Credentials pas encore disponibles (tentative ${attempts}):`, error);
if (attempts < maxAttempts) {
console.log(`⏳ Attente de ${delayMs}ms avant la prochaine tentative...`);
await new Promise(resolve => setTimeout(resolve, delayMs));
}
}
}
throw new Error('Credentials non disponibles après toutes les tentatives');
} finally {
isWaitingForCredentials = false;
}
}
// Variables pour éviter les appels multiples
let isPairingInProgress = false;
let pairingAttempts = 0;
const MAX_PAIRING_ATTEMPTS = 1;
async function handleMainPairing(): Promise<void> {
// Protection renforcée contre les appels multiples
if (isPairingInProgress) {
console.log('🔐 Pairing already in progress, skipping...');
return;
}
if (pairingAttempts >= MAX_PAIRING_ATTEMPTS) {
console.log('🔐 Maximum pairing attempts reached, skipping...');
return;
}
isPairingInProgress = true;
pairingAttempts++;
const container = getCorrectDOM('login-4nk-component') as HTMLElement;
const mainStatus = container.querySelector('#main-status') as HTMLElement;
try {
// Vérifier si un mode de sécurisation est déjà sélectionné
const { SecurityModeService } = await import('../../services/security-mode.service');
const securityModeService = SecurityModeService.getInstance();
let currentMode: string | null = null;
try {
currentMode = await securityModeService.getCurrentMode();
} catch (error) {
// Ignorer les erreurs de base de données lors du premier lancement
console.log('🔐 No security mode configured yet (first launch)');
currentMode = null;
}
if (!currentMode) {
// Aucun mode sélectionné, afficher le sélecteur
console.log('🔐 No security mode selected, showing selector...');
if (mainStatus) {
mainStatus.innerHTML = '<div class="spinner"></div><span>🔐 Please select your security mode...</span>';
}
// Réinitialiser le flag avant d'afficher le sélecteur
isPairingInProgress = false;
await showSecurityModeSelector();
return; // La fonction sera rappelée après sélection du mode
}
// Mode sélectionné, continuer avec l'authentification
console.log(`🔐 Using security mode: ${currentMode}`);
if (mainStatus) {
mainStatus.innerHTML = '<div class="spinner"></div><span>Authenticating with selected security mode...</span>';
}
// Import and trigger authentication with selected mode
const { secureCredentialsService } = await import('../../services/secure-credentials.service');
// Check if we have existing credentials (regardless of wallet existence)
console.log('🔍 Checking for existing credentials...');
const hasCredentials = await secureCredentialsService.hasCredentials();
if (hasCredentials) {
console.log('🔓 Existing credentials found, decrypting...');
if (mainStatus) {
mainStatus.innerHTML = '<div class="spinner"></div><span>Decrypting existing credentials...</span>';
}
try {
// This will trigger authentication for decryption of existing credentials
console.log('🔐 Starting credentials decryption process...');
const decryptedCredentials = await secureCredentialsService.retrieveCredentials('');
if (!decryptedCredentials) {
throw new Error('Failed to decrypt existing credentials - no data returned');
}
console.log('✅ Credentials decryption completed successfully');
if (mainStatus) {
mainStatus.innerHTML = '<span style="color: var(--success-color)">✅ Credentials decrypted successfully</span>';
}
} catch (error) {
console.error('❌ Credentials decryption failed:', error);
if (mainStatus) {
mainStatus.innerHTML = '<span style="color: var(--error-color)">❌ Failed to decrypt credentials. Please try again.</span>';
}
throw error; // Arrêter le processus si le déchiffrement échoue
}
} else {
console.log('🔐 No existing credentials, creating new ones...');
if (mainStatus) {
mainStatus.innerHTML = '<div class="spinner"></div><span>🔐 Setting up secure authentication...</span>';
}
try {
// This will trigger authentication for creation of new credentials
console.log('🔐 Starting credentials creation process...');
if (mainStatus) {
mainStatus.innerHTML = '<div class="spinner"></div><span>🔐 Creating secure credentials with your device...</span>';
}
const credentialData = await secureCredentialsService.generateSecureCredentials('4nk-secure-password');
if (!credentialData || !credentialData.spendKey || !credentialData.scanKey) {
throw new Error('Failed to generate valid credentials - missing spendKey or scanKey');
}
console.log('✅ Credentials creation completed successfully');
// Store the credentials in IndexedDB
console.log('💾 Storing credentials in IndexedDB...');
if (mainStatus) {
mainStatus.innerHTML = '<div class="spinner"></div><span>💾 Securing credentials...</span>';
}
await secureCredentialsService.storeCredentials(credentialData, '');
console.log('✅ Credentials stored successfully');
// Decrypt and make keys available to SDK
console.log('🔓 Decrypting credentials for SDK access...');
if (mainStatus) {
mainStatus.innerHTML = '<div class="spinner"></div><span>🔓 Making keys available...</span>';
}
const retrievedCredentials = await secureCredentialsService.retrieveCredentials('');
if (!retrievedCredentials) {
throw new Error('Failed to retrieve stored credentials - decryption failed');
}
console.log('✅ Credentials decrypted and available');
if (mainStatus) {
mainStatus.innerHTML = '<span style="color: var(--success-color)">✅ Secure authentication ready</span>';
}
} catch (error) {
console.error('❌ Credentials creation/encryption/decryption failed:', error);
if (mainStatus) {
mainStatus.innerHTML = '<span style="color: var(--error-color)">❌ Failed to create/encrypt/decrypt credentials. Please try again.</span>';
}
throw error; // Arrêter le processus si la génération/chiffrement/déchiffrement échoue
}
}
// Ensure WebAuthn process is completely finished
console.log('🔐 WebAuthn process completed, waiting for final confirmation...');
await new Promise(resolve => setTimeout(resolve, 1000)); // Additional wait to ensure completion
// Wait longer to ensure credentials are fully processed and stored
console.log('⏳ Waiting for credentials to be fully processed...');
await new Promise(resolve => setTimeout(resolve, 5000)); // Increased wait time to 5 seconds
// Verify credentials are available before proceeding with retry mechanism
let credentialsReady = false;
let attempts = 0;
const maxAttempts = 10; // Increased attempts
const delayMs = 2000; // Increased delay between attempts
while (!credentialsReady && attempts < maxAttempts) {
attempts++;
console.log(`🔍 Checking credentials availability (attempt ${attempts}/${maxAttempts})...`);
try {
// Vérifier que les credentials sont réellement disponibles
const credentials = await secureCredentialsService.retrieveCredentials('');
if (!credentials || !credentials.spendKey || !credentials.scanKey) {
throw new Error('Credentials not properly available');
}
credentialsReady = true;
console.log('✅ Credentials verified as available');
} catch (error) {
console.warn(`⚠️ Credentials not ready on attempt ${attempts}:`, error);
if (attempts < maxAttempts) {
console.log(`⏳ Waiting ${delayMs}ms before next attempt...`);
await new Promise(resolve => setTimeout(resolve, delayMs));
}
}
}
// Si les credentials ne sont toujours pas disponibles après tous les essais, arrêter le processus
if (!credentialsReady) {
console.error('❌ Credentials not available after all attempts - stopping process');
if (mainStatus) {
mainStatus.innerHTML = '<span style="color: var(--error-color)">❌ Authentication failed - credentials not available</span>';
}
throw new Error('Credentials not available after maximum retry attempts');
}
console.log('✅ Credentials verified, proceeding with pairing...');
// Now proceed with pairing process
console.log('🚀 Starting pairing process...');
if (mainStatus) {
mainStatus.innerHTML = '<div class="spinner"></div><span>🚀 Starting secure pairing process...</span>';
}
// Attendre que les credentials soient réellement disponibles avant de continuer
await waitForCredentialsAvailability();
await prepareAndSendPairingTx();
} catch (error) {
// If WebAuthn fails due to no user gesture, wait for real interaction
if (error instanceof Error && error.message && error.message.includes('WebAuthn authentication was cancelled or timed out')) {
console.log('🔐 WebAuthn requires user interaction, waiting...');
if (mainStatus) {
mainStatus.innerHTML = '<span style="color: var(--info-color)">⏳ Waiting for user to validate secure key access...</span>';
}
// Set up listener for real user interaction
setupUserInteractionListener();
} else {
console.error('Pairing failed:', error);
if (mainStatus) {
mainStatus.innerHTML = '<span style="color: var(--error-color)">❌ Pairing failed: ' + (error as Error).message + '</span>';
}
throw error;
}
} finally {
// Réinitialiser les flags pour permettre de nouveaux appels
isPairingInProgress = false;
// Ne pas réinitialiser pairingAttempts ici pour éviter les boucles infinies
}
}
// Account Actions
export function setupAccountActions(): void {
const container = getCorrectDOM('login-4nk-component') as HTMLElement;
const deleteAccountButton = container.querySelector('#deleteAccountButton') as HTMLButtonElement;
if (deleteAccountButton) {
deleteAccountButton.addEventListener('click', async () => {
await handleDeleteAccount();
});
}
}
async function handleDeleteAccount(): Promise<void> {
const container = getCorrectDOM('login-4nk-component') as HTMLElement;
const mainStatus = container.querySelector('#main-status') as HTMLElement;
// Confirmation dialog
const confirmed = confirm(
'⚠️ WARNING: This will permanently delete your account and all associated data.\n\n' +
'This action cannot be undone!\n\n' +
'Are you sure you want to delete your account?'
);
if (!confirmed) {
return;
}
// Double confirmation
const doubleConfirmed = confirm(
'🚨 FINAL WARNING: You are about to permanently delete your account.\n\n' +
'All your data, credentials, and pairings will be lost forever.\n\n' +
'Type "DELETE" to confirm (case sensitive):'
);
if (doubleConfirmed) {
const userInput = prompt('Type "DELETE" to confirm account deletion:');
if (userInput !== 'DELETE') {
if (mainStatus) {
mainStatus.innerHTML = '<span style="color: var(--warning-color)">❌ Account deletion cancelled - confirmation text did not match</span>';
}
return;
}
} else {
return;
}
try {
if (mainStatus) {
mainStatus.innerHTML = '<div class="spinner"></div><span>Deleting account and all data...</span>';
}
// Get services
const service = await Services.getInstance();
const { secureCredentialsService } = await import('../../services/secure-credentials.service');
// Delete all credentials
await secureCredentialsService.deleteCredentials();
// Clear all local storage
localStorage.clear();
sessionStorage.clear();
// Clear IndexedDB
if ('indexedDB' in window) {
const databases = await indexedDB.databases();
for (const db of databases) {
if (db.name) {
indexedDB.deleteDatabase(db.name);
}
}
}
// Clear service worker caches
if ('caches' in window) {
const cacheNames = await caches.keys();
await Promise.all(
cacheNames.map(cacheName => caches.delete(cacheName))
);
}
if (mainStatus) {
mainStatus.innerHTML = '<span style="color: var(--success-color)">✅ Account and all data deleted successfully</span>';
}
// Reload the page to start fresh
setTimeout(() => {
window.location.reload();
}, 2000);
} catch (error) {
console.error('Account deletion failed:', error);
if (mainStatus) {
mainStatus.innerHTML = '<span style="color: var(--error-color)">❌ Failed to delete account</span>';
}
}
}