Audit du code source — pq-akarup-mobile
Date : 21 juillet 2026
Version : v2 (ajout section 3 — protocole post-quantique et conformité NIST)
Périmètre : archive pq-akarup-mobile_copie.zip (copie de travail, HEAD e3a4483 sur develop)
Méthode : restauration depuis le .git embarqué des dossiers exclus du zip (modules/, __mocks__/, docs/), exécution du workflow CSPN complet (npm ci, tsc --noEmit, vitest run, eslint), greps ciblés sur les pièges connus du projet, et lecture ligne à ligne des primitives cryptographiques ainsi que du code source de @noble/post-quantum 0.5.4 tel qu'installé dans node_modules.
1. Verdict global : sain
Le workflow CSPN passe intégralement sur cette copie :
| Vérification | Résultat |
|---|---|
npx tsc --noEmit | 0 erreur |
npx vitest run | 888 tests verts, 1 skipped, 62 fichiers |
| Parité i18n fr/en | 708 clés = 708 clés, zéro manquante dans les deux sens |
| ESLint | 5 erreurs (cosmétiques), 123 warnings |
Le seul fichier de test en échec est lib/crypto/__tests__/test-vectors-interop.test.ts, uniquement parce qu'il lit ../documentation/strategie et doc tech/test-vectors-archi006.json hors du repo. Dépendance d'environnement, pas un bug (voir constat C4).
Pièges historiques du projet : tous propres
expo-file-system: tous les imports actifs passent par le subpath/legacy(piège SDK 54). Aucun import direct détecté.- Argon2id : toutes les dérivations passent par
buildEffectiveSaltavec le préfixe de domaineAKARUP/kdf/argon2id/v2/(PasswordManager.ts:208,password-wrap.ts:96). Aucun appel avec le salt base64 brut. - Plafond
crypto.getRandomValues(65 536 octets) : tous les buffers aléatoires audités sont de petites clés. Le seul point de dépassement possible (padding par buckets du bus, tranche ~50–95 Ko) est correctement traité par chunking de 65 536 octets danspayload-encrypt.ts(fix M-MEDIA-V1 PAQUET 3). - Singleton
IdentityService:clearIdentity()(logout léger multi-utilisateur) etresetIdentity()remettent bienisInitialized = false. - Fuites de secrets dans les logs : aucune. Les
console.logd'AuthFactoryetCompleteRegisterServiceV2décrivent les étapes (« Mnemonic generated », « MasterKey unwrapped ») sans jamais interpoler la valeur d'un mnemonic, seed, MasterKey, KEK ou clé privée. - Endpoints en dur : aucun
http://hors adresses LAN de développement. La config passe parlib/config.tsavec priorité.env.local> défauts par environnement. - Hooks React après early return : spot-check sur les écrans récents (
automations.tsx,AutomationsEditor.tsx,DrivePicker.tsx) : tous les hooks sont déclarés avant les returns conditionnels. - Hooks git :
.githooks/versionné etcore.hooksPathconfiguré (recommandation antérieure appliquée). - Stockage : JWT en mémoire (zustand), pas de persistance AsyncStorage ; secrets via SecureStore/EncryptedStore ;
useSignupFlowStoreexplicitement RAM-only. Math.randomrestants : tous légitimes — identifiants locaux non cryptographiques (ids d'événements/notifs de sync), positions de vérification BIP39 (documenté non-sensible dansBip39Service.ts:303), ou code mort (C1). LedeviceIdOpaquedu bus passe bien pargetRandomValues(hash-destinataire.ts:82).
2. Constats à traiter
C1 — Cluster de code mort legacy V1 embarquant crypto-js
Priorité : haute (surface CSPN)
Les fichiers suivants ne sont référencés par aucun écran ni service actif (chaînes d'import vérifiées une à une, elles bouclent entre elles) :
src/services/RegisterService.ts(V1, remplacé parPreRegisterServiceV2/CompleteRegisterServiceV2)src/services/LoginService.ts(V1, remplacé parLoginServiceV2)lib/services/cryptoService.ts(importé uniquement par les V1 morts et le barrellib/services/index.ts)src/services/BlobCryptoService.tssrc/services/ChunkedUploadService.tssrc/managers/sync/BlobSyncManager.tssrc/services/MultiDeviceSyncService.tssrc/hooks/useBlobUpload.tssrc/hooks/useBlobDownload.ts
Ce cluster pose trois problèmes pour l'évaluation ANSSI :
- Dépendance
crypto-js: bibliothèque non auditée, redondante avec@noble/hashesdéjà présent et utilisé partout ailleurs. Un évaluateur la relèvera même si elle est morte. - Hash non-standard :
BlobCryptoService._hashBlobData(ligne 606) faitCryptoJS.SHA256(bytesToHex(data))— un SHA-256 de la chaîne hexadécimale ASCII, pas des octets. Ce hash n'interopérerait jamais avec unsha256.Sum256(data)Go. Aucun bug en production puisque le code est mort, mais c'est un piège de réutilisation accidentelle. - Ordre d'arguments ML-DSA inversé (découvert lors de l'analyse crypto, section 3) :
cryptoService.ts:202appelleml_dsa65.sign(privateKey, messageBytes)— l'ordre de l'ancienne API noble. L'API 0.5.4 installée estsign(msg, secretKey, opts?). Les deux arguments étant desUint8Array, TypeScript ne détecte rien : si ce code mort était rappelé un jour, il signerait le mauvais contenu avec le mauvais matériau sans erreur de compilation. Argument supplémentaire décisif pour la purge.
Recommandation : sprint de purge — supprimer les 9 fichiers (et leurs tests associés le cas échéant), retirer crypto-js (dependencies) et @types/crypto-js (devDependencies) du package.json, nettoyer le barrel lib/services/index.ts.
Attention : @noble/curves est actif (lib/crypto/primitives/ed25519.ts, x25519.ts, lib/crypto/bip39/IdentityDerivationService.ts) — à conserver.
C2 — Quatre fichiers .bak versionnés dans git
Priorité : moyenne (hygiène / convention)
lib/services/DeviceService.ts.before-passe-4.bak
lib/services/MessageStore.ts.before-dev-007.bak
src/managers/AuthFactory.ts.before-passe-3.bak
src/services/DeviceSetupService.ts.before-passe-4.bak
Contraire à la convention du projet (backups dans /tmp/ uniquement). L'historique git suffit pour tout rollback — le commentaire de MessageStore.ts qui justifie le .bak « conservé pour rollback » est redondant avec git log.
Recommandation : supprimer les 4 fichiers, ajouter *.bak au .gitignore.
C3 — Polyfill react-native-get-random-values non hoisté
Priorité : moyenne → haute après l'analyse crypto (robustesse du CSPRNG)
Le polyfill n'est importé (side-effect) que dans lib/services/FileCryptoServiceV2.ts:33 — et dans le cryptoService mort (C1). Or une douzaine de modules actifs appellent crypto.getRandomValues directement : GroupConversationService, KdfMetadata, ContactService, DeviceService, SPKRotationManager, BlobService, apiServiceV2, hash-destinataire, capability, k-blob, k-media, CompleteRegisterServiceV2 — auxquels s'ajoutent, vus en section 3, tous les keygens ML-KEM/ML-DSA, l'entropie BIP39, les nonces AEAD et les seeds hedgées des signatures.
crypto.getRandomValues est donc l'unique racine d'entropie de toute la cryptographie de l'application, et son existence dans le runtime repose sur un effet de bord de l'ordre de chargement des modules. Si un futur chemin de code appelle getRandomValues avant que FileCryptoServiceV2 ne soit chargé, l'appel échoue.
Recommandation : import 'react-native-get-random-values' en première ligne de lib/polyfills.ts (déjà importé tôt dans app/_layout.tsx), et retirer l'import éparpillé de FileCryptoServiceV2 une fois le hoist en place. Documenter dans la cible de sécurité CSPN que le polyfill délègue à SecRandomCopyBytes (iOS) et SecureRandom (Android), soit les CSPRNG de l'OS.
C4 — La CI GitLab n'exécute aucun test
Priorité : moyenne (traçabilité CSPN)
.gitlab-ci.yml ne contient que deux stages build et deploy (image Docker webapp). Ni tsc --noEmit, ni vitest run, ni eslint n'y figurent. Le workflow CSPN repose donc entièrement sur la discipline locale — et le hook pre-commit versionné ne fait que le bump de version (app.json), aucune vérification.
Recommandation : ajouter un stage test (npm ci && npx tsc --noEmit && npx vitest run) exécuté sur develop. Bénéfice direct pour le dossier CSPN (F-006) : trace opposable de l'exécution des vérifications à chaque push.
Point de vigilance lié : test-vectors-interop.test.ts lit les vecteurs dans le repo documentation frère (resolve(repoMobile, '..', 'documentation', ...)). En CI, il faudra soit rendre ce chemin configurable (variable d'environnement avec fallback), soit checkout le repo documentation dans le job, soit vendorer une copie du JSON de vecteurs dans le repo mobile (option la plus simple et la plus robuste — le fichier est un artefact de référence figé).
C5 — ESLint : 5 erreurs, 123 warnings
Priorité : basse (cosmétique)
Les 5 erreurs :
react/no-unescaped-entities(apostrophes non échappées) :DeviceRegisterScreen.tsx:18,ErrorScreen.tsx:17(×2),SuccessScreen.tsx:17import/namespacesur la référence calculéeLucide[name]danssrc/design/atoms/Icon.tsx:166— faux positif classique de la règle sur les accès dynamiques à un namespace ; peut être désactivé localement avec un commentaire eslint-disable ciblé.
Les 123 warnings sont quasi exclusivement des @typescript-eslint/no-unused-vars sur des catch (error) où l'erreur n'est pas utilisée (remplaçables par catch {}), plus un import/no-duplicates dans GroupConversationService (double import de bus-action.ts). 43 sont auto-corrigeables avec --fix.
C6 — Écriture transitoire en clair dans automationsDrive.saveStringToDrive
Priorité : basse (résiduel INV-015-10)
src/components/automations/automationsDrive.ts:40-41 écrit le contenu en clair dans un fichier temporaire (cacheDirectory/akarup-automations-<ts>.tmp) avant driveServiceV2.addFile, supprimé en finally. Le pattern est documenté et assumé (pont T2, piège SDK 54), mais un crash entre l'écriture et la suppression laisse un résidu en clair dans le cache sandbox de l'app.
Recommandation (mitigation peu coûteuse) : purge au démarrage de l'app des fichiers akarup-automations-*.tmp orphelins dans cacheDirectory.
C7 — Observations sur l'archive elle-même
Priorité : informative
.env.localest présent dans le zip. Contenu inspecté : uniquement des adresses LAN de développement (192.168.x.x), aucun secret. Pas de fuite, mais à garder en tête pour de futures archives partagées.- La copie excluait
modules/(dontexpo-akarup-argon2etexpo-akarup-quickjs) et__mocks__/, alors que lepackage.jsonréférencefile:./modules/expo-akarup-argon2. Sur une copie brute sans.git,npm ciéchouerait et l'audit tsc/vitest serait impossible. Pour de futures archives d'audit : incluremodules/et__mocks__/, ou fournir ungit archive HEAD.
C8 — Profil Argon2id Α-light obsolète alors que le module natif est en place
Priorité : haute (durcissement, issu de l'analyse section 3)
lib/auth/KdfMetadata.ts:113 : le profil actif ARGON2ID_PARAMS_V2 est Α-light (m = 16 MiB, t = 3, p = 1), explicitement documenté comme temporaire, choisi à cause des performances de l'implémentation JS pure @noble/hashes sous Hermes (~30 s pour le profil cible). Or lib/auth/argon2-impl.native.ts montre que le module natif expo-akarup-argon2 (phc-winner-argon2, RFC 9106) est intégré, sélectionné automatiquement par Metro sur iOS/Android, bit-à-bit identique au JS, et exécute le profil Β cible (m = 64 MiB, t = 4, p = 1) en ~892 ms sur iPhone et ~430 ms sur Xiaomi.
La contrainte qui justifiait Α-light a disparu. Le profil actuel reste au niveau « minimum OWASP » (facteur de coût attaquant ×1 500 vs ×12 000 pour Β, selon les commentaires du code lui-même), alors que RFC 9106 §4 recommande m = 64 MiB en second choix. Pour un produit visant la CSPN, rester sur le profil minimum sans justification technique est un point que l'évaluateur relèvera.
Recommandation : activer le profil Β. La mécanique de migration existe déjà : KdfMetadataV2 embarque ses paramètres par wrap (params sérialisés), donc les comptes existants restent déverrouillables avec leurs anciens paramètres, et un re-wrap au prochain login (ou au prochain changement de mot de passe) migre vers Β. Prévoir le fallback web : argon2-impl.web.ts reste en JS — soit conserver Α-light côté web avec métadonnées distinctes, soit accepter la latence, à trancher avant le desktop Electron.
3. Analyse du protocole post-quantique et conformité NIST
Cette section couvre l'intégralité de la pile cryptographique active : primitives, flux protocolaires, dérivation d'identité, et conformité aux FIPS 203/204 et aux référentiels associés. Toutes les vérifications ont été faites sur le code réel, y compris le source de @noble/post-quantum 0.5.4 installé dans node_modules (pas la documentation de la bibliothèque).
3.1 Inventaire des primitives et vérification des paramètres
| Primitive | Référence | Implémentation | Paramètres vérifiés dans le code | Conforme |
|---|---|---|---|---|
| ML-KEM-768 | FIPS 203 (final) | @noble/post-quantum 0.5.4 | ek 1184 B, dk 2400 B, ct 1088 B, ss 32 B (mlkem.ts:70-73) ; keygen seed 64 B = d‖z | ✅ |
| ML-DSA-65 | FIPS 204 (final) | @noble/post-quantum 0.5.4 | pk 1952 B, sk 4032 B, sig 3309 B (IdentityDerivationService.ts:100-104, group-message-cipher.ts:68) ; keygen seed ξ 32 B | ✅ |
| XChaCha20-Poly1305 | draft-irtf-cfrg-xchacha | @noble/ciphers 2.1.1 | clé 32 B, nonce 24 B, tag 16 B (xchacha.ts:29-36) | ⚠️ hors NIST, voir 3.5.d |
| ChaCha20-Poly1305 | RFC 8439 | @noble/ciphers 2.1.1 | nonce 12 B, enveloppe DEV-010 uniquement (chacha-std.ts) | ✅ IETF |
| HKDF-SHA-256/512 | RFC 5869 / SP 800-56C rev2 | @noble/hashes 2.0.1 | extract-then-expand, salt + info systématiques (hkdf.ts) | ✅ |
| Argon2id | RFC 9106 | phc-winner-argon2 natif vendored | m=16 MiB, t=3, p=1 (profil Α-light) | ⚠️ voir C8 |
| BIP39 | — | @scure/bip39 2.2.0 | 24 mots = 256 bits d'entropie + checksum, entropie via getRandomValues | ✅ |
| X25519 / Ed25519 | RFC 7748 / 8032 | @noble/curves 1.9.7 | volet classique de l'hybridation | ✅ |
Les tailles ne sont pas seulement déclarées : le code actif valide les tailles avant chaque opération (payload-encrypt.ts:281-291, group-message-cipher.ts:217-222, device-certification.ts:101-105, envelope/client.ts:131-135, assertSize sur les six clés du trousseau dans IdentityDerivationService). C'est une discipline de défense en profondeur rare et appréciable pour l'évaluation.
3.2 Vérification de l'usage de l'API noble (source installé, pas la doc)
Trois points critiques ont été vérifiés directement dans node_modules/@noble/post-quantum/ml-dsa.js :
a) Ordre des arguments. L'API 0.5.4 est sign(msg, secretKey, opts?) / verify(sig, msg, pubKey, opts?). Les 11 points d'appel actifs (group-message-cipher.ts:224/369, device-certification.ts:107, prekey-bundle.ts:57/108, BlobService.ts:487, StorageServiceClient.ts:292, manifestCanonical.ts:88, accountRecoveryService.ts:125, plus les verify associés) respectent tous cet ordre. Seul le code mort cryptoService.ts:202 utilise l'ordre inversé de l'ancienne API (→ C1.3).
b) Signature hedgée (recommandation FIPS 204 §3.4). Aucun appel ne passe d'options — j'ai vérifié dans le source noble le comportement par défaut : quand extraEntropy est absent, rnd = randomBytes(32) (ml-dsa.js:386-390), soit la variante hedgée recommandée par le FIPS 204 (protection contre les fautes et certaines attaques par canaux auxiliaires, par opposition au mode déterministe extraEntropy: false). Comportement conforme sans action requise.
c) Framing pure ML-DSA (FIPS 204 Algorithm 2). L'API publique noble applique M′ = 0x00 ‖ len(ctx) ‖ ctx ‖ M avec contexte vide par défaut (ml-dsa.js:522-527, getMessage), puis µ = H(tr ‖ M′). AKARUP utilise donc bien pure ML-DSA.Sign conforme, pas l'API interne. Le paramètre context FIPS 204 n'est pas utilisé — la séparation de domaine est faite en amont, dans le contenu haché (voir 3.4) : choix défendable et cohérent avec l'interop Go.
3.3 Architecture KEM-DEM et flux protocolaires
Bus pending_actions (payload-encrypt.ts) — le cœur du Blind Relay :
ml_kem768.encapsulate(pub destinataire) → (kem_ct 1088 B, ss 32 B)
HKDF-SHA-256(ss, salt="AKARUP/bus/v1") → clé AEAD 32 B
nonce aléatoire 24 B → XChaCha20-Poly1305(clé, plaintext paddé, nonce)
format versionné : version ‖ nonce ‖ ciphertext+tag, taille = bucket exact
C'est le schéma KEM-DEM canonique (celui du draft SP 800-227 du NIST sur l'usage des KEM) : le secret encapsulé n'est jamais utilisé directement comme clé mais passe par un KDF avec séparation de domaine — exactement ce que le NIST prescrit. Le padding par buckets avec tirage aléatoire chunké à 65 536 octets neutralise à la fois l'analyse de taille côté serveur et le plafond WebCrypto. La clé étant fraîche à chaque encapsulation (une par action), le nonce aléatoire 24 B est très largement au-delà du nécessaire — aucune possibilité structurelle de réutilisation nonce/clé.
Enveloppe transport DEV-010 (envelope/client.ts) :
encapsulate(pub serveur) → ss
requête : HKDF(ss, salt="ENVELOPE_REQUEST_v1", info="aead") → clé sens client→serveur
réponse : HKDF(ss, salt="ENVELOPE_RESPONSE_v1", info="aead") → clé sens serveur→client
ChaCha20-Poly1305 RFC 8439 (nonce 12 B) par sens
Les clés directionnelles distinctes dérivées du même secret par salts différents éliminent tout risque de réflexion requête/réponse et tout partage de flux nonce entre les deux sens. Le rejet silencieux sur mauvais keyID (« réponse opaque, oracle d'erreur évité », commentaire ligne 113) montre une prise en compte des oracles côté conception. Bonne architecture.
Établissement de session PQXDH (lib/crypto/pqxdh/) : trousseau hybride X25519 + ML-KEM-768 (3 keypairs, DEV-009 Scope γ), avec prékeys signées ML-DSA-65 (prekey-bundle.ts) et rotation SPK à 7 jours (SPKRotationManager.ts:35). Point fort décisif pour la CSPN : l'hybridation classique + post-quantique est exactement la posture exigée par l'ANSSI (avis sur la migration post-quantique : le PQ ne doit pas remplacer mais s'ajouter à la cryptographie éprouvée tant que le recul manque). AKARUP est alignée à la fois sur la doctrine ANSSI et sur l'état de l'art industriel (Signal PQXDH, Apple PQ3).
Chiffrement de groupe (group-message-cipher.ts) : kGroup aléatoire 32 B (getRandomValues), chiffrement XChaCha par message avec nonce frais, métadonnées liées à la signature via un hash canonique à séparateurs null-byte (anti-ambiguïté de concaténation) incluant group_id, message_id, device_id, ciphertext, nonce et timestamp — le ciphertext et le nonce étant dans le hash signé, toute altération est détectée (construction sign-the-ciphertext propre).
3.4 Dérivation d'identité déterministe (BIP39) et séparation de domaine
IdentityDerivationService.ts reproduit recovery.GeneratePQIdentityFromSeed (Go) :
mnémonique 24 mots (256 bits, entropie getRandomValues) → seed BIP39 64 B
HKDF-SHA-256(seed, salt="AKARUP-recovery-v1", info="x25519-v1") → 32 B = clé privée X25519
HKDF-SHA-256(seed, salt="AKARUP-recovery-v1", info="mldsa-v1") → 32 B = ξ pour ml_dsa65.keygen
HKDF-SHA-256(seed, salt="AKARUP-recovery-v1", info="mlkem-v1") → 64 B = d‖z pour ml_kem768.keygen
Les tailles de seeds correspondent exactement aux entrées de keygen des FIPS 203 (§7.1, d et z de 32 B) et FIPS 204 (§6.1, ξ de 32 B). Les trois branches sont indépendantes par info distincts — la compromission d'une clé dérivée ne donne aucune prise sur les autres. La conformité byte-pour-byte avec le Go est garantie par les 21 vecteurs d'interopérabilité (M-Test-Interop-Go), ce qui constitue une preuve de correction d'implémentation bien plus forte que des tests unitaires isolés.
Séparation de domaine — inventaire exhaustif : 15 préfixes versionnés relevés dans le code actif (AKARUP/bus/v1, AKARUP/group-msg/v2, AKARUP/msg-action/v1, AKARUP/device-cert/v1, AKARUP/blob/capability/v1, AKARUP/blob/chunk/, AKARUP/blob/merkle-leaf/v1, AKARUP/blob/merkle-node/v1, AKARUP/blob/nonce/v1, AKARUP/blob/notification-sig/v2, AKARUP/media/nonce/v1, AKARUP/pairing/fingerprint/v1, AKARUP/pairing/masterkey-wrap/v1, AKARUP/kdf/argon2id/v2/, AKARUP/secure-store/user-namespace/v1/). Chaque contexte cryptographique est isolé et versionné : une signature ou une clé produite dans un contexte est inutilisable dans un autre, et chaque format peut évoluer indépendamment. C'est une des meilleures disciplines de séparation de domaine que l'on puisse présenter à un évaluateur.
Hygiène mémoire : 40 points d'effacement défensif (fill(0)) relevés sur les matériaux sensibles — KEK (password-wrap.ts:164/239/245), MasterKey (password-wrap.ts:276), wrapKeys de pairing (wrap.ts:64/88), clé cachée du PasswordManager (PasswordManager.ts:278) — auxquels s'ajoute le cleanBytes interne de noble sur les intermédiaires de signature. Les limites de l'effacement mémoire en JavaScript (GC, copies immuables) sont connues et devront être mentionnées dans la cible de sécurité, mais l'effort systématique est réel.
3.5 Points de conformité NIST à nuancer (pour la cible de sécurité CSPN et le marketing)
Conformément au principe fondateur d'honnêteté totale marketing/technique, voici les cinq nuances exactes entre « conforme NIST » et ce que le code fait réellement. Aucune n'est un défaut de sécurité ; toutes doivent être formulées correctement dans la documentation.
a) Conformité algorithmique ≠ validation FIPS 140-3. @noble/post-quantum implémente les FIPS 203/204 finaux (vérifié : framing Algorithm 2, hedging, tailles), mais n'est pas — et aucune bibliothèque JavaScript n'est — un module validé CMVP. La CSPN n'exige pas de validation FIPS (l'ANSSI évalue l'implémentation elle-même, et les 21 vecteurs d'interop sont un excellent support), mais le vocabulaire marketing doit être « implémente ML-KEM-768 (FIPS 203) et ML-DSA-65 (FIPS 204) », jamais « FIPS-validated » ni « certifié NIST ».
b) Keygen dérivé de seed vs RBG approuvé. Les FIPS 203/204 exigent, pour une implémentation validée, que les seeds de keygen proviennent d'un RBG approuvé à la frontière du module. Chez AKARUP, elles proviennent de HKDF(seed BIP39), dont l'entropie initiale (256 bits) vient de getRandomValues (CSPRNG OS). La chaîne d'entropie est cryptographiquement saine et le keygen déterministe est la propriété qui rend possible la recovery BIP39 portable — c'est un choix d'architecture assumé, pas une faiblesse. À documenter explicitement dans la cible de sécurité : « keygen déterministe par dérivation HKDF d'une seed maîtresse de 256 bits d'entropie OS », avec la justification recovery.
c) Pré-hachage SHA-256 + pure ML-DSA (le point technique le plus substantiel). Le code ne signe jamais les messages bruts : il signe systématiquement sha256(canonical) via pure ML-DSA.Sign (group-message-cipher.ts:223-224, device-certification.ts:106-107, StorageServiceClient.ts:292, manifestCanonical.ts:88, accountRecoveryService.ts:125). Le FIPS 204 prévoit pour ce cas le mode HashML-DSA (§5.4), qui encode l'OID du hash dans le message signé ; pré-hacher en dehors de ce mode a deux conséquences : (1) la sécurité de la signature se rabat sur la résistance aux collisions du pré-hash — SHA-256 offre 128 bits de résistance aux collisions, sous la catégorie 3 (~192 bits) visée par ML-DSA-65 ; (2) l'absence de framing OID signifie qu'un même digest signé vaut pour tout message y menant.
En pratique le risque est faible : les messages signés sont des structures internes canoniques à préfixe de domaine et faible malléabilité (pas des documents fournis par un adversaire), et une attaque par collision exigerait de construire deux structures canoniques valides en collision SHA-256. Trois options, par ordre de préférence :
- Documenter et assumer dans la cible de sécurité (coût nul, position défendable : « signatures sur digests SHA-256 de structures canoniques à domaine séparé, sécurité collision 128 bits »).
- Passer les pré-hash en SHA-512 (déjà disponible via
@noble/hashes), ce qui remonte la résistance collision à 256 bits, au-dessus de la catégorie 3 — mais casse l'interop : à synchroniser avec le Go et les vecteurs, donc à traiter comme un sprint versionné (/v2→/v3des domaines concernés). - Utiliser le mode
prehashde noble (HashML-DSA complet avec OID) — conformité maximale, même contrainte d'interop.
La décision mérite d'être prise avant le dépôt CSPN, car changer après coup invalide les vecteurs.
d) XChaCha20-Poly1305 n'est pas un algorithme NIST. L'AEAD approuvé NIST est AES-GCM (SP 800-38D) ; ChaCha20-Poly1305 est un standard IETF (RFC 8439) accepté par le référentiel ANSSI ; XChaCha (nonce étendu 24 B) est resté au stade de draft CFRG. Le choix est techniquement excellent (constant-time naturel, marge nonce énorme, aligné Signal/WireGuard) et sans impact CSPN — mais interdit de revendiquer « 100 % algorithmes NIST ». Formulation correcte : « primitives post-quantiques NIST (FIPS 203/204), chiffrement authentifié état de l'art (famille ChaCha20-Poly1305) ». Noter que l'enveloppe DEV-010 utilise, elle, le ChaCha20-Poly1305 RFC 8439 standard — distinction à conserver dans la doc.
e) Racine d'entropie unique. Toute la pile (keygens, nonces, seeds hedgées ML-DSA, entropie BIP39, salts) repose sur crypto.getRandomValues. C'est correct (délégation aux CSPRNG OS), mais cela fait du chargement du polyfill un point de défaillance unique → renforce C3 en priorité haute.
3.6 Synthèse conformité
| Exigence / recommandation | État | Référence code |
|---|---|---|
| ML-KEM-768 conforme FIPS 203 (tailles, seed d‖z, KEM-DEM via KDF) | ✅ | mlkem.ts, payload-encrypt.ts |
| ML-DSA-65 conforme FIPS 204 (tailles, framing Alg. 2, hedged) | ✅ | 11 sites d'appel + source noble vérifié |
| Hybridation classique+PQ (doctrine ANSSI) | ✅ | pqxdh/identity.ts (X25519 + ML-KEM-768) |
| Séparation de domaine systématique et versionnée | ✅ | 15 préfixes AKARUP/* |
| Clés directionnelles distinctes (enveloppe) | ✅ | salts REQUEST/RESPONSE |
| Interop prouvée TS↔Go | ✅ | 21 vecteurs byte-pour-byte |
| Zeroization des matériaux sensibles | ✅ (limites JS à documenter) | 40 sites fill(0) |
| Rotation des prékeys signées | ✅ (7 jours) | SPKRotationManager.ts |
| Pré-hachage SHA-256 vs HashML-DSA | ⚠️ décision à formaliser avant CSPN | 3.5.c |
| Profil Argon2id aligné sur le module natif disponible | ⚠️ à migrer vers Β | C8 |
| Hoist du polyfill CSPRNG | ⚠️ | C3 |
| Vocabulaire « conforme FIPS » vs « validé FIPS » | ⚠️ règle de rédaction | 3.5.a |
Conclusion de la section : la pile post-quantique est solide, correctement paramétrée, et son architecture (KEM-DEM, hybridation, séparation de domaine, interop prouvée) est au niveau attendu pour une évaluation CSPN. Les quatre points ⚠️ sont tous des décisions de documentation ou des durcissements à coût maîtrisé, pas des vulnérabilités — le plus structurant étant le choix 3.5.c (pré-hachage), à trancher tant que les vecteurs d'interop peuvent encore évoluer.
4. Récapitulatif des actions recommandées
| # | Action | Priorité | Effort estimé |
|---|---|---|---|
| C1 | Purge du cluster legacy V1 + retrait crypto-js/@types/crypto-js | Haute | 1 sprint court |
| C8 | Migration Argon2id profil Α-light → Β (natif en place, re-wrap au login) | Haute | 0,5–1 jour + décision web |
| C3 | Hoist react-native-get-random-values dans lib/polyfills.ts | Haute (relevée) | Trivial |
| 3.5.c | Décision pré-hachage (documenter / SHA-512 / HashML-DSA) avant dépôt CSPN | Haute (décision) | Décision + éventuel sprint interop |
| C2 | Suppression des 4 .bak + *.bak dans .gitignore | Moyenne | Trivial |
| C4 | Stage test en CI GitLab + vecteurs interop vendorés ou chemin configurable | Moyenne | 0,5 jour |
| 3.5.a/d | Règles de vocabulaire « conforme FIPS 203/204 » / « AEAD état de l'art » dans la doc marketing et la cible de sécurité | Moyenne | Rédaction |
| C5 | Correction des 5 erreurs eslint + passe --fix sur les warnings | Basse | 1 h |
| C6 | Purge au boot des .tmp automations orphelins | Basse | 1 h |
C2, C3 et C5 peuvent être regroupés dans le même commit d'hygiène que C1. Le workflow CSPN (tsc --noEmit + vitest run) devra être repassé après la purge C1 pour confirmer qu'aucune chaîne d'import résiduelle ne casse.
5. Environnement d'audit
- Node/npm :
npm cipropre (0 vulnérabilité bloquante remontée à l'install) - TypeScript ~5.9.2, Vitest ^4.0.17, ESLint ^9.25.0 (config
eslint-config-expo) - Bibliothèques crypto effectivement installées :
@noble/post-quantum0.5.4,@noble/ciphers2.1.1,@noble/hashes2.0.1,@noble/curves1.9.7,@scure/bip392.2.0 - Durée vitest : 24,7 s (888 tests)
- Copie auditée strictement identique au HEAD
e3a4483après restauration des dossiers exclus (vérifié viagit status: zéro fichier modifié)