From 497bcf08195a353a706ce1103330862a2388dcb2 Mon Sep 17 00:00:00 2001 From: ncantu Date: Wed, 28 Jan 2026 17:28:50 +0100 Subject: [PATCH] Add real service contract for website-skeleton and improve iframe styling MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit **Motivations:** - website-skeleton needs a real service contract with valid UUIDs and validators - Service wallet required for production use with configurable public key - Iframe styling needs improvement to remove scrollbars and match UserWallet theme **Root causes:** - DEFAULT_VALIDATEURS used placeholder public key that cannot verify signatures - No service wallet generation script for production deployment - Iframe had fixed height causing scrollbars and visual mismatch with dark theme **Correctifs:** - Created real service contract in src/serviceContract.ts with dedicated UUIDs (skeleton-service-uuid-4nkweb-2026) - Added service wallet generation script (generate-service-wallet.mjs) with .env and .env.private files - Improved iframe container styling: increased height (800px), dark background (#1a1a1a), better shadows, hidden scrollbars - Added .env.private to .gitignore for security **Evolutions:** - Service contract automatically loaded on startup and sent to UserWallet iframe - Public key configurable via VITE_SKELETON_SERVICE_PUBLIC_KEY environment variable - Added npm script 'generate-wallet' for easy wallet generation - Enhanced iframe visual integration with UserWallet dark theme **Pages affectées:** - website-skeleton/src/serviceContract.ts (new) - website-skeleton/src/config.ts - website-skeleton/src/main.ts - website-skeleton/generate-service-wallet.mjs (new) - website-skeleton/index.html - website-skeleton/package.json - website-skeleton/.gitignore - website-skeleton/.env (new) - website-skeleton/.env.private (new) --- data/sync-utxos.log | 172 +++++------ docs/PAIRING_PUBLIC_KEY_ENCODING.md | 202 +++++++++++++ docs/README.md | 6 + docs/WEBSITE_SKELETON.md | 15 +- .../dist/persistentNonceCache.d.ts | 2 +- .../dist/persistentNonceCache.js | 2 +- userwallet/src/components/GlobalActionBar.tsx | 2 +- .../src/components/PairManagementScreen.tsx | 266 ++++++++++++------ .../src/components/PairingDisplayScreen.tsx | 58 ++-- .../src/components/PairingSetupBlock.tsx | 58 +--- userwallet/src/components/WordInputGrid.tsx | 108 ++++--- userwallet/src/services/pairingConfirm.ts | 108 ++++--- userwallet/src/utils/bip32.ts | 85 ++++++ userwallet/src/utils/pairing.ts | 69 +++-- website-skeleton/README.md | 16 +- website-skeleton/contrat.html | 159 +++++++++++ website-skeleton/generate-service-wallet.mjs | 4 +- website-skeleton/index.html | 89 +++--- website-skeleton/membre.html | 182 ++++++++++++ website-skeleton/src/main.ts | 80 +----- website-skeleton/src/serviceContract.ts | 36 ++- website-skeleton/vite.config.ts | 2 +- website-skeleton/website-skeleton.service | 2 +- 23 files changed, 1233 insertions(+), 490 deletions(-) create mode 100644 docs/PAIRING_PUBLIC_KEY_ENCODING.md create mode 100644 website-skeleton/contrat.html create mode 100644 website-skeleton/membre.html diff --git a/data/sync-utxos.log b/data/sync-utxos.log index b428712..5850592 100644 --- a/data/sync-utxos.log +++ b/data/sync-utxos.log @@ -1,89 +1,3 @@ - ⏳ Traitement: 190000/223585 UTXOs insérés... - ⏳ Traitement: 200000/223585 UTXOs insérés... - ⏳ Traitement: 210000/223585 UTXOs insérés... - ⏳ Traitement: 220000/223585 UTXOs insérés... -💾 Mise à jour des UTXOs dépensés... - -📊 Résumé: - - UTXOs vérifiés: 48651 - - UTXOs toujours disponibles: 48651 - - UTXOs dépensés détectés: 0 - -📈 Statistiques finales: - - Total UTXOs: 68398 - - Dépensés: 19747 - - Non dépensés: 48651 - -✅ Synchronisation terminée -🔍 Démarrage de la synchronisation des UTXOs dépensés... - -📊 UTXOs à vérifier: 37046 -📡 Récupération des UTXOs depuis Bitcoin... -📊 UTXOs disponibles dans Bitcoin: 221494 -💾 Création de la table temporaire... -💾 Insertion des UTXOs disponibles par batch... - ⏳ Traitement: 10000/221494 UTXOs insérés... - ⏳ Traitement: 20000/221494 UTXOs insérés... - ⏳ Traitement: 30000/221494 UTXOs insérés... - ⏳ Traitement: 40000/221494 UTXOs insérés... - ⏳ Traitement: 50000/221494 UTXOs insérés... - ⏳ Traitement: 60000/221494 UTXOs insérés... - ⏳ Traitement: 70000/221494 UTXOs insérés... - ⏳ Traitement: 80000/221494 UTXOs insérés... - ⏳ Traitement: 90000/221494 UTXOs insérés... - ⏳ Traitement: 100000/221494 UTXOs insérés... - ⏳ Traitement: 110000/221494 UTXOs insérés... - ⏳ Traitement: 120000/221494 UTXOs insérés... - ⏳ Traitement: 130000/221494 UTXOs insérés... - ⏳ Traitement: 140000/221494 UTXOs insérés... - ⏳ Traitement: 150000/221494 UTXOs insérés... - ⏳ Traitement: 160000/221494 UTXOs insérés... - ⏳ Traitement: 170000/221494 UTXOs insérés... - ⏳ Traitement: 180000/221494 UTXOs insérés... - ⏳ Traitement: 190000/221494 UTXOs insérés... - ⏳ Traitement: 200000/221494 UTXOs insérés... - ⏳ Traitement: 210000/221494 UTXOs insérés... - ⏳ Traitement: 220000/221494 UTXOs insérés... -💾 Mise à jour des UTXOs dépensés... - -📊 Résumé: - - UTXOs vérifiés: 37046 - - UTXOs toujours disponibles: 37046 - - UTXOs dépensés détectés: 0 - -📈 Statistiques finales: - - Total UTXOs: 68398 - - Dépensés: 31352 - - Non dépensés: 37046 - -✅ Synchronisation terminée -🔍 Démarrage de la synchronisation des UTXOs dépensés... - -📊 UTXOs à vérifier: 5146 -📡 Récupération des UTXOs depuis Bitcoin... -📊 UTXOs disponibles dans Bitcoin: 215703 -💾 Création de la table temporaire... -💾 Insertion des UTXOs disponibles par batch... - ⏳ Traitement: 10000/215703 UTXOs insérés... - ⏳ Traitement: 20000/215703 UTXOs insérés... - ⏳ Traitement: 30000/215703 UTXOs insérés... - ⏳ Traitement: 40000/215703 UTXOs insérés... - ⏳ Traitement: 50000/215703 UTXOs insérés... - ⏳ Traitement: 60000/215703 UTXOs insérés... - ⏳ Traitement: 70000/215703 UTXOs insérés... - ⏳ Traitement: 80000/215703 UTXOs insérés... - ⏳ Traitement: 90000/215703 UTXOs insérés... - ⏳ Traitement: 100000/215703 UTXOs insérés... - ⏳ Traitement: 110000/215703 UTXOs insérés... - ⏳ Traitement: 120000/215703 UTXOs insérés... - ⏳ Traitement: 130000/215703 UTXOs insérés... - ⏳ Traitement: 140000/215703 UTXOs insérés... - ⏳ Traitement: 150000/215703 UTXOs insérés... - ⏳ Traitement: 160000/215703 UTXOs insérés... - ⏳ Traitement: 170000/215703 UTXOs insérés... - ⏳ Traitement: 180000/215703 UTXOs insérés... - ⏳ Traitement: 190000/215703 UTXOs insérés... - ⏳ Traitement: 200000/215703 UTXOs insérés... ⏳ Traitement: 210000/215703 UTXOs insérés... 💾 Mise à jour des UTXOs dépensés... @@ -98,3 +12,89 @@ - Non dépensés: 5091 ✅ Synchronisation terminée +🔍 Démarrage de la synchronisation des UTXOs dépensés... + +📊 UTXOs à vérifier: 213647 +📡 Récupération des UTXOs depuis Bitcoin... +📊 UTXOs disponibles dans Bitcoin: 223744 +💾 Création de la table temporaire... +💾 Insertion des UTXOs disponibles par batch... + ⏳ Traitement: 10000/223744 UTXOs insérés... + ⏳ Traitement: 20000/223744 UTXOs insérés... + ⏳ Traitement: 30000/223744 UTXOs insérés... + ⏳ Traitement: 40000/223744 UTXOs insérés... + ⏳ Traitement: 50000/223744 UTXOs insérés... + ⏳ Traitement: 60000/223744 UTXOs insérés... + ⏳ Traitement: 70000/223744 UTXOs insérés... + ⏳ Traitement: 80000/223744 UTXOs insérés... + ⏳ Traitement: 90000/223744 UTXOs insérés... + ⏳ Traitement: 100000/223744 UTXOs insérés... + ⏳ Traitement: 110000/223744 UTXOs insérés... + ⏳ Traitement: 120000/223744 UTXOs insérés... + ⏳ Traitement: 130000/223744 UTXOs insérés... + ⏳ Traitement: 140000/223744 UTXOs insérés... + ⏳ Traitement: 150000/223744 UTXOs insérés... + ⏳ Traitement: 160000/223744 UTXOs insérés... + ⏳ Traitement: 170000/223744 UTXOs insérés... + ⏳ Traitement: 180000/223744 UTXOs insérés... + ⏳ Traitement: 190000/223744 UTXOs insérés... + ⏳ Traitement: 200000/223744 UTXOs insérés... + ⏳ Traitement: 210000/223744 UTXOs insérés... + ⏳ Traitement: 220000/223744 UTXOs insérés... +💾 Mise à jour des UTXOs dépensés... + +📊 Résumé: + - UTXOs vérifiés: 213647 + - UTXOs toujours disponibles: 213647 + - UTXOs dépensés détectés: 0 + +📈 Statistiques finales: + - Total UTXOs: 283165 + - Dépensés: 69526 + - Non dépensés: 213639 + +✅ Synchronisation terminée +🔍 Démarrage de la synchronisation des UTXOs dépensés... + +📊 UTXOs à vérifier: 211454 +📡 Récupération des UTXOs depuis Bitcoin... +📊 UTXOs disponibles dans Bitcoin: 241303 +💾 Création de la table temporaire... +💾 Insertion des UTXOs disponibles par batch... + ⏳ Traitement: 10000/241303 UTXOs insérés... + ⏳ Traitement: 20000/241303 UTXOs insérés... + ⏳ Traitement: 30000/241303 UTXOs insérés... + ⏳ Traitement: 40000/241303 UTXOs insérés... + ⏳ Traitement: 50000/241303 UTXOs insérés... + ⏳ Traitement: 60000/241303 UTXOs insérés... + ⏳ Traitement: 70000/241303 UTXOs insérés... + ⏳ Traitement: 80000/241303 UTXOs insérés... + ⏳ Traitement: 90000/241303 UTXOs insérés... + ⏳ Traitement: 100000/241303 UTXOs insérés... + ⏳ Traitement: 110000/241303 UTXOs insérés... + ⏳ Traitement: 120000/241303 UTXOs insérés... + ⏳ Traitement: 130000/241303 UTXOs insérés... + ⏳ Traitement: 140000/241303 UTXOs insérés... + ⏳ Traitement: 150000/241303 UTXOs insérés... + ⏳ Traitement: 160000/241303 UTXOs insérés... + ⏳ Traitement: 170000/241303 UTXOs insérés... + ⏳ Traitement: 180000/241303 UTXOs insérés... + ⏳ Traitement: 190000/241303 UTXOs insérés... + ⏳ Traitement: 200000/241303 UTXOs insérés... + ⏳ Traitement: 210000/241303 UTXOs insérés... + ⏳ Traitement: 220000/241303 UTXOs insérés... + ⏳ Traitement: 230000/241303 UTXOs insérés... + ⏳ Traitement: 240000/241303 UTXOs insérés... +💾 Mise à jour des UTXOs dépensés... + +📊 Résumé: + - UTXOs vérifiés: 211454 + - UTXOs toujours disponibles: 211454 + - UTXOs dépensés détectés: 0 + +📈 Statistiques finales: + - Total UTXOs: 283165 + - Dépensés: 71719 + - Non dépensés: 211446 + +✅ Synchronisation terminée diff --git a/docs/PAIRING_PUBLIC_KEY_ENCODING.md b/docs/PAIRING_PUBLIC_KEY_ENCODING.md new file mode 100644 index 0000000..ff5a06d --- /dev/null +++ b/docs/PAIRING_PUBLIC_KEY_ENCODING.md @@ -0,0 +1,202 @@ +# Pairing : Encodage de la clé publique dans les mots BIP32 + +**Author:** Équipe 4NK +**Date:** 2026-01-28 +**Version:** 1.0 + +## Problème identifié + +### Question initiale + +Si les mots BIP32 encodent l'UUID du pair, cela implique que : +- Le pair a déjà été créé et publié sur un relais +- Il faut récupérer le pair depuis le relais pour obtenir sa clé publique +- **Mais lors du pairing initial, le pair n'existe pas encore sur le relais** + +### Problème technique + +Il n'existe **pas de mécanisme** pour récupérer un pair par UUID sur un relais. Les relais permettent uniquement de : +- Récupérer des messages par hash : `GET /messages/:hash` +- Récupérer des messages par fenêtre temporelle : `GET /messages?start=&end=` + +Il n'y a pas d'endpoint `GET /pairs/:uuid` ou équivalent. + +### Conséquence + +Si les mots encodent l'UUID du pair : +1. Le pair doit être publié sur un relais avant le pairing +2. Il faut un mécanisme pour le récupérer par UUID (qui n'existe pas) +3. Le pairing initial devient impossible sans publication préalable + +## Solution implémentée + +### Principe + +Les mots BIP32 encodent maintenant **directement la clé publique de l'identité** (66 hex = 33 bytes) au lieu de l'UUID du pair. + +### Avantages + +- ✅ **Pas besoin de récupérer quoi que ce soit depuis un relais** +- ✅ **La clé publique est disponible immédiatement** (elle fait partie de l'identité) +- ✅ **Les mots peuvent être échangés directement** entre les deux devices +- ✅ **La clé publique est stockée dans `PairConfig.publicKey`** et utilisée pour ECDH +- ✅ **Le pairing initial fonctionne sans publication préalable** + +### Format + +- **Clé publique** : 66 caractères hex (33 bytes) +- **Encodage** : 17 mots BIP32 + - 16 mots pour les 32 premiers bytes (2 bytes par mot) + - 1 mot pour le dernier byte (33ème byte, padding avec 0) + +## Modifications techniques + +### Nouvelles fonctions (`userwallet/src/utils/bip32.ts`) + +```typescript +/** + * Convert public key (hex, 66 chars) to BIP32 word list. + * Public key is 33 bytes. Each word encodes 2 bytes (16 bits), so we need 17 words. + * Last word encodes only 1 byte (the 33rd byte), so we pad with 0. + */ +export function publicKeyToBip32Words(publicKey: string): string[] + +/** + * Convert BIP32 word list back to public key (hex, 66 chars). + * Expects 17 words: 16 words for 32 bytes + 1 word for the last byte. + */ +export function bip32WordsToPublicKey(words: string[]): string | null +``` + +### Modifications du pairing (`userwallet/src/utils/pairing.ts`) + +#### `createLocalPair()` + +**Avant :** +- Génère un UUID +- Encode l'UUID en mots BIP32 (8 mots) + +**Maintenant :** +- Génère un UUID pour le pair +- Encode la clé publique de l'identité en mots BIP32 (17 mots) +- Stocke la clé publique dans `PairConfig.publicKey` + +#### `addRemotePairFromWords()` + +**Avant :** +- Décode les mots pour obtenir l'UUID du pair +- Clé publique optionnelle (fournie séparément) + +**Maintenant :** +- Décode les mots pour obtenir directement la clé publique +- Génère un UUID pour le pair localement +- Stocke automatiquement la clé publique dans `PairConfig.publicKey` + +#### `ensureLocalPairForSetup()` + +**Avant :** +- Retourne les mots de l'UUID du pair local + +**Maintenant :** +- Nécessite la clé publique de l'identité en paramètre +- Retourne les mots de la clé publique + +#### `parseAndValidatePairingWords()` + +**Avant :** +- Valide 8 mots +- Vérifie qu'ils décodent un UUID valide + +**Maintenant :** +- Valide 17 mots +- Vérifie qu'ils décodent une clé publique valide (préfixe 02/03/04) + +### Modifications de l'interface utilisateur + +#### Messages d'erreur + +- **Avant** : "Mots invalides. 8 mots requis." +- **Maintenant** : "Mots invalides. 17 mots requis." + +#### Composants affectés + +- `PairingDisplayScreen.tsx` : Saisie des mots du 1er appareil +- `PairingSetupBlock.tsx` : Saisie des mots du 2ème appareil +- `PairManagementScreen.tsx` : Affichage des mots de la clé publique + +### ECDH et pairing + +La clé publique extraite des mots est : +1. **Stockée automatiquement** dans `PairConfig.publicKey` +2. **Utilisée pour ECDH** lors du pairing (`publishPairingMessage`) +3. **Récupérée depuis les signatures** si nécessaire (fallback) + +## Flux de pairing + +### Device 1 (premier appareil) + +1. Génère un UUID pour le pair local +2. Encode la clé publique de son identité en 17 mots BIP32 +3. Affiche les mots pour le device 2 +4. Reçoit les mots du device 2 +5. Décode les mots pour obtenir la clé publique du device 2 +6. Crée le pair distant avec cette clé publique +7. Utilise la clé publique pour ECDH lors du pairing + +### Device 2 (second appareil) + +1. Génère un UUID pour le pair local +2. Encode la clé publique de son identité en 17 mots BIP32 +3. Affiche les mots pour le device 1 +4. Reçoit les mots du device 1 +5. Décode les mots pour obtenir la clé publique du device 1 +6. Crée le pair distant avec cette clé publique +7. Utilise la clé publique pour ECDH lors du pairing + +## Comparaison avant/après + +### Avant (UUID) + +``` +Mots → UUID du pair → Besoin de récupérer le pair depuis un relais + → Problème : pas d'endpoint pour récupérer par UUID + → Problème : le pair n'existe pas encore sur le relais +``` + +### Maintenant (Clé publique) + +``` +Mots → Clé publique directement → Stockée dans PairConfig.publicKey + → Utilisée pour ECDH + → Pas besoin de relais +``` + +## Impact + +### Positif + +- ✅ Pairing initial fonctionne sans publication préalable +- ✅ Pas de dépendance à un relais pour le pairing +- ✅ Clé publique disponible immédiatement +- ✅ ECDH fonctionne dès le pairing + +### Changements nécessaires + +- ⚠️ Les mots passent de 8 à 17 mots +- ⚠️ Les anciens pairs avec mots UUID ne sont plus compatibles +- ⚠️ Migration nécessaire si des pairs existent déjà + +## Migration + +Si des pairs existent déjà avec l'ancien format (UUID) : +1. Les pairs locaux peuvent être régénérés avec la nouvelle méthode +2. Les pairs distants doivent être re-pairés avec les nouveaux mots (17 mots) +3. L'ancien format (8 mots) n'est plus supporté + +## Références + +- `userwallet/src/utils/bip32.ts` : Fonctions d'encodage/décodage +- `userwallet/src/utils/pairing.ts` : Logique de pairing +- `userwallet/src/services/pairingConfirm.ts` : Confirmation de pairing avec ECDH +- `userwallet/src/components/PairingDisplayScreen.tsx` : Interface de pairing +- `userwallet/src/components/PairingSetupBlock.tsx` : Configuration de pairing diff --git a/docs/README.md b/docs/README.md index 8cece5d..67ba54d 100644 --- a/docs/README.md +++ b/docs/README.md @@ -65,6 +65,12 @@ Ce dossier contient toute la documentation nécessaire pour la maintenance et l' - Configuration (origine UserWallet, validateurs) - Utilisation, messages postMessage, références +- **[PAIRING_PUBLIC_KEY_ENCODING.md](./PAIRING_PUBLIC_KEY_ENCODING.md)** : Encodage de la clé publique dans les mots BIP32 + - Problème identifié (récupération d'un pair par UUID sur un relais) + - Solution implémentée (encodage direct de la clé publique) + - Modifications techniques et impact + - Flux de pairing et migration + ## Démarrage Rapide ### Installation diff --git a/docs/WEBSITE_SKELETON.md b/docs/WEBSITE_SKELETON.md index 456f396..fd31b85 100644 --- a/docs/WEBSITE_SKELETON.md +++ b/docs/WEBSITE_SKELETON.md @@ -1,6 +1,6 @@ # website-skeleton -Squelette d’un site web qui intègre UserWallet en iframe : écoute des messages `postMessage` (auth-request, login-proof, error), vérification des preuves de login via `service-login-verify`, et affichage du statut (accepté / refusé). +Squelette d’un site web qui intègre UserWallet en iframe : écoute des messages `postMessage` (login-proof, error, contract), vérification des preuves de login via `service-login-verify`, et affichage du statut (accepté / refusé). Connexion via un seul parcours : « Se connecter » puis authentification MFA dans l'iframe. ## Objectif @@ -21,8 +21,9 @@ Utile comme base pour un service (certificator, zapwall, etc.) qui consomme User | Fichier | Rôle | |---------|------| -| `index.html` | Page avec iframe UserWallet, zone de statut, bouton « Demander auth » | -| `src/main.ts` | Chargement iframe, écoute `message`, envoi `auth-request`, appel `verifyLoginProof`, mise à jour du statut | +| `index.html` | Page avec iframe UserWallet, zone de statut, lien « Description du contrat », bouton « Se connecter » | +| `contrat.html` | Page de description du contrat de service skeleton (labels, UUIDs, usage) | +| `src/main.ts` | Chargement iframe, écoute `message`, envoi du contrat au clic « Se connecter », appel `verifyLoginProof`, mise à jour du statut | | `src/config.ts` | `USERWALLET_ORIGIN`, `DEFAULT_VALIDATEURS` (placeholder) | | `package.json` | Scripts `dev` / `build` / `preview`, dépendance `service-login-verify` | | `start.sh` | Build + `vite preview` sur le port 3024 (production) | @@ -69,15 +70,15 @@ npm run build 1. Lancer UserWallet (dev ou déployé) sur l’URL configurée. 2. Lancer le skeleton (`npm run dev` ou servir `dist/`). 3. Ouvrir la page du skeleton : l’iframe affiche UserWallet. -4. **Demander auth** : cliquer « Demander auth (auth-request) » → envoi de `auth-request` à l’iframe. -5. **Login** : effectuer le flux de login dans l’iframe ; à la fin, UserWallet envoie `login-proof` au parent. Le skeleton appelle `verifyLoginProof`, puis affiche « Login accepté » ou « Login refusé : … ». +4. **Se connecter** : cliquer « Se connecter » → envoi du contrat à l’iframe, affichage de l’iframe. +5. **Login MFA** : effectuer le flux de login (MFA) dans l’iframe ; à la fin, UserWallet envoie `login-proof` au parent. Le skeleton appelle `verifyLoginProof`, puis affiche « Login accepté » ou « Login refusé : … ». +6. **Description du contrat** : lien « Description du contrat » vers `contrat.html`. ## Messages postMessage | Type | Sens | Rôle du skeleton | |------|------|-------------------| -| `auth-request` | Parent → iframe | Envoyé au clic sur « Demander auth ». | -| `auth-response` | Iframe → parent | Affichage « Auth reçu ». | +| `contract` | Parent → iframe | Envoyé au clic sur « Se connecter » (et au load de l’iframe). | | `login-proof` | Iframe → parent | Vérification via `verifyLoginProof`, mise à jour du statut. | | `error` | Iframe → parent | Affichage du message d’erreur. | diff --git a/service-login-verify/dist/persistentNonceCache.d.ts b/service-login-verify/dist/persistentNonceCache.d.ts index 9dcc2e2..f73f70b 100644 --- a/service-login-verify/dist/persistentNonceCache.d.ts +++ b/service-login-verify/dist/persistentNonceCache.d.ts @@ -1,6 +1,6 @@ import type { NonceCacheLike } from './types.js'; /** - * Persistent nonce cache using IndexedDB (browser) or localStorage (fallback). + * Persistent nonce cache using IndexedDB (browser) and localStorage. * Implements NonceCacheLike interface for use with verifyLoginProof. */ export declare class PersistentNonceCache implements NonceCacheLike { diff --git a/service-login-verify/dist/persistentNonceCache.js b/service-login-verify/dist/persistentNonceCache.js index c439419..109be07 100644 --- a/service-login-verify/dist/persistentNonceCache.js +++ b/service-login-verify/dist/persistentNonceCache.js @@ -1,5 +1,5 @@ /** - * Persistent nonce cache using IndexedDB (browser) or localStorage (fallback). + * Persistent nonce cache using IndexedDB (browser) and localStorage. * Implements NonceCacheLike interface for use with verifyLoginProof. */ export class PersistentNonceCache { diff --git a/userwallet/src/components/GlobalActionBar.tsx b/userwallet/src/components/GlobalActionBar.tsx index 976fbd9..de6aee4 100644 --- a/userwallet/src/components/GlobalActionBar.tsx +++ b/userwallet/src/components/GlobalActionBar.tsx @@ -116,7 +116,7 @@ export function GlobalActionBar(): JSX.Element { + + + + + ); + })} + + )} + + ); } diff --git a/userwallet/src/components/PairingDisplayScreen.tsx b/userwallet/src/components/PairingDisplayScreen.tsx index 555d202..21822cd 100644 --- a/userwallet/src/components/PairingDisplayScreen.tsx +++ b/userwallet/src/components/PairingDisplayScreen.tsx @@ -19,16 +19,17 @@ export function PairingDisplayScreen(): JSX.Element { const { connected: pairingConnected } = usePairingConnected(); const [words2nd, setWords2nd] = useState([]); const [wordInput, setWordInput] = useState([]); - const [pubkey1stInput, setPubkey1stInput] = useState(''); const [error, setError] = useState(null); const [success, setSuccess] = useState(false); const [isConfirming, setIsConfirming] = useState(false); const [justConnected, setJustConnected] = useState(false); useEffect(() => { - const w = ensureLocalPairForSetup(); - setWords2nd(w); - }, []); // eslint-disable-line react-hooks/exhaustive-deps + if (identity !== null && identity.publicKey !== undefined) { + const w = ensureLocalPairForSetup(identity.publicKey); + setWords2nd(w); + } + }, [identity]); // eslint-disable-line react-hooks/exhaustive-deps useEffect(() => { if (isLoading || identity !== null) { @@ -51,21 +52,10 @@ export function PairingDisplayScreen(): JSX.Element { const wordsText = wordInput.join(' '); const parsed = parseAndValidatePairingWords(wordsText); if (parsed === null) { - setError('Mots invalides. 8 mots requis.'); + setError('Mots invalides. 17 mots requis.'); return; } - const pubkeyHex = pubkey1stInput.trim(); - if (pubkeyHex.length === 0) { - setError( - 'Pairing DH obligatoire : clé publique du 1ᵉʳ appareil requise (hex, 66 car.).', - ); - return; - } - if (pubkeyHex.length !== 66 || !['02', '03', '04'].includes(pubkeyHex.slice(0, 2))) { - setError('Clé publique invalide (hex 66 car., préfixe 02/03/04).'); - return; - } - const pair = addRemotePairFromWords(parsed, [], pubkeyHex); + const pair = addRemotePairFromWords(parsed, []); if (pair === null) { setError('Mots invalides. Vérifiez la saisie.'); return; @@ -87,6 +77,8 @@ export function PairingDisplayScreen(): JSX.Element { setError('Aucun relais activé. Configurez les relais pour finaliser le pairing.'); return; } + // Use pair's publicKey if available (will be updated from signatures if not) + const remotePublicKey = remote.publicKey; setIsConfirming(true); try { const ok = await runDevice2Confirmation({ @@ -96,7 +88,7 @@ export function PairingDisplayScreen(): JSX.Element { relays, start: identity.t0_anniversaire, end: Date.now(), - remotePublicKey: pubkeyHex, + remotePublicKey, }); setJustConnected(ok); } catch (err) { @@ -193,32 +185,22 @@ export function PairingDisplayScreen(): JSX.Element {

Saisir les mots du 1ᵉʳ appareil

- Saisissez les 8 mots et la clé publique affichés par le 1ᵉʳ appareil. + Saisissez les 17 mots affichés par le 1ᵉʳ appareil.

void handleSubmit(ev)} - aria-label="Saisir les mots et clé publique du 1er appareil" + aria-label="Saisir les mots du 1er appareil" > - {error !== null && ( - {identity !== null && ( -

- Clé publique de ce dispositif — à saisir sur le 2ᵉ (hex) : -
- - {identity.publicKey} - -

- )} {qrDataUrl !== null && (

Mots du 2ᵉ appareil void handleSubmitRemote(ev)} - aria-label="Saisir les mots et clé publique du 2e appareil" + aria-label="Saisir les mots du 2e appareil" > - {remoteError !== null && ( diff --git a/userwallet/src/components/WordInputGrid.tsx b/userwallet/src/components/WordInputGrid.tsx index e55e365..a838290 100644 --- a/userwallet/src/components/WordInputGrid.tsx +++ b/userwallet/src/components/WordInputGrid.tsx @@ -7,9 +7,10 @@ interface WordInputGridProps { id?: string; 'aria-describedby'?: string; 'aria-label'?: string; + wordCount?: number; } -const WORD_COUNT = 8; +const DEFAULT_WORD_COUNT = 17; /** * Filter wordlist by prefix (case-insensitive). @@ -23,7 +24,8 @@ function filterWordlist(prefix: string): string[] { } /** - * WordInputGrid component with autocomplete for 8 BIP32 words. + * WordInputGrid component with autocomplete for BIP32 words. + * Default: 17 words for public key encoding. */ export function WordInputGrid({ value, @@ -31,7 +33,9 @@ export function WordInputGrid({ id, 'aria-describedby': ariaDescribedBy, 'aria-label': ariaLabel, + wordCount = DEFAULT_WORD_COUNT, }: WordInputGridProps): JSX.Element { + const WORD_COUNT = wordCount; const [focusedIndex, setFocusedIndex] = useState(null); const [suggestions, setSuggestions] = useState([]); const [selectedSuggestionIndex, setSelectedSuggestionIndex] = useState(-1); @@ -209,41 +213,71 @@ export function WordInputGrid({ > {index + 1} - { - inputRefs.current[index] = el; - }} - id={`${id ?? 'word'}-${index}`} - type="text" - value={word} - onChange={(e: ChangeEvent) => { - handleInputChange(index, e.target.value); - }} - onKeyDown={(e) => { - handleKeyDown(index, e); - }} - onFocus={() => { - handleFocus(index); - }} - onBlur={handleBlur} - autoComplete="off" - spellCheck={false} - aria-describedby={ - index === 0 && ariaDescribedBy !== undefined - ? ariaDescribedBy - : undefined - } - style={{ - width: '100%', - padding: '0.5rem', - fontSize: '1rem', - fontFamily: 'monospace', - border: '1px solid var(--color-border, #ccc)', - borderRadius: '4px', - backgroundColor: 'var(--color-background)', - color: 'var(--color-text)', - }} - /> +

+ { + inputRefs.current[index] = el; + }} + id={`${id ?? 'word'}-${index}`} + type="password" + value={word} + onChange={(e: ChangeEvent) => { + handleInputChange(index, e.target.value); + }} + onKeyDown={(e) => { + handleKeyDown(index, e); + }} + onFocus={() => { + handleFocus(index); + }} + onBlur={handleBlur} + autoComplete="off" + spellCheck={false} + aria-describedby={ + index === 0 && ariaDescribedBy !== undefined + ? ariaDescribedBy + : undefined + } + style={{ + width: '100%', + padding: '0.5rem', + paddingRight: '2.5rem', + fontSize: '1rem', + fontFamily: 'monospace', + border: '1px solid var(--color-border, #ccc)', + borderRadius: '4px', + backgroundColor: 'var(--color-background)', + color: 'var(--color-text)', + }} + /> + {word.length > 0 && ( + + )} +
{focusedIndex === index && suggestions.length > 0 && (
{ const { relays, message, hash, recipientPublicKey, senderIdentity } = params; - if (recipientPublicKey === undefined || recipientPublicKey === '') { - throw new Error( - 'Pairing DH obligatoire : clé publique du pair distant requise (exiger publicKey).', - ); - } - const pk = senderIdentity.privateKey; - if (pk === undefined) { - throw new Error('Clé privée requise pour chiffrer le message de pairing (ECDH).'); - } + // ECDH encryption is optional: if recipientPublicKey is provided, use ECDH; otherwise publish without encryption + // The public key will be extracted from signatures and stored for future ECDH use const enabled = relays.filter((r) => r.enabled); if (enabled.length === 0) { throw new Error('No enabled relays'); } - const { encrypted, iv, senderPublicKey } = await encryptPairingMessage( - message, - recipientPublicKey, - senderIdentity, - ); - const msgCle = buildMsgCle(hash, iv, senderPublicKey); - const msgChiffre: MsgChiffre = { - hash, - message_chiffre: encrypted, - datajson_public: message.datajson, - }; - for (const r of enabled) { - await postMessageChiffre(r.endpoint, msgChiffre); - await postKey(r.endpoint, msgCle); + + // If recipientPublicKey is provided, use ECDH encryption + if (recipientPublicKey !== undefined && recipientPublicKey !== '') { + const pk = senderIdentity.privateKey; + if (pk === undefined) { + throw new Error('Clé privée requise pour chiffrer le message de pairing (ECDH).'); + } + const { encrypted, iv, senderPublicKey } = await encryptPairingMessage( + message, + recipientPublicKey, + senderIdentity, + ); + const msgCle = buildMsgCle(hash, iv, senderPublicKey); + const msgChiffre: MsgChiffre = { + hash, + message_chiffre: encrypted, + datajson_public: message.datajson, + }; + for (const r of enabled) { + await postMessageChiffre(r.endpoint, msgChiffre); + await postKey(r.endpoint, msgCle); + } + } else { + // No ECDH: publish message in clear (base64 encoded) + // The public key will be extracted from signatures and stored for future ECDH use + const payload = JSON.stringify(message); + const messageChiffre = btoa(payload); + const msgChiffre: MsgChiffre = { + hash, + message_chiffre: messageChiffre, + datajson_public: message.datajson, + }; + for (const r of enabled) { + await postMessageChiffre(r.endpoint, msgChiffre); + } } } @@ -222,7 +237,7 @@ interface PublishPairingMessageAndSignatureParams { message: MembreFinaliserMessage; hash: string; sig: Signature; - recipientPublicKey: string; + recipientPublicKey?: string; senderIdentity: LocalIdentity; } @@ -485,7 +500,7 @@ interface RunDevice1ConfirmationParams { pairRemote: string; identity: LocalIdentity; relays: RelayConfig[]; - remotePublicKey: string; + remotePublicKey?: string; } /** @@ -496,10 +511,8 @@ export async function runDevice1Confirmation( params: RunDevice1ConfirmationParams, ): Promise { const { pairLocal, pairRemote, identity, relays, remotePublicKey } = params; - if (remotePublicKey === undefined || remotePublicKey === '') { - throw new Error( - 'Pairing DH obligatoire : clé publique du pair distant requise (exiger publicKey).', - ); + if (remotePublicKey !== undefined && remotePublicKey !== '') { + // ECDH encryption enabled when remotePublicKey is provided } const { message, hash } = await createMembreFinaliserMessage( pairLocal, @@ -524,12 +537,25 @@ export async function runDevice1Confirmation( const msg = messageWithHash(message, hash); const { valid } = verifyMessageSignatures(msg, sigs); if (hasTwoDistinctSignatures(valid, identity.publicKey)) { - await buildAndPublishPairingVersion2( - relays, - message, - remotePublicKey, - identity, - ); + // Extract remote public key from signatures + const remoteSig = valid.find((s) => s.cle_publique !== identity.publicKey); + const extractedRemotePublicKey = remoteSig?.cle_publique; + + // Update pair with remote public key if found + if (extractedRemotePublicKey !== undefined) { + updatePairPublicKey(pairRemote, extractedRemotePublicKey); + } + + // Use extracted key or provided key for ECDH + const keyForEcdh = extractedRemotePublicKey ?? remotePublicKey; + if (keyForEcdh !== undefined && keyForEcdh !== '') { + await buildAndPublishPairingVersion2( + relays, + message, + keyForEcdh, + identity, + ); + } await storePairingConfirmed(pairLocal, pairRemote, hash, 2); return true; } @@ -582,6 +608,16 @@ export async function runDevice2Confirmation( if (valid.length === 0) { return false; } + + // Extract remote public key from signatures (device 1's signature) + const remoteSig = valid.find((s) => s.cle_publique !== identity.publicKey); + const extractedRemotePublicKey = remoteSig?.cle_publique; + + // Update pair with remote public key if found + if (extractedRemotePublicKey !== undefined) { + updatePairPublicKey(pairRemote, extractedRemotePublicKey); + } + const n = generateUuid(); const sig = signMembreFinaliser(hash, identity, n); await publishPairingSignature(relays, sig); diff --git a/userwallet/src/utils/bip32.ts b/userwallet/src/utils/bip32.ts index f9e66dd..2b4dd0f 100644 --- a/userwallet/src/utils/bip32.ts +++ b/userwallet/src/utils/bip32.ts @@ -318,6 +318,91 @@ export function bip32WordsToUuid(words: string[]): string | null { } } +/** + * Convert public key (hex, 66 chars) to BIP32 word list. + * Public key is 33 bytes. Each word encodes 2 bytes (16 bits), so we need 17 words. + * Last word encodes only 1 byte (the 33rd byte), so we pad with 0. + */ +export function publicKeyToBip32Words(publicKey: string): string[] { + const hex = publicKey.toLowerCase().replace(/^0x/, ''); + if (hex.length !== 66) { + throw new Error('Public key must be 66 hex characters'); + } + const bytes = hexToBytes(hex); + const words: string[] = []; + // Encode 32 bytes as 16 words (2 bytes per word) + for (let i = 0; i < 32; i += 2) { + const byte1 = bytes[i]; + const byte2 = bytes[i + 1]; + if (byte1 === undefined || byte2 === undefined) { + continue; + } + const index = (byte1 << 8) | byte2; + const wordIndex = index % BIP32_WORDLIST.length; + const word = BIP32_WORDLIST[wordIndex]; + if (word === undefined) { + continue; + } + words.push(word); + } + // Encode last byte (33rd) as a word (pad with 0 for the second byte) + const lastByte = bytes[32]; + if (lastByte !== undefined) { + const index = (lastByte << 8) | 0; + const wordIndex = index % BIP32_WORDLIST.length; + const word = BIP32_WORDLIST[wordIndex]; + if (word !== undefined) { + words.push(word); + } + } + return words; +} + +/** + * Convert BIP32 word list back to public key (hex, 66 chars). + * Expects 17 words: 16 words for 32 bytes + 1 word for the last byte. + */ +export function bip32WordsToPublicKey(words: string[]): string | null { + try { + if (words.length !== 17) { + return null; + } + const indices: number[] = []; + for (const word of words) { + const index = BIP32_WORDLIST.indexOf(word.toLowerCase()); + if (index === -1) { + return null; + } + indices.push(index); + } + // Reconstruct 33 bytes: 16 words for 32 bytes + 1 word for last byte + const bytes = new Uint8Array(33); + for (let i = 0; i < 16; i++) { + const index = indices[i]; + if (index === undefined) { + return null; + } + bytes[i * 2] = (index >> 8) & 0xff; + bytes[i * 2 + 1] = index & 0xff; + } + // Last word encodes only the 33rd byte (second byte is padding) + const lastIndex = indices[16]; + if (lastIndex === undefined) { + return null; + } + bytes[32] = (lastIndex >> 8) & 0xff; + + const hex = bytesToHex(bytes); + // Validate it's a valid public key (starts with 02, 03, or 04) + if (!['02', '03', '04'].includes(hex.slice(0, 2))) { + return null; + } + return hex; + } catch { + return null; + } +} + /** * Generate a random UUID v4. */ diff --git a/userwallet/src/utils/pairing.ts b/userwallet/src/utils/pairing.ts index 0788401..52cb145 100644 --- a/userwallet/src/utils/pairing.ts +++ b/userwallet/src/utils/pairing.ts @@ -1,7 +1,8 @@ import { generateUuid, uuidToBip32Words, - bip32WordsToUuid, + publicKeyToBip32Words, + bip32WordsToPublicKey, } from './bip32'; import type { PairConfig } from '../types/identity'; @@ -32,18 +33,23 @@ export function storePairs(pairs: PairConfig[]): void { /** * Create a new local pair. + * Returns words encoding the identity public key (not the pair UUID). */ -export function createLocalPair(membresParentsUuid: string[]): { +export function createLocalPair( + membresParentsUuid: string[], + identityPublicKey: string, +): { pair: PairConfig; words: string[]; } { const uuid = generateUuid(); - const words = uuidToBip32Words(uuid); + const words = publicKeyToBip32Words(identityPublicKey); const pair: PairConfig = { uuid, membres_parents_uuid: membresParentsUuid, is_local: true, can_sign: true, + publicKey: identityPublicKey, }; const pairs = getStoredPairs(); pairs.push(pair); @@ -53,25 +59,23 @@ export function createLocalPair(membresParentsUuid: string[]): { /** * Add a remote pair from BIP32 words. - * remotePublicKey (hex) optional: when provided, stored for ECDH encryption of pairing messages. + * Words encode the remote identity public key (66 hex chars). */ export function addRemotePairFromWords( words: string[], membresParentsUuid: string[], - remotePublicKey?: string, ): PairConfig | null { - const uuid = bip32WordsToUuid(words); - if (uuid === null) { + const publicKey = bip32WordsToPublicKey(words); + if (publicKey === null) { return null; } + const uuid = generateUuid(); const pair: PairConfig = { uuid, membres_parents_uuid: membresParentsUuid, is_local: false, can_sign: false, - ...(remotePublicKey !== undefined && remotePublicKey !== '' - ? { publicKey: remotePublicKey } - : {}), + publicKey, }; const pairs = getStoredPairs(); pairs.push(pair); @@ -89,7 +93,7 @@ export function generatePairingWords(): string[] { return uuidToBip32Words(uuid); } -const EXPECTED_PAIRING_WORD_COUNT = 8; +const EXPECTED_PAIRING_WORD_COUNT = 17; // 17 words for public key (33 bytes) /** * Parse and validate pairing words from user input (whitespace-separated). @@ -103,8 +107,8 @@ export function parseAndValidatePairingWords(text: string): string[] | null { if (words.length !== EXPECTED_PAIRING_WORD_COUNT) { return null; } - const uuid = bip32WordsToUuid(words); - return uuid !== null ? words : null; + const publicKey = bip32WordsToPublicKey(words); + return publicKey !== null ? words : null; } /** @@ -125,15 +129,16 @@ export function hasRemotePair(): boolean { /** * Ensure a local pair exists for setup (1st or 2nd device). If none, create one. - * Returns the local pair's words for QR/URL. Idempotent. + * Returns the local pair's words (encoding public key) for QR/URL. Idempotent. + * Requires identity public key. */ -export function ensureLocalPairForSetup(): string[] { +export function ensureLocalPairForSetup(identityPublicKey: string): string[] { const pairs = getStoredPairs(); const local = pairs.find((p) => p.is_local); - if (local !== undefined) { - return uuidToBip32Words(local.uuid); + if (local !== undefined && local.publicKey !== undefined) { + return publicKeyToBip32Words(local.publicKey); } - const { words } = createLocalPair([]); + const { words } = createLocalPair([], identityPublicKey); return words; } @@ -146,16 +151,16 @@ export function getPairsForMember(membreUuid: string): PairConfig[] { } /** - * Words for the local pair (8 words, BIP32-style). Null if no local pair. + * Words for the local pair (encoding public key, BIP32-style). Null if no local pair or no public key. * Does not create a pair. */ export function getLocalPairWords(): string[] | null { const pairs = getStoredPairs(); const local = pairs.find((p) => p.is_local); - if (local === undefined) { + if (local === undefined || local.publicKey === undefined) { return null; } - return uuidToBip32Words(local.uuid); + return publicKeyToBip32Words(local.publicKey); } /** @@ -165,3 +170,25 @@ export function getLocalPairWords(): string[] | null { export function isPairingConnected(): boolean { return false; } + +/** + * Remove a pair by UUID. + */ +export function removePair(pairUuid: string): void { + const pairs = getStoredPairs(); + const filtered = pairs.filter((p) => p.uuid !== pairUuid); + storePairs(filtered); +} + +/** + * Update a pair's public key (for ECDH encryption). + */ +export function updatePairPublicKey(pairUuid: string, publicKey: string): void { + const pairs = getStoredPairs(); + const pair = pairs.find((p) => p.uuid === pairUuid); + if (pair === undefined) { + return; + } + pair.publicKey = publicKey; + storePairs(pairs); +} diff --git a/website-skeleton/README.md b/website-skeleton/README.md index 57f80f5..4498f4c 100644 --- a/website-skeleton/README.md +++ b/website-skeleton/README.md @@ -1,6 +1,6 @@ # website-skeleton -Squelette d'un site qui intègre UserWallet en iframe : écoute des messages `postMessage` (auth-request, login-proof, error, contract), vérification des preuves de login via `service-login-verify`, et affichage du statut (accepté / refusé). +Squelette d'un site qui intègre UserWallet en iframe : écoute des messages `postMessage` (login-proof, error, contract), vérification des preuves de login via `service-login-verify`, et affichage du statut (accepté / refusé). Connexion via un seul parcours : « Se connecter » puis authentification MFA dans l'iframe. ## Prérequis @@ -26,7 +26,7 @@ Ouvre par défaut sur `http://localhost:3024`. L'iframe pointe vers UserWallet ( ## Configuration - **Origine UserWallet** : `src/config.ts` définit `USERWALLET_ORIGIN`. En dev, défaut `http://localhost:3018` (si UserWallet tourne en dev sur ce port). En prod, défaut `https://userwallet.certificator.4nkweb.com`. Pour override : variable d'environnement `VITE_USERWALLET_ORIGIN` (ex. `VITE_USERWALLET_ORIGIN=http://localhost:3018 npm run dev`). -- **Contrat de service** : Le skeleton a un contrat de service réel défini dans `src/serviceContract.ts` avec UUID `skeleton-service-uuid-4nkweb-2026`. Le contrat est chargé automatiquement au démarrage et envoyé à l'iframe UserWallet. +- **Contrat de service** : Le skeleton a un contrat de service réel défini dans `src/serviceContract.ts` avec UUID `32b9095a-562d-4239-ae45-2d7ffb1a40de`. Le contrat est chargé automatiquement au démarrage et envoyé à l'iframe UserWallet. - **Clé publique du service** : Configurez `VITE_SKELETON_SERVICE_PUBLIC_KEY` avec une clé publique secp256k1 compressée valide (66 hex chars, commençant par 02 ou 03). Exemple : `VITE_SKELETON_SERVICE_PUBLIC_KEY=02abc123... npm run dev`. Si non configurée, un placeholder est utilisé mais les signatures ne pourront pas être vérifiées. - **Validateurs** : Les validateurs sont extraits automatiquement du contrat de service skeleton. Le skeleton peut aussi recevoir un contrat personnalisé via `postMessage` (type `contract`) qui remplacera le contrat par défaut. @@ -64,8 +64,10 @@ Le skeleton utilise automatiquement le contrat de service skeleton au démarrage 2. Lancer le skeleton (`npm run dev` ou servir `dist/`). 3. Ouvrir la page du skeleton : l'iframe affiche UserWallet. 4. **Envoyer contrat (optionnel)** : envoyer un message `contract` avec le contrat et ses actions pour mettre à jour les validateurs. -5. **Demander auth** : bouton « Demander auth (auth-request) » → envoi de `auth-request` à l'iframe. -6. **Login** : depuis l'iframe, effectuer le flux de login ; à la fin, UserWallet envoie `login-proof` au parent. Le skeleton vérifie la preuve (`verifyLoginProof`) et affiche « Login accepté » ou « Login refusé : … ». +5. **Se connecter** : cliquer « Se connecter » → envoi du contrat à l'iframe, affichage de l'iframe. +6. **Login MFA** : depuis l'iframe, effectuer le flux de login (MFA) ; à la fin, UserWallet envoie `login-proof` au parent. Le skeleton vérifie la preuve (`verifyLoginProof`) et affiche « Login accepté » ou « Login refusé : … ». +7. **Description du contrat** : page `contrat.html` (lien depuis l'accueil) décrit le contrat de service skeleton. +8. **Description du membre** : page `membre.html` décrit le membre connecté (l'utilisateur), pas le validateur du service. Clés générées dans l'iframe (UserWallet), stockées en IndexedDB. Distinction créateur du service (wallet .env.private, jamais exposé) vs utilisateur (iframe, IndexedDB). ## Exemple d'intégration @@ -165,8 +167,10 @@ Les raisons de refus possibles : ## Structure -- `index.html` : page avec iframe, zone de statut, bouton auth. -- `src/main.ts` : chargement de l'iframe, écoute `message`, envoi `auth-request`, appel à `verifyLoginProof`, mise à jour du statut, gestion des messages `contract`. +- `index.html` : page avec iframe, liens « Description du contrat » / « Description du membre », bouton « Se connecter ». +- `src/main.ts` : chargement de l'iframe, écoute `message`, envoi du contrat au clic « Se connecter », appel à `verifyLoginProof`, gestion des messages `contract`. +- `contrat.html` : page de description du contrat de service skeleton (labels, UUIDs, usage). +- `membre.html` : page de description du membre connecté (utilisateur) et de son device (Pair) ; clés iframe / IndexedDB. Distinction service (.env.private) vs utilisateur. - `src/config.ts` : `USERWALLET_ORIGIN`, `DEFAULT_VALIDATEURS` (extraits du contrat skeleton), types `Contrat` et `Action`. - `src/serviceContract.ts` : contrat de service skeleton avec UUID dédié, action login, et configuration de la clé publique via `VITE_SKELETON_SERVICE_PUBLIC_KEY`. - `src/contract.ts` : extraction des validateurs depuis les contrats (`extractLoginValidators`), validation de structure (`isValidContract`, `isValidAction`). diff --git a/website-skeleton/contrat.html b/website-skeleton/contrat.html new file mode 100644 index 0000000..5d88ece --- /dev/null +++ b/website-skeleton/contrat.html @@ -0,0 +1,159 @@ + + + + + + Le contrat – 4NK un nouveau web + + + +

Le contrat

+

← Retour à l'accueil · Qui êtes-vous ?

+ +
+ En résumé : Ce contrat définit les règles de confiance entre vous et le service. + Il précise qui peut faire quoi et comment prouver son identité. + Tout est vérifiable et transparent. +
+ +

Qu'est-ce que ce contrat ?

+

+ Ce n'est pas un contrat papier, mais un accord numérique qui établit les règles du jeu. + Il définit : +

+
    +
  • Le service : Website Skeleton (ce site de démonstration).
  • +
  • Les actions possibles : se connecter (login).
  • +
  • Les validateurs : qui a le droit de vérifier les connexions.
  • +
+ +

Ce que le service s'engage à faire

+
+
    +
  • Ne jamais stocker vos clés privées — elles restent sur votre appareil.
  • +
  • Vérifier votre identité de façon transparente — via une preuve cryptographique que vous fournissez.
  • +
  • Respecter les règles du contrat — publiquement vérifiables.
  • +
  • Utiliser une clé de service déclarée — jamais exposée, mais vérifiable.
  • +
+
+ +

Ce que vous vous engagez à faire

+
+
    +
  • Protéger votre appareil — c'est votre coffre-fort numérique.
  • +
  • Fournir une preuve valide — en signant avec vos clés lors de la connexion.
  • +
  • Être responsable de vos clés — si vous les perdez, personne ne peut les récupérer.
  • +
+
+ +

Les parties prenantes

+ +

Le service (Website Skeleton)

+

+ C'est le site que vous utilisez. Il possède sa propre identité (un « validateur ») qui permet + de vérifier que les connexions sont légitimes. +

+ +

Vous (le membre connecté)

+

+ Vous êtes l'utilisateur qui souhaite accéder au service. Votre identité est prouvée par + la signature de votre appareil (en savoir plus). +

+ +

Comment fonctionne la validation ?

+
    +
  1. Vous demandez à vous connecter.
  2. +
  3. Votre appareil crée une preuve (login-proof) signée avec vos clés.
  4. +
  5. Le service vérifie cette preuve grâce aux règles définies dans ce contrat.
  6. +
  7. Si la preuve est valide, vous êtes connecté. Sinon, l'accès est refusé.
  8. +
+

+ Ce système est plus sûr qu'un mot de passe classique car il n'y a rien à voler côté serveur. +

+ +

Pourquoi c'est plus sûr ?

+
    +
  • Pas de base de mots de passe à pirater.
  • +
  • Vos clés ne transitent jamais sur le réseau.
  • +
  • Chaque connexion est unique (signature à usage unique).
  • +
  • Vérifiable par tous — les règles du contrat sont publiques.
  • +
+ +
+ Détails techniques du contrat +

Identifiants

+
    +
  • Contrat UUID : f9b9b336-4282-4c1c-b70b-e5197aeae3fa
  • +
  • Service UUID : 32b9095a-562d-4239-ae45-2d7ffb1a40de
  • +
  • Action login UUID : 0ac7de59-9e81-4bdc-bd19-c07750fad48e
  • +
  • Validateur (membre) : 0e865301-362f-4951-bfbc-531b7bddf820
  • +
+

Membres par rôles

+

Chaque rôle définit qui peut valider quoi. Un membre peut avoir plusieurs Pairs (appareils), chaque Pair possède une clé publique unique :

+ +

Validateur (contrat)

+
    +
  • + Membre : 0e865301-362f-4951-bfbc-531b7bddf820 +
    Signatures obligatoires : 1 +
      +
    • + Pair 1 : f2779304-0d9b-4139-9aee-8d3347819d98 +
      Clé publique : 0244f299538f4a091d93561dcee0c77de3e0d8bb917c9378405653c57f7800f174 +
    • +
    +
  • +
+ +

Validateur login (action)

+
    +
  • + Membre : 0e865301-362f-4951-bfbc-531b7bddf820 +
    Signatures obligatoires : 1 +
      +
    • + Pair 1 : f2779304-0d9b-4139-9aee-8d3347819d98 +
      Clé publique : 0244f299538f4a091d93561dcee0c77de3e0d8bb917c9378405653c57f7800f174 +
    • +
    +
  • +
+
+ +

← Retour à l'accueil · Qui êtes-vous ?

+ + diff --git a/website-skeleton/generate-service-wallet.mjs b/website-skeleton/generate-service-wallet.mjs index 619ac82..63af8cb 100755 --- a/website-skeleton/generate-service-wallet.mjs +++ b/website-skeleton/generate-service-wallet.mjs @@ -45,7 +45,7 @@ if (existsSync(envPath)) { // Write .env with public key const envContent = `# Service wallet public key for website-skeleton # Generated on ${new Date().toISOString()} -# Service UUID: skeleton-service-uuid-4nkweb-2026 +# Service UUID: 32b9095a-562d-4239-ae45-2d7ffb1a40de VITE_SKELETON_SERVICE_PUBLIC_KEY=${publicKey} `; @@ -55,7 +55,7 @@ writeFileSync(envPath, envContent, { mode: 0o600 }); const envPrivateContent = `# Service wallet private key for website-skeleton # ⚠️ SECRET: Keep this file secure and never commit it to version control # Generated on ${new Date().toISOString()} -# Service UUID: skeleton-service-uuid-4nkweb-2026 +# Service UUID: 32b9095a-562d-4239-ae45-2d7ffb1a40de # # This private key is used to sign service operations. # Store it securely and never share it. diff --git a/website-skeleton/index.html b/website-skeleton/index.html index 21ba166..b067ab7 100644 --- a/website-skeleton/index.html +++ b/website-skeleton/index.html @@ -3,7 +3,7 @@ - Website skeleton – UserWallet iframe + 4NK un nouveau web - site d'exemple -

Website skeleton – intégration iframe UserWallet

-

En attente du login depuis l'iframe.

+

4NK un nouveau web - site d'exemple

+

Le contrat · Qui êtes-vous ?

-
-
+
@@ -152,6 +172,7 @@