Fix pairing credentials restoration and wallet keys management
**Motivations :** - Le pairing échouait avec l'erreur 'Wallet keys not available - WebAuthn decryption required' - Le device stocké en base ne contenait pas les clés spend_key et scan_key dans sp_wallet - Ces clés étaient stockées séparément dans les credentials chiffrés - Il fallait restaurer ces clés dans le device en mémoire avant de pouvoir les utiliser **Modifications :** - ensureWalletKeysAvailable() : Maintenant asynchrone, vérifie si les clés sont disponibles dans le device en mémoire, sinon les restaure depuis les credentials - Restauration des clés : Si les clés ne sont pas en mémoire, la méthode récupère les credentials, restaure les clés dans le device, et le restaure via restoreDevice() - Méthodes asynchrones : getAmount() et getDeviceAddress() sont maintenant asynchrones pour supporter la restauration des clés - Appels mis à jour : Tous les appels à ces méthodes ont été mis à jour avec await **Pages affectées :** - src/services/service.ts : Restauration automatique des clés depuis les credentials - src/utils/sp-address.utils.ts : Appels asynchrones à getDeviceAddress() - src/router.ts : Appels asynchrones à getDeviceAddress() - src/components/device-management/device-management.ts : Appels asynchrones à getDeviceAddress() - src/services/iframe-pairing.service.ts : Appels asynchrones à getDeviceAddress()
This commit is contained in:
parent
0fa1423b13
commit
f7c2f86d30
@ -10,6 +10,7 @@ Le système LeCoffre.io suit un processus d'initialisation en plusieurs étapes
|
||||
- **`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)
|
||||
- **`env`** : Stockage des variables d'environnement internes (mots de passe, constantes)
|
||||
- **`processes`** : Stockage des processus de communication
|
||||
- **`labels`** : Stockage des labels de transactions
|
||||
- **`shared_secrets`** : Stockage des secrets partagés
|
||||
@ -381,15 +382,31 @@ window.location.href = '/src/pages/block-sync/block-sync.html';
|
||||
|
||||
### 5. Processus de Pairing
|
||||
|
||||
**Fichier :** `src/pages/home/home.ts` → `handleMainPairing()`
|
||||
**Fichier :** `src/pages/home/home.ts` → `handleMainPairing()` et `src/utils/sp-address.utils.ts` → `prepareAndSendPairingTx()`
|
||||
|
||||
#### But et Objectif du Pairing
|
||||
|
||||
Le processus de pairing dans LeCoffre.io sert à **créer une identité numérique vérifiable** qui permet :
|
||||
|
||||
1. **MFA (Multi-Factor Authentication) entre appareils** : Le quorum du processus permet de valider les actions critiques nécessitant plusieurs appareils appairés
|
||||
2. **Gestion autonome de la liste d'appareils** : L'utilisateur contrôle lui-même sa liste d'appareils autorisés, sans dépendre d'un tiers
|
||||
3. **Identité numérique décentralisée** : Le processus de pairing sert d'identité numérique vérifiable sur la blockchain
|
||||
4. **Système d'identité et de chiffrement** : Partage des secrets Silent Payment pour le chiffrement entre appareils appairés
|
||||
|
||||
#### Caractéristiques
|
||||
|
||||
- **Un wallet peut être appairé à plusieurs appareils** : Un même processus peut inclure N appareils
|
||||
- **Le pairing est un processus blockchain** : Création d'un processus avec états commités et vérifiables
|
||||
- **Synchronisation via relais** : Les relais synchronisent les transactions et les processus entre tous les appareils appairés
|
||||
- **Contrôle via 4 mots** : Les 4 mots permettent de contrôler le processus (rejoindre, mettre à jour, backup, support)
|
||||
|
||||
#### 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();
|
||||
// Redirection vers security-setup si aucun mode n'est configuré
|
||||
window.location.href = '/src/pages/security-setup/security-setup.html';
|
||||
return;
|
||||
}
|
||||
```
|
||||
@ -435,16 +452,21 @@ 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');
|
||||
// Génération des credentials sécurisés (mot de passe récupéré depuis le store env)
|
||||
const credentialData = await secureCredentialsService.generateSecureCredentials('');
|
||||
|
||||
// Stockage des credentials dans le store credentials
|
||||
await secureCredentialsService.storeCredentials(credentialData, '');
|
||||
|
||||
// Récupération et déchiffrement des credentials
|
||||
// Récupération et déchiffrement des credentials (mot de passe récupéré depuis le store env)
|
||||
const retrievedCredentials = await secureCredentialsService.retrieveCredentials('');
|
||||
```
|
||||
|
||||
**Store `env` :**
|
||||
- **Clé :** `CREDENTIALS_PASSWORD`
|
||||
- **Valeur :** `4nk-secure-password` (mot de passe interne pour les credentials)
|
||||
- **Description :** Mot de passe interne pour la génération des credentials de pairing
|
||||
|
||||
#### 5.4 Création du Processus de Pairing
|
||||
|
||||
```typescript
|
||||
|
||||
@ -4,6 +4,28 @@
|
||||
|
||||
Ce document résume l'analyse complète du système de pairing et les corrections apportées pour résoudre les problèmes identifiés.
|
||||
|
||||
## But et Objectif du Pairing
|
||||
|
||||
Le système de pairing dans LeCoffre.io sert avant tout à **créer une identité numérique vérifiable** qui permet :
|
||||
|
||||
1. **MFA (Multi-Factor Authentication) entre appareils** : Le quorum du processus de pairing permet de valider les actions critiques nécessitant plusieurs appareils appairés
|
||||
2. **Gestion autonome de la liste d'appareils** : L'utilisateur contrôle lui-même sa liste d'appareils autorisés, sans dépendre d'un tiers
|
||||
3. **Identité numérique décentralisée** : Le processus de pairing sert d'identité numérique vérifiable sur la blockchain
|
||||
4. **Système d'identité et de chiffrement** : Le wallet est avant tout un système d'identité et de chiffrement grâce aux secrets partagés du Silent Payment
|
||||
|
||||
### Caractéristiques principales
|
||||
|
||||
- **Un wallet peut être appairé à plusieurs appareils** : Un même processus de pairing peut inclure N appareils
|
||||
- **Le pairing est un processus blockchain** : Création d'un processus avec états commités et vérifiables
|
||||
- **Synchronisation via relais** : Les relais synchronisent les transactions et les processus entre tous les appareils appairés
|
||||
- **Contrôle via 4 mots** : Les 4 mots permettent de contrôler le processus (rejoindre, mettre à jour, backup, support)
|
||||
|
||||
### Différence avec les autres fonctionnalités
|
||||
|
||||
- **Backup du wallet** : Géré via les 4 mots dans les autres pages (permet de contrôler le processus pour auto-update)
|
||||
- **Support** : Géré via les 4 mots permettant un contrôle du processus
|
||||
- **Partage de secrets** : Les secrets Silent Payment sont partagés entre appareils appairés pour le chiffrement
|
||||
|
||||
## Problèmes Identifiés et Solutions
|
||||
|
||||
### 1. Problème de `checkConnections` pour le Pairing
|
||||
|
||||
530
docs/PROCESS_SYSTEM_ARCHITECTURE.md
Normal file
530
docs/PROCESS_SYSTEM_ARCHITECTURE.md
Normal file
@ -0,0 +1,530 @@
|
||||
# Architecture du Système de Processus et Updates
|
||||
|
||||
## Vue d'ensemble
|
||||
|
||||
Le système de processus est un **système générique et réutilisable** pour créer des "contrats" entre des membres avec des niveaux d'accès différents aux champs de données. C'est la fondation qui permet d'implémenter des fonctionnalités comme le pairing, mais aussi n'importe quel autre type de contrat décentralisé.
|
||||
|
||||
## Concepts Fondamentaux
|
||||
|
||||
### 1. Processus (Process)
|
||||
|
||||
Un **processus** est un contrat décentralisé entre plusieurs membres, commité sur la blockchain. Il représente un accord ou une entité partagée avec :
|
||||
|
||||
- **Identifiant unique** : `process_id`
|
||||
- **États successifs** : Historique des modifications
|
||||
- **Membres** : Participants au processus
|
||||
- **Rôles et permissions** : Définition des accès
|
||||
|
||||
### 2. État (State)
|
||||
|
||||
Chaque processus contient une liste d'**états** représentant l'évolution du processus dans le temps. Chaque état contient :
|
||||
|
||||
#### Données Publiques (`public_data`)
|
||||
- **Accessibles à tous** les membres du processus
|
||||
- **Inchangées** dans tous les nouveaux états (portées automatiquement)
|
||||
- **Encodées** mais non chiffrées (encodage JSON/Binary)
|
||||
- **Exemple** : Nom du processus, adresses appairées, métadonnées
|
||||
|
||||
#### Données Privées (via `pcd_commitment`)
|
||||
- **Chiffrées** et accessibles uniquement aux membres autorisés
|
||||
- **Commitment** : Hash des données privées (`pcd_commitment[field]`)
|
||||
- **Clés de déchiffrement** : Stockées dans `state.keys[field]` pour chaque membre autorisé
|
||||
- **Exemple** : Secrets, clés privées, données sensibles
|
||||
|
||||
#### Rôles (`roles`)
|
||||
- **Définition des permissions** par rôle
|
||||
- **Validation rules** : Quorum et champs accessibles par rôle
|
||||
- **Membres** : Liste des IDs de pairing process pour chaque rôle
|
||||
|
||||
#### Métadonnées d'État
|
||||
- `state_id` : Identifiant unique de l'état
|
||||
- `validation_tokens` : Tokens nécessaires pour la validation
|
||||
- `validation_result` : Résultat de la validation
|
||||
|
||||
### 3. Rôles et Permissions (`RoleDefinition`)
|
||||
|
||||
Un rôle définit qui peut accéder à quels champs et comment :
|
||||
|
||||
```typescript
|
||||
interface RoleDefinition {
|
||||
members: string[]; // IDs de pairing process (identifiants des membres)
|
||||
validation_rules: ValidationRule[]; // Règles de validation
|
||||
}
|
||||
|
||||
interface ValidationRule {
|
||||
quorum: number; // Quorum requis (ex: 1.0 = tous, 0.5 = 50%)
|
||||
fields: string[]; // Champs accessibles pour ce rôle
|
||||
}
|
||||
```
|
||||
|
||||
**Exemples de rôles** :
|
||||
- **Administrateur** : Quorum 1.0, accès à tous les champs
|
||||
- **Membre** : Quorum 0.5, accès aux champs non critiques
|
||||
- **Lecteur** : Quorum 0, accès en lecture seule aux champs publics
|
||||
|
||||
### 4. Membres
|
||||
|
||||
Les membres sont identifiés par leur **pairing process ID** (l'identité numérique vérifiable créée lors du pairing).
|
||||
|
||||
- Un membre peut participer à plusieurs processus
|
||||
- Un processus peut avoir plusieurs membres
|
||||
- Les adresses SP (Silent Payment) sont associées aux membres pour la communication
|
||||
|
||||
## Cycle de Vie d'un Processus
|
||||
|
||||
### Phase 1 : Création
|
||||
|
||||
```typescript
|
||||
createProcess(
|
||||
privateData: Record<string, any>, // Données privées initiales
|
||||
publicData: Record<string, any>, // Données publiques initiales
|
||||
roles: Record<string, RoleDefinition> // Définition des rôles
|
||||
): Promise<ApiReturn>
|
||||
```
|
||||
|
||||
**Étapes** :
|
||||
1. Encodage des données (JSON/Binary)
|
||||
2. Création du processus via SDK (WebAssembly)
|
||||
3. Génération du premier état (state 0)
|
||||
4. Établissement des connexions avec les membres
|
||||
|
||||
### Phase 2 : Mise à Jour
|
||||
|
||||
```typescript
|
||||
updateProcess(
|
||||
process: Process,
|
||||
privateData: Record<string, any>, // Nouvelles données privées
|
||||
publicData: Record<string, any>, // Nouvelles données publiques
|
||||
roles: Record<string, RoleDefinition> | null // Nouveaux rôles (optionnel)
|
||||
): Promise<ApiReturn>
|
||||
```
|
||||
|
||||
**Logique de classification des champs** :
|
||||
|
||||
Le système détermine automatiquement si un champ est public ou privé :
|
||||
|
||||
1. **Champ existant dans `public_data`** → Reste public
|
||||
2. **Champ nouveau dans `privateFields`** → Privé
|
||||
3. **Champ existant dans `pcd_commitment`** → Reste privé
|
||||
4. **Sinon** → Nouveau champ public
|
||||
|
||||
```typescript
|
||||
// Logique dans handleUpdateProcess (router.ts:811-846)
|
||||
for (const field of Object.keys(newData)) {
|
||||
// 1. Vérifier si c'est déjà public
|
||||
if (lastState.public_data[field]) {
|
||||
publicData[field] = newData[field];
|
||||
continue;
|
||||
}
|
||||
|
||||
// 2. Vérifier si c'est un nouveau champ privé
|
||||
if (privateFields.includes(field)) {
|
||||
privateData[field] = newData[field];
|
||||
continue;
|
||||
}
|
||||
|
||||
// 3. Vérifier si c'était privé dans un état précédent
|
||||
for (let i = lastStateIndex; i >= 0; i--) {
|
||||
if (process.states[i].pcd_commitment[field]) {
|
||||
privateData[field] = newData[field];
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// 4. Sinon, c'est un nouveau champ public
|
||||
if (!privateData[field]) {
|
||||
publicData[field] = newData[field];
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Phase 3 : Synchronisation (PRD Update)
|
||||
|
||||
**PRD** = Private Data Relay
|
||||
|
||||
```typescript
|
||||
createPrdUpdate(
|
||||
processId: string,
|
||||
stateId: string
|
||||
): Promise<ApiReturn>
|
||||
```
|
||||
|
||||
**Objectif** :
|
||||
- Synchroniser les **clés de déchiffrement** des données privées avec tous les membres autorisés
|
||||
- Distribuer les données privées aux membres qui ont les permissions
|
||||
- Mettre à jour les `state.keys[field]` pour chaque membre autorisé
|
||||
|
||||
**Processus** :
|
||||
1. Création d'un message de mise à jour PRD
|
||||
2. Transmission via les relais aux membres autorisés
|
||||
3. Chaque membre reçoit les clés pour les champs auxquels il a accès
|
||||
|
||||
### Phase 4 : Validation (Approbation)
|
||||
|
||||
```typescript
|
||||
approveChange(
|
||||
processId: string,
|
||||
stateId: string
|
||||
): Promise<ApiReturn>
|
||||
```
|
||||
|
||||
**Objectif** :
|
||||
- Valider un état du processus selon le **quorum requis**
|
||||
- S'assurer que suffisamment de membres ont approuvé le changement
|
||||
- Marquer l'état comme validé
|
||||
|
||||
**Quorum** :
|
||||
- Si quorum = 1.0 → Tous les membres du rôle doivent approuver
|
||||
- Si quorum = 0.5 → 50% des membres doivent approuver
|
||||
- Si quorum = 0 → Auto-approbation (pas de validation requise)
|
||||
|
||||
### Phase 5 : Commit sur Blockchain
|
||||
|
||||
Une fois validé, l'état est **committé sur la blockchain** :
|
||||
|
||||
- Création d'une transaction Bitcoin commitant l'état
|
||||
- Le `pcd_commitment` est inclut dans la transaction
|
||||
- L'état devient **immuable** et vérifiable
|
||||
|
||||
### Phase 6 : Accès aux Données
|
||||
|
||||
#### Données Publiques
|
||||
|
||||
Accessibles directement depuis `state.public_data` après décodage :
|
||||
|
||||
```typescript
|
||||
const publicData = service.getPublicData(process);
|
||||
const decodedValue = service.decodeValue(publicData['fieldName']);
|
||||
```
|
||||
|
||||
#### Données Privées
|
||||
|
||||
Nécessitent :
|
||||
1. **Permission** : Vérifier que le membre a accès au champ
|
||||
2. **Clé de déchiffrement** : Récupérer `state.keys[field]`
|
||||
3. **Commitment** : Vérifier `state.pcd_commitment[field]`
|
||||
4. **Déchiffrement** : Décrypter avec la clé
|
||||
|
||||
```typescript
|
||||
async decryptAttribute(
|
||||
processId: string,
|
||||
state: ProcessState,
|
||||
attribute: string
|
||||
): Promise<any | null>
|
||||
```
|
||||
|
||||
**Vérification des permissions** :
|
||||
```typescript
|
||||
// Vérifier si le membre a accès au champ
|
||||
for (const role of Object.values(state.roles)) {
|
||||
for (const rule of Object.values(role.validation_rules)) {
|
||||
if (rule.fields.includes(attribute)) {
|
||||
if (role.members.includes(pairingProcessId)) {
|
||||
// Le membre a accès
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
Si la clé est manquante, le système demande automatiquement aux autres membres via `requestDataFromPeers()`.
|
||||
|
||||
## Flux Complet d'un Update
|
||||
|
||||
```
|
||||
1. Mise à jour demandée
|
||||
↓
|
||||
2. Classification automatique des champs (public/privé)
|
||||
↓
|
||||
3. updateProcess() → Création d'un nouvel état
|
||||
↓
|
||||
4. createPrdUpdate() → Synchronisation des clés privées
|
||||
↓
|
||||
5. approveChange() → Validation selon quorum
|
||||
↓
|
||||
6. Commit sur blockchain → État immuable
|
||||
↓
|
||||
7. Accès aux données via getPublicData() / decryptAttribute()
|
||||
```
|
||||
|
||||
## Exemples d'Utilisation
|
||||
|
||||
### Exemple 1 : Pairing (Identité Multi-Appareils)
|
||||
|
||||
```typescript
|
||||
// Création d'un processus de pairing
|
||||
const pairingProcess = await service.createPairingProcess(
|
||||
'', // memberPublicName (vide pour pairing)
|
||||
[creatorAddress] // pairedAddresses (liste des appareils)
|
||||
);
|
||||
|
||||
// Données publiques : Liste des adresses appairées
|
||||
// Données privées : Secrets Silent Payment partagés
|
||||
// Rôles : Tous les appareils ont le même niveau d'accès (quorum 1.0)
|
||||
```
|
||||
|
||||
### Exemple 2 : Contrat de Partage avec Niveaux d'Accès
|
||||
|
||||
```typescript
|
||||
// Création d'un contrat avec différents niveaux
|
||||
const contract = await service.createProcess(
|
||||
{
|
||||
secretKey: '...', // Privé : Clé secrète
|
||||
internalNotes: '...' // Privé : Notes internes
|
||||
},
|
||||
{
|
||||
contractName: 'Mon Contrat', // Public : Nom
|
||||
description: '...' // Public : Description
|
||||
},
|
||||
{
|
||||
admin: {
|
||||
members: [adminPairingId],
|
||||
validation_rules: [
|
||||
{ quorum: 1.0, fields: ['secretKey', 'internalNotes', 'contractName'] }
|
||||
]
|
||||
},
|
||||
member: {
|
||||
members: [memberPairingId1, memberPairingId2],
|
||||
validation_rules: [
|
||||
{ quorum: 0.5, fields: ['contractName', 'description'] } // Lecture seule des publics
|
||||
]
|
||||
}
|
||||
}
|
||||
);
|
||||
```
|
||||
|
||||
### Exemple 3 : Vote Décisionnel
|
||||
|
||||
```typescript
|
||||
const voteProcess = await service.createProcess(
|
||||
{
|
||||
votes: {} // Privé : Votes individuels
|
||||
},
|
||||
{
|
||||
proposal: 'Proposition...', // Public : Proposition
|
||||
result: null // Public : Résultat
|
||||
},
|
||||
{
|
||||
voter: {
|
||||
members: [...voterIds],
|
||||
validation_rules: [
|
||||
{ quorum: 0.5, fields: ['votes'] } // 50% des votants doivent valider
|
||||
]
|
||||
}
|
||||
}
|
||||
);
|
||||
```
|
||||
|
||||
## Points Importants
|
||||
|
||||
### 1. Immutabilité
|
||||
|
||||
Une fois qu'un état est **committé**, il devient immuable. Les nouveaux états ajoutent des modifications mais ne modifient jamais les états précédents.
|
||||
|
||||
### 2. Portage des Données Publiques
|
||||
|
||||
Les données publiques sont **automatiquement portées** dans chaque nouvel état. Pas besoin de les réenvoyer à chaque update.
|
||||
|
||||
### 3. Synchronisation Automatique
|
||||
|
||||
Le système gère automatiquement la **distribution des clés privées** aux membres autorisés via les relais.
|
||||
|
||||
### 4. Quorum Flexible
|
||||
|
||||
Le système supporte différents niveaux de quorum selon les besoins :
|
||||
- **Sécurisé** : Quorum 1.0 (tous doivent approuver)
|
||||
- **Démocratique** : Quorum 0.5 (majorité)
|
||||
- **Auto** : Quorum 0 (auto-approbation)
|
||||
|
||||
### 5. Extensibilité
|
||||
|
||||
Ce système peut être utilisé pour **n'importe quel type de contrat** :
|
||||
- Gestion documentaire
|
||||
- Votes décisionnels
|
||||
- Partage de fichiers
|
||||
- Contrats intelligents décentralisés
|
||||
- etc.
|
||||
|
||||
## Méthodes Utilitaires
|
||||
|
||||
### Récupération des Processus
|
||||
|
||||
```typescript
|
||||
// Récupérer un processus spécifique
|
||||
getProcess(processId: string): Promise<Process | null>
|
||||
|
||||
// Récupérer tous les processus
|
||||
getProcesses(): Promise<Record<string, Process>>
|
||||
|
||||
// Récupérer mes processus (où je suis membre)
|
||||
getMyProcesses(): Promise<string[] | null>
|
||||
```
|
||||
|
||||
### État Commité
|
||||
|
||||
```typescript
|
||||
// Récupérer le dernier état commité
|
||||
getLastCommitedState(process: Process): ProcessState | null
|
||||
|
||||
// Récupérer l'index du dernier état commité
|
||||
getLastCommitedStateIndex(process: Process): number | null
|
||||
```
|
||||
|
||||
**Important** : Les états non commités sont des "pending states" qui attendent validation.
|
||||
|
||||
### Rôles et Membres
|
||||
|
||||
```typescript
|
||||
// Récupérer les rôles d'un processus (depuis le dernier état commité)
|
||||
getRoles(process: Process): Record<string, RoleDefinition> | null
|
||||
|
||||
// Vérifier si je suis membre d'un processus
|
||||
rolesContainsUs(roles: Record<string, RoleDefinition>): boolean
|
||||
|
||||
// Vérifier si un membre spécifique fait partie des rôles
|
||||
rolesContainsMember(roles: Record<string, RoleDefinition>, pairingProcessId: string): boolean
|
||||
|
||||
// Récupérer tous les membres connus
|
||||
getAllMembers(): Record<string, Member>
|
||||
```
|
||||
|
||||
### Données Publiques
|
||||
|
||||
```typescript
|
||||
// Récupérer les données publiques (depuis le dernier état commité)
|
||||
getPublicData(process: Process): Record<string, any> | null
|
||||
|
||||
// Décoder une valeur encodée
|
||||
decodeValue(value: number[]): any | null
|
||||
```
|
||||
|
||||
### Données Privées
|
||||
|
||||
```typescript
|
||||
// Déchiffrer un attribut privé
|
||||
async decryptAttribute(
|
||||
processId: string,
|
||||
state: ProcessState,
|
||||
attribute: string
|
||||
): Promise<any | null>
|
||||
```
|
||||
|
||||
Cette méthode :
|
||||
1. Vérifie les permissions (rôles)
|
||||
2. Récupère la clé de déchiffrement (`state.keys[attribute]`)
|
||||
3. Demande aux autres membres si la clé est manquante
|
||||
4. Déchiffre la donnée
|
||||
|
||||
## Gestion du Cache et Synchronisation
|
||||
|
||||
### Cache des Processus
|
||||
|
||||
Les processus sont mis en cache localement pour améliorer les performances :
|
||||
|
||||
```typescript
|
||||
processesCache: Record<string, Process>
|
||||
```
|
||||
|
||||
### Synchronisation avec les Relais
|
||||
|
||||
Les relais synchronisent :
|
||||
- Les nouveaux processus
|
||||
- Les mises à jour d'états
|
||||
- Les clés privées (PRD updates)
|
||||
- Les validations
|
||||
|
||||
### Connexion entre Membres
|
||||
|
||||
Avant de créer ou mettre à jour un processus, le système établit des **connexions** (secrets partagés) entre tous les membres :
|
||||
|
||||
```typescript
|
||||
checkConnections(process: Process, stateId?: string): Promise<void>
|
||||
```
|
||||
|
||||
Cela crée des secrets Silent Payment entre les membres pour permettre la communication chiffrée.
|
||||
|
||||
## Avantages du Système
|
||||
|
||||
1. **Décentralisé** : Pas de tiers de confiance, tout sur la blockchain
|
||||
2. **Vérifiable** : Chaque état est commité et vérifiable
|
||||
3. **Flexible** : Permissions granulaires par champ et par rôle
|
||||
4. **Sécurisé** : Chiffrement des données privées, distribution via relais
|
||||
5. **Générique** : Réutilisable pour n'importe quel type de contrat
|
||||
6. **Sans tiers** : Les utilisateurs contrôlent leurs propres processus
|
||||
|
||||
## Cas d'Usage Avancés
|
||||
|
||||
### Gestion Documentaire Collaborative
|
||||
|
||||
```typescript
|
||||
const documentProcess = await service.createProcess(
|
||||
{
|
||||
documentContent: encryptedContent, // Privé : Contenu chiffré
|
||||
versionHistory: [] // Privé : Historique des versions
|
||||
},
|
||||
{
|
||||
documentTitle: 'Document Important', // Public : Titre
|
||||
lastModified: timestamp, // Public : Dernière modification
|
||||
author: authorAddress // Public : Auteur
|
||||
},
|
||||
{
|
||||
owner: {
|
||||
members: [ownerPairingId],
|
||||
validation_rules: [
|
||||
{ quorum: 1.0, fields: ['documentContent', 'documentTitle'] }
|
||||
]
|
||||
},
|
||||
editor: {
|
||||
members: [...editorPairingIds],
|
||||
validation_rules: [
|
||||
{ quorum: 0.5, fields: ['documentContent'] } // 50% des éditeurs doivent valider
|
||||
]
|
||||
},
|
||||
viewer: {
|
||||
members: [...viewerPairingIds],
|
||||
validation_rules: [
|
||||
{ quorum: 0, fields: ['documentTitle'] } // Lecture seule, pas de validation
|
||||
]
|
||||
}
|
||||
}
|
||||
);
|
||||
```
|
||||
|
||||
### Système de Votation
|
||||
|
||||
```typescript
|
||||
const votingProcess = await service.createProcess(
|
||||
{
|
||||
votes: {}, // Privé : Votes individuels
|
||||
voterIds: [] // Privé : Liste des votants
|
||||
},
|
||||
{
|
||||
question: 'Question...', // Public : Question
|
||||
options: ['A', 'B', 'C'], // Public : Options
|
||||
deadline: timestamp, // Public : Deadline
|
||||
result: null // Public : Résultat (mis à jour après)
|
||||
},
|
||||
{
|
||||
voter: {
|
||||
members: [...allVoterIds],
|
||||
validation_rules: [
|
||||
{ quorum: 0.5, fields: ['votes'] } // 50% des votants doivent valider
|
||||
]
|
||||
},
|
||||
organizer: {
|
||||
members: [organizerPairingId],
|
||||
validation_rules: [
|
||||
{ quorum: 1.0, fields: ['question', 'options', 'deadline', 'result'] }
|
||||
]
|
||||
}
|
||||
}
|
||||
);
|
||||
```
|
||||
|
||||
### Contrat Intelligent Décentralisé
|
||||
|
||||
Le système peut implémenter n'importe quel type de contrat intelligent décentralisé avec :
|
||||
- Conditions de validation personnalisées (quorum)
|
||||
- Permissions granulaires par champ
|
||||
- Audit trail complet (historique des états)
|
||||
- Vérifiabilité sur la blockchain
|
||||
@ -33,7 +33,7 @@ export class DeviceManagementComponent extends HTMLElement {
|
||||
|
||||
try {
|
||||
// Get current device address and generate 4 words
|
||||
const currentAddress = this.service.getDeviceAddress();
|
||||
const currentAddress = await this.service.getDeviceAddress();
|
||||
if (currentAddress) {
|
||||
this.currentDeviceWords = await addressToWords(currentAddress);
|
||||
}
|
||||
|
||||
@ -1,6 +1,7 @@
|
||||
import { DeviceReaderService } from '../../services/device-reader.service';
|
||||
import { SecureCredentialsService } from '../../services/secure-credentials.service';
|
||||
import { SecurityModeService } from '../../services/security-mode.service';
|
||||
import { SecurityModeService, SecurityMode } from '../../services/security-mode.service';
|
||||
import Services from '../../services/service';
|
||||
import { addSubscription } from '../../utils/subscription.utils';
|
||||
import { displayEmojis, generateCreateBtn, addressToEmoji, prepareAndSendPairingTx } from '../../utils/sp-address.utils';
|
||||
import { getCorrectDOM } from '../../utils/html.utils';
|
||||
@ -74,6 +75,16 @@ export async function initHomePage(): Promise<void> {
|
||||
|
||||
console.log('✅ All prerequisites verified for pairing page');
|
||||
|
||||
// Restaurer le mode de sécurité depuis la clé PBKDF2 détectée
|
||||
// Le SecurityModeService stocke le mode uniquement en mémoire, donc on doit le restaurer
|
||||
if (pbkdf2KeyResult.mode) {
|
||||
console.log(`🔐 Restoring security mode from PBKDF2 key: ${pbkdf2KeyResult.mode}`);
|
||||
const securityModeService = SecurityModeService.getInstance();
|
||||
// Le mode retourné par checkPBKDF2Key() est compatible avec SecurityMode (sauf '2fa' qui n'est pas utilisé)
|
||||
await securityModeService.setSecurityMode(pbkdf2KeyResult.mode as SecurityMode);
|
||||
console.log(`✅ Security mode restored: ${pbkdf2KeyResult.mode}`);
|
||||
}
|
||||
|
||||
// No loading spinner - let the interface load naturally
|
||||
|
||||
// Initialize iframe pairing, content menu, and communication only if in iframe
|
||||
@ -535,301 +546,6 @@ function setupUserInteractionListener(): void {
|
||||
console.log('🔐 User interaction listeners set up');
|
||||
}
|
||||
|
||||
/**
|
||||
* Affiche le sélecteur de mode de sécurisation
|
||||
*/
|
||||
async function showSecurityModeSelector(): Promise<void> {
|
||||
return new Promise((resolve) => {
|
||||
// Créer le conteneur pour le sélecteur
|
||||
const selectorContainer = document.createElement('div');
|
||||
selectorContainer.id = 'security-mode-selector-container';
|
||||
selectorContainer.style.cssText = `
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
background: rgba(0, 0, 0, 0.8);
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
z-index: 10000;
|
||||
backdrop-filter: blur(5px);
|
||||
`;
|
||||
|
||||
// Créer le contenu du sélecteur
|
||||
const selectorContent = document.createElement('div');
|
||||
selectorContent.style.cssText = `
|
||||
background: white;
|
||||
border-radius: 12px;
|
||||
padding: 30px;
|
||||
max-width: 600px;
|
||||
width: 90%;
|
||||
max-height: 80vh;
|
||||
overflow-y: auto;
|
||||
box-shadow: 0 8px 32px rgba(0, 0, 0, 0.3);
|
||||
`;
|
||||
|
||||
selectorContent.innerHTML = `
|
||||
<div style="text-align: center; margin-bottom: 30px;">
|
||||
<h2 style="color: #2c3e50; margin-bottom: 10px;">🔐 Mode de Sécurisation</h2>
|
||||
<p style="color: #7f8c8d;">Choisissez comment vous souhaitez sécuriser vos clés privées :</p>
|
||||
</div>
|
||||
|
||||
<div style="display: grid; gap: 15px; margin-bottom: 30px;">
|
||||
<div class="security-option" data-mode="proton-pass" style="border: 2px solid #e1e8ed; border-radius: 8px; padding: 15px; cursor: pointer; transition: all 0.3s ease;">
|
||||
<div style="display: flex; align-items: center; margin-bottom: 8px;">
|
||||
<span style="font-size: 20px; margin-right: 10px;">🔒</span>
|
||||
<span style="font-weight: 600; color: #2c3e50;">Proton Pass</span>
|
||||
<span style="background: #d4edda; color: #155724; padding: 2px 8px; border-radius: 12px; font-size: 12px; font-weight: 600; margin-left: auto;">Sécurisé</span>
|
||||
</div>
|
||||
<div style="color: #5a6c7d; font-size: 14px;">Utilise Proton Pass pour l'authentification biométrique</div>
|
||||
</div>
|
||||
|
||||
<div class="security-option" data-mode="os" style="border: 2px solid #e1e8ed; border-radius: 8px; padding: 15px; cursor: pointer; transition: all 0.3s ease;">
|
||||
<div style="display: flex; align-items: center; margin-bottom: 8px;">
|
||||
<span style="font-size: 20px; margin-right: 10px;">🖥️</span>
|
||||
<span style="font-weight: 600; color: #2c3e50;">Authentificateur OS</span>
|
||||
<span style="background: #d4edda; color: #155724; padding: 2px 8px; border-radius: 12px; font-size: 12px; font-weight: 600; margin-left: auto;">Sécurisé</span>
|
||||
</div>
|
||||
<div style="color: #5a6c7d; font-size: 14px;">Utilise l'authentificateur intégré de votre système</div>
|
||||
</div>
|
||||
|
||||
<div class="security-option" data-mode="otp" style="border: 2px solid #e1e8ed; border-radius: 8px; padding: 15px; cursor: pointer; transition: all 0.3s ease;">
|
||||
<div style="display: flex; align-items: center; margin-bottom: 8px;">
|
||||
<span style="font-size: 20px; margin-right: 10px;">🔐</span>
|
||||
<span style="font-weight: 600; color: #2c3e50;">OTP (code à usage unique)</span>
|
||||
<span style="background: #d4edda; color: #155724; padding: 2px 8px; border-radius: 12px; font-size: 12px; font-weight: 600; margin-left: auto;">Sécurisé</span>
|
||||
</div>
|
||||
<div style="color: #5a6c7d; font-size: 14px;">Code OTP généré par Proton Pass, Google Authenticator, etc.</div>
|
||||
</div>
|
||||
|
||||
<div class="security-option" data-mode="password" style="border: 2px solid #e1e8ed; border-radius: 8px; padding: 15px; cursor: pointer; transition: all 0.3s ease;">
|
||||
<div style="display: flex; align-items: center; margin-bottom: 8px;">
|
||||
<span style="font-size: 20px; margin-right: 10px;">🔑</span>
|
||||
<span style="font-weight: 600; color: #2c3e50;">Mot de Passe</span>
|
||||
<span style="background: #fff3cd; color: #856404; padding: 2px 8px; border-radius: 12px; font-size: 12px; font-weight: 600; margin-left: auto;">⚠️ Non sauvegardé</span>
|
||||
</div>
|
||||
<div style="color: #5a6c7d; font-size: 14px;">Chiffrement par mot de passe (non sauvegardé, non récupérable)</div>
|
||||
</div>
|
||||
|
||||
<div class="security-option" data-mode="none" style="border: 2px solid #e1e8ed; border-radius: 8px; padding: 15px; cursor: pointer; transition: all 0.3s ease;">
|
||||
<div style="display: flex; align-items: center; margin-bottom: 8px;">
|
||||
<span style="font-size: 20px; margin-right: 10px;">🚨</span>
|
||||
<span style="font-weight: 600; color: #2c3e50;">Aucune Sécurité</span>
|
||||
<span style="background: #f5c6cb; color: #721c24; padding: 2px 8px; border-radius: 12px; font-size: 12px; font-weight: 600; margin-left: auto;">DANGEREUX</span>
|
||||
</div>
|
||||
<div style="color: #5a6c7d; font-size: 14px;">Stockage en clair sans aucune protection</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div style="text-align: center;">
|
||||
<button id="confirm-security-mode" style="background: #27ae60; color: white; border: none; padding: 12px 24px; border-radius: 6px; cursor: pointer; font-size: 16px; margin: 5px; opacity: 0.5;" disabled>
|
||||
Confirmer le Mode de Sécurisation
|
||||
</button>
|
||||
<button id="cancel-security-mode" style="background: #95a5a6; color: white; border: none; padding: 12px 24px; border-radius: 6px; cursor: pointer; font-size: 16px; margin: 5px;">
|
||||
Annuler
|
||||
</button>
|
||||
</div>
|
||||
`;
|
||||
|
||||
selectorContainer.appendChild(selectorContent);
|
||||
document.body.appendChild(selectorContainer);
|
||||
|
||||
let selectedMode: string | null = null;
|
||||
|
||||
// Gestion des événements
|
||||
const options = selectorContent.querySelectorAll('.security-option');
|
||||
const confirmBtn = selectorContent.querySelector('#confirm-security-mode') as HTMLButtonElement;
|
||||
const cancelBtn = selectorContent.querySelector('#cancel-security-mode') as HTMLButtonElement;
|
||||
|
||||
// Sélection d'un mode
|
||||
options.forEach(option => {
|
||||
const htmlOption = option as HTMLElement;
|
||||
option.addEventListener('click', () => {
|
||||
options.forEach(opt => (opt as HTMLElement).style.borderColor = '#e1e8ed');
|
||||
htmlOption.style.borderColor = '#27ae60';
|
||||
htmlOption.style.background = '#f8fff8';
|
||||
selectedMode = option.getAttribute('data-mode');
|
||||
confirmBtn.disabled = false;
|
||||
confirmBtn.style.opacity = '1';
|
||||
});
|
||||
|
||||
// Effet hover
|
||||
option.addEventListener('mouseenter', () => {
|
||||
if (htmlOption.style.borderColor !== '#27ae60') {
|
||||
htmlOption.style.borderColor = '#3498db';
|
||||
}
|
||||
});
|
||||
option.addEventListener('mouseleave', () => {
|
||||
if (htmlOption.style.borderColor !== '#27ae60') {
|
||||
htmlOption.style.borderColor = '#e1e8ed';
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
// Confirmation
|
||||
confirmBtn.addEventListener('click', async () => {
|
||||
if (selectedMode) {
|
||||
console.log(`🔐 Security mode selected: ${selectedMode}`);
|
||||
|
||||
// Vérifier si le mode nécessite une confirmation
|
||||
const securityModeService = SecurityModeService.getInstance();
|
||||
const modeConfig = securityModeService.getSecurityModeConfig(selectedMode as any);
|
||||
|
||||
if (modeConfig.requiresConfirmation) {
|
||||
// Afficher une alerte de sécurité
|
||||
const confirmed = await showSecurityWarning(modeConfig);
|
||||
if (!confirmed) {
|
||||
return; // L'utilisateur a annulé
|
||||
}
|
||||
}
|
||||
|
||||
// Définir le mode de sécurisation
|
||||
await securityModeService.setSecurityMode(selectedMode as any);
|
||||
|
||||
// Fermer le sélecteur
|
||||
document.body.removeChild(selectorContainer);
|
||||
|
||||
// Réinitialiser les flags pour permettre la relance
|
||||
isPairingInProgress = false;
|
||||
pairingAttempts = 0;
|
||||
|
||||
// Relancer l'authentification avec le mode sélectionné
|
||||
await handleMainPairing();
|
||||
resolve();
|
||||
}
|
||||
});
|
||||
|
||||
// Annulation
|
||||
cancelBtn.addEventListener('click', () => {
|
||||
console.log('❌ Security mode selection cancelled');
|
||||
document.body.removeChild(selectorContainer);
|
||||
resolve();
|
||||
});
|
||||
|
||||
// Fermer avec Escape
|
||||
const handleEscape = (e: KeyboardEvent) => {
|
||||
if (e.key === 'Escape') {
|
||||
document.body.removeChild(selectorContainer);
|
||||
document.removeEventListener('keydown', handleEscape);
|
||||
resolve();
|
||||
}
|
||||
};
|
||||
document.addEventListener('keydown', handleEscape);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Affiche une alerte de sécurité pour les modes non recommandés
|
||||
*/
|
||||
async function showSecurityWarning(modeConfig: any): Promise<boolean> {
|
||||
return new Promise((resolve) => {
|
||||
const warningContainer = document.createElement('div');
|
||||
warningContainer.style.cssText = `
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
background: rgba(0, 0, 0, 0.8);
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
z-index: 10001;
|
||||
backdrop-filter: blur(5px);
|
||||
`;
|
||||
|
||||
const warningContent = document.createElement('div');
|
||||
warningContent.style.cssText = `
|
||||
background: white;
|
||||
border-radius: 12px;
|
||||
padding: 30px;
|
||||
max-width: 500px;
|
||||
width: 90%;
|
||||
box-shadow: 0 8px 32px rgba(0, 0, 0, 0.3);
|
||||
`;
|
||||
|
||||
warningContent.innerHTML = `
|
||||
<div style="text-align: center; margin-bottom: 20px;">
|
||||
<h3 style="color: #e74c3c; margin-bottom: 10px;">⚠️ Attention - Mode de Sécurisation Non Recommandé</h3>
|
||||
</div>
|
||||
|
||||
<div style="margin-bottom: 20px;">
|
||||
<h4>Vous avez choisi : <strong>${modeConfig.name}</strong></h4>
|
||||
<p>${modeConfig.description}</p>
|
||||
</div>
|
||||
|
||||
<div style="background: #f8d7da; padding: 15px; border-radius: 8px; border-left: 4px solid #dc3545; margin-bottom: 20px;">
|
||||
<h5 style="color: #721c24; margin-top: 0;">⚠️ Risques identifiés :</h5>
|
||||
<ul style="color: #721c24; margin-bottom: 0;">
|
||||
${modeConfig.warnings.map((warning: string) => `<li>${warning}</li>`).join('')}
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<div style="background: #fff3cd; padding: 15px; border-radius: 8px; border-left: 4px solid #ffc107; margin-bottom: 20px;">
|
||||
<p style="color: #856404; margin: 0;">
|
||||
<strong>Recommandation :</strong>
|
||||
${modeConfig.securityLevel === 'low'
|
||||
? 'Nous vous recommandons fortement de choisir un mode plus sécurisé comme Proton Pass ou l\'authentificateur OS.'
|
||||
: 'Ce mode présente des risques de sécurité élevés. Assurez-vous de comprendre les implications.'
|
||||
}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div style="background: #f8f9fa; padding: 15px; border-radius: 8px; margin-bottom: 20px;">
|
||||
<label style="display: flex; align-items: center; cursor: pointer; font-weight: 500;">
|
||||
<input type="checkbox" id="understand-risks" style="margin-right: 10px; transform: scale(1.2);">
|
||||
Je comprends les risques et souhaite continuer
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<div style="text-align: center;">
|
||||
<button id="confirm-risky-mode" style="background: #e74c3c; color: white; border: none; padding: 12px 24px; border-radius: 6px; cursor: pointer; font-size: 16px; margin: 5px; opacity: 0.5;" disabled>
|
||||
Continuer Malgré les Risques
|
||||
</button>
|
||||
<button id="cancel-risky-mode" style="background: #95a5a6; color: white; border: none; padding: 12px 24px; border-radius: 6px; cursor: pointer; font-size: 16px; margin: 5px;">
|
||||
Choisir un Autre Mode
|
||||
</button>
|
||||
</div>
|
||||
`;
|
||||
|
||||
warningContainer.appendChild(warningContent);
|
||||
document.body.appendChild(warningContainer);
|
||||
|
||||
const understandCheckbox = warningContent.querySelector('#understand-risks') as HTMLInputElement;
|
||||
const confirmBtn = warningContent.querySelector('#confirm-risky-mode') as HTMLButtonElement;
|
||||
const cancelBtn = warningContent.querySelector('#cancel-risky-mode') as HTMLButtonElement;
|
||||
|
||||
// Gestion de la checkbox
|
||||
understandCheckbox.addEventListener('change', () => {
|
||||
confirmBtn.disabled = !understandCheckbox.checked;
|
||||
confirmBtn.style.opacity = understandCheckbox.checked ? '1' : '0.5';
|
||||
});
|
||||
|
||||
// Confirmation
|
||||
confirmBtn.addEventListener('click', () => {
|
||||
document.body.removeChild(warningContainer);
|
||||
resolve(true);
|
||||
});
|
||||
|
||||
// Annulation
|
||||
cancelBtn.addEventListener('click', () => {
|
||||
document.body.removeChild(warningContainer);
|
||||
resolve(false);
|
||||
});
|
||||
|
||||
// Fermer avec Escape
|
||||
const handleEscape = (e: KeyboardEvent) => {
|
||||
if (e.key === 'Escape') {
|
||||
document.body.removeChild(warningContainer);
|
||||
document.removeEventListener('keydown', handleEscape);
|
||||
resolve(false);
|
||||
}
|
||||
};
|
||||
document.addEventListener('keydown', handleEscape);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Attend que les credentials soient réellement disponibles
|
||||
@ -915,22 +631,27 @@ async function handleMainPairing(): Promise<void> {
|
||||
try {
|
||||
currentMode = await securityModeService.getCurrentMode();
|
||||
} catch (error) {
|
||||
// Ignorer les erreurs de base de données lors du premier lancement
|
||||
console.log('🔐 No security mode configured yet (first launch)');
|
||||
currentMode = null;
|
||||
// Si aucun mode n'est configuré, rediriger vers security-setup (le mode devrait déjà être configuré avant d'arriver ici)
|
||||
console.log('⚠️ No security mode configured, redirecting to security-setup...');
|
||||
if (mainStatus) {
|
||||
mainStatus.innerHTML = '<span style="color: var(--warning-color)">⚠️ Security mode not configured, redirecting...</span>';
|
||||
}
|
||||
setTimeout(() => {
|
||||
window.location.href = '/src/pages/security-setup/security-setup.html';
|
||||
}, 1000);
|
||||
return;
|
||||
}
|
||||
|
||||
if (!currentMode) {
|
||||
// Aucun mode sélectionné, afficher le sélecteur
|
||||
console.log('🔐 No security mode selected, showing selector...');
|
||||
// Aucun mode sélectionné, rediriger vers security-setup
|
||||
console.log('⚠️ No security mode selected, redirecting to security-setup...');
|
||||
if (mainStatus) {
|
||||
mainStatus.innerHTML = '<div class="spinner"></div><span>🔐 Please select your security mode...</span>';
|
||||
mainStatus.innerHTML = '<span style="color: var(--warning-color)">⚠️ Redirecting to security setup...</span>';
|
||||
}
|
||||
|
||||
// Réinitialiser le flag avant d'afficher le sélecteur
|
||||
isPairingInProgress = false;
|
||||
await showSecurityModeSelector();
|
||||
return; // La fonction sera rappelée après sélection du mode
|
||||
setTimeout(() => {
|
||||
window.location.href = '/src/pages/security-setup/security-setup.html';
|
||||
}, 1000);
|
||||
return;
|
||||
}
|
||||
|
||||
// Mode sélectionné, continuer avec l'authentification
|
||||
@ -985,7 +706,7 @@ async function handleMainPairing(): Promise<void> {
|
||||
mainStatus.innerHTML = '<div class="spinner"></div><span>🔐 Creating secure credentials with your device...</span>';
|
||||
}
|
||||
|
||||
const credentialData = await secureCredentialsService.generateSecureCredentials('4nk-secure-password');
|
||||
const credentialData = await secureCredentialsService.generateSecureCredentials('');
|
||||
|
||||
if (!credentialData?.spendKey || !credentialData.scanKey) {
|
||||
throw new Error('Failed to generate valid credentials - missing spendKey or scanKey');
|
||||
@ -1082,8 +803,33 @@ async function handleMainPairing(): Promise<void> {
|
||||
// Attendre que les credentials soient réellement disponibles avant de continuer
|
||||
await waitForCredentialsAvailability();
|
||||
|
||||
// Exécuter le processus de pairing complet
|
||||
await prepareAndSendPairingTx();
|
||||
|
||||
// Après le succès du pairing, mettre à jour l'UI
|
||||
console.log('✅ Pairing process completed successfully');
|
||||
if (mainStatus) {
|
||||
mainStatus.innerHTML = '<span style="color: var(--success-color)">✅ Pairing complété avec succès ! Les 4 mots sont affichés ci-dessus. Récupération des processus en cours...</span>';
|
||||
}
|
||||
|
||||
// Vérifier que l'appareil est appairé
|
||||
const service = await Services.getInstance();
|
||||
const isPaired = service.isPaired();
|
||||
if (isPaired) {
|
||||
console.log('✅ Device is now paired');
|
||||
|
||||
// La récupération et déchiffrement des processus est déjà gérée dans prepareAndSendPairingTx()
|
||||
// Ici on peut juste confirmer que tout est OK
|
||||
if (mainStatus) {
|
||||
mainStatus.innerHTML = '<span style="color: var(--success-color)">✅ Pairing complété ! Les 4 mots sont affichés ci-dessus. Les processus ont été récupérés et déchiffrés lorsqu\'ils sont accessibles.</span>';
|
||||
}
|
||||
} else {
|
||||
console.warn('⚠️ Device not detected as paired after pairing process');
|
||||
if (mainStatus) {
|
||||
mainStatus.innerHTML = '<span style="color: var(--warning-color)">⚠️ Pairing effectué mais appareil non détecté comme appairé. Attente de synchronisation...</span>';
|
||||
}
|
||||
}
|
||||
|
||||
} catch (error) {
|
||||
// If WebAuthn fails due to no user gesture, wait for real interaction
|
||||
if (error instanceof Error && error.message && error.message.includes('WebAuthn authentication was cancelled or timed out')) {
|
||||
|
||||
@ -91,7 +91,7 @@ document.addEventListener('DOMContentLoaded', async () => {
|
||||
|
||||
// Charger le contenu de pairing depuis home.html
|
||||
updateStatus('🔄 Initialisation du pairing...', 'loading');
|
||||
|
||||
|
||||
if (pairingContent) {
|
||||
pairingContent.innerHTML = `
|
||||
<style>
|
||||
@ -99,13 +99,13 @@ document.addEventListener('DOMContentLoaded', async () => {
|
||||
</style>
|
||||
${loginHtml}
|
||||
`;
|
||||
|
||||
|
||||
// Créer un conteneur simulant login-4nk-component pour getCorrectDOM
|
||||
const mockContainer = document.createElement('div');
|
||||
mockContainer.id = 'login-4nk-component';
|
||||
mockContainer.className = 'login-4nk-component';
|
||||
pairingContent.appendChild(mockContainer);
|
||||
|
||||
|
||||
// Déplacer le contenu de pairing-container dans le mockContainer
|
||||
const pairingContainer = pairingContent.querySelector('.pairing-container');
|
||||
if (pairingContainer && mockContainer) {
|
||||
|
||||
@ -70,9 +70,40 @@ document.addEventListener('DOMContentLoaded', async () => {
|
||||
const currentMode = pbkdf2KeyResult.mode;
|
||||
console.log('✅ Prerequisites verified: PBKDF2 key found in pbkdf2keys store for mode:', currentMode);
|
||||
|
||||
// Étape 1.5: Vérifier si un wallet existe déjà
|
||||
updateStatus('🔍 Vérification du wallet existant...', 'loading');
|
||||
updateProgress(25);
|
||||
|
||||
const { DeviceReaderService } = await import('../../services/device-reader.service');
|
||||
const deviceReader = DeviceReaderService.getInstance();
|
||||
const existingDevice = await deviceReader.getDeviceFromDatabase();
|
||||
|
||||
if (existingDevice && existingDevice.sp_wallet) {
|
||||
console.log('✅ Wallet already exists, skipping creation');
|
||||
updateStatus('✅ Wallet existant trouvé', 'success');
|
||||
updateProgress(100);
|
||||
|
||||
// Activer le bouton et rediriger
|
||||
continueBtn.disabled = false;
|
||||
continueBtn.textContent = 'Continuer';
|
||||
continueBtn.onclick = () => {
|
||||
window.location.href = '/src/pages/birthday-setup/birthday-setup.html';
|
||||
};
|
||||
|
||||
// Auto-redirection après 2 secondes
|
||||
setTimeout(() => {
|
||||
window.location.href = '/src/pages/birthday-setup/birthday-setup.html';
|
||||
}, 2000);
|
||||
return;
|
||||
}
|
||||
|
||||
console.log('🔧 No existing wallet found, creating new one...');
|
||||
updateStatus('🔧 Création du nouveau wallet...', 'loading');
|
||||
updateProgress(30);
|
||||
|
||||
// Étape 2: Initialisation des services (uniquement si les prérequis sont OK)
|
||||
updateStatus('🔄 Initialisation des services...', 'loading');
|
||||
updateProgress(20);
|
||||
updateProgress(35);
|
||||
|
||||
// Vérifier la mémoire avant d'initialiser WebAssembly
|
||||
if ((performance as any).memory) {
|
||||
|
||||
@ -338,7 +338,7 @@ export async function registerAllListeners() {
|
||||
console.log('✅ Token validated successfully');
|
||||
console.log('🚀 Starting pairing process');
|
||||
|
||||
const myAddress = services.getDeviceAddress();
|
||||
const myAddress = await services.getDeviceAddress();
|
||||
console.log('📍 Device address:', myAddress);
|
||||
|
||||
console.log('🧱 Creating pairing process...');
|
||||
@ -1215,7 +1215,7 @@ async function handleSecurityKeyManagement(): Promise<boolean> {
|
||||
} else {
|
||||
console.log('🔐 Security credentials found, verifying access...');
|
||||
// Vérifier l'accès aux credentials
|
||||
const credentials = await secureCredentialsService.retrieveCredentials('4nk-secure-password');
|
||||
const credentials = await secureCredentialsService.retrieveCredentials('');
|
||||
if (!credentials) {
|
||||
console.log('❌ Failed to access security credentials');
|
||||
window.location.href = '/src/pages/wallet-setup/wallet-setup.html';
|
||||
|
||||
@ -7,7 +7,7 @@
|
||||
|
||||
export const DATABASE_CONFIG = {
|
||||
name: '4nk',
|
||||
version: 3,
|
||||
version: 4,
|
||||
stores: {
|
||||
wallet: {
|
||||
name: 'wallet',
|
||||
@ -24,6 +24,11 @@ export const DATABASE_CONFIG = {
|
||||
keyPath: null,
|
||||
indices: []
|
||||
},
|
||||
env: {
|
||||
name: 'env',
|
||||
keyPath: 'key',
|
||||
indices: []
|
||||
},
|
||||
labels: {
|
||||
name: 'labels',
|
||||
keyPath: 'emoji',
|
||||
|
||||
223
src/services/env.service.ts
Normal file
223
src/services/env.service.ts
Normal file
@ -0,0 +1,223 @@
|
||||
/**
|
||||
* EnvService - Gestion des variables d'environnement internes
|
||||
* Stockage des constantes et variables internes de l'application
|
||||
*/
|
||||
import { secureLogger } from './secure-logger';
|
||||
import { DATABASE_CONFIG } from './database-config';
|
||||
|
||||
export interface EnvVariable {
|
||||
key: string;
|
||||
value: string;
|
||||
description?: string;
|
||||
createdAt: number;
|
||||
updatedAt: number;
|
||||
}
|
||||
|
||||
export class EnvService {
|
||||
private static instance: EnvService;
|
||||
private dbName = DATABASE_CONFIG.name;
|
||||
private storeName = DATABASE_CONFIG.stores.env.name;
|
||||
private dbVersion = DATABASE_CONFIG.version;
|
||||
|
||||
private constructor() {}
|
||||
|
||||
public static getInstance(): EnvService {
|
||||
if (!EnvService.instance) {
|
||||
EnvService.instance = new EnvService();
|
||||
}
|
||||
return EnvService.instance;
|
||||
}
|
||||
|
||||
/**
|
||||
* Ouvre la base de données IndexedDB
|
||||
*/
|
||||
private async openDatabase(): Promise<IDBDatabase> {
|
||||
const { openDatabase } = await import('./database-config');
|
||||
return openDatabase();
|
||||
}
|
||||
|
||||
/**
|
||||
* Stocke une variable d'environnement
|
||||
*/
|
||||
async setVariable(key: string, value: string, description?: string): Promise<void> {
|
||||
try {
|
||||
const db = await this.openDatabase();
|
||||
const transaction = db.transaction([this.storeName], 'readwrite');
|
||||
const store = transaction.objectStore(this.storeName);
|
||||
|
||||
const envVar: EnvVariable = {
|
||||
key,
|
||||
value,
|
||||
description,
|
||||
createdAt: Date.now(),
|
||||
updatedAt: Date.now()
|
||||
};
|
||||
|
||||
await new Promise<void>((resolve, reject) => {
|
||||
const request = store.put(envVar);
|
||||
request.onsuccess = () => resolve();
|
||||
request.onerror = () => reject(request.error);
|
||||
});
|
||||
|
||||
secureLogger.info('Environment variable stored successfully', {
|
||||
component: 'EnvService',
|
||||
operation: 'setVariable',
|
||||
key
|
||||
});
|
||||
} catch (error) {
|
||||
secureLogger.error('Failed to store environment variable', error as Error, {
|
||||
component: 'EnvService',
|
||||
operation: 'setVariable',
|
||||
key
|
||||
});
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Récupère une variable d'environnement
|
||||
*/
|
||||
async getVariable(key: string): Promise<string | null> {
|
||||
try {
|
||||
const db = await this.openDatabase();
|
||||
const transaction = db.transaction([this.storeName], 'readonly');
|
||||
const store = transaction.objectStore(this.storeName);
|
||||
|
||||
return new Promise<string | null>((resolve, reject) => {
|
||||
const request = store.get(key);
|
||||
request.onsuccess = () => {
|
||||
const result = request.result as EnvVariable | undefined;
|
||||
if (result) {
|
||||
secureLogger.info('Environment variable retrieved successfully', {
|
||||
component: 'EnvService',
|
||||
operation: 'getVariable',
|
||||
key
|
||||
});
|
||||
resolve(result.value);
|
||||
} else {
|
||||
secureLogger.info('Environment variable not found', {
|
||||
component: 'EnvService',
|
||||
operation: 'getVariable',
|
||||
key
|
||||
});
|
||||
resolve(null);
|
||||
}
|
||||
};
|
||||
request.onerror = () => reject(request.error);
|
||||
});
|
||||
} catch (error) {
|
||||
secureLogger.error('Failed to get environment variable', error as Error, {
|
||||
component: 'EnvService',
|
||||
operation: 'getVariable',
|
||||
key
|
||||
});
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Vérifie si une variable d'environnement existe
|
||||
*/
|
||||
async hasVariable(key: string): Promise<boolean> {
|
||||
try {
|
||||
const value = await this.getVariable(key);
|
||||
return value !== null;
|
||||
} catch (error) {
|
||||
secureLogger.error('Failed to check environment variable existence', error as Error, {
|
||||
component: 'EnvService',
|
||||
operation: 'hasVariable',
|
||||
key
|
||||
});
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Supprime une variable d'environnement
|
||||
*/
|
||||
async deleteVariable(key: string): Promise<void> {
|
||||
try {
|
||||
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.delete(key);
|
||||
request.onsuccess = () => resolve();
|
||||
request.onerror = () => reject(request.error);
|
||||
});
|
||||
|
||||
secureLogger.info('Environment variable deleted successfully', {
|
||||
component: 'EnvService',
|
||||
operation: 'deleteVariable',
|
||||
key
|
||||
});
|
||||
} catch (error) {
|
||||
secureLogger.error('Failed to delete environment variable', error as Error, {
|
||||
component: 'EnvService',
|
||||
operation: 'deleteVariable',
|
||||
key
|
||||
});
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Récupère toutes les variables d'environnement
|
||||
*/
|
||||
async getAllVariables(): Promise<EnvVariable[]> {
|
||||
try {
|
||||
const db = await this.openDatabase();
|
||||
const transaction = db.transaction([this.storeName], 'readonly');
|
||||
const store = transaction.objectStore(this.storeName);
|
||||
|
||||
return new Promise<EnvVariable[]>((resolve, reject) => {
|
||||
const request = store.getAll();
|
||||
request.onsuccess = () => {
|
||||
const result = request.result as EnvVariable[];
|
||||
secureLogger.info('All environment variables retrieved successfully', {
|
||||
component: 'EnvService',
|
||||
operation: 'getAllVariables',
|
||||
count: result.length
|
||||
});
|
||||
resolve(result);
|
||||
};
|
||||
request.onerror = () => reject(request.error);
|
||||
});
|
||||
} catch (error) {
|
||||
secureLogger.error('Failed to get all environment variables', error as Error, {
|
||||
component: 'EnvService',
|
||||
operation: 'getAllVariables'
|
||||
});
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Initialise les variables d'environnement par défaut
|
||||
*/
|
||||
async initializeDefaults(): Promise<void> {
|
||||
try {
|
||||
// Vérifier si les variables par défaut existent déjà
|
||||
const hasCredentialsPassword = await this.hasVariable('CREDENTIALS_PASSWORD');
|
||||
|
||||
if (!hasCredentialsPassword) {
|
||||
await this.setVariable(
|
||||
'CREDENTIALS_PASSWORD',
|
||||
'4nk-secure-password',
|
||||
'Mot de passe interne pour la génération des credentials de pairing'
|
||||
);
|
||||
secureLogger.info('Default environment variables initialized', {
|
||||
component: 'EnvService',
|
||||
operation: 'initializeDefaults'
|
||||
});
|
||||
}
|
||||
} catch (error) {
|
||||
secureLogger.error('Failed to initialize default environment variables', error as Error, {
|
||||
component: 'EnvService',
|
||||
operation: 'initializeDefaults'
|
||||
});
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -73,7 +73,7 @@ export default class IframePairingService {
|
||||
const _device = service.dumpDeviceFromMemory();
|
||||
// Use _device variable
|
||||
console.log('Device from memory:', _device);
|
||||
const creatorAddress = service.getDeviceAddress();
|
||||
const creatorAddress = await service.getDeviceAddress();
|
||||
|
||||
if (creatorAddress) {
|
||||
// Generate and send the 4 words
|
||||
|
||||
@ -39,7 +39,7 @@ export class SecureCredentialsService {
|
||||
* Génère des credentials selon le mode de sécurisation sélectionné
|
||||
*/
|
||||
async generateSecureCredentials(
|
||||
password: string,
|
||||
_password: string,
|
||||
_options: CredentialOptions = {}
|
||||
): Promise<CredentialData> {
|
||||
// Protection contre les appels multiples
|
||||
@ -77,17 +77,30 @@ export class SecureCredentialsService {
|
||||
useEncryption: modeConfig.implementation.useEncryption
|
||||
});
|
||||
|
||||
// Récupérer le mot de passe des credentials depuis le service env
|
||||
const { EnvService } = await import('./env.service');
|
||||
const envService = EnvService.getInstance();
|
||||
|
||||
// Initialiser les variables par défaut si nécessaire
|
||||
await envService.initializeDefaults();
|
||||
|
||||
// Récupérer le mot de passe des credentials
|
||||
const credentialsPassword = await envService.getVariable('CREDENTIALS_PASSWORD');
|
||||
if (!credentialsPassword) {
|
||||
throw new Error('Credentials password not found in environment variables');
|
||||
}
|
||||
|
||||
// Adapter le comportement selon le mode
|
||||
if (modeConfig.implementation.useWebAuthn && modeConfig.implementation.useEncryption) {
|
||||
return this.generateWebAuthnCredentials(password, _options);
|
||||
return this.generateWebAuthnCredentials(credentialsPassword, _options);
|
||||
} else if (modeConfig.implementation.useEncryption) {
|
||||
if (currentMode === 'password') {
|
||||
return this.generatePasswordCredentials(password, _options);
|
||||
return this.generatePasswordCredentials(credentialsPassword, _options);
|
||||
} else {
|
||||
return this.generateEncryptedCredentials(password, _options);
|
||||
return this.generateEncryptedCredentials(credentialsPassword, _options);
|
||||
}
|
||||
} else {
|
||||
return this.generatePlainCredentials(password, _options);
|
||||
return this.generatePlainCredentials(credentialsPassword, _options);
|
||||
}
|
||||
} finally {
|
||||
this.isGeneratingCredentials = false;
|
||||
@ -461,15 +474,19 @@ export class SecureCredentialsService {
|
||||
// Générer des clés aléatoires
|
||||
const keys = encryptionService.generateRandomKeys();
|
||||
|
||||
// Chiffrer avec le mot de passe
|
||||
const encrypted = await encryptionService.encrypt(
|
||||
JSON.stringify(keys),
|
||||
// Chiffrer les clés individuellement avec le mot de passe
|
||||
const encryptedSpendKey = await encryptionService.encrypt(
|
||||
keys.spendKey,
|
||||
password
|
||||
);
|
||||
const encryptedScanKey = await encryptionService.encrypt(
|
||||
keys.scanKey,
|
||||
password
|
||||
);
|
||||
|
||||
return {
|
||||
spendKey: encrypted,
|
||||
scanKey: '', // Scan key est inclus dans les données chiffrées
|
||||
spendKey: encryptedSpendKey, // ✅ Clé chiffrée
|
||||
scanKey: encryptedScanKey, // ✅ Clé chiffrée
|
||||
salt: new Uint8Array(16), // Placeholder for compatibility
|
||||
iterations: 100000,
|
||||
timestamp: Date.now()
|
||||
@ -519,10 +536,11 @@ export class SecureCredentialsService {
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Récupère des credentials existants
|
||||
*/
|
||||
async retrieveCredentials(password: string): Promise<CredentialData | null> {
|
||||
async retrieveCredentials(_password: string): Promise<CredentialData | null> {
|
||||
// Protection contre les appels multiples
|
||||
if (this.isRetrievingCredentials) {
|
||||
secureLogger.warn('Credentials retrieval already in progress, skipping...', {
|
||||
@ -549,14 +567,27 @@ export class SecureCredentialsService {
|
||||
return null;
|
||||
}
|
||||
|
||||
// Récupérer le mot de passe des credentials depuis le service env
|
||||
const { EnvService } = await import('./env.service');
|
||||
const envService = EnvService.getInstance();
|
||||
|
||||
// Initialiser les variables par défaut si nécessaire
|
||||
await envService.initializeDefaults();
|
||||
|
||||
// Récupérer le mot de passe des credentials
|
||||
const credentialsPassword = await envService.getVariable('CREDENTIALS_PASSWORD');
|
||||
if (!credentialsPassword) {
|
||||
throw new Error('Credentials password not found in environment variables');
|
||||
}
|
||||
|
||||
// Déchiffrer selon le mode
|
||||
const currentMode = await this.securityModeService.getCurrentMode();
|
||||
const modeConfig = this.securityModeService.getSecurityModeConfig(currentMode!);
|
||||
|
||||
if (modeConfig.implementation.useWebAuthn && credentials.webAuthnCredentialId) {
|
||||
return this.decryptWithWebAuthn(credentials, password);
|
||||
return this.decryptWithWebAuthn(credentials, credentialsPassword);
|
||||
} else if (modeConfig.implementation.useEncryption) {
|
||||
return this.decryptWithPassword(credentials, password);
|
||||
return this.decryptWithPassword(credentials, credentialsPassword);
|
||||
} else {
|
||||
return credentials;
|
||||
}
|
||||
@ -612,7 +643,7 @@ export class SecureCredentialsService {
|
||||
const { EncryptionService } = await import('./encryption.service');
|
||||
const encryptionService = EncryptionService.getInstance();
|
||||
|
||||
// Déchiffrer les clés
|
||||
// Déchiffrer les clés avec la méthode standard decrypt()
|
||||
const spendKey = await encryptionService.decrypt(
|
||||
credentials.spendKey,
|
||||
password
|
||||
|
||||
@ -775,7 +775,7 @@ export default class Services {
|
||||
// Ensure the amount is available before proceeding
|
||||
await this.getTokensFromFaucet();
|
||||
const unconnectedAddresses = new Set<string>();
|
||||
const myAddress = this.getDeviceAddress();
|
||||
const myAddress = await this.getDeviceAddress();
|
||||
for (const member of Array.from(members)) {
|
||||
const sp_addresses = member.sp_addresses;
|
||||
if (!sp_addresses || sp_addresses.length === 0) {continue;}
|
||||
@ -807,7 +807,7 @@ export default class Services {
|
||||
}
|
||||
|
||||
private async ensureSufficientAmount(): Promise<void> {
|
||||
const availableAmt = this.getAmount();
|
||||
const availableAmt = await this.getAmount();
|
||||
const target: bigint = DEFAULTAMOUNT * 10n;
|
||||
|
||||
console.log(`💰 Current amount: ${availableAmt}, target: ${target}`);
|
||||
@ -868,7 +868,7 @@ export default class Services {
|
||||
let attempts = 20; // Increased attempts for blockchain confirmation
|
||||
|
||||
while (attempts > 0) {
|
||||
const amount = this.getAmount();
|
||||
const amount = await this.getAmount();
|
||||
console.log(`🪙 Attempt ${21 - attempts}: current amount ${amount}, target ${target}`);
|
||||
|
||||
if (amount >= target) {
|
||||
@ -888,7 +888,7 @@ export default class Services {
|
||||
console.log('✅ Block scan completed after transaction');
|
||||
|
||||
// Check amount again after scanning
|
||||
const newAmount = this.getAmount();
|
||||
const newAmount = await this.getAmount();
|
||||
console.log(`💰 Amount after transaction scan: ${newAmount}`);
|
||||
|
||||
if (newAmount > 0n) {
|
||||
@ -984,7 +984,7 @@ export default class Services {
|
||||
roles: Record<string, RoleDefinition>
|
||||
): Promise<ApiReturn> {
|
||||
// Vérifier que les clés sont disponibles avant toute opération
|
||||
this.ensureWalletKeysAvailable();
|
||||
await this.ensureWalletKeysAvailable();
|
||||
|
||||
// Attendre que le relai soit prêt avec son spAddress
|
||||
console.log('⏳ Waiting for relays to be ready...');
|
||||
@ -1338,7 +1338,7 @@ export default class Services {
|
||||
}
|
||||
|
||||
// Check amount after scanning
|
||||
const updatedAmount = this.getAmount();
|
||||
const updatedAmount = await this.getAmount();
|
||||
console.log(`💰 Amount after block scan: ${updatedAmount}`);
|
||||
|
||||
// Update user with scan results
|
||||
@ -1718,7 +1718,7 @@ export default class Services {
|
||||
return;
|
||||
}
|
||||
// We can check if our address is included and simply unpair if it's not
|
||||
if (!spAddressList.includes(this.getDeviceAddress())) {
|
||||
if (!spAddressList.includes(await this.getDeviceAddress())) {
|
||||
await this.unpairDevice();
|
||||
return;
|
||||
}
|
||||
@ -1741,31 +1741,61 @@ export default class Services {
|
||||
/**
|
||||
* Vérifie que les clés du wallet sont disponibles avant toute opération
|
||||
*/
|
||||
private ensureWalletKeysAvailable(): void {
|
||||
private async ensureWalletKeysAvailable(): Promise<void> {
|
||||
try {
|
||||
const device = this.dumpDeviceFromMemory();
|
||||
if (!device?.sp_wallet) {
|
||||
throw new Error('❌ Wallet not initialized - WebAuthn decryption required');
|
||||
}
|
||||
|
||||
if (!device.sp_wallet.spend_key || !device.sp_wallet.scan_key) {
|
||||
throw new Error('❌ Wallet keys not available - WebAuthn decryption required');
|
||||
// Vérifier si les clés sont déjà disponibles dans le device
|
||||
if (device.sp_wallet.spend_key && device.sp_wallet.scan_key) {
|
||||
console.log('✅ Wallet keys available for operation');
|
||||
return;
|
||||
}
|
||||
|
||||
console.log('✅ Wallet keys available for operation');
|
||||
// Si les clés ne sont pas disponibles, essayer de les restaurer depuis les credentials
|
||||
console.log('🔐 Wallet keys not in memory, attempting to restore from credentials...');
|
||||
|
||||
try {
|
||||
const { SecureCredentialsService } = await import('./secure-credentials.service');
|
||||
const secureCredentialsService = SecureCredentialsService.getInstance();
|
||||
|
||||
// Récupérer les credentials
|
||||
const credentials = await secureCredentialsService.retrieveCredentials('');
|
||||
if (!credentials) {
|
||||
throw new Error('No credentials found');
|
||||
}
|
||||
|
||||
// Restaurer les clés dans le device en mémoire
|
||||
if (device.sp_wallet) {
|
||||
device.sp_wallet.spend_key = credentials.spendKey;
|
||||
device.sp_wallet.scan_key = credentials.scanKey;
|
||||
|
||||
// Restaurer le device avec les clés
|
||||
this.restoreDevice(device);
|
||||
|
||||
console.log('✅ Wallet keys restored from credentials');
|
||||
} else {
|
||||
throw new Error('No wallet in device to restore keys to');
|
||||
}
|
||||
} catch (credentialError) {
|
||||
console.error('❌ Failed to restore wallet keys from credentials:', credentialError);
|
||||
throw new Error('❌ Wallet keys not available - WebAuthn decryption required');
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('❌ Wallet keys not available:', error);
|
||||
throw new Error('❌ Wallet keys not available - WebAuthn decryption required');
|
||||
}
|
||||
}
|
||||
|
||||
public getAmount(): bigint {
|
||||
public async getAmount(): Promise<bigint> {
|
||||
if (!this.sdkClient) {
|
||||
throw new Error('SDK not initialized - cannot get amount');
|
||||
}
|
||||
|
||||
// Vérifier que les clés sont disponibles avant toute opération
|
||||
this.ensureWalletKeysAvailable();
|
||||
await this.ensureWalletKeysAvailable();
|
||||
|
||||
try {
|
||||
const amount = this.sdkClient.get_available_amount();
|
||||
@ -1794,14 +1824,14 @@ export default class Services {
|
||||
}
|
||||
}
|
||||
|
||||
getDeviceAddress(): string {
|
||||
async getDeviceAddress(): Promise<string> {
|
||||
try {
|
||||
if (!this.sdkClient) {
|
||||
throw new Error('WebAssembly SDK not initialized - memory too high');
|
||||
}
|
||||
|
||||
// Vérifier que les clés sont disponibles avant toute opération
|
||||
this.ensureWalletKeysAvailable();
|
||||
await this.ensureWalletKeysAvailable();
|
||||
|
||||
return this.sdkClient.get_address();
|
||||
} catch (e) {
|
||||
|
||||
@ -2711,19 +2711,33 @@ export async function discoverAndJoinPairingProcess(creatorAddress: string): Pro
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Crée et complète le processus de pairing 4NK
|
||||
*
|
||||
* Le pairing sert à créer une identité numérique vérifiable permettant :
|
||||
* - MFA (Multi-Factor Authentication) entre appareils via le quorum du processus
|
||||
* - Gestion autonome de la liste d'appareils autorisés (sans tiers)
|
||||
* - Identité numérique décentralisée vérifiable sur la blockchain
|
||||
* - Partage des secrets Silent Payment pour le chiffrement entre appareils appairés
|
||||
*
|
||||
* Le processus de pairing est commité sur la blockchain et permet à plusieurs appareils
|
||||
* de participer au même quorum pour valider les actions critiques.
|
||||
*
|
||||
* @throws {Error} Si le processus de pairing ne peut pas être créé ou complété
|
||||
*/
|
||||
export async function prepareAndSendPairingTx(): Promise<void> {
|
||||
const service = await Services.getInstance();
|
||||
|
||||
try {
|
||||
console.log(`🔍 DEBUG: Creator preparing pairing process...`);
|
||||
console.log(`🔐 Pairing 4NK: Démarrage du processus de pairing (création d'identité numérique vérifiable)...`);
|
||||
|
||||
// Get the creator's own address
|
||||
const creatorAddress = service.getDeviceAddress();
|
||||
const creatorAddress = await service.getDeviceAddress();
|
||||
if (!creatorAddress) {
|
||||
throw new Error('Creator address not available');
|
||||
}
|
||||
|
||||
console.log(`🔍 DEBUG: Creator address: ${creatorAddress}`);
|
||||
console.log(`🔐 Pairing 4NK: Adresse du créateur (premier appareil): ${creatorAddress}`);
|
||||
|
||||
// Update UI with creator address
|
||||
updateCreatorStatus(`Creator address: ${creatorAddress}`);
|
||||
@ -2731,26 +2745,159 @@ export async function prepareAndSendPairingTx(): Promise<void> {
|
||||
// Secure credentials already initialized in the click handler
|
||||
|
||||
// Create pairing process with creator's address
|
||||
// Ce processus servira d'identité numérique vérifiable et permettra l'ajout d'autres appareils
|
||||
console.log(`🔐 Pairing 4NK: Création du processus de pairing (identité numérique décentralisée)...`);
|
||||
const createPairingProcessReturn = await service.createPairingProcess(
|
||||
creatorAddress, // Use creator's address as memberPublicName
|
||||
[creatorAddress] // Include creator's address in pairedAddresses
|
||||
[creatorAddress] // Include creator's address in pairedAddresses (liste des appareils appairés)
|
||||
);
|
||||
|
||||
if (!createPairingProcessReturn.updated_process) {
|
||||
throw new Error('createPairingProcess returned an empty new process');
|
||||
}
|
||||
|
||||
service.setProcessId(createPairingProcessReturn.updated_process.process_id);
|
||||
service.setStateId(
|
||||
createPairingProcessReturn.updated_process.current_process.states[0].state_id
|
||||
);
|
||||
const pairingId = createPairingProcessReturn.updated_process.process_id;
|
||||
const stateId = createPairingProcessReturn.updated_process.current_process.states[0].state_id;
|
||||
|
||||
console.log(`🔐 Pairing 4NK: Processus de pairing créé - ID: ${pairingId}, State ID: ${stateId}`);
|
||||
|
||||
service.setProcessId(pairingId);
|
||||
service.setStateId(stateId);
|
||||
|
||||
// Register device as paired
|
||||
// Enregistre l'appareil dans le processus de pairing
|
||||
console.log(`🔐 Pairing 4NK: Enregistrement de l'appareil dans le processus de pairing...`);
|
||||
service.pairDevice(pairingId, [creatorAddress]);
|
||||
|
||||
// Handle API return
|
||||
console.log(`🔐 Pairing 4NK: Traitement de la réponse API pour createPairingProcess...`);
|
||||
await service.handleApiReturn(createPairingProcessReturn);
|
||||
|
||||
// Create PRD update (Private Data Relay update)
|
||||
// Met à jour les données privées via les relais pour synchroniser les secrets partagés
|
||||
console.log(`🔐 Pairing 4NK: Création de la mise à jour PRD (Private Data Relay) pour synchroniser les secrets partagés...`);
|
||||
const createPrdUpdateReturn = await service.createPrdUpdate(pairingId, stateId);
|
||||
console.log(`🔐 Pairing 4NK: Résultat de la mise à jour PRD:`, createPrdUpdateReturn);
|
||||
await service.handleApiReturn(createPrdUpdateReturn);
|
||||
console.log(`🔐 Pairing 4NK: Mise à jour PRD complétée avec succès (secrets partagés synchronisés)`);
|
||||
|
||||
// Approve change
|
||||
// Approbation du changement d'état du processus (nécessaire pour le quorum)
|
||||
console.log(`🔐 Pairing 4NK: Approbation du changement d'état du processus...`);
|
||||
const approveChangeReturn = await service.approveChange(pairingId, stateId);
|
||||
console.log(`🔐 Pairing 4NK: Résultat de l'approbation:`, approveChangeReturn);
|
||||
await service.handleApiReturn(approveChangeReturn);
|
||||
console.log(`🔐 Pairing 4NK: Changement approuvé avec succès`);
|
||||
|
||||
// Wait for pairing commitment
|
||||
// Attente du commit du processus sur la blockchain (rend l'identité vérifiable)
|
||||
console.log(`🔐 Pairing 4NK: Attente du commit du processus de pairing sur la blockchain (création d'identité vérifiable)...`);
|
||||
await service.waitForPairingCommitment(pairingId);
|
||||
console.log(`🔐 Pairing 4NK: Commit du processus de pairing reçu (identité numérique vérifiable créée)`);
|
||||
|
||||
// Confirm pairing
|
||||
// Confirmation finale du pairing (appareil maintenant appairé et partie du quorum)
|
||||
console.log(`🔐 Pairing 4NK: Confirmation finale du pairing (appareil maintenant partie du quorum MFA)...`);
|
||||
await service.confirmPairing(pairingId);
|
||||
console.log(`🔐 Pairing 4NK: Pairing confirmé avec succès (MFA activé entre appareils)`);
|
||||
|
||||
console.log(`✅ Pairing 4NK: Processus de pairing complété avec succès (identité numérique vérifiable créée, MFA activé)!`);
|
||||
|
||||
// Générer et afficher les 4 mots pour le partage avec d'autres appareils
|
||||
console.log(`🔐 Pairing 4NK: Génération des 4 mots de pairing...`);
|
||||
try {
|
||||
await generateWordsDisplay(creatorAddress);
|
||||
console.log(`✅ Pairing 4NK: 4 mots générés et affichés`);
|
||||
updateCreatorStatus('✅ Pairing complété ! Partagez les 4 mots avec les autres appareils.');
|
||||
} catch (wordsError) {
|
||||
console.warn(`⚠️ Pairing 4NK: Erreur lors de la génération des mots (non bloquant):`, wordsError);
|
||||
updateCreatorStatus('✅ Pairing complété !');
|
||||
}
|
||||
|
||||
// Vérifier que l'appareil est maintenant appairé
|
||||
const isPaired = service.isPaired();
|
||||
console.log(`🔐 Pairing 4NK: Statut d'appairage après confirmation: ${isPaired ? 'Appairé ✅' : 'Non appairé ⚠️'}`);
|
||||
|
||||
if (!isPaired) {
|
||||
console.warn(`⚠️ Pairing 4NK: L'appareil n'est pas détecté comme appairé après confirmation`);
|
||||
}
|
||||
|
||||
// Étape suivante : Récupération et déchiffrement des processus
|
||||
console.log(`🔍 Pairing 4NK: Récupération des processus...`);
|
||||
try {
|
||||
const myProcesses = await service.getMyProcesses();
|
||||
if (myProcesses && myProcesses.length > 0) {
|
||||
console.log(`✅ Pairing 4NK: ${myProcesses.length} processus trouvés pour cet appareil`);
|
||||
|
||||
// Récupérer tous les processus
|
||||
const allProcesses = await service.getProcesses();
|
||||
console.log(`✅ Pairing 4NK: ${Object.keys(allProcesses).length} processus récupérés au total`);
|
||||
|
||||
// Pour chaque processus, tenter de déchiffrer les attributs possibles
|
||||
for (const processId of myProcesses) {
|
||||
try {
|
||||
const process = await service.getProcess(processId);
|
||||
if (!process) {
|
||||
console.warn(`⚠️ Pairing 4NK: Processus ${processId} introuvable`);
|
||||
continue;
|
||||
}
|
||||
|
||||
const lastCommitedState = service.getLastCommitedState(process);
|
||||
if (!lastCommitedState) {
|
||||
console.log(`ℹ️ Pairing 4NK: Processus ${processId} n'a pas encore d'état commité`);
|
||||
continue;
|
||||
}
|
||||
|
||||
console.log(`🔍 Pairing 4NK: Tentative de déchiffrement des attributs pour le processus ${processId}...`);
|
||||
|
||||
// Vérifier les connexions avec les membres du processus
|
||||
await service.checkConnections(process);
|
||||
|
||||
// Tenter de déchiffrer tous les attributs privés auxquels on a accès
|
||||
let decryptedCount = 0;
|
||||
for (const attribute of Object.keys(lastCommitedState.pcd_commitment)) {
|
||||
// Ignorer les rôles et les données publiques
|
||||
if (attribute === 'roles' || lastCommitedState.public_data[attribute]) {
|
||||
continue;
|
||||
}
|
||||
|
||||
try {
|
||||
const decryptedAttribute = await service.decryptAttribute(processId, lastCommitedState, attribute);
|
||||
if (decryptedAttribute) {
|
||||
decryptedCount++;
|
||||
console.log(`✅ Pairing 4NK: Attribut "${attribute}" déchiffré avec succès pour le processus ${processId}`);
|
||||
} else {
|
||||
console.log(`ℹ️ Pairing 4NK: Attribut "${attribute}" non accessible ou clé manquante pour le processus ${processId}`);
|
||||
}
|
||||
} catch (decryptError) {
|
||||
console.warn(`⚠️ Pairing 4NK: Erreur lors du déchiffrement de l'attribut "${attribute}" pour le processus ${processId}:`, decryptError);
|
||||
}
|
||||
}
|
||||
|
||||
if (decryptedCount > 0) {
|
||||
console.log(`✅ Pairing 4NK: ${decryptedCount} attribut(s) déchiffré(s) pour le processus ${processId}`);
|
||||
} else {
|
||||
console.log(`ℹ️ Pairing 4NK: Aucun attribut déchiffré pour le processus ${processId} (normal si pas d'attributs privés ou pas d'accès)`);
|
||||
}
|
||||
} catch (processError) {
|
||||
console.warn(`⚠️ Pairing 4NK: Erreur lors du traitement du processus ${processId}:`, processError);
|
||||
}
|
||||
}
|
||||
|
||||
console.log(`✅ Pairing 4NK: Récupération et déchiffrement des processus terminés`);
|
||||
} else {
|
||||
console.log(`ℹ️ Pairing 4NK: Aucun processus trouvé pour cet appareil (normal pour un nouveau pairing)`);
|
||||
}
|
||||
} catch (processRetrievalError) {
|
||||
console.warn(`⚠️ Pairing 4NK: Erreur lors de la récupération des processus (non bloquant):`, processRetrievalError);
|
||||
}
|
||||
|
||||
console.log(`✅ DEBUG: Creator pairing process created and 4 words generated`);
|
||||
console.log(`⏳ DEBUG: Creator waiting for joiner to enter 4 words...`);
|
||||
} catch (err) {
|
||||
console.error(err);
|
||||
console.error(`❌ Pairing 4NK: Erreur lors du processus de pairing:`, err);
|
||||
updateCreatorStatus(`❌ Erreur lors du pairing: ${(err as Error).message}`, true);
|
||||
throw err;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user