Website-skeleton partie connectée, contrat en dur, navigate-login; UserWallet pairing-relay-status, redirect; website-data, proxy data, cryptographie, fixKnowledge
**Motivations:** - Partie connectée du skeleton accessible seulement si pairing satisfait + relais OK, avec page type skeleton (avatar, notifications). - Éviter « Aucun service disponible » : contrat présent en dur dans la page, transmis à l’iframe ; navigation évidente ou automatique vers login. - Sécuriser postMessage (origine UserWallet uniquement) ; déployer data sur le proxy et certificat data.certificator.4nkweb.com. - Vulgariser cryptographie (ECDH, AES-GCM, Schnorr, workflow, collecte signatures) ; documenter correctifs et architecture. **Root causes:** - Section connectée affichée sans vérifier pairing/relay ; possibilité de forger pairing-relay-status depuis la console. - Iframe masquée ou /login chargé avant réception du contrat → graphe vide, redirection vers /services. - Pas de contrôle d’origine sur les messages reçus ; pas de projet website-data ni config Nginx/certificat pour data. **Correctifs:** - Vérification msg.origin === USERWALLET_ORIGIN dans handleMessage (skeleton). - Si session mais pas pairingRelayStatus : afficher iframe pour réception du statut, message « Vérification du statut… ». - Contrat envoyé dès load iframe (init iframe.src = USERWALLET_ORIGIN) ; au clic « Se connecter », envoi contract + navigate-login (service, membre). - UserWallet : écoute navigate-login → navigation /login?service=&membre= ; LoginScreen avec service+membre en URL ne redirige plus vers /services, dispatch E_SELECT_SERVICE / E_SELECT_MEMBER. **Evolutions:** - Message pairing-relay-status (iframe → parent) ; canShowConnectedSection exige login + pairing OK + relay OK ; page connectée avec header avatar + icône notifications. - Skeleton : getLoginContext, sendNavigateLoginToIframe, onIframeLoad, loginRequested/iframeLoaded ; contrat envoyé avec serviceUuid, membreUuid. - UserWallet : PairingRelayStatusMessage, envoi depuis HomeScreen/LoginScreen ; type navigate-login, handleNavigateLogin dans useChannel. - Page cryptographie.html (workflow, algorithmes, collecte signatures) ; liens nav, build. - website-data (Vite, channel, config), start/service/install ; configure-nginx-proxy + Certbot pour data.certificator.4nkweb.com. - fixKnowledge (postmessage-origin, section-connectee-non-affichee) ; features (partie-connectee-pairing-relay, userwallet-iframe-key-isolation). **Pages affectées:** - website-skeleton (index, main, config, serviceContract, cryptographie, technique, membre, contrat, vite.config, README). - userwallet (HomeScreen, LoginScreen, useChannel, iframeChannel, relay, crypto, iframe, Pairing*, RelaySettings, WordInputGrid, syncUpdateGraph, specs/synthese). - website-data (nouveau), configure-nginx-proxy, docs DOMAINS_AND_PORTS README, features, fixKnowledge, userwallet features/docs.
This commit is contained in:
parent
497bcf0819
commit
f9fe0e3419
@ -250,7 +250,34 @@ server {
|
||||
}
|
||||
EOF
|
||||
|
||||
# 7. Relay / api-relay (port 3019)
|
||||
# 7. Website data (port 3025)
|
||||
echo "📝 Configuration de data.certificator.4nkweb.com..."
|
||||
${SUDO_CMD} tee "${NGINX_SITES_AVAILABLE}/data.certificator.4nkweb.com" > /dev/null << 'EOF'
|
||||
# Website data (iframe data, non clés)
|
||||
server {
|
||||
listen 80;
|
||||
server_name data.certificator.4nkweb.com;
|
||||
|
||||
access_log /var/log/nginx/data.certificator.4nkweb.com.access.log;
|
||||
error_log /var/log/nginx/data.certificator.4nkweb.com.error.log;
|
||||
|
||||
location / {
|
||||
proxy_pass http://192.168.1.105:3025;
|
||||
proxy_http_version 1.1;
|
||||
proxy_set_header Upgrade $http_upgrade;
|
||||
proxy_set_header Connection 'upgrade';
|
||||
proxy_set_header Host $host;
|
||||
proxy_set_header X-Real-IP $remote_addr;
|
||||
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
||||
proxy_set_header X-Forwarded-Proto $scheme;
|
||||
proxy_cache_bypass $http_upgrade;
|
||||
proxy_read_timeout 300s;
|
||||
proxy_connect_timeout 75s;
|
||||
}
|
||||
}
|
||||
EOF
|
||||
|
||||
# 8. Relay / api-relay (port 3019)
|
||||
echo "📝 Configuration de relay.certificator.4nkweb.com..."
|
||||
${SUDO_CMD} tee "${NGINX_SITES_AVAILABLE}/relay.certificator.4nkweb.com" > /dev/null << 'EOF'
|
||||
# API Relay (UserWallet)
|
||||
@ -288,6 +315,7 @@ ${SUDO_CMD} ln -sf "${NGINX_SITES_AVAILABLE}/anchorage.certificator.4nkweb.com"
|
||||
${SUDO_CMD} ln -sf "${NGINX_SITES_AVAILABLE}/watermark.certificator.4nkweb.com" "${NGINX_SITES_ENABLED}/watermark.certificator.4nkweb.com"
|
||||
${SUDO_CMD} ln -sf "${NGINX_SITES_AVAILABLE}/userwallet.certificator.4nkweb.com" "${NGINX_SITES_ENABLED}/userwallet.certificator.4nkweb.com"
|
||||
${SUDO_CMD} ln -sf "${NGINX_SITES_AVAILABLE}/skeleton.certificator.4nkweb.com" "${NGINX_SITES_ENABLED}/skeleton.certificator.4nkweb.com"
|
||||
${SUDO_CMD} ln -sf "${NGINX_SITES_AVAILABLE}/data.certificator.4nkweb.com" "${NGINX_SITES_ENABLED}/data.certificator.4nkweb.com"
|
||||
${SUDO_CMD} ln -sf "${NGINX_SITES_AVAILABLE}/relay.certificator.4nkweb.com" "${NGINX_SITES_ENABLED}/relay.certificator.4nkweb.com"
|
||||
|
||||
# Tester la configuration Nginx
|
||||
@ -319,6 +347,7 @@ DOMAINS=(
|
||||
"watermark.certificator.4nkweb.com"
|
||||
"userwallet.certificator.4nkweb.com"
|
||||
"skeleton.certificator.4nkweb.com"
|
||||
"data.certificator.4nkweb.com"
|
||||
"relay.certificator.4nkweb.com"
|
||||
)
|
||||
|
||||
@ -349,6 +378,7 @@ echo " - anchorage.certificator.4nkweb.com -> http://192.168.1.105:3010"
|
||||
echo " - watermark.certificator.4nkweb.com -> http://192.168.1.105:3022"
|
||||
echo " - userwallet.certificator.4nkweb.com -> http://192.168.1.105:3018"
|
||||
echo " - skeleton.certificator.4nkweb.com -> http://192.168.1.105:3024"
|
||||
echo " - data.certificator.4nkweb.com -> http://192.168.1.105:3025"
|
||||
echo " - relay.certificator.4nkweb.com -> http://192.168.1.105:3019"
|
||||
echo ""
|
||||
echo "⚠️ Note: Si les services tournent sur une autre machine,"
|
||||
|
||||
@ -1,100 +1,100 @@
|
||||
⏳ Traitement: 210000/215703 UTXOs insérés...
|
||||
💾 Mise à jour des UTXOs dépensés...
|
||||
|
||||
📊 Résumé:
|
||||
- UTXOs vérifiés: 5146
|
||||
- UTXOs toujours disponibles: 5146
|
||||
- UTXOs dépensés détectés: 0
|
||||
|
||||
📈 Statistiques finales:
|
||||
- Total UTXOs: 68398
|
||||
- Dépensés: 63307
|
||||
- Non dépensés: 5091
|
||||
- Total UTXOs: 283165
|
||||
- Dépensés: 124307
|
||||
- Non dépensés: 158858
|
||||
|
||||
✅ Synchronisation terminée
|
||||
🔍 Démarrage de la synchronisation des UTXOs dépensés...
|
||||
|
||||
📊 UTXOs à vérifier: 213647
|
||||
📊 UTXOs à vérifier: 141938
|
||||
📡 Récupération des UTXOs depuis Bitcoin...
|
||||
📊 UTXOs disponibles dans Bitcoin: 223744
|
||||
📊 UTXOs disponibles dans Bitcoin: 272415
|
||||
💾 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...
|
||||
⏳ Traitement: 10000/272415 UTXOs insérés...
|
||||
⏳ Traitement: 20000/272415 UTXOs insérés...
|
||||
⏳ Traitement: 30000/272415 UTXOs insérés...
|
||||
⏳ Traitement: 40000/272415 UTXOs insérés...
|
||||
⏳ Traitement: 50000/272415 UTXOs insérés...
|
||||
⏳ Traitement: 60000/272415 UTXOs insérés...
|
||||
⏳ Traitement: 70000/272415 UTXOs insérés...
|
||||
⏳ Traitement: 80000/272415 UTXOs insérés...
|
||||
⏳ Traitement: 90000/272415 UTXOs insérés...
|
||||
⏳ Traitement: 100000/272415 UTXOs insérés...
|
||||
⏳ Traitement: 110000/272415 UTXOs insérés...
|
||||
⏳ Traitement: 120000/272415 UTXOs insérés...
|
||||
⏳ Traitement: 130000/272415 UTXOs insérés...
|
||||
⏳ Traitement: 140000/272415 UTXOs insérés...
|
||||
⏳ Traitement: 150000/272415 UTXOs insérés...
|
||||
⏳ Traitement: 160000/272415 UTXOs insérés...
|
||||
⏳ Traitement: 170000/272415 UTXOs insérés...
|
||||
⏳ Traitement: 180000/272415 UTXOs insérés...
|
||||
⏳ Traitement: 190000/272415 UTXOs insérés...
|
||||
⏳ Traitement: 200000/272415 UTXOs insérés...
|
||||
⏳ Traitement: 210000/272415 UTXOs insérés...
|
||||
⏳ Traitement: 220000/272415 UTXOs insérés...
|
||||
⏳ Traitement: 230000/272415 UTXOs insérés...
|
||||
⏳ Traitement: 240000/272415 UTXOs insérés...
|
||||
⏳ Traitement: 250000/272415 UTXOs insérés...
|
||||
⏳ Traitement: 260000/272415 UTXOs insérés...
|
||||
⏳ Traitement: 270000/272415 UTXOs insérés...
|
||||
💾 Mise à jour des UTXOs dépensés...
|
||||
|
||||
📊 Résumé:
|
||||
- UTXOs vérifiés: 213647
|
||||
- UTXOs toujours disponibles: 213647
|
||||
- UTXOs vérifiés: 141938
|
||||
- UTXOs toujours disponibles: 141938
|
||||
- UTXOs dépensés détectés: 0
|
||||
|
||||
📈 Statistiques finales:
|
||||
- Total UTXOs: 283165
|
||||
- Dépensés: 69526
|
||||
- Non dépensés: 213639
|
||||
- Dépensés: 141287
|
||||
- Non dépensés: 141878
|
||||
|
||||
✅ Synchronisation terminée
|
||||
🔍 Démarrage de la synchronisation des UTXOs dépensés...
|
||||
|
||||
📊 UTXOs à vérifier: 211454
|
||||
📊 UTXOs à vérifier: 124298
|
||||
📡 Récupération des UTXOs depuis Bitcoin...
|
||||
📊 UTXOs disponibles dans Bitcoin: 241303
|
||||
📊 UTXOs disponibles dans Bitcoin: 270669
|
||||
💾 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...
|
||||
⏳ Traitement: 10000/270669 UTXOs insérés...
|
||||
⏳ Traitement: 20000/270669 UTXOs insérés...
|
||||
⏳ Traitement: 30000/270669 UTXOs insérés...
|
||||
⏳ Traitement: 40000/270669 UTXOs insérés...
|
||||
⏳ Traitement: 50000/270669 UTXOs insérés...
|
||||
⏳ Traitement: 60000/270669 UTXOs insérés...
|
||||
⏳ Traitement: 70000/270669 UTXOs insérés...
|
||||
⏳ Traitement: 80000/270669 UTXOs insérés...
|
||||
⏳ Traitement: 90000/270669 UTXOs insérés...
|
||||
⏳ Traitement: 100000/270669 UTXOs insérés...
|
||||
⏳ Traitement: 110000/270669 UTXOs insérés...
|
||||
⏳ Traitement: 120000/270669 UTXOs insérés...
|
||||
⏳ Traitement: 130000/270669 UTXOs insérés...
|
||||
⏳ Traitement: 140000/270669 UTXOs insérés...
|
||||
⏳ Traitement: 150000/270669 UTXOs insérés...
|
||||
⏳ Traitement: 160000/270669 UTXOs insérés...
|
||||
⏳ Traitement: 170000/270669 UTXOs insérés...
|
||||
⏳ Traitement: 180000/270669 UTXOs insérés...
|
||||
⏳ Traitement: 190000/270669 UTXOs insérés...
|
||||
⏳ Traitement: 200000/270669 UTXOs insérés...
|
||||
⏳ Traitement: 210000/270669 UTXOs insérés...
|
||||
⏳ Traitement: 220000/270669 UTXOs insérés...
|
||||
⏳ Traitement: 230000/270669 UTXOs insérés...
|
||||
⏳ Traitement: 240000/270669 UTXOs insérés...
|
||||
⏳ Traitement: 250000/270669 UTXOs insérés...
|
||||
⏳ Traitement: 260000/270669 UTXOs insérés...
|
||||
⏳ Traitement: 270000/270669 UTXOs insérés...
|
||||
💾 Mise à jour des UTXOs dépensés...
|
||||
|
||||
📊 Résumé:
|
||||
- UTXOs vérifiés: 211454
|
||||
- UTXOs toujours disponibles: 211454
|
||||
- UTXOs vérifiés: 124298
|
||||
- UTXOs toujours disponibles: 124298
|
||||
- UTXOs dépensés détectés: 0
|
||||
|
||||
📈 Statistiques finales:
|
||||
- Total UTXOs: 283165
|
||||
- Dépensés: 71719
|
||||
- Non dépensés: 211446
|
||||
- Dépensés: 158927
|
||||
- Non dépensés: 124238
|
||||
|
||||
✅ Synchronisation terminée
|
||||
|
||||
@ -22,6 +22,7 @@ Ce document liste tous les domaines, ports et services de l'infrastructure Certi
|
||||
| `faucet.certificator.4nkweb.com` | API Faucet | 3021 | API REST pour distribuer des sats |
|
||||
| `mempool.4nkweb.com` | Mempool | 3015 | Explorateur de blockchain Bitcoin |
|
||||
| `skeleton.certificator.4nkweb.com` | Website skeleton | 3024 | Site squelette iframe UserWallet |
|
||||
| `data.certificator.4nkweb.com` | Website data | 3025 | Iframe data (non clés), site ↔ data ↔ userwallet |
|
||||
|
||||
### Configuration Nginx
|
||||
|
||||
@ -44,6 +45,7 @@ Tous les domaines sont routés via le proxy Nginx sur le serveur `192.168.1.100`
|
||||
| 3022 | API Filigrane | `api-filigrane` | `api-filigrane/filigrane-api.service` |
|
||||
| 3023 | API ClamAV | `api-clamav` | `api-clamav/clamav-api.service` |
|
||||
| 3024 | Website skeleton | `website-skeleton` | `website-skeleton/website-skeleton.service` |
|
||||
| 3025 | Website data | `website-data` | (à définir, ex. `website-data/website-data.service`) |
|
||||
|
||||
**Important :** Les ports 3010, 3020, 3021, 3022, 3023, 3024 sont fixes et définis dans les fichiers de service systemd. Ils ne peuvent pas être modifiés sans modifier les services.
|
||||
|
||||
|
||||
@ -65,6 +65,8 @@ Ce dossier contient toute la documentation nécessaire pour la maintenance et l'
|
||||
- Configuration (origine UserWallet, validateurs)
|
||||
- Utilisation, messages postMessage, références
|
||||
|
||||
- **website-data** (`website-data/`, `features/website-data-architecture.md`) : Iframe data (data.certificator.4nkweb.com), flux site ↔ data (+notifications) ↔ userwallet (relais).
|
||||
|
||||
- **[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)
|
||||
|
||||
@ -1,6 +1,6 @@
|
||||
# UserWallet – Champs obligatoires et attributs CNIL (complément specs)
|
||||
|
||||
**Author:** Équipe 4NK
|
||||
**Author:** Équipe 4NK
|
||||
**Date:** 2026-01-26
|
||||
|
||||
## Objectif
|
||||
@ -13,7 +13,7 @@ Documenter le complément de spécifications : champs (Champ) obligatoires des c
|
||||
|
||||
## Synthèse
|
||||
|
||||
- **Champs obligatoires** : tous les contrats ont certains des 9 types (partage institutions, RSSI, Correspondant CNIL, Responsable cybersécurité, support infogérant / admin / N1 / N2 / N3). Identification via `types` ou `datajson`.
|
||||
- **Champs obligatoires (rôles)** : tous les contrats ont certains des 11 types : partage institutions, RSSI, Correspondant CNIL, Responsable cybersécurité, support infogérant / admin / N1 / N2 / N3, **validation (contrat)**, **validation du login**. Identification via `types` ou `datajson`.
|
||||
- **Attributs CNIL dans datajson** : `raisons_usage_tiers`, `raisons_partage_tiers` (tableaux [raisons, tiers]), `conditions_conservation` (au moins `delai_expiration`).
|
||||
|
||||
## Modifications code
|
||||
|
||||
39
features/website-data-architecture.md
Normal file
39
features/website-data-architecture.md
Normal file
@ -0,0 +1,39 @@
|
||||
# website-data — architecture site <-> data <-> userwallet
|
||||
|
||||
**Objectif**
|
||||
|
||||
Projet iframe **data** (data.certificator.4nkweb.com) dédié aux **données non clés**, qui communique avec le **site** et avec **UserWallet** (clés, relais). Flux :
|
||||
|
||||
```
|
||||
site <-> data (+notifications) <-> userwallet (sur les relais)
|
||||
```
|
||||
|
||||
**Impacts**
|
||||
|
||||
- Nouveau projet `website-data/` (Vite, TypeScript), servi sur data.certificator.4nkweb.com (port 3025).
|
||||
- Le site parent intègre une seule iframe **Data**. Data embarque **UserWallet** en iframe enfant.
|
||||
- Contrat, auth, login-proof, etc. transitent par Data entre site et UserWallet.
|
||||
- Les clés et la crypto restent dans UserWallet ; Data ne fait aucune opération de signature/chiffrement.
|
||||
|
||||
**Modifications**
|
||||
|
||||
- **website-data** : `package.json`, `vite.config.ts`, `index.html`, `src/config.ts`, `src/channel.ts`, `src/main.ts`.
|
||||
- Messages postMessage : Site ↔ Data (contract, auth-request, data-request, data-response, notification, pong, error, login-proof, auth-response, service-status). Data ↔ UserWallet : mêmes types applicables, relayés.
|
||||
- Notifications : Data → Site (`notification` avec `kind` / `detail`). Actuellement pour userwallet:login-proof, userwallet:auth-response, userwallet:error. Placeholder pour notifications relay (data non clés).
|
||||
|
||||
**Modalités de déploiement**
|
||||
|
||||
- Build `website-data` (`npm run build`), servir `dist/` sur le port 3025.
|
||||
- Configurer Nginx (ou équivalent) pour data.certificator.4nkweb.com → backend 3025, certificat Let’s Encrypt.
|
||||
- S’aligner sur la procédure utilisée pour website-skeleton / userwallet (cf. docs infrastructure 4NK).
|
||||
|
||||
**Modalités d’analyse**
|
||||
|
||||
- Ouvrir le site parent avec iframe Data (src = data.certificator.4nkweb.com ou localhost:3025 en dev).
|
||||
- Envoyer `contract`, `auth-request` au Data iframe ; vérifier relay vers UserWallet et réponses (login-proof, etc.) remontées au site.
|
||||
- Vérifier les `notification` reçues par le site (kind `userwallet:*`).
|
||||
|
||||
**Pages affectées**
|
||||
|
||||
- `website-data/` (nouveau)
|
||||
- `features/website-data-architecture.md` (ce document)
|
||||
21
features/website-skeleton-partie-connectee-pairing-relay.md
Normal file
21
features/website-skeleton-partie-connectee-pairing-relay.md
Normal file
@ -0,0 +1,21 @@
|
||||
# Website-skeleton: partie connectée conditionnée au pairing et au relais
|
||||
|
||||
**Objectif:** La partie connectée du website-skeleton n’est accessible que si le statut pairing est satisfait (Requis: Oui, Satisfait: Oui) et le statut réseau relais est OK. La page connectée adopte un style skeleton avec avatar et icône notifications.
|
||||
|
||||
**Impacts:**
|
||||
- UserWallet envoie au parent (postMessage) un message `pairing-relay-status` avec `pairingSatisfied` et `relayOk`.
|
||||
- Website-skeleton affiche la section connectée uniquement lorsque l’utilisateur est connecté (login-proof reçu) **et** que le dernier `pairing-relay-status` reçu indique les deux conditions remplies.
|
||||
- La page connectée affiche un en-tête type skeleton avec photo d’avatar (placeholder) et bouton icône notifications.
|
||||
|
||||
**Modifications:**
|
||||
- **userwallet**
|
||||
- `src/utils/iframeChannel.ts`: nouveau type de message `pairing-relay-status` et interface `PairingRelayStatusMessage`.
|
||||
- `src/components/HomeScreen.tsx`: en iframe, envoi de `pairing-relay-status` lorsque pairing et relay changent.
|
||||
- `src/components/LoginScreen.tsx`: en iframe, envoi de `pairing-relay-status` (pairing satisfait, relay OK) pour que le parent reçoive l’état avant ou avec le login-proof.
|
||||
- **website-skeleton**
|
||||
- `src/main.ts`: réception de `pairing-relay-status`, stockage du dernier état; `canShowConnectedSection()` exige login + pairing satisfait + relay OK; `updateUI()` utilise cette condition pour afficher la section connectée. Vérification de `msg.origin === USERWALLET_ORIGIN` dans `handleMessage` pour n’accepter que les messages provenant de l’iframe UserWallet (évite qu’un script ou la console forge un message et force l’accès à la partie connectée).
|
||||
- `index.html`: section connectée refaite avec en-tête (avatar placeholder, icône notifications), liens et bouton déconnexion inchangés.
|
||||
|
||||
**Modalités de déploiement:** Déploiement classique du website-skeleton et du userwallet (build puis déploiement des artefacts).
|
||||
|
||||
**Modalités d’analyse:** Vérifier en conditions réelles : 1) sans pairing/relay OK, après login-proof la section connectée ne s’affiche pas ; 2) avec pairing satisfait et relay OK, après login-proof la section connectée s’affiche avec avatar et icône notifications ; 3) si le statut pairing ou relay repasse à non satisfait après connexion, la section connectée disparaît.
|
||||
50
fixKnowledge/userwallet-skeleton-contract-types-version.md
Normal file
50
fixKnowledge/userwallet-skeleton-contract-types-version.md
Normal file
@ -0,0 +1,50 @@
|
||||
# UserWallet iframe: Invalid contract structure / types_names_chiffres undefined
|
||||
|
||||
**Problème**
|
||||
|
||||
En iframe (skeleton → UserWallet), console :
|
||||
|
||||
- `Invalid contract structure received via channel: missing required fields`
|
||||
- `Error updating graph: TypeError: Cannot read properties of undefined (reading 'types_names_chiffres')` dans `syncUpdateGraph.ts`
|
||||
|
||||
**Impacts**
|
||||
|
||||
- Le contrat skeleton n’est pas accepté par UserWallet.
|
||||
- Le graphe n’est pas mis à jour ; le login skeleton échoue côté graphe.
|
||||
|
||||
**Root causes**
|
||||
|
||||
1. **useChannel** exige sur le contrat principal `uuid`, `version`, `validateurs`. Le skeleton n’envoyait pas `version` → validation « missing required fields ».
|
||||
2. **syncUpdateGraph** lit `msg.types.types_names_chiffres`. Le skeleton mettait `types_names_chiffres` dans `datajson` uniquement, pas de `types` à la racine → `msg.types` undefined → crash.
|
||||
|
||||
**Correctifs**
|
||||
|
||||
1. **userwallet `syncUpdateGraph.ts`**
|
||||
- Vérifier `msg?.types?.types_names_chiffres` avant utilisation.
|
||||
- Si absent : `console.warn` avec `uuid` et `return` (ne pas ajouter au graphe).
|
||||
- Évite le crash et signale les messages mal formés.
|
||||
|
||||
2. **website-skeleton `serviceContract.ts`**
|
||||
- Contrat : ajouter `version: '1.0'` et
|
||||
`types: { types_uuid: [SKELETON_CONTRACT_TYPE_UUID], types_names_chiffres: 'contrat' }`.
|
||||
- Action login : idem avec `version: '1.0'` et
|
||||
`types: { types_uuid: [SKELETON_ACTION_TYPE_UUID], types_names_chiffres: 'action,login' }`.
|
||||
- Alignement avec `MessageBase` / `MessageAValider` attendus par UserWallet.
|
||||
- `datajson` inchangé (types_uuid, types_names_chiffres, etc.).
|
||||
|
||||
**Fichiers modifiés**
|
||||
|
||||
- `userwallet/src/services/syncUpdateGraph.ts`
|
||||
- `website-skeleton/src/serviceContract.ts`
|
||||
- `website-skeleton/src/config.ts` (Contrat / Action : `version?`, `types?` documentés)
|
||||
|
||||
**Modalités de déploiement**
|
||||
|
||||
- Rebuild UserWallet et website-skeleton.
|
||||
- Redéployer les deux selon l’infra (skeleton, UserWallet iframe).
|
||||
|
||||
**Modalités d’analyse**
|
||||
|
||||
- Reproduire : ouvrir skeleton en iframe UserWallet, observer la console.
|
||||
- Vérifier l’absence de « Invalid contract structure » et de « types_names_chiffres » undefined.
|
||||
- Vérifier que le graphe reçoit bien contrat + action login (login possible).
|
||||
23
fixKnowledge/website-skeleton-postmessage-origin-check.md
Normal file
23
fixKnowledge/website-skeleton-postmessage-origin-check.md
Normal file
@ -0,0 +1,23 @@
|
||||
# Website-skeleton: vérification de l’origine des messages postMessage
|
||||
|
||||
**Problème:** Un utilisateur pouvait forcer l’accès à la partie connectée en exécutant dans la console du navigateur un `window.postMessage({ type: 'pairing-relay-status', payload: { pairingSatisfied: true, relayOk: true } }, '*')`. Si une session (login-proof) existait déjà, la section connectée s’affichait sans que pairing/relais soient réellement OK.
|
||||
|
||||
**Impacts:** Risque de contournement des conditions d’accès (pairing satisfait, relais OK) et affichage de la partie connectée dans un état non conforme.
|
||||
|
||||
**Cause:** Le handler `handleMessage` du website-skeleton ne vérifiait pas l’origine du message ; tout script s’exécutant dans la même page (ou la console) pouvait envoyer un faux `pairing-relay-status` ou `login-proof` (pour login-proof la vérification côté preuve reste, mais l’origine n’était pas contrôlée).
|
||||
|
||||
**Root cause:** Absence de contrôle de `event.origin` sur les messages reçus via `window.addEventListener('message', handleMessage)`.
|
||||
|
||||
**Correctifs:**
|
||||
- Au début de `handleMessage`, ignorer tout message dont `msg.origin !== USERWALLET_ORIGIN`. Seuls les messages émis par l’iframe UserWallet (même origine que `USERWALLET_ORIGIN`) sont traités. Un `postMessage` depuis la console a pour origine l’URL du site skeleton, pas celle du UserWallet, donc il est rejeté.
|
||||
|
||||
**Evolutions:** Aucune.
|
||||
|
||||
**Pages affectées:**
|
||||
- `website-skeleton/src/main.ts` (vérification `msg.origin !== USERWALLET_ORIGIN` en tête de `handleMessage`)
|
||||
- `features/website-skeleton-partie-connectee-pairing-relay.md` (mention de la vérification d’origine)
|
||||
- `fixKnowledge/website-skeleton-postmessage-origin-check.md` (ce document)
|
||||
|
||||
**Modalités de déploiement:** Déploiement classique du website-skeleton.
|
||||
|
||||
**Modalités d’analyse:** En console sur la page skeleton, exécuter `window.postMessage({ type: 'pairing-relay-status', payload: { pairingSatisfied: true, relayOk: true } }, '*')` alors qu’une session existe : la section connectée ne doit pas s’afficher si pairing/relay ne sont pas OK (message rejeté car origine ≠ UserWallet).
|
||||
@ -0,0 +1,26 @@
|
||||
# Website-skeleton: section connectée non affichée malgré pairing/relay OK
|
||||
|
||||
**Problème:** L'utilisateur voit dans UserWallet (HomeScreen) le statut « Pairing Satisfait: Oui » et « Statut réseau relais: OK », mais la page skeleton ne redirige pas vers la section connectée.
|
||||
|
||||
**Impacts:** L'utilisateur connecté (session existante) qui rafraîchit la page skeleton reste sur la section « Se connecter » au lieu de voir la section connectée.
|
||||
|
||||
**Cause:** La section connectée n’est affichée que si `canShowConnectedSection()` est vrai : session (login-proof) **et** dernier `pairing-relay-status` reçu avec `pairingSatisfied` et `relayOk`. Après un rafraîchissement, la session est en sessionStorage mais `pairingRelayStatus` n’est pas persisté. L’iframe UserWallet est dans un conteneur en `display: none` tant qu’on affiche la section login ; dans ce cas l’iframe peut ne pas charger (ou charger avec retard), donc le parent ne reçoit jamais `pairing-relay-status` et `pairingRelayStatus` reste `null`.
|
||||
|
||||
**Root cause:** Quand l’utilisateur a une session mais pas encore de `pairing-relay-status`, l’iframe est cachée donc elle ne charge pas (ou pas à temps) et n’envoie pas le message au parent.
|
||||
|
||||
**Correctifs:**
|
||||
- Quand `isLoggedIn()` est vrai et `pairingRelayStatus === null`, afficher l’iframe (conteneur en `display: block`) pour qu’elle charge et envoie `pairing-relay-status`.
|
||||
- Nouvelle branche dans `updateUI()` : `else if (isLoggedIn() && pairingRelayStatus === null)` → `showLoginInterfaceWithIframe(true)`.
|
||||
- `showLoginInterfaceWithIframe(waitingForStatus)` : si `waitingForStatus` est vrai, afficher l’iframe et le message « Vérification du statut pairing et relais… » ; sinon comportement identique à `showLoginInterface()` (iframe cachée).
|
||||
- Élément `#waiting-status` dans la section login, affiché uniquement en attente du statut.
|
||||
|
||||
**Evolutions:** Aucune.
|
||||
|
||||
**Pages affectées:**
|
||||
- `website-skeleton/src/main.ts` (updateUI, showLoginInterfaceWithIframe, ref waitingStatusEl)
|
||||
- `website-skeleton/index.html` (élément #waiting-status)
|
||||
- `fixKnowledge/website-skeleton-section-connectee-non-affichee.md` (ce document)
|
||||
|
||||
**Modalités de déploiement:** Build et déploiement classique du website-skeleton.
|
||||
|
||||
**Modalités d’analyse:** 1) Se connecter sur le skeleton (login-proof reçu, section connectée affichée). 2) Rafraîchir la page : la section « Vérification du statut pairing et relais… » et l’iframe s’affichent brièvement, puis la section connectée s’affiche dès réception de `pairing-relay-status`. 3) Sans session : pas de changement (clic « Se connecter » puis login dans l’iframe pour obtenir la section connectée).
|
||||
@ -1,30 +1,32 @@
|
||||
# Complément de specs – Champs obligatoires et attributs CNIL
|
||||
|
||||
**Author:** Équipe 4NK
|
||||
**Date:** 2026-01-26
|
||||
**Author:** Équipe 4NK
|
||||
**Date:** 2026-01-26
|
||||
|
||||
Référence : `specs.md` (Contrat, Champ, MessageBase, DataJson).
|
||||
|
||||
---
|
||||
|
||||
## 1. Champs obligatoires des contrats
|
||||
## 1. Champs obligatoires des contrats (rôles)
|
||||
|
||||
Tous les contrats ont **certains** des champs (objets `Champ`) suivants. Chaque type correspond à un usage métier précis ; un contrat donné en possède un sous-ensemble selon le contexte.
|
||||
Tous les contrats ont **certains** des champs (objets `Champ`) suivants. Chaque type correspond à un **rôle** / usage métier précis ; un contrat donné en possède un sous-ensemble selon le contexte.
|
||||
|
||||
| Type de Champ | Description |
|
||||
|---------------|-------------|
|
||||
| Partage avec les institutions | Champ dédié au partage de données avec des institutions |
|
||||
| Type de Champ (rôle) | Description |
|
||||
|----------------------|-------------|
|
||||
| Partage avec les institutions | Partage de données avec des institutions |
|
||||
| Messages au RSSI | Messages au RSSI de la société responsable du service |
|
||||
| Messages au Correspondant CNIL | Messages au Correspondant CNIL de la société responsable du service |
|
||||
| Messages au Responsable cybersécurité | Messages au Responsable cybersécurité au bord de la société responsable du service |
|
||||
| Messages de support infogérant | Messages de support de l'infogérant du service (peut inclure un membre du miner pour la gestion des clés API) |
|
||||
| Messages de support administrateur système | Messages de support de l’administrateur système du service |
|
||||
| Messages de support niveau 1 | Messages de support de niveau 1 du service |
|
||||
| Messages de support niveau 2 | Messages de support de niveau 2 du service |
|
||||
| Messages de support niveau 3 | Messages de support de niveau 3 du service |
|
||||
| Messages au Responsable cybersécurité | Messages au Responsable cybersécurité de la société responsable du service |
|
||||
| Messages de support infogérant | Support infogérant (peut inclure membre du miner pour clés API) |
|
||||
| Messages de support administrateur système | Support admin système du service |
|
||||
| Messages de support niveau 1 | Support N1 du service |
|
||||
| Messages de support niveau 2 | Support N2 du service |
|
||||
| Messages de support niveau 3 | Support N3 du service |
|
||||
| **Validation (contrat)** | Validation du contrat ; validateurs du contrat (`contrat.validateurs.membres_du_role`) |
|
||||
| **Validation du login** | Validation du login ; validateurs de l'action login (`action.validateurs_action.membres_du_role`) |
|
||||
|
||||
- Les `Champ` sont des `MessageAValider` avec `contrats_parents_uuid` (au moins 1).
|
||||
- L’identification du type (partage, RSSI, CNIL, cybersécurité, support N1/N2/N3, etc.) se fait via `types.types_names_chiffres` / `types_uuid` ou via des métadonnées dans `datajson`, selon les conventions du catalogue.
|
||||
- L’identification du type / rôle (partage, RSSI, CNIL, cybersécurité, support N1/N2/N3, validation, validation login, etc.) se fait via `types.types_names_chiffres` / `types_uuid` ou via des métadonnées dans `datajson`, selon les conventions du catalogue.
|
||||
|
||||
---
|
||||
|
||||
@ -34,9 +36,9 @@ Pour les objets concernés (ex. contrats, champs), la partie **`datajson`** peut
|
||||
|
||||
### 2.1 Usage et partage avec les tiers
|
||||
|
||||
- **Raisons de l’usage avec les tiers et description du tiers**
|
||||
- **Raisons de l’usage avec les tiers et description du tiers**
|
||||
Tableau de couples `[raisons, tiers]` : pour chaque usage avec un tiers, les raisons et la description du tiers.
|
||||
- **Raisons du partage avec les tiers et description du tiers**
|
||||
- **Raisons du partage avec les tiers et description du tiers**
|
||||
Tableau de couples `[raisons, tiers]` : pour chaque partage avec un tiers, les raisons et la description du tiers.
|
||||
|
||||
Structure suggérée (à préciser dans le catalogue) :
|
||||
@ -54,7 +56,7 @@ Structure suggérée (à préciser dans le catalogue) :
|
||||
|
||||
### 2.2 Conditions de conservation
|
||||
|
||||
- **Conditions de conservation**
|
||||
- **Conditions de conservation**
|
||||
Objet JSON contenant **au moins** le **délai d’expiration** (durée de conservation des données).
|
||||
|
||||
Exemple :
|
||||
@ -76,7 +78,7 @@ Exemple :
|
||||
|
||||
| Domaine | Contenu |
|
||||
|--------|---------|
|
||||
| Champs obligatoires | Sous-ensemble des 9 types (partage, RSSI, CNIL, cybersécurité, support infogérant / admin / N1 / N2 / N3) selon le contrat |
|
||||
| Champs obligatoires (rôles) | Sous-ensemble des 11 types : partage, RSSI, CNIL, cybersécurité, support infogérant / admin / N1 / N2 / N3, **validation (contrat)**, **validation du login** |
|
||||
| `datajson` CNIL | `raisons_usage_tiers`, `raisons_partage_tiers` (tableaux [raisons, tiers]) ; `conditions_conservation` (JSON avec au moins `delai_expiration`) |
|
||||
|
||||
---
|
||||
|
||||
@ -1,6 +1,6 @@
|
||||
# Synthèse structurée – UserWallet & API Relay
|
||||
|
||||
**Author:** Équipe 4NK
|
||||
**Author:** Équipe 4NK
|
||||
**Date:** 2026-01-26
|
||||
|
||||
## 1. userwallet/docs
|
||||
@ -15,7 +15,7 @@
|
||||
|
||||
Spécification du login décentralisé (secp256k1, contrats, pairing mFA, relais) :
|
||||
|
||||
**Complément** : `specs-champs-obligatoires-cnil.md` — champs obligatoires (partage, RSSI, CNIL, cybersécurité, support N1/N2/N3, etc.) et attributs CNIL dans `datajson` (`raisons_usage_tiers`, `raisons_partage_tiers`, `conditions_conservation`, `membre_miner_uuid` pour les champs infogérant). Voir `features/userwallet-membre-miner-infogerant.md`.
|
||||
**Complément** : `specs-champs-obligatoires-cnil.md` — champs obligatoires / rôles (partage, RSSI, CNIL, cybersécurité, support N1/N2/N3, validation contrat, validation login, etc.) et attributs CNIL dans `datajson` (`raisons_usage_tiers`, `raisons_partage_tiers`, `conditions_conservation`, `membre_miner_uuid` pour les champs infogérant). Voir `features/userwallet-membre-miner-infogerant.md`.
|
||||
|
||||
- **Modèle** : messages publiés sans signatures ni clés ; signatures et clés publiées séparément ; tout adressé par hash canonique ; récupération par **GET uniquement** (pull).
|
||||
- **Objets** : Service, Contrat, Champ, Action, ActionLogin, Membre, Pair, MessageBase, Hash, Signature, Validateurs, MsgChiffre, MsgSignature, MsgCle.
|
||||
|
||||
34
userwallet/features/userwallet-iframe-key-isolation.md
Normal file
34
userwallet/features/userwallet-iframe-key-isolation.md
Normal file
@ -0,0 +1,34 @@
|
||||
# UserWallet iframe — cloisonnement des clés et du chiffrement
|
||||
|
||||
**Objectif**
|
||||
|
||||
Garantir qu’aucune clé secrète ne quitte l’iframe UserWallet et qu’aucune opération de signature ou de chiffrement n’est effectuée en dehors du domaine de l’iframe.
|
||||
|
||||
**Règles**
|
||||
|
||||
1. **Clés**
|
||||
- Seules les **clés publiques** peuvent sortir de l’iframe (via postMessage vers le parent).
|
||||
- Les clés privées (`privateKey`, `cle_privee`) ne doivent jamais être envoyées au parent ni à un autre domaine.
|
||||
|
||||
2. **Crypto**
|
||||
- Toutes les opérations de **signature**, **chiffrement** et **déchiffrement** sont réalisées **uniquement dans l’iframe** (UserWallet).
|
||||
- Le parent (site intégrateur, ex. skeleton) ne doit **jamais** signer, chiffrer ni déchiffrer. Il peut uniquement **vérifier** des signatures à l’aide des clés publiques (ex. `verifyLoginProof`).
|
||||
|
||||
**Implémentation**
|
||||
|
||||
- **iframeChannel** : `sendToChannel` appelle `assertNoSecretsInChannelPayload` avant `postMessage`. Toute présence de `privateKey` ou `cle_privee` dans le payload (récursif) lève une erreur ; le message n’est pas envoyé.
|
||||
- **iframe.ts** : `sendMessageToParent` appelle aussi `assertNoSecretsInChannelPayload` sur le payload avant `postMessage`. Tous les envois vers le parent passent par cette vérification.
|
||||
- **Messages envoyés au parent** : `auth-response` (signature, publicKey, message), `login-proof` (challenge, signatures avec `cle_publique`), `error`, `service-status`, `pairing-relay-status` (pairingSatisfied, relayOk). Aucun de ces messages ne contient de clé secrète.
|
||||
- **Messages reçus du parent** : `auth-request`, `contract`. Le contrat peut contenir des `cle_publique` de service (validateurs) ; ce sont des clés publiques. Le parent n’envoie jamais de clé privée.
|
||||
- **Parent (intégrateur)** : Ne doit jamais signer, chiffrer ni déchiffrer. Il peut uniquement **vérifier** des signatures (ex. `verifyLoginProof` avec clés publiques). Ex. skeleton : vérification uniquement, pas de crypto secrète.
|
||||
|
||||
**Contrôles**
|
||||
|
||||
- Avant tout envoi vers le parent : vérification systématique des payloads pour `privateKey` et `cle_privee`.
|
||||
- Aucun usage de `privateKey` / `cle_privee` dans les types de messages définis pour le canal iframe (`AuthResponseMessage`, `LoginProofMessage`, etc.).
|
||||
|
||||
**Pages affectées**
|
||||
|
||||
- `userwallet/src/utils/iframeChannel.ts` (assertion, `sendToChannel`)
|
||||
- `userwallet/src/utils/iframe.ts` (assertion, `sendMessageToParent`)
|
||||
- `userwallet/features/userwallet-iframe-key-isolation.md` (ce document)
|
||||
@ -4,6 +4,7 @@ import { useIdentity } from '../hooks/useIdentity';
|
||||
import { isPairingSatisfied, hasRemotePair } from '../utils/pairing';
|
||||
import { usePairingConnected } from '../hooks/usePairingConnected';
|
||||
import { getStoredRelays } from '../utils/relay';
|
||||
import { isInIframe, sendToChannel } from '../utils/iframeChannel';
|
||||
import type { LocalIdentity } from '../types/identity';
|
||||
import { PairingSetupBlock } from './PairingSetupBlock';
|
||||
|
||||
@ -54,6 +55,19 @@ export function HomeScreen(): JSX.Element {
|
||||
logHomeStatus(identity, pairingSatisfied, relayStatus, relays.length);
|
||||
}, [identity, pairingSatisfied, relayStatus, relays.length]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!isInIframe()) {
|
||||
return;
|
||||
}
|
||||
sendToChannel({
|
||||
type: 'pairing-relay-status',
|
||||
payload: {
|
||||
pairingSatisfied,
|
||||
relayOk: relayStatus === 'OK',
|
||||
},
|
||||
});
|
||||
}, [pairingSatisfied, relayStatus]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!showSetupBlock || identity === null || hasScrolledAndLoggedRef.current) {
|
||||
if (!showSetupBlock) {
|
||||
|
||||
@ -9,6 +9,7 @@ import { getStoredRelays } from '../utils/relay';
|
||||
import { GraphResolver } from '../services/graphResolver';
|
||||
import { LoginBuilder } from '../services/loginBuilder';
|
||||
import { useChannel } from '../hooks/useChannel';
|
||||
import { isInIframe, sendToChannel } from '../utils/iframeChannel';
|
||||
import { isPairingSatisfied, getPairsForMember } from '../utils/pairing';
|
||||
import {
|
||||
buildAllowedPubkeys,
|
||||
@ -65,10 +66,31 @@ export function LoginScreen(): JSX.Element {
|
||||
} | null>(null);
|
||||
|
||||
const graphResolver = useState(() => new GraphResolver())[0];
|
||||
const pairingSatisfied = isPairingSatisfied();
|
||||
const relayOk = getStoredRelays().length > 0;
|
||||
|
||||
// Rediriger selon l'état de la machine à états
|
||||
useEffect(() => {
|
||||
if (!isInIframe()) {
|
||||
return;
|
||||
}
|
||||
sendToChannel({
|
||||
type: 'pairing-relay-status',
|
||||
payload: {
|
||||
pairingSatisfied,
|
||||
relayOk,
|
||||
},
|
||||
});
|
||||
}, [pairingSatisfied, relayOk]);
|
||||
|
||||
// Rediriger selon l'état de la machine à états. Si service+membre en URL (parent iframe),
|
||||
// ne pas rediriger vers /services; dispatcher E_SELECT_* pour aller en S_LOGIN_BUILD_PATH.
|
||||
useEffect(() => {
|
||||
if (loginState === 'S_LOGIN_SELECT_SERVICE') {
|
||||
if (serviceUuid !== '' && membreUuid !== '') {
|
||||
dispatch({ type: 'E_SELECT_SERVICE', serviceUuid });
|
||||
dispatch({ type: 'E_SELECT_MEMBER', membreUuid });
|
||||
return;
|
||||
}
|
||||
navigate('/services');
|
||||
return;
|
||||
}
|
||||
@ -80,7 +102,7 @@ export function LoginScreen(): JSX.Element {
|
||||
navigate('/services');
|
||||
return;
|
||||
}
|
||||
}, [loginState, serviceUuid, navigate]);
|
||||
}, [loginState, serviceUuid, membreUuid, navigate, dispatch]);
|
||||
|
||||
const handleBuildPath = useCallback(async (): Promise<void> => {
|
||||
if (serviceUuid === '' || membreUuid === '') {
|
||||
|
||||
@ -139,18 +139,63 @@ export function PairManagementScreen(): JSX.Element {
|
||||
}}
|
||||
>
|
||||
<p>
|
||||
<strong>Mots (8):</strong>
|
||||
<strong>Mots (17):</strong>
|
||||
</p>
|
||||
<p
|
||||
<div
|
||||
style={{
|
||||
fontFamily: 'monospace',
|
||||
fontSize: '1rem',
|
||||
wordBreak: 'break-word',
|
||||
margin: 0,
|
||||
display: 'flex',
|
||||
flexWrap: 'wrap',
|
||||
gap: '0.5rem',
|
||||
marginTop: '0.5rem',
|
||||
}}
|
||||
>
|
||||
{words.join(' ')}
|
||||
</p>
|
||||
{words.map((word, wordIndex) => {
|
||||
const visible = visibleWordIndices.get(pair.uuid)?.has(wordIndex) ?? false;
|
||||
return (
|
||||
<div
|
||||
key={wordIndex}
|
||||
style={{
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
gap: '0.25rem',
|
||||
padding: '0.25rem 0.5rem',
|
||||
backgroundColor: '#f5f5f5',
|
||||
borderRadius: '4px',
|
||||
border: '1px solid #ddd',
|
||||
}}
|
||||
>
|
||||
<span style={{ fontSize: '0.75rem', color: '#666', fontWeight: 'bold' }}>
|
||||
{wordIndex + 1}:
|
||||
</span>
|
||||
<span
|
||||
style={{
|
||||
fontFamily: 'monospace',
|
||||
fontSize: '0.9rem',
|
||||
minWidth: '80px',
|
||||
}}
|
||||
>
|
||||
{visible ? `${wordIndex + 1}. ${word}` : '•••••'}
|
||||
</span>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => handleToggleWordVisibility(pair.uuid, wordIndex)}
|
||||
style={{
|
||||
background: 'none',
|
||||
border: 'none',
|
||||
cursor: 'pointer',
|
||||
padding: '0.125rem',
|
||||
fontSize: '0.875rem',
|
||||
color: '#666',
|
||||
}}
|
||||
aria-label={`${visible ? 'Masquer' : 'Afficher'} le mot ${wordIndex + 1}`}
|
||||
title={`${visible ? 'Masquer' : 'Afficher'} le mot ${wordIndex + 1}`}
|
||||
>
|
||||
{visible ? '👁' : '👁🗨'}
|
||||
</button>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
<div style={{ marginTop: '0.75rem', display: 'flex', gap: '0.5rem' }}>
|
||||
|
||||
@ -23,6 +23,19 @@ export function PairingDisplayScreen(): JSX.Element {
|
||||
const [success, setSuccess] = useState(false);
|
||||
const [isConfirming, setIsConfirming] = useState(false);
|
||||
const [justConnected, setJustConnected] = useState(false);
|
||||
const [visibleWordIndices, setVisibleWordIndices] = useState<Set<number>>(new Set());
|
||||
|
||||
const toggleWordVisibility = (index: number): void => {
|
||||
setVisibleWordIndices((prev) => {
|
||||
const next = new Set(prev);
|
||||
if (next.has(index)) {
|
||||
next.delete(index);
|
||||
} else {
|
||||
next.add(index);
|
||||
}
|
||||
return next;
|
||||
});
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
if (identity !== null && identity.publicKey !== undefined) {
|
||||
@ -106,7 +119,6 @@ export function PairingDisplayScreen(): JSX.Element {
|
||||
|
||||
if (success) {
|
||||
const showConnected = pairingConnected || justConnected;
|
||||
const words2ndText = words2nd.length > 0 ? words2nd.join(' ') : '—';
|
||||
return (
|
||||
<main>
|
||||
{showConnected && (
|
||||
@ -134,44 +146,63 @@ export function PairingDisplayScreen(): JSX.Element {
|
||||
<h2 id="words-2nd-heading" style={{ marginTop: 0, marginBottom: '0.5rem' }}>
|
||||
Mots du 2ᵉ appareil — à copier sur le 1ᵉʳ
|
||||
</h2>
|
||||
<p
|
||||
aria-label="Mots 2e appareil"
|
||||
style={{
|
||||
fontFamily: 'monospace',
|
||||
fontSize: '1.1rem',
|
||||
wordBreak: 'break-word',
|
||||
margin: 0,
|
||||
padding: '0.75rem',
|
||||
backgroundColor: 'var(--color-background)',
|
||||
color: 'var(--color-text)',
|
||||
borderRadius: '4px',
|
||||
border: '1px solid var(--color-info-border, #93c5fd)',
|
||||
}}
|
||||
>
|
||||
{words2ndText}
|
||||
</p>
|
||||
{identity !== null && (
|
||||
<>
|
||||
<h3 id="pubkey-2nd-heading" style={{ marginTop: '1rem', marginBottom: '0.5rem' }}>
|
||||
Clé publique de ce dispositif — à copier sur le 1ᵉʳ (hex)
|
||||
</h3>
|
||||
<p
|
||||
aria-label="Clé publique 2e appareil"
|
||||
style={{
|
||||
fontFamily: 'monospace',
|
||||
fontSize: '0.9rem',
|
||||
wordBreak: 'break-all',
|
||||
margin: 0,
|
||||
padding: '0.5rem',
|
||||
backgroundColor: 'var(--color-background)',
|
||||
color: 'var(--color-text)',
|
||||
borderRadius: '4px',
|
||||
border: '1px solid var(--color-info-border, #93c5fd)',
|
||||
}}
|
||||
>
|
||||
{identity.publicKey}
|
||||
</p>
|
||||
</>
|
||||
{words2nd.length > 0 ? (
|
||||
<div
|
||||
aria-label="Mots 2e appareil"
|
||||
style={{
|
||||
display: 'flex',
|
||||
flexWrap: 'wrap',
|
||||
gap: '0.5rem',
|
||||
fontFamily: 'monospace',
|
||||
fontSize: '1rem',
|
||||
marginTop: '0.5rem',
|
||||
padding: '0.75rem',
|
||||
backgroundColor: 'var(--color-background)',
|
||||
borderRadius: '4px',
|
||||
border: '1px solid var(--color-info-border, #93c5fd)',
|
||||
}}
|
||||
>
|
||||
{words2nd.map((word, index) => {
|
||||
const visible = visibleWordIndices.has(index);
|
||||
return (
|
||||
<div
|
||||
key={index}
|
||||
style={{
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
gap: '0.25rem',
|
||||
padding: '0.25rem 0.5rem',
|
||||
backgroundColor: 'var(--color-info-bg, #dbeafe)',
|
||||
borderRadius: '4px',
|
||||
border: '1px solid var(--color-info-border, #93c5fd)',
|
||||
}}
|
||||
>
|
||||
<span style={{ fontWeight: 'bold' }}>{index + 1}.</span>
|
||||
<span style={{ minWidth: '80px' }}>
|
||||
{visible ? word : '•••••'}
|
||||
</span>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => toggleWordVisibility(index)}
|
||||
style={{
|
||||
background: 'none',
|
||||
border: 'none',
|
||||
cursor: 'pointer',
|
||||
padding: '0.125rem',
|
||||
fontSize: '0.875rem',
|
||||
color: 'var(--color-text-secondary, #666)',
|
||||
}}
|
||||
aria-label={`${visible ? 'Masquer' : 'Afficher'} le mot ${index + 1}`}
|
||||
title={`${visible ? 'Masquer' : 'Afficher'} le mot ${index + 1}`}
|
||||
>
|
||||
{visible ? '👁' : '👁🗨'}
|
||||
</button>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
) : (
|
||||
<p style={{ margin: 0, padding: '0.75rem' }}>—</p>
|
||||
)}
|
||||
</div>
|
||||
<p>
|
||||
|
||||
@ -28,6 +28,19 @@ export function PairingSetupBlock(): JSX.Element {
|
||||
const [remoteError, setRemoteError] = useState<string | null>(null);
|
||||
const [hasCopiedToSecondDevice, setHasCopiedToSecondDevice] = useState(false);
|
||||
const [isConfirming, setIsConfirming] = useState(false);
|
||||
const [visibleWordIndices, setVisibleWordIndices] = useState<Set<number>>(new Set());
|
||||
|
||||
const toggleWordVisibility = (index: number): void => {
|
||||
setVisibleWordIndices((prev) => {
|
||||
const next = new Set(prev);
|
||||
if (next.has(index)) {
|
||||
next.delete(index);
|
||||
} else {
|
||||
next.add(index);
|
||||
}
|
||||
return next;
|
||||
});
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
if (identity !== null && identity.publicKey !== undefined) {
|
||||
@ -115,12 +128,56 @@ export function PairingSetupBlock(): JSX.Element {
|
||||
<p>
|
||||
<strong>Mots du 1ᵉʳ appareil</strong> — à saisir sur le 2ᵉ (QR) :
|
||||
</p>
|
||||
<p
|
||||
<div
|
||||
aria-label="Mots 1er appareil"
|
||||
style={{ fontFamily: 'monospace', fontSize: '1rem', wordBreak: 'break-word' }}
|
||||
style={{
|
||||
display: 'flex',
|
||||
flexWrap: 'wrap',
|
||||
gap: '0.5rem',
|
||||
fontFamily: 'monospace',
|
||||
fontSize: '1rem',
|
||||
marginTop: '0.5rem',
|
||||
}}
|
||||
>
|
||||
{words.join(' ')}
|
||||
</p>
|
||||
{words.map((word, index) => {
|
||||
const visible = visibleWordIndices.has(index);
|
||||
return (
|
||||
<div
|
||||
key={index}
|
||||
style={{
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
gap: '0.25rem',
|
||||
padding: '0.25rem 0.5rem',
|
||||
backgroundColor: 'var(--color-info-bg, #e8f4fd)',
|
||||
borderRadius: '4px',
|
||||
border: '1px solid var(--color-info-border, #93c5fd)',
|
||||
}}
|
||||
>
|
||||
<span style={{ fontWeight: 'bold' }}>{index + 1}.</span>
|
||||
<span style={{ minWidth: '80px' }}>
|
||||
{visible ? word : '•••••'}
|
||||
</span>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => toggleWordVisibility(index)}
|
||||
style={{
|
||||
background: 'none',
|
||||
border: 'none',
|
||||
cursor: 'pointer',
|
||||
padding: '0.125rem',
|
||||
fontSize: '0.875rem',
|
||||
color: 'var(--color-text-secondary, #666)',
|
||||
}}
|
||||
aria-label={`${visible ? 'Masquer' : 'Afficher'} le mot ${index + 1}`}
|
||||
title={`${visible ? 'Masquer' : 'Afficher'} le mot ${index + 1}`}
|
||||
>
|
||||
{visible ? '👁' : '👁🗨'}
|
||||
</button>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
{qrDataUrl !== null && (
|
||||
<p>
|
||||
<img
|
||||
|
||||
@ -1,6 +1,6 @@
|
||||
import { useState, useEffect } from 'react';
|
||||
import { useNavigate } from 'react-router-dom';
|
||||
import { getStoredRelays, storeRelays, testRelay } from '../utils/relay';
|
||||
import { getStoredRelays, storeRelays, testRelay, MAX_RELAYS } from '../utils/relay';
|
||||
import type { RelayConfig } from '../types/identity';
|
||||
|
||||
export function RelaySettingsScreen(): JSX.Element {
|
||||
@ -17,6 +17,9 @@ export function RelaySettingsScreen(): JSX.Element {
|
||||
if (newEndpoint.trim() === '') {
|
||||
return;
|
||||
}
|
||||
if (relays.length >= MAX_RELAYS) {
|
||||
return;
|
||||
}
|
||||
const newRelay: RelayConfig = {
|
||||
endpoint: newEndpoint.trim(),
|
||||
priority: relays.length,
|
||||
@ -102,6 +105,9 @@ export function RelaySettingsScreen(): JSX.Element {
|
||||
</section>
|
||||
<section aria-labelledby="add-relay">
|
||||
<h2 id="add-relay">Ajouter un relais</h2>
|
||||
<p>
|
||||
Maximum {MAX_RELAYS} relais. Connectez-vous au maximum pour dupliquer les dépôts et les flux.
|
||||
</p>
|
||||
<div>
|
||||
<input
|
||||
type="text"
|
||||
@ -109,14 +115,22 @@ export function RelaySettingsScreen(): JSX.Element {
|
||||
onChange={(e) => {
|
||||
setNewEndpoint(e.target.value);
|
||||
}}
|
||||
placeholder="http://relay.example.com:3019"
|
||||
placeholder="https://relay.example.com"
|
||||
onKeyDown={(e) => {
|
||||
if (e.key === 'Enter') {
|
||||
handleAdd();
|
||||
}
|
||||
}}
|
||||
disabled={relays.length >= MAX_RELAYS}
|
||||
/>
|
||||
<button onClick={handleAdd}>Ajouter</button>
|
||||
<button onClick={handleAdd} disabled={relays.length >= MAX_RELAYS}>
|
||||
Ajouter
|
||||
</button>
|
||||
{relays.length >= MAX_RELAYS && (
|
||||
<span style={{ marginLeft: '0.5rem', color: 'var(--color-text-secondary, #666)' }}>
|
||||
Limite de {MAX_RELAYS} relais atteinte.
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
</section>
|
||||
<div>
|
||||
|
||||
@ -39,6 +39,7 @@ export function WordInputGrid({
|
||||
const [focusedIndex, setFocusedIndex] = useState<number | null>(null);
|
||||
const [suggestions, setSuggestions] = useState<string[]>([]);
|
||||
const [selectedSuggestionIndex, setSelectedSuggestionIndex] = useState<number>(-1);
|
||||
const [visibleWords, setVisibleWords] = useState<Set<number>>(new Set());
|
||||
const inputRefs = useRef<(HTMLInputElement | null)[]>([]);
|
||||
const suggestionsRef = useRef<HTMLDivElement | null>(null);
|
||||
|
||||
@ -219,7 +220,7 @@ export function WordInputGrid({
|
||||
inputRefs.current[index] = el;
|
||||
}}
|
||||
id={`${id ?? 'word'}-${index}`}
|
||||
type="password"
|
||||
type={visibleWords.has(index) ? 'text' : 'password'}
|
||||
value={word}
|
||||
onChange={(e: ChangeEvent<HTMLInputElement>) => {
|
||||
handleInputChange(index, e.target.value);
|
||||
@ -241,7 +242,7 @@ export function WordInputGrid({
|
||||
style={{
|
||||
width: '100%',
|
||||
padding: '0.5rem',
|
||||
paddingRight: '2.5rem',
|
||||
paddingRight: word.length > 0 ? '2.5rem' : '0.5rem',
|
||||
fontSize: '1rem',
|
||||
fontFamily: 'monospace',
|
||||
border: '1px solid var(--color-border, #ccc)',
|
||||
@ -254,10 +255,15 @@ export function WordInputGrid({
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => {
|
||||
const input = inputRefs.current[index];
|
||||
if (input !== null) {
|
||||
input.type = input.type === 'password' ? 'text' : 'password';
|
||||
}
|
||||
setVisibleWords((prev) => {
|
||||
const next = new Set(prev);
|
||||
if (next.has(index)) {
|
||||
next.delete(index);
|
||||
} else {
|
||||
next.add(index);
|
||||
}
|
||||
return next;
|
||||
});
|
||||
}}
|
||||
style={{
|
||||
position: 'absolute',
|
||||
@ -271,10 +277,10 @@ export function WordInputGrid({
|
||||
fontSize: '0.875rem',
|
||||
color: 'var(--color-text-secondary, #666)',
|
||||
}}
|
||||
aria-label={`${word.length > 0 ? 'Afficher' : 'Masquer'} le mot ${index + 1}`}
|
||||
title={`${word.length > 0 ? 'Afficher' : 'Masquer'} le mot ${index + 1}`}
|
||||
aria-label={`${visibleWords.has(index) ? 'Masquer' : 'Afficher'} le mot ${index + 1}`}
|
||||
title={`${visibleWords.has(index) ? 'Masquer' : 'Afficher'} le mot ${index + 1}`}
|
||||
>
|
||||
👁
|
||||
{visibleWords.has(index) ? '👁' : '👁🗨'}
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
|
||||
@ -1,4 +1,5 @@
|
||||
import { useEffect, useCallback, useState } from 'react';
|
||||
import { useNavigate } from 'react-router-dom';
|
||||
import {
|
||||
listenToChannel,
|
||||
sendToChannel,
|
||||
@ -15,6 +16,7 @@ import type { LoginProof } from '../types/identity';
|
||||
import type { Contrat, Action } from '../types/contract';
|
||||
|
||||
export function useChannel() {
|
||||
const navigate = useNavigate();
|
||||
const { identity } = useIdentity();
|
||||
const graphResolver = useState(() => {
|
||||
return new GraphResolver();
|
||||
@ -121,6 +123,29 @@ export function useChannel() {
|
||||
[graphResolver],
|
||||
);
|
||||
|
||||
const handleNavigateLogin = useCallback(
|
||||
(message: ChannelMessage): void => {
|
||||
if (message.type !== 'navigate-login') {
|
||||
return;
|
||||
}
|
||||
const payload = message.payload as { service?: string; member?: string };
|
||||
const service =
|
||||
typeof payload?.service === 'string' ? payload.service : '';
|
||||
const member =
|
||||
typeof payload?.member === 'string' ? payload.member : '';
|
||||
const params = new URLSearchParams();
|
||||
if (service !== '') {
|
||||
params.set('service', service);
|
||||
}
|
||||
if (member !== '') {
|
||||
params.set('membre', member);
|
||||
}
|
||||
const qs = params.toString();
|
||||
navigate(qs !== '' ? `/login?${qs}` : '/login');
|
||||
},
|
||||
[navigate],
|
||||
);
|
||||
|
||||
const sendLoginProof = useCallback((proof: LoginProof): void => {
|
||||
sendToChannel({
|
||||
type: 'login-proof',
|
||||
@ -138,11 +163,13 @@ export function useChannel() {
|
||||
handleAuthRequest(message as AuthRequestMessage);
|
||||
} else if (message.type === 'contract') {
|
||||
handleContract(message as ContractMessage);
|
||||
} else if (message.type === 'navigate-login') {
|
||||
handleNavigateLogin(message);
|
||||
}
|
||||
});
|
||||
|
||||
return cleanup;
|
||||
}, [handleAuthRequest, handleContract]);
|
||||
}, [handleAuthRequest, handleContract, handleNavigateLogin]);
|
||||
|
||||
return {
|
||||
sendLoginProof,
|
||||
|
||||
@ -12,6 +12,7 @@ import type {
|
||||
|
||||
/**
|
||||
* Update graph cache with decrypted message.
|
||||
* Message must have types.types_names_chiffres (MessageBase). Skips otherwise.
|
||||
*/
|
||||
export function updateGraphFromMessage(
|
||||
decrypted: unknown,
|
||||
@ -19,7 +20,15 @@ export function updateGraphFromMessage(
|
||||
): void {
|
||||
try {
|
||||
const msg = decrypted as MessageBase;
|
||||
const typeNames = msg.types.types_names_chiffres.toLowerCase();
|
||||
const raw = msg?.types?.types_names_chiffres;
|
||||
if (typeof raw !== 'string' || raw.length === 0) {
|
||||
console.warn(
|
||||
'[SyncService] message missing types.types_names_chiffres, skipped',
|
||||
{ uuid: (msg as { uuid?: string })?.uuid },
|
||||
);
|
||||
return;
|
||||
}
|
||||
const typeNames = raw.toLowerCase();
|
||||
|
||||
if (typeNames.includes('service')) {
|
||||
graphResolver.addService(msg as unknown as Service);
|
||||
|
||||
@ -1,13 +1,21 @@
|
||||
import {
|
||||
etc as secpEtc,
|
||||
getPublicKey,
|
||||
sign,
|
||||
Signature,
|
||||
utils as secpUtils,
|
||||
verify,
|
||||
} from '@noble/secp256k1';
|
||||
import { hmac } from '@noble/hashes/hmac';
|
||||
import { sha256 } from '@noble/hashes/sha256';
|
||||
import { bytesToHex, hexToBytes } from '@noble/hashes/utils';
|
||||
|
||||
// Require hmacSha256Sync for secp256k1 sign() (RFC6979). Set once at module load.
|
||||
if (secpEtc.hmacSha256Sync === undefined) {
|
||||
secpEtc.hmacSha256Sync = (k: Uint8Array, ...m: Uint8Array[]): Uint8Array =>
|
||||
hmac(sha256, k, secpEtc.concatBytes(...m));
|
||||
}
|
||||
|
||||
export interface KeyPair {
|
||||
privateKey: string;
|
||||
publicKey: string;
|
||||
|
||||
@ -1,3 +1,4 @@
|
||||
import { assertNoSecretsInChannelPayload } from './iframeChannel';
|
||||
import type {
|
||||
IframeMessage,
|
||||
AuthResponseMessage,
|
||||
@ -8,12 +9,14 @@ import type {
|
||||
/**
|
||||
* Sends a message to the parent window if running in an iframe.
|
||||
* Uses postMessage API for cross-origin communication.
|
||||
* Ensures no private keys or secrets leave the iframe (key isolation).
|
||||
*/
|
||||
export function sendMessageToParent(message: IframeMessage): void {
|
||||
if (window.parent === window) {
|
||||
console.warn('Not in iframe context, message not sent');
|
||||
return;
|
||||
}
|
||||
assertNoSecretsInChannelPayload(message.payload);
|
||||
window.parent.postMessage(message, '*');
|
||||
}
|
||||
|
||||
|
||||
@ -5,10 +5,26 @@ import type { LoginProof } from '../types/identity';
|
||||
* Messages pour la communication iframe avec Channel Messages.
|
||||
*/
|
||||
export interface ChannelMessage {
|
||||
type: 'auth-request' | 'auth-response' | 'login-proof' | 'service-status' | 'error' | 'contract';
|
||||
type:
|
||||
| 'auth-request'
|
||||
| 'auth-response'
|
||||
| 'login-proof'
|
||||
| 'service-status'
|
||||
| 'error'
|
||||
| 'contract'
|
||||
| 'pairing-relay-status'
|
||||
| 'navigate-login';
|
||||
payload?: unknown;
|
||||
}
|
||||
|
||||
export interface PairingRelayStatusMessage extends ChannelMessage {
|
||||
type: 'pairing-relay-status';
|
||||
payload: {
|
||||
pairingSatisfied: boolean;
|
||||
relayOk: boolean;
|
||||
};
|
||||
}
|
||||
|
||||
export interface AuthRequestMessage extends ChannelMessage {
|
||||
type: 'auth-request';
|
||||
payload: {
|
||||
@ -54,14 +70,49 @@ export interface ContractMessage extends ChannelMessage {
|
||||
};
|
||||
}
|
||||
|
||||
const FORBIDDEN_PAYLOAD_KEYS = ['privateKey', 'cle_privee'] as const;
|
||||
|
||||
/**
|
||||
* Recursively check that no secret keys (privateKey, cle_privee) are present
|
||||
* in a postMessage-to-parent payload. Throws if any forbidden key is found.
|
||||
* Use for any postMessage to parent (iframeChannel, iframe.ts). Key isolation.
|
||||
*/
|
||||
export function assertNoSecretsInChannelPayload(
|
||||
value: unknown,
|
||||
path = 'payload',
|
||||
): void {
|
||||
if (value === null || value === undefined) {
|
||||
return;
|
||||
}
|
||||
if (typeof value === 'object' && !Array.isArray(value)) {
|
||||
const obj = value as Record<string, unknown>;
|
||||
for (const k of Object.keys(obj)) {
|
||||
if (FORBIDDEN_PAYLOAD_KEYS.includes(k as (typeof FORBIDDEN_PAYLOAD_KEYS)[number])) {
|
||||
throw new Error(
|
||||
`[iframeChannel] Forbidden key "${k}" in channel ${path}: no secrets may leave iframe`,
|
||||
);
|
||||
}
|
||||
assertNoSecretsInChannelPayload(obj[k], `${path}.${k}`);
|
||||
}
|
||||
return;
|
||||
}
|
||||
if (Array.isArray(value)) {
|
||||
value.forEach((item, i) =>
|
||||
assertNoSecretsInChannelPayload(item, `${path}[${i}]`),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Send message to parent window (Channel Messages).
|
||||
* Ensures no private keys or secrets leave the iframe (key isolation).
|
||||
*/
|
||||
export function sendToChannel(message: ChannelMessage): void {
|
||||
if (window.parent === window) {
|
||||
console.warn('Not in iframe context, message not sent');
|
||||
return;
|
||||
}
|
||||
assertNoSecretsInChannelPayload(message.payload);
|
||||
window.parent.postMessage(message, '*');
|
||||
}
|
||||
|
||||
|
||||
@ -4,19 +4,63 @@ import type { MsgChiffre, MsgSignature, MsgCle } from '../types/message';
|
||||
/** Timeout for relay fetch (GET/POST). X_RELAY_TIMEOUT when exceeded. */
|
||||
export const RELAY_FETCH_TIMEOUT_MS = 15000;
|
||||
|
||||
/** Maximum number of relays that can be configured. */
|
||||
export const MAX_RELAYS = 8;
|
||||
|
||||
/**
|
||||
* Default relay endpoint.
|
||||
*/
|
||||
const DEFAULT_RELAY_ENDPOINT = 'https://relay.certificator.4nkweb.com';
|
||||
|
||||
/**
|
||||
* Get stored relay configurations.
|
||||
* Initializes with default relay if none are configured.
|
||||
*/
|
||||
export function getStoredRelays(): RelayConfig[] {
|
||||
try {
|
||||
const stored = localStorage.getItem('userwallet_relays');
|
||||
if (stored === null) {
|
||||
return [];
|
||||
// Initialize with default relay
|
||||
const defaultRelays: RelayConfig[] = [
|
||||
{
|
||||
endpoint: DEFAULT_RELAY_ENDPOINT,
|
||||
priority: 0,
|
||||
enabled: true,
|
||||
},
|
||||
];
|
||||
storeRelays(defaultRelays);
|
||||
return defaultRelays;
|
||||
}
|
||||
return JSON.parse(stored) as RelayConfig[];
|
||||
const raw = JSON.parse(stored) as RelayConfig[];
|
||||
// If empty, initialize with default relay
|
||||
if (raw.length === 0) {
|
||||
const defaultRelays: RelayConfig[] = [
|
||||
{
|
||||
endpoint: DEFAULT_RELAY_ENDPOINT,
|
||||
priority: 0,
|
||||
enabled: true,
|
||||
},
|
||||
];
|
||||
storeRelays(defaultRelays);
|
||||
return defaultRelays;
|
||||
}
|
||||
const relays = raw.slice(0, MAX_RELAYS);
|
||||
if (raw.length > MAX_RELAYS) {
|
||||
storeRelays(relays);
|
||||
}
|
||||
return relays;
|
||||
} catch (error) {
|
||||
console.error('Error reading stored relays:', error);
|
||||
return [];
|
||||
// Initialize with default relay on error
|
||||
const defaultRelays: RelayConfig[] = [
|
||||
{
|
||||
endpoint: DEFAULT_RELAY_ENDPOINT,
|
||||
priority: 0,
|
||||
enabled: true,
|
||||
},
|
||||
];
|
||||
storeRelays(defaultRelays);
|
||||
return defaultRelays;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
5
website-data/.gitignore
vendored
Normal file
5
website-data/.gitignore
vendored
Normal file
@ -0,0 +1,5 @@
|
||||
node_modules
|
||||
dist
|
||||
*.local
|
||||
.env.private
|
||||
.env.backup
|
||||
84
website-data/README.md
Normal file
84
website-data/README.md
Normal file
@ -0,0 +1,84 @@
|
||||
# website-data
|
||||
|
||||
Iframe dédiée à la **data** (données non clés) à `data.certificator.4nkweb.com`. Elle communique avec le **site** (parent) et avec **UserWallet** (clés, relais).
|
||||
|
||||
## Architecture
|
||||
|
||||
```
|
||||
site <-> data (+notifications) <-> userwallet (sur les relais)
|
||||
```
|
||||
|
||||
- **Site** : intègre l’iframe Data, envoie `contract`, `auth-request`, `data-request`, etc.
|
||||
- **Data** : iframe dédiée aux données (hors clés). Relaye contrat / auth vers UserWallet ; reçoit `login-proof`, `auth-response`, etc. et les transmet au site. Émet des `notification` (ex. `userwallet:login-proof`).
|
||||
- **UserWallet** : embarquée par Data, gère clés et relais (sync, signatures). Tourne sur les relais (api-relay).
|
||||
|
||||
Data n’héberge **aucune clé** ; toute la crypto reste dans UserWallet.
|
||||
|
||||
## Prérequis
|
||||
|
||||
- **UserWallet** : servi sur l’URL configurée (voir ci‑dessous).
|
||||
|
||||
## Installation
|
||||
|
||||
```bash
|
||||
cd website-data
|
||||
npm install
|
||||
npm run build
|
||||
```
|
||||
|
||||
## Développement
|
||||
|
||||
```bash
|
||||
npm run dev
|
||||
```
|
||||
|
||||
Ouvre par défaut sur `http://localhost:3025`. En standalone, la page indique qu’il faut l’embedder dans un parent pour le flux site <-> data <-> userwallet.
|
||||
|
||||
## Configuration
|
||||
|
||||
- **UserWallet** : `src/config.ts`, `USERWALLET_ORIGIN`. En dev : `http://localhost:3018`. En prod : `https://userwallet.certificator.4nkweb.com`. Override : `VITE_USERWALLET_ORIGIN`.
|
||||
- **Data** : `VITE_DATA_ORIGIN` pour la cible postMessage depuis le site (dev `http://localhost:3025`, prod `https://data.certificator.4nkweb.com`).
|
||||
|
||||
## Messages postMessage
|
||||
|
||||
### Site → Data
|
||||
|
||||
| Type | Rôle |
|
||||
|------|------|
|
||||
| `contract` | Contrat + contrats_fils + actions → transmis à UserWallet |
|
||||
| `auth-request` | Demande d’auth → transmise à UserWallet |
|
||||
| `data-request` | Requête data (réponse `data-response`) |
|
||||
| `ping` | Réponse `pong` |
|
||||
|
||||
### Data → Site
|
||||
|
||||
| Type | Rôle |
|
||||
|------|------|
|
||||
| `data-response` | Réponse à `data-request` |
|
||||
| `notification` | Événements (ex. `userwallet:login-proof`, `userwallet:auth-response`, `userwallet:error`) |
|
||||
| `pong` | Réponse à `ping` |
|
||||
| `error` | Erreur |
|
||||
| `login-proof`, `auth-response`, `service-status` | Relayés depuis UserWallet |
|
||||
|
||||
### Data ↔ UserWallet
|
||||
|
||||
Data embarque UserWallet en iframe. Elle envoie `contract`, `auth-request` et reçoit `login-proof`, `auth-response`, `error`, `service-status`, qu’elle relaye au site.
|
||||
|
||||
## Notifications
|
||||
|
||||
Data envoie des `notification` au site avec `payload: { kind, detail }`, notamment :
|
||||
|
||||
- `userwallet:login-proof`, `userwallet:auth-response`, `userwallet:error` lors des événements UserWallet.
|
||||
- Un flux **relay** (non implémenté) pourra alimenter des `notification` à partir des relais (data non clés).
|
||||
|
||||
## Production
|
||||
|
||||
- **Domaine** : `data.certificator.4nkweb.com`
|
||||
- **Port** : 3025 (configurable dans `vite.config.ts`).
|
||||
- **Déploiement** : build `dist/`, servir via Nginx / systemd comme pour `website-skeleton`. Voir `docs/` et infra 4NK pour proxy et certificats.
|
||||
|
||||
## Fichiers principaux
|
||||
|
||||
- `src/config.ts` : origines Data et UserWallet.
|
||||
- `src/channel.ts` : types et helpers postMessage (site <-> data, data <-> userwallet).
|
||||
- `src/main.ts` : init, relay site/userwallet, notifications.
|
||||
27
website-data/index.html
Normal file
27
website-data/index.html
Normal file
@ -0,0 +1,27 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="fr">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<title>Data – data.certificator.4nkweb.com</title>
|
||||
<style>
|
||||
* { box-sizing: border-box; }
|
||||
body {
|
||||
font-family: system-ui, -apple-system, sans-serif;
|
||||
margin: 0;
|
||||
padding: 1rem;
|
||||
line-height: 1.5;
|
||||
background: #f8f9fa;
|
||||
}
|
||||
h1 { font-size: 1.25rem; margin: 0 0 1rem; }
|
||||
.status { padding: 0.5rem; border-radius: 6px; background: #e9ecef; font-size: 0.9rem; }
|
||||
.status.in-iframe { background: #d1e7dd; }
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<h1>Data iframe</h1>
|
||||
<p class="status" id="status">Chargement…</p>
|
||||
<div id="userwallet-container" style="margin-top:1rem;"></div>
|
||||
<script type="module" src="/src/main.ts"></script>
|
||||
</body>
|
||||
</html>
|
||||
29
website-data/install-website-data.sh
Executable file
29
website-data/install-website-data.sh
Executable file
@ -0,0 +1,29 @@
|
||||
#!/bin/bash
|
||||
# Install and run website-data on this machine (backend port 3025).
|
||||
# Usage: ./install-website-data.sh
|
||||
# Then run update-proxy-nginx.sh from repo root for proxy + certificate (data.certificator.4nkweb.com).
|
||||
|
||||
set -e
|
||||
|
||||
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
|
||||
cd "$SCRIPT_DIR"
|
||||
|
||||
echo "=== Installation website-data ==="
|
||||
|
||||
if [ ! -d node_modules ]; then
|
||||
echo "Installation des dépendances..."
|
||||
npm install
|
||||
fi
|
||||
|
||||
echo "Build..."
|
||||
npm run build
|
||||
|
||||
echo "Installation du service systemd..."
|
||||
sudo sed "s|/srv/4NK/data.certificator.4nkweb.com|${SCRIPT_DIR}|g" website-data.service > /tmp/website-data.service
|
||||
sudo cp /tmp/website-data.service /etc/systemd/system/
|
||||
sudo systemctl daemon-reload
|
||||
sudo systemctl enable website-data
|
||||
sudo systemctl start website-data
|
||||
|
||||
echo "=== website-data installé et démarré (port 3025) ==="
|
||||
echo "Pour proxy + certificat: ./update-proxy-nginx.sh (depuis la racine du dépôt)"
|
||||
1002
website-data/package-lock.json
generated
Normal file
1002
website-data/package-lock.json
generated
Normal file
File diff suppressed because it is too large
Load Diff
16
website-data/package.json
Normal file
16
website-data/package.json
Normal file
@ -0,0 +1,16 @@
|
||||
{
|
||||
"name": "website-data",
|
||||
"version": "1.0.0",
|
||||
"description": "Data iframe (data.certificator.4nkweb.com). Non-key data; communicates with site and UserWallet (relays).",
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"dev": "vite",
|
||||
"build": "vite build",
|
||||
"preview": "vite preview",
|
||||
"type-check": "tsc --noEmit"
|
||||
},
|
||||
"devDependencies": {
|
||||
"typescript": "^5.2.2",
|
||||
"vite": "^5.0.8"
|
||||
}
|
||||
}
|
||||
99
website-data/src/channel.ts
Normal file
99
website-data/src/channel.ts
Normal file
@ -0,0 +1,99 @@
|
||||
/**
|
||||
* postMessage channel: Site <-> Data (+notifications), Data <-> UserWallet (relays).
|
||||
* Data does not hold keys; all crypto stays in UserWallet.
|
||||
*/
|
||||
|
||||
/** Messages from Site (parent) to Data iframe. */
|
||||
export type SiteToDataMessage =
|
||||
| { type: 'data-request'; payload?: { id?: string; query?: unknown } }
|
||||
| { type: 'contract'; payload?: { contrat?: unknown; contrats_fils?: unknown[]; actions?: unknown[] } }
|
||||
| { type: 'auth-request'; payload?: { serviceId: string } }
|
||||
| { type: 'ping'; payload?: unknown };
|
||||
|
||||
/** Messages from Data iframe to Site (parent). */
|
||||
export type DataToSiteMessage =
|
||||
| { type: 'data-response'; payload: { id?: string; data?: unknown; error?: string } }
|
||||
| { type: 'notification'; payload: { kind: string; detail?: unknown } }
|
||||
| { type: 'pong'; payload?: unknown }
|
||||
| { type: 'error'; payload: { message: string; code?: string } }
|
||||
| { type: 'login-proof'; payload: unknown }
|
||||
| { type: 'auth-response'; payload: unknown }
|
||||
| { type: 'service-status'; payload: unknown };
|
||||
|
||||
/** Messages Data -> UserWallet (when Data embeds UserWallet). Forwards contract, auth-request, etc. */
|
||||
export type DataToUserWalletMessage =
|
||||
| { type: 'contract'; payload: { contrat?: unknown; contrats_fils?: unknown[]; actions?: unknown[] } }
|
||||
| { type: 'auth-request'; payload: { serviceId: string } };
|
||||
|
||||
/** Messages UserWallet -> Data. login-proof, auth-response, error. */
|
||||
export type UserWalletToDataMessage =
|
||||
| { type: 'login-proof'; payload: unknown }
|
||||
| { type: 'auth-response'; payload: unknown }
|
||||
| { type: 'error'; payload: { message: string; code?: string } }
|
||||
| { type: 'service-status'; payload: unknown };
|
||||
|
||||
export function isInIframe(): boolean {
|
||||
return typeof window !== 'undefined' && window.parent !== window;
|
||||
}
|
||||
|
||||
/**
|
||||
* Send message to parent (Site). Use when Data runs as iframe.
|
||||
*/
|
||||
export function sendToParent<T extends DataToSiteMessage>(message: T, targetOrigin = '*'): void {
|
||||
if (typeof window === 'undefined' || window.parent === window) {
|
||||
return;
|
||||
}
|
||||
window.parent.postMessage(message, targetOrigin);
|
||||
}
|
||||
|
||||
/**
|
||||
* Listen for messages from parent (Site). Only invokes handler when event.source === window.parent.
|
||||
*/
|
||||
export function listenToParent(
|
||||
handler: (event: MessageEvent) => void,
|
||||
): () => void {
|
||||
const wrapped = (event: MessageEvent): void => {
|
||||
if (event.source !== window.parent || event.data?.type == null) {
|
||||
return;
|
||||
}
|
||||
handler(event);
|
||||
};
|
||||
window.addEventListener('message', wrapped);
|
||||
return () => window.removeEventListener('message', wrapped);
|
||||
}
|
||||
|
||||
/**
|
||||
* Send message to UserWallet iframe (when Data embeds it). Target USERWALLET_ORIGIN.
|
||||
*/
|
||||
export function sendToUserWallet(
|
||||
message: DataToUserWalletMessage,
|
||||
iframe: HTMLIFrameElement | null,
|
||||
targetOrigin: string,
|
||||
): void {
|
||||
if (iframe?.contentWindow == null) {
|
||||
return;
|
||||
}
|
||||
iframe.contentWindow.postMessage(message, targetOrigin);
|
||||
}
|
||||
|
||||
/**
|
||||
* Listen for messages from UserWallet iframe (Data is parent).
|
||||
* Only invokes handler when event.source === userwalletIframe.contentWindow.
|
||||
*/
|
||||
export function listenToUserWallet(
|
||||
handler: (event: MessageEvent) => void,
|
||||
userwalletIframe: HTMLIFrameElement | null,
|
||||
): () => void {
|
||||
const wrapped = (event: MessageEvent): void => {
|
||||
if (event.data?.type == null) {
|
||||
return;
|
||||
}
|
||||
const fromUw = userwalletIframe?.contentWindow != null && event.source === userwalletIframe.contentWindow;
|
||||
if (!fromUw) {
|
||||
return;
|
||||
}
|
||||
handler(event);
|
||||
};
|
||||
window.addEventListener('message', wrapped);
|
||||
return () => window.removeEventListener('message', wrapped);
|
||||
}
|
||||
20
website-data/src/config.ts
Normal file
20
website-data/src/config.ts
Normal file
@ -0,0 +1,20 @@
|
||||
/**
|
||||
* Configuration for Data iframe (data.certificator.4nkweb.com).
|
||||
* Override via env: VITE_USERWALLET_ORIGIN, VITE_DATA_ORIGIN.
|
||||
*/
|
||||
|
||||
const env =
|
||||
typeof import.meta !== 'undefined'
|
||||
? (import.meta as { env?: { VITE_USERWALLET_ORIGIN?: string; VITE_DATA_ORIGIN?: string; DEV?: boolean } })
|
||||
.env
|
||||
: undefined;
|
||||
|
||||
/** UserWallet iframe origin (embedded by Data). Dev default localhost:3018. */
|
||||
export const USERWALLET_ORIGIN =
|
||||
env?.VITE_USERWALLET_ORIGIN ??
|
||||
(env?.DEV ? 'http://localhost:3018' : 'https://userwallet.certificator.4nkweb.com');
|
||||
|
||||
/** Data iframe origin (for postMessage target from site). */
|
||||
export const DATA_ORIGIN =
|
||||
env?.VITE_DATA_ORIGIN ??
|
||||
(env?.DEV ? 'http://localhost:3025' : 'https://data.certificator.4nkweb.com');
|
||||
132
website-data/src/main.ts
Normal file
132
website-data/src/main.ts
Normal file
@ -0,0 +1,132 @@
|
||||
import { USERWALLET_ORIGIN } from './config.js';
|
||||
import {
|
||||
isInIframe,
|
||||
sendToParent,
|
||||
listenToParent,
|
||||
sendToUserWallet,
|
||||
listenToUserWallet,
|
||||
type SiteToDataMessage,
|
||||
} from './channel.js';
|
||||
|
||||
const statusEl = document.getElementById('status') as HTMLParagraphElement | null;
|
||||
const userwalletContainer = document.getElementById('userwallet-container') as HTMLDivElement | null;
|
||||
let userwalletIframe: HTMLIFrameElement | null = null;
|
||||
|
||||
function setStatus(text: string, inIframe = false): void {
|
||||
if (statusEl != null) {
|
||||
statusEl.textContent = text;
|
||||
statusEl.classList.toggle('in-iframe', inIframe);
|
||||
}
|
||||
}
|
||||
|
||||
function ensureUserWalletIframe(): HTMLIFrameElement | null {
|
||||
if (userwalletIframe != null) {
|
||||
return userwalletIframe;
|
||||
}
|
||||
if (userwalletContainer == null) {
|
||||
return null;
|
||||
}
|
||||
const iframe = document.createElement('iframe');
|
||||
iframe.id = 'userwallet';
|
||||
iframe.title = 'UserWallet';
|
||||
iframe.src = USERWALLET_ORIGIN;
|
||||
iframe.setAttribute('style', 'width:100%;min-height:320px;border:1px solid #dee2e6;border-radius:8px;');
|
||||
userwalletContainer.appendChild(iframe);
|
||||
userwalletIframe = iframe;
|
||||
return iframe;
|
||||
}
|
||||
|
||||
function handleSiteMessage(msg: MessageEvent): void {
|
||||
const d = msg.data as SiteToDataMessage;
|
||||
if (d?.type == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (d.type === 'ping') {
|
||||
sendToParent({ type: 'pong', payload: undefined });
|
||||
return;
|
||||
}
|
||||
|
||||
if (d.type === 'data-request') {
|
||||
sendToParent({
|
||||
type: 'data-response',
|
||||
payload: { id: (d.payload as { id?: string })?.id, data: null },
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
if (d.type === 'contract') {
|
||||
const m = d as { type: 'contract'; payload?: { contrat?: unknown; contrats_fils?: unknown[]; actions?: unknown[] } };
|
||||
const payload = m.payload;
|
||||
const iframe = ensureUserWalletIframe();
|
||||
if (iframe != null && payload != null) {
|
||||
sendToUserWallet(
|
||||
{ type: 'contract', payload: { contrat: payload.contrat, contrats_fils: payload.contrats_fils ?? [], actions: payload.actions ?? [] } },
|
||||
iframe,
|
||||
USERWALLET_ORIGIN,
|
||||
);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
if (d.type === 'auth-request') {
|
||||
const m = d as { type: 'auth-request'; payload?: { serviceId: string } };
|
||||
const payload = m.payload ?? { serviceId: '' };
|
||||
const iframe = ensureUserWalletIframe();
|
||||
if (iframe != null) {
|
||||
sendToUserWallet({ type: 'auth-request', payload: { serviceId: payload.serviceId } }, iframe, USERWALLET_ORIGIN);
|
||||
}
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
/** Emit notification to site (e.g. relay updates, userwallet events). */
|
||||
function notifySite(kind: string, detail?: unknown): void {
|
||||
sendToParent({ type: 'notification', payload: { kind, detail } });
|
||||
}
|
||||
|
||||
/* Placeholder: relay notifications. Data fetches from api-relay (non-key); UserWallet uses relays for keys/signatures. Wire relay subscription -> notifySite('relay:...', detail) when needed. */
|
||||
|
||||
function handleUserWalletMessage(msg: MessageEvent): void {
|
||||
const d = msg.data as { type: string; payload?: unknown };
|
||||
if (d?.type == null) {
|
||||
return;
|
||||
}
|
||||
if (d.type === 'login-proof') {
|
||||
sendToParent({ type: 'login-proof', payload: d.payload });
|
||||
notifySite('userwallet:login-proof', d.payload);
|
||||
return;
|
||||
}
|
||||
if (d.type === 'auth-response') {
|
||||
sendToParent({ type: 'auth-response', payload: d.payload });
|
||||
notifySite('userwallet:auth-response', d.payload);
|
||||
return;
|
||||
}
|
||||
if (d.type === 'service-status') {
|
||||
sendToParent({ type: 'service-status', payload: d.payload });
|
||||
return;
|
||||
}
|
||||
if (d.type === 'error') {
|
||||
const p = d.payload as { message?: string; code?: string } | undefined;
|
||||
sendToParent({ type: 'error', payload: { message: p?.message ?? 'unknown', code: p?.code } });
|
||||
notifySite('userwallet:error', d.payload);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
function init(): void {
|
||||
const inIframe = isInIframe();
|
||||
setStatus(
|
||||
inIframe
|
||||
? 'Data iframe actif. En attente de messages du site.'
|
||||
: 'Data (standalone). Embed in parent for site <-> data <-> userwallet.',
|
||||
inIframe,
|
||||
);
|
||||
|
||||
listenToParent(handleSiteMessage);
|
||||
ensureUserWalletIframe();
|
||||
const uw = userwalletIframe;
|
||||
listenToUserWallet(handleUserWalletMessage, uw);
|
||||
}
|
||||
|
||||
init();
|
||||
20
website-data/start.sh
Executable file
20
website-data/start.sh
Executable file
@ -0,0 +1,20 @@
|
||||
#!/bin/bash
|
||||
# Build and serve website-data on port 3025 (production).
|
||||
# Usage: ./start.sh
|
||||
# Proxy: data.certificator.4nkweb.com -> backend:3025.
|
||||
|
||||
set -e
|
||||
|
||||
cd "$(dirname "$0")"
|
||||
export PORT=${PORT:-3025}
|
||||
|
||||
if [ ! -d node_modules ]; then
|
||||
echo "Installation des dépendances..."
|
||||
npm install
|
||||
fi
|
||||
|
||||
echo "Build du frontend website-data..."
|
||||
npm run build
|
||||
|
||||
echo "Démarrage du serveur sur le port $PORT..."
|
||||
exec npx vite preview --port "$PORT" --host 0.0.0.0
|
||||
15
website-data/tsconfig.json
Normal file
15
website-data/tsconfig.json
Normal file
@ -0,0 +1,15 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"target": "ES2022",
|
||||
"module": "ESNext",
|
||||
"moduleResolution": "bundler",
|
||||
"strict": true,
|
||||
"noEmit": true,
|
||||
"skipLibCheck": true,
|
||||
"isolatedModules": true,
|
||||
"resolveJsonModule": true,
|
||||
"esModuleInterop": true,
|
||||
"allowSyntheticDefaultImports": true
|
||||
},
|
||||
"include": ["src"]
|
||||
}
|
||||
20
website-data/vite.config.ts
Normal file
20
website-data/vite.config.ts
Normal file
@ -0,0 +1,20 @@
|
||||
import { defineConfig } from 'vite';
|
||||
|
||||
export default defineConfig({
|
||||
root: '.',
|
||||
build: {
|
||||
outDir: 'dist',
|
||||
rollupOptions: {
|
||||
input: ['index.html'],
|
||||
},
|
||||
},
|
||||
server: {
|
||||
port: 3025,
|
||||
strictPort: false,
|
||||
},
|
||||
preview: {
|
||||
port: 3025,
|
||||
host: '0.0.0.0',
|
||||
allowedHosts: ['data.certificator.4nkweb.com'],
|
||||
},
|
||||
});
|
||||
21
website-data/website-data.service
Normal file
21
website-data/website-data.service
Normal file
@ -0,0 +1,21 @@
|
||||
[Unit]
|
||||
Description=4NK Website data (iframe data, non clés)
|
||||
After=network.target
|
||||
|
||||
[Service]
|
||||
Type=simple
|
||||
User=ncantu
|
||||
WorkingDirectory=/srv/4NK/data.certificator.4nkweb.com
|
||||
Environment=PORT=3025
|
||||
Environment=NODE_ENV=production
|
||||
ExecStart=/bin/bash /srv/4NK/data.certificator.4nkweb.com/start.sh
|
||||
Restart=always
|
||||
RestartSec=10
|
||||
StandardOutput=journal
|
||||
StandardError=journal
|
||||
|
||||
NoNewPrivileges=true
|
||||
PrivateTmp=true
|
||||
|
||||
[Install]
|
||||
WantedBy=multi-user.target
|
||||
@ -69,6 +69,10 @@ Le skeleton utilise automatiquement le contrat de service skeleton au démarrage
|
||||
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 <strong>membre connecté</strong> (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).
|
||||
|
||||
### Cloisonnement iframe
|
||||
|
||||
Le skeleton (parent) **ne signe, ne chiffre ni ne déchiffre jamais**. Il utilise uniquement `verifyLoginProof` (vérification de signatures avec clés publiques). Aucune clé privée n’est reçue ni utilisée par le parent. Toutes les opérations de signature et de chiffrement restent dans l’iframe UserWallet. Voir `userwallet/features/userwallet-iframe-key-isolation.md`.
|
||||
|
||||
## Exemple d'intégration
|
||||
|
||||
### Envoi de contrat depuis le parent
|
||||
@ -143,7 +147,7 @@ window.addEventListener('message', (event) => {
|
||||
nonceCache, // Cache anti-rejeu
|
||||
timestampWindowMs: 300000 // 5 minutes
|
||||
});
|
||||
|
||||
|
||||
if (result.accept) {
|
||||
// Ouvrir la session utilisateur
|
||||
console.log('Login accepté');
|
||||
@ -171,6 +175,7 @@ Les raisons de refus possibles :
|
||||
- `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 <strong>membre connecté</strong> (utilisateur) et de son device (Pair) ; clés iframe / IndexedDB. Distinction service (.env.private) vs utilisateur.
|
||||
- `cryptographie.html` : page explicative de la cryptographie utilisée (ECDH secp256k1, AES-256-GCM, SHA-256, Schnorr, HKDF) avec workflow complet et vulgarisation.
|
||||
- `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`).
|
||||
|
||||
@ -34,6 +34,11 @@
|
||||
.member-list > li > em { color: #666; font-size: 0.9rem; }
|
||||
.pair-list { list-style: none; margin-top: 0.5rem; padding-left: 1rem; border-left: 3px solid #007bff; }
|
||||
.pair-list > li { background: #fff; padding: 0.5rem 0.75rem; border-radius: 6px; margin: 0.4rem 0; border: 1px solid #e8e8e8; }
|
||||
.roles-intro { margin-bottom: 1rem; }
|
||||
.role-block { background: #f8f9fa; border: 1px solid #e0e0e0; border-radius: 8px; padding: 0.75rem 1rem; margin: 0.5rem 0; }
|
||||
.role-block h4 { margin-top: 0; }
|
||||
.role-block .usage, .role-block .validation { font-size: 0.9rem; color: #555; margin: 0.25rem 0; }
|
||||
.role-block .usage strong, .role-block .validation strong { color: #333; }
|
||||
@media (max-width: 768px) {
|
||||
body { padding: 0.75rem; }
|
||||
h1 { font-size: 1.25rem; }
|
||||
@ -42,7 +47,7 @@
|
||||
</head>
|
||||
<body>
|
||||
<h1>Le contrat</h1>
|
||||
<p><a href="index.html">← Retour à l'accueil</a> · <a href="membre.html">Qui êtes-vous ?</a></p>
|
||||
<p><a href="index.html">← Retour à l'accueil</a> · <a href="membre.html">Qui êtes-vous ?</a> · <a href="technique.html">Réseau P2P</a> · <a href="cryptographie.html">Cryptographie</a></p>
|
||||
|
||||
<div class="highlight">
|
||||
<strong>En résumé :</strong> Ce contrat définit les règles de confiance entre vous et le service.
|
||||
@ -113,6 +118,21 @@
|
||||
<li><strong>Vérifiable par tous</strong> — les règles du contrat sont publiques.</li>
|
||||
</ul>
|
||||
|
||||
<h2>Infrastructure, gouvernance et preuves</h2>
|
||||
<p>
|
||||
L'<strong>infrastructure</strong> (relais, stockage des messages et des signatures) est <strong>décentralisée et neutre</strong> :
|
||||
elle ne dépend pas d'un acteur unique.
|
||||
</p>
|
||||
<p>
|
||||
Les <strong>membres</strong> ont une <strong>liberté de gouvernance</strong>. Certains peuvent choisir une organisation
|
||||
<strong>centralisée</strong> lorsque c'est préférable (équipe, processus, conformité). D'autres restent décentralisés.
|
||||
</p>
|
||||
<p>
|
||||
L'important est d'avoir les <strong>preuves de la bonne exécution du contrat entre les parties</strong> :
|
||||
signatures, nonces, règles publiques. Ces preuves restent vérifiables quelle que soit l'organisation
|
||||
(décentralisée ou centralisée) des membres.
|
||||
</p>
|
||||
|
||||
<details>
|
||||
<summary>Détails techniques du contrat</summary>
|
||||
<h3>Identifiants</h3>
|
||||
@ -122,10 +142,73 @@
|
||||
<li>Action login UUID : <span class="meta">0ac7de59-9e81-4bdc-bd19-c07750fad48e</span></li>
|
||||
<li>Validateur (membre) : <span class="meta">0e865301-362f-4951-bfbc-531b7bddf820</span></li>
|
||||
</ul>
|
||||
<h3>Membres par rôles</h3>
|
||||
<p>Chaque rôle définit qui peut valider quoi. Un membre peut avoir plusieurs Pairs (appareils), chaque Pair possède une clé publique unique :</p>
|
||||
<h3>Rôles par défaut (types de Champ)</h3>
|
||||
<p class="roles-intro">
|
||||
Les <strong>11 rôles</strong> ci‑dessous existent par défaut. Chacun peut être <strong>vide</strong> (aucun membre)
|
||||
ou <strong>plein</strong> (membres et pairs définis). Tous sont <strong>connus des participants</strong> ;
|
||||
les conditions d'usage et de validation s'appliquent dès qu'un rôle est rempli.
|
||||
</p>
|
||||
|
||||
<div class="role-block">
|
||||
<h4>1. Partage avec les institutions</h4>
|
||||
<p class="usage"><strong>Usage :</strong> Partager des données avec des institutions.</p>
|
||||
<p class="validation"><strong>Validation :</strong> Conformité aux validateurs du champ ; signatures requises selon <code>membres_du_role</code>.</p>
|
||||
</div>
|
||||
<div class="role-block">
|
||||
<h4>2. Messages au RSSI</h4>
|
||||
<p class="usage"><strong>Usage :</strong> Messages au RSSI de la société responsable du service.</p>
|
||||
<p class="validation"><strong>Validation :</strong> Signatures des membres du rôle conformes aux <code>signatures_obligatoires</code>.</p>
|
||||
</div>
|
||||
<div class="role-block">
|
||||
<h4>3. Messages au Correspondant CNIL</h4>
|
||||
<p class="usage"><strong>Usage :</strong> Messages au Correspondant CNIL de la société responsable du service.</p>
|
||||
<p class="validation"><strong>Validation :</strong> Signatures des membres du rôle ; attributs CNIL requis si applicables.</p>
|
||||
</div>
|
||||
<div class="role-block">
|
||||
<h4>4. Messages au Responsable cybersécurité</h4>
|
||||
<p class="usage"><strong>Usage :</strong> Messages au Responsable cybersécurité de la société responsable du service.</p>
|
||||
<p class="validation"><strong>Validation :</strong> Signatures conformes aux validateurs du champ.</p>
|
||||
</div>
|
||||
<div class="role-block">
|
||||
<h4>5. Messages de support infogérant</h4>
|
||||
<p class="usage"><strong>Usage :</strong> Support infogérant du service (peut inclure un membre du miner pour la gestion des clés API).</p>
|
||||
<p class="validation"><strong>Validation :</strong> Signatures des membres du rôle ; <code>membre_miner_uuid</code> optionnel dans <code>datajson</code> si applicable.</p>
|
||||
</div>
|
||||
<div class="role-block">
|
||||
<h4>6. Messages de support administrateur système</h4>
|
||||
<p class="usage"><strong>Usage :</strong> Support administrateur système du service.</p>
|
||||
<p class="validation"><strong>Validation :</strong> Signatures conformes aux validateurs du champ.</p>
|
||||
</div>
|
||||
<div class="role-block">
|
||||
<h4>7. Messages de support niveau 1</h4>
|
||||
<p class="usage"><strong>Usage :</strong> Support niveau 1 du service.</p>
|
||||
<p class="validation"><strong>Validation :</strong> Signatures conformes aux validateurs du champ.</p>
|
||||
</div>
|
||||
<div class="role-block">
|
||||
<h4>8. Messages de support niveau 2</h4>
|
||||
<p class="usage"><strong>Usage :</strong> Support niveau 2 du service.</p>
|
||||
<p class="validation"><strong>Validation :</strong> Signatures conformes aux validateurs du champ.</p>
|
||||
</div>
|
||||
<div class="role-block">
|
||||
<h4>9. Messages de support niveau 3</h4>
|
||||
<p class="usage"><strong>Usage :</strong> Support niveau 3 du service.</p>
|
||||
<p class="validation"><strong>Validation :</strong> Signatures conformes aux validateurs du champ.</p>
|
||||
</div>
|
||||
<div class="role-block">
|
||||
<h4>10. Validation (contrat)</h4>
|
||||
<p class="usage"><strong>Usage :</strong> Valider le contrat ; définir qui peut signer pour le contrat (<code>contrat.validateurs.membres_du_role</code>).</p>
|
||||
<p class="validation"><strong>Validation :</strong> Au moins une signature valide par membre du rôle ; clés publiques autorisées dans <code>signatures_obligatoires</code>.</p>
|
||||
</div>
|
||||
<div class="role-block">
|
||||
<h4>11. Validation du login</h4>
|
||||
<p class="usage"><strong>Usage :</strong> Valider le login ; définir qui peut signer pour l'action login (<code>action.validateurs_action.membres_du_role</code>).</p>
|
||||
<p class="validation"><strong>Validation :</strong> Preuve de login (hash, nonce) signée par les pairs des membres du rôle ; nonce unique ; clés autorisées.</p>
|
||||
</div>
|
||||
|
||||
<h3>Membres par rôles (ce contrat)</h3>
|
||||
<p>Ce contrat skeleton remplit uniquement les rôles <strong>Validation (contrat)</strong> et <strong>Validation du login</strong>. Les autres rôles sont vides mais connus.</p>
|
||||
|
||||
<h4>Validateur (contrat)</h4>
|
||||
<h4>Validation (contrat)</h4>
|
||||
<ul class="member-list">
|
||||
<li>
|
||||
<strong>Membre</strong> : <span class="meta">0e865301-362f-4951-bfbc-531b7bddf820</span>
|
||||
@ -139,7 +222,7 @@
|
||||
</li>
|
||||
</ul>
|
||||
|
||||
<h4>Validateur login (action)</h4>
|
||||
<h4>Validation du login</h4>
|
||||
<ul class="member-list">
|
||||
<li>
|
||||
<strong>Membre</strong> : <span class="meta">0e865301-362f-4951-bfbc-531b7bddf820</span>
|
||||
@ -154,6 +237,6 @@
|
||||
</ul>
|
||||
</details>
|
||||
|
||||
<p><a href="index.html">← Retour à l'accueil</a> · <a href="membre.html">Qui êtes-vous ?</a></p>
|
||||
<p><a href="index.html">← Retour à l'accueil</a> · <a href="membre.html">Qui êtes-vous ?</a> · <a href="technique.html">Réseau P2P</a> · <a href="cryptographie.html">Cryptographie</a></p>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
448
website-skeleton/cryptographie.html
Normal file
448
website-skeleton/cryptographie.html
Normal file
@ -0,0 +1,448 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="fr">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<title>Cryptographie expliquée – 4NK un nouveau web</title>
|
||||
<style>
|
||||
* { box-sizing: border-box; }
|
||||
body {
|
||||
font-family: system-ui, -apple-system, sans-serif;
|
||||
max-width: 900px;
|
||||
margin: 0 auto;
|
||||
padding: 1rem;
|
||||
line-height: 1.6;
|
||||
color: #333;
|
||||
}
|
||||
h1 { font-size: 1.5rem; margin-bottom: 1rem; color: #222; }
|
||||
h2 { font-size: 1.2rem; margin-top: 1.5rem; margin-bottom: 0.5rem; color: #333; }
|
||||
h3 { font-size: 1rem; margin-top: 1rem; margin-bottom: 0.35rem; font-weight: 600; }
|
||||
p { margin: 0.75rem 0; }
|
||||
ul, ol { margin: 0.5rem 0; padding-left: 1.5rem; }
|
||||
li { margin: 0.4rem 0; }
|
||||
a { color: #007bff; }
|
||||
a:hover { text-decoration: underline; }
|
||||
.highlight { background: #e8f4fd; padding: 1rem; border-radius: 8px; border-left: 4px solid #007bff; margin: 1rem 0; }
|
||||
.info { background: #d4edda; padding: 1rem; border-radius: 8px; border-left: 4px solid #28a745; margin: 1rem 0; }
|
||||
.warning { background: #fff3cd; padding: 1rem; border-radius: 8px; border-left: 4px solid #ffc107; margin: 1rem 0; }
|
||||
.algo { background: #f8f9fa; padding: 1rem; border-radius: 8px; border: 1px solid #e0e0e0; margin: 1rem 0; }
|
||||
.algo-title { font-weight: 700; color: #007bff; margin-bottom: 0.5rem; }
|
||||
details { margin: 1rem 0; }
|
||||
summary { cursor: pointer; font-weight: 600; color: #555; }
|
||||
.meta { font-family: ui-monospace, monospace; font-size: 0.85rem; color: #666; background: #f5f5f5; padding: 0.2em 0.4em; border-radius: 4px; word-break: break-all; }
|
||||
.workflow { background: #f5f5f5; padding: 1rem; border-radius: 8px; margin: 1rem 0; font-family: ui-monospace, monospace; font-size: 0.85rem; white-space: pre-wrap; line-height: 1.4; overflow-x: auto; }
|
||||
.step { background: #fff; border: 1px solid #ddd; border-radius: 8px; padding: 1rem; margin: 0.75rem 0; }
|
||||
.step-num { display: inline-block; width: 28px; height: 28px; background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); color: white; border-radius: 50%; text-align: center; line-height: 28px; font-weight: 700; margin-right: 0.75rem; font-size: 0.9rem; }
|
||||
.algo-table { width: 100%; border-collapse: collapse; margin: 1rem 0; }
|
||||
.algo-table th, .algo-table td { padding: 0.5rem; text-align: left; border-bottom: 1px solid #ddd; }
|
||||
.algo-table th { background: #f5f5f5; font-weight: 600; }
|
||||
.algo-table code { background: #f5f5f5; padding: 0.2em 0.4em; border-radius: 4px; font-size: 0.9em; }
|
||||
@media (max-width: 768px) {
|
||||
body { padding: 0.75rem; }
|
||||
h1 { font-size: 1.25rem; }
|
||||
.workflow { font-size: 0.75rem; padding: 0.75rem; }
|
||||
.algo-table { font-size: 0.9rem; }
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<h1>Cryptographie expliquée</h1>
|
||||
<p><a href="index.html">← Retour à l'accueil</a> · <a href="contrat.html">Le contrat</a> · <a href="membre.html">Qui êtes-vous ?</a> · <a href="technique.html">Réseau P2P</a></p>
|
||||
|
||||
<div class="highlight">
|
||||
<strong>En résumé :</strong> Vos messages sont protégés par plusieurs couches de cryptographie.
|
||||
Seul le destinataire peut les lire (chiffrement), et il peut vérifier que c'est bien vous qui les avez envoyés (signature).
|
||||
Tout cela sans que personne ne connaisse vos clés secrètes.
|
||||
</div>
|
||||
|
||||
<h2>Comment ça marche en simple ?</h2>
|
||||
<p>
|
||||
Imaginez que vous voulez envoyer une lettre secrète à un ami. Dans le monde réel, vous pourriez :
|
||||
</p>
|
||||
<ol>
|
||||
<li><strong>Mettre la lettre dans une enveloppe fermée</strong> (chiffrement) — seul celui qui a la clé peut l'ouvrir</li>
|
||||
<li><strong>Signer l'enveloppe</strong> (signature) — pour prouver que c'est bien vous qui l'avez envoyée</li>
|
||||
<li><strong>Donner la clé au destinataire</strong> (échange de clé) — mais sans que personne d'autre ne puisse l'intercepter</li>
|
||||
</ol>
|
||||
<p>
|
||||
C'est exactement ce que fait la cryptographie numérique, mais de façon <strong>mathématiquement impossible à falsifier</strong>.
|
||||
</p>
|
||||
|
||||
<h2>Les algorithmes utilisés</h2>
|
||||
<p>
|
||||
Voici les « outils » cryptographiques du système. Chacun a un rôle précis :
|
||||
</p>
|
||||
|
||||
<table class="algo-table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Algorithme</th>
|
||||
<th>Rôle</th>
|
||||
<th>Analogie</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr>
|
||||
<td><code>SHA-256</code></td>
|
||||
<td>Empreinte unique du message</td>
|
||||
<td>Comme une empreinte digitale : unique et impossible à inverser</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><code>ECDH secp256k1</code></td>
|
||||
<td>Échange de clé sécurisé</td>
|
||||
<td>Comme mélanger deux couleurs en public pour créer un secret commun</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><code>HKDF-SHA256</code></td>
|
||||
<td>Dérivation de clé</td>
|
||||
<td>Transformer le secret partagé en une clé utilisable</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><code>AES-256-GCM</code></td>
|
||||
<td>Chiffrement du message</td>
|
||||
<td>Un coffre-fort numérique ultra-sécurisé</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><code>Schnorr secp256k1</code></td>
|
||||
<td>Signature numérique</td>
|
||||
<td>Votre signature manuscrite, mais impossible à falsifier</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
<h2>Détail de chaque algorithme</h2>
|
||||
|
||||
<div class="algo">
|
||||
<div class="algo-title">SHA-256 — L'empreinte digitale</div>
|
||||
<p>
|
||||
SHA-256 crée une « empreinte » unique de 64 caractères hexadécimaux (256 bits) pour n'importe quel message.
|
||||
Deux messages différents auront toujours des empreintes différentes.
|
||||
</p>
|
||||
<ul>
|
||||
<li><strong>Entrée</strong> : n'importe quel texte ou données</li>
|
||||
<li><strong>Sortie</strong> : 64 caractères hexadécimaux (ex: <span class="meta">a7f3c9...</span>)</li>
|
||||
<li><strong>Propriété clé</strong> : impossible de retrouver le message à partir de l'empreinte</li>
|
||||
</ul>
|
||||
<p><em>Usage</em> : identifier chaque message de façon unique sur le relais.</p>
|
||||
</div>
|
||||
|
||||
<div class="algo">
|
||||
<div class="algo-title">ECDH secp256k1 — Le secret partagé</div>
|
||||
<p>
|
||||
ECDH (Elliptic Curve Diffie-Hellman) permet à deux personnes de créer un <strong>secret commun</strong>
|
||||
sans jamais l'échanger directement. C'est comme de la magie mathématique !
|
||||
</p>
|
||||
<div class="info">
|
||||
<strong>L'analogie des couleurs :</strong> Imaginez qu'Alice et Bob veulent créer une couleur secrète.
|
||||
<ul>
|
||||
<li>Alice mélange une couleur publique avec sa couleur secrète → obtient « couleur A »</li>
|
||||
<li>Bob mélange la même couleur publique avec sa couleur secrète → obtient « couleur B »</li>
|
||||
<li>Ils échangent « couleur A » et « couleur B » (publiquement, tout le monde peut voir)</li>
|
||||
<li>Alice mélange « couleur B » + sa couleur secrète → obtient la couleur finale</li>
|
||||
<li>Bob mélange « couleur A » + sa couleur secrète → obtient <strong>la même couleur finale</strong></li>
|
||||
</ul>
|
||||
Résultat : Alice et Bob ont la même couleur secrète, sans l'avoir jamais échangée !
|
||||
Un espion qui a vu « couleur A » et « couleur B » ne peut pas retrouver la couleur finale.
|
||||
</div>
|
||||
<ul>
|
||||
<li><strong>Courbe</strong> : secp256k1 (la même que Bitcoin)</li>
|
||||
<li><strong>Entrée</strong> : votre clé privée + la clé publique du destinataire</li>
|
||||
<li><strong>Sortie</strong> : un secret partagé (32 octets)</li>
|
||||
</ul>
|
||||
<p><em>Usage</em> : créer un secret pour chiffrer les messages entre deux personnes.</p>
|
||||
</div>
|
||||
|
||||
<div class="algo">
|
||||
<div class="algo-title">HKDF-SHA256 — Le raffineur de clé</div>
|
||||
<p>
|
||||
HKDF (HMAC-based Key Derivation Function) transforme le secret partagé ECDH en une clé
|
||||
de chiffrement de haute qualité.
|
||||
</p>
|
||||
<ul>
|
||||
<li><strong>Entrée</strong> : le secret partagé ECDH (32 octets)</li>
|
||||
<li><strong>Sortie</strong> : une clé AES-256 (32 octets) de qualité cryptographique</li>
|
||||
<li><strong>Pourquoi</strong> : s'assurer que la clé est uniformément aléatoire</li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<div class="algo">
|
||||
<div class="algo-title">AES-256-GCM — Le coffre-fort</div>
|
||||
<p>
|
||||
AES-256-GCM est l'algorithme de chiffrement qui protège le contenu du message.
|
||||
Il offre à la fois la <strong>confidentialité</strong> (personne ne peut lire) et
|
||||
l'<strong>intégrité</strong> (personne ne peut modifier sans qu'on le sache).
|
||||
</p>
|
||||
<ul>
|
||||
<li><strong>Taille de clé</strong> : 256 bits (très sécurisé)</li>
|
||||
<li><strong>Mode</strong> : GCM (Galois/Counter Mode) avec authentification</li>
|
||||
<li><strong>IV</strong> : vecteur d'initialisation de 12 octets (différent à chaque chiffrement)</li>
|
||||
<li><strong>Tag</strong> : code d'authentification de 16 octets (détecte les modifications)</li>
|
||||
</ul>
|
||||
<p><em>Usage</em> : chiffrer le message pour que seul le destinataire puisse le lire.</p>
|
||||
</div>
|
||||
|
||||
<div class="algo">
|
||||
<div class="algo-title">Schnorr secp256k1 — La signature</div>
|
||||
<p>
|
||||
La signature Schnorr prouve que vous êtes bien l'auteur du message, sans révéler votre clé privée.
|
||||
Elle est <strong>plus efficace</strong> et <strong>plus simple</strong> que les signatures ECDSA traditionnelles.
|
||||
</p>
|
||||
<ul>
|
||||
<li><strong>Entrée</strong> : le hash du message + votre clé privée</li>
|
||||
<li><strong>Sortie</strong> : une signature de 64 octets (128 caractères hex)</li>
|
||||
<li><strong>Vérification</strong> : n'importe qui peut vérifier avec votre clé publique</li>
|
||||
</ul>
|
||||
<div class="warning">
|
||||
<strong>Important :</strong> La signature prouve que vous avez signé, mais elle ne révèle
|
||||
jamais votre clé privée. Même en voyant 1000 de vos signatures, personne ne peut
|
||||
retrouver votre clé secrète.
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<h2>Le workflow complet</h2>
|
||||
<p>
|
||||
Voici ce qui se passe quand vous envoyez un message sécurisé, étape par étape :
|
||||
</p>
|
||||
|
||||
<h3>Phase 1 : Préparation</h3>
|
||||
<div class="step">
|
||||
<span class="step-num">1</span>
|
||||
<strong>Création du message</strong><br>
|
||||
Votre message est structuré avec : les données, un horodatage, un identifiant unique (UUID), et les règles de validation.
|
||||
</div>
|
||||
<div class="step">
|
||||
<span class="step-num">2</span>
|
||||
<strong>Calcul de l'empreinte (hash)</strong><br>
|
||||
<span class="meta">Message → SHA-256 → Hash (64 caractères)</span><br>
|
||||
Cette empreinte unique identifie le message sur le réseau.
|
||||
</div>
|
||||
<div class="step">
|
||||
<span class="step-num">3</span>
|
||||
<strong>Génération du nonce</strong><br>
|
||||
Un nombre aléatoire unique (nonce) est créé pour éviter qu'un même message soit rejoué.
|
||||
</div>
|
||||
|
||||
<h3>Phase 2 : Chiffrement</h3>
|
||||
<div class="step">
|
||||
<span class="step-num">4</span>
|
||||
<strong>Échange de clé ECDH</strong><br>
|
||||
<span class="meta">Votre clé privée + Clé publique du destinataire → ECDH → Secret partagé</span><br>
|
||||
Un secret commun est calculé mathématiquement, sans jamais être transmis.
|
||||
</div>
|
||||
<div class="step">
|
||||
<span class="step-num">5</span>
|
||||
<strong>Dérivation de la clé AES</strong><br>
|
||||
<span class="meta">Secret partagé → HKDF-SHA256 → Clé AES-256</span><br>
|
||||
Le secret est transformé en une clé de chiffrement de qualité.
|
||||
</div>
|
||||
<div class="step">
|
||||
<span class="step-num">6</span>
|
||||
<strong>Chiffrement du message</strong><br>
|
||||
<span class="meta">Clé AES + IV aléatoire + Message → AES-256-GCM → Message chiffré + Tag</span><br>
|
||||
Le message est enfermé dans un « coffre-fort » numérique.
|
||||
</div>
|
||||
|
||||
<h3>Phase 3 : Signature</h3>
|
||||
<div class="step">
|
||||
<span class="step-num">7</span>
|
||||
<strong>Signature Schnorr</strong><br>
|
||||
<span class="meta">Hash + Nonce + Votre clé privée → Schnorr → Signature</span><br>
|
||||
Vous signez l'empreinte du message avec votre clé privée.
|
||||
</div>
|
||||
|
||||
<h3>Phase 4 : Publication sur le relais</h3>
|
||||
<div class="step">
|
||||
<span class="step-num">8</span>
|
||||
<strong>Envoi au relais</strong><br>
|
||||
Trois éléments sont envoyés séparément :
|
||||
<ul style="margin-top: 0.5rem;">
|
||||
<li><strong>MsgChiffre</strong> : le message chiffré + son hash</li>
|
||||
<li><strong>MsgCle</strong> : l'IV + votre clé publique (pour que le destinataire puisse déchiffrer)</li>
|
||||
<li><strong>MsgSignature</strong> : votre signature + clé publique</li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<h3>Phase 5 : Collecte des signatures (multi-signature)</h3>
|
||||
<div class="step">
|
||||
<span class="step-num">9</span>
|
||||
<strong>Attente des co-signataires</strong><br>
|
||||
Si le contrat exige plusieurs signatures (ex: 2 appareils sur 3), le système attend que les autres signent :
|
||||
<ul style="margin-top: 0.5rem;">
|
||||
<li>Interrogation du relais toutes les 2 secondes</li>
|
||||
<li>Timeout après 5 minutes si signatures manquantes</li>
|
||||
<li>Progression affichée (ex: "2/3 signatures")</li>
|
||||
</ul>
|
||||
</div>
|
||||
<div class="step">
|
||||
<span class="step-num">10</span>
|
||||
<strong>Validation</strong><br>
|
||||
Le message est validé quand :
|
||||
<ul style="margin-top: 0.5rem;">
|
||||
<li>Toutes les signatures requises sont présentes (cardinalité)</li>
|
||||
<li>Les dépendances sont respectées (ex: "A doit signer avant B")</li>
|
||||
<li>Les clés publiques correspondent aux signataires autorisés</li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<h3>Phase 6 : Réception et déchiffrement</h3>
|
||||
<div class="step">
|
||||
<span class="step-num">11</span>
|
||||
<strong>Scan des clés</strong><br>
|
||||
Le destinataire interroge le relais pour récupérer les MsgCle récentes.
|
||||
</div>
|
||||
<div class="step">
|
||||
<span class="step-num">12</span>
|
||||
<strong>Récupération du message</strong><br>
|
||||
Avec le hash trouvé dans la MsgCle, il récupère le message chiffré.
|
||||
</div>
|
||||
<div class="step">
|
||||
<span class="step-num">13</span>
|
||||
<strong>Déchiffrement ECDH inverse</strong><br>
|
||||
<span class="meta">Sa clé privée + Clé publique de l'émetteur (df) → ECDH → Même secret partagé</span><br>
|
||||
Il recalcule le même secret, puis déchiffre avec AES-GCM.
|
||||
</div>
|
||||
<div class="step">
|
||||
<span class="step-num">14</span>
|
||||
<strong>Vérification des signatures</strong><br>
|
||||
Chaque signature est vérifiée avec la clé publique du signataire.
|
||||
</div>
|
||||
|
||||
<h2>Schéma récapitulatif</h2>
|
||||
<div class="workflow">┌─────────────────────────────────────────────────────────────────┐
|
||||
│ ÉMETTEUR │
|
||||
├─────────────────────────────────────────────────────────────────┤
|
||||
│ Message → SHA-256 → Hash │
|
||||
│ │
|
||||
│ Clé privée émetteur ─┐ │
|
||||
│ ├──→ ECDH → Secret partagé │
|
||||
│ Clé publique dest. ──┘ │
|
||||
│ │
|
||||
│ Secret partagé → HKDF-SHA256 → Clé AES-256 │
|
||||
│ │
|
||||
│ Clé AES + IV + Message → AES-256-GCM → Message chiffré │
|
||||
│ │
|
||||
│ Hash + Nonce + Clé privée → Schnorr → Signature │
|
||||
└─────────────────────────────────────────────────────────────────┘
|
||||
│
|
||||
▼
|
||||
┌─────────────────────────────────────────────────────────────────┐
|
||||
│ RELAIS │
|
||||
├─────────────────────────────────────────────────────────────────┤
|
||||
│ POST /messages ← MsgChiffre (hash, message_chiffre) │
|
||||
│ POST /keys ← MsgCle (hash, iv, df_ecdh = clé pub émett.) │
|
||||
│ POST /signatures← MsgSignature (hash, signature, clé publique) │
|
||||
│ │
|
||||
│ ... attente des co-signatures (GET /signatures/:hash) ... │
|
||||
└─────────────────────────────────────────────────────────────────┘
|
||||
│
|
||||
▼
|
||||
┌─────────────────────────────────────────────────────────────────┐
|
||||
│ DESTINATAIRE │
|
||||
├─────────────────────────────────────────────────────────────────┤
|
||||
│ GET /keys?start=&end= → Liste des MsgCle récentes │
|
||||
│ GET /messages/:hash → MsgChiffre │
|
||||
│ GET /signatures/:hash → MsgSignature[] │
|
||||
│ │
|
||||
│ Clé privée dest. ────┐ │
|
||||
│ ├──→ ECDH → Même secret partagé │
|
||||
│ Clé publique émett. ─┘ (df_ecdh_scannable) │
|
||||
│ │
|
||||
│ Secret partagé → HKDF-SHA256 → Clé AES-256 │
|
||||
│ │
|
||||
│ Clé AES + IV + Ciphertext → AES-256-GCM → Message original │
|
||||
│ │
|
||||
│ Vérification : Signature + Clé publique → Schnorr verify → OK │
|
||||
└─────────────────────────────────────────────────────────────────┘</div>
|
||||
|
||||
<h2>Pourquoi c'est sécurisé ?</h2>
|
||||
|
||||
<h3>Confidentialité</h3>
|
||||
<ul>
|
||||
<li><strong>Seul le destinataire peut lire</strong> : le secret partagé ECDH ne peut être calculé que par l'émetteur et le destinataire</li>
|
||||
<li><strong>AES-256</strong> : considéré incassable avec les technologies actuelles (2²⁵⁶ combinaisons possibles)</li>
|
||||
<li><strong>IV unique</strong> : même si vous envoyez le même message deux fois, le résultat chiffré sera différent</li>
|
||||
</ul>
|
||||
|
||||
<h3>Intégrité</h3>
|
||||
<ul>
|
||||
<li><strong>GCM</strong> : le mode Galois/Counter détecte toute modification du message chiffré</li>
|
||||
<li><strong>Hash</strong> : l'empreinte SHA-256 garantit que le message n'a pas été altéré</li>
|
||||
</ul>
|
||||
|
||||
<h3>Authenticité</h3>
|
||||
<ul>
|
||||
<li><strong>Signature Schnorr</strong> : prouve mathématiquement que c'est bien vous qui avez signé</li>
|
||||
<li><strong>Multi-signature</strong> : plusieurs appareils peuvent être requis pour valider</li>
|
||||
<li><strong>Anti-rejeu</strong> : le nonce empêche de réutiliser une ancienne signature</li>
|
||||
</ul>
|
||||
|
||||
<h3>Séparation des données</h3>
|
||||
<ul>
|
||||
<li><strong>Message, clé et signature séparés</strong> : même si quelqu'un intercepte un élément, il ne peut rien faire sans les autres</li>
|
||||
<li><strong>Clé privée jamais transmise</strong> : seules les clés publiques et les signatures circulent</li>
|
||||
</ul>
|
||||
|
||||
<details>
|
||||
<summary>Détails techniques pour les développeurs</summary>
|
||||
|
||||
<h3>Paramètres cryptographiques</h3>
|
||||
<table class="algo-table">
|
||||
<tr><th>Paramètre</th><th>Valeur</th></tr>
|
||||
<tr><td>Courbe elliptique</td><td>secp256k1 (256 bits)</td></tr>
|
||||
<tr><td>Taille clé AES</td><td>256 bits</td></tr>
|
||||
<tr><td>Taille IV (AES-GCM)</td><td>96 bits (12 octets)</td></tr>
|
||||
<tr><td>Taille Tag (AES-GCM)</td><td>128 bits (16 octets)</td></tr>
|
||||
<tr><td>Hash</td><td>SHA-256 (256 bits)</td></tr>
|
||||
<tr><td>KDF</td><td>HKDF-SHA256</td></tr>
|
||||
<tr><td>Signature</td><td>Schnorr secp256k1 (64 octets)</td></tr>
|
||||
</table>
|
||||
|
||||
<h3>Bibliothèques utilisées</h3>
|
||||
<ul>
|
||||
<li><code>@noble/secp256k1</code> : ECDH, Schnorr, manipulation de clés</li>
|
||||
<li><code>@noble/hashes</code> : SHA-256, HKDF</li>
|
||||
<li><code>Web Crypto API</code> : AES-256-GCM (navigateur)</li>
|
||||
</ul>
|
||||
|
||||
<h3>Format des clés</h3>
|
||||
<ul>
|
||||
<li><strong>Clé privée</strong> : 32 octets (64 caractères hex)</li>
|
||||
<li><strong>Clé publique (compressée)</strong> : 33 octets (66 caractères hex, préfixe 02 ou 03)</li>
|
||||
<li><strong>Signature Schnorr</strong> : 64 octets (128 caractères hex)</li>
|
||||
</ul>
|
||||
|
||||
<h3>Fichiers source</h3>
|
||||
<ul>
|
||||
<li><code>userwallet/src/utils/encryption.ts</code> : encryptWithECDH, decryptWithECDH</li>
|
||||
<li><code>userwallet/src/utils/crypto.ts</code> : signMessage, verifySignature, generateChallenge</li>
|
||||
<li><code>userwallet/src/utils/relay.ts</code> : postMessageChiffre, postSignature, postKey</li>
|
||||
<li><code>userwallet/src/utils/collectSignatures.ts</code> : runCollectLoop, fetchSignaturesForHash</li>
|
||||
<li><code>userwallet/src/utils/loginValidation.ts</code> : hasEnoughSignatures, checkDependenciesSatisfied</li>
|
||||
<li><code>service-login-verify/</code> : verifyLoginProof (côté parent)</li>
|
||||
</ul>
|
||||
|
||||
<h3>Collecte des signatures</h3>
|
||||
<table class="algo-table">
|
||||
<tr><th>Constante</th><th>Valeur</th><th>Description</th></tr>
|
||||
<tr><td>COLLECT_POLL_MS</td><td>2 000 ms</td><td>Intervalle entre chaque interrogation</td></tr>
|
||||
<tr><td>COLLECT_TIMEOUT_MS</td><td>300 000 ms</td><td>Timeout global (5 minutes)</td></tr>
|
||||
<tr><td>COLLECT_FETCH_TIMEOUT_MS</td><td>15 000 ms</td><td>Timeout par requête</td></tr>
|
||||
</table>
|
||||
</details>
|
||||
|
||||
<h2>En résumé</h2>
|
||||
<div class="info">
|
||||
<strong>Le système combine plusieurs couches de protection :</strong>
|
||||
<ol>
|
||||
<li><strong>ECDH</strong> pour créer un secret sans jamais l'échanger</li>
|
||||
<li><strong>AES-256-GCM</strong> pour chiffrer le message</li>
|
||||
<li><strong>Schnorr</strong> pour prouver votre identité</li>
|
||||
<li><strong>SHA-256</strong> pour identifier chaque message de façon unique</li>
|
||||
<li><strong>Multi-signature</strong> pour exiger plusieurs validations</li>
|
||||
</ol>
|
||||
Résultat : vos messages sont <strong>confidentiels</strong>, <strong>authentiques</strong> et <strong>infalsifiables</strong>.
|
||||
</div>
|
||||
|
||||
<p><a href="index.html">← Retour à l'accueil</a> · <a href="contrat.html">Le contrat</a> · <a href="membre.html">Qui êtes-vous ?</a> · <a href="technique.html">Réseau P2P</a></p>
|
||||
</body>
|
||||
</html>
|
||||
@ -103,12 +103,51 @@
|
||||
background: #fafafa;
|
||||
}
|
||||
#connected-section {
|
||||
padding: 1.5rem;
|
||||
background: #f9f9f9;
|
||||
border: 1px solid #ddd;
|
||||
border-radius: 6px;
|
||||
padding: 0;
|
||||
margin: 1rem 0;
|
||||
}
|
||||
.connected-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
padding: 0.75rem 0;
|
||||
margin-bottom: 1rem;
|
||||
border-bottom: 1px solid #e0e0e0;
|
||||
}
|
||||
.connected-avatar {
|
||||
width: 48px;
|
||||
height: 48px;
|
||||
border-radius: 50%;
|
||||
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
color: white;
|
||||
font-size: 1.25rem;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
.connected-notifications {
|
||||
width: 44px;
|
||||
height: 44px;
|
||||
min-width: 44px;
|
||||
min-height: 44px;
|
||||
border: 1px solid #ccc;
|
||||
border-radius: 8px;
|
||||
background: #fff;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
cursor: pointer;
|
||||
transition: all 0.2s;
|
||||
}
|
||||
.connected-notifications:hover {
|
||||
background: #f5f5f5;
|
||||
border-color: #999;
|
||||
}
|
||||
.connected-notifications svg {
|
||||
width: 24px;
|
||||
height: 24px;
|
||||
}
|
||||
#user-info {
|
||||
margin: 1rem 0;
|
||||
padding: 1rem;
|
||||
@ -155,7 +194,8 @@
|
||||
<h1>4NK un nouveau web - site d'exemple</h1>
|
||||
|
||||
<div id="login-section">
|
||||
<p><a href="contrat.html">Le contrat</a> · <a href="membre.html">Qui êtes-vous ?</a></p>
|
||||
<p><a href="contrat.html">Le contrat</a> · <a href="membre.html">Qui êtes-vous ?</a> · <a href="technique.html">Réseau P2P</a> · <a href="cryptographie.html">Cryptographie</a></p>
|
||||
<p id="waiting-status" style="display: none;" role="status" aria-live="polite">Vérification du statut pairing et relais…</p>
|
||||
<div class="button-group">
|
||||
<button type="button" id="btn-login" class="primary">Se connecter</button>
|
||||
</div>
|
||||
@ -170,9 +210,18 @@
|
||||
</div>
|
||||
|
||||
<div id="connected-section" style="display: none;">
|
||||
<header class="connected-header" aria-label="Zone connectée">
|
||||
<div class="connected-avatar" aria-hidden="true" title="Avatar">👤</div>
|
||||
<button type="button" class="connected-notifications" aria-label="Notifications" title="Notifications">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true">
|
||||
<path d="M18 8A6 6 0 0 0 6 8c0 7-3 9-3 9h18s-3-2-3-9"></path>
|
||||
<path d="M13.73 21a2 2 0 0 1-3.46 0"></path>
|
||||
</svg>
|
||||
</button>
|
||||
</header>
|
||||
<h2>Vous êtes connecté</h2>
|
||||
<div id="user-info"></div>
|
||||
<p><a href="contrat.html">Le contrat</a> · <a href="membre.html">Qui êtes-vous ?</a></p>
|
||||
<p><a href="contrat.html">Le contrat</a> · <a href="membre.html">Qui êtes-vous ?</a> · <a href="technique.html">Réseau P2P</a> · <a href="cryptographie.html">Cryptographie</a></p>
|
||||
<div class="button-group">
|
||||
<button type="button" id="btn-logout" class="danger">Se déconnecter</button>
|
||||
</div>
|
||||
|
||||
@ -37,7 +37,7 @@
|
||||
</head>
|
||||
<body>
|
||||
<h1>Qui êtes-vous ?</h1>
|
||||
<p><a href="index.html">← Retour à l'accueil</a> · <a href="contrat.html">Voir le contrat</a></p>
|
||||
<p><a href="index.html">← Retour à l'accueil</a> · <a href="contrat.html">Voir le contrat</a> · <a href="technique.html">Réseau P2P</a> · <a href="cryptographie.html">Cryptographie</a></p>
|
||||
|
||||
<div class="highlight">
|
||||
<strong>En résumé :</strong> Vous êtes un <strong>membre</strong> qui peut avoir plusieurs appareils (Pairs).
|
||||
@ -177,6 +177,6 @@
|
||||
})();
|
||||
</script>
|
||||
|
||||
<p><a href="index.html">← Retour à l'accueil</a> · <a href="contrat.html">Voir le contrat</a></p>
|
||||
<p><a href="index.html">← Retour à l'accueil</a> · <a href="contrat.html">Voir le contrat</a> · <a href="technique.html">Réseau P2P</a> · <a href="cryptographie.html">Cryptographie</a></p>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
@ -18,14 +18,14 @@ import type { Validateurs } from 'service-login-verify';
|
||||
*/
|
||||
function getDefaultValidateurs(): Validateurs {
|
||||
const contractData = getSkeletonServiceContractData();
|
||||
const loginAction = contractData.actions.find((a) =>
|
||||
const loginAction = contractData.actions.find((a) =>
|
||||
a.datajson?.types_names_chiffres?.includes('login')
|
||||
);
|
||||
|
||||
|
||||
if (loginAction?.validateurs_action !== undefined) {
|
||||
return loginAction.validateurs_action;
|
||||
}
|
||||
|
||||
|
||||
// Fallback to contract validators if no action found
|
||||
return contractData.contrat.validateurs;
|
||||
}
|
||||
@ -35,9 +35,12 @@ export const DEFAULT_VALIDATEURS = getDefaultValidateurs();
|
||||
|
||||
/**
|
||||
* Contract structure (matches userwallet types).
|
||||
* version and types required for UserWallet iframe (MessageBase/MessageAValider).
|
||||
*/
|
||||
export interface Contrat {
|
||||
uuid: string;
|
||||
version?: string;
|
||||
types?: { types_uuid: string[]; types_names_chiffres: string };
|
||||
validateurs: {
|
||||
membres_du_role: Array<{
|
||||
membre_uuid: string;
|
||||
@ -57,9 +60,12 @@ export interface Contrat {
|
||||
|
||||
/**
|
||||
* Action structure (matches userwallet types).
|
||||
* version and types required for UserWallet iframe (MessageBase/MessageAValider).
|
||||
*/
|
||||
export interface Action {
|
||||
uuid: string;
|
||||
version?: string;
|
||||
types?: { types_uuid: string[]; types_names_chiffres: string };
|
||||
validateurs_action: {
|
||||
membres_du_role: Array<{
|
||||
membre_uuid: string;
|
||||
|
||||
@ -20,11 +20,21 @@ const iframeContainer = document.getElementById('iframe-container') as HTMLDivEl
|
||||
const userInfo = document.getElementById('user-info') as HTMLDivElement;
|
||||
const loginSection = document.getElementById('login-section') as HTMLDivElement;
|
||||
const connectedSection = document.getElementById('connected-section') as HTMLDivElement;
|
||||
const waitingStatusEl = document.getElementById('waiting-status') as HTMLParagraphElement | null;
|
||||
|
||||
const nonceCache = new NonceCache(3600000);
|
||||
let currentValidateurs: Validateurs = DEFAULT_VALIDATEURS as Validateurs;
|
||||
let allowedPubkeys = buildAllowedPubkeysFromValidateurs(currentValidateurs);
|
||||
|
||||
/** Last pairing/relay status from iframe. Connected section is shown only when both are satisfied. */
|
||||
let pairingRelayStatus: { pairingSatisfied: boolean; relayOk: boolean } | null =
|
||||
null;
|
||||
|
||||
/** True when user clicked "Se connecter"; we send navigate-login once iframe is ready. */
|
||||
let loginRequested = false;
|
||||
/** True when iframe has fired load; we can send contract / navigate-login. */
|
||||
let iframeLoaded = false;
|
||||
|
||||
// Store received contract to send to iframe
|
||||
// Initialize with skeleton service contract
|
||||
const skeletonContractData = getSkeletonServiceContractData();
|
||||
@ -68,7 +78,23 @@ function isLoggedIn(): boolean {
|
||||
return getSession() !== null;
|
||||
}
|
||||
|
||||
/** True when user can access connected section: logged in + pairing satisfied + relay OK. */
|
||||
function canShowConnectedSection(): boolean {
|
||||
if (!isLoggedIn() || pairingRelayStatus === null) {
|
||||
return false;
|
||||
}
|
||||
return pairingRelayStatus.pairingSatisfied && pairingRelayStatus.relayOk;
|
||||
}
|
||||
|
||||
function showLoginInterface(): void {
|
||||
showLoginInterfaceWithIframe(false);
|
||||
}
|
||||
|
||||
/**
|
||||
* Show login section. When waitingForStatus is true, also show iframe so it loads
|
||||
* and can send pairing-relay-status (needed when user has session but refreshed page).
|
||||
*/
|
||||
function showLoginInterfaceWithIframe(waitingForStatus: boolean): void {
|
||||
if (loginSection !== null) {
|
||||
loginSection.style.display = 'block';
|
||||
}
|
||||
@ -76,7 +102,10 @@ function showLoginInterface(): void {
|
||||
connectedSection.style.display = 'none';
|
||||
}
|
||||
if (iframeContainer !== null) {
|
||||
iframeContainer.style.display = 'none';
|
||||
iframeContainer.style.display = waitingForStatus ? 'block' : 'none';
|
||||
}
|
||||
if (waitingStatusEl !== null) {
|
||||
waitingStatusEl.style.display = waitingForStatus ? 'block' : 'none';
|
||||
}
|
||||
}
|
||||
|
||||
@ -93,22 +122,41 @@ function showConnectedInterface(): void {
|
||||
}
|
||||
|
||||
function updateUI(): void {
|
||||
if (isLoggedIn()) {
|
||||
if (canShowConnectedSection()) {
|
||||
console.warn('[skeleton] showing connected section');
|
||||
showConnectedInterface();
|
||||
const session = getSession();
|
||||
if (session !== null && userInfo !== null) {
|
||||
const publicKeys = session.proof.signatures.map((sig) => sig.cle_publique.substring(0, 16) + '...').join(', ');
|
||||
const publicKeys = session.proof.signatures
|
||||
.map((sig) => sig.cle_publique.substring(0, 16) + '...')
|
||||
.join(', ');
|
||||
userInfo.textContent = `Connecté - Clés publiques: ${publicKeys}`;
|
||||
}
|
||||
} else if (isLoggedIn() && pairingRelayStatus === null) {
|
||||
showLoginInterfaceWithIframe(true);
|
||||
} else {
|
||||
showLoginInterface();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Service and member UUIDs from current contract (for navigate-login).
|
||||
*/
|
||||
function getLoginContext(): { service: string; member: string } | null {
|
||||
const contrat = storedContract ?? skeletonContractData.contrat;
|
||||
if (contrat === null) {
|
||||
return null;
|
||||
}
|
||||
const firstMember = contrat.validateurs?.membres_du_role?.[0];
|
||||
const member = firstMember?.membre_uuid ?? '';
|
||||
return { service: contrat.uuid, member };
|
||||
}
|
||||
|
||||
function sendContractToIframe(): void {
|
||||
if (iframe?.contentWindow == null || storedContract === null) {
|
||||
return;
|
||||
}
|
||||
const ctx = getLoginContext();
|
||||
iframe.contentWindow.postMessage(
|
||||
{
|
||||
type: 'contract',
|
||||
@ -116,20 +164,50 @@ function sendContractToIframe(): void {
|
||||
contrat: storedContract,
|
||||
contrats_fils: storedContratsFils,
|
||||
actions: storedActions,
|
||||
serviceUuid: ctx?.service,
|
||||
membreUuid: ctx?.member ?? undefined,
|
||||
},
|
||||
},
|
||||
USERWALLET_ORIGIN,
|
||||
);
|
||||
}
|
||||
|
||||
function handleLogin(): void {
|
||||
if (iframe?.contentWindow == null) {
|
||||
function sendNavigateLoginToIframe(): void {
|
||||
const ctx = getLoginContext();
|
||||
if (ctx === null || iframe?.contentWindow == null) {
|
||||
return;
|
||||
}
|
||||
iframe.contentWindow.postMessage(
|
||||
{
|
||||
type: 'navigate-login',
|
||||
payload: { service: ctx.service, member: ctx.member },
|
||||
},
|
||||
USERWALLET_ORIGIN,
|
||||
);
|
||||
}
|
||||
|
||||
function onIframeLoad(): void {
|
||||
iframeLoaded = true;
|
||||
sendContractToIframe();
|
||||
if (loginRequested) {
|
||||
sendNavigateLoginToIframe();
|
||||
loginRequested = false;
|
||||
}
|
||||
}
|
||||
|
||||
function handleLogin(): void {
|
||||
if (iframe === null) {
|
||||
return;
|
||||
}
|
||||
loginRequested = true;
|
||||
if (iframeContainer !== null) {
|
||||
iframeContainer.style.display = 'block';
|
||||
}
|
||||
if (iframeLoaded) {
|
||||
sendContractToIframe();
|
||||
sendNavigateLoginToIframe();
|
||||
loginRequested = false;
|
||||
}
|
||||
}
|
||||
|
||||
function handleLogout(): void {
|
||||
@ -161,6 +239,19 @@ function updateValidatorsFromContract(
|
||||
|
||||
function handleMessage(msg: MessageEvent): void {
|
||||
const d = msg.data;
|
||||
if (msg.origin !== USERWALLET_ORIGIN) {
|
||||
const type = d?.type as string | undefined;
|
||||
if (type === 'pairing-relay-status' || type === 'login-proof') {
|
||||
console.warn(
|
||||
'[skeleton] message ignored: origin mismatch',
|
||||
'received',
|
||||
msg.origin,
|
||||
'expected',
|
||||
USERWALLET_ORIGIN,
|
||||
);
|
||||
}
|
||||
return;
|
||||
}
|
||||
if (d?.type === undefined) {
|
||||
return;
|
||||
}
|
||||
@ -202,6 +293,31 @@ function handleMessage(msg: MessageEvent): void {
|
||||
});
|
||||
if (result.accept) {
|
||||
setSession(proof);
|
||||
console.warn('[skeleton] login-proof received, session set');
|
||||
updateUI();
|
||||
} else {
|
||||
console.warn('[skeleton] login-proof rejected:', result.reason);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
if (d.type === 'pairing-relay-status') {
|
||||
const payload = d.payload as { pairingSatisfied?: boolean; relayOk?: boolean };
|
||||
if (
|
||||
typeof payload?.pairingSatisfied === 'boolean' &&
|
||||
typeof payload?.relayOk === 'boolean'
|
||||
) {
|
||||
pairingRelayStatus = {
|
||||
pairingSatisfied: payload.pairingSatisfied,
|
||||
relayOk: payload.relayOk,
|
||||
};
|
||||
console.warn(
|
||||
'[skeleton] pairing-relay-status received:',
|
||||
'pairingSatisfied=',
|
||||
payload.pairingSatisfied,
|
||||
'relayOk=',
|
||||
payload.relayOk,
|
||||
);
|
||||
updateUI();
|
||||
}
|
||||
return;
|
||||
@ -217,6 +333,7 @@ function init(): void {
|
||||
return;
|
||||
}
|
||||
iframe.src = USERWALLET_ORIGIN;
|
||||
iframe.addEventListener('load', onIframeLoad);
|
||||
btnLogin?.addEventListener('click', handleLogin);
|
||||
btnLogout?.addEventListener('click', handleLogout);
|
||||
window.addEventListener('message', handleMessage);
|
||||
@ -231,10 +348,6 @@ function init(): void {
|
||||
allowedPubkeys = buildAllowedPubkeysFromValidateurs(validateurs);
|
||||
}
|
||||
|
||||
iframe.addEventListener('load', () => {
|
||||
sendContractToIframe();
|
||||
});
|
||||
|
||||
updateUI();
|
||||
}
|
||||
|
||||
|
||||
@ -37,10 +37,16 @@ const SKELETON_ACTION_TYPE_UUID = 'fdb638c2-357a-4da2-bda1-966c6ca0de9d';
|
||||
|
||||
/**
|
||||
* Get skeleton service contract with current public key.
|
||||
* Includes version and types for MessageBase/MessageAValider (UserWallet iframe).
|
||||
*/
|
||||
function getSkeletonServiceContract(): Contrat {
|
||||
return {
|
||||
uuid: SKELETON_CONTRACT_UUID,
|
||||
version: '1.0',
|
||||
types: {
|
||||
types_uuid: [SKELETON_CONTRACT_TYPE_UUID],
|
||||
types_names_chiffres: 'contrat',
|
||||
},
|
||||
validateurs: {
|
||||
membres_du_role: [
|
||||
{
|
||||
@ -60,16 +66,23 @@ function getSkeletonServiceContract(): Contrat {
|
||||
services_uuid: [SKELETON_SERVICE_UUID],
|
||||
types_uuid: [SKELETON_CONTRACT_TYPE_UUID],
|
||||
label: 'Contrat de service Website Skeleton',
|
||||
role: 'validation (contrat)',
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Get skeleton login action with current public key.
|
||||
* Includes version and types for MessageBase/MessageAValider (UserWallet iframe).
|
||||
*/
|
||||
function getSkeletonLoginAction(): Action {
|
||||
return {
|
||||
uuid: SKELETON_LOGIN_ACTION_UUID,
|
||||
version: '1.0',
|
||||
types: {
|
||||
types_uuid: [SKELETON_ACTION_TYPE_UUID],
|
||||
types_names_chiffres: 'action,login',
|
||||
},
|
||||
validateurs_action: {
|
||||
membres_du_role: [
|
||||
{
|
||||
@ -89,6 +102,7 @@ function getSkeletonLoginAction(): Action {
|
||||
services_uuid: [SKELETON_SERVICE_UUID],
|
||||
types_uuid: [SKELETON_ACTION_TYPE_UUID],
|
||||
label: 'Action login Website Skeleton',
|
||||
role: 'validation du login',
|
||||
},
|
||||
};
|
||||
}
|
||||
@ -96,17 +110,17 @@ function getSkeletonLoginAction(): Action {
|
||||
/**
|
||||
* Get skeleton service public key from environment.
|
||||
* The public key must be a valid secp256k1 compressed public key (66 hex chars: 02/03 + 64 hex).
|
||||
*
|
||||
*
|
||||
* To configure: set VITE_SKELETON_SERVICE_PUBLIC_KEY environment variable.
|
||||
* Example: VITE_SKELETON_SERVICE_PUBLIC_KEY=02abc123... npm run dev
|
||||
*
|
||||
*
|
||||
* If not configured, returns a placeholder and logs a warning.
|
||||
* The placeholder will not work for real signature verification.
|
||||
*/
|
||||
function getSkeletonPublicKey(): string {
|
||||
const env = typeof import.meta !== 'undefined' ? (import.meta as { env?: { VITE_SKELETON_SERVICE_PUBLIC_KEY?: string } }).env : undefined;
|
||||
const configuredKey = env?.VITE_SKELETON_SERVICE_PUBLIC_KEY;
|
||||
|
||||
|
||||
if (configuredKey !== undefined && configuredKey.length === 66 && /^[0-9a-fA-F]{66}$/.test(configuredKey)) {
|
||||
if (configuredKey.startsWith('02') || configuredKey.startsWith('03')) {
|
||||
return configuredKey;
|
||||
@ -117,7 +131,7 @@ function getSkeletonPublicKey(): string {
|
||||
);
|
||||
return '02' + '0'.repeat(64);
|
||||
}
|
||||
|
||||
|
||||
console.warn(
|
||||
'VITE_SKELETON_SERVICE_PUBLIC_KEY not configured. ' +
|
||||
'Set it to a valid secp256k1 compressed public key (66 hex chars, starting with 02 or 03). ' +
|
||||
|
||||
312
website-skeleton/technique.html
Normal file
312
website-skeleton/technique.html
Normal file
@ -0,0 +1,312 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="fr">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<title>Réseau P2P – 4NK un nouveau web</title>
|
||||
<style>
|
||||
* { box-sizing: border-box; }
|
||||
body {
|
||||
font-family: system-ui, -apple-system, sans-serif;
|
||||
max-width: 900px;
|
||||
margin: 0 auto;
|
||||
padding: 1rem;
|
||||
line-height: 1.6;
|
||||
color: #333;
|
||||
}
|
||||
h1 { font-size: 1.5rem; margin-bottom: 1rem; color: #222; }
|
||||
h2 { font-size: 1.2rem; margin-top: 1.5rem; margin-bottom: 0.5rem; color: #333; }
|
||||
h3 { font-size: 1rem; margin-top: 1rem; margin-bottom: 0.35rem; font-weight: 600; }
|
||||
p { margin: 0.75rem 0; }
|
||||
ul { margin: 0.5rem 0; padding-left: 1.5rem; }
|
||||
li { margin: 0.4rem 0; }
|
||||
a { color: #007bff; }
|
||||
a:hover { text-decoration: underline; }
|
||||
.highlight { background: #e8f4fd; padding: 1rem; border-radius: 8px; border-left: 4px solid #007bff; margin: 1rem 0; }
|
||||
.info { background: #d4edda; padding: 1rem; border-radius: 8px; border-left: 4px solid #28a745; margin: 1rem 0; }
|
||||
details { margin: 1rem 0; }
|
||||
summary { cursor: pointer; font-weight: 600; color: #555; }
|
||||
.meta { font-family: ui-monospace, monospace; font-size: 0.85rem; color: #666; background: #f5f5f5; padding: 0.2em 0.4em; border-radius: 4px; word-break: break-all; }
|
||||
.relay-list { list-style: none; padding-left: 0; }
|
||||
.relay-list > li { background: #f8f9fa; padding: 0.75rem 1rem; border-radius: 8px; border: 1px solid #e0e0e0; margin: 0.5rem 0; }
|
||||
.relay-list > li > strong { color: #007bff; }
|
||||
.endpoint-table { width: 100%; border-collapse: collapse; margin: 1rem 0; }
|
||||
.endpoint-table th, .endpoint-table td { padding: 0.5rem; text-align: left; border-bottom: 1px solid #ddd; }
|
||||
.endpoint-table th { background: #f5f5f5; font-weight: 600; }
|
||||
.endpoint-table code { background: #f5f5f5; padding: 0.2em 0.4em; border-radius: 4px; font-size: 0.9em; }
|
||||
@media (max-width: 768px) {
|
||||
body { padding: 0.75rem; }
|
||||
h1 { font-size: 1.25rem; }
|
||||
.endpoint-table { font-size: 0.9rem; }
|
||||
.endpoint-table th, .endpoint-table td { padding: 0.4rem; }
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<h1>Réseau P2P</h1>
|
||||
<p><a href="index.html">← Retour à l'accueil</a> · <a href="contrat.html">Le contrat</a> · <a href="membre.html">Qui êtes-vous ?</a> · <a href="cryptographie.html">Cryptographie</a></p>
|
||||
|
||||
<div class="highlight">
|
||||
<strong>En résumé :</strong> Au lieu d'utiliser un seul serveur central (comme Gmail ou Facebook),
|
||||
le système utilise plusieurs <strong>relais</strong> qui fonctionnent comme des boîtes aux lettres publiques.
|
||||
Vos messages chiffrés sont stockés dans ces relais, et vous pouvez choisir lesquels utiliser.
|
||||
</div>
|
||||
|
||||
<h2>Qu'est-ce qu'un réseau P2P ?</h2>
|
||||
<p>
|
||||
Imaginez que vous voulez envoyer une lettre. Dans un système classique (comme la poste traditionnelle),
|
||||
tous les courriers passent par un seul bureau central. Si ce bureau tombe en panne, plus rien ne fonctionne.
|
||||
</p>
|
||||
<p>
|
||||
Avec un réseau pair-à-pair (P2P), c'est différent : il y a plusieurs <strong>relais</strong> (comme plusieurs
|
||||
boîtes aux lettres) répartis sur Internet. Vous pouvez choisir où déposer vos messages, et si un relais
|
||||
ne fonctionne plus, vous pouvez utiliser un autre.
|
||||
</p>
|
||||
<p>
|
||||
Les avantages de cette approche :
|
||||
</p>
|
||||
<ul>
|
||||
<li><strong>Plus de robustesse</strong> : si un relais tombe en panne, d'autres continuent de fonctionner</li>
|
||||
<li><strong>Pas de point unique de défaillance</strong> : personne ne contrôle tout le système</li>
|
||||
<li><strong>Vous choisissez</strong> : vous décidez quels relais utiliser, comme choisir votre opérateur téléphonique</li>
|
||||
<li><strong>Copies multiples</strong> : vos messages peuvent être copiés sur plusieurs relais pour plus de sécurité</li>
|
||||
</ul>
|
||||
|
||||
<h2>Comment fonctionnent les relais ?</h2>
|
||||
<p>
|
||||
Un relais, c'est comme une boîte aux lettres publique sur Internet. Quand vous voulez communiquer avec quelqu'un,
|
||||
vous déposez trois choses séparément dans cette boîte :
|
||||
</p>
|
||||
<ol>
|
||||
<li><strong>Le message chiffré</strong> : votre message codé (comme une lettre dans une enveloppe scellée)</li>
|
||||
<li><strong>La signature</strong> : une preuve que c'est bien vous qui avez envoyé le message (comme votre signature manuscrite)</li>
|
||||
<li><strong>La clé de déchiffrement</strong> : la clé pour décoder le message (comme la clé de votre boîte aux lettres)</li>
|
||||
</ol>
|
||||
<div class="info">
|
||||
<strong>Pourquoi séparer ces trois éléments ?</strong> C'est comme mettre votre lettre, votre signature et votre clé
|
||||
dans trois enveloppes différentes. Même si quelqu'un trouve une enveloppe, il ne peut pas tout faire sans les autres.
|
||||
C'est plus sûr !
|
||||
</div>
|
||||
|
||||
<h3>Comment récupérer vos messages ?</h3>
|
||||
<p>
|
||||
Le système fonctionne comme une boîte aux lettres classique : vous allez vérifier régulièrement s'il y a du courrier.
|
||||
C'est ce qu'on appelle le modèle <strong>"pull-only"</strong> (vous tirez les informations, elles ne vous sont pas poussées).
|
||||
</p>
|
||||
<ul>
|
||||
<li>Votre appareil vérifie périodiquement les relais pour voir s'il y a de nouveaux messages</li>
|
||||
<li>Pas de notification instantanée (comme une alerte SMS), mais une vérification régulière</li>
|
||||
<li>Communication simple via Internet, comme consulter une page web</li>
|
||||
</ul>
|
||||
<p>
|
||||
Cette méthode fonctionne partout, même dans les environnements les plus restrictifs, car elle utilise
|
||||
simplement le protocole HTTP (comme quand vous visitez un site web).
|
||||
</p>
|
||||
|
||||
<h3>Les relais se parlent entre eux</h3>
|
||||
<p>
|
||||
Les relais peuvent être configurés pour se copier mutuellement les messages :
|
||||
</p>
|
||||
<ul>
|
||||
<li>Quand vous déposez un message sur un relais, il peut automatiquement le copier vers d'autres relais</li>
|
||||
<li>Un système intelligent évite de copier plusieurs fois le même message (grâce à une empreinte unique)</li>
|
||||
<li>Résultat : vos messages sont disponibles sur plusieurs relais, comme avoir plusieurs copies de sauvegarde</li>
|
||||
</ul>
|
||||
<p>
|
||||
<em>Exemple :</em> Si vous déposez un message sur le relais A, et que A est configuré pour copier vers B et C,
|
||||
votre message sera disponible sur les trois relais. Si A tombe en panne, vous pouvez toujours récupérer votre message depuis B ou C.
|
||||
</p>
|
||||
|
||||
<h2>Quels relais utiliser ?</h2>
|
||||
<p>
|
||||
Par défaut, un relais principal est configuré. Vous pouvez en ajouter d'autres, jusqu'à <strong>8 relais au maximum</strong>.
|
||||
L'application tente de se connecter à tous les relais activés pour déposer et récupérer les messages.
|
||||
</p>
|
||||
<ul class="relay-list">
|
||||
<li>
|
||||
<strong>Relais principal (configuré automatiquement)</strong>
|
||||
<br>Adresse : <span class="meta">https://relay.certificator.4nkweb.com</span>
|
||||
<br>Ce relais est déjà configuré quand vous utilisez le système pour la première fois.
|
||||
</li>
|
||||
</ul>
|
||||
<div class="info">
|
||||
<strong>Jusqu'à 8 relais :</strong> Dans les paramètres de UserWallet, vous pouvez ajouter vos propres relais
|
||||
(comme plusieurs boîtes aux lettres). Maximum 8 au total. Vous activez ou désactivez chaque relais,
|
||||
et choisissez l'ordre de priorité. L'application utilise tous les relais activés pour déposer et récupérer.
|
||||
</div>
|
||||
|
||||
<h2>Pourquoi dupliquer les relais et les flux ?</h2>
|
||||
<p>
|
||||
Plus vous utilisez de relais, plus vos messages ont de chances d'être disponibles et de circuler.
|
||||
</p>
|
||||
<ul>
|
||||
<li><strong>Déposer sur plusieurs relais</strong> : quand vous envoyez un message, l'application le dépose sur chaque relais activé.
|
||||
Si un relais est indisponible, les autres reçoivent quand même le message. Comme poster la même lettre à plusieurs boîtes.</li>
|
||||
<li><strong>Récupérer depuis plusieurs relais</strong> : pour lire vos messages, l'application interroge chaque relais.
|
||||
Si un relais ne répond pas ou n'a pas le message, un autre peut l'avoir. Comme vérifier plusieurs boîtes aux lettres.</li>
|
||||
<li><strong>Résultat</strong> : plus de robustesse (un relais en panne ne bloque pas tout), plus de chances que vos messages
|
||||
soient bien livrés, et les relais peuvent se copier entre eux pour multiplier les copies.</li>
|
||||
</ul>
|
||||
<p>
|
||||
En résumé : configurer jusqu'à 8 relais et tous les activer, c'est maximiser les chemins pour envoyer et recevoir,
|
||||
sans dépendre d'un seul point.
|
||||
</p>
|
||||
|
||||
<h2>Les actions possibles avec un relais</h2>
|
||||
<p>
|
||||
Un relais offre plusieurs "actions" (appelées endpoints) que vous pouvez utiliser. C'est comme une boîte aux lettres
|
||||
avec plusieurs fonctions : déposer, récupérer, vérifier l'état, etc.
|
||||
</p>
|
||||
<p>
|
||||
<em>Note technique :</em> Ces actions utilisent le protocole HTTP (comme les sites web) avec des méthodes GET (lire)
|
||||
et POST (écrire).
|
||||
</p>
|
||||
<table class="endpoint-table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Méthode</th>
|
||||
<th>Endpoint</th>
|
||||
<th>Description</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr>
|
||||
<td><code>GET</code></td>
|
||||
<td><code>/health</code></td>
|
||||
<td>Vérifier si le relais fonctionne correctement</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><code>GET</code></td>
|
||||
<td><code>/messages?start=<ts>&end=<ts>&service=<uuid></code></td>
|
||||
<td>Récupérer les messages entre deux dates (optionnel : filtrer par service)</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><code>POST</code></td>
|
||||
<td><code>/messages</code></td>
|
||||
<td>Déposer un message chiffré dans le relais</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><code>GET</code></td>
|
||||
<td><code>/messages/:hash</code></td>
|
||||
<td>Récupérer un message précis grâce à son identifiant unique (hash)</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><code>GET</code></td>
|
||||
<td><code>/signatures/:hash</code></td>
|
||||
<td>Récupérer les signatures associées à un message</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><code>POST</code></td>
|
||||
<td><code>/signatures</code></td>
|
||||
<td>Déposer une signature dans le relais</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><code>GET</code></td>
|
||||
<td><code>/keys/:hash</code></td>
|
||||
<td>Récupérer les clés pour déchiffrer un message</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><code>POST</code></td>
|
||||
<td><code>/keys</code></td>
|
||||
<td>Déposer une clé de déchiffrement dans le relais</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><code>GET</code></td>
|
||||
<td><code>/metrics</code></td>
|
||||
<td>Consulter les statistiques du relais (nombre de messages, signatures, clés stockées)</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><code>GET</code></td>
|
||||
<td><code>/bloom</code></td>
|
||||
<td>Obtenir une liste des messages déjà vus (pour éviter de demander plusieurs fois le même message)</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
<h2>Comment les relais stockent les données</h2>
|
||||
<h3>Organisation du stockage</h3>
|
||||
<p>
|
||||
Les relais organisent les données comme une bibliothèque bien rangée :
|
||||
</p>
|
||||
<ul>
|
||||
<li><strong>Messages</strong> : rangés par identifiant unique (hash) pour les retrouver rapidement</li>
|
||||
<li><strong>Signatures</strong> : classées par message associé</li>
|
||||
<li><strong>Clés</strong> : classées par message associé</li>
|
||||
<li><strong>Éviter les doublons</strong> : le système se souvient des messages déjà vus pour ne pas les stocker plusieurs fois</li>
|
||||
<li><strong>Indexation</strong> : un système de classement permet de retrouver rapidement les messages d'un service particulier</li>
|
||||
</ul>
|
||||
|
||||
<h3>Protection contre les abus</h3>
|
||||
<p>
|
||||
Les relais sont protégés contre les utilisations abusives :
|
||||
</p>
|
||||
<ul>
|
||||
<li><strong>Limitation des requêtes</strong> : chaque adresse IP ne peut faire qu'un nombre limité de requêtes par minute</li>
|
||||
<li><strong>Contrôle d'accès</strong> : seuls les sites autorisés peuvent utiliser le relais</li>
|
||||
<li><strong>Timeout</strong> : si une requête prend trop de temps, elle est annulée automatiquement</li>
|
||||
<li><strong>Compression</strong> : les réponses sont compressées pour être plus rapides</li>
|
||||
</ul>
|
||||
|
||||
<h3>Éviter les doublons</h3>
|
||||
<p>
|
||||
Chaque message a une empreinte unique (comme une empreinte digitale). Le système utilise cette empreinte
|
||||
pour s'assurer qu'un même message n'est pas stocké plusieurs fois, même s'il arrive depuis plusieurs relais.
|
||||
Cela évite aussi que les messages tournent en boucle entre les relais.
|
||||
</p>
|
||||
|
||||
<details>
|
||||
<summary>Détails techniques pour les développeurs</summary>
|
||||
<h3>Configuration des relais pairs</h3>
|
||||
<p>
|
||||
Les administrateurs de relais peuvent configurer leurs relais pour se copier mutuellement les messages.
|
||||
Cela se fait via une variable de configuration :
|
||||
</p>
|
||||
<pre class="meta">PEER_RELAYS=http://relay1:3019,http://relay2:3019</pre>
|
||||
<p>
|
||||
Quand un message arrive sur un relais, il est automatiquement copié vers tous
|
||||
les relais pairs configurés (si le message n'a pas déjà été vu).
|
||||
</p>
|
||||
|
||||
<h3>Bloom filter</h3>
|
||||
<p>
|
||||
Le endpoint <code>/bloom</code> retourne une liste compacte des messages déjà vus par le relais.
|
||||
Les applications peuvent utiliser cette liste pour éviter de demander plusieurs fois le même message,
|
||||
ce qui réduit la charge sur le réseau et améliore les performances.
|
||||
</p>
|
||||
|
||||
<h3>Statistiques du relais</h3>
|
||||
<p>
|
||||
Le endpoint <code>/metrics</code> expose des statistiques au format Prometheus (pour le monitoring) :
|
||||
</p>
|
||||
<ul>
|
||||
<li>Nombre de messages stockés</li>
|
||||
<li>Nombre de signatures stockées</li>
|
||||
<li>Nombre de clés stockées</li>
|
||||
</ul>
|
||||
|
||||
<h3>Gestion des erreurs et timeouts</h3>
|
||||
<p>
|
||||
Les applications utilisent un délai d'attente de 15 secondes pour toutes les requêtes vers les relais.
|
||||
Si une requête échoue ou prend trop de temps, l'application peut essayer un autre relais configuré,
|
||||
ou réessayer plus tard avec un délai progressif (backoff exponentiel).
|
||||
</p>
|
||||
</details>
|
||||
|
||||
<h2>Pourquoi cette architecture ?</h2>
|
||||
<p>
|
||||
Cette façon de faire a été choisie pour plusieurs raisons importantes :
|
||||
</p>
|
||||
<ul>
|
||||
<li><strong>Simplicité</strong> : utilise le protocole HTTP standard (comme les sites web), donc ça fonctionne partout</li>
|
||||
<li><strong>Fiabilité</strong> : pas besoin de maintenir une connexion permanente, comme consulter une page web</li>
|
||||
<li><strong>Capacité</strong> : chaque relais peut gérer de nombreux utilisateurs en même temps</li>
|
||||
<li><strong>Contrôle</strong> : vous choisissez quels relais utiliser, personne ne vous impose un choix</li>
|
||||
<li><strong>Sécurité</strong> : les messages, signatures et clés sont séparés, ce qui rend le système plus sûr</li>
|
||||
</ul>
|
||||
<p>
|
||||
En résumé, c'est un système simple, fiable et qui vous donne le contrôle, tout en étant sécurisé.
|
||||
</p>
|
||||
|
||||
<p><a href="index.html">← Retour à l'accueil</a> · <a href="contrat.html">Le contrat</a> · <a href="membre.html">Qui êtes-vous ?</a> · <a href="cryptographie.html">Cryptographie</a></p>
|
||||
</body>
|
||||
</html>
|
||||
@ -5,7 +5,7 @@ export default defineConfig({
|
||||
build: {
|
||||
outDir: 'dist',
|
||||
rollupOptions: {
|
||||
input: ['index.html', 'contrat.html', 'membre.html'],
|
||||
input: ['index.html', 'contrat.html', 'membre.html', 'technique.html', 'cryptographie.html'],
|
||||
},
|
||||
},
|
||||
server: {
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user