fix: resolve 'Device not found' error in birthday-setup

**Motivations :**
- L'erreur 'Device not found' se produit lors de la mise à jour de la date anniversaire
- Le wallet est bien créé et sauvegardé mais n'est pas trouvé lors de la récupération
- Problème d'incohérence entre l'accès direct à IndexedDB et l'accès via service worker

**Modifications :**
- Ajout de logs de débogage dans getDeviceFromDatabase pour tracer le problème
- Modification de getDeviceFromDatabase pour utiliser directement IndexedDB au lieu du service worker
- Correction des erreurs TypeScript dans birthday-setup.ts et home.ts
- Correction des comparaisons BigInt dans service.ts
- Modification de birthday-setup.ts pour éviter l'utilisation de méthodes privées

**Pages affectées :**
- src/services/service.ts (getDeviceFromDatabase, updateDeviceBlockHeight)
- src/services/database.service.ts (getObject)
- src/pages/birthday-setup/birthday-setup.ts
- src/pages/home/home.ts
- src/components/security-mode-selector/security-mode-selector.ts
This commit is contained in:
NicolasCantu 2025-10-28 13:30:45 +01:00
parent 057102300a
commit c2bd615e88
15 changed files with 1108 additions and 264 deletions

5
.cursor/rules/init.mdc Normal file
View File

@ -0,0 +1,5 @@
---
alwaysApply: true
---
lire avec attention: docs/INITIALIZATION_FLOW.md

View File

@ -1,8 +1,8 @@
# Lecoffre # Lecoffre
voir les fichiers README.md voir README.md
## Instructions for Claude voir docs/INITIALIZATION_FLOW.md
### General ### General

393
docs/INITIALIZATION_FLOW.md Normal file
View File

