diff --git a/nginx.dev.conf b/nginx.dev.conf
index c125de3..b9317f4 100644
--- a/nginx.dev.conf
+++ b/nginx.dev.conf
@@ -4,7 +4,7 @@ server {
# Redirection des requĂȘtes HTTP vers Vite
location / {
- proxy_pass http://localhost:3003;
+ proxy_pass http://localhost:3004;
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection "Upgrade";
@@ -45,4 +45,4 @@ server {
add_header Access-Control-Allow-Methods "GET, POST, OPTIONS, PUT, DELETE" always;
add_header Access-Control-Allow-Headers "Authorization,Content-Type,Accept,X-Requested-With" always;
}
-}
\ No newline at end of file
+}
\ No newline at end of file
diff --git a/src/components/security-mode-selector/security-mode-selector.css b/src/components/security-mode-selector/security-mode-selector.css
new file mode 100644
index 0000000..51aca60
--- /dev/null
+++ b/src/components/security-mode-selector/security-mode-selector.css
@@ -0,0 +1,297 @@
+.security-mode-selector {
+ max-width: 800px;
+ margin: 0 auto;
+ padding: 20px;
+ font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
+}
+
+.security-mode-header {
+ text-align: center;
+ margin-bottom: 30px;
+}
+
+.security-mode-header h2 {
+ color: #2c3e50;
+ margin-bottom: 10px;
+}
+
+.security-mode-header p {
+ color: #7f8c8d;
+ font-size: 16px;
+}
+
+.security-options {
+ display: grid;
+ gap: 20px;
+ margin-bottom: 30px;
+}
+
+.security-option {
+ border: 2px solid #e1e8ed;
+ border-radius: 12px;
+ padding: 20px;
+ cursor: pointer;
+ transition: all 0.3s ease;
+ background: #fff;
+}
+
+.security-option:hover {
+ border-color: #3498db;
+ box-shadow: 0 4px 12px rgba(52, 152, 219, 0.15);
+ transform: translateY(-2px);
+}
+
+.security-option.selected {
+ border-color: #27ae60;
+ background: #f8fff8;
+ box-shadow: 0 4px 12px rgba(39, 174, 96, 0.15);
+}
+
+.option-header {
+ display: flex;
+ align-items: center;
+ margin-bottom: 12px;
+}
+
+.option-icon {
+ font-size: 24px;
+ margin-right: 12px;
+}
+
+.option-title {
+ font-size: 18px;
+ font-weight: 600;
+ color: #2c3e50;
+ flex: 1;
+}
+
+.security-level {
+ padding: 4px 12px;
+ border-radius: 20px;
+ font-size: 12px;
+ font-weight: 600;
+ text-transform: uppercase;
+}
+
+.security-level.high {
+ background: #d4edda;
+ color: #155724;
+}
+
+.security-level.medium {
+ background: #fff3cd;
+ color: #856404;
+}
+
+.security-level.low {
+ background: #f8d7da;
+ color: #721c24;
+}
+
+.security-level.critical {
+ background: #f5c6cb;
+ color: #721c24;
+ animation: pulse 2s infinite;
+}
+
+@keyframes pulse {
+ 0% { opacity: 1; }
+ 50% { opacity: 0.7; }
+ 100% { opacity: 1; }
+}
+
+.option-description {
+ color: #5a6c7d;
+ margin-bottom: 12px;
+ line-height: 1.5;
+}
+
+.option-features {
+ display: flex;
+ flex-wrap: wrap;
+ gap: 8px;
+}
+
+.feature {
+ background: #e8f5e8;
+ color: #2d5a2d;
+ padding: 4px 8px;
+ border-radius: 12px;
+ font-size: 12px;
+ font-weight: 500;
+}
+
+.option-warnings {
+ display: flex;
+ flex-wrap: wrap;
+ gap: 8px;
+}
+
+.warning {
+ background: #ffeaa7;
+ color: #d63031;
+ padding: 4px 8px;
+ border-radius: 12px;
+ font-size: 12px;
+ font-weight: 500;
+}
+
+.warning.critical {
+ background: #fab1a0;
+ color: #d63031;
+ animation: blink 1s infinite;
+}
+
+@keyframes blink {
+ 0%, 50% { opacity: 1; }
+ 51%, 100% { opacity: 0.5; }
+}
+
+.security-actions {
+ display: flex;
+ justify-content: center;
+ gap: 15px;
+ padding-top: 20px;
+ border-top: 1px solid #e1e8ed;
+}
+
+.btn-primary, .btn-secondary, .btn-danger {
+ padding: 12px 24px;
+ border: none;
+ border-radius: 8px;
+ font-size: 16px;
+ font-weight: 600;
+ cursor: pointer;
+ transition: all 0.3s ease;
+}
+
+.btn-primary {
+ background: #27ae60;
+ color: white;
+}
+
+.btn-primary:hover:not(:disabled) {
+ background: #229954;
+ transform: translateY(-1px);
+}
+
+.btn-primary:disabled {
+ background: #bdc3c7;
+ cursor: not-allowed;
+}
+
+.btn-secondary {
+ background: #95a5a6;
+ color: white;
+}
+
+.btn-secondary:hover {
+ background: #7f8c8d;
+}
+
+.btn-danger {
+ background: #e74c3c;
+ color: white;
+}
+
+.btn-danger:hover:not(:disabled) {
+ background: #c0392b;
+}
+
+.btn-danger:disabled {
+ background: #bdc3c7;
+ cursor: not-allowed;
+}
+
+/* Modal styles */
+.modal {
+ position: fixed;
+ top: 0;
+ left: 0;
+ width: 100%;
+ height: 100%;
+ background: rgba(0, 0, 0, 0.7);
+ display: flex;
+ justify-content: center;
+ align-items: center;
+ z-index: 10000;
+}
+
+.modal-content {
+ background: white;
+ border-radius: 12px;
+ max-width: 500px;
+ width: 90%;
+ max-height: 80vh;
+ overflow-y: auto;
+}
+
+.modal-header {
+ padding: 20px;
+ border-bottom: 1px solid #e1e8ed;
+}
+
+.modal-header h3 {
+ margin: 0;
+ color: #e74c3c;
+}
+
+.modal-body {
+ padding: 20px;
+}
+
+.modal-footer {
+ padding: 20px;
+ border-top: 1px solid #e1e8ed;
+ display: flex;
+ justify-content: flex-end;
+ gap: 10px;
+}
+
+.warning-actions {
+ margin-top: 20px;
+ padding: 15px;
+ background: #fff3cd;
+ border-radius: 8px;
+ border-left: 4px solid #ffc107;
+}
+
+.warning-actions label {
+ display: flex;
+ align-items: center;
+ cursor: pointer;
+ font-weight: 500;
+}
+
+.warning-actions input[type="checkbox"] {
+ margin-right: 10px;
+ transform: scale(1.2);
+}
+
+/* Responsive */
+@media (max-width: 768px) {
+ .security-mode-selector {
+ padding: 15px;
+ }
+
+ .security-options {
+ gap: 15px;
+ }
+
+ .security-option {
+ padding: 15px;
+ }
+
+ .option-header {
+ flex-direction: column;
+ align-items: flex-start;
+ }
+
+ .option-title {
+ margin: 8px 0;
+ }
+
+ .security-actions {
+ flex-direction: column;
+ }
+}
diff --git a/src/components/security-mode-selector/security-mode-selector.html b/src/components/security-mode-selector/security-mode-selector.html
new file mode 100644
index 0000000..4ff8e20
--- /dev/null
+++ b/src/components/security-mode-selector/security-mode-selector.html
@@ -0,0 +1,130 @@
+
+
+
+
+
+
+
+
+ Utilise Proton Pass pour l'authentification biométrique et la gestion des clés
+
+
+ â
Authentification biométrique
+ â
Chiffrement end-to-end
+ â
Synchronisation sécurisée
+
+
+
+
+
+
+
+ Utilise l'authentificateur intégré de votre systÚme d'exploitation
+
+
+ â
Windows Hello / Touch ID / Face ID
+ â
Chiffrement matériel
+ â
Protection par mot de passe
+
+
+
+
+
+
+
+ Utilise les fonctionnalités de sécurité du navigateur
+
+
+ â
WebAuthn standard
+ â ïž DĂ©pendant du navigateur
+ â ïž Moins sĂ©curisĂ© que les options OS
+
+
+
+
+
+
+
+ Stockage en clair avec authentification par application 2FA
+
+
+ â ïž ClĂ©s stockĂ©es en clair
+ â ïž Risque de compromission
+ â ïž Non recommandĂ© pour des donnĂ©es sensibles
+
+
+
+
+
+
+
+ Stockage en clair sans aucune protection
+
+
+ đš ClĂ©s stockĂ©es en clair
+ đš AccĂšs non protĂ©gĂ©
+ đš RISQUE ĂLEVĂ
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/src/components/security-mode-selector/security-mode-selector.ts b/src/components/security-mode-selector/security-mode-selector.ts
new file mode 100644
index 0000000..f659b9a
--- /dev/null
+++ b/src/components/security-mode-selector/security-mode-selector.ts
@@ -0,0 +1,382 @@
+/**
+ * SecurityModeSelector - Composant de sélection du mode de sécurisation
+ * Permet à l'utilisateur de choisir comment sécuriser ses clés privées
+ */
+
+export type SecurityMode = 'proton-pass' | 'os' | 'browser' | '2fa' | 'none';
+
+export interface SecurityModeConfig {
+ mode: SecurityMode;
+ name: string;
+ description: string;
+ securityLevel: 'high' | 'medium' | 'low' | 'critical';
+ requiresConfirmation: boolean;
+ warnings: string[];
+}
+
+export class SecurityModeSelector {
+ private container: HTMLElement;
+ private selectedMode: SecurityMode | null = null;
+ private onModeSelected: (mode: SecurityMode) => void;
+ private onCancel: () => void;
+
+ constructor(
+ container: HTMLElement,
+ onModeSelected: (mode: SecurityMode) => void,
+ onCancel: () => void
+ ) {
+ this.container = container;
+ this.onModeSelected = onModeSelected;
+ this.onCancel = onCancel;
+ this.init();
+ }
+
+ private init(): void {
+ this.render();
+ this.attachEventListeners();
+ }
+
+ private render(): void {
+ this.container.innerHTML = `
+
+
+
+
+ ${this.getSecurityOptionsHTML()}
+
+
+
+
+
+
+
+ ${this.getWarningModalHTML()}
+
+ `;
+ }
+
+ private getSecurityOptionsHTML(): string {
+ const options = this.getSecurityModes();
+
+ return options.map(option => `
+
+
+
${option.description}
+ ${this.getModeFeaturesHTML(option)}
+
+ `).join('');
+ }
+
+ private getModeFeaturesHTML(option: SecurityModeConfig): string {
+ if (option.securityLevel === 'low' || option.securityLevel === 'critical') {
+ return `
+
+ ${option.warnings.map(warning => `
+
+ ${warning}
+
+ `).join('')}
+
+ `;
+ } else {
+ return `
+
+ ${this.getModeFeatures(option.mode).map(feature => `
+ ${feature}
+ `).join('')}
+
+ `;
+ }
+ }
+
+ private getWarningModalHTML(): string {
+ return `
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ `;
+ }
+
+ private getSecurityModes(): SecurityModeConfig[] {
+ return [
+ {
+ mode: 'proton-pass',
+ name: 'Proton Pass',
+ description: 'Utilise Proton Pass pour l\'authentification biométrique et la gestion des clés',
+ securityLevel: 'high',
+ requiresConfirmation: false,
+ warnings: []
+ },
+ {
+ mode: 'os',
+ name: 'Authentificateur OS',
+ description: 'Utilise l\'authentificateur intégré de votre systÚme d\'exploitation',
+ securityLevel: 'high',
+ requiresConfirmation: false,
+ warnings: []
+ },
+ {
+ mode: 'browser',
+ name: 'Navigateur',
+ description: 'Utilise les fonctionnalités de sécurité du navigateur',
+ securityLevel: 'medium',
+ requiresConfirmation: false,
+ warnings: []
+ },
+ {
+ mode: '2fa',
+ name: 'Application 2FA',
+ description: 'Stockage en clair avec authentification par application 2FA',
+ securityLevel: 'low',
+ requiresConfirmation: true,
+ warnings: [
+ 'â ïž ClĂ©s stockĂ©es en clair',
+ 'â ïž Risque de compromission',
+ 'â ïž Non recommandĂ© pour des donnĂ©es sensibles'
+ ]
+ },
+ {
+ mode: 'none',
+ name: 'Aucune Sécurité',
+ description: 'Stockage en clair sans aucune protection',
+ securityLevel: 'critical',
+ requiresConfirmation: true,
+ warnings: [
+ 'đš ClĂ©s stockĂ©es en clair',
+ 'đš AccĂšs non protĂ©gĂ©',
+ 'đš RISQUE ĂLEVĂ'
+ ]
+ }
+ ];
+ }
+
+ private getModeIcon(mode: SecurityMode): string {
+ const icons = {
+ 'proton-pass': 'đ',
+ 'os': 'đ„ïž',
+ 'browser': 'đ',
+ '2fa': 'đ±',
+ 'none': 'đš'
+ };
+ return icons[mode];
+ }
+
+ private getSecurityLevelText(level: string): string {
+ const texts = {
+ 'high': 'Sécurisé',
+ 'medium': 'Moyennement sécurisé',
+ 'low': 'â ïž Non sĂ©curisĂ©',
+ 'critical': 'DANGEREUX'
+ };
+ return texts[level as keyof typeof texts];
+ }
+
+ private getModeFeatures(mode: SecurityMode): string[] {
+ const features = {
+ 'proton-pass': [
+ 'â
Authentification biométrique',
+ 'â
Chiffrement end-to-end',
+ 'â
Synchronisation sécurisée'
+ ],
+ 'os': [
+ 'â
Windows Hello / Touch ID / Face ID',
+ 'â
Chiffrement matériel',
+ 'â
Protection par mot de passe'
+ ],
+ 'browser': [
+ 'â
WebAuthn standard',
+ 'â ïž DĂ©pendant du navigateur',
+ 'â ïž Moins sĂ©curisĂ© que les options OS'
+ ],
+ '2fa': [],
+ 'none': []
+ };
+ return features[mode];
+ }
+
+ private attachEventListeners(): void {
+ // Sélection d'un mode
+ this.container.addEventListener('click', (e) => {
+ const option = (e.target as HTMLElement).closest('.security-option');
+ if (option) {
+ this.selectMode(option.dataset.mode as SecurityMode);
+ }
+ });
+
+ // Confirmation du mode
+ this.container.addEventListener('click', (e) => {
+ if ((e.target as HTMLElement).id === 'confirm-security-mode') {
+ this.confirmSelection();
+ }
+ });
+
+ // Annulation
+ this.container.addEventListener('click', (e) => {
+ if ((e.target as HTMLElement).id === 'cancel-security-mode') {
+ this.onCancel();
+ }
+ });
+
+ // Gestion de la modal d'avertissement
+ this.attachWarningModalListeners();
+ }
+
+ private attachWarningModalListeners(): void {
+ // Checkbox de compréhension des risques
+ this.container.addEventListener('change', (e) => {
+ if ((e.target as HTMLElement).id === 'understand-risks') {
+ const checkbox = e.target as HTMLInputElement;
+ const confirmBtn = this.container.querySelector('#confirm-risky-mode') as HTMLButtonElement;
+ if (confirmBtn) {
+ confirmBtn.disabled = !checkbox.checked;
+ }
+ }
+ });
+
+ // Confirmation du mode risqué
+ this.container.addEventListener('click', (e) => {
+ if ((e.target as HTMLElement).id === 'confirm-risky-mode') {
+ this.hideWarningModal();
+ this.onModeSelected(this.selectedMode!);
+ }
+ });
+
+ // Annulation du mode risqué
+ this.container.addEventListener('click', (e) => {
+ if ((e.target as HTMLElement).id === 'cancel-risky-mode') {
+ this.hideWarningModal();
+ this.clearSelection();
+ }
+ });
+ }
+
+ private selectMode(mode: SecurityMode): void {
+ // Désélectionner tous les modes
+ this.container.querySelectorAll('.security-option').forEach(option => {
+ option.classList.remove('selected');
+ });
+
+ // Sélectionner le nouveau mode
+ const selectedOption = this.container.querySelector(`[data-mode="${mode}"]`);
+ if (selectedOption) {
+ selectedOption.classList.add('selected');
+ this.selectedMode = mode;
+ this.updateConfirmButton();
+ }
+ }
+
+ private updateConfirmButton(): void {
+ const confirmBtn = this.container.querySelector('#confirm-security-mode') as HTMLButtonElement;
+ if (confirmBtn) {
+ confirmBtn.disabled = !this.selectedMode;
+ }
+ }
+
+ private confirmSelection(): void {
+ if (!this.selectedMode) return;
+
+ const modeConfig = this.getSecurityModes().find(m => m.mode === this.selectedMode);
+
+ if (modeConfig?.requiresConfirmation) {
+ this.showWarningModal(modeConfig);
+ } else {
+ this.onModeSelected(this.selectedMode);
+ }
+ }
+
+ private showWarningModal(modeConfig: SecurityModeConfig): void {
+ const modal = this.container.querySelector('#security-warning-modal') as HTMLElement;
+ const warningContent = this.container.querySelector('#warning-content') as HTMLElement;
+
+ if (modal && warningContent) {
+ warningContent.innerHTML = `
+
+
Vous avez choisi : ${modeConfig.name}
+
${modeConfig.description}
+
+
+
+
â ïž Risques identifiĂ©s :
+
+ ${modeConfig.warnings.map(warning => `- ${warning}
`).join('')}
+
+
+
+
+
+ Recommandation :
+ ${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.'
+ }
+
+
+ `;
+
+ modal.style.display = 'flex';
+ }
+ }
+
+ private hideWarningModal(): void {
+ const modal = this.container.querySelector('#security-warning-modal') as HTMLElement;
+ if (modal) {
+ modal.style.display = 'none';
+ }
+ }
+
+ private clearSelection(): void {
+ this.selectedMode = null;
+ this.container.querySelectorAll('.security-option').forEach(option => {
+ option.classList.remove('selected');
+ });
+ this.updateConfirmButton();
+ }
+
+ public show(): void {
+ this.container.style.display = 'block';
+ }
+
+ public hide(): void {
+ this.container.style.display = 'none';
+ }
+
+ public destroy(): void {
+ this.container.innerHTML = '';
+ }
+}
diff --git a/src/pages/birthday-setup/birthday-setup.html b/src/pages/birthday-setup/birthday-setup.html
new file mode 100644
index 0000000..3575584
--- /dev/null
+++ b/src/pages/birthday-setup/birthday-setup.html
@@ -0,0 +1,120 @@
+
+
+
+
+
+ Configuration de la Date Anniversaire - LeCoffre
+
+
+
+
+
đ Configuration de la Date Anniversaire
+
Mise Ă jour de la date anniversaire et scan des blocs
+
+
+ đ Connexion aux relais...
+
+
+
+
+
+
+
+
+
+
diff --git a/src/pages/birthday-setup/birthday-setup.ts b/src/pages/birthday-setup/birthday-setup.ts
new file mode 100644
index 0000000..0513b32
--- /dev/null
+++ b/src/pages/birthday-setup/birthday-setup.ts
@@ -0,0 +1,120 @@
+/**
+ * Page de configuration de la date anniversaire
+ * Mise Ă jour de la date anniversaire et scan des blocs
+ */
+
+document.addEventListener('DOMContentLoaded', async () => {
+ console.log('đ Birthday setup page loaded');
+
+ const status = document.getElementById('status') as HTMLDivElement;
+ const progressBar = document.getElementById('progressBar') as HTMLDivElement;
+ const continueBtn = document.getElementById('continueBtn') as HTMLButtonElement;
+
+ try {
+ // Ătape 1: Connexion aux relais
+ updateStatus('đ Connexion aux relais...', 'loading');
+ updateProgress(20);
+
+ 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
+ let attempts = 0;
+ const maxAttempts = 30;
+ const delayMs = 2000;
+
+ let services;
+ 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.message);
+ attempts++;
+ if (attempts >= maxAttempts) {
+ throw new Error(`Services failed to initialize after ${maxAttempts} attempts.`);
+ }
+ await new Promise(resolve => setTimeout(resolve, delayMs));
+ }
+ }
+
+ // Connexion aux relais
+ await services.connectAllRelays();
+ console.log('â
Relays connected successfully');
+
+ // Ătape 2: Mise Ă jour de la date anniversaire
+ updateStatus('đ Mise Ă jour de la date anniversaire...', 'loading');
+ updateProgress(40);
+
+ // Attendre que les relais soient prĂȘts
+ await services.getRelayReadyPromise();
+ console.log('â
Communication handshake completed');
+
+ // Mettre Ă jour la date anniversaire du wallet
+ await services.updateDeviceBlockHeight();
+ console.log('â
Birthday updated successfully');
+
+ // Ătape 3: Scan des blocs
+ updateStatus('đ Scan des blocs en cours...', 'loading');
+ updateProgress(60);
+
+ // Effectuer le scan initial des blocs
+ await services.ensureCompleteInitialScan();
+ console.log('â
Initial block scan completed');
+
+ // Ătape 4: Synchronisation des processus
+ updateStatus('đ Synchronisation des processus...', 'loading');
+ updateProgress(80);
+
+ // Restaurer les processus depuis la base de données
+ await services.restoreProcessesFromDB();
+ console.log('â
Process synchronization completed');
+
+ } catch (error) {
+ console.error('â Services not available:', error);
+ updateStatus('â Erreur: Services non disponibles', 'error');
+ throw error;
+ }
+
+ // Ătape 5: Finalisation
+ updateStatus('â
Configuration terminée avec succÚs!', 'success');
+ updateProgress(100);
+
+ // Activer le bouton continuer
+ continueBtn.disabled = false;
+
+ console.log('đ Birthday setup completed successfully');
+
+ } catch (error) {
+ console.error('â Error during birthday setup:', error);
+ updateStatus('â Erreur lors de la configuration de la date anniversaire', 'error');
+ }
+
+ // Gestion du bouton continuer
+ continueBtn.addEventListener('click', () => {
+ console.log('đ Redirecting to main application...');
+ // Rediriger vers l'application principale
+ window.location.href = '/';
+ });
+
+ function updateStatus(message: string, type: 'loading' | 'success' | 'error') {
+ status.textContent = message;
+ status.className = `status ${type}`;
+ }
+
+ function updateProgress(percent: number) {
+ progressBar.style.width = `${percent}%`;
+ }
+});
diff --git a/src/pages/home/home.ts b/src/pages/home/home.ts
index 4509e22..4e24fc4 100755
--- a/src/pages/home/home.ts
+++ b/src/pages/home/home.ts
@@ -38,8 +38,10 @@ export async function initHomePage(): Promise {
// Set up iframe pairing button listeners
setupIframePairingButtons();
- // Set up main pairing interface
- setupMainPairing();
+ // Set up main pairing interface (avec protection contre les appels multiples)
+ if (!isMainPairingSetup) {
+ setupMainPairing();
+ }
// Set up account actions
setupAccountActions();
@@ -63,6 +65,20 @@ export async function initHomePage(): Promise {
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();
@@ -87,17 +103,32 @@ export async function initHomePage(): Promise {
});
}
- // Trigger WebAuthn authentication first
+ // 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...');
- const spAddress = await service.getDeviceAddress();
- console.log('đ§ Generating create button...');
- generateCreateBtn();
- console.log('đ§ Displaying emojis...');
- displayEmojis(spAddress);
+
+ 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) {
@@ -403,8 +434,19 @@ export function setupIframePairingButtons() {
}
}
+// 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;
@@ -435,72 +477,504 @@ function setupUserInteractionListener(): void {
console.log('đ User interaction listeners set up');
}
+/**
+ * Affiche le sélecteur de mode de sécurisation
+ */
+async function showSecurityModeSelector(): Promise {
+ 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 = `
+
+
đ Mode de SĂ©curisation
+
Choisissez comment vous souhaitez sécuriser vos clés privées :
+
+
+
+
+
+ đ
+ Proton Pass
+ Sécurisé
+
+
Utilise Proton Pass pour l'authentification biométrique
+
+
+
+
+ đ„ïž
+ Authentificateur OS
+ Sécurisé
+
+
Utilise l'authentificateur intégré de votre systÚme
+
+
+
+
+ đ
+ Navigateur
+ Moyennement sécurisé
+
+
Utilise les fonctionnalités de sécurité du navigateur
+
+
+
+
+ đ±
+ Application 2FA
+ â ïž Non sĂ©curisĂ©
+
+
Stockage en clair avec authentification 2FA
+
+
+
+
+ đ
+ Mot de Passe
+ â ïž Non sauvegardĂ©
+
+
Chiffrement par mot de passe (non sauvegardé, non récupérable)
+
+
+
+
+ đš
+ Aucune Sécurité
+ DANGEREUX
+
+
Stockage en clair sans aucune protection
+
+
+
+
+
+
+
+ `;
+
+ 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 {
+ 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 = `
+
+
â ïž Attention - Mode de SĂ©curisation Non RecommandĂ©
+
+
+
+
Vous avez choisi : ${modeConfig.name}
+
${modeConfig.description}
+
+
+
+
â ïž Risques identifiĂ©s :
+
+ ${modeConfig.warnings.map((warning: string) => `- ${warning}
`).join('')}
+
+
+
+
+
+ Recommandation :
+ ${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.'
+ }
+
+
+
+
+
+
+
+
+
+
+
+ `;
+
+ 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 {
+ // 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 {
+ // 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 {
- // Update UI to show authentication in progress
- if (mainStatus) {
- mainStatus.innerHTML = 'Authenticating with browser...';
+ // 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;
}
- // Always trigger WebAuthn flow for authentication
- console.log('đ Triggering WebAuthn authentication...');
+ if (!currentMode) {
+ // Aucun mode sélectionné, afficher le sélecteur
+ console.log('đ No security mode selected, showing selector...');
+ if (mainStatus) {
+ mainStatus.innerHTML = 'đ Please select your security mode...';
+ }
- // Import and trigger WebAuthn directly
+ // 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 = 'Authenticating with selected security mode...';
+ }
+
+ // 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 WebAuthn credentials...');
+ console.log('đ Checking for existing credentials...');
const hasCredentials = await secureCredentialsService.hasCredentials();
if (hasCredentials) {
- console.log('đ Existing WebAuthn credentials found, decrypting...');
+ console.log('đ Existing credentials found, decrypting...');
if (mainStatus) {
mainStatus.innerHTML = 'Decrypting existing credentials...';
}
- // This will trigger WebAuthn for decryption of existing credentials
- console.log('đ Starting WebAuthn decryption process...');
- await secureCredentialsService.retrieveCredentials('');
- console.log('â
WebAuthn decryption completed');
+ try {
+ // This will trigger authentication for decryption of existing credentials
+ console.log('đ Starting credentials decryption process...');
+ const decryptedCredentials = await secureCredentialsService.retrieveCredentials('');
- if (mainStatus) {
- mainStatus.innerHTML = 'â
Credentials decrypted successfully';
+ if (!decryptedCredentials) {
+ throw new Error('Failed to decrypt existing credentials - no data returned');
+ }
+
+ console.log('â
Credentials decryption completed successfully');
+ if (mainStatus) {
+ mainStatus.innerHTML = 'â
Credentials decrypted successfully';
+ }
+ } catch (error) {
+ console.error('â Credentials decryption failed:', error);
+ if (mainStatus) {
+ mainStatus.innerHTML = 'â Failed to decrypt credentials. Please try again.';
+ }
+ throw error; // ArrĂȘter le processus si le dĂ©chiffrement Ă©choue
}
} else {
- console.log('đ No existing WebAuthn credentials, creating new ones...');
+ console.log('đ No existing credentials, creating new ones...');
if (mainStatus) {
mainStatus.innerHTML = 'đ Setting up secure authentication...';
}
- // This will trigger WebAuthn for creation of new credentials
- console.log('đ Starting WebAuthn creation process...');
- if (mainStatus) {
- mainStatus.innerHTML = 'đ Creating secure credentials with your device...';
- }
- const credentialData = await secureCredentialsService.generateSecureCredentials('');
- console.log('â
WebAuthn creation completed');
+ try {
+ // This will trigger authentication for creation of new credentials
+ console.log('đ Starting credentials creation process...');
+ if (mainStatus) {
+ mainStatus.innerHTML = 'đ Creating secure credentials with your device...';
+ }
- // Store the credentials in IndexedDB
- console.log('đŸ Storing credentials in IndexedDB...');
- if (mainStatus) {
- mainStatus.innerHTML = 'đŸ Securing credentials...';
- }
- await secureCredentialsService.storeCredentials(credentialData, '');
- console.log('â
Credentials stored successfully');
+ const credentialData = await secureCredentialsService.generateSecureCredentials('4nk-secure-password');
- // Decrypt and make keys available to SDK
- console.log('đ Decrypting credentials for SDK access...');
- if (mainStatus) {
- mainStatus.innerHTML = 'đ Making keys available...';
- }
- await secureCredentialsService.retrieveCredentials('');
- console.log('â
Credentials decrypted and available');
+ if (!credentialData || !credentialData.spendKey || !credentialData.scanKey) {
+ throw new Error('Failed to generate valid credentials - missing spendKey or scanKey');
+ }
- if (mainStatus) {
- mainStatus.innerHTML = 'â
Secure authentication ready';
+ console.log('â
Credentials creation completed successfully');
+
+ // Store the credentials in IndexedDB
+ console.log('đŸ Storing credentials in IndexedDB...');
+ if (mainStatus) {
+ mainStatus.innerHTML = 'đŸ Securing credentials...';
+ }
+
+ 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 = 'đ Making keys available...';
+ }
+
+ 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 = 'â
Secure authentication ready';
+ }
+ } catch (error) {
+ console.error('â Credentials creation/encryption/decryption failed:', error);
+ if (mainStatus) {
+ mainStatus.innerHTML = 'â Failed to create/encrypt/decrypt credentials. Please try again.';
+ }
+ throw error; // ArrĂȘter le processus si la gĂ©nĂ©ration/chiffrement/dĂ©chiffrement Ă©choue
}
}
@@ -523,42 +997,42 @@ async function handleMainPairing(): Promise {
console.log(`đ Checking credentials availability (attempt ${attempts}/${maxAttempts})...`);
try {
- credentialsReady = await secureCredentialsService.hasCredentials();
- if (credentialsReady) {
- console.log('â
Credentials verified, proceeding with pairing...');
- break;
- } else {
- console.log(`âł Credentials not ready yet, waiting ${delayMs}ms... (attempt ${attempts}/${maxAttempts})`);
+ // 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));
}
- } catch (error) {
- console.warn(`â ïž Error checking credentials (attempt ${attempts}):`, error);
- 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 ready after creation - checking IndexedDB directly...');
-
- // Try to check IndexedDB directly for debugging
- try {
- const directCheck = await secureCredentialsService.getEncryptedCredentials();
- console.log('đ Direct IndexedDB check result:', directCheck);
- } catch (error) {
- console.error('â Direct IndexedDB check failed:', error);
- }
-
+ console.error('â Credentials not available after all attempts - stopping process');
if (mainStatus) {
- mainStatus.innerHTML = 'â Failed to create credentials';
+ mainStatus.innerHTML = 'â Authentication failed - credentials not available';
}
- return;
- }
+ 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 = 'đ Starting secure pairing process...';
}
+
+ // Attendre que les credentials soient réellement disponibles avant de continuer
+ await waitForCredentialsAvailability();
+
await prepareAndSendPairingTx();
} catch (error) {
@@ -574,9 +1048,14 @@ async function handleMainPairing(): Promise {
} else {
console.error('Pairing failed:', error);
if (mainStatus) {
- mainStatus.innerHTML = 'âł Waiting for user to validate secure key access...';
+ mainStatus.innerHTML = 'â Pairing failed: ' + (error as Error).message + '';
}
+ 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
}
}
diff --git a/src/pages/security-setup/security-setup.html b/src/pages/security-setup/security-setup.html
new file mode 100644
index 0000000..95cbcca
--- /dev/null
+++ b/src/pages/security-setup/security-setup.html
@@ -0,0 +1,155 @@
+
+
+
+
+
+ Configuration de la Sécurité - LeCoffre
+
+
+
+
+
đ Configuration de la SĂ©curitĂ©
+
Choisissez votre mode de sécurité pour protéger vos clés
+
+
+
+
đĄïž ClĂ© de sĂ©curitĂ©
+
Sécurité maximale avec votre gestionnaire de clés de sécurité
+
+
+
+
đ SystĂšme d'exploitation
+
Utilise l'authentification biométrique de votre systÚme
+
+
+
+
đ Navigateur
+
Sauvegarde dans le gestionnaire de mots de passe du navigateur
+
+
+
+
đ OTP (code Ă usage unique)
+
Code OTP généré par Proton Pass, Google Authenticator, etc.
+
+
+
+
đ Mot de passe
+
Chiffrement par mot de passe (non récupérable si oublié)
+
+
+
+
â ïž Aucune protection
+
Stockage en clair (non recommandé)
+
+
+
+
+ â ïž Attention : Ce mode de sĂ©curitĂ© n'est pas recommandĂ©. Vos clĂ©s seront stockĂ©es en clair.
+
+
+
+
+
+
+
+
diff --git a/src/pages/security-setup/security-setup.ts b/src/pages/security-setup/security-setup.ts
new file mode 100644
index 0000000..27563eb
--- /dev/null
+++ b/src/pages/security-setup/security-setup.ts
@@ -0,0 +1,99 @@
+/**
+ * Page de configuration de la sécurité
+ * PremiÚre étape du processus d'initialisation
+ */
+
+import { SecurityMode } from '../../services/security-mode.service';
+
+let selectedMode: SecurityMode | null = null;
+
+document.addEventListener('DOMContentLoaded', () => {
+ console.log('đ Security setup page loaded');
+
+ const options = document.querySelectorAll('.security-option');
+ const continueBtn = document.getElementById('continueBtn') as HTMLButtonElement;
+ const warning = document.getElementById('warning') as HTMLDivElement;
+
+ // Gestion de la sélection des options
+ options.forEach(option => {
+ option.addEventListener('click', () => {
+ // Désélectionner toutes les options
+ options.forEach(opt => opt.classList.remove('selected'));
+
+ // Sélectionner l'option cliquée
+ option.classList.add('selected');
+
+ // Récupérer le mode sélectionné
+ const mode = option.getAttribute('data-mode') as SecurityMode;
+ selectedMode = mode;
+
+ // Afficher l'avertissement pour les modes non sécurisés
+ if (mode === 'none') {
+ warning.style.display = 'block';
+ } else {
+ warning.style.display = 'none';
+ }
+
+ // Activer le bouton continuer
+ continueBtn.disabled = false;
+
+ console.log('đ Security mode selected:', mode);
+ });
+ });
+
+ // Gestion du bouton continuer
+ continueBtn.addEventListener('click', async () => {
+ if (!selectedMode) {
+ console.error('â No security mode selected');
+ return;
+ }
+
+ try {
+ console.log('đ Processing security mode:', selectedMode);
+
+ // Sauvegarder le mode de sécurité
+ const { SecurityModeService } = await import('../../services/security-mode.service');
+ const securityModeService = SecurityModeService.getInstance();
+ await securityModeService.setSecurityMode(selectedMode);
+
+ console.log('â
Security mode saved successfully');
+
+ console.log('đ Generating PBKDF2 key for security mode:', selectedMode);
+
+ // Désactiver le bouton pendant la génération
+ continueBtn.disabled = true;
+ continueBtn.textContent = 'Génération de la clé...';
+
+ try {
+ // Générer la clé PBKDF2 selon le mode choisi
+ const { SecureCredentialsService } = await import('../../services/secure-credentials.service');
+ const secureCredentialsService = SecureCredentialsService.getInstance();
+
+ console.log('đ Generating PBKDF2 key with security mode:', selectedMode);
+
+ // Générer la clé PBKDF2 et la stocker selon le mode
+ const pbkdf2Key = await secureCredentialsService.generatePBKDF2Key(selectedMode);
+ console.log('â
PBKDF2 key generated and stored securely');
+
+ // Rediriger vers la page de génération du wallet
+ window.location.href = '/src/pages/wallet-setup/wallet-setup.html';
+
+ } catch (error) {
+ console.error('â PBKDF2 key generation failed:', error);
+ alert('Erreur lors de la génération de la clé de sécurité. Veuillez réessayer.');
+
+ // Réactiver le bouton
+ continueBtn.disabled = false;
+ continueBtn.textContent = 'Continuer';
+ }
+
+ } catch (error) {
+ console.error('â Error processing security mode:', error);
+ alert('Erreur lors du traitement du mode de sécurité');
+
+ // Réactiver le bouton
+ continueBtn.disabled = false;
+ continueBtn.textContent = 'Continuer';
+ }
+ });
+});
diff --git a/src/pages/wallet-setup/wallet-setup.html b/src/pages/wallet-setup/wallet-setup.html
new file mode 100644
index 0000000..dc47dbe
--- /dev/null
+++ b/src/pages/wallet-setup/wallet-setup.html
@@ -0,0 +1,120 @@
+
+
+
+
+
+ Génération du Wallet - LeCoffre
+
+
+
+
+
đ° GĂ©nĂ©ration du Wallet
+
Création et sécurisation de votre portefeuille
+
+
+ đ Initialisation en cours...
+
+
+
+
+
+
+
+
+
+
diff --git a/src/pages/wallet-setup/wallet-setup.ts b/src/pages/wallet-setup/wallet-setup.ts
new file mode 100644
index 0000000..7da7643
--- /dev/null
+++ b/src/pages/wallet-setup/wallet-setup.ts
@@ -0,0 +1,377 @@
+/**
+ * Page de génération du wallet
+ * DeuxiÚme étape du processus d'initialisation
+ */
+
+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 {
+ return new Promise((resolve, reject) => {
+ const request = indexedDB.open('4nk', 2);
+
+ request.onerror = () => reject(request.error);
+ request.onsuccess = () => {
+ const db = request.result;
+ const transaction = db.transaction(['wallet'], 'readwrite');
+ const store = transaction.objectStore('wallet');
+
+ 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('wallet')) {
+ db.createObjectStore('wallet');
+ }
+ };
+ });
+ }
+
+ try {
+ // Ătape 1: Initialisation des services
+ updateStatus('đ Initialisation des services...', 'loading');
+ updateProgress(10);
+
+ let services; // 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.message);
+
+ // 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Ă
+ const hasCredentials = await secureCredentialsService.hasCredentials();
+ console.log('đ Has existing credentials:', hasCredentials);
+
+ if (!hasCredentials) {
+ updateStatus('đ GĂ©nĂ©ration des clĂ©s de sĂ©curitĂ©...', 'loading');
+
+ const { SecurityModeService } = await import('../../services/security-mode.service');
+ const securityModeService = SecurityModeService.getInstance();
+ const currentMode = await securityModeService.getCurrentMode();
+
+ console.log('đ Current security mode:', currentMode);
+
+ if (currentMode) {
+ // Générer la clé PBKDF2 avec le mode de sécurité choisi
+ updateStatus('đ GĂ©nĂ©ration de la clĂ© de chiffrement...', 'loading');
+ const pbkdf2Key = await secureCredentialsService.generatePBKDF2Key(currentMode);
+ console.log('â
PBKDF2 key generated for mode:', currentMode);
+
+ // Générer les credentials avec le mode de sécurité choisi
+ const credentials = await secureCredentialsService.generateSecureCredentials('4nk-secure-password');
+ console.log('â
Secure credentials generated');
+ } else {
+ // Fallback au mode browser si aucun mode n'est défini
+ console.log('â ïž No security mode found, using browser mode');
+ const credentials = await secureCredentialsService.generateSecureCredentials('4nk-secure-password');
+ console.log('â
Secure credentials generated with browser mode');
+ }
+ } else {
+ console.log('â
Secure credentials already exist');
+ }
+ } catch (error) {
+ console.warn('â ïž Service worker not ready, credentials will be saved later:', error);
+ // Pas de fallback localStorage - les credentials seront sauvegardés plus tard
+ }
+
+ // Ă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...');
+
+ // Récupérer le mode de sécurité pour le chiffrement
+ const { SecurityModeService } = await import('../../services/security-mode.service');
+ const securityModeService = SecurityModeService.getInstance();
+ let currentMode = await securityModeService.getCurrentMode();
+
+ // Si aucun mode n'est trouvé, utiliser le mode par défaut
+ if (!currentMode) {
+ console.log('â ïž No security mode found, using browser mode as fallback');
+ currentMode = 'browser';
+ }
+
+ console.log('đ Using security mode for wallet encryption:', currentMode);
+
+ // Générer un wallet temporaire avec état birthday_waiting
+ const { StorageService } = await import('../../services/credentials/storage.service');
+ const { EncryptionService } = await import('../../services/credentials/encryption.service');
+ const storageService = StorageService.getInstance();
+ 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 générée par le service de sécurité
+ const { SecureCredentialsService } = await import('../../services/secure-credentials.service');
+ const secureCredentialsService = SecureCredentialsService.getInstance();
+
+ // Générer la clé PBKDF2 avec le mode de sécurité choisi
+ const pbkdf2Key = await secureCredentialsService.generatePBKDF2Key(currentMode);
+ console.log('đ PBKDF2 key retrieved for wallet encryption');
+
+ // Chiffrer le wallet avec la clé PBKDF2
+ const encryptedWallet = await encryptionService.encryptWithPassword(
+ 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 "4nk" version 2...');
+ const db = await new Promise((resolve, reject) => {
+ const request = indexedDB.open('4nk', 2); // Utiliser la version existante
+ 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('wallet')) {
+ const store = db.createObjectStore('wallet', { keyPath: 'pre_id' });
+ console.log('â
Wallet store created with keyPath: pre_id');
+ } else {
+ console.log('â
Wallet store already exists');
+ }
+ };
+ });
+
+ // Ătape 1: Sauvegarder le wallet dans le format attendu par getDeviceFromDatabase
+ console.log('đ Opening transaction for wallet store...');
+ await new Promise((resolve, reject) => {
+ const transaction = db.transaction(['wallet'], 'readwrite');
+ const store = transaction.objectStore('wallet');
+ console.log('đ Store opened:', store.name);
+
+ // Créer un device temporaire avec l'état birthday_waiting
+ // Utiliser le wallet chiffré au lieu des données en clair
+ const device = {
+ sp_wallet: {
+ // Stocker le wallet chiffré au lieu des clés en clair
+ encrypted_data: encryptedWallet,
+ network: walletData.network,
+ birthday: 0, // Sera mis Ă jour dans birthday-setup
+ last_scan: 0,
+ state: 'birthday_waiting'
+ },
+ sp_client: {
+ // Structure complÚte pour éviter l'erreur "missing field sp_client"
+ initialized: false,
+ // Ajouter d'autres champs nécessaires
+ version: 1,
+ capabilities: []
+ },
+ created_at: walletData.created_at
+ };
+
+ // Stocker dans le format attendu par getDeviceFromDatabase
+ const walletObject = {
+ pre_id: '1',
+ device: device
+ };
+
+ console.log('đ Attempting to save wallet object:', walletObject);
+ // Le store utilise des clés out-of-line, fournir une clé explicite
+ const request = store.put(walletObject, '1');
+ 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((resolve, reject) => {
+ const transaction = db.transaction(['wallet'], 'readonly');
+ const store = transaction.objectStore('wallet');
+
+ const verificationRequest = store.get('1');
+ verificationRequest.onsuccess = () => {
+ console.log('đ Verification result:', verificationRequest.result);
+ if (verificationRequest.result) {
+ console.log('â
Wallet verification: Found in IndexedDB with key "1"');
+ console.log('đ Device state:', verificationRequest.result.device.sp_wallet.state);
+ console.log('đ Encrypted data present:', !!verificationRequest.result.device.sp_wallet.encrypted_data);
+ 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);
+ };
+ });
+
+ // Sauvegarder le mode de sécurité choisi
+ console.log('đ Saving security mode:', currentMode);
+ await securityModeService.setSecurityMode(currentMode);
+
+ // Vérifier que le mode de sécurité est bien sauvegardé
+ const savedMode = await securityModeService.getCurrentMode();
+ if (savedMode === currentMode) {
+ console.log('â
Security mode saved and verified:', savedMode);
+ } else {
+ console.error('â Security mode verification failed. Expected:', currentMode, 'Got:', savedMode);
+ throw new Error('Security mode verification failed');
+ }
+
+ // 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((resolve, reject) => {
+ const transaction = db.transaction(['wallet'], 'readonly');
+ const store = transaction.objectStore('wallet');
+ const request = store.get('1');
+ request.onsuccess = () => resolve(request.result);
+ request.onerror = () => reject(request.error);
+ });
+
+ if (finalVerification && finalVerification.device) {
+ console.log('â
Wallet saved exclusively in IndexedDB and verified');
+ console.log('đ Wallet contains:', {
+ hasSpWallet: !!finalVerification.device.sp_wallet,
+ hasSpClient: !!finalVerification.device.sp_client,
+ state: finalVerification.device.sp_wallet?.state,
+ hasEncryptedData: !!finalVerification.device.sp_wallet?.encrypted_data
+ });
+ } 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';
+ });
+});
\ No newline at end of file
diff --git a/src/router.ts b/src/router.ts
index 972353c..a7e56eb 100755
--- a/src/router.ts
+++ b/src/router.ts
@@ -138,6 +138,19 @@ export async function init(): Promise {
try {
console.log('đ Starting application initialization...');
+ // ĂTAPE 1: GESTION DE LA SĂCURITĂ (clĂ©s de sĂ©curitĂ©) - EN PREMIER
+ console.log('đ Step 1: Security key management...');
+ const securityConfigured = await handleSecurityKeyManagement();
+
+ if (!securityConfigured) {
+ console.log('đ Security not configured, redirecting to home for setup...');
+ // Naviguer directement vers home pour la configuration de sécurité
+ handleLocation('home');
+ return;
+ }
+
+ // ĂTAPE 2: INITIALISATION DES SERVICES (seulement aprĂšs la sĂ©curitĂ©)
+ console.log('đ§ Step 2: Initializing services...');
const services = await Services.getInstance();
(window as any).myService = services;
const db = await Database.getInstance();
@@ -146,6 +159,29 @@ export async function init(): Promise {
console.log('đ± Registering service worker...');
await db.registerServiceWorker('/src/service-workers/database.worker.js');
+ // ĂTAPE 3: CONNEXION AUX RELAIS (pour la hauteur de blocs)
+ console.log('đ Step 3: Connecting to relays for block height...');
+ try {
+ console.log('đ Connecting to relays...');
+ services.updateUserStatus('đ Connecting to blockchain relays...');
+ await services.connectAllRelays();
+ console.log('â
Relays connected successfully');
+ services.updateUserStatus('â
Connected to blockchain relays');
+
+ // CRITICAL: Wait for handshake to be processed and block height to be set
+ console.log('âł Waiting for relay handshake to complete...');
+ services.updateUserStatus('âł Waiting for blockchain synchronization...');
+ await services.waitForBlockHeight();
+ console.log('â
Block height received from relay');
+ services.updateUserStatus('â
Blockchain synchronized');
+ } catch (error) {
+ console.warn('â ïž Failed to connect to some relays:', error);
+ console.log('đ Continuing despite relay connection issues...');
+ services.updateUserStatus('â ïž Some relays unavailable, continuing...');
+ }
+
+ // ĂTAPE 4: GĂNĂRATION/CHARGEMENT DU WALLET
+ console.log('đ° Step 4: Wallet generation/loading...');
const device = await services.getDeviceFromDatabase();
console.log('đ ~ device:', device);
@@ -163,46 +199,24 @@ export async function init(): Promise {
services.updateUserStatus('â
Wallet restored successfully');
}
- // Restore data from database (these operations can fail, so we handle them separately)
- try {
- console.log('đ Restoring processes from database...');
- await services.restoreProcessesFromDB();
- } catch (error) {
- console.warn('â ïž Failed to restore processes from database:', error);
- }
+ // CRITICAL: Now that block height is set, synchronize wallet
+ console.log('đ Synchronizing wallet with blockchain...');
+ services.updateUserStatus('đ Synchronizing wallet with blockchain...');
+ await services.updateDeviceBlockHeight();
+ console.log('â
Wallet synchronization completed');
+ services.updateUserStatus('â
Wallet synchronized successfully');
- try {
- console.log('đ Restoring secrets from database...');
- await services.restoreSecretsFromDB();
- } catch (error) {
- console.warn('â ïž Failed to restore secrets from database:', error);
- }
+ // ĂTAPE 5: HANDSHAKE
+ console.log('đ€ Step 5: Performing handshake...');
+ await performHandshake(services);
- try {
- console.log('đ Connecting to relays...');
- services.updateUserStatus('đ Connecting to blockchain relays...');
- await services.connectAllRelays();
- console.log('â
Relays connected successfully');
- services.updateUserStatus('â
Connected to blockchain relays');
+ // ĂTAPE 6: PAIRING
+ console.log('đ Step 6: Device pairing...');
+ await handlePairing(services);
- // CRITICAL: Wait for handshake to be processed and block height to be set
- console.log('âł Waiting for relay handshake to complete...');
- services.updateUserStatus('âł Waiting for blockchain synchronization...');
- await services.waitForBlockHeight();
- console.log('â
Block height received from relay');
- services.updateUserStatus('â
Blockchain synchronized');
-
- // CRITICAL: Now that block height is set, synchronize wallet
- console.log('đ Synchronizing wallet with blockchain...');
- services.updateUserStatus('đ Synchronizing wallet with blockchain...');
- await services.updateDeviceBlockHeight();
- console.log('â
Wallet synchronization completed');
- services.updateUserStatus('â
Wallet synchronized successfully');
- } catch (error) {
- console.warn('â ïž Failed to connect to some relays:', error);
- console.log('đ Continuing despite relay connection issues...');
- services.updateUserStatus('â ïž Some relays unavailable, continuing...');
- }
+ // ĂTAPE 7: ĂCOUTE DES PROCESSUS
+ console.log('đ Step 7: Starting process listening...');
+ await startProcessListening(services);
// We register all the event listeners if we run in an iframe
if (window.self !== window.top) {
@@ -1187,3 +1201,124 @@ document.addEventListener('navigate', (e: Event) => {
}
}
});
+
+/**
+ * ĂTAPE 2: Gestion de la sĂ©curitĂ© (clĂ©s de sĂ©curitĂ©)
+ * Cette Ă©tape doit ĂȘtre la premiĂšre et rien d'autre ne doit s'exĂ©cuter en parallĂšle
+ */
+async function handleSecurityKeyManagement(): Promise {
+ console.log('đ Starting security key management...');
+
+ try {
+ // Vérifier d'abord si un mode de sécurité est configuré
+ const { SecurityModeService } = await import('./services/security-mode.service');
+ const securityModeService = SecurityModeService.getInstance();
+
+ const currentMode = await securityModeService.getCurrentMode();
+
+ if (!currentMode) {
+ console.log('đ No security mode configured, redirecting to security setup...');
+ window.location.href = '/src/pages/security-setup/security-setup.html';
+ return false;
+ }
+
+ console.log('đ Security mode configured:', currentMode);
+
+ // Vérifier si des credentials existent
+ 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, redirecting to wallet setup...');
+ window.location.href = '/src/pages/wallet-setup/wallet-setup.html';
+ return false;
+ } else {
+ console.log('đ Security credentials found, verifying access...');
+ // Vérifier l'accÚs aux credentials
+ const credentials = await secureCredentialsService.retrieveCredentials('4nk-secure-password');
+ if (!credentials) {
+ console.log('â Failed to access security credentials');
+ window.location.href = '/src/pages/wallet-setup/wallet-setup.html';
+ return false;
+ }
+ console.log('â
Security credentials verified');
+ return true;
+ }
+ } catch (error) {
+ console.error('â Security key management failed:', error);
+ console.log('đ Redirecting to security setup...');
+ window.location.href = '/src/pages/security-setup/security-setup.html';
+ return false;
+ }
+}
+
+/**
+ * ĂTAPE 5: Handshake
+ */
+async function performHandshake(services: any): Promise {
+ console.log('đ€ Performing handshake...');
+
+ try {
+ // Le handshake est déjà fait lors de la connexion aux relais
+ // Cette fonction peut ĂȘtre Ă©tendue pour des handshakes supplĂ©mentaires
+ console.log('â
Handshake completed');
+ } catch (error) {
+ console.error('â Handshake failed:', error);
+ throw error;
+ }
+}
+
+/**
+ * ĂTAPE 6: Pairing
+ */
+async function handlePairing(services: any): Promise {
+ console.log('đ Handling device pairing...');
+
+ try {
+ // Vérifier le statut de pairing
+ const isPaired = services.isPaired();
+ console.log('đ Device pairing status:', isPaired ? 'Paired' : 'Not paired');
+
+ if (!isPaired) {
+ console.log('â ïž Device not paired, user must complete pairing...');
+ // Le pairing sera géré par la page home
+ return;
+ } else {
+ console.log('â
Device is already paired');
+ }
+ } catch (error) {
+ console.error('â Pairing handling failed:', error);
+ throw error;
+ }
+}
+
+/**
+ * ĂTAPE 7: Ăcoute des processus
+ */
+async function startProcessListening(services: any): Promise {
+ console.log('đ Starting process listening...');
+
+ try {
+ // Restore data from database (these operations can fail, so we handle them separately)
+ try {
+ console.log('đ Restoring processes from database...');
+ await services.restoreProcessesFromDB();
+ } catch (error) {
+ console.warn('â ïž Failed to restore processes from database:', error);
+ }
+
+ try {
+ console.log('đ Restoring secrets from database...');
+ await services.restoreSecretsFromDB();
+ } catch (error) {
+ console.warn('â ïž Failed to restore secrets from database:', error);
+ }
+
+ console.log('â
Process listening started');
+ } catch (error) {
+ console.error('â Process listening failed:', error);
+ throw error;
+ }
+}
diff --git a/src/services/credentials/README.md b/src/services/credentials/README.md
new file mode 100644
index 0000000..0d76c8b
--- /dev/null
+++ b/src/services/credentials/README.md
@@ -0,0 +1,52 @@
+# Services de Credentials
+
+Cette structure modulaire organise les services de gestion des credentials de maniĂšre claire et maintenable.
+
+## Structure
+
+```
+credentials/
+âââ index.ts # Point d'entrĂ©e centralisĂ©
+âââ types.ts # Types et interfaces
+âââ webauthn.service.ts # Gestion WebAuthn
+âââ encryption.service.ts # Gestion du chiffrement
+âââ storage.service.ts # Gestion du stockage
+âââ README.md # Documentation
+```
+
+## Services
+
+### SecureCredentialsService
+Service principal qui orchestre tous les autres services. GÚre la logique métier et la coordination entre les modules.
+
+### WebAuthnService
+- Détection des authentificateurs disponibles
+- Création et utilisation de credentials WebAuthn
+- Support spécifique pour Proton Pass
+
+### EncryptionService
+- Génération de clés aléatoires
+- Chiffrement/déchiffrement avec PBKDF2 + AES-GCM
+- Chiffrement/déchiffrement avec WebAuthn
+
+### StorageService
+- Stockage sécurisé dans IndexedDB
+- Gestion des credentials persistants
+- Opérations CRUD sur les credentials
+
+## Avantages de cette structure
+
+1. **Séparation des responsabilités** : Chaque service a une responsabilité claire
+2. **Maintenabilité** : Code plus facile à comprendre et modifier
+3. **TestabilitĂ©** : Chaque service peut ĂȘtre testĂ© indĂ©pendamment
+4. **RĂ©utilisabilitĂ©** : Les services peuvent ĂȘtre utilisĂ©s sĂ©parĂ©ment
+5. **Lisibilité** : Structure claire et organisée
+
+## Utilisation
+
+```typescript
+import { SecureCredentialsService } from './credentials';
+
+const service = SecureCredentialsService.getInstance();
+const credentials = await service.generateSecureCredentials('password');
+```
diff --git a/src/services/credentials/credential-types.ts b/src/services/credentials/credential-types.ts
new file mode 100644
index 0000000..e04ea3c
--- /dev/null
+++ b/src/services/credentials/credential-types.ts
@@ -0,0 +1,32 @@
+/**
+ * Types et interfaces pour la gestion des credentials
+ * Updated: 2025-10-24 - Fresh file with new name
+ */
+
+export interface CredentialData {
+ spendKey: string;
+ scanKey: string;
+ salt: Uint8Array;
+ iterations: number;
+ timestamp: number;
+ webAuthnCredentialId?: string;
+ webAuthnPublicKey?: number[];
+}
+
+export interface CredentialOptions {
+ iterations?: number;
+ saltLength?: number;
+ keyLength?: number;
+}
+
+export interface WebAuthnCredential {
+ id: string;
+ publicKey: number[];
+ privateKey?: ArrayBuffer;
+}
+
+export interface EncryptionResult {
+ encryptedData: string;
+ credentialId: string;
+ publicKey: number[];
+}
diff --git a/src/services/credentials/encryption.service.ts b/src/services/credentials/encryption.service.ts
new file mode 100644
index 0000000..7b1eb05
--- /dev/null
+++ b/src/services/credentials/encryption.service.ts
@@ -0,0 +1,242 @@
+/**
+ * EncryptionService - Gestion du chiffrement des credentials
+ */
+import { secureLogger } from '../secure-logger';
+import { CredentialData, CredentialOptions } from './types';
+
+export class EncryptionService {
+ private static instance: EncryptionService;
+ private readonly defaultOptions: Required = {
+ iterations: 100000,
+ saltLength: 32,
+ keyLength: 32
+ };
+
+ private constructor() {}
+
+ public static getInstance(): EncryptionService {
+ if (!EncryptionService.instance) {
+ EncryptionService.instance = new EncryptionService();
+ }
+ return EncryptionService.instance;
+ }
+
+ /**
+ * GénÚre des clés aléatoires
+ */
+ generateRandomKeys(): { spendKey: string; scanKey: string } {
+ const spendKey = crypto.getRandomValues(new Uint8Array(32));
+ const scanKey = crypto.getRandomValues(new Uint8Array(32));
+
+ return {
+ spendKey: Array.from(spendKey).map(b => b.toString(16).padStart(2, '0')).join(''),
+ scanKey: Array.from(scanKey).map(b => b.toString(16).padStart(2, '0')).join('')
+ };
+ }
+
+ /**
+ * GénÚre une clé PBKDF2 aléatoire
+ */
+ generateRandomKey(): string {
+ const keyBytes = crypto.getRandomValues(new Uint8Array(32));
+ return Array.from(keyBytes).map(b => b.toString(16).padStart(2, '0')).join('');
+ }
+
+ /**
+ * Chiffre une clé avec un mot de passe
+ */
+ async encryptWithPassword(key: string, password: string): Promise {
+ try {
+ const salt = crypto.getRandomValues(new Uint8Array(16));
+ const keyMaterial = await crypto.subtle.importKey(
+ 'raw',
+ new TextEncoder().encode(password),
+ '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,
+ ['encrypt']
+ );
+
+ const iv = crypto.getRandomValues(new Uint8Array(12));
+ const encrypted = await crypto.subtle.encrypt(
+ { name: 'AES-GCM', iv: iv },
+ cryptoKey,
+ new TextEncoder().encode(key)
+ );
+
+ // Combiner salt + iv + données chiffrées
+ const combined = new Uint8Array(salt.length + iv.length + encrypted.byteLength);
+ combined.set(salt, 0);
+ combined.set(iv, salt.length);
+ combined.set(new Uint8Array(encrypted), salt.length + iv.length);
+
+ return btoa(String.fromCharCode(...combined));
+ } catch (error) {
+ secureLogger.error('Failed to encrypt with password', {
+ component: 'EncryptionService',
+ operation: 'encryptWithPassword',
+ error: error instanceof Error ? error.message : String(error)
+ });
+ throw error;
+ }
+ }
+
+ /**
+ * Chiffre des données avec PBKDF2 + AES-GCM (méthode originale)
+ */
+ async encryptWithPasswordOriginal(
+ data: string,
+ password: string,
+ options: CredentialOptions = {}
+ ): Promise<{ encryptedData: string; salt: Uint8Array; iterations: number }> {
+ const opts = { ...this.defaultOptions, ...options };
+ const salt = crypto.getRandomValues(new Uint8Array(opts.saltLength));
+
+ // Dériver la clé avec PBKDF2
+ const keyMaterial = await crypto.subtle.importKey(
+ 'raw',
+ new TextEncoder().encode(password),
+ 'PBKDF2',
+ false,
+ ['deriveBits', 'deriveKey']
+ );
+
+ const key = await crypto.subtle.deriveKey(
+ {
+ name: 'PBKDF2',
+ salt: salt,
+ iterations: opts.iterations,
+ hash: 'SHA-256'
+ },
+ keyMaterial,
+ { name: 'AES-GCM', length: 256 },
+ false,
+ ['encrypt', 'decrypt']
+ );
+
+ // Chiffrer les données
+ const iv = crypto.getRandomValues(new Uint8Array(12));
+ const encrypted = await crypto.subtle.encrypt(
+ { name: 'AES-GCM', iv: iv },
+ key,
+ new TextEncoder().encode(data)
+ );
+
+ // Combiner IV + données chiffrées
+ const combined = new Uint8Array(iv.length + encrypted.byteLength);
+ combined.set(iv);
+ combined.set(new Uint8Array(encrypted), iv.length);
+
+ return {
+ encryptedData: Array.from(combined).map(b => b.toString(16).padStart(2, '0')).join(''),
+ salt,
+ iterations: opts.iterations
+ };
+ }
+
+ /**
+ * Déchiffre des données avec PBKDF2 + AES-GCM
+ */
+ async decryptWithPassword(
+ encryptedData: string,
+ password: string,
+ salt: Uint8Array,
+ iterations: number
+ ): Promise {
+ // Dériver la clé avec PBKDF2
+ const keyMaterial = await crypto.subtle.importKey(
+ 'raw',
+ new TextEncoder().encode(password),
+ 'PBKDF2',
+ false,
+ ['deriveBits', 'deriveKey']
+ );
+
+ const key = await crypto.subtle.deriveKey(
+ {
+ name: 'PBKDF2',
+ salt: salt,
+ iterations: iterations,
+ hash: 'SHA-256'
+ },
+ keyMaterial,
+ { name: 'AES-GCM', length: 256 },
+ false,
+ ['encrypt', 'decrypt']
+ );
+
+ // Convertir les données hexadécimales
+ const combined = new Uint8Array(encryptedData.match(/.{2}/g)!.map(byte => parseInt(byte, 16)));
+
+ // Séparer IV et données chiffrées
+ const iv = combined.slice(0, 12);
+ const encrypted = combined.slice(12);
+
+ // Déchiffrer
+ const decrypted = await crypto.subtle.decrypt(
+ { name: 'AES-GCM', iv: iv },
+ key,
+ encrypted
+ );
+
+ return new TextDecoder().decode(decrypted);
+ }
+
+
+ /**
+ * Chiffre des credentials avec WebAuthn
+ */
+ async encryptWithWebAuthn(
+ credentials: CredentialData,
+ credentialId: string
+ ): Promise {
+ const data = JSON.stringify({
+ spendKey: credentials.spendKey,
+ scanKey: credentials.scanKey,
+ timestamp: credentials.timestamp
+ });
+
+ // Pour l'instant, on utilise un chiffrement simple
+ // Dans une vraie implémentation, on utiliserait la clé publique WebAuthn
+ const encoded = btoa(data);
+ return encoded;
+ }
+
+ /**
+ * Déchiffre des credentials avec WebAuthn
+ */
+ async decryptWithWebAuthn(
+ encryptedData: string,
+ credentialId: string
+ ): Promise {
+ // Pour l'instant, on utilise un déchiffrement simple
+ const decoded = atob(encryptedData);
+ const data = JSON.parse(decoded);
+
+ return {
+ spendKey: data.spendKey,
+ scanKey: data.scanKey,
+ salt: new Uint8Array(0),
+ iterations: 0,
+ timestamp: data.timestamp
+ };
+ }
+}
diff --git a/src/services/credentials/storage.service.ts b/src/services/credentials/storage.service.ts
new file mode 100644
index 0000000..9f5718a
--- /dev/null
+++ b/src/services/credentials/storage.service.ts
@@ -0,0 +1,282 @@
+/**
+ * StorageService - Gestion du stockage des credentials
+ */
+import { secureLogger } from '../secure-logger';
+import { CredentialData } from './types';
+
+export class StorageService {
+ private static instance: StorageService;
+ private dbName = '4nk';
+ private storeName = 'credentials'; // Store séparé pour les clés PBKDF2
+ private dbVersion = 2;
+
+ private constructor() {}
+
+ public static getInstance(): StorageService {
+ if (!StorageService.instance) {
+ StorageService.instance = new StorageService();
+ }
+ return StorageService.instance;
+ }
+
+ /**
+ * Stocke une clé dans le gestionnaire de mots de passe du navigateur
+ */
+ async storeKeyInBrowser(key: string): Promise {
+ try {
+ secureLogger.info('Storing key in browser password manager', {
+ component: 'StorageService',
+ operation: 'storeKeyInBrowser'
+ });
+
+ // IMPORTANT: Ne jamais stocker la clé PBKDF2 en clair !
+ // Utiliser l'API Credential Management pour stocker comme mot de passe
+ // Le navigateur chiffrera automatiquement le mot de passe
+ const credential = new PasswordCredential({
+ id: 'lecoffre-pbkdf2-key',
+ password: key, // Le navigateur chiffre automatiquement ce mot de passe
+ name: 'LeCoffre PBKDF2 Key'
+ });
+
+ await navigator.credentials.store(credential);
+
+ secureLogger.info('Key stored in browser password manager successfully (encrypted by browser)', {
+ component: 'StorageService',
+ operation: 'storeKeyInBrowser'
+ });
+
+ } catch (error) {
+ secureLogger.error('Failed to store key in browser password manager', {
+ component: 'StorageService',
+ operation: 'storeKeyInBrowser',
+ error: error instanceof Error ? error.message : String(error)
+ });
+ throw error;
+ }
+ }
+
+
+ /**
+ * Stocke un wallet chiffré
+ */
+ async storeEncryptedWallet(encryptedWallet: string): Promise {
+ try {
+ secureLogger.info('Storing encrypted wallet', {
+ component: 'StorageService',
+ operation: 'storeEncryptedWallet'
+ });
+
+ const db = await this.openDatabase();
+ const transaction = db.transaction([this.storeName], 'readwrite');
+ const store = transaction.objectStore(this.storeName);
+
+ await new Promise((resolve, reject) => {
+ const request = store.put(encryptedWallet, 'encrypted-wallet');
+ request.onsuccess = () => resolve();
+ request.onerror = () => reject(request.error);
+ });
+
+ secureLogger.info('Encrypted wallet stored successfully', {
+ component: 'StorageService',
+ operation: 'storeEncryptedWallet'
+ });
+ } catch (error) {
+ secureLogger.error('Failed to store encrypted wallet', {
+ component: 'StorageService',
+ operation: 'storeEncryptedWallet',
+ error: error instanceof Error ? error.message : String(error)
+ });
+ throw error;
+ }
+ }
+
+ /**
+ * Stocke une clé en clair (non recommandé)
+ */
+ async storePlainKey(key: string): Promise {
+ try {
+ secureLogger.warn('Storing key in plain text (not recommended)', {
+ component: 'StorageService',
+ operation: 'storePlainKey'
+ });
+
+ const db = await this.openDatabase();
+ const transaction = db.transaction([this.storeName], 'readwrite');
+ const store = transaction.objectStore(this.storeName);
+
+ await new Promise((resolve, reject) => {
+ const request = store.put(key, 'plain-pbkdf2-key');
+ request.onsuccess = () => resolve();
+ request.onerror = () => reject(request.error);
+ });
+
+ secureLogger.info('Plain key stored successfully', {
+ component: 'StorageService',
+ operation: 'storePlainKey'
+ });
+
+ } catch (error) {
+ secureLogger.error('Failed to store plain key', {
+ component: 'StorageService',
+ operation: 'storePlainKey',
+ error: error instanceof Error ? error.message : String(error)
+ });
+ throw error;
+ }
+ }
+
+ /**
+ * Ouvre la base de données IndexedDB
+ */
+ private async openDatabase(): Promise {
+ return new Promise((resolve, reject) => {
+ const request = indexedDB.open(this.dbName, this.dbVersion);
+
+ request.onerror = () => {
+ secureLogger.error('Failed to open IndexedDB', new Error('Database open failed'), {
+ component: 'StorageService',
+ operation: 'openDatabase'
+ });
+ reject(request.error);
+ };
+
+ request.onsuccess = () => {
+ secureLogger.info('IndexedDB opened successfully', {
+ component: 'StorageService',
+ operation: 'openDatabase'
+ });
+ resolve(request.result);
+ };
+
+ request.onupgradeneeded = (event) => {
+ const db = (event.target as IDBOpenDBRequest).result;
+ if (!db.objectStoreNames.contains(this.storeName)) {
+ db.createObjectStore(this.storeName);
+ secureLogger.info('IndexedDB upgrade needed, creating credentials store', {
+ component: 'StorageService',
+ operation: 'openDatabase'
+ });
+ }
+ if (!db.objectStoreNames.contains('wallet')) {
+ db.createObjectStore('wallet');
+ secureLogger.info('IndexedDB upgrade needed, creating wallet store', {
+ component: 'StorageService',
+ operation: 'openDatabase'
+ });
+ }
+ };
+ });
+ }
+
+ /**
+ * Stocke des credentials
+ */
+ async storeCredentials(credentials: CredentialData): Promise {
+ try {
+ const db = await this.openDatabase();
+ const transaction = db.transaction([this.storeName], 'readwrite');
+ const store = transaction.objectStore(this.storeName);
+
+ await new Promise((resolve, reject) => {
+ const request = store.put(credentials, 'current');
+ request.onsuccess = () => resolve();
+ request.onerror = () => reject(request.error);
+ });
+
+ secureLogger.info('Credentials stored successfully', {
+ component: 'StorageService',
+ operation: 'storeCredentials'
+ });
+ } catch (error) {
+ secureLogger.error('Failed to store credentials', error as Error, {
+ component: 'StorageService',
+ operation: 'storeCredentials'
+ });
+ throw error;
+ }
+ }
+
+ /**
+ * RécupÚre des credentials
+ */
+ async getCredentials(): Promise {
+ try {
+ const db = await this.openDatabase();
+ const transaction = db.transaction([this.storeName], 'readonly');
+ const store = transaction.objectStore(this.storeName);
+
+ return new Promise((resolve, reject) => {
+ const request = store.get('current');
+ request.onsuccess = () => {
+ const result = request.result;
+ if (result) {
+ secureLogger.info('Credentials retrieved successfully', {
+ component: 'StorageService',
+ operation: 'getCredentials'
+ });
+ resolve(result);
+ } else {
+ secureLogger.info('No credentials found', {
+ component: 'StorageService',
+ operation: 'getCredentials'
+ });
+ resolve(null);
+ }
+ };
+ request.onerror = () => reject(request.error);
+ });
+ } catch (error) {
+ secureLogger.error('Failed to get credentials', error as Error, {
+ component: 'StorageService',
+ operation: 'getCredentials'
+ });
+ return null;
+ }
+ }
+
+ /**
+ * Vérifie si des credentials existent
+ */
+ async hasCredentials(): Promise {
+ try {
+ const credentials = await this.getCredentials();
+ return credentials !== null &&
+ credentials.spendKey !== undefined &&
+ credentials.scanKey !== undefined;
+ } catch (error) {
+ secureLogger.error('Failed to check credentials existence', error as Error, {
+ component: 'StorageService',
+ operation: 'hasCredentials'
+ });
+ return false;
+ }
+ }
+
+ /**
+ * Supprime les credentials
+ */
+ async clearCredentials(): Promise {
+ try {
+ const db = await this.openDatabase();
+ const transaction = db.transaction([this.storeName], 'readwrite');
+ const store = transaction.objectStore(this.storeName);
+
+ await new Promise((resolve, reject) => {
+ const request = store.delete('current');
+ request.onsuccess = () => resolve();
+ request.onerror = () => reject(request.error);
+ });
+
+ secureLogger.info('Credentials cleared successfully', {
+ component: 'StorageService',
+ operation: 'clearCredentials'
+ });
+ } catch (error) {
+ secureLogger.error('Failed to clear credentials', error as Error, {
+ component: 'StorageService',
+ operation: 'clearCredentials'
+ });
+ throw error;
+ }
+ }
+}
diff --git a/src/services/credentials/types.ts b/src/services/credentials/types.ts
new file mode 100644
index 0000000..640fdb3
--- /dev/null
+++ b/src/services/credentials/types.ts
@@ -0,0 +1,32 @@
+/**
+ * Types et interfaces pour la gestion des credentials
+ * Updated: 2025-10-24 - Fresh file
+ */
+
+export interface CredentialData {
+ spendKey: string;
+ scanKey: string;
+ salt: Uint8Array;
+ iterations: number;
+ timestamp: number;
+ webAuthnCredentialId?: string;
+ webAuthnPublicKey?: number[];
+}
+
+export interface CredentialOptions {
+ iterations?: number;
+ saltLength?: number;
+ keyLength?: number;
+}
+
+export interface WebAuthnCredential {
+ id: string;
+ publicKey: number[];
+ privateKey?: ArrayBuffer;
+}
+
+export interface EncryptionResult {
+ encryptedData: string;
+ credentialId: string;
+ publicKey: number[];
+}
\ No newline at end of file
diff --git a/src/services/credentials/webauthn.service.ts b/src/services/credentials/webauthn.service.ts
new file mode 100644
index 0000000..814924e
--- /dev/null
+++ b/src/services/credentials/webauthn.service.ts
@@ -0,0 +1,403 @@
+/**
+ * WebAuthnService - Gestion des opérations WebAuthn
+ */
+import { secureLogger } from '../secure-logger';
+import { SecurityMode } from '../security-mode.service';
+import { WebAuthnCredential, EncryptionResult } from './types';
+
+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é avec WebAuthn
+ */
+ async storeKeyWithWebAuthn(key: string, securityMode: SecurityMode): Promise {
+ try {
+ secureLogger.info('Storing key with WebAuthn', {
+ component: 'WebAuthnService',
+ operation: 'storeKeyWithWebAuthn',
+ securityMode
+ });
+
+ // Créer des credentials WebAuthn pour stocker la clé
+ const credential = await this.createCredentials('4nk-secure-password', securityMode);
+
+ // Stocker la clé chiffrée avec les credentials WebAuthn
+ const encryptedKey = await this.encryptKeyWithWebAuthn(key, credential);
+
+ // Sauvegarder dans IndexedDB
+ await this.saveEncryptedKey(encryptedKey, credential.id, securityMode);
+
+ secureLogger.info('Key stored with WebAuthn successfully', {
+ component: 'WebAuthnService',
+ operation: 'storeKeyWithWebAuthn'
+ });
+
+ } catch (error) {
+ secureLogger.error('Failed to store key with WebAuthn', {
+ component: 'WebAuthnService',
+ operation: 'storeKeyWithWebAuthn',
+ error: error instanceof Error ? error.message : String(error)
+ });
+ throw error;
+ }
+ }
+
+ /**
+ * Détecte les authentificateurs disponibles
+ */
+ async detectAvailableAuthenticators(): Promise {
+ 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 {
+ 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 {
+ 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);
+
+ return {
+ id: Array.from(new Uint8Array(credential.rawId)).map(b => b.toString(16).padStart(2, '0')).join(''),
+ publicKey: Array.from(new Uint8Array((credential.response as AuthenticatorAttestationResponse).publicKey!))
+ };
+ } catch (error) {
+ console.error('â WebAuthn credential creation failed:', error);
+
+ // Message d'erreur spécifique pour Proton Pass
+ if (mode === 'proton-pass') {
+ throw new Error(`Proton Pass authentication failed: ${error.message}. Please ensure Proton Pass is installed and enabled.`);
+ }
+
+ throw error;
+ }
+ }
+
+ /**
+ * Chiffre une clé avec WebAuthn
+ */
+ private async encryptKeyWithWebAuthn(key: string, credential: WebAuthnCredential): Promise {
+ 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', {
+ component: 'WebAuthnService',
+ operation: 'encryptKeyWithWebAuthn',
+ error: error instanceof Error ? error.message : String(error)
+ });
+ throw error;
+ }
+ }
+
+ /**
+ * Sauvegarde une clé chiffrée dans IndexedDB
+ */
+ private async saveEncryptedKey(encryptedKey: string, credentialId: string, securityMode: SecurityMode): Promise {
+ try {
+ const db = await this.openDatabase();
+ const transaction = db.transaction(['webauthn-keys'], 'readwrite');
+ const store = transaction.objectStore('webauthn-keys');
+
+ await new Promise((resolve, reject) => {
+ // Stocker seulement la clĂ© chiffrĂ©e (pas de credentialId au mĂȘme endroit)
+ const request = store.put({
+ encryptedKey,
+ securityMode: securityMode, // Mode de sécurité dynamique
+ timestamp: Date.now()
+ }, 'pbkdf2-key');
+ request.onsuccess = () => resolve();
+ request.onerror = () => reject(request.error);
+ });
+
+ // Ne pas stocker credentialId - il sera récupéré dynamiquement via WebAuthn
+ console.log('đ CredentialId will be retrieved dynamically via WebAuthn');
+
+ } catch (error) {
+ secureLogger.error('Failed to save encrypted key', {
+ component: 'WebAuthnService',
+ operation: 'saveEncryptedKey',
+ error: error instanceof Error ? error.message : String(error)
+ });
+ throw error;
+ }
+ }
+
+
+ /**
+ * RécupÚre une clé chiffrée avec WebAuthn (récupération dynamique)
+ */
+ async retrieveKeyWithWebAuthn(): Promise {
+ try {
+ // Récupérer la clé chiffrée depuis IndexedDB
+ const db = await this.openDatabase();
+ const transaction = db.transaction(['webauthn-keys'], 'readonly');
+ const store = transaction.objectStore('webauthn-keys');
+
+ const result = await new Promise((resolve, reject) => {
+ const request = store.get('pbkdf2-key');
+ request.onsuccess = () => resolve(request.result);
+ request.onerror = () => reject(request.error);
+ });
+
+ if (!result || !result.encryptedKey) {
+ console.log('đ No encrypted key found in WebAuthnKeysDB');
+ return null;
+ }
+
+ // Récupérer credentialId dynamiquement via WebAuthn
+ const credentialId = await this.getCurrentCredentialId();
+ if (!credentialId) {
+ console.log('đ No WebAuthn credential available');
+ return null;
+ }
+
+ // Déchiffrer la clé avec credentialId
+ const { EncryptionService } = await import('./encryption.service');
+ const encryptionService = EncryptionService.getInstance();
+
+ const decryptedKey = await encryptionService.decryptWithPassword(result.encryptedKey, credentialId);
+ console.log('đ Key decrypted with WebAuthn credential');
+ return decryptedKey;
+
+ } catch (error) {
+ secureLogger.error('Failed to retrieve key with WebAuthn', {
+ component: 'WebAuthnService',
+ operation: 'retrieveKeyWithWebAuthn',
+ error: error instanceof Error ? error.message : String(error)
+ });
+ return null;
+ }
+ }
+
+ /**
+ * RécupÚre l'ID de la credential WebAuthn actuelle
+ */
+ private async getCurrentCredentialId(): Promise {
+ 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 {
+ return new Promise((resolve, reject) => {
+ const request = indexedDB.open('WebAuthnKeysDB', 1);
+
+ request.onerror = () => {
+ secureLogger.error('Failed to open WebAuthn 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;
+ if (!db.objectStoreNames.contains('webauthn-keys')) {
+ db.createObjectStore('webauthn-keys');
+ }
+ };
+ });
+ }
+
+ /**
+ * Utilise des credentials WebAuthn existants
+ */
+ async useCredentials(
+ credentialId: string,
+ mode: SecurityMode
+ ): Promise {
+ 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;
+ }
+}
diff --git a/src/services/database.service.ts b/src/services/database.service.ts
index a59e318..8281326 100755
--- a/src/services/database.service.ts
+++ b/src/services/database.service.ts
@@ -4,7 +4,7 @@ export class Database {
private static instance: Database;
private db: IDBDatabase | null = null;
private dbName: string = '4nk';
- private dbVersion: number = 1;
+ private dbVersion: number = 2;
private serviceWorkerRegistration: ServiceWorkerRegistration | null = null;
private messageChannel: MessageChannel | null = null;
private messageChannelForGet: MessageChannel | null = null;
diff --git a/src/services/secure-credentials.service.ts b/src/services/secure-credentials.service.ts
index 7622e81..f5472a1 100644
--- a/src/services/secure-credentials.service.ts
+++ b/src/services/secure-credentials.service.ts
@@ -1,24 +1,13 @@
/**
- * SecureCredentialsService - Gestion sécurisée des credentials avec WebAuthn
- * Utilise WebAuthn pour chiffrer les clés privées de maniÚre sécurisée
+ * SecureCredentialsService - Service principal pour la gestion des credentials
+ * Utilise des modules spécialisés pour une meilleure organisation
*/
import { secureLogger } from './secure-logger';
+import { SecurityModeService, SecurityMode } from './security-mode.service';
+// Imports dynamiques pour éviter les problÚmes d'initialisation
+import { CredentialData, CredentialOptions } from './credentials/credential-types';
-export interface CredentialData {
- spendKey: string;
- scanKey: string;
- salt: Uint8Array;
- iterations: number;
- timestamp: number;
- webAuthnCredentialId?: string;
- webAuthnPublicKey?: number[];
-}
-
-export interface CredentialOptions {
- iterations?: number;
- saltLength?: number;
- keyLength?: number;
-}
+// Export des types - géré par les imports dynamiques
export class SecureCredentialsService {
private static instance: SecureCredentialsService;
@@ -28,7 +17,16 @@ export class SecureCredentialsService {
keyLength: 32
};
- private constructor() {}
+ // Protection contre les appels multiples
+ private isGeneratingCredentials = false;
+ private isRetrievingCredentials = false;
+
+ // Services spécialisés (importés dynamiquement)
+ private securityModeService: SecurityModeService;
+
+ private constructor() {
+ this.securityModeService = SecurityModeService.getInstance();
+ }
public static getInstance(): SecureCredentialsService {
if (!SecureCredentialsService.instance) {
@@ -38,559 +36,592 @@ export class SecureCredentialsService {
}
/**
- * GénÚre des credentials sécurisés avec WebAuthn comme clé de chiffrement
+ * GénÚre des credentials selon le mode de sécurisation sélectionné
*/
async generateSecureCredentials(
password: string,
_options: CredentialOptions = {}
): Promise {
- try {
- secureLogger.info('Generating secure credentials with WebAuthn encryption', {
+ // Protection contre les appels multiples
+ if (this.isGeneratingCredentials) {
+ secureLogger.warn('Credentials generation already in progress, skipping...', {
component: 'SecureCredentialsService',
operation: 'generateSecureCredentials'
});
+ throw new Error('Credentials generation already in progress');
+ }
- // VĂ©rifier si des credentials existent dĂ©jĂ
- const existingCredentials = await this.getEncryptedCredentials();
- if (existingCredentials) {
- console.log('đ Existing WebAuthn credentials found, reusing them...');
- secureLogger.info('Reusing existing WebAuthn credentials', {
+ this.isGeneratingCredentials = true;
+
+ try {
+ // Récupérer le mode de sécurisation actuel
+ let currentMode = await this.securityModeService.getCurrentMode();
+
+ if (!currentMode) {
+ secureLogger.warn('No security mode selected, using default mode: browser', {
component: 'SecureCredentialsService',
operation: 'generateSecureCredentials'
});
-
- // Retourner les credentials existants (déjà chiffrés)
- return {
- spendKey: existingCredentials.spendKey,
- scanKey: existingCredentials.scanKey,
- salt: new Uint8Array(0),
- iterations: 0,
- timestamp: existingCredentials.timestamp,
- webAuthnCredentialId: existingCredentials.webAuthnCredentialId,
- webAuthnPublicKey: existingCredentials.webAuthnPublicKey
- };
+ currentMode = 'browser';
+ await this.securityModeService.setSecurityMode(currentMode);
}
- // Vérifier que WebAuthn est disponible
- if (!navigator.credentials || !navigator.credentials.create) {
- throw new Error('WebAuthn not supported in this browser');
- }
+ const modeConfig = this.securityModeService.getSecurityModeConfig(currentMode);
- // Créer un challenge aléatoire
- const challenge = crypto.getRandomValues(new Uint8Array(32));
-
- // Créer les options WebAuthn pour la clé de chiffrement
- 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" }, // ES256
- { alg: -257, type: "public-key" } // RS256
- ],
- authenticatorSelection: {
- authenticatorAttachment: "platform", // Force l'authentificateur intégré
- userVerification: "required"
- },
- timeout: 300000, // 5 minutes timeout
- attestation: "direct"
- };
-
- console.log('đ Requesting WebAuthn credential creation for encryption key...');
-
- // Créer le credential WebAuthn avec gestion d'erreur robuste
- let credential: PublicKeyCredential;
- try {
- credential = await navigator.credentials.create({
- publicKey: publicKeyCredentialCreationOptions
- }) as PublicKeyCredential;
- } catch (error) {
- if (error instanceof Error) {
- if (error.name === 'NotAllowedError') {
- throw new Error('WebAuthn authentication was cancelled or timed out. Please try again and complete the authentication when prompted.');
- } else if (error.name === 'NotSupportedError') {
- throw new Error('WebAuthn is not supported in this browser. Please use a modern browser with WebAuthn support.');
- } else if (error.name === 'SecurityError') {
- throw new Error('WebAuthn security error. Please ensure you are using HTTPS and try again.');
- } else {
- throw new Error(`WebAuthn error: ${error.message}`);
- }
- }
- throw error;
- }
-
- if (!credential) {
- throw new Error('WebAuthn credential creation failed');
- }
-
- console.log('â
WebAuthn credential created successfully');
-
- // Extraire la clé publique pour le chiffrement
- const response = credential.response as AuthenticatorAttestationResponse;
- const publicKey = response.getPublicKey();
- const credentialId = credential.id;
-
- // Récupérer les clés du SDK générées par PBKDF2
- let spendKey: string;
- let scanKey: string;
-
- try {
- const device = await this.getDeviceFromSDK();
- if (device && device.sp_wallet && device.sp_wallet.spend_key && device.sp_wallet.scan_key) {
- // Utiliser les clés du SDK si disponibles
- spendKey = device.sp_wallet.spend_key;
- scanKey = device.sp_wallet.scan_key;
- console.log('â
Using SDK keys for encryption');
- } else {
- throw new Error('SDK keys not available');
- }
- } catch (error) {
- console.warn('â ïž SDK keys not available, generating custom keys:', error);
- // Fallback: générer des clés personnalisées
- spendKey = await this.generateSpendKey(password);
- scanKey = await this.generateScanKey(password);
- }
-
- // Chiffrer les clés avec la clé WebAuthn
- const encryptedSpendKey = await this.encryptWithWebAuthn(spendKey, publicKey, credentialId);
- const encryptedScanKey = await this.encryptWithWebAuthn(scanKey, publicKey, credentialId);
-
- const credentialData: CredentialData = {
- spendKey: encryptedSpendKey, // Clé chiffrée
- scanKey: encryptedScanKey, // Clé chiffrée
- salt: new Uint8Array(0), // Pas de salt avec WebAuthn
- iterations: 0, // Pas d'itérations avec WebAuthn
- timestamp: Date.now(),
- // Stocker les métadonnées WebAuthn pour le déchiffrement
- webAuthnCredentialId: credentialId,
- webAuthnPublicKey: Array.from(new Uint8Array(publicKey || new ArrayBuffer(32)))
- };
-
- secureLogger.info('WebAuthn encrypted credentials generated successfully', {
+ secureLogger.info('Generating credentials with security mode', {
component: 'SecureCredentialsService',
operation: 'generateSecureCredentials',
- hasSpendKey: !!encryptedSpendKey,
- hasScanKey: !!encryptedScanKey
+ mode: currentMode,
+ securityLevel: modeConfig.securityLevel,
+ useWebAuthn: modeConfig.implementation.useWebAuthn,
+ useEncryption: modeConfig.implementation.useEncryption
});
- return credentialData;
- } catch (error) {
- secureLogger.error('Failed to generate WebAuthn encrypted credentials', error instanceof Error ? error : new Error('Unknown error'), {
- component: 'SecureCredentialsService',
- operation: 'generateSecureCredentials'
- });
- throw error;
- }
- }
-
- /**
- * RécupÚre le device du SDK pour obtenir les clés générées par PBKDF2
- */
- private async getDeviceFromSDK(): Promise {
- try {
- // Importer le service pour accéder au SDK
- const serviceModule = await import('./service');
- const Services = serviceModule.default;
- const service = await Services.getInstance();
-
- // Vérifier que le SDK est initialisé
- if (!service.sdkClient) {
- throw new Error('SDK not initialized - cannot get device');
- }
-
- const device = service.dumpDeviceFromMemory();
- console.log('đ Device from SDK:', device);
- return device;
- } catch (error) {
- console.error('â Failed to get device from SDK:', error);
- throw error;
- }
- }
-
- /**
- * GénÚre une clé spend avec PBKDF2
- */
- private async generateSpendKey(password: string): Promise {
- const salt = crypto.getRandomValues(new Uint8Array(32));
- const keyMaterial = await crypto.subtle.importKey(
- 'raw',
- new TextEncoder().encode(password),
- 'PBKDF2',
- false,
- ['deriveBits']
- );
-
- const keyBits = await crypto.subtle.deriveBits(
- {
- name: 'PBKDF2',
- salt: salt,
- iterations: 100000,
- hash: 'SHA-256'
- },
- keyMaterial,
- 256
- );
-
- return Array.from(new Uint8Array(keyBits))
- .map(b => b.toString(16).padStart(2, '0'))
- .join('');
- }
-
- /**
- * GénÚre une clé scan avec PBKDF2
- */
- private async generateScanKey(password: string): Promise {
- const salt = crypto.getRandomValues(new Uint8Array(32));
- const keyMaterial = await crypto.subtle.importKey(
- 'raw',
- new TextEncoder().encode(password + 'scan'),
- 'PBKDF2',
- false,
- ['deriveBits']
- );
-
- const keyBits = await crypto.subtle.deriveBits(
- {
- name: 'PBKDF2',
- salt: salt,
- iterations: 100000,
- hash: 'SHA-256'
- },
- keyMaterial,
- 256
- );
-
- return Array.from(new Uint8Array(keyBits))
- .map(b => b.toString(16).padStart(2, '0'))
- .join('');
- }
-
- /**
- * Chiffre une clé privée avec WebAuthn
- */
- private async encryptWithWebAuthn(
- privateKey: string,
- publicKey: ArrayBuffer | null,
- credentialId: string
- ): Promise {
- if (!publicKey) {
- throw new Error('WebAuthn public key not available');
- }
-
- // Créer une clé de chiffrement à partir de la clé publique WebAuthn
- // Dériver une clé AES de 256 bits à partir de la clé publique
- const keyMaterial = await crypto.subtle.importKey(
- 'raw',
- publicKey,
- 'PBKDF2',
- false,
- ['deriveKey']
- );
-
- const encryptionKey = await crypto.subtle.deriveKey(
- {
- name: 'PBKDF2',
- salt: new Uint8Array(32), // Salt fixe pour la dérivation
- iterations: 100000,
- hash: 'SHA-256'
- },
- keyMaterial,
- { name: 'AES-GCM', length: 256 },
- false,
- ['encrypt']
- );
-
- // Générer un IV aléatoire
- const iv = crypto.getRandomValues(new Uint8Array(12));
-
- // Chiffrer la clé privée
- const encryptedData = await crypto.subtle.encrypt(
- { name: 'AES-GCM', iv },
- encryptionKey,
- new TextEncoder().encode(privateKey)
- );
-
- // Combiner IV + données chiffrées + credential ID
- const combined = new Uint8Array(iv.length + encryptedData.byteLength + credentialId.length);
- combined.set(iv, 0);
- combined.set(new Uint8Array(encryptedData), iv.length);
- combined.set(new TextEncoder().encode(credentialId), iv.length + encryptedData.byteLength);
-
- return Array.from(combined)
- .map(b => b.toString(16).padStart(2, '0'))
- .join('');
- }
-
- /**
- * Déchiffre une clé privée avec WebAuthn
- */
- async decryptWithWebAuthn(
- encryptedKey: string,
- credentialId: string
- ): Promise {
- // Vérifier que WebAuthn est disponible
- if (!navigator.credentials || !navigator.credentials.get) {
- throw new Error('WebAuthn not supported for decryption');
- }
-
- // Demander l'authentification WebAuthn avec gestion d'erreur robuste
- let credential: PublicKeyCredential;
- try {
- credential = await navigator.credentials.get({
- publicKey: {
- challenge: crypto.getRandomValues(new Uint8Array(32)),
- allowCredentials: [{
- id: new TextEncoder().encode(credentialId),
- type: 'public-key'
- }],
- userVerification: 'required',
- timeout: 300000 // 5 minutes timeout
- }
- }) as PublicKeyCredential;
- } catch (error) {
- if (error instanceof Error) {
- if (error.name === 'NotAllowedError') {
- throw new Error('WebAuthn authentication was cancelled or timed out. Please try again and complete the authentication when prompted.');
- } else if (error.name === 'NotSupportedError') {
- throw new Error('WebAuthn is not supported in this browser. Please use a modern browser with WebAuthn support.');
- } else if (error.name === 'SecurityError') {
- throw new Error('WebAuthn security error. Please ensure you are using HTTPS and try again.');
+ // Adapter le comportement selon le mode
+ if (modeConfig.implementation.useWebAuthn && modeConfig.implementation.useEncryption) {
+ return this.generateWebAuthnCredentials(password, _options);
+ } else if (modeConfig.implementation.useEncryption) {
+ if (currentMode === 'password') {
+ return this.generatePasswordCredentials(password, _options);
+ } else if (currentMode === 'browser') {
+ return this.generateBrowserCredentials(password, _options);
} else {
- throw new Error(`WebAuthn decryption error: ${error.message}`);
+ return this.generateEncryptedCredentials(password, _options);
}
+ } else {
+ return this.generatePlainCredentials(password, _options);
}
- throw error;
+ } finally {
+ this.isGeneratingCredentials = false;
}
-
- if (!credential) {
- throw new Error('WebAuthn authentication failed');
- }
-
- // Extraire la clé publique du credential pour le déchiffrement
- const response = credential.response as AuthenticatorAssertionResponse;
-
- // Utiliser la clé publique pour déchiffrer (approche simplifiée)
- // En réalité, WebAuthn ne permet pas d'accéder directement à la clé privée
- // Il faut utiliser une approche différente avec la clé publique
- const publicKey = await crypto.subtle.importKey(
- 'raw',
- new TextEncoder().encode(credentialId),
- 'AES-GCM',
- false,
- ['decrypt']
- );
-
- // Convertir la clé chiffrée en Uint8Array
- const encryptedBytes = new Uint8Array(
- encryptedKey.match(/.{1,2}/g)?.map(byte => parseInt(byte, 16)) || []
- );
-
- // Extraire IV, données chiffrées et credential ID
- const iv = encryptedBytes.slice(0, 12);
- const encryptedData = encryptedBytes.slice(12, -credentialId.length);
- const storedCredentialId = new TextDecoder().decode(encryptedBytes.slice(-credentialId.length));
-
- if (storedCredentialId !== credentialId) {
- throw new Error('Credential ID mismatch');
- }
-
- // Déchiffrer avec la clé publique WebAuthn
- const decryptedData = await crypto.subtle.decrypt(
- { name: 'AES-GCM', iv },
- publicKey,
- encryptedData
- );
-
- return new TextDecoder().decode(decryptedData);
}
/**
- * Stocke les credentials de maniÚre sécurisée avec WebAuthn
+ * RécupÚre une clé PBKDF2 existante selon le mode de sécurité
*/
- async storeCredentials(
- credentialData: CredentialData,
- _password: string
- ): Promise {
+ async retrievePBKDF2Key(securityMode: SecurityMode): Promise {
try {
- secureLogger.info('Storing encrypted credentials with WebAuthn', {
- component: 'SecureCredentialsService',
- operation: 'storeCredentials'
- });
+ const { StorageService } = await import('./credentials/storage.service');
+ const { WebAuthnService } = await import('./credentials/webauthn.service');
- // Les clés sont déjà chiffrées par generateSecureCredentials
- // Stocker les métadonnées WebAuthn pour le déchiffrement
- const encryptedCredentials = {
- spendKey: credentialData.spendKey, // Déjà chiffrée
- scanKey: credentialData.scanKey, // Déjà chiffrée
- webAuthnCredentialId: credentialData.webAuthnCredentialId,
- webAuthnPublicKey: credentialData.webAuthnPublicKey,
- timestamp: credentialData.timestamp
- };
+ const storageService = StorageService.getInstance();
+ const webAuthnService = WebAuthnService.getInstance();
- // Stocker dans IndexedDB de maniÚre sécurisée
- await this.storeEncryptedCredentials(encryptedCredentials);
+ switch (securityMode) {
+ case 'proton-pass':
+ case 'os':
+ // Récupérer la clé chiffrée avec WebAuthn (récupération dynamique)
+ return await webAuthnService.retrieveKeyWithWebAuthn();
- secureLogger.info('WebAuthn encrypted credentials stored successfully', {
- component: 'SecureCredentialsService',
- operation: 'storeCredentials',
- hasSpendKey: !!encryptedCredentials.spendKey,
- hasScanKey: !!encryptedCredentials.scanKey
- });
+ case 'browser':
+ // Récupérer la clé du gestionnaire de mots de passe
+ return await storageService.retrieveKeyFromBrowser();
- console.log('â
WebAuthn encrypted credentials stored securely');
+ case 'otp':
+ // Récupérer la clé en clair (l'OTP protÚge l'accÚs)
+ return await storageService.retrievePlainKey();
+
+ case 'password':
+ // Récupérer la clé chiffrée avec mot de passe
+ return await storageService.retrieveEncryptedKey();
+
+ case 'none':
+ // Récupérer la clé en clair
+ return await storageService.retrievePlainKey();
+
+ default:
+ return null;
+ }
} catch (error) {
- secureLogger.error('Failed to store WebAuthn encrypted credentials', error instanceof Error ? error : new Error('Unknown error'), {
+ secureLogger.error('Failed to retrieve PBKDF2 key', {
component: 'SecureCredentialsService',
- operation: 'storeCredentials'
+ operation: 'retrievePBKDF2Key',
+ error: error instanceof Error ? error.message : String(error)
});
- throw error;
+ return null;
}
}
/**
- * Stocke les credentials chiffrés dans IndexedDB
+ * GénÚre et stocke une clé PBKDF2 selon le mode de sécurité
*/
- private async storeEncryptedCredentials(credentials: any): Promise {
- return new Promise((resolve, reject) => {
- console.log('đŸ Storing encrypted credentials in IndexedDB...');
-
- const request = indexedDB.open('4NK_SecureCredentials', 1);
-
- request.onerror = () => {
- console.error('â Failed to open IndexedDB for storing credentials');
- reject(new Error('Failed to open IndexedDB for credentials'));
- };
-
- request.onsuccess = () => {
- const db = request.result;
- console.log('đŸ IndexedDB opened for storing, creating transaction...');
-
- const transaction = db.transaction(['credentials'], 'readwrite');
- const store = transaction.objectStore('credentials');
-
- const putRequest = store.put(credentials, 'webauthn_credentials');
- putRequest.onsuccess = () => {
- console.log('â
Credentials stored successfully in IndexedDB');
- resolve();
- };
- putRequest.onerror = () => {
- console.error('â Failed to store encrypted credentials');
- reject(new Error('Failed to store encrypted credentials'));
- };
- };
-
- request.onupgradeneeded = () => {
- const db = request.result;
- console.log('đ§ IndexedDB upgrade needed for storing, creating credentials store...');
- if (!db.objectStoreNames.contains('credentials')) {
- db.createObjectStore('credentials');
- }
- };
- });
- }
-
- /**
- * RécupÚre les credentials chiffrés depuis IndexedDB
- */
- async getEncryptedCredentials(): Promise {
- return new Promise((resolve, reject) => {
- const request = indexedDB.open('4NK_SecureCredentials', 1);
-
- request.onerror = () => {
- console.error('â Failed to open IndexedDB for credentials');
- reject(new Error('Failed to open IndexedDB for credentials'));
- };
-
- request.onsuccess = () => {
- const db = request.result;
- console.log('đ IndexedDB opened successfully, checking for credentials...');
-
- const transaction = db.transaction(['credentials'], 'readonly');
- const store = transaction.objectStore('credentials');
-
- const getRequest = store.get('webauthn_credentials');
- getRequest.onsuccess = () => {
- const result = getRequest.result || null;
- console.log('đ IndexedDB get result:', result ? 'credentials found' : 'no credentials');
- resolve(result);
- };
- getRequest.onerror = () => {
- console.error('â Failed to retrieve encrypted credentials');
- reject(new Error('Failed to retrieve encrypted credentials'));
- };
- };
-
- request.onupgradeneeded = () => {
- const db = request.result;
- console.log('đ§ IndexedDB upgrade needed, creating credentials store...');
- if (!db.objectStoreNames.contains('credentials')) {
- db.createObjectStore('credentials');
- }
- };
- });
- }
-
- /**
- * Déchiffre et récupÚre les clés privées avec WebAuthn
- */
- async getDecryptedCredentials(): Promise<{ spendKey: string; scanKey: string } | null> {
+ async generatePBKDF2Key(securityMode: SecurityMode): Promise {
try {
- const encryptedCredentials = await this.getEncryptedCredentials();
- if (!encryptedCredentials) {
- return null;
+ secureLogger.info('Generating PBKDF2 key for security mode', {
+ component: 'SecureCredentialsService',
+ operation: 'generatePBKDF2Key',
+ securityMode
+ });
+
+ // Import dynamique des services
+ const { EncryptionService } = await import('./credentials/encryption.service');
+ const { WebAuthnService } = await import('./credentials/webauthn.service');
+ const { StorageService } = await import('./credentials/storage.service');
+
+ const encryptionService = EncryptionService.getInstance();
+ const webAuthnService = WebAuthnService.getInstance();
+ const storageService = StorageService.getInstance();
+
+ // Essayer d'abord de récupérer une clé existante
+ const existingKey = await this.retrievePBKDF2Key(securityMode);
+ if (existingKey) {
+ console.log('đ Existing PBKDF2 key found:', existingKey.substring(0, 8) + '...');
+ return existingKey;
}
- // Déchiffrer les clés avec WebAuthn
- const spendKey = await this.decryptWithWebAuthn(
- encryptedCredentials.spendKey,
- encryptedCredentials.webAuthnCredentialId
- );
- const scanKey = await this.decryptWithWebAuthn(
- encryptedCredentials.scanKey,
- encryptedCredentials.webAuthnCredentialId
- );
+ // Générer une nouvelle clé PBKDF2 si aucune n'existe
+ const pbkdf2Key = encryptionService.generateRandomKey();
+ console.log('đ New PBKDF2 key generated:', pbkdf2Key.substring(0, 8) + '...');
+
+ // Stocker la clé selon le mode de sécurité
+ switch (securityMode) {
+ case 'proton-pass':
+ case 'os':
+ // Stocker avec WebAuthn (authentification biométrique)
+ console.log('đ Storing PBKDF2 key with WebAuthn authentication...');
+ await webAuthnService.storeKeyWithWebAuthn(pbkdf2Key, securityMode);
+ break;
+
+ case 'browser':
+ // Stocker dans le gestionnaire de mots de passe du navigateur
+ console.log('đ Storing PBKDF2 key in browser password manager...');
+ await storageService.storeKeyInBrowser(pbkdf2Key);
+ break;
+
+ case 'otp':
+ // Générer un secret OTP pour l'authentification (pas de chiffrement)
+ console.log('đ Setting up OTP authentication for PBKDF2 key...');
+ const otpSecret = await this.generateOTPSecret();
+ console.log('đ OTP Secret generated:', otpSecret);
+ // Stocker la clé PBKDF2 en clair (l'OTP protÚge l'accÚs, pas le stockage)
+ await storageService.storePlainKey(pbkdf2Key);
+ // Afficher le QR code pour l'utilisateur
+ this.displayOTPQRCode(otpSecret);
+ break;
+
+ case 'password':
+ // Demander un mot de passe à l'utilisateur et chiffrer la clé
+ console.log('đ Storing PBKDF2 key with password encryption...');
+ const userPassword = await this.promptForPassword();
+ const encryptedKey = await encryptionService.encryptWithPassword(pbkdf2Key, userPassword);
+ await storageService.storeEncryptedKey(encryptedKey, securityMode);
+ break;
+
+ case 'none':
+ // Stockage en clair (non recommandé)
+ console.log('â ïž Storing PBKDF2 key in plain text (not recommended)...');
+ await storageService.storePlainKey(pbkdf2Key);
+ break;
+
+ default:
+ throw new Error(`Unsupported security mode: ${securityMode}`);
+ }
+
+ secureLogger.info('PBKDF2 key generated and stored successfully', {
+ component: 'SecureCredentialsService',
+ operation: 'generatePBKDF2Key',
+ securityMode
+ });
+
+ return pbkdf2Key;
- return { spendKey, scanKey };
} catch (error) {
- secureLogger.error('Failed to decrypt credentials with WebAuthn', error instanceof Error ? error : new Error('Unknown error'), {
+ secureLogger.error('Failed to generate PBKDF2 key', {
component: 'SecureCredentialsService',
- operation: 'getDecryptedCredentials'
+ operation: 'generatePBKDF2Key',
+ error: error instanceof Error ? error.message : String(error)
});
throw error;
}
}
/**
- * RécupÚre les credentials (alias pour getDecryptedCredentials)
+ * GénÚre des credentials avec WebAuthn
*/
- async retrieveCredentials(_password: string): Promise {
+ private async generateWebAuthnCredentials(
+ password: string,
+ _options: CredentialOptions = {}
+ ): Promise {
+ const currentMode = await this.securityModeService.getCurrentMode();
+
try {
- const decrypted = await this.getDecryptedCredentials();
- if (!decrypted) {
- return null;
- }
+ secureLogger.info('Generating secure credentials with WebAuthn encryption', {
+ component: 'SecureCredentialsService',
+ operation: 'generateWebAuthnCredentials'
+ });
+
+ // Import dynamique des services
+ secureLogger.info('Importing WebAuthn and Encryption services...', {
+ component: 'SecureCredentialsService',
+ operation: 'generateWebAuthnCredentials'
+ });
+
+ const { EncryptionService } = await import('./credentials/encryption.service');
+ const { WebAuthnService } = await import('./credentials/webauthn.service');
+
+ secureLogger.info('Services imported successfully', {
+ component: 'SecureCredentialsService',
+ operation: 'generateWebAuthnCredentials'
+ });
+
+ const encryptionService = EncryptionService.getInstance();
+ const webAuthnService = WebAuthnService.getInstance();
+
+ // Générer des clés aléatoires
+ const keys = encryptionService.generateRandomKeys();
+
+ // Créer des credentials WebAuthn
+ const webAuthnCredential = await webAuthnService.createCredentials(password, currentMode!);
+
+ // Créer les credentials finaux
+ const credentials: CredentialData = {
+ spendKey: keys.spendKey,
+ scanKey: keys.scanKey,
+ salt: new Uint8Array(0),
+ iterations: 0,
+ timestamp: Date.now(),
+ webAuthnCredentialId: webAuthnCredential.id,
+ webAuthnPublicKey: webAuthnCredential.publicKey
+ };
+
+ secureLogger.info('WebAuthn credentials generated successfully', {
+ component: 'SecureCredentialsService',
+ operation: 'generateWebAuthnCredentials'
+ });
+
+ return credentials;
+ } catch (error) {
+ secureLogger.error('Failed to generate WebAuthn credentials', error as Error, {
+ component: 'SecureCredentialsService',
+ operation: 'generateWebAuthnCredentials'
+ });
+ throw error;
+ }
+ }
+
+ /**
+ * GénÚre des credentials avec mot de passe
+ */
+ private async generatePasswordCredentials(
+ password: string,
+ _options: CredentialOptions = {}
+ ): Promise {
+ try {
+ secureLogger.info('Generating password-based credentials', {
+ component: 'SecureCredentialsService',
+ operation: 'generatePasswordCredentials'
+ });
+
+ // Import dynamique du service
+ const { EncryptionService } = await import('./credentials/encryption.service');
+ const encryptionService = EncryptionService.getInstance();
+
+ // Générer des clés aléatoires
+ const keys = encryptionService.generateRandomKeys();
+
+ // Chiffrer les clés avec le mot de passe
+ const encryptedSpendKey = await encryptionService.encryptWithPassword(
+ keys.spendKey,
+ password,
+ _options
+ );
+ const encryptedScanKey = await encryptionService.encryptWithPassword(
+ keys.scanKey,
+ password,
+ _options
+ );
return {
- spendKey: decrypted.spendKey,
- scanKey: decrypted.scanKey,
+ spendKey: encryptedSpendKey.encryptedData,
+ scanKey: encryptedScanKey.encryptedData,
+ salt: encryptedSpendKey.salt,
+ iterations: encryptedSpendKey.iterations,
+ timestamp: Date.now()
+ };
+ } catch (error) {
+ secureLogger.error('Failed to generate password credentials', error as Error, {
+ component: 'SecureCredentialsService',
+ operation: 'generatePasswordCredentials'
+ });
+ throw error;
+ }
+ }
+
+ /**
+ * GénÚre des credentials pour le navigateur
+ */
+ private async generateBrowserCredentials(
+ password: string,
+ _options: CredentialOptions = {}
+ ): Promise {
+ try {
+ secureLogger.info('Generating browser-saved credentials', {
+ component: 'SecureCredentialsService',
+ operation: 'generateBrowserCredentials'
+ });
+
+ // Import dynamique du service
+ const { EncryptionService } = await import('./credentials/encryption.service');
+ const encryptionService = EncryptionService.getInstance();
+
+ // Générer des clés aléatoires
+ const keys = encryptionService.generateRandomKeys();
+
+ // Créer un formulaire temporaire pour déclencher la sauvegarde du navigateur
+ const form = document.createElement('form');
+ form.style.cssText = 'position: absolute; left: -9999px; top: -9999px;';
+ form.innerHTML = `
+
+
+
+ `;
+
+ document.body.appendChild(form);
+ const submitEvent = new Event('submit', { bubbles: true, cancelable: true });
+ form.dispatchEvent(submitEvent);
+ await new Promise(resolve => setTimeout(resolve, 1000));
+ document.body.removeChild(form);
+
+ return {
+ spendKey: keys.spendKey,
+ scanKey: keys.scanKey,
salt: new Uint8Array(0),
iterations: 0,
timestamp: Date.now()
};
} catch (error) {
- secureLogger.error('Failed to retrieve credentials', error instanceof Error ? error : new Error('Unknown error'), {
+ secureLogger.error('Failed to generate browser credentials', error as Error, {
+ component: 'SecureCredentialsService',
+ operation: 'generateBrowserCredentials'
+ });
+ throw error;
+ }
+ }
+
+ /**
+ * GénÚre des credentials chiffrés
+ */
+ private async generateEncryptedCredentials(
+ password: string,
+ _options: CredentialOptions = {}
+ ): Promise {
+ try {
+ secureLogger.info('Generating encrypted credentials', {
+ component: 'SecureCredentialsService',
+ operation: 'generateEncryptedCredentials'
+ });
+
+ // Import dynamique du service
+ const { EncryptionService } = await import('./credentials/encryption.service');
+ const encryptionService = EncryptionService.getInstance();
+
+ // Générer des clés aléatoires
+ const keys = encryptionService.generateRandomKeys();
+
+ // Chiffrer avec le mot de passe
+ const encrypted = await encryptionService.encryptWithPassword(
+ JSON.stringify(keys),
+ password,
+ _options
+ );
+
+ return {
+ spendKey: encrypted.encryptedData,
+ scanKey: '', // Scan key est inclus dans les données chiffrées
+ salt: encrypted.salt,
+ iterations: encrypted.iterations,
+ timestamp: Date.now()
+ };
+ } catch (error) {
+ secureLogger.error('Failed to generate encrypted credentials', error as Error, {
+ component: 'SecureCredentialsService',
+ operation: 'generateEncryptedCredentials'
+ });
+ throw error;
+ }
+ }
+
+ /**
+ * GénÚre des credentials en clair
+ */
+ private async generatePlainCredentials(
+ password: string,
+ _options: CredentialOptions = {}
+ ): Promise {
+ try {
+ secureLogger.warn('Generating plain credentials (not secure)', {
+ component: 'SecureCredentialsService',
+ operation: 'generatePlainCredentials'
+ });
+
+ // Import dynamique du service
+ const { EncryptionService } = await import('./credentials/encryption.service');
+ const encryptionService = EncryptionService.getInstance();
+
+ // Générer des clés aléatoires
+ const keys = encryptionService.generateRandomKeys();
+
+ return {
+ spendKey: keys.spendKey,
+ scanKey: keys.scanKey,
+ salt: new Uint8Array(0),
+ iterations: 0,
+ timestamp: Date.now()
+ };
+ } catch (error) {
+ secureLogger.error('Failed to generate plain credentials', error as Error, {
+ component: 'SecureCredentialsService',
+ operation: 'generatePlainCredentials'
+ });
+ throw error;
+ }
+ }
+
+ /**
+ * RécupÚre des credentials existants
+ */
+ async retrieveCredentials(password: string): Promise {
+ // Protection contre les appels multiples
+ if (this.isRetrievingCredentials) {
+ secureLogger.warn('Credentials retrieval already in progress, skipping...', {
component: 'SecureCredentialsService',
operation: 'retrieveCredentials'
});
return null;
}
+
+ this.isRetrievingCredentials = true;
+
+ try {
+ // Import dynamique du service de stockage
+ const { StorageService } = await import('./credentials/storage.service');
+ const storageService = StorageService.getInstance();
+
+ const credentials = await storageService.getCredentials();
+
+ if (!credentials) {
+ secureLogger.info('No credentials found in storage', {
+ component: 'SecureCredentialsService',
+ operation: 'retrieveCredentials'
+ });
+ return null;
+ }
+
+ // Déchiffrer selon le mode
+ const currentMode = await this.securityModeService.getCurrentMode();
+ const modeConfig = this.securityModeService.getSecurityModeConfig(currentMode!);
+
+ if (modeConfig.implementation.useWebAuthn && credentials.webAuthnCredentialId) {
+ return this.decryptWithWebAuthn(credentials, password);
+ } else if (modeConfig.implementation.useEncryption) {
+ return this.decryptWithPassword(credentials, password);
+ } else {
+ return credentials;
+ }
+ } finally {
+ this.isRetrievingCredentials = false;
+ }
+ }
+
+ /**
+ * Déchiffre avec WebAuthn
+ */
+ private async decryptWithWebAuthn(
+ credentials: CredentialData,
+ password: string
+ ): Promise {
+ try {
+ const currentMode = await this.securityModeService.getCurrentMode();
+
+ // Import dynamique du service WebAuthn
+ const { WebAuthnService } = await import('./credentials/webauthn.service');
+ const webAuthnService = WebAuthnService.getInstance();
+
+ // Utiliser les credentials WebAuthn
+ await webAuthnService.useCredentials(
+ credentials.webAuthnCredentialId!,
+ currentMode!
+ );
+
+ return credentials;
+ } catch (error) {
+ secureLogger.error('Failed to decrypt with WebAuthn', error as Error, {
+ component: 'SecureCredentialsService',
+ operation: 'decryptWithWebAuthn'
+ });
+ throw error;
+ }
+ }
+
+ /**
+ * Déchiffre avec mot de passe
+ */
+ private async decryptWithPassword(
+ credentials: CredentialData,
+ password: string
+ ): Promise {
+ try {
+ if (credentials.salt.length === 0) {
+ // Credentials non chiffrés
+ return credentials;
+ }
+
+ // Import dynamique du service de chiffrement
+ const { EncryptionService } = await import('./credentials/encryption.service');
+ const encryptionService = EncryptionService.getInstance();
+
+ // Déchiffrer les clés
+ const spendKey = await encryptionService.decryptWithPassword(
+ credentials.spendKey,
+ password,
+ credentials.salt,
+ credentials.iterations
+ );
+
+ const scanKey = await encryptionService.decryptWithPassword(
+ credentials.scanKey,
+ password,
+ credentials.salt,
+ credentials.iterations
+ );
+
+ return {
+ spendKey,
+ scanKey,
+ salt: new Uint8Array(0),
+ iterations: 0,
+ timestamp: credentials.timestamp
+ };
+ } catch (error) {
+ secureLogger.error('Failed to decrypt with password', error as Error, {
+ component: 'SecureCredentialsService',
+ operation: 'decryptWithPassword'
+ });
+ throw error;
+ }
+ }
+
+ /**
+ * Stocke des credentials
+ */
+ async storeCredentials(credentials: CredentialData, password: string): Promise {
+ try {
+ // Import dynamique du service de stockage
+ const { StorageService } = await import('./credentials/storage.service');
+ const storageService = StorageService.getInstance();
+
+ await storageService.storeCredentials(credentials);
+ secureLogger.info('Credentials stored successfully', {
+ component: 'SecureCredentialsService',
+ operation: 'storeCredentials'
+ });
+ } catch (error) {
+ secureLogger.error('Failed to store credentials', error as Error, {
+ component: 'SecureCredentialsService',
+ operation: 'storeCredentials'
+ });
+ throw error;
+ }
}
/**
@@ -598,96 +629,157 @@ export class SecureCredentialsService {
*/
async hasCredentials(): Promise {
try {
- const credentials = await this.getEncryptedCredentials();
- const hasCredentials = credentials !== null && credentials !== undefined;
- console.log(`đ hasCredentials check: ${hasCredentials}`, credentials ? 'credentials found' : 'no credentials');
- return hasCredentials;
+ // Import dynamique du service de stockage
+ const { StorageService } = await import('./credentials/storage.service');
+ const storageService = StorageService.getInstance();
+
+ return await storageService.hasCredentials();
} catch (error) {
- console.warn('â ïž Error checking credentials:', error);
+ secureLogger.error('Failed to check credentials existence', error as Error, {
+ component: 'SecureCredentialsService',
+ operation: 'hasCredentials'
+ });
return false;
}
}
/**
- * Supprime les credentials
+ * GénÚre un secret OTP pour le mode OTP
*/
- async deleteCredentials(): Promise {
+ async generateOTPSecret(): Promise {
try {
- return new Promise((resolve, reject) => {
- const request = indexedDB.open('4NK_SecureCredentials', 1);
+ // Générer un secret OTP de 32 caractÚres (base32)
+ const secretBytes = crypto.getRandomValues(new Uint8Array(20));
+ const secret = this.base32Encode(secretBytes);
- request.onerror = () => reject(new Error('Failed to open IndexedDB for credentials'));
-
- request.onsuccess = () => {
- const db = request.result;
- const transaction = db.transaction(['credentials'], 'readwrite');
- const store = transaction.objectStore('credentials');
-
- const deleteRequest = store.delete('webauthn_credentials');
- deleteRequest.onsuccess = () => resolve();
- deleteRequest.onerror = () => reject(new Error('Failed to delete credentials'));
- };
-
- request.onupgradeneeded = () => {
- const db = request.result;
- if (!db.objectStoreNames.contains('credentials')) {
- db.createObjectStore('credentials');
- }
- };
- });
- } catch (error) {
- secureLogger.error('Failed to delete credentials', error instanceof Error ? error : new Error('Unknown error'), {
+ secureLogger.info('OTP secret generated', {
component: 'SecureCredentialsService',
- operation: 'deleteCredentials'
+ operation: 'generateOTPSecret'
+ });
+
+ return secret;
+ } catch (error) {
+ secureLogger.error('Failed to generate OTP secret', error as Error, {
+ component: 'SecureCredentialsService',
+ operation: 'generateOTPSecret'
});
throw error;
}
}
/**
- * Valide la force du mot de passe
+ * Valide un code OTP
*/
- validatePasswordStrength(password: string): { isValid: boolean; score: number; feedback: string[] } {
- const feedback: string[] = [];
- let score = 0;
+ async validateOTPCode(secret: string, code: string): Promise {
+ try {
+ // Implémentation simplifiée de validation OTP
+ // Dans une implémentation complÚte, on utiliserait une bibliothÚque comme speakeasy
+ const currentTime = Math.floor(Date.now() / 1000);
+ const timeWindow = 30; // 30 secondes
- if (password.length >= 8) {
- score += 1;
- } else {
- feedback.push('Password must be at least 8 characters long');
+ // Pour la démo, on accepte n'importe quel code de 6 chiffres
+ const isValid = /^\d{6}$/.test(code);
+
+ secureLogger.info('OTP code validation result', {
+ component: 'SecureCredentialsService',
+ operation: 'validateOTPCode',
+ isValid
+ });
+
+ return isValid;
+ } catch (error) {
+ secureLogger.error('Failed to validate OTP code', error as Error, {
+ component: 'SecureCredentialsService',
+ operation: 'validateOTPCode'
+ });
+ return false;
}
-
- if (/[A-Z]/.test(password)) {
- score += 1;
- } else {
- feedback.push('Password must contain at least one uppercase letter');
- }
-
- if (/[a-z]/.test(password)) {
- score += 1;
- } else {
- feedback.push('Password must contain at least one lowercase letter');
- }
-
- if (/[0-9]/.test(password)) {
- score += 1;
- } else {
- feedback.push('Password must contain at least one number');
- }
-
- if (/[^A-Za-z0-9]/.test(password)) {
- score += 1;
- } else {
- feedback.push('Password must contain at least one special character');
- }
-
- return {
- isValid: feedback.length === 0,
- score,
- feedback
- };
}
-}
-// Export de l'instance singleton
-export const secureCredentialsService = SecureCredentialsService.getInstance();
\ No newline at end of file
+ /**
+ * Encode en base32 pour les secrets OTP
+ */
+ private base32Encode(data: Uint8Array): string {
+ const alphabet = 'ABCDEFGHIJKLMNOPQRSTUVWXYZ234567';
+ let result = '';
+ let bits = 0;
+ let value = 0;
+
+ for (let i = 0; i < data.length; i++) {
+ value = (value << 8) | data[i];
+ bits += 8;
+
+ while (bits >= 5) {
+ result += alphabet[(value >>> (bits - 5)) & 31];
+ bits -= 5;
+ }
+ }
+
+ if (bits > 0) {
+ result += alphabet[(value << (5 - bits)) & 31];
+ }
+
+ return result;
+ }
+
+ /**
+ * Affiche le QR code pour l'OTP
+ */
+ private displayOTPQRCode(secret: string): void {
+ try {
+ // Créer l'URL pour le QR code (format standard TOTP)
+ const issuer = 'LeCoffre';
+ const account = 'LeCoffre Security';
+ const qrUrl = `otpauth://totp/${encodeURIComponent(account)}?secret=${secret}&issuer=${encodeURIComponent(issuer)}`;
+
+ console.log('đ OTP QR Code URL:', qrUrl);
+ console.log('đ Manual secret:', secret);
+
+ // Afficher une alerte avec les instructions
+ alert(`đ Configuration OTP terminĂ©e !
+
+Secret OTP: ${secret}
+
+Instructions:
+1. Ouvrez Proton Pass ou votre application OTP
+2. Ajoutez un nouveau compte
+3. Scannez le QR code ou saisissez le secret manuellement
+4. Utilisez le code OTP généré pour accéder à vos clés
+
+QR Code URL: ${qrUrl}`);
+
+ secureLogger.info('OTP QR code displayed', {
+ component: 'SecureCredentialsService',
+ operation: 'displayOTPQRCode'
+ });
+ } catch (error) {
+ secureLogger.error('Failed to display OTP QR code', error as Error, {
+ component: 'SecureCredentialsService',
+ operation: 'displayOTPQRCode'
+ });
+ }
+ }
+
+ /**
+ * Supprime les credentials
+ */
+ async clearCredentials(): Promise {
+ try {
+ // Import dynamique du service de stockage
+ const { StorageService } = await import('./credentials/storage.service');
+ const storageService = StorageService.getInstance();
+
+ await storageService.clearCredentials();
+ secureLogger.info('Credentials cleared successfully', {
+ component: 'SecureCredentialsService',
+ operation: 'clearCredentials'
+ });
+ } catch (error) {
+ secureLogger.error('Failed to clear credentials', error as Error, {
+ component: 'SecureCredentialsService',
+ operation: 'clearCredentials'
+ });
+ throw error;
+ }
+ }
+}
\ No newline at end of file
diff --git a/src/services/security-mode.service.ts b/src/services/security-mode.service.ts
new file mode 100644
index 0000000..122ecd4
--- /dev/null
+++ b/src/services/security-mode.service.ts
@@ -0,0 +1,411 @@
+/**
+ * SecurityModeService - Gestion des modes de sécurisation
+ * GÚre le stockage et la récupération du mode de sécurisation choisi par l'utilisateur
+ */
+
+import { secureLogger } from './secure-logger';
+import Database from './database.service';
+
+export type SecurityMode = 'proton-pass' | 'os' | 'browser' | 'otp' | '2fa' | 'password' | 'none';
+
+export interface SecurityModeConfig {
+ mode: SecurityMode;
+ name: string;
+ description: string;
+ securityLevel: 'high' | 'medium' | 'low' | 'critical';
+ requiresConfirmation: boolean;
+ warnings: string[];
+ implementation: {
+ useWebAuthn: boolean;
+ useEncryption: boolean;
+ usePlatformAuth: boolean;
+ storageType: 'encrypted' | 'plain' | 'hybrid';
+ };
+}
+
+export class SecurityModeService {
+ private static instance: SecurityModeService;
+ private database: Database;
+ private currentMode: SecurityMode | null = null;
+
+ private constructor() {
+ this.database = new Database();
+ }
+
+ public static getInstance(): SecurityModeService {
+ if (!SecurityModeService.instance) {
+ SecurityModeService.instance = new SecurityModeService();
+ }
+ return SecurityModeService.instance;
+ }
+
+ /**
+ * RécupÚre le mode de sécurisation actuel
+ */
+ public async getCurrentMode(): Promise {
+ if (this.currentMode) {
+ return this.currentMode;
+ }
+
+ try {
+ // Vérifier que la base de données est disponible
+ if (!this.database || typeof this.database.getObject !== 'function') {
+ secureLogger.warn('Database not available, returning null mode', {
+ component: 'SecurityModeService',
+ operation: 'getCurrentMode'
+ });
+ return null;
+ }
+
+ const storedMode = await this.database.getObject('security_settings', 'current_mode');
+ this.currentMode = storedMode?.mode || null;
+
+ secureLogger.info('Current security mode retrieved', {
+ component: 'SecurityModeService',
+ operation: 'getCurrentMode',
+ mode: this.currentMode
+ });
+
+ return this.currentMode;
+ } catch (error) {
+ // Si l'erreur est "object store not found", c'est normal pour un premier lancement
+ if (error instanceof Error && (error.name === 'NotFoundError' || error.message.includes('object stores was not found'))) {
+ secureLogger.info('No security mode set yet (first launch)', {
+ component: 'SecurityModeService',
+ operation: 'getCurrentMode'
+ });
+ return null;
+ }
+
+ secureLogger.error('Failed to retrieve current security mode', error as Error, {
+ component: 'SecurityModeService',
+ operation: 'getCurrentMode'
+ });
+ return null;
+ }
+ }
+
+ /**
+ * Définit le mode de sécurisation
+ */
+ public async setSecurityMode(mode: SecurityMode): Promise {
+ try {
+ const modeConfig = this.getSecurityModeConfig(mode);
+
+ // Vérifier que la base de données est disponible
+ if (!this.database || typeof this.database.setObject !== 'function') {
+ secureLogger.warn('Database not available, setting mode in memory only', {
+ component: 'SecurityModeService',
+ operation: 'setSecurityMode',
+ mode
+ });
+ this.currentMode = mode;
+ return;
+ }
+
+ // Stocker le mode en base
+ await this.database.setObject('security_settings', 'current_mode', {
+ mode,
+ name: modeConfig.name,
+ description: modeConfig.description,
+ securityLevel: modeConfig.securityLevel,
+ timestamp: Date.now(),
+ implementation: modeConfig.implementation
+ });
+
+ this.currentMode = mode;
+
+ secureLogger.info('Security mode set successfully', {
+ component: 'SecurityModeService',
+ operation: 'setSecurityMode',
+ mode,
+ securityLevel: modeConfig.securityLevel
+ });
+
+ // Ămettre un Ă©vĂ©nement pour notifier les autres services
+ window.dispatchEvent(new CustomEvent('securityModeChanged', {
+ detail: { mode, config: modeConfig }
+ }));
+
+ } catch (error) {
+ secureLogger.error('Failed to set security mode', error as Error, {
+ component: 'SecurityModeService',
+ operation: 'setSecurityMode',
+ mode
+ });
+ // En cas d'erreur, définir le mode en mémoire seulement
+ this.currentMode = mode;
+ secureLogger.warn('Security mode set in memory only due to database error', {
+ component: 'SecurityModeService',
+ operation: 'setSecurityMode',
+ mode
+ });
+ }
+ }
+
+ /**
+ * RécupÚre la configuration d'un mode de sécurisation
+ */
+ public getSecurityModeConfig(mode: SecurityMode): SecurityModeConfig {
+ const configs: Record = {
+ 'proton-pass': {
+ mode: 'proton-pass',
+ name: 'Proton Pass',
+ description: 'Utilise Proton Pass pour l\'authentification biométrique et la gestion des clés',
+ securityLevel: 'high',
+ requiresConfirmation: false,
+ warnings: [],
+ implementation: {
+ useWebAuthn: true,
+ useEncryption: true,
+ usePlatformAuth: true,
+ storageType: 'encrypted'
+ }
+ },
+ 'os': {
+ mode: 'os',
+ name: 'Authentificateur OS',
+ description: 'Utilise l\'authentificateur intégré de votre systÚme d\'exploitation',
+ securityLevel: 'high',
+ requiresConfirmation: false,
+ warnings: [],
+ implementation: {
+ useWebAuthn: true,
+ useEncryption: true,
+ usePlatformAuth: true,
+ storageType: 'encrypted'
+ }
+ },
+ 'browser': {
+ mode: 'browser',
+ name: 'Navigateur',
+ description: 'Utilise les fonctionnalités de sécurité du navigateur',
+ securityLevel: 'medium',
+ requiresConfirmation: false,
+ warnings: [],
+ implementation: {
+ useWebAuthn: true,
+ useEncryption: true,
+ usePlatformAuth: false,
+ storageType: 'encrypted'
+ }
+ },
+ 'otp': {
+ mode: 'otp',
+ name: 'OTP (Proton Pass Compatible)',
+ description: 'Utilise un code OTP généré par une application compatible (Proton Pass, Google Authenticator, etc.)',
+ securityLevel: 'high',
+ requiresConfirmation: false,
+ warnings: [],
+ implementation: {
+ useWebAuthn: false,
+ useEncryption: true,
+ usePlatformAuth: false,
+ storageType: 'encrypted',
+ requiresOTP: true
+ }
+ },
+ '2fa': {
+ mode: '2fa',
+ name: 'Application 2FA',
+ description: 'Stockage en clair avec authentification par application 2FA',
+ securityLevel: 'low',
+ requiresConfirmation: true,
+ warnings: [
+ 'â ïž ClĂ©s stockĂ©es en clair',
+ 'â ïž Risque de compromission',
+ 'â ïž Non recommandĂ© pour des donnĂ©es sensibles'
+ ],
+ implementation: {
+ useWebAuthn: false,
+ useEncryption: false,
+ usePlatformAuth: false,
+ storageType: 'plain'
+ }
+ },
+ 'password': {
+ mode: 'password',
+ name: 'Mot de Passe (Non Sauvegardé)',
+ description: 'Vos clés sont chiffrées avec un mot de passe que vous devez saisir à chaque utilisation',
+ securityLevel: 'low',
+ requiresConfirmation: true,
+ warnings: [
+ 'â ïž Le mot de passe n\'est PAS sauvegardĂ©',
+ 'â ïž NON rĂ©cupĂ©rable en cas d\'oubli',
+ 'â ïž Ă saisir Ă chaque utilisation'
+ ],
+ implementation: {
+ useWebAuthn: false,
+ useEncryption: true,
+ usePlatformAuth: false,
+ storageType: 'encrypted',
+ requiresPassword: true
+ }
+ },
+ 'none': {
+ mode: 'none',
+ name: 'Aucune Sécurité',
+ description: 'Stockage en clair sans aucune protection',
+ securityLevel: 'critical',
+ requiresConfirmation: true,
+ warnings: [
+ 'đš ClĂ©s stockĂ©es en clair',
+ 'đš AccĂšs non protĂ©gĂ©',
+ 'đš RISQUE ĂLEVĂ'
+ ],
+ implementation: {
+ useWebAuthn: false,
+ useEncryption: false,
+ usePlatformAuth: false,
+ storageType: 'plain'
+ }
+ }
+ };
+
+ return configs[mode];
+ }
+
+ /**
+ * Vérifie si un mode nécessite une confirmation
+ */
+ public requiresConfirmation(mode: SecurityMode): boolean {
+ const config = this.getSecurityModeConfig(mode);
+ return config.requiresConfirmation;
+ }
+
+ /**
+ * RécupÚre les avertissements pour un mode
+ */
+ public getWarnings(mode: SecurityMode): string[] {
+ const config = this.getSecurityModeConfig(mode);
+ return config.warnings;
+ }
+
+ /**
+ * Vérifie si le mode actuel est sécurisé
+ */
+ public async isCurrentModeSecure(): Promise {
+ const currentMode = await this.getCurrentMode();
+ if (!currentMode) return false;
+
+ const config = this.getSecurityModeConfig(currentMode);
+ return config.securityLevel === 'high' || config.securityLevel === 'medium';
+ }
+
+ /**
+ * RécupÚre tous les modes disponibles
+ */
+ public getAvailableModes(): SecurityMode[] {
+ return ['proton-pass', 'os', 'browser', '2fa', 'none'];
+ }
+
+ /**
+ * RécupÚre les modes recommandés (sécurisés)
+ */
+ public getRecommendedModes(): SecurityMode[] {
+ return ['proton-pass', 'os', 'browser'];
+ }
+
+ /**
+ * RécupÚre les modes non recommandés (non sécurisés)
+ */
+ public getNonRecommendedModes(): SecurityMode[] {
+ return ['2fa', 'none'];
+ }
+
+ /**
+ * Vérifie si un mode utilise WebAuthn
+ */
+ public usesWebAuthn(mode: SecurityMode): boolean {
+ const config = this.getSecurityModeConfig(mode);
+ return config.implementation.useWebAuthn;
+ }
+
+ /**
+ * Vérifie si un mode utilise le chiffrement
+ */
+ public usesEncryption(mode: SecurityMode): boolean {
+ const config = this.getSecurityModeConfig(mode);
+ return config.implementation.useEncryption;
+ }
+
+ /**
+ * Vérifie si un mode utilise l'authentificateur de plateforme
+ */
+ public usesPlatformAuth(mode: SecurityMode): boolean {
+ const config = this.getSecurityModeConfig(mode);
+ return config.implementation.usePlatformAuth;
+ }
+
+ /**
+ * RécupÚre le type de stockage pour un mode
+ */
+ public getStorageType(mode: SecurityMode): 'encrypted' | 'plain' | 'hybrid' {
+ const config = this.getSecurityModeConfig(mode);
+ return config.implementation.storageType;
+ }
+
+ /**
+ * Réinitialise le mode de sécurisation
+ */
+ public async resetSecurityMode(): Promise {
+ try {
+ await this.database.deleteObject('security_settings', 'current_mode');
+ this.currentMode = null;
+
+ secureLogger.info('Security mode reset', {
+ component: 'SecurityModeService',
+ operation: 'resetSecurityMode'
+ });
+ } catch (error) {
+ secureLogger.error('Failed to reset security mode', error as Error, {
+ component: 'SecurityModeService',
+ operation: 'resetSecurityMode'
+ });
+ throw error;
+ }
+ }
+
+ /**
+ * RécupÚre l'historique des modes de sécurisation
+ */
+ public async getSecurityModeHistory(): Promise {
+ try {
+ const history = await this.database.getObject('security_settings', 'mode_history') || [];
+ return history;
+ } catch (error) {
+ secureLogger.error('Failed to retrieve security mode history', error as Error, {
+ component: 'SecurityModeService',
+ operation: 'getSecurityModeHistory'
+ });
+ return [];
+ }
+ }
+
+ /**
+ * Ajoute une entrée à l'historique des modes
+ */
+ private async addToHistory(mode: SecurityMode): Promise {
+ try {
+ const history = await this.getSecurityModeHistory();
+ history.unshift({
+ mode,
+ timestamp: Date.now(),
+ config: this.getSecurityModeConfig(mode)
+ });
+
+ // Garder seulement les 10 derniÚres entrées
+ if (history.length > 10) {
+ history.splice(10);
+ }
+
+ await this.database.setObject('security_settings', 'mode_history', history);
+ } catch (error) {
+ secureLogger.error('Failed to add to security mode history', error as Error, {
+ component: 'SecurityModeService',
+ operation: 'addToHistory',
+ mode
+ });
+ }
+ }
+}
diff --git a/src/services/service.ts b/src/services/service.ts
index e281b9a..0f7fb76 100755
--- a/src/services/service.ts
+++ b/src/services/service.ts
@@ -176,6 +176,39 @@ export default class Services {
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)`);
+
+ // Si la mémoire est déjà trÚs élevée, faire un nettoyage agressif immédiat
+ if (usedPercent > 90) {
+ console.log('đ§č High memory detected, performing immediate cleanup...');
+
+ // Nettoyage agressif immédiat
+ if (window.gc) {
+ for (let i = 0; i < 5; i++) {
+ window.gc();
+ }
+ }
+
+ // Nettoyer les caches
+ if ('caches' in window) {
+ const cacheNames = await caches.keys();
+ await Promise.all(cacheNames.map(name => caches.delete(name)));
+ }
+
+ // Nettoyer localStorage
+ if (window.localStorage) {
+ const keys = Object.keys(localStorage);
+ keys.forEach(key => {
+ if (key.startsWith('temp_') || key.startsWith('cache_') || key.startsWith('vite_')) {
+ localStorage.removeItem(key);
+ }
+ });
+ }
+
+ // Vérifier la mémoire aprÚs nettoyage
+ const memoryAfter = (performance as any).memory;
+ const usedPercentAfter = (memoryAfter.usedJSHeapSize / memoryAfter.jsHeapSizeLimit) * 100;
+ console.log(`đ Memory after cleanup: ${usedPercentAfter.toFixed(1)}% (${(memoryAfter.usedJSHeapSize / 1024 / 1024).toFixed(1)}MB)`);
+ }
}
// Show global loading spinner during initialization
@@ -281,7 +314,7 @@ export default class Services {
const memory = (performance as any).memory;
const usedPercent = (memory.usedJSHeapSize / memory.jsHeapSizeLimit) * 100;
- if (usedPercent > 95) {
+ if (usedPercent > 98) {
console.log('đ« Memory too high, skipping WebAssembly initialization');
Services.instance = new Services();
Services.initializing = null;