Update login state machine and add new login components
**Motivations:** - Continue implementation of login state machine - Add new login components for collect, share and sign screens - Add utilities for login signing, validation, and publishing - Update documentation for CNIL compliance and login workflow - Add features for timeouts, backoff, and validator acceptance **Root causes:** - N/A (feature additions and improvements) **Correctifs:** - Update login state machine implementation - Update graph resolver and sync service **Evolutions:** - Add LoginCollectShare and LoginSignScreen components - Add useSignAndPostLogin hook - Add login utilities: loginSign, loginPublish, loginValidation, collectSignatures - Add sync utilities: syncUpdateGraph, syncValidate - Add validatorsAccept utility - Update App.tsx and LoginScreen.tsx - Update loginBuilder and loginStateMachine services - Add documentation for CNIL compliance, remote collection, timeouts, and validator acceptance - Update identity types **Pages affectées:** - userwallet/src/App.tsx - userwallet/src/components/LoginScreen.tsx - userwallet/src/components/LoginCollectShare.tsx (new) - userwallet/src/components/LoginSignScreen.tsx (new) - userwallet/src/hooks/useSignAndPostLogin.ts (new) - userwallet/src/services/graphResolver.ts - userwallet/src/services/loginBuilder.ts - userwallet/src/services/loginStateMachine.ts - userwallet/src/services/syncService.ts - userwallet/src/services/syncUpdateGraph.ts (new) - userwallet/src/services/syncValidate.ts (new) - userwallet/src/types/identity.ts - userwallet/src/utils/relay.ts - userwallet/src/utils/collectSignatures.ts (new) - userwallet/src/utils/loginPublish.ts (new) - userwallet/src/utils/loginSign.ts (new) - userwallet/src/utils/loginValidation.ts (new) - userwallet/src/utils/validatorsAccept.ts (new) - userwallet/docs/synthese.md - userwallet/docs/specs-champs-obligatoires-cnil.md - userwallet/features/userwallet-acceptation-version-validateurs.md (new) - userwallet/features/userwallet-dh-systematique-scan-fetch.md (new) - features/userwallet-contrat-login-reste-a-faire.md - features/userwallet-login-state-machine.md - features/userwallet-validation-conformite.md - features/userwallet-champs-obligatoires-cnil.md (new) - features/userwallet-collecte-distante-2-devices.md (new) - features/userwallet-timeouts-backoff.md (new) - mempool (submodule) - hash_list.txt - hash_list_cache.txt
This commit is contained in:
parent
c3c11f0ef0
commit
5de870fa13
27
features/userwallet-champs-obligatoires-cnil.md
Normal file
27
features/userwallet-champs-obligatoires-cnil.md
Normal file
@ -0,0 +1,27 @@
|
|||||||
|
# UserWallet – Champs obligatoires et attributs CNIL (complément specs)
|
||||||
|
|
||||||
|
**Author:** Équipe 4NK
|
||||||
|
**Date:** 2026-01-26
|
||||||
|
|
||||||
|
## Objectif
|
||||||
|
|
||||||
|
Documenter le complément de spécifications : champs (Champ) obligatoires des contrats et attributs CNIL dans `datajson`.
|
||||||
|
|
||||||
|
## Référence
|
||||||
|
|
||||||
|
`userwallet/docs/specs-champs-obligatoires-cnil.md` — document détaillé.
|
||||||
|
|
||||||
|
## 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`.
|
||||||
|
- **Attributs CNIL dans datajson** : `raisons_usage_tiers`, `raisons_partage_tiers` (tableaux [raisons, tiers]), `conditions_conservation` (au moins `delai_expiration`).
|
||||||
|
|
||||||
|
## Modifications code
|
||||||
|
|
||||||
|
- **`types/message.ts`** : `RaisonsTiers`, `ConditionsConservation` ; `DataJson` étendu avec `raisons_usage_tiers?`, `raisons_partage_tiers?`, `conditions_conservation?`.
|
||||||
|
- **`docs/synthese.md`** : référence au complément specs.
|
||||||
|
|
||||||
|
## Modalités d’analyse
|
||||||
|
|
||||||
|
- Vérifier que les objets Champ / Contrat chargés peuvent porter les nouveaux champs CNIL sans erreur.
|
||||||
|
- Validation stricte CNIL (présence des champs lorsque requis) non implémentée ; à prévoir selon politique métier.
|
||||||
40
features/userwallet-collecte-distante-2-devices.md
Normal file
40
features/userwallet-collecte-distante-2-devices.md
Normal file
@ -0,0 +1,40 @@
|
|||||||
|
# UserWallet – Collecte distante (2 devices)
|
||||||
|
|
||||||
|
**Author:** Équipe 4NK
|
||||||
|
**Date:** 2026-01-26
|
||||||
|
|
||||||
|
## Objectif
|
||||||
|
|
||||||
|
Supporter `cardinalite_minimale > 1` avec deux appareils : signature locale sur le 1ᵉʳ, signature sur le 2ᵉ, récupération des sigs via les relais, puis publication de la preuve lorsque toutes les sigs requises sont réunies.
|
||||||
|
|
||||||
|
## Flux
|
||||||
|
|
||||||
|
1. **Device 1** : construire le challenge, signer localement (pairs locaux du membre), publier message + sigs locales sur les relais.
|
||||||
|
2. **Device 1** : boucle de collecte — fetch des sigs par hash sur les relais, merge avec les sigs locales, dédup par pair. Dès que `hasEnoughSignatures` (assez de pairs distincts par membre par rapport à `cardinalite_minimale`), on arrête.
|
||||||
|
3. **Device 2** : obtenir hash et nonce (lien ou QR émis par Device 1 pendant la collecte). Ouvrir `/login-sign?hash=...&nonce=...`, signer `hash-nonce` avec la clé du pair local, poster la signature sur les relais.
|
||||||
|
4. **Device 1** : une fois assez de sigs (dont celle du 2ᵉ), vérification (dépendances, clés autorisées, strict), marquage du nonce, envoi de la preuve au parent (iframe).
|
||||||
|
|
||||||
|
## Modifications
|
||||||
|
|
||||||
|
- **`loginBuilder.signChallenge`** : signature de `hash-nonce` uniquement (plus `pairUuid`), pour alignement vérif + relais.
|
||||||
|
- **`loginValidation`** : `requiredSigsPerMember`, `hasEnoughSignatures` (pairs distincts par membre). Suppression du refus systématique `cardinalite > 1`.
|
||||||
|
- **`collectSignatures`** : `fetchSignaturesForHash`, `buildPairToMembers`, `buildPubkeyToPair`, `mapMsgSignaturesToProofFormat`, `runCollectLoop` (poll + timeout).
|
||||||
|
- **`loginPublish`** : `publishMessageAndSigs` (message + sigs locales vers relais).
|
||||||
|
- **`LoginScreen`** : signature pour les **pairs locaux** du membre uniquement ; après publication, boucle de collecte si `loginPath` ; vérification et envoi de la preuve après collecte. Affichage « En attente des signatures des autres appareils… » + `LoginCollectShare` (lien + QR vers `/login-sign`).
|
||||||
|
- **`LoginSignScreen`** : route `/login-sign?hash=...&nonce=...` ; signature et publication de la sig sur les relais.
|
||||||
|
- **`LoginCollectShare`** : lien et QR vers `/login-sign?hash=...&nonce=...` pendant la collecte.
|
||||||
|
|
||||||
|
## Modalités de déploiement
|
||||||
|
|
||||||
|
Déploiement classique du front userwallet. Aucune évolution côte relais.
|
||||||
|
|
||||||
|
## Modalités d’analyse
|
||||||
|
|
||||||
|
- **Device 1** : construction du chemin, challenge, publication → collecte → affichage du lien/QR. Ouvrir le lien sur le 2ᵉ appareil, signer → retour sur le 1ᵉʳ, la collecte doit finir et la preuve être envoyée.
|
||||||
|
- **Device 2** : aller sur `/login-sign?hash=...&nonce=...` (ou scanner le QR), vérifier « Signature publiée ».
|
||||||
|
- Vérifier timeout de collecte (5 min) si le 2ᵉ ne signe pas.
|
||||||
|
|
||||||
|
## Références
|
||||||
|
|
||||||
|
- `features/userwallet-validation-conformite.md` (§ Cardinalite_minimale)
|
||||||
|
- `userwallet/docs/specs.md` (collecte signatures mFA, S_LOGIN_COLLECT_SIGNATURES)
|
||||||
@ -33,8 +33,8 @@ Référence : `userwallet/docs/specs.md` (modèle, objets, machine à états, ca
|
|||||||
|
|
||||||
### 3.1 Machine à états formelle
|
### 3.1 Machine à états formelle
|
||||||
|
|
||||||
- **Fait** : `loginStateMachine` (états S_LOGIN_*, événements E_*), `useLoginStateMachine`, dispatch dans LoginScreen, Retour → E_BACK. Voir `features/userwallet-login-state-machine.md`.
|
- **Fait** : `loginStateMachine` (états S_LOGIN_*, événements E_*), `useLoginStateMachine`, dispatch dans LoginScreen, Retour → E_BACK ; G_PAIRING_SATISFIED, E_ADD_PAIR / E_SYNC_NOW. **Timeouts** : `RELAY_FETCH_TIMEOUT_MS` sur tous les fetch relay (GET/POST). Pas de backoff. Voir `features/userwallet-login-state-machine.md`, `features/userwallet-timeouts-backoff.md`.
|
||||||
- **À prévoir** : timeouts (réseau, collecte signatures), reprise sur erreur, backoff ; gardes explicites (G_PAIRING_SATISFIED, etc.) côté UI.
|
- **À prévoir** : timeouts sur collecte signatures (fetch relay) si distinct.
|
||||||
|
|
||||||
### 3.2 Écrans et UX alignés specs
|
### 3.2 Écrans et UX alignés specs
|
||||||
|
|
||||||
@ -65,8 +65,8 @@ Référence : `userwallet/docs/specs.md` (modèle, objets, machine à états, ca
|
|||||||
|
|
||||||
### 3.5 Validation et conformité
|
### 3.5 Validation et conformité
|
||||||
|
|
||||||
- **Fait** : `verifyMessageSignaturesStrict`, `filterSignaturesByAuthorizedPubkeys` ; avant publish login, si `cle_publique` dans requirements → vérif stricte, sinon X_PUBKEY_NOT_AUTHORIZED. Contrats en version non supportée exclus du graphe (sync) ; `contrat_version` dans LoginPath et affichage. Voir `features/userwallet-validation-conformite.md`.
|
- **Fait** : `verifyMessageSignaturesStrict`, `filterSignaturesByAuthorizedPubkeys` ; `buildAllowedPubkeys` (résolution `cle_publique` depuis pairs) ; `checkCardinalitySupported` (refus si `cardinalite_minimale > 1`) ; **`checkDependenciesSatisfied`** (membres des `dependances` doivent avoir ≥1 signature dans la preuve, via pair du membre) ; contrats version non supportée exclus ; `contrat_version` affiché. Voir `features/userwallet-validation-conformite.md`.
|
||||||
- **À renforcer** : cardinalité, dépendances entre signatures ; résolution `cle_publique` depuis graphe quand absente des requirements.
|
- **Fait** : support de `cardinalite_minimale > 1` via **collecte distante** (2 devices). Device 1 publie message + sigs locales, boucle de collecte (fetch sigs par hash), Device 2 signe via `/login-sign?hash=...&nonce=...` (lien/QR) et poste sa sig. Voir `features/userwallet-collecte-distante-2-devices.md`.
|
||||||
|
|
||||||
### 3.6 Scan et optimisation (optionnel)
|
### 3.6 Scan et optimisation (optionnel)
|
||||||
|
|
||||||
|
|||||||
@ -25,11 +25,16 @@ Implémenter la machine à états formelle du login (S_LOGIN_* , E_* , transitio
|
|||||||
|
|
||||||
S_LOGIN_SELECT_SERVICE, S_LOGIN_SELECT_MEMBER, S_LOGIN_BUILD_PATH, S_LOGIN_CHECK_PAIRS, S_LOGIN_NEED_MORE_PAIRS, S_LOGIN_BUILD_CHALLENGE, S_LOGIN_COLLECT_SIGNATURES, S_LOGIN_PUBLISH_PROOF, S_LOGIN_VERIFY_LOCAL, S_LOGIN_SUCCESS, S_LOGIN_FAILURE, S_ERROR_RECOVERABLE.
|
S_LOGIN_SELECT_SERVICE, S_LOGIN_SELECT_MEMBER, S_LOGIN_BUILD_PATH, S_LOGIN_CHECK_PAIRS, S_LOGIN_NEED_MORE_PAIRS, S_LOGIN_BUILD_CHALLENGE, S_LOGIN_COLLECT_SIGNATURES, S_LOGIN_PUBLISH_PROOF, S_LOGIN_VERIFY_LOCAL, S_LOGIN_SUCCESS, S_LOGIN_FAILURE, S_ERROR_RECOVERABLE.
|
||||||
|
|
||||||
|
## Compléments (après 3.1)
|
||||||
|
|
||||||
|
- **G_PAIRING_SATISFIED** : LoginScreen vérifie `isPairingSatisfied()` ; si faux, affiche « Pairing obligatoire » + « Configurer le pairing » → /manage-pairs, « Retour » → /.
|
||||||
|
- **E_ADD_PAIR / E_SYNC_NOW** : quand chemin incomplet ou S_ERROR_RECOVERABLE, boutons « Synchroniser » (→ /sync) et « Ajouter un pair » (→ /manage-pairs) ; dispatch avant navigation. E_ADD_PAIR depuis S_ERROR_RECOVERABLE → S_LOGIN_SELECT_SERVICE.
|
||||||
|
- **Timeouts** : `RELAY_FETCH_TIMEOUT_MS` sur tous les fetch relay. Pas de backoff. Voir `features/userwallet-timeouts-backoff.md`.
|
||||||
|
|
||||||
## Reste à faire (hors scope 3.1)
|
## Reste à faire (hors scope 3.1)
|
||||||
|
|
||||||
- Timeouts (réseau, collecte signatures), backoff.
|
- Timeouts (réseau, collecte signatures), backoff.
|
||||||
- Gardes explicites (G_PAIRING_SATISFIED, etc.) côté UI avant dispatch.
|
- S_LOGIN_NEED_MORE_PAIRS : écran dédié (actuellement chemin incomplet + recovery).
|
||||||
- S_LOGIN_NEED_MORE_PAIRS : boutons E_ADD_PAIR, E_SYNC_NOW dédiés.
|
|
||||||
|
|
||||||
## Modalités de déploiement
|
## Modalités de déploiement
|
||||||
|
|
||||||
|
|||||||
30
features/userwallet-timeouts-backoff.md
Normal file
30
features/userwallet-timeouts-backoff.md
Normal file
@ -0,0 +1,30 @@
|
|||||||
|
# UserWallet – Timeouts réseau (3.1)
|
||||||
|
|
||||||
|
**Author:** Équipe 4NK
|
||||||
|
**Date:** 2026-01-26
|
||||||
|
|
||||||
|
## Objectif
|
||||||
|
|
||||||
|
Timeouts sur les appels relay (X_RELAY_TIMEOUT). Pas de backoff.
|
||||||
|
|
||||||
|
## Impacts
|
||||||
|
|
||||||
|
- **Relay** : tous les fetch (GET /messages, /signatures, /keys, /bloom ; POST /messages, /signatures, /keys) utilisent `AbortSignal.timeout(RELAY_FETCH_TIMEOUT_MS)` (15 s). En cas de dépassement, l’appel échoue (AbortError).
|
||||||
|
|
||||||
|
## Modifications
|
||||||
|
|
||||||
|
- **`utils/relay.ts`** : `RELAY_FETCH_TIMEOUT_MS` ; `signal: AbortSignal.timeout(...)` sur chaque `fetch`.
|
||||||
|
- **`services/syncService.ts`** : pas de retry ; en échec fetch, le relais est marqué `ok: false`.
|
||||||
|
|
||||||
|
## Modalités de déploiement
|
||||||
|
|
||||||
|
Déploiement classique du front userwallet.
|
||||||
|
|
||||||
|
## Modalités d’analyse
|
||||||
|
|
||||||
|
- Relais lent ou coupé : timeout après 15 s ; `relayStatus` indique `ok: false` pour ce relais. Log « Error syncing from … ».
|
||||||
|
|
||||||
|
## Références
|
||||||
|
|
||||||
|
- `features/userwallet-contrat-login-reste-a-faire.md` (§ 3.1)
|
||||||
|
- `userwallet/docs/specs.md` (X_RELAY_TIMEOUT)
|
||||||
@ -17,7 +17,8 @@ Renforcer la validation : validateurs stricts, clé autorisée, version des cont
|
|||||||
**Vérification stricte**
|
**Vérification stricte**
|
||||||
|
|
||||||
- `utils/verification.ts` : `filterSignaturesByAuthorizedPubkeys`, `verifyMessageSignaturesStrict`. Garde `verifyMessageSignatures` (crypto seule) pour usage générique.
|
- `utils/verification.ts` : `filterSignaturesByAuthorizedPubkeys`, `verifyMessageSignaturesStrict`. Garde `verifyMessageSignatures` (crypto seule) pour usage générique.
|
||||||
- `LoginScreen` : avant publish, si `loginPath.signatures_requises` comporte des `cle_publique`, construction de `allowedPubkeys` ; appel à `verifyMessageSignaturesStrict` sur la preuve ; si aucune signature autorisée ou présence de non autorisées → erreur X_PUBKEY_NOT_AUTHORIZED, pas de publication.
|
- `utils/loginValidation.ts` : `buildAllowedPubkeys` (depuis requirements + pairs ; clé locale pour pair local), `checkCardinalitySupported` (refus si `cardinalite_minimale > 1`), **`checkDependenciesSatisfied`** (membres des `dependances` doivent avoir ≥1 sig dans la preuve via pair du membre).
|
||||||
|
- `LoginScreen` : avant publish, `checkCardinalitySupported`, **`checkDependenciesSatisfied`** (sinon DEPENDENCIES_UNSATISFIED) ; `buildAllowedPubkeys` ; `verifyMessageSignaturesStrict` ; si sig non autorisée → X_PUBKEY_NOT_AUTHORIZED.
|
||||||
|
|
||||||
**Version contrats**
|
**Version contrats**
|
||||||
|
|
||||||
@ -34,7 +35,27 @@ Déploiement classique du front userwallet.
|
|||||||
|
|
||||||
- Contrat en base avec `version` non supportée → sync : log « Contrat … version … not supported, skipped », absent du graphe.
|
- Contrat en base avec `version` non supportée → sync : log « Contrat … version … not supported, skipped », absent du graphe.
|
||||||
- Login path résolu avec contrat → « Version contrat » affiché.
|
- Login path résolu avec contrat → « Version contrat » affiché.
|
||||||
- Preuve avec signature dont `cle_publique` hors validateurs (si `cle_publique` dans requirements) → X_PUBKEY_NOT_AUTHORIZED avant publish.
|
- Preuve avec signature dont `cle_publique` hors validateurs (requirements ou résolution pairs) → X_PUBKEY_NOT_AUTHORIZED avant publish.
|
||||||
|
- Requirement avec `cardinalite_minimale > 1` → CARDINALITY_UNSUPPORTED avant publish.
|
||||||
|
- `dependances` non satisfaites (membre requis sans signature) → DEPENDENCIES_UNSATISFIED avant publish.
|
||||||
|
|
||||||
|
## Cardinalite_minimale — explication
|
||||||
|
|
||||||
|
**Ce que ça veut dire**
|
||||||
|
|
||||||
|
Dans les validateurs, un requirement peut avoir `cardinalite_minimale` (ex. 1, 2, …). Cela impose **au moins N signatures** pour ce requirement, en général venant de **N pairs distincts** du même membre (mFA). Ex. : « il faut 2 signatures du membre X » → 2 pairs (2 devices) doivent signer.
|
||||||
|
|
||||||
|
**Pourquoi c’est refusé aujourd’hui**
|
||||||
|
|
||||||
|
Le flux login ne produit **qu’une seule signature par requirement** (un pair par requirement). Si `cardinalite_minimale > 1`, on exigerait plusieurs sigs pour le même requirement, ce qui n’est pas implémenté. D’où le refus : avant publish, `checkCardinalitySupported` vérifie que tout requirement a `cardinalite_minimale` ≤ 1 ; sinon → CARDINALITY_UNSUPPORTED.
|
||||||
|
|
||||||
|
**Implémenté (collecte distante 2 devices)**
|
||||||
|
|
||||||
|
- Device 1 signe localement, publie message + sigs, puis **boucle de collecte** (fetch sigs par hash sur les relais) jusqu’à avoir assez de pairs distincts par membre (`hasEnoughSignatures`).
|
||||||
|
- Device 2 ouvre `/login-sign?hash=...&nonce=...` (lien ou QR affiché pendant la collecte), signe `hash-nonce` avec son pair local, poste sa signature sur les relais.
|
||||||
|
- Device 1 récupère ainsi les sigs du 2ᵉ appareil, finalise la preuve, vérifie (dépendances, clés autorisées), marque le nonce, envoie la preuve au parent.
|
||||||
|
|
||||||
|
Voir `features/userwallet-collecte-distante-2-devices.md`.
|
||||||
|
|
||||||
## Références
|
## Références
|
||||||
|
|
||||||
|
|||||||
227
hash_list.txt
227
hash_list.txt
@ -22188,3 +22188,230 @@ e00b3bb7b9abd55bbd3112b5bc641008e335b12ae0624548221c047291aa2e68;2b1898464a91291
|
|||||||
06121bc2751e64748e536cf49484043bf5ef87fb027922063c8a5a984fc6284b;fbe7563f8099ebbde05178514a4497397a9ace6f23911c99f6a626757ce6f7db;10051;1;2026-01-26T12:59:22.507Z
|
06121bc2751e64748e536cf49484043bf5ef87fb027922063c8a5a984fc6284b;fbe7563f8099ebbde05178514a4497397a9ace6f23911c99f6a626757ce6f7db;10051;1;2026-01-26T12:59:22.507Z
|
||||||
3e4726a5ed732d0abbdb1069e42c6f13d34ea16ebd8353e2d24720a866424cc2;1ce6d0179d049585c37e1e756173b619131055f4c7b41d3d08735c2ea540f6fa;10051;1;2026-01-26T12:59:22.510Z
|
3e4726a5ed732d0abbdb1069e42c6f13d34ea16ebd8353e2d24720a866424cc2;1ce6d0179d049585c37e1e756173b619131055f4c7b41d3d08735c2ea540f6fa;10051;1;2026-01-26T12:59:22.510Z
|
||||||
6100ed0ee59a17a23292979ef05d6baecaf6686b7a8045385a05b57c8035a684;e357d1504e963d89982fabeb0d63e10a3305b515fbe22f5a2b87a88abd846afe;10051;1;2026-01-26T12:59:22.512Z
|
6100ed0ee59a17a23292979ef05d6baecaf6686b7a8045385a05b57c8035a684;e357d1504e963d89982fabeb0d63e10a3305b515fbe22f5a2b87a88abd846afe;10051;1;2026-01-26T12:59:22.512Z
|
||||||
|
7215b51e9d2d3502b5bfde1bf88de26997676dfdd3e27494582e173e481b44ff;c44ca4a43ccce1ea81e2debe80d775b07222cdb206c0939f79767db2853da808;10052;1;2026-01-26T13:00:22.495Z
|
||||||
|
7b9f00a136d9f12e6867a14bff2fb50d939f72917c42467a20329b75355a5d02;18b4ce8cb6631dcf539718dc1a3105086cdb135d62f889e593576628444adb0a;10052;1;2026-01-26T13:00:22.497Z
|
||||||
|
bfa0f7a88fb8a4f9249e909b51cc9e1240d9c01a0ffa32af34f87f708b8ac9a2;159bf97b811c7a0fdf2ed719669198c2248bfe01e833c562dbb5bd03d6f7181a;10052;1;2026-01-26T13:00:22.499Z
|
||||||
|
80df91e877bf5e71ded87f6dded79cff11bb4473868fcdce3024f4c8f29c8a7e;d17046aefc28f037294abcb50d44e7b8b6dcc6e837b3cf7a104d87a90f3f104e;10052;1;2026-01-26T13:00:22.501Z
|
||||||
|
93dc5962fd59708b174d5c3c0bd5cff25a13449f9704a487d5253e50ee911d08;8f909665985168ad5db0546ee156c500adc9572d7fa6cbc0dc3c6c5235a91d56;10052;1;2026-01-26T13:00:22.504Z
|
||||||
|
aaf3edf8e02e402043a131ba4e9875b6fe621d5432b3b87cce02621b0fdc0f74;4f56e44253be7b7c172c2a14bd682d5d64fe2fed44090f0135e732e3f9af736d;10052;1;2026-01-26T13:00:22.506Z
|
||||||
|
73724934bd3b256b1f30049fc35e2f375a9259f599c9db74cbd8baf5b1187123;c84c058ef2665ebc56f62603d7d1fcbee29da986d660d09f6b66f2ca380fa282;10052;1;2026-01-26T13:00:22.508Z
|
||||||
|
d6950e04f03f4b26d8d96f3dcd6b1397823ea85a33b594ecd2353659cf39f59e;402b53b87f6d8cef21103b496675766ec0d4382f750df5a57e7caabf9741db8d;10052;1;2026-01-26T13:00:22.510Z
|
||||||
|
b0a91e9e7110fbf9955f06a019932e7f9392f06b2b79105ae7693ada8b639997;a6e7710fdd0a792ebb791813e3ea152f4685360ead303201cd2479dc4bde60a6;10052;1;2026-01-26T13:00:22.512Z
|
||||||
|
963e436f8d8796d04e6b6aaf7fa6251d06680bbb2894c7aa1ddf7359f8acb022;455941b31a47ad293d78b9d6c0f445f37f82cb33daec9b6e98328bb5e774d8b5;10052;1;2026-01-26T13:00:22.514Z
|
||||||
|
677f6f4466f2331310557296350891fda7bad4fcecbbad0dc45ad8b3c8cfb989;275f758f27daf8e801274ae5151bf6367ea2a92f27ba0b04ab1aab29e0b34abf;10052;1;2026-01-26T13:00:22.516Z
|
||||||
|
52b3e52857e93b78ef86cf0626bd02ba99755dff4cd24f8f2b522b0226e14a35;0ba71e4ef93d24ace847c1fc7414508fa9c9e4e0192c6dd0539cfd1ae645b3cd;10052;1;2026-01-26T13:00:22.519Z
|
||||||
|
bd112b689e67f55bc335d4724c8d54da5bd4fcfccdf93661e71cbe8c212dbaf4;2adde006795a2268d9c227dee6b3af598133ca9317681a8b73d69530f34159e5;10052;1;2026-01-26T13:00:22.522Z
|
||||||
|
bb6115963e6a5f4721be55d86f9ac8ec4f651714e707d3cac027ebb8cc1c9a54;aa98c273772d474290a881a8ebdb59dcf59f5e7bf5a146a2eff67c8912ef1a19;10053;1;2026-01-26T13:01:22.412Z
|
||||||
|
8dcc247a690f8eebae1b35942ca96d48d355763bfa9e7a2f431689a4f0348b37;fbfb59f9867fe87748c89f4877ca1a1b4512fb23559af287104fe761cfac2723;10053;1;2026-01-26T13:01:22.414Z
|
||||||
|
de0b5e6b582a8d6d8f64edd1603bde002572daa7d68b0249133137f2bebbe7a6;6cfd101f77965ac7ae9ba855fd16772258a9c6ce311a1c4ce18f7961e8e0b129;10053;1;2026-01-26T13:01:22.417Z
|
||||||
|
fd6080715c36a907ddc00ead7a8675bfba90ea61b9bd0153dde2460144127323;b59edd2421af262783767b680d1ae5fc209f9f9b0e62ef0dbfeca2e0b578b13b;10053;1;2026-01-26T13:01:22.419Z
|
||||||
|
1212a86ec51b1dced37ea4baeccbec9ecd79961797e60cc65e503eef64be076f;a494f7266f41e044b6f2c55061e86039771c160965f0196e4ff8c12cd647cb45;10053;1;2026-01-26T13:01:22.421Z
|
||||||
|
0f6cd1ab9bd22e2a0f1fae03875f5d821d4c151d1c400ed5bb4dee43d01bc7eb;0c0f16cc44fd4e59cdf027dccae3149239d4efbbd81f995d94b6219f3924284e;10053;1;2026-01-26T13:01:22.423Z
|
||||||
|
8863b0fb3336d1f19f3fb5447322ddd230f3ab6a186b6c2ca67d9c40dd0c3187;122c2a3c8b362cf79ea51376c3fcf9a45ca07eaffa5da3ff9b0963a94bb5da52;10053;1;2026-01-26T13:01:22.425Z
|
||||||
|
86695473a28f73d5a56267804b422027028965a078d8112e5abe04f8b2b452a1;5087e410f6d592d92e7a0cc677fa616f2d170b5a9062e316b2455416c4cdaf9d;10053;1;2026-01-26T13:01:22.427Z
|
||||||
|
e3d81382b96c56c97a9582588b66b813a07eb696af18d015c92a613f91effff2;fe7edcca7eba95fbef9a4975eac73f92fef181bcaefdff4c4a81b9a56eb6bc9d;10053;1;2026-01-26T13:01:22.429Z
|
||||||
|
903bf2fc232d7ef6fb7cf405b7f185b4082e7e432649cac139bdeca8dd5b727b;ec412710f42bb3667b3f6452d17dbf06d909a2ba38230d1fd23f331bd96ef0a1;10053;1;2026-01-26T13:01:22.431Z
|
||||||
|
39b045ee96ae0ef583233fb230013fa093f5f210bc8b17a82d1be0e9d434ce43;1abfe59bcd1b920170e32035ffdd92a691aaa6255591451568b4493418671ca5;10053;1;2026-01-26T13:01:22.433Z
|
||||||
|
37171bab7903389055c74efab25c8a9758ce436571a9ac8ae38a777455465432;2b3659ae0706da43c49ec152aaca853e001265e41fe27619add259cfc33567b3;10053;1;2026-01-26T13:01:22.436Z
|
||||||
|
d0c6750242dd3613660e2461b4e8d96a0673d6c546fd04ca4331a382162a5089;dcb43a098bcf261702f156f754f8ea03eadbb7514363d02d1a9efd870a3097bb;10053;1;2026-01-26T13:01:22.438Z
|
||||||
|
9d8a0aa2d860bf18a18eafed687f58c530f3c128f41f4bfaf6a85b0d0b57c2f9;8d0d6401963fa69a561ccd593ccf219d8a9268456e92cb927ee49559abdf2bc1;10053;1;2026-01-26T13:01:22.440Z
|
||||||
|
3ff0529894a263b3de141e85e03528341e5cbc70e9f566327771343c7c662a5f;3ed20330c0fcb16030da97edf89bee378b20c939a2a3da0bba49242ece2c80cf;10053;1;2026-01-26T13:01:22.442Z
|
||||||
|
ccde40c98482d2274a4bddf01dddd9278a8a8b398a11e0f5b2784f6933a618f7;4e73063e89687225398709ffa01d87fa31e1decff92f90f79980a66a1e039eda;10053;1;2026-01-26T13:01:22.444Z
|
||||||
|
93dc5962fd59708b174d5c3c0bd5cff25a13449f9704a487d5253e50ee911d08;589bd3e0e58c4ea13ced809fbeb7cc1f85dd63ed054bfd21195a7ba6aa403ced;10053;1;2026-01-26T13:01:22.447Z
|
||||||
|
f33e7441b2a201056b062d83a0aa65a8d78fdefd2b8a2abea9db09c8c878b867;b7868d8612a838ed833b19cc9c2487cb63a3d432dc76fa934f0c33e7fb0806f2;10053;1;2026-01-26T13:01:22.449Z
|
||||||
|
265d0fce64dcf56d11d3d6d2202eaeee2e2c4e29e1b8d2ed850e7a56bd24d75c;aecb71828e73c1ed7fed2ccf91d2c05d502915bda64f404b3057cbd54a30301c;10054;2;2026-01-26T13:02:22.471Z
|
||||||
|
56ce0ccbfbda935081afbdedeb2c2a6f3b1fc50f6c5011e88676b3f35d50850d;e985291676c45ce779cc20806a524613b944326d3b62bb8ab2ce8302bc266d39;10054;2;2026-01-26T13:02:22.474Z
|
||||||
|
741abef5adde8f13b93291f9becf2c6e86062cd8a09784ac674098162dca527b;0e110eb25eb706f8d3433c0edbc788bf9d022e0aa1b78ef37038e7af17225048;10054;2;2026-01-26T13:02:22.477Z
|
||||||
|
e61888fa5009a58fddd20f3c7347ef05230b25cfcbe6521f67e7d83f8ef3edd3;2cc6600b3b3ca3e96f5305707dcc75ced00f9b6333558e66e4a0059114ae9f61;10054;2;2026-01-26T13:02:22.479Z
|
||||||
|
0b70e0de2ca4ef6b964a35601ea7d15e9e24179635fe1653e6bc4778f686eac5;f64317e9d09062ec1b0f490da940cd98a78e24eb1889aa9b14cc14e1c216326d;10054;2;2026-01-26T13:02:22.482Z
|
||||||
|
4f026eae94d5c2be2cf35cbe08a28bbadb024b6e13fec1f9608157fded569d5d;f68e651fec8dc3bc1978ddb8343db3178a4130d6f40861f415a561e74c57957d;10054;2;2026-01-26T13:02:22.486Z
|
||||||
|
6d94e5ca5b12d95a87c8ceb8a91fa7b1a97a6e1867b7e9d23cb7af2ff7d2830e;19b55abf27c3aaadc16882ca17773d1feb86e2ce77921d965822eae69fb2ad8a;10054;2;2026-01-26T13:02:22.489Z
|
||||||
|
4248f20d858166d13ad5b7d5a6114389ba8f9043ed3eb3f49c992cdbed825f8d;167313176a74841ca3e6ad8b9307892c56e2f92f25255db1eb5bc7a47696aa98;10054;2;2026-01-26T13:02:22.491Z
|
||||||
|
a0593d88f7af23dad94310700c11b443c181634e2488637746ed35d188d75842;ad211c67f74acc56b49970411da29af0ca1b25bd8a2aeedb99e146f8beaf6aa3;10054;2;2026-01-26T13:02:22.494Z
|
||||||
|
ca3b804ba4b173db8bd540c8e8f430e2080895bce0a028dd9bafdd8fec3b0669;a69191780ce93ca5e308bc4e0eb4d51d94ec5e9f5832bfea40494076df8b0eb7;10054;2;2026-01-26T13:02:22.499Z
|
||||||
|
90e25718842a81307e476403582bd84c852929283e9f2437d1e61b0b1461e32b;5f14bc3bb5cd375555f3549e51aa83a76ca054eccc0548b6a5b77a6472fb32b9;10054;2;2026-01-26T13:02:22.501Z
|
||||||
|
3a2ae26dba4671b9b7be50dae7e78f36de8c36b50d3fd6d2f5031e3d9da6499f;2fa4aa6bc717647581337e2925a19f990020be3b2db2169511eb78c4fc83c1da;10054;2;2026-01-26T13:02:22.504Z
|
||||||
|
fe6767db6eb19164e5e86e80769e534b7fc6629933e2dadd7bbff5cec2d2c5c1;4e43e12d44d1304e9e3c07dd697b56048f7f493189a66326f3d341655c4fa8ee;10054;2;2026-01-26T13:02:22.506Z
|
||||||
|
4113afb63b631afb701a9d128e7d0f50825205988be8911932f3790463a4fd6b;bcbb24adf8716b8abcfce163fdb9db2ba21b3351abb016884f2b5ad8674023fe;10054;2;2026-01-26T13:02:22.508Z
|
||||||
|
1a0ea056ae30d69619c0d4f4a4a280244b60c7744313cc2d4034303116473452;071d3f4bf3c574b1cd66cb9acf4d17bae3305287481074bf2189693dac62c12c;10055;1;2026-01-26T13:02:22.518Z
|
||||||
|
61336880f92e51e3198af9434137b16e838e22d84163a234fa5785c3d05ac8d6;53dc474c56e7425b1e9b5b10f4949aeab1728683df1a07b0898dab4a03f22fd5;10055;1;2026-01-26T13:02:22.520Z
|
||||||
|
8637cc84ac732e688cbcb5a9f5be3755600c1b42526d89ba3bedd5c2ce30db9d;7cb5083d5dcf0cc640c7137e231ec42b449ffa10ab13425c113e6f0b99e67bf4;10055;1;2026-01-26T13:02:22.522Z
|
||||||
|
b2a098c9e0db2eeb042bdc5fa461fc17e6ee9c003f65d5205cc23e707355ac87;3eaf2444fdc60770fe0cfbb89013773d427c6b7f9391a1b60a5029b546b1c906;10056;1;2026-01-26T13:03:22.507Z
|
||||||
|
3045c345c5db27444a2e548b53ec45e369084bb74e242c53a78f647a70392d27;94dbf22a024a801e26fdca9f7e9de734503e86eb031dc34d80a908592c6d3656;10056;1;2026-01-26T13:03:22.511Z
|
||||||
|
28859a2376ddb9e3fbeea3a85cbd6e6ac4fe39b0594fc921e3d0145c96b145bd;e0593d0b6e8be5c12d4ee07bc953a12c7e4a492729c33d4b8d62465eeddeb57e;10056;1;2026-01-26T13:03:22.514Z
|
||||||
|
1f5207fb9603f2a3a880b55aef42939f6fffc741e004cd2c03bb3dd64a9eab39;a57734fc37e69f60cc13fb4557167c0b60ff6d9ca80a7d432d35acff24d9d8bd;10056;1;2026-01-26T13:03:22.519Z
|
||||||
|
0d3a5baa7ea0b8cb007683dd603546e406f7eebc38a3259b9228d9c0ca334326;fd7e6d5092c93046ebc499f79e892cd07c34ddb032e8a4b94d66e765daafe514;10057;2;2026-01-26T13:04:22.502Z
|
||||||
|
85657a9d34b0e7e73066333af46e29fe8f3aeca34de0a342d682909e3866db84;2de2b753c35c58ae5f673ae20110bf4214370999491e02f5ab3f47f55fa6b816;10057;2;2026-01-26T13:04:22.508Z
|
||||||
|
d700774c525705c05427415483910757eeb16dbd023fad758316fb3f222e8ade;3b06d0c188b02b250743db8f1934d009f677f479db573c9309d7832fe93a4b1a;10057;2;2026-01-26T13:04:22.512Z
|
||||||
|
4b46079e3f018c2472e576770fc2a159b3aad74fd5933a9f72f4326729241259;3535e70469759c71c81e8fe7a4c213961fed80af0b17063806710d35977a704f;10057;2;2026-01-26T13:04:22.515Z
|
||||||
|
5d00c6e49d1e39063126cb5d6cee77fa0e7a2effa9db227cc02f7fdf7a43ff63;e0a62afb9d180c2b59d07071d2262712cbda365a56639bd5115daf22c83cec4f;10057;2;2026-01-26T13:04:22.519Z
|
||||||
|
977a17eda01fd7be564f2c89535aaf26012db19878ea136fd005f1be3a655ae6;4ce53c63a4c95a9534eb4f1f9bc5a4b64e4b498ad0b9cd71787854e8b867c766;10057;2;2026-01-26T13:04:22.523Z
|
||||||
|
7bcd13301152297de3cfd11190575a5b87c79db9f493a1940e61c0d67fc84fb1;cca42a6354a170b782e5b9acdc7fde1f49370799f584bac281aeeb74ebf9636c;10057;2;2026-01-26T13:04:22.527Z
|
||||||
|
7f6e2eb9d03f4632702bb1d50629dcf6bbec800f63a9c753969befcacaca21c7;21802268d69750c1946e739f1dd5d7db928c2a19d1bcb73ddaa27e0c3aa76a8c;10057;2;2026-01-26T13:04:22.530Z
|
||||||
|
e2bc5b3cfcba8fc05d58e5a63afbeb1cf7c5b66a57171d10fc012809b43ee12f;dcf2a15afe410c700c7f43439c566aa0d4e71c8f6d32832cf591e34bba8d0da3;10057;2;2026-01-26T13:04:22.533Z
|
||||||
|
e122330f7cff47df7b4a568f1ef231ab85a31700f2fb2bb1f4f440d879ae048d;8ee8b853f0f86e61bf3b54b938c5e44a3660ac472d36b1defe67650b2b444cbc;10057;2;2026-01-26T13:04:22.535Z
|
||||||
|
cadee6e9cb47280d95979afb9298cdfbf420e438740247bf49a29d59d5248c96;b430a2f263e4f89d4a99bea8c3aa5719f6f8028b373aee92e1068f51d672adc0;10057;2;2026-01-26T13:04:22.537Z
|
||||||
|
d3210c8cb4b8d50f866620d4b6dea767d70be9c1772115b8f3c5548f5cf01c6e;abcac40f8da424988ea40df3e028e2cf754fe70f8d220ea1ec2ad1605abd96c6;10057;2;2026-01-26T13:04:22.539Z
|
||||||
|
9b2d6fa8c11104af4142702ef22204906f437609c58cb96fab34578d970d1e66;c0a7f914ee8ee6e39be84a1a0f53ea379c3ca2b6dd7bbad42f7bab56e03884c7;10057;2;2026-01-26T13:04:22.542Z
|
||||||
|
5e6f9e0fcb22f01c8e6b850070d1a1ddab737f1ada99a3bb5c4a29873f1d92d1;b7f2e6211860f48a3ca5f4ad9b919c55922d15beb5965e6461132c637bcb74cb;10057;2;2026-01-26T13:04:22.544Z
|
||||||
|
6930f4c38ff1982f25569c3ea2e22056408d101afe1cb67cfe6ce498a40feac9;6d376088b706bfdc1470fd56b4312654976e12e93bb318a92708bb5b117958d6;10057;2;2026-01-26T13:04:22.546Z
|
||||||
|
b524ac47961b1f6c60f3bf8995194baf1ee42e1a9da948580db3548a08b44e43;6767afb807c85861a085d0620a2c020fc79b88e708fd93ed36a1d174726ec4eb;10057;2;2026-01-26T13:04:22.548Z
|
||||||
|
b33eff7cc09675b6a6aaf683df76f4ecba172aebdb9321af60f90cc397fe563f;feda38f701bfadb2c2a6d63ef9b618b4ff2545ca8ddf022478ccd164c0ba3e29;10058;1;2026-01-26T13:04:22.557Z
|
||||||
|
b5b139f90722c320a7b1952aac832d4cb3d463292e3b9cba997f9bc0f509a9bc;269bcaf1fae4663845412f0ff6c7026e762fa8fff154f563b138578737164148;10058;1;2026-01-26T13:04:22.559Z
|
||||||
|
f5adeb7de44347cc7ea61e4809595477e7d782ecdd18a92ca31aab4fb264aabf;f9f42a887209ffb487921cd3140e8b6e7361afd1aa6b107bcc55e27844a8536a;10058;1;2026-01-26T13:04:22.561Z
|
||||||
|
69d7c66f8e7bcb1e0b9a678fcc5bbbe9ebeda914085c66847b794f2bd63fd05b;393bbee0aee50270b901b5de6cf5782f457415d33323536e6805810969e0bf6c;10058;1;2026-01-26T13:04:22.563Z
|
||||||
|
a3d00144b716eba229dcc467d86d5aadd2c1c0b7dd5e21dcd13a2bb52a201ef1;1c48efa8ed8fd905f903a5f501fbfe580ae95f6cf5b267e1620c02c9d1bdcc7f;10058;1;2026-01-26T13:04:22.565Z
|
||||||
|
a1a3d82edc02fd2b48a4149ad83a1febfa8a46e8bfb4c8e9f4d1c50ef3d2c20a;f4bb2b67d96a18b3d024781043f1e743f21240ae6c07b42ad8904095c96b3e8e;10058;1;2026-01-26T13:04:22.567Z
|
||||||
|
2550488656a3cbcc99368a54385a52deb03ea1ccbcb32389a9a5611b581a7c4d;8f909aa7085d2ef240470e951aabd9a8bacf47688bb82ffbab7d85d89a4e2bbb;10058;1;2026-01-26T13:04:22.569Z
|
||||||
|
7a6a78837abffee022bd294d8e7fb191aaecb5ab4442a3cdbcd43cba410c2f04;ba6af75be8d68430450f9be8d240295ce8630d0bbcfa1222e0d8a2f4ed3ecec6;10058;1;2026-01-26T13:04:22.571Z
|
||||||
|
eb3c8928e13441f45451f33a74e8478bf53f6d9d5d242aabbb9c2dfbbd18dde6;978406058058a5cb835eef74a535a54315042f3466f9b7cfc5b5232f6494bdf8;10058;1;2026-01-26T13:04:22.572Z
|
||||||
|
4b3b31182c85fd2e23ef2118551ea94ba2efd6c675ab40633019713db57789df;44022f80d4e6c4dc5ed4e614c9a2a7d72da4127a0e20d77d8c71cfd4b3bcac0a;10059;1;2026-01-26T13:05:22.489Z
|
||||||
|
5d6edccfab0ac3a6daa10e0f07166dd40c7fba923433b7c4c31e1022a8a02aa1;bd47d59858be6e7413580c2a635604f7331c9aca3a5116c27b483e5171cf0571;10059;1;2026-01-26T13:05:22.495Z
|
||||||
|
846b5a2507ad29635d48b8dcca342a62e5bb3af255e02216e35e6f9518c50076;d68451681716e9d74d3d46438d1ad26c3eefd302eee561e0ae8e5da7d6248ab0;10059;1;2026-01-26T13:05:22.497Z
|
||||||
|
5bdc308d0b738d1d23821964ce1a197750f3595c46ea368d87f9a96d966a060a;8e4c90fdffcfa81c8736275ee62af863c8a6fb43f7eb201c477bfffc5522d5b5;10059;1;2026-01-26T13:05:22.503Z
|
||||||
|
3c897fff69a7e23d63774bcad8f929c8a37836c05dfd04cea7f01bc4cf4616e0;baf6007c11ae40e4757ce432276333e82b83db5a39476cb150dc715d856b212a;10060;2;2026-01-26T13:06:22.484Z
|
||||||
|
bad75f719ec49fa53a63115f8626c197a4b3bdfa23e471e4c574faef16863c01;86113b24f6c91dcb718cd7c7e3915c7c7f1d7752c4ab233061bc9664e6e5a957;10060;2;2026-01-26T13:06:22.489Z
|
||||||
|
c51f00345179cb5699360f367f43fd93d1b4e756a9ccf4fbbb229050749ba03a;a5f5d806acfaf72c803cd74fc988e93a8a0efbaa78d6e5a5ce7876f087447db5;10060;2;2026-01-26T13:06:22.493Z
|
||||||
|
44ed50a63004a383eab4e76d323aa6e0b3d8629369d821a70de1ccc41ff55815;8f1b019b37a432ebddc8fbc9b87f6b66dd15f5d2e4db8ee7878df958c77835b6;10060;2;2026-01-26T13:06:22.497Z
|
||||||
|
cf89863d3d440eb248a5170ee9c9e002ac68360acb67f67b881eb857f7f82a75;efbf2cd4659dc4bad7d52287383ffcfbb97af2d34dc9b67eb3a0f979e0eefbe2;10060;2;2026-01-26T13:06:22.501Z
|
||||||
|
3d212e9926065e44cf233e080f9a1cbed90ba21e86a5efc9709b8e419f39f340;7adbcf012592fa70cdb57b397b2bad7124926569e3747787f91ccc4c37e6fc1c;10061;1;2026-01-26T13:06:22.522Z
|
||||||
|
8dc7b08f10c78f44b403650ce92b7205cfee6e70e032a98a99657cecdd744fad;b76f1ec313fd8620749d7769ba9932499699fa0c2fc8afb7fafef7e30460cb29;10061;1;2026-01-26T13:06:22.527Z
|
||||||
|
faccc87eaa2df26eb5f986e9544be68443e951503d9c3b5784b6d88fe4e08c50;e49a6089a43637ae07fa6e66ad2eec5587ec6219a6415b690326f52d30f72a54;10061;1;2026-01-26T13:06:22.531Z
|
||||||
|
7519e23a81fe0cc849dc07734a05750c6bd15f1084ca57558394046db3486117;ea201af5f5bec690261b44e693ac9b18d70b66cf31cf4ef0d713fb23947db493;10061;1;2026-01-26T13:06:22.535Z
|
||||||
|
0e5aa0ded46a30413b8b33248ff82bf1c1a0e163f1c3b2e3d3ba511058134f1a;da186a1165be8a6b5814521dd9c95707aa807e5b60ebbd257951deaa94c14595;10061;1;2026-01-26T13:06:22.540Z
|
||||||
|
0fee0bd29378bdb2d4550885367b07030c8bb8495e50a1f5881e2f204d73c464;2737e8f6f5d765f8a5b707f4a230b02d2e294ca31d29acaed26d8923dec93aa8;10061;1;2026-01-26T13:06:22.544Z
|
||||||
|
b4108e73b5f78d60ea1d251175b3170fff1915ea934dcd774e82c130f346d717;75770bb78a3d182f5068fb175ae223b0eac40b0d3d466bfb68d453d0b698e8ab;10061;1;2026-01-26T13:06:22.549Z
|
||||||
|
5965076df1d3b0897852c3913fd01ad3ffd3ec36f90c37582a11c5d6667ce758;2ff727c27bea4e0657d52eac0011df97d95f2e7f9ab0773afb29e19282c063cf;10061;1;2026-01-26T13:06:22.555Z
|
||||||
|
c97a498b6e6141b10606de1f86dfd53b8d716e1a13b40df081d0eacf8e4295bf;9dc4d6d4f8a0237e3eba0c1f3ccbd06e3463147f0e7cc9f8c029092b122c00e7;10061;1;2026-01-26T13:06:22.559Z
|
||||||
|
11149aeda276ba4679a9bfb670d77a40bfa188825ab6a30cbb05aedc9c2972f0;7a05a5f163eecc3402a7633033c11e8eb5e40da0a97a5c757a0d440d37c35517;10062;1;2026-01-26T13:08:22.447Z
|
||||||
|
97fe1b3abfe91d0dfac57794d463728fb7f1f8e6fecd8c5bf540f3e7727e79fa;83310612110c6feee90e5738d1cb97bcffd52bf290438acbdaf8bf3db4d8a14f;10062;1;2026-01-26T13:08:22.450Z
|
||||||
|
33346aa951fc8449f07f850ee33346f223ce5117bf212de58b7a6e59fab05baa;6afcd3b77c41ce11e430fb0defaaf7841fc60612a221de94a30304e29c62a454;10062;1;2026-01-26T13:08:22.454Z
|
||||||
|
3ba9acd0e21f65fb095074164d3bb4b386f37dcec37f679f2ed476df69d94f14;578693cdf7c1f0089037999e30b47881ae4223b72cde16704f5cf0010cb5d356;10062;1;2026-01-26T13:08:22.458Z
|
||||||
|
2053d91b9f67fdbadab8a84c933a8cf89e937d6703e9f585d171a17c4419766e;e9276c00e054e9c9ddecdfb72240e23042867f33db12e1c6b1489e632c466485;10062;1;2026-01-26T13:08:22.460Z
|
||||||
|
9926e4e2828292bed111bbfaa223380b7268db9296c26e8a0c761d47a3f83145;a724ff549a4ee045c19e38152db2906902a89e639f8549d27d1f87787539a985;10062;1;2026-01-26T13:08:22.470Z
|
||||||
|
edb5d775fe14495b165d6472d9537995e856095abb8284879934dd95efde9589;ac1ab03e1f26309f669b97cc42b78629f69565369607caa6936188595664dbb6;10062;1;2026-01-26T13:08:22.474Z
|
||||||
|
077abaaf906792332d8c0777a5dda0de1c66348538a27667482fc8145b2f3406;a321261374aaa4cac52cd08faf918cebbbff4b47e6bd8868c6bc1bb7239b57c1;10062;1;2026-01-26T13:08:22.478Z
|
||||||
|
00f31feaed4fb9bfe54cc4df7e3f162d1a701cd0e4dffe7762ffa1d33e2045c6;e88912f5429b73f3eeb32dfec104bcdbc4a6697a6618c920805c369e9b8cb6e4;10062;1;2026-01-26T13:08:22.481Z
|
||||||
|
4e11a0401f8a2099599d02dc4bd1e99a3179f3c58ff42b0f0e3fbeef6a6b68f4;dc3bd18916452b4cdc5bdc506d7aca7c7a878151cb6807f8df33e7755eb022ec;10062;1;2026-01-26T13:08:22.486Z
|
||||||
|
c3e451c0d261913a31c8f4ba1f52a172af2441f69b7684afdf6d6f116651c4b2;a2ce333df12cfcd25b1abbb76a0d20f8b37d6769b24e697452bd8a8488ea78f9;10062;1;2026-01-26T13:08:22.488Z
|
||||||
|
348d6dec21338eb7e12b9b8301b405395e7ce5db50aadf474e8bf5d328bfa291;97d25634836599ebabbdba01e4bee3cdd54317b6b67124e91388d0f21d539230;10063;2;2026-01-26T13:09:22.470Z
|
||||||
|
1234567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef;d62799ded244b3489d2dd2dbc0aed3ad1d27cdb966fd374b9dce3fb5b8bcc638;10063;2;2026-01-26T13:09:22.473Z
|
||||||
|
ff8ecab0f89f419907a8e24e7cb45694ef709a9879cb0d9cd40879c169a7993c;e9df287015670eeb66d3c0c7e0612422f26bde220fef7b6bd25a42bd55b4ba3c;10063;2;2026-01-26T13:09:22.477Z
|
||||||
|
760903e499eff6cb5ed0f62525f05c98cab8a1bc509bde5a6f46abe66428a844;0cb6352e7a5afe08823e4ea5d9a7001cda42f8576593bace02c8882b3cb21441;10063;2;2026-01-26T13:09:22.481Z
|
||||||
|
86f12f86ea11bf65990f4be3680869613929f6d8885bee49ccdf8ab10a138785;363fbb84aefca9a76bd8704eeca0dc2567ceedfc759b3e128abcf9608614a445;10063;2;2026-01-26T13:09:22.483Z
|
||||||
|
511894b1d15b6a0aea5d227b4959ca95fe9149ce224d291a14c771ffe5f71e85;f521c3bd9fbcfcf8940271fdf43fdaf1c10b2e6b5b7cdf4dfe8b45b73e1d1f4d;10063;2;2026-01-26T13:09:22.486Z
|
||||||
|
d88e8d61dd386e07fd0f70fb5d315fdd8e1f3aedcbdc0b16690464499bdc7442;207b768d54436df53b936416a09fe1151fd76aa4ad2ff363b79597359edaf46f;10063;2;2026-01-26T13:09:22.488Z
|
||||||
|
7239962460fa2b3c2beac64be9a70dd56fa53988d272f1c8f8a61d92686d3815;3a16f63d78305627158c679cba2c3b0d777d2da093153b846be84e0e9ab27582;10063;2;2026-01-26T13:09:22.491Z
|
||||||
|
84f0018d4f8f352f87a2dad9c594dfc56e563501e6ed0851e12797127f76b768;f6d6a790efcd24f97641342a771a35c4434ccc301ba5a9b78bccfed510b96c97;10063;2;2026-01-26T13:09:22.494Z
|
||||||
|
869478a5912d2c6bbb7ac9d9372b9356f54ee389aef6cbdedc88fa4eca2130f8;3427c1ceb7c566250a4e214bec14c8321553743b810ad78d9ee7108d01d10798;10063;2;2026-01-26T13:09:22.496Z
|
||||||
|
5fd61687c9ca4eb155630726cf49a4b4972c7320ae60a0273c946384ba0b5b8f;d048bcbf7db7c12dbf5e330eef88e50f25b4a7c49497317d2ee97dae06b3cf9e;10063;2;2026-01-26T13:09:22.499Z
|
||||||
|
e916d12a7b6b5abe947b6b103d00194c1ef830ca6d0ecc29505b1f1c66826f99;c47b6747283468857a00de31a060b3baead582c02aa63676cc0a18132d2ad2b0;10063;2;2026-01-26T13:09:22.501Z
|
||||||
|
479bd5ac60ac7d2789f5d633c34d2d3388e80c5573db6c082ad50b0e84cc7ef9;3364da23dee9326cf35f80d03df6e0d4447af43e05e105f9871debcaf9ae51bd;10063;2;2026-01-26T13:09:22.505Z
|
||||||
|
6807003629a733ae572a0c1fdbe80c4acaa7c9d8eb3f86856b5b56ec7475250a;3ccc5b6a70532f891c732e9a5f7bb860f33d7ce1a802f7344edabf7b9c92a8be;10063;2;2026-01-26T13:09:22.508Z
|
||||||
|
a3171a357ed5469a81933d013c8349815401fb63a7fd87d03bcad53c07adb0be;c47aad001ad300689eb9ee677f5d04e55c23afcb318f58e10a7cbf40ef9e97c1;10063;2;2026-01-26T13:09:22.510Z
|
||||||
|
1dc40d3b5d994ebf83288bc6046c07c7cdbb7d465ebec11268439061e55a75d1;e532c8d13e0d124e91f89fdc4ce76c519d02f4ec210c6725e54ca39c5a24fcfb;10063;2;2026-01-26T13:09:22.513Z
|
||||||
|
1234567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef;1aa447479fe8df1e1b6536f23683a62bb9a118666e852619eaeacfb169c1d604;10069;2;2026-01-26T13:15:22.418Z
|
||||||
|
29284dbd36fd123d66245dcd06b1db835f2c1f0d097ca2026f07f3f342cb2dc7;8dd2b717b56de3b1a301452b77da989a0498b8eb19eae8e78765e7a70f5a0e55;10075;1;2026-01-26T13:20:22.398Z
|
||||||
|
ee4f2b50ca883966a9ae0bc5cf3456a41c55017560677016a92620a30c365677;5e1129a7665d0ce5b61d9a9459c89221a6e3926cedc2c818c8b00e5a5ba05ecd;10075;1;2026-01-26T13:20:22.402Z
|
||||||
|
eb89b67e9f8dff45be334f4cba27fcef7bd67d298d758914ccab37357e61c9b2;7fa1eccf7b274e531121a43f999f270e7629a24e1799c5b50522e756bf0bb3d6;10075;1;2026-01-26T13:20:22.406Z
|
||||||
|
d4fb8a5adf7c3383584648295334f7be10bda4417dbf5369a98df1276002bf74;e7f67622d324f96c0d354bc9db3968b87c476aad7c8b8a12de3dab3f2cf7f602;10076;2;2026-01-26T13:22:22.369Z
|
||||||
|
28b81be38bfdfb9e3a96189f26cf8440a7cbcb73d78e9ead5f8f5ba171cc989e;989057c2124439d55d7d23a84f685482c3d5a21bc6bdb69fea0f08da7cfc400b;10076;2;2026-01-26T13:22:22.371Z
|
||||||
|
4bc0d5882a30ab956a49b27074ee1d9ae2bb1a301d20edec9327c9622c1045e1;d93d60f961ef9b6dca58965ea129b62a7f1d0c7e01a53b1bfed2e4d23e2da00d;10076;2;2026-01-26T13:22:22.373Z
|
||||||
|
6334f92c3d0e0224de56dcaed3fd126c3a809d75f0996adcac6fa04f5a7e3f6b;dac4322268e0e854ce9e25a58eb67a34cca6a484407a2a79cacbb16d627d682b;10076;2;2026-01-26T13:22:22.375Z
|
||||||
|
d39de468595cb4003f4bd85ee8cc3bb1d579ca6aa683ba1e5cf9bf76c0a8759c;75c320fdec659d3bee55b0458f227bb4b34810686100e39bea0dc2fc469a193a;10076;2;2026-01-26T13:22:22.378Z
|
||||||
|
ce1f23779d4161b0ba66dc3851c41affd9dab61bfe0747a30460df0606695953;9213d52519fcd5fb9907c0d28a5fc13253f60c420fa97db9b6ccca9097e2f23f;10076;2;2026-01-26T13:22:22.380Z
|
||||||
|
58138b833be0bf5b655f5ff38f57e2f6ba3e77b2b5d138c408e51eb5517e7a0b;cb8fba42762568d42aa0389d2c29a7aaca05f7f893774540bf7a6f72b4442540;10076;2;2026-01-26T13:22:22.382Z
|
||||||
|
19ae45319ecb8bef0c3ded77fb7e84e84531efef0ec1913677c9be43d4f56143;f0cc3fafb774acb00d4dd04e44ef4d122fdd5d7adc44f07d843054d47f07d444;10076;2;2026-01-26T13:22:22.384Z
|
||||||
|
4ae864998e7799a76f28b4777982884ee0d22880deb780ebdce51efd522de2af;1d80606e3cec54dc4b2d1e308972587669ffe5dedac46c2de9631c422f757f4d;10076;2;2026-01-26T13:22:22.386Z
|
||||||
|
71643c955c59d673c2fa56a53243037a89f3b22fb4b46a9b1f7340b8f0d642ee;c713bd7f88c36dafed5f44eeaca3b31fc2c8f119cc06abaf9f122daf5ea27157;10076;2;2026-01-26T13:22:22.388Z
|
||||||
|
9536a1f49750ebadf379694944ca364730e8fc0beb0d46a88a24c58614e23c38;bc24a20d8757c00db9f72b281a92146a64567e6228da9c60bfd4dc185716605a;10076;2;2026-01-26T13:22:22.390Z
|
||||||
|
8bbfbf339d2df331062c65a942670054b0e92349e0a812caa30acb039cbcbe28;aabcce523d16f380455a1f3aeebe16d553331e2d7700ba556420825b6fe33567;10076;2;2026-01-26T13:22:22.393Z
|
||||||
|
eb73fbb26543acfb04b11c51787567d1f06291e1f9e1914f53106d12f329ec7f;9b30856ef3295004abb4d8f6806689d7ba71881af6d0cad0d39949cab8f8fb79;10076;2;2026-01-26T13:22:22.396Z
|
||||||
|
1280a147a99b742f6f8e38787520ed8a9a9d7d01201331204bb271cff548a4a3;ffdb26a6bdbee2a03cfb820aed84456fc0b9d142bab80897abd47655a84c257e;10076;2;2026-01-26T13:22:22.398Z
|
||||||
|
34413517c3c83028546951f3c5268b72d39dc5a2b5eaab5fdaddc8f3b691b910;8d5c1a1bc63d03924b5dbd0963ed7b25c183870de5bd871391015c09d762eb8e;10076;2;2026-01-26T13:22:22.400Z
|
||||||
|
05daef882f238a11e60c93049edc3add549e0d3ed6001fcf92818f2863ec43fa;19ae7097e7ae5a3a68eca3fda1a782ac178494faa86e2e3887ed72712e7e5990;10076;2;2026-01-26T13:22:22.402Z
|
||||||
|
94e24de41afebcf9093c081c8a866fb47975bdaea06bd81370201905ee39f780;d15b50a07078fc8df8e084ff61d373ad29fcab64c24f79f2fb3ab8340f74429c;10076;2;2026-01-26T13:22:22.404Z
|
||||||
|
d7d47c99719bb39c92bac96db42eaade5e22860404f8f4cc8fb6ad3ed171af5b;2207ca4086536e33d0d5b6db474903ec1396d668d82aca89f2e8eb4f5b94b5ae;10076;2;2026-01-26T13:22:22.406Z
|
||||||
|
d1d428e11cbb511a683490c280c400efba9de3a6abb34baf2340be4672ca2df5;86f4e9d727113b252c27dbc1ca0e3d14b21f72b78570533886f5548f4a77dfb5;10076;2;2026-01-26T13:22:22.408Z
|
||||||
|
665a9d17e86df762e1548382311c09e45150942d4c8fb0731a37812f8a808fa0;eab04d97fcf85465e3a97297611bfd692a034ad758c68ff2322153d5335372b9;10076;2;2026-01-26T13:22:22.410Z
|
||||||
|
5797158d853b2be55145155b72add5b23af9e2ba38c91162e1250221597eb7d9;90eb430ea5f7298f9e71814a07018bad87b9f47446dc0c69437480d05de446d2;10076;2;2026-01-26T13:22:22.412Z
|
||||||
|
3475943cb4fc7b3870d99f5cb622c66e20c17404ef50e1185a3732b0f6e3bf0f;1925f57da844019fbc55f4c88fb2995600b871388e48406dcfd37fe9dc546dd3;10076;2;2026-01-26T13:22:22.414Z
|
||||||
|
9822fc3feae0e2d0e409f7809370aa7d9abdc7df1a0b55960b66866f88b5aac5;2e15a140fbd01ef8df53a288e4af88fc1758fac379ab3af8bd0194c037f090d4;10076;2;2026-01-26T13:22:22.416Z
|
||||||
|
b408f5b4b42d110250777db1ab016fdae56821ab0de52d67a6f90e8947a3a020;455ecd38464beb22fdc5fd5cd587e8bfe964a8e40f2311402ed0c586e89e53d8;10076;2;2026-01-26T13:22:22.418Z
|
||||||
|
c067030ebec085e8c3d91a59498f24561bbdfd73c882b82e2ed897286237d729;894cefca35c6157d0541e6fc31ddf2b72c7b5f9a3ad3f893edaf0b6e619d5cd9;10076;2;2026-01-26T13:22:22.421Z
|
||||||
|
e54794ed6ef4bc058bc7bd8fc28126b76a39bed2f7d1d4ec21ce4c55fdbcc39d;1e4e67012bec57d02159a7dbaf22d9fe4021b2ac83a172e54b6a5948f5bb18e9;10076;2;2026-01-26T13:22:22.423Z
|
||||||
|
3f6846ad574e3e79076b70161bf307ee0dbe9c0bc729d6c9a0c48a4c8eea77d2;2ba4eec6478745e3972625e9743ff44576370431df83fd0aa24d54ab8fae07ee;10076;2;2026-01-26T13:22:22.425Z
|
||||||
|
463a11a2aabd2bb384c4080763e424c662c9dfd1b12e15f8ae5ad6ac3c9d05e3;78d26c5b11fdc1b9eb04a388cbb14c086e68e7233c3ae3a4aa9e26a7348f9bf3;10076;2;2026-01-26T13:22:22.427Z
|
||||||
|
13d539a40522cfcba19acb1f5a35c6e0549283c62b8fdc016a9ad70b127a3c23;d0eab1f9199b362b319b1b7723f3d6bd008b02922142686a97f705063bc97904;10077;1;2026-01-26T13:22:22.443Z
|
||||||
|
4a122c665d5c3c13c54bf295129179a167bfd6d5f0887f03b8641d23f1e2c4c8;0b9f77703bf806bd6a90a5098c8a3ff618bd19bc0b2c36dd91e5e23c3cb2b117;10077;1;2026-01-26T13:22:22.445Z
|
||||||
|
f3477cd39c8ef70b3466d89b19d52debeae08296c812cba59fc62ee5ac63a3cc;9bb3349749aed4d8c08051df435d4448b5ec11186e66399b5956455575a3601b;10077;1;2026-01-26T13:22:22.449Z
|
||||||
|
5100371c4804cb88f252a47c40907d886986e1b97bb0f88041e12634c1c11344;ed301145bfa181bd56502c2d6307d6231533d98b588ac6d53e86dea0314c532a;10077;1;2026-01-26T13:22:22.451Z
|
||||||
|
a0ed5c723bdbbae8f9eee3e38d9ba718af60ff322ae05cf01c66f937e720c0e3;0a3b3071cd779d51be5d534b75089394c267a10783c29b0033c20d494b7aed57;10077;1;2026-01-26T13:22:22.453Z
|
||||||
|
cad30c4f5206ad00e7dcc88318b790c2f52dc7676ae3114c7898f4a8c9b243ec;348606bdc26a62cc867f2e63dba1c719213f15bdd83a75356204d39f1d4ebc65;10077;1;2026-01-26T13:22:22.455Z
|
||||||
|
1cad2c073c941d9b96ba67f8b6590028ae8f0709c27e99bf3d5e6085e5ddced7;265ed1b69a462bd156248f9df627a0f86d78fbc1c083ff97463d6c1b76666b75;10077;1;2026-01-26T13:22:22.457Z
|
||||||
|
cd1d1b4340c464e87b356b00c2abc218b89571e2474b044bc719572bf60a1244;b55642f9d29f1a4a1911fe09a6408f430b0dbe64bc1ef1a1e99ee64a16238d7b;10077;1;2026-01-26T13:22:22.459Z
|
||||||
|
ffb4eecfb4e6d9f2152dccd1106a484a4f1b09fad9ab70f11f9ec68c86a90834;004c86472252484a9ac5df900d39e03871e99a5014ae5461fd1ee04e55ba03b0;10077;1;2026-01-26T13:22:22.461Z
|
||||||
|
a7268f32dcc82f0d32a0a47edf234f9ded5999ac322e533e35b5a9f268f89519;6169a1c9a572a0357f3063a845881a7d7cb43b005ee360667a5704aa5956cfb6;10077;1;2026-01-26T13:22:22.463Z
|
||||||
|
739bfb9090e20de3cb9c7042e3de22280014c5e14840b9df4a1845f23a1fb4fb;8f5251d4965bc13f2bab23009ab13021dbfdccb995b8d0efc6d117e8191dffb7;10077;1;2026-01-26T13:22:22.465Z
|
||||||
|
8647c1b7e00bbec76edb93a6c7a6fbf62845cf9a75a351b5f53e948958a7b60b;1fd7b55bf56bc3f9a483a3a1304be75e4cd8a331b062f5635c67dee1bd2726bc;10077;1;2026-01-26T13:22:22.468Z
|
||||||
|
0a5a214570557e4d173dcb2200d0cc411c7f3aec71e8fbea11c6dd32f612e693;63e8bba641d08974520948d38ed243e854899dea1ea2552db5f636f0f62626bd;10077;1;2026-01-26T13:22:22.470Z
|
||||||
|
c34d09487c91e536673d55a4046c345689c23365de5bed25054b5c3fb5a39356;0a73a9671c3e01c0d24ea20604b6df51aa784a4cd491f4b8286d8527a8e85bc8;10077;1;2026-01-26T13:22:22.472Z
|
||||||
|
6e631ca1e56e3f0264e9a59a86f4ce934377cc1cb2c85f05e6ebc11b5e94fe46;1e9fad8fcc155bfa998d6b1cac4ae1c1f1623a3ce25d3d985a0bfcef586eb9d7;10077;1;2026-01-26T13:22:22.474Z
|
||||||
|
4f00c30d951d5c6dfda886ea908a76828166afb031570b4545ef2d69615cf99d;d912880113e824e3fa43139335a4ccb62a462d70b4d99769bd1e2639d50b9adc;10077;1;2026-01-26T13:22:22.476Z
|
||||||
|
45a47b97a3655560315de7c74f3de67bfd23263362f6b8b435598fa006f885a4;e7dc7187c755513b673d3d2d7bc0505be76e68e2e1ae9e6dad3ecde9f7d79fdc;10077;1;2026-01-26T13:22:22.478Z
|
||||||
|
c0069805470b41d2941e4c5b686cfc4bf409c65e88db216e0e110dcc571e592c;8adc04cb83318d848a7ba90b5f19b7374f4f6b7ef143332a28169d6ae5f7daf4;10077;1;2026-01-26T13:22:22.481Z
|
||||||
|
e27042391ba1ee610ddff37def5dac7411e3ac64ab8974f65ac863fcf0ae15c7;ba0224a0a65f8741fc576d3749514575d3e88a7886873972ef42eee7ce1fe4f8;10077;1;2026-01-26T13:22:22.483Z
|
||||||
|
1234567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef;92b296008e7e0e8d8615d7583119656a89680de8e5ff554a3a176772f04fa907;10082;1;2026-01-26T13:36:21.835Z
|
||||||
|
1234567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef;a7cae1c0723583b37d9bd09319e55a5e46c4eb95a53e6ce004f0ccd783891515;10082;1;2026-01-26T13:36:21.837Z
|
||||||
|
d12114492ac7cdb06e63d9b5cf248f4896cd7e7ed304a2dbeda398de14ff9fad;3ad2510330c195d48f4b0c1ac13a73d77974cd8741325a2cbd2d5cb247848a02;10084;1;2026-01-26T13:52:22.329Z
|
||||||
|
86210d7719230a5cd40afdd9a8d4472fb6c10e3b2acdd8a6a9c10b6f4189b5e4;85ecb639df1942444fe5348cd4b91afdd261218e56e6510026eb75e93945be03;10084;1;2026-01-26T13:52:22.333Z
|
||||||
|
0c61c9e9a7442dd8df60128fcf105c09afe7d56cae5e742e80ed9176484e112a;9779c2a8d44f695da159f24e574c680e5ddc41210c11131198413f2422eae70e;10084;1;2026-01-26T13:52:22.336Z
|
||||||
|
decde15bffb205751939d36dc28e2dc0707fc7dec903ed42b0c82326fd7dd2e9;ac9e27eace16d48309d38c58413a3c6dffacec8cd945a05f0ee921523fc7a125;10084;1;2026-01-26T13:52:22.340Z
|
||||||
|
77e2e0cf0bbe60081f6527940467754ffa084e53f613541510d301a8cdf61268;a6403296f2ff72ba933bc0c6c2a9aabcee120231ad40ebcc881cf6c0c167cd2c;10084;1;2026-01-26T13:52:22.344Z
|
||||||
|
1a3ccaf9dfb4752b1429c4da73837827160f2b7f43ca1fe53912d42a05c4683c;857bd31e766222fe1ede8b20def72ed106a4d249290ae9cc5fd72633d5886937;10084;1;2026-01-26T13:52:22.347Z
|
||||||
|
df8c12f6cc3430ff7c0cc6d49b87638c8532315f3e1a6f49f9f248a4ebe518d5;be88a857ec3052e9253c57be953817e08a7ce4430c1f8f8cfed15278e6654238;10084;1;2026-01-26T13:52:22.351Z
|
||||||
|
12b9eb4a7c6a43c783de9bd0f98f8d384e9676ba7e53926ec0a2a316ad7ecb31;7d298ea8ddd1479daec94cd49963a6c5c46266673bd5de0bcb5d37390e862d39;10084;1;2026-01-26T13:52:22.355Z
|
||||||
|
388cece8b03f45a255213d9b1dfb394ab3bb9232a6707d9fc94513e872c7d53c;0b4af26d735ee321e77d8bbeffa0c864820e8c4184438ac7f3d2b782d14ac13a;10084;1;2026-01-26T13:52:22.359Z
|
||||||
|
a66ca5d792e8d97313a590364ee7e6ab1305c10fe9a2b4be8f73b525b2f14772;4e017c3e596935c511733d1c4c3f4d3ac1823f553a89559ab698fe365905566f;10084;1;2026-01-26T13:52:22.362Z
|
||||||
|
e7d43d1558132596e88beaa84b55f5c327dfc6aa213b836585d596a93dce4e75;14ca7760d8531c5bc8d571b0dacc7324e199fb9fc449623c87161f8536f73d71;10084;1;2026-01-26T13:52:22.366Z
|
||||||
|
640221e3d64340ba39144c588dc57a0d9dad29f2ce825f2a0baedfa6577926b6;17db0d4b4dc60953ffc130a5dddee7d32a7b09947e2964d0f3ba1c245c430f83;10084;1;2026-01-26T13:52:22.369Z
|
||||||
|
877a1f9542fa33268431ad91002500f4e2fd88767976509e5bee596f64d994a4;1be46f1be24a519a69060ecd082872647a46b70f16c13002cd20d539df4a428a;10084;1;2026-01-26T13:52:22.373Z
|
||||||
|
2e53630dd33a27169a728cd0df75dabe537776fcd8d9dacb711e854ea2ad28d9;d80a3911684e25478834d7d64396bd9ec03f2fa850d79ff7e482e9c0c0e1a094;10084;1;2026-01-26T13:52:22.377Z
|
||||||
|
698c20ead1f47313bd6fefdbf1f7bf7f23edfc9d73e1dfb2783da573ea4c303c;efc01359ba732f8e5edd1b1bf2a7463faed985778e9bd35740de5ede3351d394;10084;1;2026-01-26T13:52:22.380Z
|
||||||
|
bff06b11c21b4212ea906c0d33ac3531fdd0fa28d8bdb9f17b90b56fee2dd825;d127deeee1507dc1ccef82d5a20b707c2429173967807295f38aac5437cc4598;10084;1;2026-01-26T13:52:22.383Z
|
||||||
|
76ad4da418d1729efd41daa0f029eaeaaa5c9e7237a37305ce895319cf7d286c;e70b70be4513a0c403415b17443a7215f02f62113be5efecd332732e0bd5c1a5;10084;1;2026-01-26T13:52:22.387Z
|
||||||
|
ca902ab65676c0d3f2dce1a94267509ae460efd8d32cc31a314c9c686d7fbea8;933c221c99e16ac7b604919b77dea479d4f8fec8f93e38534ce6acfce251dbaa;10084;1;2026-01-26T13:52:22.390Z
|
||||||
|
1234567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef;67e4c94a1ff175c81b5cbb03e09639fd43f16cca807d21fae46f911e0eda3dbd;10084;1;2026-01-26T13:52:22.393Z
|
||||||
|
6052153a53669fd9e53ee774c62db01aa7a03ea3d7e950fd951dc25c66953de6;3df3e9f13ca3943fb64d161aef1fc74f5734fb57c7e8990a7d549e860affb3cd;10084;1;2026-01-26T13:52:22.396Z
|
||||||
|
f234660475ee9beb546304ad97870740d41e4ef28fe595e86e3bec223669102f;d517d7435414970c6037095ee4f9152d027325745fc2b2a09611338bfdafead1;10084;1;2026-01-26T13:52:22.400Z
|
||||||
|
1234567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef;2b3dac891a8b17406f9c5c3d7fbb2e49eb387c29ef4b5df86e113bc7c09331d6;10084;1;2026-01-26T13:52:22.402Z
|
||||||
|
50c4d20dfd2d55a4ac365689377fd7bed1e3830ecf4c00f4c2846dfb182d31f1;49746db5d89877d817a7e4746ef07fa9010a966b09f3b60ee8d6d94ae2e27dfd;10084;1;2026-01-26T13:52:22.405Z
|
||||||
|
0e4747f1d803a7f576c4b4809f95a9e7b018635b3f4685494685d8c16423b446;0136e453bcf3c07a0f5584f120ebeaa928cb1d5bbb7b23dade9f3114b10b0704;10085;2;2026-01-26T13:57:22.265Z
|
||||||
|
79e49866df36f2a54a68e8592e7b8638090276709cc190af44b70f5d361ef49d;a1d0023f13569dc60408cc61c41f5fef4f6bddc1db4a1e4e5269fbb255e5b50c;10085;2;2026-01-26T13:57:22.269Z
|
||||||
|
9f03199a704b168df559a511986f8840b73a9fc0a720c2d8057427951e30b6c2;79ca05dd9747e44c31c3c851b254af5a84196111e7174a42fe00664dee6fd80c;10085;2;2026-01-26T13:57:22.273Z
|
||||||
|
f4ce4659fd752df5b45c29174de555063b35ded4e1831591000d7f31915bb676;5e3fd222b3a048b3ce4cbe94b499d234678adbda114b72b1a0b0c62162d0882b;10085;2;2026-01-26T13:57:22.277Z
|
||||||
|
5357b1e37d32881d5a7c5692e94cfcf25345d8fc52e35085ed1100070c9910cd;6f7b10b04e0cdfc0db92f9d14002c43f4e8da57fb6af76e06645d3c49add702f;10085;2;2026-01-26T13:57:22.281Z
|
||||||
|
f79a450283f5a1e0cb95c4b6bf44d00220488088fcfbc29ac067ea0c4cab6e1d;d003afb43152427432c133928c08705269648045d85db1c1cc4a233ad07c653d;10085;2;2026-01-26T13:57:22.285Z
|
||||||
|
8f174ff7f171e839897b487e9469c0eaa1b76b340a33f0899cce1120d48c8617;a6707d085e40c8350803aea5e1bb6f8a501a66661dea5324a83fbfae9fc0b451;10085;2;2026-01-26T13:57:22.289Z
|
||||||
|
090f968f0b956dc935aa69c8cc931231abb226474af32c56e50548aaab8f9929;63d2269b86637d01a65f860cb960ec8cc46d3d19ef14be6de192c0ff28fe8a55;10085;2;2026-01-26T13:57:22.293Z
|
||||||
|
c12b4aeeefc21dddc1784189e6fbd017dbe5ab622896728f795aa488347469b7;c487acb090df9b04454736eb48cd78aa4be5b2a0f2e8e7902d453a165e82ab55;10085;2;2026-01-26T13:57:22.297Z
|
||||||
|
f384d647ac98514b721c76cf8710018628d1db0b41058c7228772abd4618f41d;21ac917de4fce85261f5b0943bef3e980ed92a27af123c588f0bfbc376e1b564;10085;2;2026-01-26T13:57:22.301Z
|
||||||
|
2e02864259cab3745cf89098f5a89d7af347207bca1e44492d7ce1ffe39d66a4;c3aac2cc07df2e3113eaad6919799656f22796979e987d460cddd149a4387976;10085;2;2026-01-26T13:57:22.305Z
|
||||||
|
307d875d053721800caa6c94b77432330ffb5746e9a58bc11efea2d6ed523bac;951c64067969d0f883509dc12df5559a114ebd3b175788c2718179de8483087b;10085;2;2026-01-26T13:57:22.309Z
|
||||||
|
0d679f9f38f283da47104cd2c20b03c2078e72f0615927db182f757454470596;20f8d44c4c80d18fdbf944952aacb77473c830eceb5b8a03fc3ecc8e8a74a57f;10085;2;2026-01-26T13:57:22.313Z
|
||||||
|
7d59e497518716b093a6d8bb020f759f03600f13eaac37674b6c7c0c75de2af4;0a0b030cc3c76e5c1ee21dbc9ad92163d7e52fc7bbef58df5de9c3eb58718681;10085;2;2026-01-26T13:57:22.317Z
|
||||||
|
74b01ad4f18b86dbd0e03bf89d1c6933d04ecb21bc3e5e4813a86e09a602ca3a;0f6dfeccf1526419a0093296d467cc9d9b5eed0ca9b0fa70648af625f4b99c8c;10085;2;2026-01-26T13:57:22.321Z
|
||||||
|
1e375792668cd35c4b2181e5fb7616da16ce4732ae513db1eabfe6b7d8830918;82054fc0201affdef02d7f962165ce15123e873aad3c9b5eb0b0eb4f84789591;10085;2;2026-01-26T13:57:22.325Z
|
||||||
|
b0a692730fc6dcdd9188b0a3d44573600f1bf5b48106b1b6987ceba87d1fcb55;50f58dba42c5dc806603b665010332ff238832222ed48bc7f65ef864e65f4db2;10085;2;2026-01-26T13:57:22.329Z
|
||||||
|
6b3351529e8fef1e590c89af27fab254e9d0f014397c193f6120ff12184e7c3b;6c3e32519a0d5a3ce1ea6710f260934304f1e796b34ae18a0a8c157a9a9f5db7;10085;2;2026-01-26T13:57:22.332Z
|
||||||
|
6188c6ccb5bd270f29d94d26f8a03a785fd1dcc47a3c183ff0d60184220acecd;a76827df05a4bfbce4b6549534017f1011e7f85e49ff3af32c372e209a4090c4;10085;2;2026-01-26T13:57:22.336Z
|
||||||
|
dedd3c148390e446079aa06ef2bb9c1b10419949cb77b972f621a0ee45433cb4;edfb5fc6f84edddc0bd9d6776d01fcb673b82808f9ae8614e5f338648418ecc8;10085;2;2026-01-26T13:57:22.340Z
|
||||||
|
f2b66d13aac2aa6af11f3182a4046e4cf5c412635cf3725470e005757d828c0b;1d7b034970b9142b8ff292af1ccfd55d921cc7948d6651ca1dfb1ab4da851ace;10085;2;2026-01-26T13:57:22.345Z
|
||||||
|
7c8bf431d4e0c5d4aaa8fff128d2bb05968421fa7cb39588f0700bab12acdda4;d1bbff7e38b78e948e1fd5c72988fd6ae3466be5883a1f1dcac928b364459dd5;10085;2;2026-01-26T13:57:22.349Z
|
||||||
|
4f0cd6842670f4118b57c3c75b9b3197068780d8f0e0529296fb1460bc2fb3ac;cfa9005918138fad4a63969590ef08526ffd3a5a193fe74d301e6a8a8f0a69dc;10085;2;2026-01-26T13:57:22.353Z
|
||||||
|
3e5f302024602a9f180ad1ff60350b35c29e97fc219599711d9f566e605db235;bd7eac69fd60077d4e4f7c3ee5c5229a9c675ef02ac7aefc59639e618c4061e4;10085;2;2026-01-26T13:57:22.357Z
|
||||||
|
cd969d726a3e4499c3e7681ea78e074038d8c0f3dd915d0aa066ce01f446c84a;f2128d50f0b5412e0b917ecef25956102d23a43d4ca3b7163e59b71975e0a8ec;10085;2;2026-01-26T13:57:22.361Z
|
||||||
|
0251b9929642405b088fb86ec491681bed144e2441d93a78a02e48cb663c2ba5;b6d13add9c9a8b0d864fb48b392f8c268dc144312037e78eb013c5361daa01f0;10085;2;2026-01-26T13:57:22.365Z
|
||||||
|
27f0c7453791ff40e7eddcb95a19d5bed1fb02211dd7ce3866ccd776d511e9db;7ebea36c902b2460603b816cd743978673677022a7f418eeb79dba72078e38fd;10085;2;2026-01-26T13:57:22.368Z
|
||||||
|
6998185e97f64dc64d1d8b6963fd9520e52d7d579cd9bb60548395a91800dfef;92369c3c0ab0bb80c77ed60d868eb1836d578eab180fda50b6c30d5bb16803fe;10085;2;2026-01-26T13:57:22.372Z
|
||||||
|
1234567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef;a0832d9512c3b04b5fafaef612c9969a5687f5ed7e5f0900dc29122f79ad7e5a;10086;1;2026-01-26T13:57:22.387Z
|
||||||
@ -1 +1 @@
|
|||||||
2026-01-26T12:59:22.551Z;10051;0000000d02cc6dd24f8c8773b4ce967dfe284705296a141d00adcaf95b9f1d5f
|
2026-01-26T14:03:22.233Z;10089;000000035b7f3ee40fa24b8767cd80a5f138a2236540ec29e37f4067008ebd6a
|
||||||
@ -81,8 +81,13 @@ Exemple :
|
|||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## 4. Références
|
## 4. Types (userwallet)
|
||||||
|
|
||||||
|
- **`DataJson`** : champs optionnels `raisons_usage_tiers`, `raisons_partage_tiers` (`RaisonsTiers[]`), `conditions_conservation` (`ConditionsConservation`). Voir `src/types/message.ts`.
|
||||||
|
|
||||||
|
## 5. Références
|
||||||
|
|
||||||
- `userwallet/docs/specs.md` (MessageBase, DataJson, Champ, Contrat)
|
- `userwallet/docs/specs.md` (MessageBase, DataJson, Champ, Contrat)
|
||||||
- `userwallet/src/types/message.ts` (DataJson)
|
- `userwallet/docs/specs-champs-obligatoires-cnil.md` (ce document)
|
||||||
|
- `userwallet/src/types/message.ts` (DataJson, RaisonsTiers, ConditionsConservation)
|
||||||
- `userwallet/src/types/contract.ts` (Champ)
|
- `userwallet/src/types/contract.ts` (Champ)
|
||||||
|
|||||||
@ -15,6 +15,8 @@
|
|||||||
|
|
||||||
Spécification du login décentralisé (secp256k1, contrats, pairing mFA, relais) :
|
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`).
|
||||||
|
|
||||||
- **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).
|
- **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.
|
- **Objets** : Service, Contrat, Champ, Action, ActionLogin, Membre, Pair, MessageBase, Hash, Signature, Validateurs, MsgChiffre, MsgSignature, MsgCle.
|
||||||
- **Graphe** : Service → Contrat → Champ → Action(login) → Membre → Pair ; contraintes « au moins 1 parent ».
|
- **Graphe** : Service → Contrat → Champ → Action(login) → Membre → Pair ; contraintes « au moins 1 parent ».
|
||||||
@ -52,7 +54,7 @@ Spécification du login décentralisé (secp256k1, contrats, pairing mFA, relais
|
|||||||
- **Pairing** : BIP32 UUID ↔ 8 mots (BIP32-style), `PairConfig` avec `membres_parents_uuid`, `is_local`, `can_sign`, `publicKey?` (optionnel, ECDH). WordInputGrid pour saisie. Confirmation croisée « membre finaliser » (IndexedDB), statut « Connecté ».
|
- **Pairing** : BIP32 UUID ↔ 8 mots (BIP32-style), `PairConfig` avec `membres_parents_uuid`, `is_local`, `can_sign`, `publicKey?` (optionnel, ECDH). WordInputGrid pour saisie. Confirmation croisée « membre finaliser » (IndexedDB), statut « Connecté ».
|
||||||
- **Graphe** : GraphResolver avec caches (services, contrats, champs, actions, membres, pairs), `resolveLoginPath`, validation des parents.
|
- **Graphe** : GraphResolver avec caches (services, contrats, champs, actions, membres, pairs), `resolveLoginPath`, validation des parents.
|
||||||
- **Login** : LoginBuilder (challenge, nonce, chiffrement « for all », preuve avec signatures), publication message → signatures → clés.
|
- **Login** : LoginBuilder (challenge, nonce, chiffrement « for all », preuve avec signatures), publication message → signatures → clés.
|
||||||
- **Sync** : SyncService, HashCache (IndexedDB), `markSeenBatch`, déduplication, fetch clés/signatures, vérification hash/signatures/timestamp, mise à jour du graphe. Détection des confirmations de pairing au sync.
|
- **Sync** : SyncService, HashCache (IndexedDB), `markSeenBatch`, déduplication, fetch clés/signatures, vérification hash/signatures/timestamp, mise à jour du graphe. Détection des confirmations de pairing au sync. **Acceptation** : une version d'objet (MessageAValider) n'est acceptée que si elle est signée par les validateurs conformément aux conditions de l'action de validation ; voir `features/userwallet-acceptation-version-validateurs.md`. **DH** : le DH n'est pas systématique pour tous les types de messages (login sans MsgCle ; pairing optionnel ; sync en base64) ; voir `features/userwallet-dh-systematique-scan-fetch.md`.
|
||||||
- **Iframe** : iframeChannel + useChannel ; messages `auth-request`, `auth-response`, `login-proof`, `service-status`, `error` ; postMessage vers parent avec `'*'`.
|
- **Iframe** : iframeChannel + useChannel ; messages `auth-request`, `auth-response`, `login-proof`, `service-status`, `error` ; postMessage vers parent avec `'*'`.
|
||||||
- **Export/import** : identité, relais, pairs, hash_cache, pairing_confirm (IndexedDB). « Supprimer » global avec option export avant suppression.
|
- **Export/import** : identité, relais, pairs, hash_cache, pairing_confirm (IndexedDB). « Supprimer » global avec option export avant suppression.
|
||||||
- **ESLint** : config flat, `typescript-eslint`, type-aware ; voir `features/userwallet-eslint-fix.md`.
|
- **ESLint** : config flat, `typescript-eslint`, type-aware ; voir `features/userwallet-eslint-fix.md`.
|
||||||
|
|||||||
@ -0,0 +1,34 @@
|
|||||||
|
# UserWallet – Acceptation d'une version d'objet et validateurs
|
||||||
|
|
||||||
|
**Author:** Équipe 4NK
|
||||||
|
**Date:** 2026-01-26
|
||||||
|
|
||||||
|
## Règle
|
||||||
|
|
||||||
|
On ne peut accepter une version d'un objet que si elle est **signée des validateurs conformément aux conditions de signatures de cette action de validation**.
|
||||||
|
|
||||||
|
## Concrètement
|
||||||
|
|
||||||
|
- **Objets concernés** : tout message « à valider » (`MessageAValider`) : Contrat, Champ, Action, Membre. Chacun porte un champ `validateurs` (membres_du_role, signatures_obligatoires).
|
||||||
|
- **Acceptation** : lors du sync, un message est ajouté au graphe uniquement si :
|
||||||
|
1. Hash canonique et timestamp valides ;
|
||||||
|
2. Pour un `MessageAValider` : les signatures jointes (fetch par hash) satisfont les validateurs :
|
||||||
|
- au moins une `cle_publique` dans les `signatures_obligatoires` (sinon on ne peut pas vérifier) ;
|
||||||
|
- toutes les signatures sont crypto-valides et leurs clés sont autorisées par les validateurs ;
|
||||||
|
- au moins une signature autorisée (refus si aucune).
|
||||||
|
- **Refus** : si validateurs présents mais non vérifiables (aucune `cle_publique`), ou signatures manquantes / non autorisées → message **non** ajouté au graphe, compté « non valide ».
|
||||||
|
|
||||||
|
## Implémentation
|
||||||
|
|
||||||
|
- **`utils/validatorsAccept`** : `buildAllowedPubkeysFromValidateurs`, `canVerifyValidateurs`, `validateSignaturesAgainstValidateurs`, `getValidateursIfMessageAValider`, `validateMessageAValiderForSync`.
|
||||||
|
- **`syncValidate` / `syncUpdateGraph` / `SyncService`** : `validateDecryptedMessage` puis `updateGraphFromMessage` ; pour `MessageAValider`, exige signatures et conformité validateurs ; pour Service / Pair, hash + timestamp + crypto seul si sigs présentes.
|
||||||
|
|
||||||
|
## Limites actuelles
|
||||||
|
|
||||||
|
- **Cardinalité et dépendances** : non appliquées lors du sync (nécessitent résolution pairs / graphe). Seule la conformité « clés autorisées » + au moins une sig est vérifiée.
|
||||||
|
- **Pairs** : la résolution pair ↔ membre n’est pas utilisée en sync ; seules les `cle_publique` explicites dans `signatures_obligatoires` le sont.
|
||||||
|
|
||||||
|
## Références
|
||||||
|
|
||||||
|
- `userwallet/docs/specs.md` : validateurs, « Contrat invalide (hash/signature/validateurs non satisfaits) », « Interdire l’acceptation d’une signature valide cryptographiquement mais non autorisée contractuellement ».
|
||||||
|
- `features/userwallet-validation-conformite.md` : validation login, clés autorisées, cardinalité.
|
||||||
78
userwallet/features/userwallet-dh-systematique-scan-fetch.md
Normal file
78
userwallet/features/userwallet-dh-systematique-scan-fetch.md
Normal file
@ -0,0 +1,78 @@
|
|||||||
|
# UserWallet – DH systématique et flux scan → fetch par hash → déchiffrer
|
||||||
|
|
||||||
|
**Author:** Équipe 4NK
|
||||||
|
**Date:** 2026-01-26
|
||||||
|
|
||||||
|
## Question
|
||||||
|
|
||||||
|
Le DH est-il systématiquement mis en place pour les types de messages envoyés (sauf DH) afin que l’utilisateur **scanne** → **aille chercher le hash** avec le message → qu’il **sache alors déchiffrer** ?
|
||||||
|
|
||||||
|
## Réponse courte
|
||||||
|
|
||||||
|
**Non.** Aujourd’hui le DH n’est pas systématique pour tous les types de messages. Seul le **pairing** utilise ECDH + MsgCle lorsque la clé publique du pair distant est connue ; le **login** et le **sync générique** ne suivent pas ce schéma.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## État actuel par type de message
|
||||||
|
|
||||||
|
### 1. Pairing (membre finaliser)
|
||||||
|
|
||||||
|
- **Envoi** : si `PairConfig.publicKey` (pair distant) est défini → chiffrement ECDH, POST `MsgChiffre` + POST `MsgCle` (`df_ecdh_scannable` = clé publique de l’émetteur). Sinon → message en clair (base64), **aucun** `MsgCle`.
|
||||||
|
- **Réception** : `fetchPairingMessage` (hors sync générique) fetch messages → fetch keys par hash → déchiffrement ECDH si `senderPublicKey` / identité connus, sinon base64.
|
||||||
|
- **DH systématique ?** Non. DH seulement si clé publique du pair connue ; sinon pas de DH, pas de MsgCle.
|
||||||
|
|
||||||
|
### 2. Login (challenge)
|
||||||
|
|
||||||
|
- **Envoi** : `encryptForAll` (clé symétrique aléatoire), POST `MsgChiffre` + POST signatures. **Aucun** POST `MsgCle`.
|
||||||
|
- **Réception** : la preuve est envoyée au parent (iframe) via `postMessage`. Le message login sur le relais n’est pas déchiffré par le sync (pas de MsgCle, donc `fetchKeys` vide → `indechiffrable`).
|
||||||
|
- **DH systématique ?** Non. Pas de DH, pas de MsgCle. Pas de flux « scan → fetch par hash → déchiffrer » pour le login.
|
||||||
|
|
||||||
|
### 3. Sync générique (Service, Contrat, Champ, Action, Membre, Pair…)
|
||||||
|
|
||||||
|
- **Réception** : `tryDecrypt` fait `fetchKeys(hash)` puis, si `keys.length > 0`, **`atob(message_chiffre)`** (base64) uniquement. Les clés et le `df_ecdh_scannable` **ne sont pas utilisés** pour du vrai déchiffrement ECDH.
|
||||||
|
- **Flux** : on fetch les **messages** (par plage / service), puis les **clés par hash**. Pas de « scan des MsgCle en premier → hashes déchiffrables via ECDH → fetch messages par hash » comme dans les specs.
|
||||||
|
- **DH systématique ?** Non. Le sync suppose du base64 et n’applique pas un schéma DH systématique.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Ce que prévoient les specs
|
||||||
|
|
||||||
|
- **Message individuel de déchiffrement** : hash + clé de chiffrement + **df** = « diffie-hellman à scanner » pour obtenir la clé de déchiffrement (secret partagé).
|
||||||
|
- **Flux** : « Récupérer et scanner tous les messages de clés » (depuis date anniversaire / checkpoint) ; les messages déchiffrables sont identifiés **via les DH** ; puis on va **chercher le message par hash** et on déchiffre.
|
||||||
|
- **Publish to all** : tout est chiffré ; seuls les pairs qui peuvent dériver la clé (via ECDH) peuvent lire. Le « matériel DH » est publiable car inexploitable par les tiers.
|
||||||
|
|
||||||
|
Donc, pour les types de messages **hors DH** : chiffrement + MsgCle avec **df_ecdh_scannable** systématique, et flux **scan des clés → fetch message par hash → déchiffrer**.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Écarts principaux
|
||||||
|
|
||||||
|
| Aspect | Specs | Actuel |
|
||||||
|
|--------|--------|--------|
|
||||||
|
| DH pour tous les messages (hors DH) | Oui, via MsgCle + df | Non : login sans MsgCle ; pairing optionnel ; sync en base64 |
|
||||||
|
| Flux | Scan MsgCle → hashes déchiffrables → fetch message par hash | Fetch messages → fetch keys par hash ; pas de scan MsgCle centré ECDH |
|
||||||
|
| Utilisation des clés en sync | Déchiffrement ECDH avec df | Base64 si `keys.length > 0` ; clés non utilisées |
|
||||||
|
| Login | Sous-entendu publish to all + MsgCle/DH si d’autres doivent déchiffrer | `encryptForAll` seul, pas de MsgCle |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Pistes pour alignement
|
||||||
|
|
||||||
|
1. **Login** : si le message login sur le relais doit être déchiffrable (ex. par le service ou un autre client) → publier des `MsgCle` avec **df_ecdh_scannable** pour les destinataires concernés (p.ex. clé publique du service ou des pairs), au lieu de se limiter à `encryptForAll` sans MsgCle.
|
||||||
|
2. **Pairing** : rendre DH **obligatoire** dès qu’un pair distant existe (exiger `publicKey`), et ne plus envoyer de membre finaliser en clair (base64).
|
||||||
|
3. **Sync** :
|
||||||
|
- Implémenter un **scan des MsgCle** (depuis anniversaire / checkpoint), identification des hash déchiffrables via ECDH (quand on a la clé privée correspondante).
|
||||||
|
- Ensuite **fetch des messages par hash** pour ces hash uniquement.
|
||||||
|
- Remplacer le « base64 si keys > 0 » par un **vrai déchiffrement ECDH** utilisant `df_ecdh_scannable` et les clés.
|
||||||
|
4. **Types contrats / graphe** (service, contrat, champ, action, membre, pair) : définir quels types sont publiés par qui, avec quels MsgCle / DH, et appliquer le même flux scan → fetch par hash → déchiffrer pour tous.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Références
|
||||||
|
|
||||||
|
- `userwallet/docs/specs.md` : Message individuel de déchiffrement, df DH à scanner, « récupérer et scanner tous les messages de clés », fenêtre de scan, fetch par hash.
|
||||||
|
- `userwallet/features/userwallet-pairing-connecte.md` : chiffrement pairing ECDH vs base64, MsgCle.
|
||||||
|
- `userwallet/src/utils/encryption.ts` : `encryptWithECDH` / `decryptWithECDH`, `encryptForAll` / `decryptForAll`.
|
||||||
|
- `userwallet/src/services/pairingConfirm.ts` : publication pairing + MsgCle, `fetchPairingMessage`, ECDH.
|
||||||
|
- `userwallet/src/services/syncService.ts` : `tryDecrypt` (base64), `fetchKeys`.
|
||||||
|
- `userwallet/src/services/loginBuilder.ts` : `encryptForAll`, pas de MsgCle.
|
||||||
@ -14,6 +14,7 @@ import { RelaySettingsScreen } from './components/RelaySettingsScreen';
|
|||||||
import { PairManagementScreen } from './components/PairManagementScreen';
|
import { PairManagementScreen } from './components/PairManagementScreen';
|
||||||
import { SyncScreen } from './components/SyncScreen';
|
import { SyncScreen } from './components/SyncScreen';
|
||||||
import { LoginScreen } from './components/LoginScreen';
|
import { LoginScreen } from './components/LoginScreen';
|
||||||
|
import { LoginSignScreen } from './components/LoginSignScreen';
|
||||||
import { ServiceListScreen } from './components/ServiceListScreen';
|
import { ServiceListScreen } from './components/ServiceListScreen';
|
||||||
import { DataExportImportScreen } from './components/DataExportImportScreen';
|
import { DataExportImportScreen } from './components/DataExportImportScreen';
|
||||||
import { UnlockScreen } from './components/UnlockScreen';
|
import { UnlockScreen } from './components/UnlockScreen';
|
||||||
@ -37,6 +38,7 @@ function AppContent(): JSX.Element {
|
|||||||
<Route path="/create-identity" element={<CreateIdentityScreen />} />
|
<Route path="/create-identity" element={<CreateIdentityScreen />} />
|
||||||
<Route path="/import-identity" element={<ImportIdentityScreen />} />
|
<Route path="/import-identity" element={<ImportIdentityScreen />} />
|
||||||
<Route path="/login" element={<LoginScreen />} />
|
<Route path="/login" element={<LoginScreen />} />
|
||||||
|
<Route path="/login-sign" element={<LoginSignScreen />} />
|
||||||
<Route path="/manage-pairs" element={<PairManagementScreen />} />
|
<Route path="/manage-pairs" element={<PairManagementScreen />} />
|
||||||
<Route path="/relay-settings" element={<RelaySettingsScreen />} />
|
<Route path="/relay-settings" element={<RelaySettingsScreen />} />
|
||||||
<Route path="/sync" element={<SyncScreen />} />
|
<Route path="/sync" element={<SyncScreen />} />
|
||||||
|
|||||||
50
userwallet/src/components/LoginCollectShare.tsx
Normal file
50
userwallet/src/components/LoginCollectShare.tsx
Normal file
@ -0,0 +1,50 @@
|
|||||||
|
import { useEffect, useState } from 'react';
|
||||||
|
import QRCode from 'qrcode';
|
||||||
|
import type { LoginProof } from '../types/identity';
|
||||||
|
|
||||||
|
const QR_SIZE = 200;
|
||||||
|
|
||||||
|
function buildLoginSignUrl(hash: string, nonce: string): string {
|
||||||
|
const params = new URLSearchParams({ hash, nonce });
|
||||||
|
return `${window.location.origin}/login-sign?${params.toString()}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface LoginCollectShareProps {
|
||||||
|
proof: LoginProof;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Device 1: show link + QR for "Demander signature sur l'autre appareil" during collect.
|
||||||
|
*/
|
||||||
|
export function LoginCollectShare({ proof }: LoginCollectShareProps): JSX.Element {
|
||||||
|
const [qrDataUrl, setQrDataUrl] = useState<string | null>(null);
|
||||||
|
const url = buildLoginSignUrl(proof.challenge.hash, proof.challenge.nonce);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
QRCode.toDataURL(url, { width: QR_SIZE })
|
||||||
|
.then(setQrDataUrl)
|
||||||
|
.catch((err: unknown) => {
|
||||||
|
console.error('QR login-sign failed:', err);
|
||||||
|
});
|
||||||
|
}, [url]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<section aria-label={`Demander signature sur l${"'"}autre appareil`}>
|
||||||
|
<h3>{`Demander signature sur l${"'"}autre appareil`}</h3>
|
||||||
|
<p>Ouvrez ce lien ou scannez le QR sur le 2ᵉ appareil.</p>
|
||||||
|
<p>
|
||||||
|
<a href={url} target="_blank" rel="noopener noreferrer">
|
||||||
|
{url}
|
||||||
|
</a>
|
||||||
|
</p>
|
||||||
|
{qrDataUrl !== null && (
|
||||||
|
<img
|
||||||
|
src={qrDataUrl}
|
||||||
|
alt="QR code : lien pour signer le login sur le 2e appareil"
|
||||||
|
width={QR_SIZE}
|
||||||
|
height={QR_SIZE}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</section>
|
||||||
|
);
|
||||||
|
}
|
||||||
@ -4,18 +4,33 @@ import { useIdentity } from '../hooks/useIdentity';
|
|||||||
import { useErrorHandler } from '../hooks/useErrorHandler';
|
import { useErrorHandler } from '../hooks/useErrorHandler';
|
||||||
import { useLoginStateMachine } from '../hooks/useLoginStateMachine';
|
import { useLoginStateMachine } from '../hooks/useLoginStateMachine';
|
||||||
import { ErrorDisplay } from './ErrorDisplay';
|
import { ErrorDisplay } from './ErrorDisplay';
|
||||||
|
import { LoginCollectShare } from './LoginCollectShare';
|
||||||
import { getStoredRelays } from '../utils/relay';
|
import { getStoredRelays } from '../utils/relay';
|
||||||
import { GraphResolver } from '../services/graphResolver';
|
import { GraphResolver } from '../services/graphResolver';
|
||||||
import { LoginBuilder } from '../services/loginBuilder';
|
import { LoginBuilder } from '../services/loginBuilder';
|
||||||
import { postMessageChiffre, postSignature } from '../utils/relay';
|
|
||||||
import { useChannel } from '../hooks/useChannel';
|
import { useChannel } from '../hooks/useChannel';
|
||||||
|
import { isPairingSatisfied, getPairsForMember } from '../utils/pairing';
|
||||||
|
import {
|
||||||
|
buildAllowedPubkeys,
|
||||||
|
checkDependenciesSatisfied,
|
||||||
|
hasRemoteSignatures,
|
||||||
|
} from '../utils/loginValidation';
|
||||||
|
import { publishMessageAndSigs } from '../utils/loginPublish';
|
||||||
|
import {
|
||||||
|
buildPairToMembers,
|
||||||
|
buildPubkeyToPair,
|
||||||
|
runCollectLoop,
|
||||||
|
COLLECT_POLL_MS,
|
||||||
|
COLLECT_TIMEOUT_MS,
|
||||||
|
type ProofSignature,
|
||||||
|
} from '../utils/collectSignatures';
|
||||||
import {
|
import {
|
||||||
verifyTimestamp,
|
verifyTimestamp,
|
||||||
verifyMessageSignaturesStrict,
|
verifyMessageSignaturesStrict,
|
||||||
} from '../utils/verification';
|
} from '../utils/verification';
|
||||||
import * as nonceStore from '../utils/nonceStore';
|
import * as nonceStore from '../utils/nonceStore';
|
||||||
import type { LoginPath, LoginProof } from '../types/identity';
|
import type { LoginPath, LoginProof } from '../types/identity';
|
||||||
import type { MessageBase, MsgSignature } from '../types/message';
|
import type { MessageBase } from '../types/message';
|
||||||
|
|
||||||
export function LoginScreen(): JSX.Element {
|
export function LoginScreen(): JSX.Element {
|
||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
@ -30,6 +45,15 @@ export function LoginScreen(): JSX.Element {
|
|||||||
const [proof, setProof] = useState<LoginProof | null>(null);
|
const [proof, setProof] = useState<LoginProof | null>(null);
|
||||||
const [isBuilding, setIsBuilding] = useState(false);
|
const [isBuilding, setIsBuilding] = useState(false);
|
||||||
const [isPublishing, setIsPublishing] = useState(false);
|
const [isPublishing, setIsPublishing] = useState(false);
|
||||||
|
const [isCollecting, setIsCollecting] = useState(false);
|
||||||
|
const [awaitingRemoteAccept, setAwaitingRemoteAccept] = useState(false);
|
||||||
|
const [collectedMerged, setCollectedMerged] = useState<ProofSignature[] | null>(
|
||||||
|
null,
|
||||||
|
);
|
||||||
|
const [collectedPublishStats, setCollectedPublishStats] = useState<{
|
||||||
|
successCount: number;
|
||||||
|
relaysCount: number;
|
||||||
|
} | null>(null);
|
||||||
|
|
||||||
const graphResolver = new GraphResolver();
|
const graphResolver = new GraphResolver();
|
||||||
|
|
||||||
@ -102,6 +126,18 @@ export function LoginScreen(): JSX.Element {
|
|||||||
loginPath.membre_uuid,
|
loginPath.membre_uuid,
|
||||||
);
|
);
|
||||||
|
|
||||||
|
const localPairs = getPairsForMember(loginPath.membre_uuid).filter(
|
||||||
|
(p) =>
|
||||||
|
p.is_local &&
|
||||||
|
loginPath.pairs_attendus.includes(p.uuid),
|
||||||
|
);
|
||||||
|
|
||||||
|
const pk = identity.privateKey;
|
||||||
|
if (pk === undefined) {
|
||||||
|
handleError('Clé privée indisponible. Déverrouillez l\'identité.', 'IDENTITY_LOCKED');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
const signatures: Array<{
|
const signatures: Array<{
|
||||||
signature: string;
|
signature: string;
|
||||||
cle_publique: string;
|
cle_publique: string;
|
||||||
@ -109,31 +145,21 @@ export function LoginScreen(): JSX.Element {
|
|||||||
pair_uuid: string;
|
pair_uuid: string;
|
||||||
}> = [];
|
}> = [];
|
||||||
|
|
||||||
const pk = identity.privateKey;
|
for (const pair of localPairs) {
|
||||||
if (pk === undefined) {
|
const sig = loginBuilder.signChallenge(challenge, pair.uuid, pk);
|
||||||
handleError('Clé privée indisponible. Déverrouillez l\'identité.', 'IDENTITY_LOCKED');
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
for (const req of loginPath.signatures_requises) {
|
|
||||||
if (req.pair_uuid === undefined) {
|
|
||||||
handleError('Pair UUID manquant pour une signature requise', 'MISSING_PAIR_UUID');
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
const sig = loginBuilder.signChallenge(
|
|
||||||
challenge,
|
|
||||||
req.pair_uuid,
|
|
||||||
pk,
|
|
||||||
);
|
|
||||||
signatures.push({
|
signatures.push({
|
||||||
signature: sig,
|
signature: sig,
|
||||||
cle_publique: identity.publicKey,
|
cle_publique: identity.publicKey,
|
||||||
nonce: challenge.nonce,
|
nonce: challenge.nonce,
|
||||||
pair_uuid: req.pair_uuid,
|
pair_uuid: pair.uuid,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
if (signatures.length === 0) {
|
if (signatures.length === 0) {
|
||||||
handleError('Aucune signature générée', 'NO_SIGNATURES');
|
handleError(
|
||||||
|
'Aucun pair local pour ce membre. Signez sur ce device ou ajoutez un pair.',
|
||||||
|
'NO_LOCAL_PAIRS',
|
||||||
|
);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -168,18 +194,81 @@ export function LoginScreen(): JSX.Element {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
setIsPublishing(true);
|
||||||
|
clearError();
|
||||||
|
try {
|
||||||
|
const relays = getStoredRelays().filter((r) => r.enabled);
|
||||||
|
if (relays.length === 0) {
|
||||||
|
handleError('Aucun relais activé', 'NO_ENABLED_RELAYS');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const loginBuilder = new LoginBuilder(identity, relays.map((r) => r.endpoint));
|
||||||
|
const msgChiffre = loginBuilder.challengeToMsgChiffre(proof.challenge);
|
||||||
|
|
||||||
|
const successCount = await publishMessageAndSigs(
|
||||||
|
relays,
|
||||||
|
msgChiffre,
|
||||||
|
proof.signatures,
|
||||||
|
);
|
||||||
|
if (successCount === 0) {
|
||||||
|
handleError('Échec de la publication sur tous les relais', 'PUBLISH_FAILED');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
let merged = proof.signatures;
|
||||||
if (loginPath !== null) {
|
if (loginPath !== null) {
|
||||||
const allowedPubkeys = new Set<string>();
|
setIsCollecting(true);
|
||||||
for (const req of loginPath.signatures_requises) {
|
try {
|
||||||
if (req.cle_publique !== undefined) {
|
const pairToMembers = buildPairToMembers(loginPath.pairs_attendus);
|
||||||
allowedPubkeys.add(req.cle_publique);
|
const pubkeyToPair = buildPubkeyToPair(
|
||||||
|
identity.publicKey,
|
||||||
|
loginPath.pairs_attendus,
|
||||||
|
);
|
||||||
|
const endpoints = relays.map((r) => r.endpoint);
|
||||||
|
merged = await runCollectLoop(
|
||||||
|
endpoints,
|
||||||
|
proof.challenge.hash,
|
||||||
|
proof.signatures,
|
||||||
|
loginPath,
|
||||||
|
pairToMembers,
|
||||||
|
pubkeyToPair,
|
||||||
|
{ pollMs: COLLECT_POLL_MS, timeoutMs: COLLECT_TIMEOUT_MS },
|
||||||
|
);
|
||||||
|
} finally {
|
||||||
|
setIsCollecting(false);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (
|
||||||
|
loginPath !== null &&
|
||||||
|
hasRemoteSignatures(merged, loginPath.pairs_attendus)
|
||||||
|
) {
|
||||||
|
setCollectedMerged(merged);
|
||||||
|
setCollectedPublishStats({
|
||||||
|
successCount,
|
||||||
|
relaysCount: relays.length,
|
||||||
|
});
|
||||||
|
setAwaitingRemoteAccept(true);
|
||||||
|
setIsPublishing(false);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const finalProof = await loginBuilder.buildProof(proof.challenge, merged);
|
||||||
|
if (loginPath !== null) {
|
||||||
|
if (!checkDependenciesSatisfied(loginPath, finalProof)) {
|
||||||
|
handleError(
|
||||||
|
'Dépendances entre signatures non satisfaites (membres requis manquants)',
|
||||||
|
'DEPENDENCIES_UNSATISFIED',
|
||||||
|
);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const allowedPubkeys = buildAllowedPubkeys(loginPath, identity.publicKey);
|
||||||
if (allowedPubkeys.size > 0) {
|
if (allowedPubkeys.size > 0) {
|
||||||
const minimalMsg = {
|
const minimalMsg = {
|
||||||
hash: { hash_value: proof.challenge.hash },
|
hash: { hash_value: proof.challenge.hash },
|
||||||
} as unknown as MessageBase;
|
} as unknown as MessageBase;
|
||||||
const sigs = proof.signatures.map((s) => ({
|
const sigs = merged.map((s) => ({
|
||||||
hash: proof.challenge.hash,
|
hash: proof.challenge.hash,
|
||||||
cle_publique: s.cle_publique,
|
cle_publique: s.cle_publique,
|
||||||
signature: s.signature,
|
signature: s.signature,
|
||||||
@ -200,53 +289,9 @@ export function LoginScreen(): JSX.Element {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
setIsPublishing(true);
|
|
||||||
clearError();
|
|
||||||
try {
|
|
||||||
const relays = getStoredRelays().filter((r) => r.enabled);
|
|
||||||
if (relays.length === 0) {
|
|
||||||
handleError('Aucun relais activé', 'NO_ENABLED_RELAYS');
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const loginBuilder = new LoginBuilder(identity, relays.map((r) => r.endpoint));
|
|
||||||
const msgChiffre = loginBuilder.challengeToMsgChiffre(proof.challenge);
|
|
||||||
|
|
||||||
const publishResults: Array<{ relay: string; success: boolean }> = [];
|
|
||||||
|
|
||||||
for (const relay of relays) {
|
|
||||||
try {
|
|
||||||
await postMessageChiffre(relay.endpoint, msgChiffre);
|
|
||||||
|
|
||||||
for (const sig of proof.signatures) {
|
|
||||||
const msgSig: MsgSignature = {
|
|
||||||
signature: {
|
|
||||||
hash: proof.challenge.hash,
|
|
||||||
cle_publique: sig.cle_publique,
|
|
||||||
signature: sig.signature,
|
|
||||||
nonce: sig.nonce,
|
|
||||||
},
|
|
||||||
hash_cible: proof.challenge.hash,
|
|
||||||
};
|
|
||||||
await postSignature(relay.endpoint, msgSig);
|
|
||||||
}
|
|
||||||
|
|
||||||
publishResults.push({ relay: relay.endpoint, success: true });
|
|
||||||
} catch (err) {
|
|
||||||
handleError(err, `Erreur lors de la publication sur ${relay.endpoint}`);
|
|
||||||
publishResults.push({ relay: relay.endpoint, success: false });
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const successCount = publishResults.filter((r) => r.success).length;
|
|
||||||
if (successCount === 0) {
|
|
||||||
handleError('Échec de la publication sur tous les relais', 'PUBLISH_FAILED');
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
await nonceStore.markUsed(proof.challenge.nonce, proof.challenge.timestamp);
|
await nonceStore.markUsed(proof.challenge.nonce, proof.challenge.timestamp);
|
||||||
|
|
||||||
const updatedProof = { ...proof, statut: 'publie' as const };
|
const updatedProof = { ...finalProof, statut: 'publie' as const };
|
||||||
setProof(updatedProof);
|
setProof(updatedProof);
|
||||||
sendLoginProof(updatedProof);
|
sendLoginProof(updatedProof);
|
||||||
|
|
||||||
@ -271,7 +316,29 @@ export function LoginScreen(): JSX.Element {
|
|||||||
return (
|
return (
|
||||||
<main>
|
<main>
|
||||||
<p>Identité requise pour se connecter</p>
|
<p>Identité requise pour se connecter</p>
|
||||||
<button onClick={() => navigate('/')}>Retour</button>
|
<button type="button" onClick={(): void => navigate('/')}>
|
||||||
|
Retour
|
||||||
|
</button>
|
||||||
|
</main>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!isPairingSatisfied()) {
|
||||||
|
return (
|
||||||
|
<main>
|
||||||
|
<h1>Se connecter</h1>
|
||||||
|
<p>Pairing obligatoire (G_PAIRING_SATISFIED). Configurez au moins un pair.</p>
|
||||||
|
<div>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onClick={(): void => navigate('/manage-pairs')}
|
||||||
|
>
|
||||||
|
Configurer le pairing
|
||||||
|
</button>
|
||||||
|
<button type="button" onClick={(): void => navigate('/')}>
|
||||||
|
Retour
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
</main>
|
</main>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@ -281,6 +348,102 @@ export function LoginScreen(): JSX.Element {
|
|||||||
navigate('/');
|
navigate('/');
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const handleSyncNow = (): void => {
|
||||||
|
dispatch({ type: 'E_SYNC_NOW' });
|
||||||
|
navigate('/sync');
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleAddPair = (): void => {
|
||||||
|
dispatch({ type: 'E_ADD_PAIR' });
|
||||||
|
navigate('/manage-pairs');
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleRefuseRemote = (): void => {
|
||||||
|
dispatch({ type: 'E_LOCAL_VERDICT_REJECT' });
|
||||||
|
setAwaitingRemoteAccept(false);
|
||||||
|
setCollectedMerged(null);
|
||||||
|
setCollectedPublishStats(null);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleAcceptRemote = async (): Promise<void> => {
|
||||||
|
if (
|
||||||
|
identity === null ||
|
||||||
|
proof === null ||
|
||||||
|
loginPath === null ||
|
||||||
|
collectedMerged === null ||
|
||||||
|
collectedPublishStats === null
|
||||||
|
) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
clearError();
|
||||||
|
const relays = getStoredRelays().filter((r) => r.enabled);
|
||||||
|
const loginBuilder = new LoginBuilder(
|
||||||
|
identity,
|
||||||
|
relays.map((r) => r.endpoint),
|
||||||
|
);
|
||||||
|
const finalProof = await loginBuilder.buildProof(
|
||||||
|
proof.challenge,
|
||||||
|
collectedMerged,
|
||||||
|
);
|
||||||
|
if (!checkDependenciesSatisfied(loginPath, finalProof)) {
|
||||||
|
handleError(
|
||||||
|
'Dépendances entre signatures non satisfaites (membres requis manquants)',
|
||||||
|
'DEPENDENCIES_UNSATISFIED',
|
||||||
|
);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const allowedPubkeys = buildAllowedPubkeys(loginPath, identity.publicKey);
|
||||||
|
if (allowedPubkeys.size > 0) {
|
||||||
|
const minimalMsg = {
|
||||||
|
hash: { hash_value: proof.challenge.hash },
|
||||||
|
} as unknown as MessageBase;
|
||||||
|
const sigs = collectedMerged.map((s) => ({
|
||||||
|
hash: proof.challenge.hash,
|
||||||
|
cle_publique: s.cle_publique,
|
||||||
|
signature: s.signature,
|
||||||
|
nonce: s.nonce,
|
||||||
|
}));
|
||||||
|
const { valid, unauthorized } = verifyMessageSignaturesStrict(
|
||||||
|
minimalMsg,
|
||||||
|
sigs,
|
||||||
|
allowedPubkeys,
|
||||||
|
);
|
||||||
|
if (unauthorized.length > 0 || valid.length === 0) {
|
||||||
|
handleError(
|
||||||
|
'Signature(s) avec clé non autorisée par les validateurs (X_PUBKEY_NOT_AUTHORIZED)',
|
||||||
|
'X_PUBKEY_NOT_AUTHORIZED',
|
||||||
|
);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
await nonceStore.markUsed(
|
||||||
|
proof.challenge.nonce,
|
||||||
|
proof.challenge.timestamp,
|
||||||
|
);
|
||||||
|
const updatedProof = { ...finalProof, statut: 'publie' as const };
|
||||||
|
setProof(updatedProof);
|
||||||
|
sendLoginProof(updatedProof);
|
||||||
|
if (
|
||||||
|
collectedPublishStats.successCount < collectedPublishStats.relaysCount
|
||||||
|
) {
|
||||||
|
handleError(
|
||||||
|
`Publication partielle: ${collectedPublishStats.successCount}/${collectedPublishStats.relaysCount} relais`,
|
||||||
|
'PARTIAL_PUBLISH',
|
||||||
|
);
|
||||||
|
dispatch({ type: 'E_PUBLISH_LOGIN_PARTIAL' });
|
||||||
|
} else {
|
||||||
|
dispatch({ type: 'E_PUBLISH_LOGIN_OK' });
|
||||||
|
dispatch({ type: 'E_LOCAL_VERDICT_ACCEPT' });
|
||||||
|
}
|
||||||
|
setAwaitingRemoteAccept(false);
|
||||||
|
setCollectedMerged(null);
|
||||||
|
setCollectedPublishStats(null);
|
||||||
|
};
|
||||||
|
|
||||||
|
const showRecoveryActions =
|
||||||
|
(loginPath !== null && loginPath.statut === 'incomplet') ||
|
||||||
|
loginState === 'S_ERROR_RECOVERABLE';
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<main>
|
<main>
|
||||||
<h1>Se connecter</h1>
|
<h1>Se connecter</h1>
|
||||||
@ -318,7 +481,11 @@ export function LoginScreen(): JSX.Element {
|
|||||||
/>
|
/>
|
||||||
</label>
|
</label>
|
||||||
</div>
|
</div>
|
||||||
<button onClick={handleBuildPath} disabled={isBuilding}>
|
<button
|
||||||
|
type="button"
|
||||||
|
onClick={handleBuildPath}
|
||||||
|
disabled={isBuilding}
|
||||||
|
>
|
||||||
{isBuilding ? 'Construction...' : 'Construire le chemin'}
|
{isBuilding ? 'Construction...' : 'Construire le chemin'}
|
||||||
</button>
|
</button>
|
||||||
</section>
|
</section>
|
||||||
@ -350,11 +517,35 @@ export function LoginScreen(): JSX.Element {
|
|||||||
<strong>Signatures requises:</strong> {loginPath.signatures_requises.length}
|
<strong>Signatures requises:</strong> {loginPath.signatures_requises.length}
|
||||||
</p>
|
</p>
|
||||||
{loginPath.statut === 'complet' && (
|
{loginPath.statut === 'complet' && (
|
||||||
<button onClick={handleBuildChallenge} disabled={isBuilding}>
|
<button
|
||||||
|
type="button"
|
||||||
|
onClick={handleBuildChallenge}
|
||||||
|
disabled={isBuilding}
|
||||||
|
>
|
||||||
{isBuilding ? 'Construction...' : 'Construire le challenge'}
|
{isBuilding ? 'Construction...' : 'Construire le challenge'}
|
||||||
</button>
|
</button>
|
||||||
)}
|
)}
|
||||||
|
{loginPath.statut === 'incomplet' && showRecoveryActions && (
|
||||||
|
<div>
|
||||||
|
<button type="button" onClick={handleSyncNow}>
|
||||||
|
Synchroniser
|
||||||
|
</button>
|
||||||
|
<button type="button" onClick={handleAddPair}>
|
||||||
|
Ajouter un pair
|
||||||
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
)}
|
||||||
|
{showRecoveryActions && loginPath === null && (
|
||||||
|
<section aria-label="Reprise">
|
||||||
|
<button type="button" onClick={handleSyncNow}>
|
||||||
|
Synchroniser
|
||||||
|
</button>
|
||||||
|
<button type="button" onClick={handleAddPair}>
|
||||||
|
Ajouter un pair
|
||||||
|
</button>
|
||||||
</section>
|
</section>
|
||||||
)}
|
)}
|
||||||
{proof !== null && (
|
{proof !== null && (
|
||||||
@ -370,16 +561,66 @@ export function LoginScreen(): JSX.Element {
|
|||||||
<p>
|
<p>
|
||||||
<strong>Signatures:</strong> {proof.signatures.length}
|
<strong>Signatures:</strong> {proof.signatures.length}
|
||||||
</p>
|
</p>
|
||||||
{proof.statut === 'en_attente' && (
|
{proof.statut === 'en_attente' && !awaitingRemoteAccept && (
|
||||||
<button onClick={handlePublish} disabled={isPublishing}>
|
<button
|
||||||
{isPublishing ? 'Publication...' : 'Publier la preuve'}
|
type="button"
|
||||||
|
onClick={handlePublish}
|
||||||
|
disabled={isPublishing}
|
||||||
|
>
|
||||||
|
{isCollecting
|
||||||
|
? 'En attente des signatures des autres appareils…'
|
||||||
|
: isPublishing
|
||||||
|
? 'Publication…'
|
||||||
|
: 'Publier la preuve'}
|
||||||
</button>
|
</button>
|
||||||
)}
|
)}
|
||||||
|
{isCollecting && proof !== null && (
|
||||||
|
<LoginCollectShare proof={proof} />
|
||||||
|
)}
|
||||||
|
{awaitingRemoteAccept &&
|
||||||
|
collectedMerged !== null &&
|
||||||
|
proof !== null &&
|
||||||
|
loginPath !== null && (
|
||||||
|
<section
|
||||||
|
aria-labelledby="confirm-remote-sigs"
|
||||||
|
style={{ marginTop: '1rem' }}
|
||||||
|
>
|
||||||
|
<h3 id="confirm-remote-sigs">
|
||||||
|
Confirmer les signatures du 2ᵉ appareil
|
||||||
|
</h3>
|
||||||
|
<p>
|
||||||
|
Des signatures ont été reçues du 2ᵉ appareil. Les mots ont
|
||||||
|
pu être visibles à l'écran et interceptés par une
|
||||||
|
tierce personne. Confirmer que c'est bien vous qui avez
|
||||||
|
validé sur l'autre appareil ?
|
||||||
|
</p>
|
||||||
|
<div>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onClick={(): void => void handleAcceptRemote()}
|
||||||
|
>
|
||||||
|
Accepter
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onClick={handleRefuseRemote}
|
||||||
|
>
|
||||||
|
Refuser
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
)}
|
)}
|
||||||
<div>
|
<div>
|
||||||
<button onClick={handleBack}>Retour</button>
|
<button
|
||||||
|
type="button"
|
||||||
|
onClick={handleBack}
|
||||||
|
disabled={awaitingRemoteAccept}
|
||||||
|
>
|
||||||
|
Retour
|
||||||
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</main>
|
</main>
|
||||||
);
|
);
|
||||||
|
|||||||
73
userwallet/src/components/LoginSignScreen.tsx
Normal file
73
userwallet/src/components/LoginSignScreen.tsx
Normal file
@ -0,0 +1,73 @@
|
|||||||
|
import { useNavigate, useSearchParams } from 'react-router-dom';
|
||||||
|
import { useIdentity } from '../hooks/useIdentity';
|
||||||
|
import { useSignAndPostLogin } from '../hooks/useSignAndPostLogin';
|
||||||
|
|
||||||
|
function LoginSignForm(props: {
|
||||||
|
status: 'idle' | 'signing' | 'done' | 'error';
|
||||||
|
error: string | null;
|
||||||
|
onBack: () => void;
|
||||||
|
}): JSX.Element {
|
||||||
|
const { status, error, onBack } = props;
|
||||||
|
return (
|
||||||
|
<main>
|
||||||
|
<h1>Signer le login</h1>
|
||||||
|
{error !== null && <p role="alert">{error}</p>}
|
||||||
|
{status === 'signing' && <p>Signature et publication en cours…</p>}
|
||||||
|
{status === 'done' && (
|
||||||
|
<p>Signature publiée. Retournez sur le 1ᵉʳ appareil.</p>
|
||||||
|
)}
|
||||||
|
{(status === 'done' || status === 'error') && (
|
||||||
|
<button type="button" onClick={onBack}>Retour</button>
|
||||||
|
)}
|
||||||
|
</main>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function LoginSignError(props: {
|
||||||
|
title: string;
|
||||||
|
message: string;
|
||||||
|
onBack: () => void;
|
||||||
|
}): JSX.Element {
|
||||||
|
const { title, message, onBack } = props;
|
||||||
|
return (
|
||||||
|
<main>
|
||||||
|
<h1>{title}</h1>
|
||||||
|
<p>{message}</p>
|
||||||
|
<button type="button" onClick={onBack}>Retour</button>
|
||||||
|
</main>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Device 2: sign login challenge (hash, nonce) and post to relays.
|
||||||
|
* Route: /login-sign?hash=...&nonce=...
|
||||||
|
*/
|
||||||
|
export function LoginSignScreen(): JSX.Element {
|
||||||
|
const navigate = useNavigate();
|
||||||
|
const [searchParams] = useSearchParams();
|
||||||
|
const { identity } = useIdentity();
|
||||||
|
const hash = searchParams.get('hash') ?? '';
|
||||||
|
const nonce = searchParams.get('nonce') ?? '';
|
||||||
|
const { status, error } = useSignAndPostLogin(hash, nonce, identity);
|
||||||
|
const goBack = (): void => navigate('/');
|
||||||
|
|
||||||
|
if (hash === '' || nonce === '') {
|
||||||
|
return (
|
||||||
|
<LoginSignError
|
||||||
|
title="Signer le login"
|
||||||
|
message="Paramètres manquants : hash et nonce requis (URL ou QR)."
|
||||||
|
onBack={goBack}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
if (identity === null) {
|
||||||
|
return (
|
||||||
|
<LoginSignError
|
||||||
|
title="Signer le login"
|
||||||
|
message="Identité requise."
|
||||||
|
onBack={goBack}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return <LoginSignForm status={status} error={error} onBack={goBack} />;
|
||||||
|
}
|
||||||
41
userwallet/src/hooks/useSignAndPostLogin.ts
Normal file
41
userwallet/src/hooks/useSignAndPostLogin.ts
Normal file
@ -0,0 +1,41 @@
|
|||||||
|
import { useState, useEffect } from 'react';
|
||||||
|
import { signAndPostLoginChallenge } from '../utils/loginSign';
|
||||||
|
import type { LocalIdentity } from '../types/identity';
|
||||||
|
|
||||||
|
type Status = 'idle' | 'signing' | 'done' | 'error';
|
||||||
|
|
||||||
|
export function useSignAndPostLogin(
|
||||||
|
hash: string,
|
||||||
|
nonce: string,
|
||||||
|
identity: LocalIdentity | null,
|
||||||
|
): { status: Status; error: string | null } {
|
||||||
|
const [status, setStatus] = useState<Status>('idle');
|
||||||
|
const [error, setError] = useState<string | null>(null);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (hash === '' || nonce === '' || identity === null || status !== 'idle') {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const pk = identity.privateKey;
|
||||||
|
if (pk === undefined) {
|
||||||
|
setError(`Déverrouillez l${"'"}identité pour signer.`);
|
||||||
|
setStatus('error');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
setStatus('signing');
|
||||||
|
signAndPostLoginChallenge(hash, nonce, pk, identity.publicKey)
|
||||||
|
.then((ok) => {
|
||||||
|
setStatus(ok === 0 ? 'error' : 'done');
|
||||||
|
if (ok === 0) {
|
||||||
|
setError('Échec publication sur tous les relais.');
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.catch((err) => {
|
||||||
|
console.error('signAndPostLoginChallenge failed:', err);
|
||||||
|
setError('Erreur lors de la signature.');
|
||||||
|
setStatus('error');
|
||||||
|
});
|
||||||
|
}, [hash, nonce, identity, status]);
|
||||||
|
|
||||||
|
return { status, error };
|
||||||
|
}
|
||||||
@ -242,6 +242,7 @@ export class GraphResolver {
|
|||||||
pair_uuid: undefined,
|
pair_uuid: undefined,
|
||||||
cle_publique: sigReq.cle_publique,
|
cle_publique: sigReq.cle_publique,
|
||||||
cardinalite_minimale: sigReq.cardinalite_minimale,
|
cardinalite_minimale: sigReq.cardinalite_minimale,
|
||||||
|
dependances: sigReq.dependances,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -72,13 +72,14 @@ export class LoginBuilder {
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* Sign a challenge with a pair's private key.
|
* Sign a challenge with a pair's private key.
|
||||||
|
* Signs hash-nonce only (verification and relay fetch use same format; pair_uuid is metadata).
|
||||||
*/
|
*/
|
||||||
signChallenge(
|
signChallenge(
|
||||||
challenge: LoginChallenge,
|
challenge: LoginChallenge,
|
||||||
pairUuid: string,
|
_pairUuid: string,
|
||||||
pairPrivateKey: string,
|
pairPrivateKey: string,
|
||||||
): string {
|
): string {
|
||||||
const messageToSign = `${challenge.hash}-${challenge.nonce}-${pairUuid}`;
|
const messageToSign = `${challenge.hash}-${challenge.nonce}`;
|
||||||
return signMessage(messageToSign, pairPrivateKey);
|
return signMessage(messageToSign, pairPrivateKey);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -137,6 +137,9 @@ export function transition(
|
|||||||
errorCode: 'E_PUBLISH_LOGIN_PARTIAL',
|
errorCode: 'E_PUBLISH_LOGIN_PARTIAL',
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
if (event.type === 'E_LOCAL_VERDICT_REJECT') {
|
||||||
|
return { nextState: 'S_LOGIN_FAILURE' };
|
||||||
|
}
|
||||||
if (event.type === 'E_BACK') {
|
if (event.type === 'E_BACK') {
|
||||||
return { nextState: 'S_LOGIN_COLLECT_SIGNATURES' };
|
return { nextState: 'S_LOGIN_COLLECT_SIGNATURES' };
|
||||||
}
|
}
|
||||||
@ -165,6 +168,9 @@ export function transition(
|
|||||||
if (event.type === 'E_SYNC_NOW') {
|
if (event.type === 'E_SYNC_NOW') {
|
||||||
return { nextState: 'S_LOGIN_BUILD_PATH' };
|
return { nextState: 'S_LOGIN_BUILD_PATH' };
|
||||||
}
|
}
|
||||||
|
if (event.type === 'E_ADD_PAIR') {
|
||||||
|
return { nextState: 'S_LOGIN_SELECT_SERVICE' };
|
||||||
|
}
|
||||||
if (event.type === 'E_BACK') {
|
if (event.type === 'E_BACK') {
|
||||||
return { nextState: 'S_LOGIN_SELECT_SERVICE' };
|
return { nextState: 'S_LOGIN_SELECT_SERVICE' };
|
||||||
}
|
}
|
||||||
|
|||||||
@ -5,20 +5,12 @@ import {
|
|||||||
} from '../utils/relay';
|
} from '../utils/relay';
|
||||||
import { fetchAndLoadBloom } from '../utils/bloom';
|
import { fetchAndLoadBloom } from '../utils/bloom';
|
||||||
import type { RelayConfig } from '../types/identity';
|
import type { RelayConfig } from '../types/identity';
|
||||||
import type { MessageBase, MsgChiffre, MsgSignature, MsgCle } from '../types/message';
|
import type { MsgChiffre, MsgSignature, MsgCle } from '../types/message';
|
||||||
import { GraphResolver } from './graphResolver';
|
import { GraphResolver } from './graphResolver';
|
||||||
import { verifyMessageHash, verifyMessageSignatures, verifyTimestamp } from '../utils/verification';
|
import { validateDecryptedMessage } from './syncValidate';
|
||||||
|
import { updateGraphFromMessage } from './syncUpdateGraph';
|
||||||
import { HashCache } from '../utils/cache';
|
import { HashCache } from '../utils/cache';
|
||||||
import { runSyncLoop, type SyncOneRelayResult } from './syncLoop';
|
import { runSyncLoop, type SyncOneRelayResult } from './syncLoop';
|
||||||
import { isContractVersionSupported } from '../utils/contractVersion';
|
|
||||||
import type {
|
|
||||||
Service,
|
|
||||||
Contrat,
|
|
||||||
Champ,
|
|
||||||
Action,
|
|
||||||
Membre,
|
|
||||||
Pair,
|
|
||||||
} from '../types/contract';
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Service for synchronizing messages from relays.
|
* Service for synchronizing messages from relays.
|
||||||
@ -73,9 +65,12 @@ export class SyncService {
|
|||||||
if (decrypted === null) {
|
if (decrypted === null) {
|
||||||
return 'indechiffrable';
|
return 'indechiffrable';
|
||||||
}
|
}
|
||||||
const valid = await this.validateMessage(decrypted);
|
const valid = await validateDecryptedMessage(
|
||||||
|
decrypted,
|
||||||
|
(h) => this.fetchSignatures(h),
|
||||||
|
);
|
||||||
if (valid) {
|
if (valid) {
|
||||||
this.updateGraph(decrypted);
|
updateGraphFromMessage(decrypted, this.graphResolver);
|
||||||
return 'validated';
|
return 'validated';
|
||||||
}
|
}
|
||||||
return 'nonValide';
|
return 'nonValide';
|
||||||
@ -266,69 +261,4 @@ export class SyncService {
|
|||||||
return allSignatures;
|
return allSignatures;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Validate a decrypted message (check hash, signatures, timestamp, etc.).
|
|
||||||
*/
|
|
||||||
private async validateMessage(decrypted: unknown): Promise<boolean> {
|
|
||||||
try {
|
|
||||||
const msg = decrypted as MessageBase;
|
|
||||||
if (msg.hash === undefined) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
const hashValid = await verifyMessageHash(msg);
|
|
||||||
if (!hashValid) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!verifyTimestamp(msg.timestamp)) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
const signatures = await this.fetchSignatures(msg.hash.hash_value);
|
|
||||||
if (signatures.length > 0) {
|
|
||||||
const { valid } = verifyMessageSignatures(msg, signatures.map((s) => s.signature));
|
|
||||||
if (valid.length === 0) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return true;
|
|
||||||
} catch {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Update graph cache with decrypted message.
|
|
||||||
*/
|
|
||||||
private updateGraph(decrypted: unknown): void {
|
|
||||||
try {
|
|
||||||
const msg = decrypted as MessageBase;
|
|
||||||
const typeNames = msg.types.types_names_chiffres.toLowerCase();
|
|
||||||
|
|
||||||
if (typeNames.includes('service')) {
|
|
||||||
this.graphResolver.addService(msg as unknown as Service);
|
|
||||||
} else if (typeNames.includes('contrat')) {
|
|
||||||
const c = msg as unknown as Contrat;
|
|
||||||
if (!isContractVersionSupported(c.version)) {
|
|
||||||
console.warn(
|
|
||||||
`[SyncService] Contrat ${c.uuid} version ${c.version} not supported, skipped`,
|
|
||||||
);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
this.graphResolver.addContrat(c);
|
|
||||||
} else if (typeNames.includes('champ')) {
|
|
||||||
this.graphResolver.addChamp(msg as unknown as Champ);
|
|
||||||
} else if (typeNames.includes('action')) {
|
|
||||||
this.graphResolver.addAction(msg as unknown as Action);
|
|
||||||
} else if (typeNames.includes('membre')) {
|
|
||||||
this.graphResolver.addMembre(msg as unknown as Membre);
|
|
||||||
} else if (typeNames.includes('pair')) {
|
|
||||||
this.graphResolver.addPair(msg as unknown as Pair);
|
|
||||||
}
|
|
||||||
} catch (error) {
|
|
||||||
console.error('Error updating graph:', error);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
47
userwallet/src/services/syncUpdateGraph.ts
Normal file
47
userwallet/src/services/syncUpdateGraph.ts
Normal file
@ -0,0 +1,47 @@
|
|||||||
|
import { isContractVersionSupported } from '../utils/contractVersion';
|
||||||
|
import type { MessageBase } from '../types/message';
|
||||||
|
import type { GraphResolver } from './graphResolver';
|
||||||
|
import type {
|
||||||
|
Service,
|
||||||
|
Contrat,
|
||||||
|
Champ,
|
||||||
|
Action,
|
||||||
|
Membre,
|
||||||
|
Pair,
|
||||||
|
} from '../types/contract';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Update graph cache with decrypted message.
|
||||||
|
*/
|
||||||
|
export function updateGraphFromMessage(
|
||||||
|
decrypted: unknown,
|
||||||
|
graphResolver: GraphResolver,
|
||||||
|
): void {
|
||||||
|
try {
|
||||||
|
const msg = decrypted as MessageBase;
|
||||||
|
const typeNames = msg.types.types_names_chiffres.toLowerCase();
|
||||||
|
|
||||||
|
if (typeNames.includes('service')) {
|
||||||
|
graphResolver.addService(msg as unknown as Service);
|
||||||
|
} else if (typeNames.includes('contrat')) {
|
||||||
|
const c = msg as unknown as Contrat;
|
||||||
|
if (!isContractVersionSupported(c.version)) {
|
||||||
|
console.warn(
|
||||||
|
`[SyncService] Contrat ${c.uuid} version ${c.version} not supported, skipped`,
|
||||||
|
);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
graphResolver.addContrat(c);
|
||||||
|
} else if (typeNames.includes('champ')) {
|
||||||
|
graphResolver.addChamp(msg as unknown as Champ);
|
||||||
|
} else if (typeNames.includes('action')) {
|
||||||
|
graphResolver.addAction(msg as unknown as Action);
|
||||||
|
} else if (typeNames.includes('membre')) {
|
||||||
|
graphResolver.addMembre(msg as unknown as Membre);
|
||||||
|
} else if (typeNames.includes('pair')) {
|
||||||
|
graphResolver.addPair(msg as unknown as Pair);
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error updating graph:', error);
|
||||||
|
}
|
||||||
|
}
|
||||||
44
userwallet/src/services/syncValidate.ts
Normal file
44
userwallet/src/services/syncValidate.ts
Normal file
@ -0,0 +1,44 @@
|
|||||||
|
import { verifyMessageHash, verifyMessageSignatures, verifyTimestamp } from '../utils/verification';
|
||||||
|
import {
|
||||||
|
getValidateursIfMessageAValider,
|
||||||
|
validateMessageAValiderForSync,
|
||||||
|
} from '../utils/validatorsAccept';
|
||||||
|
import type { MessageBase, MsgSignature } from '../types/message';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Validate a decrypted message (hash, timestamp, signatures).
|
||||||
|
* MessageAValider: accept only if signed by validators per validateurs;
|
||||||
|
* otherwise crypto-only check when signatures present.
|
||||||
|
*/
|
||||||
|
export async function validateDecryptedMessage(
|
||||||
|
decrypted: unknown,
|
||||||
|
fetchSignatures: (hash: string) => Promise<MsgSignature[]>,
|
||||||
|
): Promise<boolean> {
|
||||||
|
try {
|
||||||
|
const msg = decrypted as MessageBase;
|
||||||
|
if (msg.hash === undefined) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
if (!(await verifyMessageHash(msg)) || !verifyTimestamp(msg.timestamp)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
const signatures = await fetchSignatures(msg.hash.hash_value);
|
||||||
|
const sigList = signatures.map((s) => s.signature);
|
||||||
|
const validateurs = getValidateursIfMessageAValider(msg);
|
||||||
|
if (validateurs !== null) {
|
||||||
|
return validateMessageAValiderForSync(
|
||||||
|
msg,
|
||||||
|
sigList,
|
||||||
|
validateurs,
|
||||||
|
'[SyncValidate]',
|
||||||
|
);
|
||||||
|
}
|
||||||
|
if (sigList.length > 0) {
|
||||||
|
const { valid } = verifyMessageSignatures(msg, sigList);
|
||||||
|
return valid.length > 0;
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
} catch {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -64,6 +64,8 @@ export interface SignatureRequirement {
|
|||||||
pair_uuid?: string;
|
pair_uuid?: string;
|
||||||
cle_publique?: string;
|
cle_publique?: string;
|
||||||
cardinalite_minimale?: number;
|
cardinalite_minimale?: number;
|
||||||
|
/** UUIDs of other requirements that must be satisfied first. */
|
||||||
|
dependances?: string[];
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
165
userwallet/src/utils/collectSignatures.ts
Normal file
165
userwallet/src/utils/collectSignatures.ts
Normal file
@ -0,0 +1,165 @@
|
|||||||
|
import { getSignatures } from './relay';
|
||||||
|
import { getStoredPairs } from './pairing';
|
||||||
|
import { hasEnoughSignatures } from './loginValidation';
|
||||||
|
import type { LoginPath } from '../types/identity';
|
||||||
|
import type { MsgSignature } from '../types/message';
|
||||||
|
|
||||||
|
export interface ProofSignature {
|
||||||
|
signature: string;
|
||||||
|
cle_publique: string;
|
||||||
|
nonce: string;
|
||||||
|
pair_uuid: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Fetch signatures for hash from all relays, aggregate and deduplicate.
|
||||||
|
*/
|
||||||
|
export async function fetchSignaturesForHash(
|
||||||
|
relayEndpoints: string[],
|
||||||
|
hash: string,
|
||||||
|
): Promise<MsgSignature[]> {
|
||||||
|
const seen = new Set<string>();
|
||||||
|
const out: MsgSignature[] = [];
|
||||||
|
for (const ep of relayEndpoints) {
|
||||||
|
try {
|
||||||
|
const list = await getSignatures(ep, hash);
|
||||||
|
for (const m of list) {
|
||||||
|
const s = m.signature;
|
||||||
|
const key = `${s.cle_publique}\t${s.nonce}\t${s.signature}`;
|
||||||
|
if (seen.has(key)) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
seen.add(key);
|
||||||
|
out.push(m);
|
||||||
|
}
|
||||||
|
} catch {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return out;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Map pair_uuid -> membres_parents_uuid for pairs in pairsAttendus.
|
||||||
|
*/
|
||||||
|
export function buildPairToMembers(
|
||||||
|
pairsAttendus: string[],
|
||||||
|
): Map<string, string[]> {
|
||||||
|
const pairs = getStoredPairs();
|
||||||
|
const set = new Set(pairsAttendus);
|
||||||
|
const m = new Map<string, string[]>();
|
||||||
|
for (const p of pairs) {
|
||||||
|
if (!set.has(p.uuid)) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
m.set(p.uuid, p.membres_parents_uuid);
|
||||||
|
}
|
||||||
|
return m;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Map cle_publique -> pair_uuid for local (identity) and remote (pair.publicKey) pairs.
|
||||||
|
*/
|
||||||
|
export function buildPubkeyToPair(
|
||||||
|
identityPublicKey: string,
|
||||||
|
pairsAttendus: string[],
|
||||||
|
): Map<string, string> {
|
||||||
|
const pairs = getStoredPairs();
|
||||||
|
const set = new Set(pairsAttendus);
|
||||||
|
const m = new Map<string, string>();
|
||||||
|
for (const p of pairs) {
|
||||||
|
if (!set.has(p.uuid)) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if (p.is_local) {
|
||||||
|
m.set(identityPublicKey, p.uuid);
|
||||||
|
} else if (p.publicKey !== undefined) {
|
||||||
|
m.set(p.publicKey, p.uuid);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return m;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Map MsgSignature[] to ProofSignature[] using pubkey->pair. Skip unknown pubkeys.
|
||||||
|
*/
|
||||||
|
export function mapMsgSignaturesToProofFormat(
|
||||||
|
msgs: MsgSignature[],
|
||||||
|
hash: string,
|
||||||
|
pubkeyToPair: Map<string, string>,
|
||||||
|
): ProofSignature[] {
|
||||||
|
const out: ProofSignature[] = [];
|
||||||
|
for (const m of msgs) {
|
||||||
|
const s = m.signature;
|
||||||
|
if (s.hash !== hash) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
const pairUuid = pubkeyToPair.get(s.cle_publique);
|
||||||
|
if (pairUuid === undefined) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
out.push({
|
||||||
|
signature: s.signature,
|
||||||
|
cle_publique: s.cle_publique,
|
||||||
|
nonce: s.nonce,
|
||||||
|
pair_uuid: pairUuid,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
return out;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const COLLECT_POLL_MS = 2000;
|
||||||
|
export const COLLECT_TIMEOUT_MS = 300000;
|
||||||
|
|
||||||
|
export function delay(ms: number): Promise<void> {
|
||||||
|
return new Promise((resolve) => {
|
||||||
|
setTimeout(resolve, ms);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Merge our sigs with fetched, dedup by pair_uuid (keep first).
|
||||||
|
*/
|
||||||
|
function mergeDedupByPair(
|
||||||
|
ours: ProofSignature[],
|
||||||
|
fetched: ProofSignature[],
|
||||||
|
): ProofSignature[] {
|
||||||
|
const byPair = new Map<string, ProofSignature>();
|
||||||
|
for (const s of ours) {
|
||||||
|
byPair.set(s.pair_uuid, s);
|
||||||
|
}
|
||||||
|
for (const s of fetched) {
|
||||||
|
if (!byPair.has(s.pair_uuid)) {
|
||||||
|
byPair.set(s.pair_uuid, s);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return Array.from(byPair.values());
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Collect signatures from relays until we have enough per member, or timeout.
|
||||||
|
*/
|
||||||
|
export async function runCollectLoop(
|
||||||
|
relayEndpoints: string[],
|
||||||
|
hash: string,
|
||||||
|
ourSigs: ProofSignature[],
|
||||||
|
path: LoginPath,
|
||||||
|
pairToMembers: Map<string, string[]>,
|
||||||
|
pubkeyToPair: Map<string, string>,
|
||||||
|
opts: { pollMs: number; timeoutMs: number },
|
||||||
|
): Promise<ProofSignature[]> {
|
||||||
|
const start = Date.now();
|
||||||
|
let merged = ourSigs;
|
||||||
|
for (;;) {
|
||||||
|
if (hasEnoughSignatures(path, merged, pairToMembers)) {
|
||||||
|
return merged;
|
||||||
|
}
|
||||||
|
if (Date.now() - start >= opts.timeoutMs) {
|
||||||
|
throw new Error('Collecte distante : timeout (signatures manquantes)');
|
||||||
|
}
|
||||||
|
const msgs = await fetchSignaturesForHash(relayEndpoints, hash);
|
||||||
|
const fetched = mapMsgSignaturesToProofFormat(msgs, hash, pubkeyToPair);
|
||||||
|
merged = mergeDedupByPair(ourSigs, fetched);
|
||||||
|
await delay(opts.pollMs);
|
||||||
|
}
|
||||||
|
}
|
||||||
39
userwallet/src/utils/loginPublish.ts
Normal file
39
userwallet/src/utils/loginPublish.ts
Normal file
@ -0,0 +1,39 @@
|
|||||||
|
import { postMessageChiffre, postSignature } from './relay';
|
||||||
|
import type { RelayConfig } from '../types/identity';
|
||||||
|
import type { MsgChiffre, MsgSignature } from '../types/message';
|
||||||
|
import type { ProofSignature } from './collectSignatures';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Publish message + ourSigs to relays (best effort per relay).
|
||||||
|
*/
|
||||||
|
export async function publishMessageAndSigs(
|
||||||
|
relays: RelayConfig[],
|
||||||
|
msgChiffre: MsgChiffre,
|
||||||
|
ourSigs: ProofSignature[],
|
||||||
|
): Promise<number> {
|
||||||
|
let ok = 0;
|
||||||
|
for (const r of relays) {
|
||||||
|
if (!r.enabled) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
await postMessageChiffre(r.endpoint, msgChiffre);
|
||||||
|
for (const sig of ourSigs) {
|
||||||
|
const msgSig: MsgSignature = {
|
||||||
|
signature: {
|
||||||
|
hash: msgChiffre.hash,
|
||||||
|
cle_publique: sig.cle_publique,
|
||||||
|
signature: sig.signature,
|
||||||
|
nonce: sig.nonce,
|
||||||
|
},
|
||||||
|
hash_cible: msgChiffre.hash,
|
||||||
|
};
|
||||||
|
await postSignature(r.endpoint, msgSig);
|
||||||
|
}
|
||||||
|
ok++;
|
||||||
|
} catch (err) {
|
||||||
|
console.error(`Publish to ${r.endpoint} failed:`, err);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return ok;
|
||||||
|
}
|
||||||
33
userwallet/src/utils/loginSign.ts
Normal file
33
userwallet/src/utils/loginSign.ts
Normal file
@ -0,0 +1,33 @@
|
|||||||
|
import { getStoredRelays } from './relay';
|
||||||
|
import { postSignature } from './relay';
|
||||||
|
import { signMessage } from './crypto';
|
||||||
|
import type { MsgSignature } from '../types/message';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Sign hash-nonce with privateKey and post MsgSignature to all enabled relays.
|
||||||
|
* Returns number of relays that accepted.
|
||||||
|
*/
|
||||||
|
export async function signAndPostLoginChallenge(
|
||||||
|
hash: string,
|
||||||
|
nonce: string,
|
||||||
|
privateKey: string,
|
||||||
|
publicKey: string,
|
||||||
|
): Promise<number> {
|
||||||
|
const messageToSign = `${hash}-${nonce}`;
|
||||||
|
const signature = signMessage(messageToSign, privateKey);
|
||||||
|
const msgSig: MsgSignature = {
|
||||||
|
signature: { hash, cle_publique: publicKey, signature, nonce },
|
||||||
|
hash_cible: hash,
|
||||||
|
};
|
||||||
|
const relays = getStoredRelays().filter((r) => r.enabled);
|
||||||
|
let ok = 0;
|
||||||
|
for (const r of relays) {
|
||||||
|
try {
|
||||||
|
await postSignature(r.endpoint, msgSig);
|
||||||
|
ok++;
|
||||||
|
} catch (err) {
|
||||||
|
console.error(`Post signature to ${r.endpoint} failed:`, err);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return ok;
|
||||||
|
}
|
||||||
151
userwallet/src/utils/loginValidation.ts
Normal file
151
userwallet/src/utils/loginValidation.ts
Normal file
@ -0,0 +1,151 @@
|
|||||||
|
import { getStoredPairs } from './pairing';
|
||||||
|
import type { LoginPath, SignatureRequirement } from '../types/identity';
|
||||||
|
import type { LoginProof } from '../types/identity';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Build allowed pubkeys for strict verification: from requirements (cle_publique)
|
||||||
|
* and from pairs (pair.publicKey, or identityKey for local pair).
|
||||||
|
*/
|
||||||
|
export function buildAllowedPubkeys(
|
||||||
|
loginPath: LoginPath,
|
||||||
|
identityPublicKey: string,
|
||||||
|
): Set<string> {
|
||||||
|
const out = new Set<string>();
|
||||||
|
const pairs = getStoredPairs();
|
||||||
|
const pairByUuid = new Map(pairs.map((p) => [p.uuid, p]));
|
||||||
|
const pairsSet = new Set(loginPath.pairs_attendus);
|
||||||
|
|
||||||
|
for (const req of loginPath.signatures_requises) {
|
||||||
|
if (req.cle_publique !== undefined) {
|
||||||
|
out.add(req.cle_publique);
|
||||||
|
}
|
||||||
|
if (req.pair_uuid === undefined) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
const pair = pairByUuid.get(req.pair_uuid);
|
||||||
|
if (pair === undefined || !pairsSet.has(req.pair_uuid)) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if (pair.is_local) {
|
||||||
|
out.add(identityPublicKey);
|
||||||
|
} else if (pair.publicKey !== undefined) {
|
||||||
|
out.add(pair.publicKey);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return out;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Required signature count per member (max of cardinalite_minimale over requirements).
|
||||||
|
*/
|
||||||
|
export function requiredSigsPerMember(
|
||||||
|
requirements: SignatureRequirement[],
|
||||||
|
): Map<string, number> {
|
||||||
|
const m = new Map<string, number>();
|
||||||
|
for (const r of requirements) {
|
||||||
|
const c = r.cardinalite_minimale ?? 1;
|
||||||
|
const cur = m.get(r.membre_uuid) ?? 0;
|
||||||
|
m.set(r.membre_uuid, Math.max(cur, c));
|
||||||
|
}
|
||||||
|
return m;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Check we have enough distinct pairs per member (for collecte distante).
|
||||||
|
* Count distinct pair_uuids per member via pairToMembers.
|
||||||
|
*/
|
||||||
|
export function hasEnoughSignatures(
|
||||||
|
path: LoginPath,
|
||||||
|
signatures: Array<{ pair_uuid: string }>,
|
||||||
|
pairToMembers: Map<string, string[]>,
|
||||||
|
): boolean {
|
||||||
|
const required = requiredSigsPerMember(path.signatures_requises);
|
||||||
|
const pairsPerMember = new Map<string, Set<string>>();
|
||||||
|
for (const sig of signatures) {
|
||||||
|
const members = pairToMembers.get(sig.pair_uuid);
|
||||||
|
if (members === undefined) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
for (const memb of members) {
|
||||||
|
let set = pairsPerMember.get(memb);
|
||||||
|
if (set === undefined) {
|
||||||
|
set = new Set<string>();
|
||||||
|
pairsPerMember.set(memb, set);
|
||||||
|
}
|
||||||
|
set.add(sig.pair_uuid);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
for (const [member, need] of required) {
|
||||||
|
const have = pairsPerMember.get(member)?.size ?? 0;
|
||||||
|
if (have < need) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* True if any signature comes from a non-local pair (2nd device).
|
||||||
|
* Used to require manual accept before validating when words may have been intercepted.
|
||||||
|
*/
|
||||||
|
export function hasRemoteSignatures(
|
||||||
|
signatures: Array<{ pair_uuid: string }>,
|
||||||
|
pairsAttendus: string[],
|
||||||
|
): boolean {
|
||||||
|
const pairs = getStoredPairs();
|
||||||
|
const set = new Set(pairsAttendus);
|
||||||
|
const pairByUuid = new Map(pairs.map((p) => [p.uuid, p]));
|
||||||
|
for (const sig of signatures) {
|
||||||
|
if (!set.has(sig.pair_uuid)) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
const pair = pairByUuid.get(sig.pair_uuid);
|
||||||
|
if (pair !== undefined && !pair.is_local) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Members that have at least one signature in proof (via pair → membres_parents_uuid).
|
||||||
|
*/
|
||||||
|
function membersWithSignatures(proof: LoginProof): Set<string> {
|
||||||
|
const pairs = getStoredPairs();
|
||||||
|
const pairByUuid = new Map(pairs.map((p) => [p.uuid, p]));
|
||||||
|
const out = new Set<string>();
|
||||||
|
for (const sig of proof.signatures) {
|
||||||
|
const pair = pairByUuid.get(sig.pair_uuid);
|
||||||
|
if (pair === undefined) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
for (const m of pair.membres_parents_uuid) {
|
||||||
|
out.add(m);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return out;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Check that dependencies between requirements are satisfied.
|
||||||
|
* For each requirement with dependances (member UUIDs), those members must have
|
||||||
|
* at least one signature in the proof (via a pair of that member).
|
||||||
|
*/
|
||||||
|
export function checkDependenciesSatisfied(
|
||||||
|
path: LoginPath,
|
||||||
|
proof: LoginProof,
|
||||||
|
): boolean {
|
||||||
|
const membersSigned = membersWithSignatures(proof);
|
||||||
|
for (const req of path.signatures_requises) {
|
||||||
|
const deps = req.dependances;
|
||||||
|
if (deps === undefined || deps.length === 0) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
for (const dep of deps) {
|
||||||
|
if (!membersSigned.has(dep)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
@ -1,6 +1,9 @@
|
|||||||
import type { RelayConfig } from '../types/identity';
|
import type { RelayConfig } from '../types/identity';
|
||||||
import type { MsgChiffre, MsgSignature, MsgCle } from '../types/message';
|
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;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Get stored relay configurations.
|
* Get stored relay configurations.
|
||||||
*/
|
*/
|
||||||
@ -56,7 +59,9 @@ export async function getMessagesChiffres(
|
|||||||
if (serviceUuid !== undefined) {
|
if (serviceUuid !== undefined) {
|
||||||
params.append('service', serviceUuid);
|
params.append('service', serviceUuid);
|
||||||
}
|
}
|
||||||
const response = await fetch(`${relay}/messages?${params.toString()}`);
|
const response = await fetch(`${relay}/messages?${params.toString()}`, {
|
||||||
|
signal: AbortSignal.timeout(RELAY_FETCH_TIMEOUT_MS),
|
||||||
|
});
|
||||||
if (!response.ok) {
|
if (!response.ok) {
|
||||||
throw new Error(
|
throw new Error(
|
||||||
`Relay GET /messages failed: ${response.status} ${response.statusText} (${relay})`,
|
`Relay GET /messages failed: ${response.status} ${response.statusText} (${relay})`,
|
||||||
@ -70,7 +75,9 @@ export async function getMessagesChiffres(
|
|||||||
* Throws on fetch failure or non-ok response.
|
* Throws on fetch failure or non-ok response.
|
||||||
*/
|
*/
|
||||||
export async function getSignatures(relay: string, hash: string): Promise<MsgSignature[]> {
|
export async function getSignatures(relay: string, hash: string): Promise<MsgSignature[]> {
|
||||||
const response = await fetch(`${relay}/signatures/${hash}`);
|
const response = await fetch(`${relay}/signatures/${hash}`, {
|
||||||
|
signal: AbortSignal.timeout(RELAY_FETCH_TIMEOUT_MS),
|
||||||
|
});
|
||||||
if (!response.ok) {
|
if (!response.ok) {
|
||||||
throw new Error(
|
throw new Error(
|
||||||
`Relay GET /signatures/${hash} failed: ${response.status} ${response.statusText} (${relay})`,
|
`Relay GET /signatures/${hash} failed: ${response.status} ${response.statusText} (${relay})`,
|
||||||
@ -84,7 +91,9 @@ export async function getSignatures(relay: string, hash: string): Promise<MsgSig
|
|||||||
* Throws on fetch failure or non-ok response.
|
* Throws on fetch failure or non-ok response.
|
||||||
*/
|
*/
|
||||||
export async function getKeys(relay: string, hash: string): Promise<MsgCle[]> {
|
export async function getKeys(relay: string, hash: string): Promise<MsgCle[]> {
|
||||||
const response = await fetch(`${relay}/keys/${hash}`);
|
const response = await fetch(`${relay}/keys/${hash}`, {
|
||||||
|
signal: AbortSignal.timeout(RELAY_FETCH_TIMEOUT_MS),
|
||||||
|
});
|
||||||
if (!response.ok) {
|
if (!response.ok) {
|
||||||
throw new Error(
|
throw new Error(
|
||||||
`Relay GET /keys/${hash} failed: ${response.status} ${response.statusText} (${relay})`,
|
`Relay GET /keys/${hash} failed: ${response.status} ${response.statusText} (${relay})`,
|
||||||
@ -102,6 +111,7 @@ export async function postMessageChiffre(relay: string, message: MsgChiffre): Pr
|
|||||||
method: 'POST',
|
method: 'POST',
|
||||||
headers: { 'Content-Type': 'application/json' },
|
headers: { 'Content-Type': 'application/json' },
|
||||||
body: JSON.stringify(message),
|
body: JSON.stringify(message),
|
||||||
|
signal: AbortSignal.timeout(RELAY_FETCH_TIMEOUT_MS),
|
||||||
});
|
});
|
||||||
if (!response.ok) {
|
if (!response.ok) {
|
||||||
throw new Error(
|
throw new Error(
|
||||||
@ -119,6 +129,7 @@ export async function postSignature(relay: string, signature: MsgSignature): Pro
|
|||||||
method: 'POST',
|
method: 'POST',
|
||||||
headers: { 'Content-Type': 'application/json' },
|
headers: { 'Content-Type': 'application/json' },
|
||||||
body: JSON.stringify(signature),
|
body: JSON.stringify(signature),
|
||||||
|
signal: AbortSignal.timeout(RELAY_FETCH_TIMEOUT_MS),
|
||||||
});
|
});
|
||||||
if (!response.ok) {
|
if (!response.ok) {
|
||||||
throw new Error(
|
throw new Error(
|
||||||
@ -136,6 +147,7 @@ export async function postKey(relay: string, key: MsgCle): Promise<void> {
|
|||||||
method: 'POST',
|
method: 'POST',
|
||||||
headers: { 'Content-Type': 'application/json' },
|
headers: { 'Content-Type': 'application/json' },
|
||||||
body: JSON.stringify(key),
|
body: JSON.stringify(key),
|
||||||
|
signal: AbortSignal.timeout(RELAY_FETCH_TIMEOUT_MS),
|
||||||
});
|
});
|
||||||
if (!response.ok) {
|
if (!response.ok) {
|
||||||
throw new Error(
|
throw new Error(
|
||||||
@ -150,7 +162,9 @@ export async function postKey(relay: string, key: MsgCle): Promise<void> {
|
|||||||
* Throws on fetch failure or non-ok response.
|
* Throws on fetch failure or non-ok response.
|
||||||
*/
|
*/
|
||||||
export async function getBloom(relay: string): Promise<unknown> {
|
export async function getBloom(relay: string): Promise<unknown> {
|
||||||
const response = await fetch(`${relay}/bloom`);
|
const response = await fetch(`${relay}/bloom`, {
|
||||||
|
signal: AbortSignal.timeout(RELAY_FETCH_TIMEOUT_MS),
|
||||||
|
});
|
||||||
if (!response.ok) {
|
if (!response.ok) {
|
||||||
throw new Error(
|
throw new Error(
|
||||||
`Relay GET /bloom failed: ${response.status} ${response.statusText} (${relay})`,
|
`Relay GET /bloom failed: ${response.status} ${response.statusText} (${relay})`,
|
||||||
|
|||||||
124
userwallet/src/utils/validatorsAccept.ts
Normal file
124
userwallet/src/utils/validatorsAccept.ts
Normal file
@ -0,0 +1,124 @@
|
|||||||
|
import {
|
||||||
|
verifyMessageSignaturesStrict,
|
||||||
|
} from './verification';
|
||||||
|
import type { MessageBase, Signature, Validateurs } from '../types/message';
|
||||||
|
|
||||||
|
function isValidateursShape(v: unknown): v is Validateurs {
|
||||||
|
return (
|
||||||
|
typeof v === 'object' &&
|
||||||
|
v !== null &&
|
||||||
|
'membres_du_role' in v &&
|
||||||
|
Array.isArray((v as Validateurs).membres_du_role)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Return validateurs if message is MessageAValider, else null.
|
||||||
|
*/
|
||||||
|
export function getValidateursIfMessageAValider(
|
||||||
|
msg: MessageBase,
|
||||||
|
): Validateurs | null {
|
||||||
|
const v = (msg as { validateurs?: unknown }).validateurs;
|
||||||
|
return isValidateursShape(v) ? v : null;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Build allowed pubkeys from validateurs (cle_publique in signatures_obligatoires).
|
||||||
|
* Used when accepting a version of an object during sync; no pair resolution.
|
||||||
|
*/
|
||||||
|
export function buildAllowedPubkeysFromValidateurs(
|
||||||
|
validateurs: Validateurs,
|
||||||
|
): Set<string> {
|
||||||
|
const out = new Set<string>();
|
||||||
|
for (const m of validateurs.membres_du_role) {
|
||||||
|
for (const r of m.signatures_obligatoires) {
|
||||||
|
if (r.cle_publique !== undefined) {
|
||||||
|
out.add(r.cle_publique);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return out;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Check if we can verify validators from validateurs (at least one cle_publique).
|
||||||
|
*/
|
||||||
|
export function canVerifyValidateurs(validateurs: Validateurs): boolean {
|
||||||
|
return buildAllowedPubkeysFromValidateurs(validateurs).size > 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface ValidateSignaturesResult {
|
||||||
|
accept: boolean;
|
||||||
|
reason?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Verify that signatures satisfy validateurs (allowed pubkeys, no unauthorized).
|
||||||
|
* Accept only if all signers are allowed and at least one valid signature.
|
||||||
|
* Used when accepting a version of an object (sync); cardinality/deps require
|
||||||
|
* pair resolution and are not enforced here.
|
||||||
|
*/
|
||||||
|
export function validateSignaturesAgainstValidateurs(
|
||||||
|
message: MessageBase,
|
||||||
|
signatures: Signature[],
|
||||||
|
validateurs: Validateurs,
|
||||||
|
): ValidateSignaturesResult {
|
||||||
|
const allowed = buildAllowedPubkeysFromValidateurs(validateurs);
|
||||||
|
if (allowed.size === 0) {
|
||||||
|
return {
|
||||||
|
accept: false,
|
||||||
|
reason: 'validators_not_verifiable_no_cle_publique',
|
||||||
|
};
|
||||||
|
}
|
||||||
|
const { valid, unauthorized } = verifyMessageSignaturesStrict(
|
||||||
|
message,
|
||||||
|
signatures,
|
||||||
|
allowed,
|
||||||
|
);
|
||||||
|
if (unauthorized.length > 0) {
|
||||||
|
return {
|
||||||
|
accept: false,
|
||||||
|
reason: 'signature_cle_publique_not_authorized',
|
||||||
|
};
|
||||||
|
}
|
||||||
|
if (valid.length === 0) {
|
||||||
|
return {
|
||||||
|
accept: false,
|
||||||
|
reason: 'no_validator_signature',
|
||||||
|
};
|
||||||
|
}
|
||||||
|
return { accept: true };
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Full check for sync: validators verifiable, sigs present, all allowed, ≥1 valid.
|
||||||
|
* Logs warnings on failure. Used by SyncService.
|
||||||
|
*/
|
||||||
|
export function validateMessageAValiderForSync(
|
||||||
|
msg: MessageBase,
|
||||||
|
sigList: Signature[],
|
||||||
|
validateurs: Validateurs,
|
||||||
|
logPrefix: string,
|
||||||
|
): boolean {
|
||||||
|
if (!canVerifyValidateurs(validateurs)) {
|
||||||
|
console.warn(
|
||||||
|
`${logPrefix} MessageAValider: no cle_publique in validateurs, skip`,
|
||||||
|
);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
if (sigList.length === 0) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
const result = validateSignaturesAgainstValidateurs(
|
||||||
|
msg,
|
||||||
|
sigList,
|
||||||
|
validateurs,
|
||||||
|
);
|
||||||
|
if (!result.accept) {
|
||||||
|
if (result.reason !== undefined) {
|
||||||
|
console.warn(`${logPrefix} MessageAValider validation failed: ${result.reason}`);
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
Loading…
x
Reference in New Issue
Block a user