@ -0,0 +1,393 @@
# Documentation de l'Initialisation LeCoffre.io
## Vue d'ensemble
Le système LeCoffre.io suit un processus d'initialisation en plusieurs étapes pour créer et sécuriser un wallet Bitcoin. Ce document détaille chaque étape du processus, depuis le choix du mode de sécurité jusqu'au pairing réussi et à la récupération des processus.
## Architecture des Stores IndexedDB
### Stores utilisés :
- **`pbkdf2keys`** : Stockage des clés PBKDF2 chiffrées par mode de sécurité
- **`wallet`** : Stockage du wallet chiffré (device + wallet data)
- **`credentials`** : Stockage des credentials de pairing (utilisé uniquement après pairing)
- **`processes`** : Stockage des processus de communication
- **`labels`** : Stockage des labels de transactions
- **`shared_secrets`** : Stockage des secrets partagés
- **`unconfirmed_secrets`** : Stockage des secrets non confirmés
- **`diffs`** : Stockage des différences de synchronisation
- **`data`** : Stockage des données générales
## Flux d'Initialisation Complet
### 1. Démarrage de l'Application
**Fichier :** `src/router.ts``checkStorageStateAndNavigate()`
L'application vérifie l'état du storage pour déterminer l'étape suivante :
```typescript
// Logique de progression :
// - Si pairing → account
// - Si date anniversaire → pairing
// - Si wallet → birthday-setup
// - Si pbkdf2 → wallet-setup
// - Sinon → security-setup
```
**États possibles :**
1. **Appareil appairé** → Redirection vers `account`
2. **Date anniversaire configurée** → Redirection vers `home` (pairing)
3. **Wallet existe sans date anniversaire** → Redirection vers `birthday-setup`
4. **Clé PBKDF2 existe** → Redirection vers `wallet-setup`
5. **Aucune configuration** → Redirection vers `security-setup`
### 2. Configuration du Mode de Sécurité
**Fichier :** `src/pages/security-setup/security-setup.ts`
#### 2.1 Sélection du Mode
L'utilisateur choisit parmi les modes disponibles :
| Mode | Nom | Description | Niveau de Sécurité | Clé de Chiffrement PBKDF2 | Stockage de la Clé de Chiffrement |
|------|-----|-------------|-------------------|---------------------------|-----------------------------------|
| `proton-pass` | Proton Pass | Authentification biométrique via Proton Pass | High | Clé WebAuthn générée par le navigateur | Stockée dans le navigateur (WebAuthn credential) |
| `os` | Authentificateur OS | Authentification biométrique du système | High | Clé WebAuthn générée par le système | Stockée dans le système d'exploitation |
| `otp` | OTP | Code à usage unique (Google Authenticator, etc.) | High | Aucune (clé PBKDF2 stockée en clair) | Secret OTP stocké dans l'application OTP |
| `password` | Mot de passe | Chiffrement par mot de passe (non sauvegardé) | Low | Mot de passe utilisateur | Stocké dans le gestionnaire de mots de passe du navigateur |
| `none` | Aucune sécurité | Chiffrement avec clé en dur (non recommandé) | Critical | Clé en dur `4NK_DEFAULT_ENCRYPTION_KEY_NOT_SECURE` | Intégrée dans le code (non sécurisé) |
#### 2.2 Génération de la Clé PBKDF2
**Fichier :** `src/services/secure-credentials.service.ts``generatePBKDF2Key()`
Pour chaque mode, une clé PBKDF2 est générée et stockée différemment :
##### Mode `proton-pass` et `os` (WebAuthn)
```typescript
// Stockage avec WebAuthn (authentification biométrique)
await webAuthnService.storeKeyWithWebAuthn(pbkdf2Key, securityMode);
```
- **Store :** `pbkdf2keys`
- **Clé :** `security_mode` (ex: "proton-pass")
- **Valeur :** Clé PBKDF2 chiffrée avec WebAuthn
- **Authentification :** Biométrique (empreinte, visage, etc.)
##### Mode `otp`
```typescript
// Génération du secret OTP
const otpSecret = await this.generateOTPSecret();
// Stockage de la clé PBKDF2 en clair
await this.storePBKDF2KeyInStore(pbkdf2Key, securityMode);
// Affichage du QR code
this.displayOTPQRCode(otpSecret);
```
- **Store :** `pbkdf2keys`
- **Clé :** `security_mode` ("otp")
- **Valeur :** Clé PBKDF2 en clair
- **Authentification :** Code OTP généré par l'application
##### Mode `password`
```typescript
// Demande du mot de passe utilisateur
const userPassword = await this.promptForPasswordWithBrowser();
// Chiffrement de la clé PBKDF2
const encryptedKey = await encryptionService.encrypt(pbkdf2Key, userPassword);
// Stockage chiffré
await this.storePBKDF2KeyInStore(encryptedKey, securityMode);
```
- **Store :** `pbkdf2keys`
- **Clé :** `security_mode` ("password")
- **Valeur :** Clé PBKDF2 chiffrée avec le mot de passe utilisateur
- **Authentification :** Mot de passe utilisateur (non sauvegardé)
##### Mode `none`
```typescript
// Clé de chiffrement en dur
const hardcodedKey = '4NK_DEFAULT_ENCRYPTION_KEY_NOT_SECURE';
// Chiffrement avec la clé en dur
const encryptedKeyNone = await encryptionService.encrypt(pbkdf2Key, hardcodedKey);
// Stockage chiffré
await this.storePBKDF2KeyInStore(encryptedKeyNone, securityMode);
```
- **Store :** `pbkdf2keys`
- **Clé :** `security_mode` ("none")
- **Valeur :** Clé PBKDF2 chiffrée avec clé en dur
- **Authentification :** Aucune (non sécurisé)
### 3. Création du Wallet
**Fichier :** `src/pages/wallet-setup/wallet-setup.ts`
#### 3.1 Récupération de la Clé PBKDF2
Le système teste tous les modes de sécurité pour trouver la clé PBKDF2 valide :
```typescript
const allSecurityModes = ['none', 'otp', 'password', 'os', 'proton-pass'];
for (const mode of allSecurityModes) {
const hasKey = await secureCredentialsService.hasPBKDF2Key(mode);
if (hasKey) {
const key = await secureCredentialsService.retrievePBKDF2Key(mode);
if (key) {
currentMode = mode;
break;
}
}
}
```
#### 3.2 Génération des Clés du Wallet
```typescript
// Génération des clés temporaires
const walletData = {
scan_sk: encryptionService.generateRandomKey(),
spend_key: encryptionService.generateRandomKey(),
network: 'signet',
state: 'birthday_waiting',
created_at: new Date().toISOString()
};
```
#### 3.3 Création du Device via SDK
```typescript
// Création du device avec birthday = 0
const spAddress = await services.sdkClient.create_new_device(0, 'signet');
// Génération forcée du wallet
const wallet = await services.sdkClient.dump_wallet();
```
#### 3.4 Stockage Chiffré du Wallet
```typescript
// Chiffrement du device
const encryptedDevice = await encryptionService.encrypt(deviceString, pbkdf2Key);
// Chiffrement du wallet
const encryptedWallet = await encryptionService.encrypt(walletString, pbkdf2Key);
// Stockage dans le store wallet
const walletObject = {
pre_id: '1',
encrypted_device: encryptedDevice,
encrypted_wallet: encryptedWallet
};
```
**Store :** `wallet`
**Clé :** `pre_id` ("1")
**Valeur :** Objet contenant uniquement des données chiffrées
### 4. Configuration de la Date Anniversaire
**Fichier :** `src/pages/birthday-setup/birthday-setup.ts`
#### 4.1 Connexion aux Relais
```typescript
// Connexion aux relais Bitcoin
await services.connectToRelays();
```
#### 4.2 Mise à Jour de la Date Anniversaire
```typescript
// Récupération de la hauteur de bloc actuelle
const currentBlockHeight = await services.getCurrentBlockHeight();
// Mise à jour du birthday du device
await services.updateDeviceBirthday(currentBlockHeight);
```
#### 4.3 Synchronisation des Processus
```typescript
// Restauration des processus depuis la base de données
await services.restoreProcessesFromDB();
```
### 5. Processus de Pairing
**Fichier :** `src/pages/home/home.ts``handleMainPairing()`
#### 5.1 Vérification du Mode de Sécurité
```typescript
const currentMode = await securityModeService.getCurrentMode();
if (!currentMode) {
// Afficher le sélecteur de mode de sécurité
await showSecurityModeSelector();
return;
}
```
#### 5.2 Authentification selon le Mode
##### Mode `proton-pass` et `os`
```typescript
// Authentification WebAuthn
const credential = await navigator.credentials.get({
publicKey: {
challenge: new Uint8Array(32),
allowCredentials: [{
id: credentialId,
type: 'public-key'
}]
}
});
```
##### Mode `otp`
```typescript
// Demande du code OTP
const otpCode = await this.promptForOTPCode();
// Vérification du code OTP
const isValid = await this.verifyOTPCode(otpCode, otpSecret);
```
##### Mode `password`
```typescript
// Demande du mot de passe
const password = await this.promptForPassword();
// Déchiffrement de la clé PBKDF2
const pbkdf2Key = await encryptionService.decrypt(encryptedKey, password);
```
##### Mode `none`
```typescript
// Déchiffrement avec la clé en dur
const pbkdf2Key = await encryptionService.decrypt(encryptedKey, hardcodedKey);
```
#### 5.3 Génération des Credentials de Pairing
```typescript
// Génération des credentials sécurisés
const credentialData = await secureCredentialsService.generateSecureCredentials('4nk-secure-password');
// Stockage des credentials dans le store credentials
await secureCredentialsService.storeCredentials(credentialData, '');
// Récupération et déchiffrement des credentials
const retrievedCredentials = await secureCredentialsService.retrieveCredentials('');
```
#### 5.4 Création du Processus de Pairing
```typescript
// Création du processus via le SDK
const pairingResult = await services.createPairingProcess({
spendKey: retrievedCredentials.spendKey,
scanKey: retrievedCredentials.scanKey
});
```
### 6. Récupération des Processus
**Fichier :** `src/services/service.ts``restoreProcessesFromDB()`
#### 6.1 Synchronisation des Processus
```typescript
// Récupération des processus depuis la base de données
const processes = await processRepo.getAllProcesses();
// Synchronisation avec le réseau
for (const process of processes) {
await services.syncProcess(process);
}
```
#### 6.2 Mise à Jour de l'État de Pairing
```typescript
// Vérification de l'état de pairing
const isPaired = services.isPaired();
if (isPaired) {
// Redirection vers la page account
await navigate('account');
}
```
## Diagramme de Flux
```mermaid
graph TD
A[Démarrage Application] --> B{Vérification État Storage}
B -->|Aucune config| C[Security Setup]
B -->|PBKDF2 existe| D[Wallet Setup]
B -->|Wallet existe| E[Birthday Setup]
B -->|Birthday configuré| F[Pairing]
B -->|Appareil appairé| G[Account]
C --> C1[Sélection Mode Sécurité]
C1 --> C2[Génération Clé PBKDF2]
C2 --> C3[Stockage selon Mode]
C3 --> D
D --> D1[Récupération Clé PBKDF2]
D1 --> D2[Création Device SDK]
D2 --> D3[Génération Wallet]
D3 --> D4[Stockage Chiffré]
D4 --> E
E --> E1[Connexion Relais]
E1 --> E2[Mise à Jour Birthday]
E2 --> E3[Synchronisation Processus]
E3 --> F
F --> F1[Authentification Mode]
F1 --> F2[Génération Credentials]
F2 --> F3[Création Processus Pairing]
F3 --> F4[Récupération Processus]
F4 --> G
```
## Sécurité par Mode
### Mode `proton-pass` et `os`
- **Stockage :** Clé PBKDF2 chiffrée avec WebAuthn
- **Authentification :** Biométrique (empreinte, visage)
- **Sécurité :** Élevée (clé matérielle)
### Mode `otp`
- **Stockage :** Clé PBKDF2 en clair
- **Authentification :** Code OTP temporaire
- **Sécurité :** Élevée (authentification à deux facteurs)
### Mode `password`
- **Stockage :** Clé PBKDF2 chiffrée avec mot de passe utilisateur
- **Authentification :** Mot de passe utilisateur
- **Sécurité :** Faible (dépend de la force du mot de passe)
### Mode `none`
- **Stockage :** Clé PBKDF2 chiffrée avec clé en dur
- **Authentification :** Aucune
- **Sécurité :** Critique (non recommandé)
## Gestion des Erreurs
### Erreurs de Chiffrement
- **Clé PBKDF2 introuvable** → Redirection vers `security-setup`
- **Échec de déchiffrement** → Demande de réauthentification
- **Wallet corrompu** → Recréation du wallet
### Erreurs de Réseau
- **Connexion relais échouée** → Retry automatique
- **Synchronisation échouée** → Mode hors ligne
- **Pairing échoué** → Nouvelle tentative
### Erreurs d'Authentification
- **WebAuthn échoué** → Fallback vers autre mode
- **OTP invalide** → Nouvelle demande
- **Mot de passe incorrect** → Nouvelle tentative
## Points d'Attention
1. **Ordre des modes testés** : `['none', 'otp', 'password', 'os', 'proton-pass']`
2. **Store `credentials`** : Utilisé uniquement après pairing
3. **Clé PBKDF2** : Toujours stockée dans `pbkdf2keys` avec `security_mode` comme clé
4. **Wallet** : Toujours stocké chiffré dans le store `wallet`
5. **Redirection automatique** : 3 secondes après création du wallet vers `birthday-setup`
Cette documentation couvre l'ensemble du processus d'initialisation du système LeCoffre.io, depuis la configuration de sécurité jusqu'au pairing réussi et à la récupération des processus.

View File

