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:
parent
057102300a
commit
c2bd615e88
5
.cursor/rules/init.mdc
Normal file
5
.cursor/rules/init.mdc
Normal file
@ -0,0 +1,5 @@
|
|||||||
|
---
|
||||||
|
alwaysApply: true
|
||||||
|
---
|
||||||
|
|
||||||
|
lire avec attention: docs/INITIALIZATION_FLOW.md
|
||||||
@ -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
393
docs/INITIALIZATION_FLOW.md
Normal 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.
|
||||||
@ -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);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@ -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...');
|
||||||
|
|||||||
@ -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';
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@ -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);
|
||||||
|
|||||||
@ -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';
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
@ -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());
|
||||||
|
|||||||
@ -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}`);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
};
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@ -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);
|
||||||
|
|||||||
@ -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
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -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
|
||||||
*/
|
*/
|
||||||
|
|||||||
@ -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
144
test-birthday-setup.html
Normal 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>
|
||||||
Loading…
x
Reference in New Issue
Block a user