← Seguridad

Audit du code source — application mobile pq-akarup-mobile

Claude (Anthropic) · 2026-07-21

Los análisis publicados aquí son generados por asistentes de IA (Claude, ChatGPT, Gemini) a los que sometemos el código de la aplicación para una revisión orientada a la seguridad y la criptografía poscuántica. Se publican sin ninguna modificación — sin cortes ni reformulaciones — y se actualizan regularmente. No constituyen una auditoría de seguridad formal ni sustituyen una evaluación independiente.

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érificationRésultat
npx tsc --noEmit0 erreur
npx vitest run888 tests verts, 1 skipped, 62 fichiers
Parité i18n fr/en708 clés = 708 clés, zéro manquante dans les deux sens
ESLint5 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 buildEffectiveSalt avec le préfixe de domaine AKARUP/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 dans payload-encrypt.ts (fix M-MEDIA-V1 PAQUET 3).
  • Singleton IdentityService : clearIdentity() (logout léger multi-utilisateur) et resetIdentity() remettent bien isInitialized = false.
  • Fuites de secrets dans les logs : aucune. Les console.log d'AuthFactory et CompleteRegisterServiceV2 dé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 par lib/config.ts avec 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é et core.hooksPath configuré (recommandation antérieure appliquée).
  • Stockage : JWT en mémoire (zustand), pas de persistance AsyncStorage ; secrets via SecureStore/EncryptedStore ; useSignupFlowStore explicitement RAM-only.
  • Math.random restants : tous légitimes — identifiants locaux non cryptographiques (ids d'événements/notifs de sync), positions de vérification BIP39 (documenté non-sensible dans Bip39Service.ts:303), ou code mort (C1). Le deviceIdOpaque du bus passe bien par getRandomValues (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é par PreRegisterServiceV2/CompleteRegisterServiceV2)
  • src/services/LoginService.ts (V1, remplacé par LoginServiceV2)
  • lib/services/cryptoService.ts (importé uniquement par les V1 morts et le barrel lib/services/index.ts)
  • src/services/BlobCryptoService.ts
  • src/services/ChunkedUploadService.ts
  • src/managers/sync/BlobSyncManager.ts
  • src/services/MultiDeviceSyncService.ts
  • src/hooks/useBlobUpload.ts
  • src/hooks/useBlobDownload.ts

Ce cluster pose trois problèmes pour l'évaluation ANSSI :

  1. Dépendance crypto-js : bibliothèque non auditée, redondante avec @noble/hashes déjà présent et utilisé partout ailleurs. Un évaluateur la relèvera même si elle est morte.
  2. Hash non-standard : BlobCryptoService._hashBlobData (ligne 606) fait CryptoJS.SHA256(bytesToHex(data)) — un SHA-256 de la chaîne hexadécimale ASCII, pas des octets. Ce hash n'interopérerait jamais avec un sha256.Sum256(data) Go. Aucun bug en production puisque le code est mort, mais c'est un piège de réutilisation accidentelle.
  3. Ordre d'arguments ML-DSA inversé (découvert lors de l'analyse crypto, section 3) : cryptoService.ts:202 appelle ml_dsa65.sign(privateKey, messageBytes) — l'ordre de l'ancienne API noble. L'API 0.5.4 installée est sign(msg, secretKey, opts?). Les deux arguments étant des Uint8Array, 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:17
  • import/namespace sur la référence calculée Lucide[name] dans src/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.local est 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/ (dont expo-akarup-argon2 et expo-akarup-quickjs) et __mocks__/, alors que le package.json référence file:./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 : inclure modules/ et __mocks__/, ou fournir un git 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

PrimitiveRéférenceImplémentationParamètres vérifiés dans le codeConforme
ML-KEM-768FIPS 203 (final)@noble/post-quantum 0.5.4ek 1184 B, dk 2400 B, ct 1088 B, ss 32 B (mlkem.ts:70-73) ; keygen seed 64 B = d‖z
ML-DSA-65FIPS 204 (final)@noble/post-quantum 0.5.4pk 1952 B, sk 4032 B, sig 3309 B (IdentityDerivationService.ts:100-104, group-message-cipher.ts:68) ; keygen seed ξ 32 B
XChaCha20-Poly1305draft-irtf-cfrg-xchacha@noble/ciphers 2.1.1clé 32 B, nonce 24 B, tag 16 B (xchacha.ts:29-36)⚠️ hors NIST, voir 3.5.d
ChaCha20-Poly1305RFC 8439@noble/ciphers 2.1.1nonce 12 B, enveloppe DEV-010 uniquement (chacha-std.ts)✅ IETF
HKDF-SHA-256/512RFC 5869 / SP 800-56C rev2@noble/hashes 2.0.1extract-then-expand, salt + info systématiques (hkdf.ts)
Argon2idRFC 9106phc-winner-argon2 natif vendoredm=16 MiB, t=3, p=1 (profil Α-light)⚠️ voir C8
BIP39@scure/bip39 2.2.024 mots = 256 bits d'entropie + checksum, entropie via getRandomValues
X25519 / Ed25519RFC 7748 / 8032@noble/curves 1.9.7volet 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 :

  1. 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 »).
  2. 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/v3 des domaines concernés).
  3. Utiliser le mode prehash de 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ÉtatRé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ée15 préfixes AKARUP/*
Clés directionnelles distinctes (enveloppe)salts REQUEST/RESPONSE
Interop prouvée TS↔Go21 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 CSPN3.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édaction3.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

#ActionPrioritéEffort estimé
C1Purge du cluster legacy V1 + retrait crypto-js/@types/crypto-jsHaute1 sprint court
C8Migration Argon2id profil Α-light → Β (natif en place, re-wrap au login)Haute0,5–1 jour + décision web
C3Hoist react-native-get-random-values dans lib/polyfills.tsHaute (relevée)Trivial
3.5.cDécision pré-hachage (documenter / SHA-512 / HashML-DSA) avant dépôt CSPNHaute (décision)Décision + éventuel sprint interop
C2Suppression des 4 .bak + *.bak dans .gitignoreMoyenneTrivial
C4Stage test en CI GitLab + vecteurs interop vendorés ou chemin configurableMoyenne0,5 jour
3.5.a/dRègles de vocabulaire « conforme FIPS 203/204 » / « AEAD état de l'art » dans la doc marketing et la cible de sécuritéMoyenneRédaction
C5Correction des 5 erreurs eslint + passe --fix sur les warningsBasse1 h
C6Purge au boot des .tmp automations orphelinsBasse1 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 ci propre (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-quantum 0.5.4, @noble/ciphers 2.1.1, @noble/hashes 2.0.1, @noble/curves 1.9.7, @scure/bip39 2.2.0
  • Durée vitest : 24,7 s (888 tests)
  • Copie auditée strictement identique au HEAD e3a4483 après restauration des dossiers exclus (vérifié via git status : zéro fichier modifié)