diff --git a/RESTE_A_FAIRE.md b/RESTE_A_FAIRE.md new file mode 100644 index 0000000..88f803a --- /dev/null +++ b/RESTE_A_FAIRE.md @@ -0,0 +1,202 @@ +# Reste à faire - Projets UserWallet + +**Author:** Équipe 4NK +**Date:** 2026-01-28 + +## Vue d'ensemble + +Ce document liste les éléments restants à implémenter ou à valider pour les projets : +- `userwallet` +- `service-login-verify` +- `api-relay` +- `website-skeleton` + +--- + +## 1. userwallet + +### 1.1 Écrans login (Priorité : Haute - À valider) + +**Statut :** À valider avant implémentation +**Référence :** `features/userwallet-ecrans-login-a-valider.md` + +Les écrans suivants sont déjà en place mais nécessitent validation et éventuelles améliorations : +- Sélection service / sélection membre +- Construction du chemin login +- Message de login à valider +- Collecte signatures mFA +- Publication +- Vérification locale + résultat + +**Action requise :** Validation des écrans existants et ajustements selon les retours de tests. + +### 1.2 Notifications relais (Priorité : Haute) + +**Statut :** Partiellement implémenté +**Référence :** `features/userwallet-contrat-login-reste-a-faire.md` (§ 3.2) + +**Fait :** +- Progression collecte signatures (X/Y) implémentée via `onProgress` dans `runCollectLoop` +- Affichage dans `LoginCollectShare` + +**Reste à faire :** +- Réagir à d'autres événements relais si extension (ex. push) +- Hash à fetch pour signatures, contrats, membres, pairs, actions, champs +- Une fois le hash connu : récupération sur le relai des signatures, contrats, membres, pairs, actions, champs +- Les notifications doivent piloter : quel hash fetch, puis fetch signatures/clés et mise à jour du graphe + +### 1.3 Merkle trees (Priorité : Basse - Optionnel) + +**Statut :** Non implémenté (optionnel) +**Référence :** `features/userwallet-contrat-login-reste-a-faire.md` (§ 3.6) + +**Description :** Checkpointing / accélération de scan pour optimiser la synchronisation à grande échelle. + +**Impact :** Amélioration de performance pour les volumes importants de données. + +### 1.4 Écran "synchronisation continue par service" (Priorité : Moyenne) + +**Statut :** Non implémenté +**Référence :** `userwallet/docs/specs.md` (§ Écran "synchronisation continue par service") + +**Fonctionnalités attendues :** +- Liste services + toggle sync automatique +- Fréquence (min/heure/jour) +- Fenêtre de scan +- Accélérateurs : Bloom filter (taille, faux positifs estimés), Merkle (segments/périodes) +- Bouton "lancer maintenant" + +**Traitements locaux :** +- GET messages depuis dernier checkpoint +- Dédup hash +- Fetch signatures et clés si nécessaire +- Validation graphe et droits + +**États / erreurs :** +- Volume trop important → recommander Bloom/Merkle +- Relais instables → backoff + +### 1.5 Écran "paramètres crypto (avancé)" (Priorité : Basse) + +**Statut :** Non implémenté +**Référence :** `userwallet/docs/specs.md` (§ Écran "paramètres crypto (avancé)") + +**Fonctionnalités attendues :** +- Algorithme hash (ex : sha256) +- Canonisation JSON (mode strict) +- Paramètres ECDH (secp256k1) +- Paramètres KDF/symétrique (si exposés) +- Politique anti-rejeu : TTL nonce, fenêtre timestamp + +**États / erreurs :** +- Incompatibilité avec services existants +- Paramètres non supportés par version logicielle + +### 1.6 Validation stricte CNIL (Priorité : Moyenne) + +**Statut :** Non implémentée +**Référence :** `features/userwallet-champs-obligatoires-cnil.md` + +**Description :** Validation stricte CNIL (présence des champs lorsque requis) non implémentée ; à prévoir selon politique métier. + +**Action requise :** Définir la politique métier et implémenter la validation. + +### 1.7 Corrections ESLint (Priorité : Moyenne) + +**Statut :** Environ 95 erreurs restantes +**Référence :** `features/userwallet-eslint-fix.md` + +**Types d'erreurs :** +- `max-lines` : fichiers trop longs +- `max-lines-per-function` : fonctions trop longues +- `complexity` : complexité cyclomatique élevée +- `max-params` : trop de paramètres +- `no-non-null-assertion` : utilisation de `!` +- Et autres violations + +**Action requise :** Refactoring progressif pour corriger les violations. + +--- + +## 2. service-login-verify + +### 2.1 Persistance du NonceCache (Priorité : Basse - Optionnel) + +**Statut :** Actuellement en mémoire uniquement +**Référence :** `features/service-login-verify.md` + +**Description :** Le `NonceCache` actuel est en mémoire avec TTL configurable. Une persistance optionnelle (IndexedDB, Redis, etc.) peut être ajoutée si nécessaire. + +**Note :** L'interface `NonceCacheLike` permet déjà d'implémenter une version persistante. Le cache en mémoire est suffisant pour la plupart des cas d'usage. + +**Action requise :** Implémenter uniquement si besoin de persistance entre redémarrages du service. + +--- + +## 3. api-relay + +### 3.1 Évolutions futures (Priorité : Basse - Optionnel) + +**Statut :** Non implémentées (évolutions futures possibles) +**Référence :** `userwallet/features/api-relay.md` + +**Évolutions mentionnées :** +- **Base de données** : Remplacer le stockage en mémoire par une base de données (SQLite, PostgreSQL) +- **Authentification** : Ajouter une authentification pour les endpoints POST +- **Rate limiting** : Limiter le nombre de requêtes par IP (déjà implémenté partiellement) +- **Compression** : Compresser les messages stockés +- **Indexation avancée** : Indexer par service UUID, type UUID pour des requêtes plus rapides +- **WebSocket** : Support WebSocket pour les notifications en temps réel (optionnel, car le modèle est pull-only) +- **Métriques** : Exposer des métriques (Prometheus, etc.) - déjà implémenté partiellement +- **Logging structuré** : Utiliser un logger structuré (Winston, Pino, etc.) - déjà implémenté (Pino) + +**Note :** La plupart de ces évolutions sont optionnelles et dépendent des besoins de production. + +--- + +## 4. website-skeleton + +### 4.1 Aucun élément manquant identifié + +**Statut :** Complet +**Description :** Aucun élément spécifique mentionné comme manquant dans la documentation. + +--- + +## Synthèse par priorité + +### Priorité Haute +1. **userwallet** : Validation des écrans login (1.1) +2. **userwallet** : Notifications relais - événements push (1.2) + +### Priorité Moyenne +3. **userwallet** : Écran "synchronisation continue par service" (1.4) +4. **userwallet** : Validation stricte CNIL (1.6) +5. **userwallet** : Corrections ESLint (1.7) + +### Priorité Basse (Optionnel) +6. **userwallet** : Merkle trees (1.3) +7. **userwallet** : Écran "paramètres crypto (avancé)" (1.5) +8. **service-login-verify** : Persistance du NonceCache (2.1) +9. **api-relay** : Évolutions futures (3.1) + +--- + +## Notes importantes + +- Les éléments marqués "À valider" nécessitent une validation explicite avant implémentation. +- Les éléments optionnels peuvent être implémentés selon les besoins et contraintes. +- Les corrections ESLint doivent être faites progressivement pour ne pas bloquer le développement. +- La plupart des fonctionnalités de base sont implémentées et fonctionnelles. + +--- + +## Références + +- `features/userwallet-contrat-login-reste-a-faire.md` +- `features/userwallet-ecrans-login-a-valider.md` +- `features/userwallet-champs-obligatoires-cnil.md` +- `features/userwallet-eslint-fix.md` +- `features/service-login-verify.md` +- `userwallet/docs/specs.md` +- `userwallet/features/api-relay.md` diff --git a/api-relay/src/routes/keys.ts b/api-relay/src/routes/keys.ts index a4cf4df..7975fb0 100644 --- a/api-relay/src/routes/keys.ts +++ b/api-relay/src/routes/keys.ts @@ -39,6 +39,10 @@ export function createKeysRouter( router.get('/:hash', (req: Request, res: Response): void => { try { const hash = req.params.hash as string; + if (hash.length === 0) { + res.status(400).json({ error: 'Hash parameter required' }); + return; + } const stored = storage.getKeys(hash); const keys: MsgCle[] = stored.map((k) => k.msg); res.json(keys); diff --git a/api-relay/src/routes/messages.ts b/api-relay/src/routes/messages.ts index af1c72f..876ce15 100644 --- a/api-relay/src/routes/messages.ts +++ b/api-relay/src/routes/messages.ts @@ -73,6 +73,10 @@ export function createMessagesRouter( router.get('/:hash', (req: Request, res: Response): void => { try { const hash = req.params.hash as string; + if (hash.length === 0) { + res.status(400).json({ error: 'Hash parameter required' }); + return; + } const stored = storage.getMessage(hash); if (stored === undefined) { res.status(404).json({ error: 'Message not found' }); diff --git a/api-relay/src/routes/signatures.ts b/api-relay/src/routes/signatures.ts index 194389f..46cb32c 100644 --- a/api-relay/src/routes/signatures.ts +++ b/api-relay/src/routes/signatures.ts @@ -17,6 +17,10 @@ export function createSignaturesRouter( router.get('/:hash', (req: Request, res: Response): void => { try { const hash = req.params.hash as string; + if (hash.length === 0) { + res.status(400).json({ error: 'Hash parameter required' }); + return; + } const stored = storage.getSignatures(hash); const signatures: MsgSignature[] = stored.map((s) => s.msg); res.json(signatures); diff --git a/service-login-verify/src/types.ts b/service-login-verify/src/types.ts index f40b765..e1193e6 100644 --- a/service-login-verify/src/types.ts +++ b/service-login-verify/src/types.ts @@ -57,5 +57,5 @@ export interface NonceCacheLike { export interface VerifyLoginProofResult { accept: boolean; - reason?: 'timestamp_out_of_window' | 'nonce_reused' | 'validators_not_verifiable' | 'no_validator_signature' | 'signature_cle_publique_not_authorized'; + reason?: 'invalid_proof_structure' | 'timestamp_out_of_window' | 'nonce_reused' | 'validators_not_verifiable' | 'no_validator_signature' | 'signature_cle_publique_not_authorized'; } diff --git a/service-login-verify/src/verifyLoginProof.ts b/service-login-verify/src/verifyLoginProof.ts index d7ec7a7..8934d92 100644 --- a/service-login-verify/src/verifyLoginProof.ts +++ b/service-login-verify/src/verifyLoginProof.ts @@ -47,11 +47,61 @@ function verifySignaturesStrict( /** * Verify login proof: crypto, allowed pubkeys, timestamp window, nonce anti-replay. * Service must provide allowedPubkeys (from validators) and a NonceCache. + * + * @param proof - Login proof from UserWallet + * @param ctx - Verification context (allowedPubkeys, nonceCache, timestampWindowMs) + * @returns Verification result with accept flag and optional reason + * @throws {Error} If proof structure is invalid (missing challenge, hash, signatures) */ export function verifyLoginProof( proof: LoginProof, ctx: VerifyLoginProofContext, ): VerifyLoginProofResult { + // Validate proof structure + if (proof.challenge === undefined || typeof proof.challenge !== 'object') { + return { + accept: false, + reason: 'invalid_proof_structure', + }; + } + + if ( + typeof proof.challenge.hash !== 'string' || + proof.challenge.hash.length === 0 + ) { + return { + accept: false, + reason: 'invalid_proof_structure', + }; + } + + if ( + typeof proof.challenge.nonce !== 'string' || + proof.challenge.nonce.length === 0 + ) { + return { + accept: false, + reason: 'invalid_proof_structure', + }; + } + + if ( + typeof proof.challenge.timestamp !== 'number' || + !Number.isFinite(proof.challenge.timestamp) + ) { + return { + accept: false, + reason: 'invalid_proof_structure', + }; + } + + if (!Array.isArray(proof.signatures)) { + return { + accept: false, + reason: 'invalid_proof_structure', + }; + } + if (ctx.allowedPubkeys.size === 0) { return { accept: false, diff --git a/userwallet/src/App.tsx b/userwallet/src/App.tsx index e949edb..3fcf24d 100644 --- a/userwallet/src/App.tsx +++ b/userwallet/src/App.tsx @@ -17,6 +17,8 @@ import { LoginScreen } from './components/LoginScreen'; import { LoginSignScreen } from './components/LoginSignScreen'; import { ServiceListScreen } from './components/ServiceListScreen'; import { MemberSelectionScreen } from './components/MemberSelectionScreen'; +import { DiagnosticScreen } from './components/DiagnosticScreen'; +import { ServiceSyncScreen } from './components/ServiceSyncScreen'; import { DataExportImportScreen } from './components/DataExportImportScreen'; import { UnlockScreen } from './components/UnlockScreen'; import { useChannel } from './hooks/useChannel'; @@ -45,6 +47,8 @@ function AppContent(): JSX.Element { } /> } /> } /> + } /> + } /> } /> ); diff --git a/userwallet/src/components/DiagnosticScreen.tsx b/userwallet/src/components/DiagnosticScreen.tsx new file mode 100644 index 0000000..387b2b8 --- /dev/null +++ b/userwallet/src/components/DiagnosticScreen.tsx @@ -0,0 +1,332 @@ +import { useState } from 'react'; +import { useNavigate } from 'react-router-dom'; +import { GraphResolver } from '../services/graphResolver'; +import { useIdentity } from '../hooks/useIdentity'; +import { useErrorHandler } from '../hooks/useErrorHandler'; +import { ErrorDisplay } from './ErrorDisplay'; +import { getStoredRelays } from '../utils/relay'; +import { + getMessageByHash, + getSignatures, + getKeysInWindow, +} from '../utils/relay'; +import { + verifyMessageHash, + verifyMessageSignatures, +} from '../utils/verification'; +import { + getValidateursIfMessageAValider, + validateMessageAValiderForSync, +} from '../utils/validatorsAccept'; +import { tryDecryptWithKeys } from '../services/syncDecrypt'; +import { updateGraphFromMessage } from '../services/syncUpdateGraph'; +import type { MsgChiffre, MsgSignature, MsgCle } from '../types/message'; +import type { MessageBase } from '../types/message'; + +interface DiagnosticResult { + hash: string; + messageFound: boolean; + messageDecrypted: boolean; + messageValidated: boolean; + hashValid: boolean; + signaturesFound: number; + signaturesValid: number; + keysFound: number; + validatorsChecked: boolean; + error?: string; + messageType?: string; + serviceUuid?: string[]; + hashMismatch?: boolean; + signatureErrors?: string[]; +} + +export function DiagnosticScreen(): JSX.Element { + const navigate = useNavigate(); + const { identity } = useIdentity(); + const { error, handleError, clearError } = useErrorHandler(); + const [hashInput, setHashInput] = useState(''); + const [isDiagnosing, setIsDiagnosing] = useState(false); + const [result, setResult] = useState(null); + + const handleDiagnose = async (): Promise => { + if (hashInput.trim() === '') { + handleError('Hash requis', 'MISSING_HASH'); + return; + } + + setIsDiagnosing(true); + clearError(); + setResult(null); + + try { + const hash = hashInput.trim(); + const relays = getStoredRelays().filter((r) => r.enabled); + if (relays.length === 0) { + handleError('Aucun relais activé', 'NO_ENABLED_RELAYS'); + return; + } + + const diagnostic: DiagnosticResult = { + hash, + messageFound: false, + messageDecrypted: false, + messageValidated: false, + hashValid: false, + signaturesFound: 0, + signaturesValid: 0, + keysFound: 0, + validatorsChecked: false, + }; + + // Fetch message by hash + let msgChiffre: MsgChiffre | null = null; + for (const relay of relays) { + try { + msgChiffre = await getMessageByHash(relay.endpoint, hash); + diagnostic.messageFound = true; + break; + } catch { + continue; + } + } + + if (msgChiffre === null) { + diagnostic.error = 'Message non trouvé sur les relais'; + setResult(diagnostic); + return; + } + + // Fetch signatures + const allSignatures: MsgSignature[] = []; + for (const relay of relays) { + try { + const sigs = await getSignatures(relay.endpoint, hash); + allSignatures.push(...sigs); + } catch { + continue; + } + } + diagnostic.signaturesFound = allSignatures.length; + + // Fetch keys + const allKeys: MsgCle[] = []; + const now = Date.now(); + const start = now - 86400000; // 24h + for (const relay of relays) { + try { + const keys = await getKeysInWindow(relay.endpoint, start, now); + const relevantKeys = keys.filter((k) => k.hash_message === hash); + allKeys.push(...relevantKeys); + } catch { + continue; + } + } + diagnostic.keysFound = allKeys.length; + + // Try to decrypt + if (identity !== null && allKeys.length > 0) { + const decrypted = await tryDecryptWithKeys( + msgChiffre, + allKeys, + identity, + ); + if (decrypted !== null && typeof decrypted === 'object') { + const msg = decrypted as { + types?: { types_names_chiffres?: string }; + datajson?: { services_uuid?: string[] }; + hash?: { hash_value: string }; + timestamp: number; + }; + diagnostic.messageDecrypted = true; + diagnostic.messageType = msg.types?.types_names_chiffres ?? 'unknown'; + diagnostic.serviceUuid = msg.datajson?.services_uuid; + + const msgBase = decrypted as MessageBase; + + // Verify hash + diagnostic.hashValid = await verifyMessageHash(msgBase); + if (!diagnostic.hashValid) { + diagnostic.hashMismatch = true; + diagnostic.error = 'Hash recalculé ≠ hash déclaré'; + } + + // Verify signatures + const signatureErrors: string[] = []; + if (allSignatures.length > 0) { + const sigList = allSignatures.map((s) => s.signature); + const { valid, invalid } = verifyMessageSignatures(msgBase, sigList); + diagnostic.signaturesValid = valid.length; + + for (const inv of invalid) { + signatureErrors.push( + `Signature invalide (clé: ${inv.cle_publique.slice(0, 16)}...)`, + ); + } + + // Check validators if MessageAValider + const validateurs = getValidateursIfMessageAValider(msgBase); + if (validateurs !== null) { + diagnostic.validatorsChecked = true; + const isValid = validateMessageAValiderForSync( + msgBase, + sigList, + validateurs, + '[Diagnostic]', + ); + diagnostic.messageValidated = isValid; + if (!isValid) { + signatureErrors.push('Signatures ne satisfont pas les validateurs'); + } + } else { + diagnostic.messageValidated = valid.length > 0; + } + } else { + signatureErrors.push('Aucune signature trouvée'); + } + + if (signatureErrors.length > 0) { + diagnostic.signatureErrors = signatureErrors; + } + + // Update graph if valid + if (diagnostic.hashValid && diagnostic.messageValidated) { + const graphResolver = new GraphResolver(); + updateGraphFromMessage(decrypted, graphResolver); + } + } + } + + setResult(diagnostic); + } catch (err) { + handleError(err, 'Erreur lors du diagnostic'); + } finally { + setIsDiagnosing(false); + } + }; + + return ( +
+