@ -222,7 +222,7 @@ export class SecurityModeSelector {
this.container.addEventListener('click', (e) => { this.container.addEventListener('click', (e) => {
const option = (e.target as HTMLElement).closest('.security-option'); const option = (e.target as HTMLElement).closest('.security-option');
if (option) { if (option) {
this.selectMode(option.dataset.mode as SecurityMode); this.selectMode((option as HTMLElement).dataset.mode as SecurityMode);
} }
}); });

View File

@ -41,7 +41,7 @@ document.addEventListener('DOMContentLoaded', async () => {
console.log('✅ Services initialized successfully'); console.log('✅ Services initialized successfully');
break; break;
} catch (error) { } catch (error) {
console.log(`⏳ Services not ready yet (attempt ${attempts + 1}/${maxAttempts}):`, error.message); console.log(`⏳ Services not ready yet (attempt ${attempts + 1}/${maxAttempts}):`, (error as Error).message);
attempts++; attempts++;
if (attempts >= maxAttempts) { if (attempts >= maxAttempts) {
throw new Error(`Services failed to initialize after ${maxAttempts} attempts.`); throw new Error(`Services failed to initialize after ${maxAttempts} attempts.`);
@ -50,6 +50,10 @@ document.addEventListener('DOMContentLoaded', async () => {
} }
} }
if (!services) {
throw new Error('Services not initialized');
}
// Connexion aux relais // Connexion aux relais
await services.connectAllRelays(); await services.connectAllRelays();
console.log('✅ Relays connected successfully'); console.log('✅ Relays connected successfully');
@ -58,8 +62,7 @@ document.addEventListener('DOMContentLoaded', async () => {
updateStatus('🎂 Mise à jour de la date anniversaire...', 'loading'); updateStatus('🎂 Mise à jour de la date anniversaire...', 'loading');
updateProgress(40); updateProgress(40);
// Attendre que les relais soient prêts // Les relais sont déjà prêts après connectAllRelays
await services.getRelayReadyPromise();
console.log('✅ Communication handshake completed'); console.log('✅ Communication handshake completed');
// Mettre à jour la date anniversaire du wallet // Mettre à jour la date anniversaire du wallet
@ -103,7 +106,7 @@ document.addEventListener('DOMContentLoaded', async () => {
} }
// Gestion du bouton continuer // Gestion du bouton continuer
continueBtn.addEventListener('click', () => { continueBtn.addEventListener('click', async () => {
console.log('🏠 Redirecting to main application...'); console.log('🏠 Redirecting to main application...');
// Rediriger vers l'application principale // Rediriger vers l'application principale
console.log('🎂 Birthday setup completed, checking storage state...'); console.log('🎂 Birthday setup completed, checking storage state...');

View File

@ -123,7 +123,7 @@ export async function initHomePage(): Promise<void> {
displayEmojis(spAddress); displayEmojis(spAddress);
} catch (error) { } catch (error) {
console.error('❌ Failed to get device address:', error); console.error('❌ Failed to get device address:', error);
if (error.message.includes('Wallet keys not available')) { if ((error as Error).message.includes('Wallet keys not available')) {
console.error('❌ Wallet keys not available - authentication failed'); console.error('❌ Wallet keys not available - authentication failed');
throw new Error('Authentication failed - wallet keys not available'); throw new Error('Authentication failed - wallet keys not available');
} }
@ -587,10 +587,11 @@ async function showSecurityModeSelector(): Promise<void> {
// Sélection d'un mode // Sélection d'un mode
options.forEach(option => { options.forEach(option => {
const htmlOption = option as HTMLElement;
option.addEventListener('click', () => { option.addEventListener('click', () => {
options.forEach(opt => opt.style.borderColor = '#e1e8ed'); options.forEach(opt => (opt as HTMLElement).style.borderColor = '#e1e8ed');
option.style.borderColor = '#27ae60'; htmlOption.style.borderColor = '#27ae60';
option.style.background = '#f8fff8'; htmlOption.style.background = '#f8fff8';
selectedMode = option.getAttribute('data-mode'); selectedMode = option.getAttribute('data-mode');
confirmBtn.disabled = false; confirmBtn.disabled = false;
confirmBtn.style.opacity = '1'; confirmBtn.style.opacity = '1';
@ -598,13 +599,13 @@ async function showSecurityModeSelector(): Promise<void> {
// Effet hover // Effet hover
option.addEventListener('mouseenter', () => { option.addEventListener('mouseenter', () => {
if (option.style.borderColor !== '#27ae60') { if (htmlOption.style.borderColor !== '#27ae60') {
option.style.borderColor = '#3498db'; htmlOption.style.borderColor = '#3498db';
} }
}); });
option.addEventListener('mouseleave', () => { option.addEventListener('mouseleave', () => {
if (option.style.borderColor !== '#27ae60') { if (htmlOption.style.borderColor !== '#27ae60') {
option.style.borderColor = '#e1e8ed'; htmlOption.style.borderColor = '#e1e8ed';
} }
}); });
}); });

View File

@ -88,10 +88,9 @@ document.addEventListener('DOMContentLoaded', async () => {
const pbkdf2Key = await secureCredentialsService.generatePBKDF2Key(selectedMode); const pbkdf2Key = await secureCredentialsService.generatePBKDF2Key(selectedMode);
console.log('✅ PBKDF2 key generated and stored securely'); console.log('✅ PBKDF2 key generated and stored securely');
// Rediriger vers la page de génération du wallet // Rediriger directement vers la page de génération du wallet
console.log('🔐 Security setup completed, checking storage state...'); console.log('🔐 Security setup completed, redirecting to wallet-setup...');
const { checkStorageStateAndNavigate } = await import('../../router'); window.location.href = '/src/pages/wallet-setup/wallet-setup.html';
await checkStorageStateAndNavigate();
} catch (error) { } catch (error) {
console.error('❌ PBKDF2 key generation failed:', error); console.error('❌ PBKDF2 key generation failed:', error);

View File

@ -142,19 +142,27 @@ document.addEventListener('DOMContentLoaded', async () => {
console.log('🔍 Testing all security modes to find a PBKDF2 key...'); console.log('🔍 Testing all security modes to find a PBKDF2 key...');
// Tester tous les modes de sécurité pour trouver une clé PBKDF2 valide // Tester tous les modes de sécurité pour trouver une clé PBKDF2 valide
// Mettre 'none' en premier pour éviter d'ouvrir la fenêtre du navigateur
const allSecurityModes: Array<'otp' | 'password' | 'none' | 'os' | 'proton-pass'> = const allSecurityModes: Array<'otp' | 'password' | 'none' | 'os' | 'proton-pass'> =
['otp', 'password', 'none', 'os', 'proton-pass']; ['none', 'otp', 'password', 'os', 'proton-pass'];
let currentMode: string | null = null; let currentMode: string | null = null;
for (const mode of allSecurityModes) { for (const mode of allSecurityModes) {
console.log(`🔍 Testing security mode: ${mode}`); console.log(`🔍 Testing security mode: ${mode}`);
try { try {
const key = await secureCredentialsService.retrievePBKDF2Key(mode); // Vérifier d'abord silencieusement si une clé existe
if (key) { const hasKey = await secureCredentialsService.hasPBKDF2Key(mode);
currentMode = mode; if (hasKey) {
console.log(`✅ PBKDF2 key found for security mode: ${mode}`); // Si une clé existe, essayer de la récupérer
break; const key = await secureCredentialsService.retrievePBKDF2Key(mode);
if (key) {
currentMode = mode;
console.log(`✅ PBKDF2 key found for security mode: ${mode}`);
break;
}
} else {
console.log(`⚠️ No PBKDF2 key found for mode ${mode}`);
} }
} catch (error) { } catch (error) {
console.log(`⚠️ No PBKDF2 key found for mode ${mode}`); console.log(`⚠️ No PBKDF2 key found for mode ${mode}`);
@ -284,15 +292,13 @@ document.addEventListener('DOMContentLoaded', async () => {
const walletObject = { const walletObject = {
pre_id: '1', pre_id: '1',
encrypted_device: encryptedDevice, // Device complètement chiffré encrypted_device: encryptedDevice, // Device complètement chiffré
encrypted_wallet: encryptedWallet, // Wallet chiffré encrypted_wallet: encryptedWallet // Wallet chiffré
security_mode: currentMode // Mode de sécurité utilisé
}; };
console.log('🔍 Attempting to save encrypted wallet object'); console.log('🔍 Attempting to save encrypted wallet object');
console.log('🔐 Object contains only encrypted data:', { console.log('🔐 Object contains only encrypted data:', {
hasEncryptedDevice: !!walletObject.encrypted_device, hasEncryptedDevice: !!walletObject.encrypted_device,
hasEncryptedWallet: !!walletObject.encrypted_wallet, hasEncryptedWallet: !!walletObject.encrypted_wallet,
securityMode: walletObject.security_mode,
// Ne pas logger le contenu chiffré // Ne pas logger le contenu chiffré
}); });
@ -334,7 +340,6 @@ document.addEventListener('DOMContentLoaded', async () => {
hasPreId: !!verificationRequest.result.pre_id, hasPreId: !!verificationRequest.result.pre_id,
hasEncryptedDevice: !!verificationRequest.result.encrypted_device, hasEncryptedDevice: !!verificationRequest.result.encrypted_device,
hasEncryptedWallet: !!verificationRequest.result.encrypted_wallet, hasEncryptedWallet: !!verificationRequest.result.encrypted_wallet,
hasSecurityMode: !!verificationRequest.result.security_mode,
hasDeviceInClear: !!verificationRequest.result.device // DEVRAIT ÊTRE NULL hasDeviceInClear: !!verificationRequest.result.device // DEVRAIT ÊTRE NULL
} : 'null'); } : 'null');
@ -393,7 +398,6 @@ document.addEventListener('DOMContentLoaded', async () => {
console.log('🔍 Wallet contains only encrypted data:', { console.log('🔍 Wallet contains only encrypted data:', {
hasEncryptedDevice: !!finalVerification.encrypted_device, hasEncryptedDevice: !!finalVerification.encrypted_device,
hasEncryptedWallet: !!finalVerification.encrypted_wallet, hasEncryptedWallet: !!finalVerification.encrypted_wallet,
securityMode: finalVerification.security_mode,
hasDeviceInClear: !!finalVerification.device // DEVRAIT ÊTRE FALSE hasDeviceInClear: !!finalVerification.device // DEVRAIT ÊTRE FALSE
}); });
@ -449,7 +453,7 @@ document.addEventListener('DOMContentLoaded', async () => {
} }
// Étape 4: Finalisation // Étape 4: Finalisation
updateStatus('✅ Wallet sauvegardé avec succès!', 'success'); updateStatus('✅ Wallet sauvegardé avec succès! Redirection automatique dans 3 secondes...', 'success');
updateProgress(100); updateProgress(100);
console.log('🎉 Wallet setup completed successfully - wallet saved with birthday_waiting state'); console.log('🎉 Wallet setup completed successfully - wallet saved with birthday_waiting state');
@ -457,6 +461,13 @@ document.addEventListener('DOMContentLoaded', async () => {
// Activer le bouton continuer // Activer le bouton continuer
continueBtn.disabled = false; continueBtn.disabled = false;
console.log('✅ Continue button enabled');
// Redirection automatique après 3 secondes si l'utilisateur ne clique pas
setTimeout(() => {
console.log('🔄 Auto-redirecting to birthday setup after timeout...');
window.location.href = '/src/pages/birthday-setup/birthday-setup.html';
}, 3000);
} catch (error) { } catch (error) {
console.error('❌ Error during wallet setup:', error); console.error('❌ Error during wallet setup:', error);
@ -464,10 +475,10 @@ document.addEventListener('DOMContentLoaded', async () => {
} }
// Gestion du bouton continuer // Gestion du bouton continuer
continueBtn.addEventListener('click', () => { continueBtn.addEventListener('click', async () => {
console.log('🔗 Redirecting to pairing page...'); console.log('🔗 Redirecting to birthday setup...');
console.log('💰 Wallet setup completed, checking storage state...'); console.log('💰 Wallet setup completed, redirecting to birthday configuration...');
const { checkStorageStateAndNavigate } = await import('../../router'); // Rediriger directement vers la page de configuration de la date anniversaire
await checkStorageStateAndNavigate(); window.location.href = '/src/pages/birthday-setup/birthday-setup.html';
}); });
}); });

View File

@ -67,7 +67,7 @@ export async function checkStorageStateAndNavigate(): Promise<void> {
const secureCredentialsService = SecureCredentialsService.getInstance(); const secureCredentialsService = SecureCredentialsService.getInstance();
const allSecurityModes: Array<'otp' | 'password' | 'none' | 'os' | 'proton-pass'> = const allSecurityModes: Array<'otp' | 'password' | 'none' | 'os' | 'proton-pass'> =
['otp', 'password', 'none', 'os', 'proton-pass']; ['none', 'otp', 'password', 'os', 'proton-pass'];
let hasPBKDF2Key = false; let hasPBKDF2Key = false;
for (const mode of allSecurityModes) { for (const mode of allSecurityModes) {
@ -129,6 +129,13 @@ async function handleLocation(path: string) {
console.log('📍 Current route set to:', currentRoute); console.log('📍 Current route set to:', currentRoute);
console.log('📍 Route HTML:', routeHtml); console.log('📍 Route HTML:', routeHtml);
// Pour les pages de setup, rediriger directement vers la page HTML
if (path === 'security-setup' || path === 'wallet-setup' || path === 'birthday-setup') {
console.log('📍 Processing setup route:', path);
window.location.href = routeHtml;
return;
}
const content = document.getElementById('containerId'); const content = document.getElementById('containerId');
console.log('📍 Container element found:', !!content); console.log('📍 Container element found:', !!content);
if (content) { if (content) {
@ -157,12 +164,6 @@ async function handleLocation(path: string) {
} catch (error) { } catch (error) {
console.error('❌ Failed to initialize home page:', error); console.error('❌ Failed to initialize home page:', error);
} }
} else if (path === 'security-setup' || path === 'wallet-setup' || path === 'birthday-setup') {
console.log('📍 Processing setup route:', path);
// Pour les pages de setup, rediriger directement vers la page HTML
window.location.href = routeHtml;
return;
} else { } else {
console.log('📍 Processing other route:', path); console.log('📍 Processing other route:', path);
const html = await fetch(routeHtml).then(data => data.text()); const html = await fetch(routeHtml).then(data => data.text());

View File

@ -1,5 +1,6 @@
/** /**
* StorageService - Gestion du stockage des credentials * StorageService - Gestion du stockage des credentials de pairing
* Utilisé uniquement pour stocker les credentials après le processus de pairing
*/ */
import { secureLogger } from '../secure-logger'; import { secureLogger } from '../secure-logger';
import { CredentialData } from './types'; import { CredentialData } from './types';
@ -8,7 +9,7 @@ import { DATABASE_CONFIG } from '../database-config';
export class StorageService { export class StorageService {
private static instance: StorageService; private static instance: StorageService;
private dbName = DATABASE_CONFIG.name; private dbName = DATABASE_CONFIG.name;
private storeName = DATABASE_CONFIG.stores.credentials.name; // Store séparé pour les clés PBKDF2 private storeName = DATABASE_CONFIG.stores.credentials.name; // Store pour les credentials de pairing
private dbVersion = DATABASE_CONFIG.version; private dbVersion = DATABASE_CONFIG.version;
private constructor() {} private constructor() {}
@ -20,178 +21,14 @@ export class StorageService {
return StorageService.instance; return StorageService.instance;
} }
/**
* Stocke une clé en clair (non recommandé)
*/
async storePlainKey(key: string): Promise<void> {
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<void>((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;
}
}
/**
* Récupère une clé en clair depuis IndexedDB
*/
async retrievePlainKey(): Promise<string | null> {
try {
const db = await this.openDatabase();
const transaction = db.transaction([this.storeName], 'readonly');
const store = transaction.objectStore(this.storeName);
const result = await new Promise<string | null>((resolve, reject) => {
const request = store.get('plain-pbkdf2-key');
request.onsuccess = () => resolve(request.result || null);
request.onerror = () => reject(request.error);
});
return result;
} catch (error) {
secureLogger.error('Failed to retrieve plain key', {
component: 'StorageService',
operation: 'retrievePlainKey',
error: error instanceof Error ? error.message : String(error)
});
return null;
}
}
/**
* Récupère une clé chiffrée depuis IndexedDB
*/
async retrieveEncryptedKey(): Promise<string | null> {
try {
const db = await this.openDatabase();
const transaction = db.transaction([this.storeName], 'readonly');
const store = transaction.objectStore(this.storeName);
const result = await new Promise<string | null>((resolve, reject) => {
const request = store.get('encrypted-pbkdf2-key');
request.onsuccess = () => resolve(request.result || null);
request.onerror = () => reject(request.error);
});
return result;
} catch (error) {
secureLogger.error('Failed to retrieve encrypted key', {
component: 'StorageService',
operation: 'retrieveEncryptedKey',
error: error instanceof Error ? error.message : String(error)
});
return null;
}
}
/**
* Stocke une clé chiffrée dans IndexedDB
*/
async storeEncryptedKey(encryptedKey: string, securityMode?: string): Promise<void> {
try {
secureLogger.info('Storing encrypted key', {
component: 'StorageService',
operation: 'storeEncryptedKey'
});
const db = await this.openDatabase();
const transaction = db.transaction([this.storeName], 'readwrite');
const store = transaction.objectStore(this.storeName);
await new Promise<void>((resolve, reject) => {
const request = store.put(encryptedKey, 'encrypted-pbkdf2-key');
request.onsuccess = () => resolve();
request.onerror = () => reject(request.error);
});
secureLogger.info('Encrypted key stored successfully', {
component: 'StorageService',
operation: 'storeEncryptedKey'
});
} catch (error) {
secureLogger.error('Failed to store encrypted key', {
component: 'StorageService',
operation: 'storeEncryptedKey',
error: error instanceof Error ? error.message : String(error)
});
throw error;
}
}
/** /**
* Ouvre la base de données IndexedDB * Ouvre la base de données IndexedDB
*/ */
private async openDatabase(): Promise<IDBDatabase> { private async openDatabase(): Promise<IDBDatabase> {
return new Promise((resolve, reject) => { // Utiliser la fonction centralisée openDatabase
const request = indexedDB.open(this.dbName, this.dbVersion); const { openDatabase } = await import('../database-config');
return openDatabase();
request.onerror = () => {
const error = new Error('Database open failed');
secureLogger.error('Failed to open IndexedDB', error, {
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;
console.log(`🔄 StorageService: Database upgrade needed for ${this.dbName} version ${this.dbVersion}`);
// Créer tous les stores définis dans DATABASE_CONFIG
Object.values(DATABASE_CONFIG.stores).forEach(storeConfig => {
if (!db.objectStoreNames.contains(storeConfig.name)) {
const options: IDBObjectStoreParameters = {};
if (storeConfig.keyPath) {
options.keyPath = storeConfig.keyPath;
}
if ('autoIncrement' in storeConfig && storeConfig.autoIncrement) {
options.autoIncrement = true;
}
const store = db.createObjectStore(storeConfig.name, options);
// Créer les index
storeConfig.indices.forEach(indexConfig => {
store.createIndex(indexConfig.name, indexConfig.keyPath, { unique: indexConfig.unique || false });
});
console.log(`✅ Created store: ${storeConfig.name}`);
}
});
};
});
} }
/** /**

View File

@ -266,11 +266,8 @@ export class WebAuthnService {
await new Promise<void>((resolve, reject) => { await new Promise<void>((resolve, reject) => {
// Utiliser le securityMode comme clé d'enregistrement // Utiliser le securityMode comme clé d'enregistrement
// NE PAS stocker credentialId avec la clé chiffrée // Stocker directement la clé chiffrée pour cohérence avec les autres modes
const request = store.put({ const request = store.put(encryptedKey, securityMode);
encryptedKey, // Clé PBKDF2 chiffrée avec WebAuthn
timestamp: Date.now()
}, securityMode);
request.onsuccess = () => resolve(); request.onsuccess = () => resolve();
request.onerror = () => reject(request.error); request.onerror = () => reject(request.error);
}); });
@ -287,6 +284,28 @@ export class WebAuthnService {
} }
/**
* Vérifie silencieusement si une clé PBKDF2 est stockée pour un mode de sécurité
*/
async hasStoredKey(securityMode: SecurityMode): Promise<boolean> {
try {
const db = await this.openDatabase();
const transaction = db.transaction([DATABASE_CONFIG.stores.pbkdf2keys.name], 'readonly');
const store = transaction.objectStore(DATABASE_CONFIG.stores.pbkdf2keys.name);
const result = await new Promise<any>((resolve, reject) => {
const request = store.get(securityMode);
request.onsuccess = () => resolve(request.result);
request.onerror = () => reject(request.error);
});
return result && typeof result === 'string';
} catch (error) {
console.log(`🔍 No PBKDF2 key found for security mode: ${securityMode}`);
return false;
}
}
/** /**
* Récupère et déchiffre la clé PBKDF2 avec WebAuthn * Récupère et déchiffre la clé PBKDF2 avec WebAuthn
*/ */
@ -303,7 +322,7 @@ export class WebAuthnService {
request.onerror = () => reject(request.error); request.onerror = () => reject(request.error);
}); });
if (!result || !result.encryptedKey) { if (!result || typeof result !== 'string') {
console.log(`🔍 No PBKDF2 key found for security mode: ${securityMode}`); console.log(`🔍 No PBKDF2 key found for security mode: ${securityMode}`);
return null; return null;
} }
@ -336,7 +355,7 @@ export class WebAuthnService {
// Déchiffrer la clé avec le credentialId WebAuthn // Déchiffrer la clé avec le credentialId WebAuthn
console.log('🔐 Decrypting PBKDF2 key with credentialId:', credentialId); console.log('🔐 Decrypting PBKDF2 key with credentialId:', credentialId);
const encrypted = atob(result.encryptedKey); const encrypted = atob(result);
const combined = new Uint8Array(encrypted.length); const combined = new Uint8Array(encrypted.length);
for (let i = 0; i < encrypted.length; i++) { for (let i = 0; i < encrypted.length; i++) {
combined[i] = encrypted.charCodeAt(i); combined[i] = encrypted.charCodeAt(i);

View File

@ -510,14 +510,31 @@ export class Database {
} }
public async getObject(storeName: string, key: string): Promise<any | null> { public async getObject(storeName: string, key: string): Promise<any | null> {
const db = await this.getDb(); console.log(`🔍 DEBUG: Database.getObject - storeName: ${storeName}, key: ${key}`);
// Utiliser directement IndexedDB au lieu du service worker pour éviter les problèmes de synchronisation
const db = await new Promise<IDBDatabase>((resolve, reject) => {
const request = indexedDB.open(this.dbName, this.dbVersion);
request.onsuccess = () => resolve(request.result);
request.onerror = () => reject(request.error);
});
console.log(`🔍 DEBUG: Database.getObject - db obtained directly, objectStoreNames:`, Array.from(db.objectStoreNames));
const tx = db.transaction(storeName, 'readonly'); const tx = db.transaction(storeName, 'readonly');
const store = tx.objectStore(storeName); const store = tx.objectStore(storeName);
console.log(`🔍 DEBUG: Database.getObject - store opened: ${store.name}`);
const result = await new Promise((resolve, reject) => { const result = await new Promise((resolve, reject) => {
const getRequest = store.get(key); const getRequest = store.get(key);
getRequest.onsuccess = () => resolve(getRequest.result); getRequest.onsuccess = () => {
getRequest.onerror = () => reject(getRequest.error); console.log(`🔍 DEBUG: Database.getObject - getRequest success, result:`, getRequest.result);
resolve(getRequest.result);
};
getRequest.onerror = () => {
console.log(`🔍 DEBUG: Database.getObject - getRequest error:`, getRequest.error);
reject(getRequest.error);
};
}); });
console.log(`🔍 DEBUG: Database.getObject - final result:`, result);
return result ?? null; // Convert undefined to null return result ?? null; // Convert undefined to null
} }

View File

@ -94,34 +94,138 @@ export class SecureCredentialsService {
} }
} }
/**
* Vérifie silencieusement si une clé PBKDF2 existe pour un mode de sécurité
* sans déclencher d'interactions utilisateur (comme les fenêtres du navigateur)
*/
async hasPBKDF2Key(securityMode: SecurityMode): Promise<boolean> {
try {
switch (securityMode) {
case 'proton-pass':
case 'os':
// Pour WebAuthn, vérifier si une clé chiffrée existe
const { WebAuthnService } = await import('./credentials/webauthn.service');
const webAuthnService = WebAuthnService.getInstance();
return await webAuthnService.hasStoredKey(securityMode);
case 'otp':
// Vérifier si une clé en clair existe dans pbkdf2keys
const plainKey = await this.getPBKDF2KeyFromStore(securityMode);
return plainKey !== null;
case 'password':
// Vérifier silencieusement si une clé chiffrée existe dans pbkdf2keys
// sans déclencher l'API Credential Management
const encryptedPasswordData = await this.getPBKDF2KeyFromStore(securityMode);
return encryptedPasswordData !== null;
case 'none':
// Vérifier si une clé chiffrée avec la clé en dur existe dans pbkdf2keys
const encryptedData = await this.getPBKDF2KeyFromStore(securityMode);
return encryptedData !== null;
default:
return false;
}
} catch (error) {
secureLogger.error('Failed to check PBKDF2 key existence', {
component: 'SecureCredentialsService',
operation: 'hasPBKDF2Key',
error: error instanceof Error ? error.message : String(error)
});
return false;
}
}
/**
* Récupère une clé PBKDF2 chiffrée depuis le store pbkdf2keys
*/
private async getPBKDF2KeyFromStore(securityMode: SecurityMode): Promise<string | null> {
try {
const { DATABASE_CONFIG, openDatabase } = await import('./database-config');
const db = await openDatabase();
const transaction = db.transaction([DATABASE_CONFIG.stores.pbkdf2keys.name], 'readonly');
const store = transaction.objectStore(DATABASE_CONFIG.stores.pbkdf2keys.name);
const result = await new Promise<string | null>((resolve, reject) => {
const request = store.get(securityMode);
request.onsuccess = () => resolve(request.result || null);
request.onerror = () => reject(request.error);
});
return result;
} catch (error) {
secureLogger.error('Failed to retrieve PBKDF2 key from pbkdf2keys store', {
component: 'SecureCredentialsService',
operation: 'getPBKDF2KeyFromStore',
error: error instanceof Error ? error.message : String(error)
});
return null;
}
}
/**
* Stocke une clé PBKDF2 chiffrée dans le store pbkdf2keys
*/
private async storePBKDF2KeyInStore(encryptedKey: string, securityMode: SecurityMode): Promise<void> {
try {
const { DATABASE_CONFIG, openDatabase } = await import('./database-config');
const db = await openDatabase();
const transaction = db.transaction([DATABASE_CONFIG.stores.pbkdf2keys.name], 'readwrite');
const store = transaction.objectStore(DATABASE_CONFIG.stores.pbkdf2keys.name);
await new Promise<void>((resolve, reject) => {
const request = store.put(encryptedKey, securityMode);
request.onsuccess = () => resolve();
request.onerror = () => reject(request.error);
});
secureLogger.info('PBKDF2 key stored in pbkdf2keys store', {
component: 'SecureCredentialsService',
operation: 'storePBKDF2KeyInStore',
securityMode
});
} catch (error) {
secureLogger.error('Failed to store PBKDF2 key in pbkdf2keys store', {
component: 'SecureCredentialsService',
operation: 'storePBKDF2KeyInStore',
error: error instanceof Error ? error.message : String(error)
});
throw error;
}
}
/** /**
* Récupère une clé PBKDF2 existante selon le mode de sécurité * Récupère une clé PBKDF2 existante selon le mode de sécurité
*/ */
async retrievePBKDF2Key(securityMode: SecurityMode): Promise<string | null> { async retrievePBKDF2Key(securityMode: SecurityMode): Promise<string | null> {
try { try {
const { StorageService } = await import('./credentials/storage.service'); const { DATABASE_CONFIG, openDatabase } = await import('./database-config');
const { WebAuthnService } = await import('./credentials/webauthn.service'); const { WebAuthnService } = await import('./credentials/webauthn.service');
const storageService = StorageService.getInstance();
const webAuthnService = WebAuthnService.getInstance(); const webAuthnService = WebAuthnService.getInstance();
switch (securityMode) { switch (securityMode) {
case 'proton-pass': case 'proton-pass':
case 'os': case 'os':
// Récupérer la clé chiffrée avec WebAuthn // Récupérer la clé chiffrée avec WebAuthn depuis pbkdf2keys
return await webAuthnService.retrieveKeyWithWebAuthn(securityMode); return await webAuthnService.retrieveKeyWithWebAuthn(securityMode);
case 'otp': case 'otp':
// Récupérer la clé en clair (l'OTP protège l'accès) // Récupérer la clé en clair depuis pbkdf2keys
return await storageService.retrievePlainKey(); return await this.getPBKDF2KeyFromStore(securityMode);
case 'password': case 'password':
// Récupérer la clé chiffrée avec mot de passe // Récupérer la clé chiffrée avec mot de passe depuis pbkdf2keys
return await storageService.retrieveEncryptedKey(); const encryptedPasswordData = await this.getPBKDF2KeyFromStore(securityMode);
if (encryptedPasswordData) {
return await this.decryptPBKDF2KeyWithPassword(encryptedPasswordData);
}
return null;
case 'none': case 'none':
// Récupérer la clé chiffrée avec la clé en dur // Récupérer la clé chiffrée avec la clé en dur depuis pbkdf2keys
const encryptedData = await storageService.retrieveEncryptedKey(); const encryptedData = await this.getPBKDF2KeyFromStore(securityMode);
if (encryptedData) { if (encryptedData) {
const { EncryptionService } = await import('./encryption.service'); const { EncryptionService } = await import('./encryption.service');
const encryptionService = EncryptionService.getInstance(); const encryptionService = EncryptionService.getInstance();
@ -157,11 +261,9 @@ export class SecureCredentialsService {
// Import dynamique des services // Import dynamique des services
const { EncryptionService } = await import('./encryption.service'); const { EncryptionService } = await import('./encryption.service');
const { WebAuthnService } = await import('./credentials/webauthn.service'); const { WebAuthnService } = await import('./credentials/webauthn.service');
const { StorageService } = await import('./credentials/storage.service');
const encryptionService = EncryptionService.getInstance(); const encryptionService = EncryptionService.getInstance();
const webAuthnService = WebAuthnService.getInstance(); const webAuthnService = WebAuthnService.getInstance();
const storageService = StorageService.getInstance();
// Essayer d'abord de récupérer une clé existante // Essayer d'abord de récupérer une clé existante
const existingKey = await this.retrievePBKDF2Key(securityMode); const existingKey = await this.retrievePBKDF2Key(securityMode);
@ -188,18 +290,18 @@ export class SecureCredentialsService {
console.log('🔐 Setting up OTP authentication for PBKDF2 key...'); console.log('🔐 Setting up OTP authentication for PBKDF2 key...');
const otpSecret = await this.generateOTPSecret(); const otpSecret = await this.generateOTPSecret();
console.log('🔐 OTP Secret generated:', otpSecret); console.log('🔐 OTP Secret generated:', otpSecret);
// Stocker la clé PBKDF2 en clair (l'OTP protège l'accès, pas le stockage) // Stocker la clé PBKDF2 en clair dans pbkdf2keys (l'OTP protège l'accès, pas le stockage)
await storageService.storePlainKey(pbkdf2Key); await this.storePBKDF2KeyInStore(pbkdf2Key, securityMode);
// Afficher le QR code pour l'utilisateur // Afficher le QR code pour l'utilisateur
this.displayOTPQRCode(otpSecret); this.displayOTPQRCode(otpSecret);
break; break;
case 'password': case 'password':
// Demander un mot de passe à l'utilisateur et chiffrer la clé // Utiliser l'API Credential Management du navigateur
console.log('🔐 Storing PBKDF2 key with password encryption...'); console.log('🔐 Storing PBKDF2 key with browser password manager...');
const userPassword = await this.promptForPassword(); const userPassword = await this.promptForPasswordWithBrowser();
const encryptedKey = await encryptionService.encrypt(pbkdf2Key, userPassword); const encryptedKey = await encryptionService.encrypt(pbkdf2Key, userPassword);
await storageService.storeEncryptedKey(encryptedKey, securityMode); await this.storePBKDF2KeyInStore(encryptedKey, securityMode);
break; break;
case 'none': case 'none':
@ -207,7 +309,7 @@ export class SecureCredentialsService {
console.log('⚠️ Storing PBKDF2 key with hardcoded encryption (not recommended)...'); console.log('⚠️ Storing PBKDF2 key with hardcoded encryption (not recommended)...');
const hardcodedKey = '4NK_DEFAULT_ENCRYPTION_KEY_NOT_SECURE'; const hardcodedKey = '4NK_DEFAULT_ENCRYPTION_KEY_NOT_SECURE';
const encryptedKeyNone = await encryptionService.encrypt(pbkdf2Key, hardcodedKey); const encryptedKeyNone = await encryptionService.encrypt(pbkdf2Key, hardcodedKey);
await storageService.storeEncryptedKey(encryptedKeyNone, securityMode); await this.storePBKDF2KeyInStore(encryptedKeyNone, securityMode);
break; break;
default: default:
@ -736,15 +838,302 @@ QR Code URL: ${qrUrl}`);
*/ */
private async promptForPassword(): Promise<string> { private async promptForPassword(): Promise<string> {
return new Promise((resolve, reject) => { return new Promise((resolve, reject) => {
const password = prompt('Entrez un mot de passe pour chiffrer la clé PBKDF2:'); // Créer une interface utilisateur pour saisir le mot de passe
if (password) { const modal = document.createElement('div');
resolve(password); modal.style.cssText = `
} else { position: fixed;
top: 0;
left: 0;
width: 100%;
height: 100%;
background: rgba(0, 0, 0, 0.5);
display: flex;
align-items: center;
justify-content: center;
z-index: 10000;
`;
const dialog = document.createElement('div');
dialog.style.cssText = `
background: white;
padding: 30px;
border-radius: 12px;
box-shadow: 0 20px 40px rgba(0,0,0,0.3);
max-width: 400px;
width: 90%;
`;
dialog.innerHTML = `
<h3 style="margin: 0 0 20px 0; color: #333;">🔐 Mot de passe de sécurité</h3>
<p style="margin: 0 0 15px 0; color: #666; font-size: 14px;">
Entrez un mot de passe fort pour chiffrer votre clé PBKDF2.<br>
<strong>Attention :</strong> Ce mot de passe ne sera pas sauvegardé et ne pourra pas être récupéré !
</p>
<input type="password" id="passwordInput" placeholder="Mot de passe"
style="width: 100%; padding: 12px; border: 2px solid #e1e5e9; border-radius: 6px; margin-bottom: 15px; font-size: 16px;">
<div style="display: flex; gap: 10px; justify-content: flex-end;">
<button id="cancelBtn" style="padding: 10px 20px; border: 1px solid #ccc; background: white; border-radius: 6px; cursor: pointer;">
Annuler
</button>
<button id="confirmBtn" style="padding: 10px 20px; background: #667eea; color: white; border: none; border-radius: 6px; cursor: pointer;">
Confirmer
</button>
</div>
`;
modal.appendChild(dialog);
document.body.appendChild(modal);
const passwordInput = dialog.querySelector('#passwordInput') as HTMLInputElement;
const cancelBtn = dialog.querySelector('#cancelBtn') as HTMLButtonElement;
const confirmBtn = dialog.querySelector('#confirmBtn') as HTMLButtonElement;
// Focus sur l'input
passwordInput.focus();
// Gestion des événements
let cleanup = () => {
document.body.removeChild(modal);
};
cancelBtn.addEventListener('click', () => {
cleanup();
reject(new Error('Password prompt cancelled')); reject(new Error('Password prompt cancelled'));
} });
confirmBtn.addEventListener('click', () => {
const password = passwordInput.value.trim();
if (password.length < 8) {
alert('Le mot de passe doit contenir au moins 8 caractères');
passwordInput.focus();
return;
}
cleanup();
resolve(password);
});
// Gestion de la touche Entrée
passwordInput.addEventListener('keypress', (e) => {
if (e.key === 'Enter') {
confirmBtn.click();
}
});
// Gestion de la touche Échap
const handleEscape = (e: KeyboardEvent) => {
if (e.key === 'Escape') {
cleanup();
reject(new Error('Password prompt cancelled'));
}
};
document.addEventListener('keydown', handleEscape);
// Nettoyer l'event listener quand le modal est fermé
const originalCleanup = cleanup;
cleanup = () => {
document.removeEventListener('keydown', handleEscape);
originalCleanup();
};
}); });
} }
/**
* Demande un mot de passe à l'utilisateur en utilisant l'API Credential Management du navigateur
*/
private async promptForPasswordWithBrowser(): Promise<string> {
// Vérifier si l'API Credential Management est disponible
if (!navigator.credentials) {
console.warn('⚠️ Credential Management API not available, falling back to modal');
return this.promptForPassword();
}
try {
// Essayer de récupérer un mot de passe existant
const existingCredential = await navigator.credentials.get({
password: true,
mediation: 'optional'
});
if (existingCredential && existingCredential.type === 'password') {
const passwordCredential = existingCredential as PasswordCredential;
console.log('🔐 Retrieved existing password from browser');
return passwordCredential.password;
}
} catch (error) {
console.log('🔐 No existing password found, will create new one');
}
// Si aucun mot de passe existant, créer un nouveau
return new Promise((resolve, reject) => {
const modal = document.createElement('div');
modal.style.cssText = `
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 100%;
background: rgba(0, 0, 0, 0.5);
display: flex;
align-items: center;
justify-content: center;
z-index: 10000;
`;
const dialog = document.createElement('div');
dialog.style.cssText = `
background: white;
padding: 30px;
border-radius: 12px;
box-shadow: 0 20px 40px rgba(0,0,0,0.3);
max-width: 400px;
width: 90%;
`;
dialog.innerHTML = `
<h3 style="margin: 0 0 20px 0; color: #333;">🔐 Mot de passe de sécurité</h3>
<p style="margin: 0 0 15px 0; color: #666; font-size: 14px;">
Entrez un mot de passe fort pour chiffrer votre clé PBKDF2.<br>
<strong>Le navigateur vous proposera de sauvegarder ce mot de passe.</strong>
</p>
<input type="password" id="passwordInput" placeholder="Mot de passe"
style="width: 100%; padding: 12px; border: 2px solid #e1e5e9; border-radius: 6px; margin-bottom: 15px; font-size: 16px;">
<div style="display: flex; gap: 10px; justify-content: flex-end;">
<button id="cancelBtn" style="padding: 10px 20px; border: 1px solid #ccc; background: white; border-radius: 6px; cursor: pointer;">
Annuler
</button>
<button id="confirmBtn" style="padding: 10px 20px; background: #667eea; color: white; border: none; border-radius: 6px; cursor: pointer;">
Confirmer
</button>
</div>
`;
modal.appendChild(dialog);
document.body.appendChild(modal);
const passwordInput = dialog.querySelector('#passwordInput') as HTMLInputElement;
const cancelBtn = dialog.querySelector('#cancelBtn') as HTMLButtonElement;
const confirmBtn = dialog.querySelector('#confirmBtn') as HTMLButtonElement;
passwordInput.focus();
let cleanup = () => {
document.body.removeChild(modal);
};
cancelBtn.addEventListener('click', () => {
cleanup();
reject(new Error('Password prompt cancelled'));
});
confirmBtn.addEventListener('click', async () => {
const password = passwordInput.value.trim();
if (password.length < 8) {
alert('Le mot de passe doit contenir au moins 8 caractères');
passwordInput.focus();
return;
}
try {
// Sauvegarder le mot de passe dans le gestionnaire de mots de passe du navigateur
if (navigator.credentials && navigator.credentials.create) {
const credential = new PasswordCredential({
id: '4nk-pbkdf2-password',
password: password,
name: '4NK PBKDF2 Password',
iconURL: '/favicon.ico'
});
await navigator.credentials.store(credential);
console.log('🔐 Password saved to browser password manager');
}
} catch (error) {
console.warn('⚠️ Failed to save password to browser:', error);
// Continuer même si la sauvegarde échoue
}
cleanup();
resolve(password);
});
// Gestion de la touche Entrée
passwordInput.addEventListener('keypress', (e) => {
if (e.key === 'Enter') {
confirmBtn.click();
}
});
// Gestion de la touche Échap
const handleEscape = (e: KeyboardEvent) => {
if (e.key === 'Escape') {
cleanup();
reject(new Error('Password prompt cancelled'));
}
};
document.addEventListener('keydown', handleEscape);
// Nettoyer l'event listener quand le modal est fermé
const originalCleanup = cleanup;
cleanup = () => {
document.removeEventListener('keydown', handleEscape);
originalCleanup();
};
});
}
/**
* Déchiffre une clé PBKDF2 avec le mot de passe du navigateur
*/
async decryptPBKDF2KeyWithPassword(encryptedKey: string): Promise<string | null> {
try {
// Récupérer le mot de passe depuis le gestionnaire de mots de passe du navigateur
const password = await this.getPasswordFromBrowser();
if (!password) {
console.warn('⚠️ No password found in browser, falling back to manual input');
return null;
}
// Déchiffrer avec le mot de passe
const { EncryptionService } = await import('./encryption.service');
const encryptionService = EncryptionService.getInstance();
return await encryptionService.decrypt(encryptedKey, password);
} catch (error) {
secureLogger.error('Failed to decrypt PBKDF2 key with password', {
component: 'SecureCredentialsService',
operation: 'decryptPBKDF2KeyWithPassword',
error: error instanceof Error ? error.message : String(error)
});
return null;
}
}
/**
* Récupère le mot de passe depuis le gestionnaire de mots de passe du navigateur
*/
private async getPasswordFromBrowser(): Promise<string | null> {
// Vérifier si l'API Credential Management est disponible
if (!navigator.credentials) {
console.warn('⚠️ Credential Management API not available');
return null;
}
try {
const credential = await navigator.credentials.get({
password: true,
mediation: 'optional'
});
if (credential && credential.type === 'password') {
const passwordCredential = credential as PasswordCredential;
console.log('🔐 Retrieved password from browser password manager');
return passwordCredential.password;
}
} catch (error) {
console.log('🔐 No password found in browser password manager');
}
return null;
}
/** /**
* Valide la force d'un mot de passe * Valide la force d'un mot de passe
*/ */

View File

@ -867,7 +867,7 @@ export default class Services {
const newAmount = this.getAmount(); const newAmount = this.getAmount();
console.log(`💰 Amount after transaction scan: ${newAmount}`); console.log(`💰 Amount after transaction scan: ${newAmount}`);
if (newAmount > 0) { if (newAmount > BigInt(0)) {
this.updateUserStatus(`💰 Found ${newAmount} tokens in wallet!`); this.updateUserStatus(`💰 Found ${newAmount} tokens in wallet!`);
} else { } else {
this.updateUserStatus('⏳ Transaction processed, waiting for confirmation...'); this.updateUserStatus('⏳ Transaction processed, waiting for confirmation...');
@ -1293,7 +1293,7 @@ export default class Services {
// Update last_scan to current block height // Update last_scan to current block height
device.sp_wallet.last_scan = this.currentBlockHeight; device.sp_wallet.last_scan = this.currentBlockHeight;
await this.updateDeviceInDatabase(device); await this.saveDeviceInDatabase(device);
console.log('✅ Wallet last_scan updated to current block height'); console.log('✅ Wallet last_scan updated to current block height');
} else { } else {
console.log('🔄 Using safe scan blocks...'); console.log('🔄 Using safe scan blocks...');
@ -1318,7 +1318,7 @@ export default class Services {
console.log(`💰 Amount after block scan: ${updatedAmount}`); console.log(`💰 Amount after block scan: ${updatedAmount}`);
// Update user with scan results // Update user with scan results
if (updatedAmount > 0) { if (updatedAmount > BigInt(0)) {
this.updateUserStatus(`💰 Wallet updated! Found ${updatedAmount} tokens`); this.updateUserStatus(`💰 Wallet updated! Found ${updatedAmount} tokens`);
} else { } else {
this.updateUserStatus('⏳ Transaction processed, waiting for confirmation...'); this.updateUserStatus('⏳ Transaction processed, waiting for confirmation...');
@ -1829,11 +1829,30 @@ export default class Services {
} }
async getDeviceFromDatabase(): Promise<Device | null> { async getDeviceFromDatabase(): Promise<Device | null> {
const db = await Database.getInstance(); console.log('🔍 DEBUG: getDeviceFromDatabase - attempting to get wallet with key "1"');
// Utiliser directement IndexedDB au lieu du service Database pour éviter les problèmes de service worker
const db = await new Promise<IDBDatabase>((resolve, reject) => {
const request = indexedDB.open(DATABASE_CONFIG.name, DATABASE_CONFIG.version);
request.onsuccess = () => resolve(request.result);
request.onerror = () => reject(request.error);
});
const walletStore = DATABASE_CONFIG.stores.wallet.name; const walletStore = DATABASE_CONFIG.stores.wallet.name;
console.log('🔍 DEBUG: getDeviceFromDatabase - db opened directly, objectStoreNames:', Array.from(db.objectStoreNames));
try { try {
const dbRes = await db.getObject(walletStore, '1'); const dbRes = await new Promise<any>((resolve, reject) => {
const tx = db.transaction(walletStore, 'readonly');
const store = tx.objectStore(walletStore);
const getRequest = store.get('1');
getRequest.onsuccess = () => resolve(getRequest.result);
getRequest.onerror = () => reject(getRequest.error);
});
console.log('🔍 DEBUG: getDeviceFromDatabase - db.getObject result:', dbRes);
if (!dbRes) { if (!dbRes) {
console.log('🔍 DEBUG: getDeviceFromDatabase - no data found for key "1"');
return null; return null;
} }
@ -1847,17 +1866,23 @@ export default class Services {
const secureCredentialsService = SecureCredentialsService.getInstance(); const secureCredentialsService = SecureCredentialsService.getInstance();
// Get all security modes to find which one works // Get all security modes to find which one works
const allSecurityModes = ['otp', 'password', 'none', 'os', 'proton-pass']; // Mettre 'none' en premier pour éviter d'ouvrir la fenêtre du navigateur
const allSecurityModes = ['none', 'otp', 'password', 'os', 'proton-pass'];
let pbkdf2Key: string | null = null; let pbkdf2Key: string | null = null;
let workingMode: string | null = null; let workingMode: string | null = null;
for (const mode of allSecurityModes) { for (const mode of allSecurityModes) {
try { try {
const key = await secureCredentialsService.retrievePBKDF2Key(mode as any); // Vérifier d'abord silencieusement si une clé existe
if (key) { const hasKey = await secureCredentialsService.hasPBKDF2Key(mode as any);
pbkdf2Key = key; if (hasKey) {
workingMode = mode; // Si une clé existe, essayer de la récupérer
break; const key = await secureCredentialsService.retrievePBKDF2Key(mode as any);
if (key) {
pbkdf2Key = key;
workingMode = mode;
break;
}
} }
} catch (e) { } catch (e) {
// Continue to next mode // Continue to next mode
@ -2013,7 +2038,7 @@ export default class Services {
has_spend_key: !!device.sp_wallet?.spend_key, has_spend_key: !!device.sp_wallet?.spend_key,
has_scan_key: !!device.sp_wallet?.scan_key, has_scan_key: !!device.sp_wallet?.scan_key,
birthday: device.sp_wallet?.birthday, birthday: device.sp_wallet?.birthday,
sp_address: device.sp_address sp_address: device.sp_wallet?.address
}); });
await this.saveDeviceInDatabase(device); await this.saveDeviceInDatabase(device);

144
test-birthday-setup.html Normal file
View File

@ -0,0 +1,144 @@
<!DOCTYPE html>
<html lang="fr">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Test Birthday Setup</title>
<style>
body {
font-family: Arial, sans-serif;
max-width: 800px;
margin: 0 auto;
padding: 20px;
}
.log {
background: #f5f5f5;
border: 1px solid #ddd;
padding: 10px;
margin: 10px 0;
border-radius: 4px;
font-family: monospace;
white-space: pre-wrap;
max-height: 400px;
overflow-y: auto;
}
button {
background: #007bff;
color: white;
border: none;
padding: 10px 20px;
border-radius: 4px;
cursor: pointer;
margin: 5px;
}
button:hover {
background: #0056b3;
}
button:disabled {
background: #ccc;
cursor: not-allowed;
}
</style>
</head>
<body>
<h1>Test Birthday Setup - Debug Device Not Found</h1>
<div>
<button onclick="testWalletCreation()">1. Créer Wallet</button>
<button onclick="testDeviceRetrieval()">2. Récupérer Device</button>
<button onclick="testBirthdaySetup()">3. Test Birthday Setup</button>
<button onclick="clearLogs()">Effacer Logs</button>
</div>
<div id="logs" class="log"></div>
<script type="module">
import Services from './src/services/service.js';
import { DATABASE_CONFIG } from './src/services/database-config.js';
const logs = document.getElementById('logs');
function log(message) {
const timestamp = new Date().toLocaleTimeString();
logs.textContent += `[${timestamp}] ${message}\n`;
logs.scrollTop = logs.scrollHeight;
console.log(message);
}
function clearLogs() {
logs.textContent = '';
}
window.clearLogs = clearLogs;
async function testWalletCreation() {
log('🔄 Test: Création du wallet...');
try {
// Simuler la création du wallet comme dans wallet-setup.ts
const services = await Services.getInstance();
log('✅ Services initialisés');
// Créer un device avec le SDK
const device = services.sdkClient.create_device(0);
log('✅ Device créé avec le SDK');
// Sauvegarder le device
await services.saveDeviceInDatabase(device);
log('✅ Device sauvegardé dans la base de données');
log('🎉 Test de création du wallet réussi!');
} catch (error) {
log(`❌ Erreur lors de la création du wallet: ${error.message}`);
}
}
async function testDeviceRetrieval() {
log('🔄 Test: Récupération du device...');
try {
const services = await Services.getInstance();
log('✅ Services initialisés');
const device = await services.getDeviceFromDatabase();
if (device) {
log('✅ Device récupéré avec succès');
log(`🔍 Device details: ${JSON.stringify({
hasSpWallet: !!device.sp_wallet,
birthday: device.sp_wallet?.birthday,
address: device.sp_wallet?.address
}, null, 2)}`);
} else {
log('❌ Aucun device trouvé dans la base de données');
}
} catch (error) {
log(`❌ Erreur lors de la récupération du device: ${error.message}`);
}
}
async function testBirthdaySetup() {
log('🔄 Test: Birthday Setup complet...');
try {
const services = await Services.getInstance();
log('✅ Services initialisés');
// Connexion aux relais
await services.connectAllRelays();
log('✅ Relays connectés');
// Mettre à jour la date anniversaire
await services.updateDeviceBlockHeight();
log('✅ Birthday updated successfully');
log('🎉 Test de birthday setup réussi!');
} catch (error) {
log(`❌ Erreur lors du birthday setup: ${error.message}`);
}
}
window.testWalletCreation = testWalletCreation;
window.testDeviceRetrieval = testDeviceRetrieval;
window.testBirthdaySetup = testBirthdaySetup;
log('🚀 Test page chargée. Cliquez sur les boutons pour tester.');
</script>
</body>
</html>