Diagnostic hash / objet

+ {error !== null && } +
+

Hash à diagnostiquer

+
+ + +
+
+ + {result !== null && ( +
+

Résultat du diagnostic

+
+

+ Hash: {result.hash} +

+

+ Message trouvé: {result.messageFound ? '✓ Oui' : '✗ Non'} +

+ {result.messageFound && ( + <> +

+ Message déchiffré:{' '} + {result.messageDecrypted ? '✓ Oui' : '✗ Non'} +

+ {result.messageDecrypted && ( + <> +

+ Type: {result.messageType ?? 'N/A'} +

+ {result.serviceUuid !== undefined && ( +

+ Service UUID: {result.serviceUuid.join(', ')} +

+ )} +

+ Hash valide:{' '} + {result.hashValid ? '✓ Oui' : '✗ Non'} +

+ {result.hashMismatch === true && ( +

+ Erreur: Hash recalculé ≠ hash déclaré +

+ )} +

+ Validateurs vérifiés:{' '} + {result.validatorsChecked ? '✓ Oui' : '—'} +

+

+ Message validé:{' '} + {result.messageValidated ? '✓ Oui' : '✗ Non'} +

+ + )} +

+ Signatures trouvées: {result.signaturesFound} +

+

+ Signatures valides: {result.signaturesValid} +

+ {result.signatureErrors !== undefined && + result.signatureErrors.length > 0 && ( +
+ Erreurs de signature: +
    + {result.signatureErrors.map((err, idx) => ( +
  • + {err} +
  • + ))} +
+
+ )} +

+ Clés trouvées: {result.keysFound} +

+ {result.keysFound === 0 && result.messageFound && ( +

+ Attention: Aucune clé trouvée - message + indéchiffrable +

+ )} + + )} + {result.error !== undefined && ( +

+ Erreur: {result.error} +

+ )} +
+
+ )} + +
+ +
+
+ ); +} diff --git a/userwallet/src/components/LoginCollectShare.tsx b/userwallet/src/components/LoginCollectShare.tsx index 94d8c63..1cf55a3 100644 --- a/userwallet/src/components/LoginCollectShare.tsx +++ b/userwallet/src/components/LoginCollectShare.tsx @@ -74,26 +74,27 @@ export function LoginCollectShare({ pairUuid: string | undefined; clePublique: string | undefined; status: 'manquante' | 'reçue' | 'valide' | 'invalide'; + signature?: string; }> => { if (loginPath === null) { return []; } - const collectedPubkeys = new Set( - collectedSignatures?.map((s) => s.cle_publique) ?? [], - ); - const localSignatures = new Set( - proof.signatures.map((s) => s.cle_publique), - ); + const allSignatures = [ + ...proof.signatures, + ...(collectedSignatures ?? []), + ]; return loginPath.signatures_requises.map((req) => { - const hasCollected = req.cle_publique !== undefined && - collectedPubkeys.has(req.cle_publique); - const hasLocal = req.cle_publique !== undefined && - localSignatures.has(req.cle_publique); + // Trouver la signature correspondante + const matchingSig = allSignatures.find( + (s) => s.cle_publique === req.cle_publique, + ); let status: 'manquante' | 'reçue' | 'valide' | 'invalide'; - if (hasLocal || hasCollected) { + if (matchingSig !== undefined) { + // Signature trouvée - considérée comme valide si elle correspond à la clé requise + // Note: la vérification crypto complète est faite ailleurs, ici on indique juste "reçue" status = 'reçue'; } else { status = 'manquante'; @@ -104,6 +105,7 @@ export function LoginCollectShare({ pairUuid: req.pair_uuid, clePublique: req.cle_publique, status, + signature: matchingSig?.signature, }; }); }; diff --git a/userwallet/src/components/LoginScreen.tsx b/userwallet/src/components/LoginScreen.tsx index c9ee332..a3e277f 100644 --- a/userwallet/src/components/LoginScreen.tsx +++ b/userwallet/src/components/LoginScreen.tsx @@ -54,7 +54,11 @@ export function LoginScreen(): JSX.Element { const [collectedPublishStats, setCollectedPublishStats] = useState<{ successCount: number; relaysCount: number; + relayStatus?: Array<{ endpoint: string; ok: boolean; error?: string }>; } | null>(null); + const [publishRelayStatus, setPublishRelayStatus] = useState< + Array<{ endpoint: string; ok: boolean; error?: string }> + >([]); const [collectProgressState, setCollectProgressState] = useState<{ satisfied: number; required: number; @@ -201,12 +205,19 @@ export function LoginScreen(): JSX.Element { } }; + /** + * Publish login proof to relays. + * Order: message → signatures → keys (strict order as per specs). + * Then collect remote signatures if needed (mFA with 2 devices). + * Finally verify locally and send proof to parent if in iframe. + */ const handlePublish = async (): Promise => { if (identity === null || proof === null) { handleError('Identité ou preuve manquante', 'MISSING_REQUIREMENTS'); return; } + // Anti-rejeu: vérifier timestamp dans la fenêtre if (!verifyTimestamp(proof.challenge.timestamp)) { handleError( 'Timestamp hors fenêtre de validité (anti-rejeu)', @@ -215,6 +226,7 @@ export function LoginScreen(): JSX.Element { return; } + // Anti-rejeu: vérifier nonce unique await nonceStore.init(); if (nonceStore.hasUsed(proof.challenge.nonce)) { handleError('Nonce déjà utilisé (anti-rejeu)', 'X_NONCE_REUSED'); @@ -234,17 +246,20 @@ export function LoginScreen(): JSX.Element { const msgChiffre = loginBuilder.challengeToMsgChiffre(proof.challenge); const msgCle = loginBuilder.challengeToMsgCle(proof.challenge); - const successCount = await publishMessageAndSigs( + // Publication en ordre strict : message → signatures → clés + const publishResult = await publishMessageAndSigs( relays, msgChiffre, proof.signatures, msgCle, ); - if (successCount === 0) { + setPublishRelayStatus(publishResult.relayStatus); + if (publishResult.successCount === 0) { handleError('Échec de la publication sur tous les relais', 'PUBLISH_FAILED'); return; } + // Collecte des signatures distantes (mFA avec 2 devices) let merged = proof.signatures; if (loginPath !== null) { setIsCollecting(true); @@ -256,6 +271,7 @@ export function LoginScreen(): JSX.Element { loginPath.pairs_attendus, ); const endpoints = relays.map((r) => r.endpoint); + // Boucle de collecte : fetch signatures par hash jusqu'à satisfaction ou timeout merged = await runCollectLoop( endpoints, proof.challenge.hash, @@ -281,14 +297,16 @@ export function LoginScreen(): JSX.Element { } } + // Si des signatures distantes ont été collectées, demander confirmation utilisateur if ( loginPath !== null && hasRemoteSignatures(merged, loginPath.pairs_attendus) ) { setCollectedMerged(merged); setCollectedPublishStats({ - successCount, + successCount: publishResult.successCount, relaysCount: relays.length, + relayStatus: publishResult.relayStatus, }); setAwaitingRemoteAccept(true); setIsPublishing(false); @@ -297,7 +315,8 @@ export function LoginScreen(): JSX.Element { const finalProof = await loginBuilder.buildProof(proof.challenge, merged); - // Vérification locale finale + // Vérification locale finale (avant envoi au parent) + // Vérifie: hash, signatures (clés autorisées), dépendances, graphe, anti-rejeu const verificationResults: string[] = []; const verificationErrors: string[] = []; let verificationSuccess = true; @@ -361,10 +380,19 @@ export function LoginScreen(): JSX.Element { // Si vérification échoue, arrêter ici if (!verificationSuccess) { - console.error('Login verification failed:', { + // Log structuré pour le débogage + const verificationLog = { + timestamp: new Date().toISOString(), + hash: proof.challenge.hash, + nonce: proof.challenge.nonce, + serviceUuid: loginPath?.service_uuid, + membreUuid: loginPath?.membre_uuid, results: verificationResults, errors: verificationErrors, - }); + signaturesCount: merged.length, + loginPathStatus: loginPath?.statut, + }; + console.error('[Login] Verification failed:', verificationLog); handleError( `Vérification locale échouée: ${verificationErrors.join('; ')}`, 'VERIFICATION_FAILED', @@ -373,15 +401,24 @@ export function LoginScreen(): JSX.Element { return; } + // Log structuré pour le succès + console.info('[Login] Verification succeeded:', { + timestamp: new Date().toISOString(), + hash: proof.challenge.hash.slice(0, 16) + '...', + serviceUuid: loginPath?.service_uuid, + membreUuid: loginPath?.membre_uuid, + signaturesCount: merged.length, + }); + await nonceStore.markUsed(proof.challenge.nonce, proof.challenge.timestamp); const updatedProof = { ...finalProof, statut: 'publie' as const }; setProof(updatedProof); sendLoginProof(updatedProof); - if (successCount < relays.length) { + if (publishResult.successCount < relays.length) { handleError( - `Publication partielle: ${successCount}/${relays.length} relais`, + `Publication partielle: ${publishResult.successCount}/${relays.length} relais`, 'PARTIAL_PUBLISH', ); dispatch({ type: 'E_PUBLISH_LOGIN_PARTIAL' }); @@ -601,35 +638,49 @@ export function LoginScreen(): JSX.Element { Clé publique Cardinalité min. Dépendances + Statut - {loginPath.signatures_requises.map((req, idx) => ( - - {req.membre_uuid.slice(0, 8)}... - - {req.pair_uuid !== undefined - ? `${req.pair_uuid.slice(0, 8)}...` - : '—'} - - - {req.cle_publique !== undefined - ? `${req.cle_publique.slice(0, 16)}...` - : '—'} - - - {req.cardinalite_minimale !== undefined - ? req.cardinalite_minimale - : '1'} - - - {req.dependances !== undefined && - req.dependances.length > 0 - ? req.dependances.join(', ') - : '—'} - - - ))} + {loginPath.signatures_requises.map((req, idx) => { + // Déterminer le statut de cette signature + const proofSigs = proof?.signatures ?? []; + const collectedSigs = collectedMerged ?? []; + const allSigs = [...proofSigs, ...collectedSigs]; + const hasSignature = req.cle_publique !== undefined && + allSigs.some((s) => s.cle_publique === req.cle_publique); + const status = hasSignature ? 'reçue' : 'manquante'; + + return ( + + {req.membre_uuid.slice(0, 8)}... + + {req.pair_uuid !== undefined + ? `${req.pair_uuid.slice(0, 8)}...` + : '—'} + + + {req.cle_publique !== undefined + ? `${req.cle_publique.slice(0, 16)}...` + : '—'} + + + {req.cardinalite_minimale !== undefined + ? req.cardinalite_minimale + : '1'} + + + {req.dependances !== undefined && + req.dependances.length > 0 + ? req.dependances.join(', ') + : '—'} + + + {status === 'reçue' ? '✓ Reçue' : '✗ Manquante'} + + + ); + })} @@ -673,6 +724,15 @@ export function LoginScreen(): JSX.Element { + + + + )} + {showRecoveryActions && loginPath === null && loginState !== 'S_ERROR_RECOVERABLE' && (
+
+ + {publishRelayStatus.length > 0 && ( +
+

Statut par relais

+ + + + + + + + + + {publishRelayStatus.map((status, idx) => ( + + + + + + ))} + +
RelaisStatutErreur
{status.endpoint}{status.ok ? '✓ OK' : '✗ Échec'}{status.error ?? '—'}
+
+ )} +
)} {isCollecting && proof !== null && ( Réessayer -