sec: audit complet 2026-08-03/04 — 46 correctifs, retrait de Google ML Kit - #14
Merged
Conversation
Audit manuel complet (lecture integrale, sans agent externe) : securite, coherence, patterns, structure. Rapport detaille dans CLAUDE-SECURITY-20260730-180405/AUDIT-MANUEL-20260803.md (non versionne). ELEVE - ML Kit / Play Services / telemetrie Google CCT embarques via mobile_scanner : declares honnetement (AntiFeatures NonFreeDep F-Droid, PRIVACY FR+EN §6/§10, THIRD_PARTY_NOTICES). Origine de ACCESS_NETWORK_STATE, absente du manifeste source. Le retrait de la dependance reste une decision produit. - SEC F6 : les deux coffres redivergeaient en taille des le franchissement d'un barreau (64 Kio), definitivement pour un leurre factice. Deni plausible au repos casse sur un simple `ls -l`. Realignement au deverrouillage (vrai leurre) et a l'ecriture (leurre factice), avec garde vitale sur le drapeau pt_decoy_configured. MOYEN - _unlockInternal etait la seule des trois fonctions d'ouverture sans try/catch (asymetrie introduite par F3 v2.4.4) : un fichier malforme laissait un indicateur de progression permanent, sans message ni issue. Garde par emplacement en plus : un leurre corrompu ne doit pas condamner le principal. - Parametres Argon2id ecrits dans les 3 formats mais jamais relus ; le .ptbak les lisait pour la derivation et bâtissait l'AAD sur les constantes. Un bump de owaspMobile2024 aurait rendu coffres, instantanes et sauvegardes indechiffrables en silence. Lecture via KdfParams.fromFileOrNull (bornes anti-DoS partagees), report des parametres a chaque reecriture. - deleteDecoyVault gardait le refus opaque que SEC F12 avait retire de deleteVault : meme traitement gracieux desormais (DecoyDeleteOutcome). - Chemin Heritage : 3 tampons de clair jamais effaces + disable() supprimait pt_heir.enc sans ecrasement alors que deleteVault le dechiquette. Shred unifie sur VaultService.shredFileSync (3 implementations coexistaient). - USE_FINGERPRINT reste reinjectee par biometric_storage : SEC F12 n'avait rien retire. Commentaire rectifie (minSdk 24, pas 26). FAIBLE - Branche v3 morte dans le deverrouillage biometrique + commentaire mensonger. - _shredShareCache : appel « au verrouillage » annonce mais inexistant ; purge avant/apres les deux exports. - shouldShowHeirOption() construit dans build() avec effet de bord d'ecriture. - .ptbak : count et exportedAt en clair hors AEAD, retires. - unlock() rend desormais UnlockResult.busy au lieu de « mot de passe incorrect » sur refus de concurrence. - network_security_config : bloc domain-config inerte retire. - MainActivity exported=false (les alias portent le LAUNCHER). - Camouflage : limite « Reglages > Applications » documentee. Trouve en relisant mes propres correctifs : le chemin biometrique ne peuplait pas le cache meta, ce qui privait silencieusement SEC F18 de sa migration d'etiquette pour quiconque n'ouvre qu'a l'empreinte. Tests : 126 -> 139. Le test de non-regression des parametres KDF a ete valide en reintroduisant temporairement le defaut (il echoue, et lui seul). flutter analyze : 0 issue. A TESTER SUR APPAREIL avant release : lancement par alias apres exported=false, sortie de la calculatrice, deverrouillage d'un coffre existant. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
AUDIT CROISE GEMINI — 6 findings, 5 confirmees apres relecture du code, 1 partiellement fausse (FLAG_SECURE notes : les instantanes systeme sont deja proteges par SEC F3, seul l'arbitrage Knox assume subsiste). CRITIQUE - Changement de mot de passe maitre : `_key` passait a la nouvelle cle AVANT l'ecriture. Si `_saveVaultV4` echouait (disque plein, E/S), la session gardait la cle neuve face a un fichier reste sous l'ancienne, avec les metadonnees anciennes en cache. Le premier ajout d'entree scellait l'incoherence : NI l'ancien NI le nouveau mot de passe n'ouvraient plus le coffre. Restauration de la cle precedente, avec point de bascule sur le renommage atomique et non sur l'ecriture du sel (qui n'est pas porteuse en v4). ELEVE - `Stopwatch` (CLOCK_MONOTONIC) ne compte PAS la veille profonde : apres deux heures ecran eteint, le verrouillage automatique ne se declenchait pas. On retient desormais le plus grand des deux temps ecoules, `elapsedRealtime` (via le canal RASP) et `Stopwatch` conserve en second temoin synchrone. - `_passwordMatchesPrimaryInternal` restaurait l'etat du coffre dans son `finally` SANS CONDITION : un `lock()` concurrent — mise en arriere-plan, minuterie, ou MODE PANIQUE — etait annule et le coffre rouvert en memoire. Compteur `_lockGeneration` + resultat transitant par une variable (un `finally` ne peut pas corriger une valeur deja rendue). - `pass_tech_export.json` (tous les mots de passe EN CLAIR) survivait dans le cache si le processus etait tue pendant le selecteur de partage. Trou de mon propre correctif SEC F8 du matin : la purge ne couvrait que `share_plus/`. - FilePicker recopie le fichier importe dans le cache et ne le supprimait jamais : un export Bitwarden en clair y restait a demeure. Dechiquetage + `clearTemporaryFiles()`, sous garde stricte verifiant que le chemin est bien une copie en cache — effacer le document source de l'utilisateur serait irreparable. RETRAIT DE GOOGLE ML KIT `mobile_scanner` entrainait Play Services, ML Kit et le transport de telemetrie `datatransport`, d'ou une permission ACCESS_NETWORK_STATE que le depot ne declarait nulle part. Une pile Google fermee dans une app qui annonce « aucun tracker ». La dependance est retiree. Le scan par camera est remplace par l'extraction du secret depuis une URI `otpauth://` collee dans le champ 2FA — la plupart des services l'affichent en clair sous le QR code. Pas de bibliotheque tierce, pas de lecture programmee du presse-papier (bloquee par Knox de toute facon). PREUVE sur le manifeste FUSIONNE : les permissions passent de INTERNET, USE_BIOMETRIC, USE_FINGERPRINT, CAMERA, ACCESS_NETWORK_STATE a INTERNET, USE_BIOMETRIC, USE_FINGERPRINT Zero occurrence de gms / mlkit / datatransport / firebase. Propage : PRIVACY.fr.md + PRIVACY.md (§6, §10), THIRD_PARTY_NOTICES.md, THREAT_MODEL.md §5.2, android/expected-permissions.txt, anti-fonctionnalite NonFreeDep retiree de fdroiddata, 21 chaines l10n orphelines supprimees, commentaires de build qui citaient la dependance disparue. Tests : 139, tous verts. flutter analyze : 0 issue. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
- `tool/generate_icon.dart` reformate. Sans lui, la nouvelle barriere `dart format --output=none --set-exit-if-changed .` ajoutee a ci.yml echouait des le premier run : c'est le seul fichier du depot qui n'etait pas au format. - `GeneratedPluginRegistrant.swift` (macOS) regenere apres le retrait de mobile_scanner. - `audit/` ignore : sorties de l'audit IA externe, pas du code source. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Avant : neuf sections de ListTile nus, bord a bord, titres 12 sp en bleu primaire. Le choix du theme et « Tout supprimer » avaient le meme poids visuel. Apres, en reprenant le vocabulaire deja etabli par about_screen : - marges 16/24/16/40 identiques ; - chaque reglage sur sa propre carte (rayon + bordure du theme, donc correct en clair comme en sombre, sans couleur codee en dur) ; - titres de section en titleMedium (16 sp au lieu de 12), gras, en onSurfaceVariant. Plus gros qu'« A propos » a dessein : neuf sections se parcourent du regard, alors qu'« A propos » se lit d'un trait. METHODE — la decoration est appliquee a la LISTE (`_decorate`), pas ecrite sur chaque tuile. Les ~400 lignes de onTap, dialogues et FutureBuilder de cet ecran ne sont pas modifiees : envelopper chaque tuile a la main aurait ete l'occasion d'egarer un branchement sans que l'analyse ni les tests ne le signalent. Un cas ne peut pas etre decore automatiquement : « Reveler l'application » ne s'affiche que si le camouflage est actif. L'envelopper poserait une carte VIDE sur l'ecran de tout le monde. Marque `_Undecorated`, il gere sa carte a l'interieur de sa condition. flutter analyze : 0 issue. Rendu NON verifie par moi (FLAG_SECURE bloque screencap ; uiautomator exigerait le mot de passe maitre) — a valider a l'oeil. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Le bleu du damier du logo est #0B5FC7. Il n'est PAS code en dur : sur le fond sombre #0D1117 il ne donne qu'environ 3,4:1 de contraste, sous l'exigence AA de 4,5:1. `cs.primary` rend ce meme bleu en theme clair et bascule sur #58A6FF en sombre. Meme raisonnement que le correctif U5 v2.4.4 sur les `Colors.grey` codes en dur. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Defaut trouve EN USAGE REEL, pas en audit — Patrice a active le mode panique pour le tester, puis n'a plus retrouve son mot de passe maitre. « Reveler l'application » n'existait que dans les Reglages, donc DERRIERE le deverrouillage. Consequence : qui active la panique et oublie son mot de passe se retrouve avec une application definitivement deguisee en calculatrice sur son lanceur, sans aucun moyen de revenir en arriere — alors que le camouflage est reversible par conception. CalculatorActivity documentait deja ce piege pour un CODE NUMERIQUE oublie (« le bouton Reveler vit dans les Reglages, devenus inatteignables »). Personne n'avait vu qu'il vaut a l'identique pour un MOT DE PASSE oublie, cas autrement plus frequent. Le bouton apparait desormais sur l'ecran de deverrouillage, uniquement quand le camouflage est actif. Aucune fuite pour le deni plausible : pour lire cet ecran il faut deja etre sorti de la calculatrice, donc le camouflage est tombe. Et l'action ne touche QUE l'icone du lanceur — elle n'ouvre rien, ne dechiffre rien, ne revele aucune donnee. Chaines l10n reutilisees (panicRevealTitle / panicRevealSnack), aucun ajout ARB. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2e passe Gemini 3.1-pro sur le code ecrit dans la journee — celui que seul moi avais relu. 15 findings, 13 confirmees apres relecture du code. GRAVE - Anti-hameconnage contourne sur Firefox : le mapping lisait `url_bar_title`, qui contient le TITRE DE PAGE et non l'URL. Un site pirate declarant `<title>mabanque.fr</title>` faisait rendre le verdict « ok » par la protection elle-meme. Le garde-fou TLD ne visait que les titres ACCIDENTELS, pas un titre choisi expres. Champ retire. - Compteur d'echecs incremente APRES la derivation : tuer l'app pendant les ~2 s d'Argon2id annulait l'essai, supprimant de fait le verrouillage progressif. Vaut pour le mot de passe MAITRE (non signale par Gemini, trouve en verifiant) ET pour l'heritier. L'essai est desormais provisionne AVANT, et efface par un deverrouillage reussi. Plancher a 1 si la reservation echoue — sinon une erreur de stockage desarmait le verrouillage. - Export en clair et suppression du coffre sans re-authentification. SEC F10 v2.5.2 avait ajoute ce controle au changement de mot de passe en nommant la menace (« acces momentane a une session deverrouillee ») ; il n'avait ete propage ni a l'un ni a l'autre — alors que l'export est PIRE, il emporte tout hors de l'appareil. MOYEN - 5 tampons `Uint8List.fromList(utf8.encode(...))` : `utf8.encode` rend deja un Uint8List, l'enveloppe creait une COPIE FANTOME du coffre en clair ou du mot de passe, jamais effacee. `vault_storage.dart` documente ce piege et l'evite depuis la v2.5.x — la regle n'avait pas ete propagee aux 5 autres sites. - 3 champs Notes sans `enableSuggestions:false` / `autocorrect:false` : Gboard apprenait et synchronisait phrases de recuperation et codes de secours. `PasswordTextField` posait deja ces drapeaux ; les TextField bruts non. - Vue heritier : mots de passe affiches en clair. Masques derriere un oeil, comme partout ailleurs dans l'app. FAIBLE - Le tampon du mot de passe MAITRE n'etait pas vide au `dispose` de l'ecran de deverrouillage — seul champ sensible de l'app a ne pas le faire (cf. B8/B9). - `gradlew assemble` / `build` contournaient la garde keystore de SEC F15 : SEC-R2 avait resserre le filtre sur « assembleRelease », or les taches generiques construisent la release sans contenir ce mot. Release signee avec la cle de DEBUG. ECARTEES : FLAG_SECURE relache sur les notes (arbitrage assume, deja traite) ; reararmement asynchrone de FLAG_SECURE (reel mais deja arbitre, correctif natif a evaluer separement). MOTIF DOMINANT, 3e fois de la journee : la parade existait deja dans le depot et n'avait pas ete propagee a son jumeau. flutter analyze : 0 issue. 139 tests verts. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…egarde Deux garde-fous nes d'un incident reel du 2026-08-03 : un coffre a ete perdu definitivement. Enchainement : activation du mode panique pour un test → suppression de l'enrolement biometrique (SEC F4, voulu) → mot de passe maitre introuvable → desinstallation. Aucune sauvegarde n'existait. 1. AVERTISSEMENT AVANT LA PANIQUE SEC F4 justifiait la suppression de la biometrie par « le cout pour l'utilisateur legitime est faible, puisque le reenrolement exige de toute facon le mot de passe maitre ». Ce raisonnement suppose que l'utilisateur CONNAIT ce mot de passe — or quelqu'un qui ouvre a l'empreinte tous les jours ne le tape parfois plus depuis des mois. Pour lui, la panique est une porte a sens unique. Un ecran l'annonce desormais, uniquement si la biometrie est active (sinon il n'y a rien a perdre et ce serait du bruit). « Annuler » en position sure et focalise par defaut. 2. INCITATION A LA SAUVEGARDE Rien dans l'application ne suggerait de creer une sauvegarde chiffree, alors que c'est le SEUL moyen de recuperation : ni cloud, ni compte, ni sequestre. - a la creation du coffre : un dialogue pose l'enjeu. On ne propose PAS d'exporter — le coffre vient d'etre cree, il est vide. - sur l'accueil : un bandeau apparait des qu'il y a au moins une entree ET qu'aucune sauvegarde n'a jamais ete faite. Non masquable a dessein : un bandeau qu'on ecarte d'un geste est ecarte le premier jour et jamais revu, ce qui est exactement ce qui a coute un coffre. Il ne part qu'en agissant. - nouveau `BackupReminder` : ne conserve QUE la date, jamais le chemin ni la phrase secrete. Preferences ordinaires et non stockage securise — un horodatage n'est pas un secret et ne revele rien du contenu. - la suppression totale du coffre reinitialise la trace : un coffre recree repart avec le rappel actif, ses futures entrees n'etant couvertes par aucune sauvegarde anterieure. 6 chaines FR/EN ajoutees (parite verifiee : 516 = 516, aucun orphelin). NB d'outillage : les deux premieres tentatives d'ajout de ces chaines ont produit un ARB casse puis un litteral « \n » affiche tel quel — le passage par un heredoc shell mangeait les antislashs. Corrige en ecrivant le script en FICHIER, avec assertions de relecture sur les sauts de ligne. flutter analyze : 0 issue. 139 tests verts. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Signale en usage reel : au demarrage, annuler l'invite biometrique la faisait revenir aussitot, en boucle, sans jamais laisser saisir le mot de passe maitre. Le bouton Retour n'y changeait rien — l'application etait inutilisable pour quiconque annule l'invite. ENCHAINEMENT L'invite est un dialogue systeme : elle met l'application en arriere-plan. A l'annulation, on revient au premier plan, `_handleLifecycle` constate que le coffre est ferme et POUSSE un nouvel ecran de deverrouillage avec `pushAndRemoveUntil`, ce qui VIDE la pile — d'ou le Retour sans effet. Le nouvel ecran relance l'invite depuis son `initState`, et la boucle se referme. Ce bloc de `main.dart` vient de SEC F19 v2.5.4 et sert a RAMENER vers le deverrouillage depuis un autre ecran apres un verrouillage automatique. Il n'avait pas prevu le cas ou l'ecran cible est DEJA celui qui est affiche — encore le meme motif que le reste de la journee. CORRECTIF, a deux niveaux, chacun suffisant seul : 1. `UnlockScreenState.estAffiche` : `main.dart` ne pousse plus un ecran de deverrouillage quand il y en a deja un. Benefice annexe : une saisie en cours n'est plus effacee au retour au premier plan. 2. `_inviteBioDejaTentee` : l'invite automatique n'est tentee qu'une fois par cycle de verrouillage. Annuler est une intention claire — « je veux taper mon mot de passe » — et elle est desormais respectee. Le bouton empreinte reste disponible pour relancer volontairement, et le drapeau est reinitialise par un deverrouillage reussi, pour que le cycle suivant repropose la biometrie. Au passage : le libelle du champ 2FA promettait encore « ou scanner QR », alors que le scanner a ete retire avec Google ML Kit. Remplace par « Collez l'URI otpauth:// ou la cle Base32 », FR et EN. flutter analyze : 0 issue. 139 tests verts. Parite i18n 516 = 516. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…plan Regression introduite par le correctif de la boucle d'invite biometrique (f086347). Trouvee en relisant mes propres correctifs. Le garde `UnlockScreenState.estAffiche` ne verifiait que l'EXISTENCE de l'ecran de deverrouillage, pas qu'il soit au premier plan. Or `_unlockAsHeir` empile `HeirViewScreen` PAR-DESSUS lui, et cette vue affiche les entrees dechiffrees de l'instantane d'heritage. Le garde repondait donc « ecran present, rien a faire » et laissait cette vue ouverte au retour au premier plan, alors que le comportement d'avant la refermait. `popUntil((r) => r.isFirst)` traite les deux cas : route au-dessus => fermee ; pas de route au-dessus => appel sans effet, donc ni boucle d'invite, ni saisie effacee. `isFirst` designe bien l'ecran voulu, `SplashGate` RENVOYANT l'ecran de deverrouillage comme widget enfant au lieu de le pousser. flutter analyze : 0 issue. 139 tests verts. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…de mot de passe Signale par Patrice en usage reel : apres avoir change son mot de passe maitre, le bouton empreinte avait disparu au lancement suivant, sans explication. Le comportement est CORRECT et necessaire : l'enveloppe biometrique scelle l'ANCIENNE cle finale, elle devient inutilisable des que la cle est rederivee. `changeMasterPassword` supprime donc l'enrolement (vault_setup.dart, slot primary uniquement — sur le leurre, le supprimer trahirait son existence). Ce qui manquait, c'est de le DIRE. L'ecran affichait « mot de passe change » et rien d'autre. C'est le meme motif que le mode panique corrige la veille : un effet de bord indispensable a la securite, mais silencieux, qui laisse l'utilisateur devant un comportement incomprehensible. Le message n'apparait que si la biometrie etait reellement active, et l'etat est releve AVANT l'appel — apres, l'information n'existe plus. flutter analyze : 0 issue. 139 tests verts. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Le fond etait le gris tres sombre que Material pose par defaut (`inverseSurface`). En theme clair il jurait avec le reste de l'application. Pose dans le THEME et non dans `SnackUtils` : les ~28 bandeaux du depot en heritent d'un coup, y compris la couleur du bouton d'action, qui serait devenue illisible sur fond bleu. Aucun site d'appel a modifier. Les couleurs viennent du SCHEMA de couleurs, jamais codees en dur. En theme clair, fond `primary` fonce et texte blanc ; en sombre, fond #58A6FF clair et texte #0D1117. Coder le bleu du damier en dur aurait reproduit l'erreur des `Colors.grey` corrigee en v2.4.4 (contraste ~3:1 en sombre, sous l'exigence AA). `_lightTheme` construit desormais son `ColorScheme` explicitement au lieu de passer par `colorSchemeSeed`, pour pouvoir en reutiliser les couleurs dans les themes de composants. Strictement equivalent : `colorSchemeSeed` fait exactement cet appel. Les messages d'ERREUR gardent leur fond rouge : ils posent explicitement `errorContainer`, ce qui ecrase le theme. Une erreur ne doit pas ressembler a une confirmation. Corollaire : l'icone de succes etait dessinee en `cs.primary`, la couleur qui devient le FOND. Passee en `cs.onPrimary`, sans quoi elle devenait invisible. flutter analyze : 0 issue. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Signale par Patrice : « il m'a demande des symboles et des chiffres ». En
verifiant, le vrai defaut etait ailleurs et plus grave.
CE QUI N'ALLAIT PAS
`PasswordStrengthService.score` n'etait appele QUE dans `setup_screen.dart`.
Les quatre autres points d'entree ne verifiaient que la LONGUEUR :
- changement du mot de passe maitre : 12 caracteres, point
- phrase secrete d'une `.ptbak` : 12 caracteres, point
- mot de passe du coffre leurre : 12 caracteres, point
- mot de passe heritier : 12 caracteres, point
On pouvait donc creer un coffre avec un mot de passe solide, puis le remplacer
par `aaaaaaaaaaaa` : accepte sans broncher, ni repetition detectee ni mot de
passe courant rejete. La porte d'entree etait gardee, aucune des autres portes
de la meme maison ne l'etait — et c'est la porte de service qui permet de
DEGRADER un coffre existant.
Le cas le plus grave etait la phrase du `.ptbak` : seul artefact NON lie au
materiel, donc seul attaquable hors ligne depuis une copie de fichier.
Par ailleurs `setupDecoyVault` n'avait AUCUNE garde au niveau du service, alors
que son jumeau `setupOrUpdateSnapshot` en avait une depuis toujours.
CE QUI CHANGE
`PasswordPolicy` : une regle, cinq appelants, plus deux gardes de service.
- 12 caracteres minimum (inchange, decision de Patrice)
- AUCUNE obligation de symbole, de chiffre ni de casse. Le NIST le
deconseille depuis SP 800-63B : ces regles poussent vers `Motdepasse2024!`.
A 12 caracteres, les minuscules seules donnent deja 56 bits.
- seuil d'entropie conserve (48 bits), qui rejette repetitions, suites et
mots de passe courants
- au moins 5 caracteres DISTINCTS — comble une limite decouverte en ecrivant
les tests : `entropyBits` compte le pool de CLASSES, pas les caracteres
employes, donc `abababababab` etait credite de 56 bits au lieu d'une
douzaine. Ce controle est pose dans `PasswordPolicy` et NON dans
`PasswordStrengthService`, dont le barème alimente aussi l'indicateur de
force et l'ecran d'audit : en changer le calcul modifierait
retroactivement le verdict rendu sur les entrees deja enregistrees.
Message d'erreur corrige : « variez majuscules, chiffres, symboles » laissait
croire a une obligation qui n'a jamais existe. Remplace par une consigne vraie
et actionnable. `setupErrorWeak`, devenue orpheline, est supprimee.
NON-REGRESSION VERIFIEE
- `PasswordPolicy` n'est appelee sur AUCUN chemin de deverrouillage : un mot
de passe existant qui ne respecterait pas la nouvelle regle ouvre toujours
le coffre. Aucun risque d'enfermement.
- l'import d'une `.ptbak` passe par `confirm: false`, hors du controle : une
vieille sauvegarde a phrase courte reste restaurable.
14 tests ajoutes, dont sept verifient qu'une phrase de passe HONNETE est bien
acceptee — une regle trop stricte est un defaut, pas une protection.
Suite complete : 153 tests verts. flutter analyze : 0 issue.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Signale par Patrice en usage reel : au lancement, annuler l'invite biometrique affichait « Echec biometrique ». Le service savait DEJA faire la difference. Le `switch` sur `AuthExceptionCode` existe depuis la v2.4.2 et son commentaire promet un « repli silencieux » pour `userCanceled`, `canceled` et `timeout`. Mais les trois cas rendaient `UnlockResult.wrongPassword` — la meme valeur qu'un echec reel. L'information etait calculee, puis jetee avant d'atteindre l'interface. Consequence : quelqu'un qui choisit de taper son mot de passe plutot que de poser son doigt lisait un message d'erreur sur l'ecran le plus sensible de l'application, et pouvait croire son empreinte defaillante. `UnlockResult.biometricCanceled` conserve la distinction jusqu'a l'ecran, qui rend simplement le champ de saisie, sans message. C'est ce que le commentaire de la v2.4.2 decrivait. Cinquieme occurrence du meme motif en deux jours : le mecanisme est correct, c'est ce qu'il TRANSMET qui ne l'est pas. Aucun audit statique n'a signale aucune des cinq — toutes viennent des tests sur appareil. flutter analyze : 0 issue. 153 tests verts. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Signale en usage reel : appuyer sur « Deverrouiller » sans rien avoir saisi ne produisait rien du tout. `_unlock()` sort en silence sur `pass.isEmpty`, donc l'appui etait avale — aucun message, aucun indice. Sur l'ecran d'entree de l'application, un bouton qui n'a aucune reaction laisse penser qu'elle est figee. Bouton grise plutot que message d'erreur : l'ecran affiche DEJA « Entrez votre mot de passe maitre » juste au-dessus du champ. Un message le repeterait a trois centimetres, et sous forme de reproche apres l'appui. Le bouton desactive dit la meme chose AVANT, selon la convention Material. `ValueListenableBuilder` sur le controleur plutot qu'un `onChanged` avec `setState` : `TextEditingController` EST deja un `ValueNotifier`, et seul le bouton se reconstruit au lieu de tout l'ecran a chaque frappe. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…efface Audit Gemini n3 (modele gemini-3.6-flash — les modeles pro renvoyaient des HTTP 503 en rafale). 6 findings, 3 confirmees apres relecture du code. PT-001 — EXPORT POTENTIELLEMENT CORROMPU (le plus important) `Share.shareXFiles` rend la main des que l'activite cible se TERMINE, ce qui ne signifie pas qu'elle a fini de LIRE. Une cible qui televerse en tache de fond — Drive, messagerie — lit encore apres notre retour. Or le `finally` dechiquetait aussitot la copie que `share_plus` place dans son cache, c'est-a- dire la source d'octets que la cible etait en train de lire. Resultat possible : une sauvegarde `.ptbak` corrompue, decouverte le jour de la restauration. Le raisonnement de SEC F8 v2.5.2 reste valable — cette copie en clair ne doit pas survivre indefiniment — mais purger a CET instant precis creait le defaut inverse. La purge est donc deplacee vers l'export SUIVANT, ou elle balaie les residus du precedent (appel deja en place avant le partage depuis le 2026-08-03). Notre propre fichier temporaire, lui, continue d'etre dechiquete immediatement : ce n'est pas celui que la cible lit. Residu borne dans le temps, sans jamais couper une lecture en cours. PT-MEM-003 — 6e tampon UTF-8 de mot de passe non efface `utf8.encode(password)` dans le controle de fuite HIBP. Meme motif que les cinq corriges le 2026-08-03 — manque a l'appel. Chemin sensible : l'utilisateur y soumet volontairement ses mots de passe, un par un. ECARTE APRES VERIFICATION PT-BRUTE-001 « la panique reinitialise le verrouillage anti-force-brute, d'ou force brute illimitee » : FAUX. `panic()` n'a qu'UN appelant, `settings_screen.dart:636`, donc derriere le deverrouillage. Ni tuile de reglages rapides, ni intent externe, ni code de panique a l'ecran de deverrouillage — la tuile et l'intent que la finding suppose n'existent pas. Qui atteint les Reglages a deja ouvert le coffre. La purge est par ailleurs deliberee (F15 v2.4.4) : un verrouillage survivant signalerait qu'une urgence vient d'avoir lieu. PT-002 : deja corrige le 2026-08-03 (purge en debut d'export). RESTE OUVERT, NON CORRIGE PT-MEM-001/002 : `encrypter.decrypt()` rend une `String` Dart, immuable donc ineffacable, sur les chemins HERITES v1/v2 (instantane heritier v1, `.ptbak` v1/v2). Reel. Corrigeable via `decryptBytes`, mais cela touche des chemins de dechiffrement herites qu'aucun test ne couvre et que l'appareil ne peut plus exercer — a traiter separement, pas en fin de lot. flutter analyze : 0 issue. 153 tests verts. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
1. STRING INEFFACABLE SUR LES CHEMINS HERITES (Gemini n3 PT-MEM-001/002) `encrypter.decrypt()` rend une `String` Dart : immuable, donc impossible a ecraser. Le coffre entier en clair restait en memoire jusqu'au ramasse-miettes sur deux chemins herites — instantane heritier v1 et `.ptbak` v1/v2. Remplace par `decryptBytes` + effacement en `finally`.⚠️ `allowMalformed: true` est REPRIS explicitement : c'est exactement ce que fait `Encrypter.decrypt` en interne (encrypt 5.0.3, `encrypter.dart:49`, verifie dans la source du paquet avant d'ecrire la moindre ligne). L'omettre aurait fait lever une exception la ou l'ancien code tolerait un octet malforme — une regression silencieuse sur des fichiers herites que plus aucun test ni appareil n'exerce. 2. FLAG_SECURE REARME DE FACON ASYNCHRONE (Gemini n2 PT-002) `suspendRelaxForBackground()` fait ce travail depuis SEC F3 v2.5.2, mais depuis Dart, a travers un canal de methode — donc APRES le retour de `onPause`. Or Android capture la vignette des applications recentes au moment du passage en arriere-plan. Une note ouverte pouvait donc s'y retrouver lisible. `MainActivity.onPause()` pose desormais le flag lui-meme, sur le thread UI, dans le cycle de vie natif, avant la capture. Plus de course.⚠️ Ce n'est PAS un retour a la v2.3.8. Le piege Knox documente en v2.3.9 vient de FLAG_SECURE pose a la CREATION de la fenetre, qui rend tout `clearFlags` ulterieur sans effet sur le presse-papier. Ici la fenetre existe depuis longtemps — la distinction est ce qui rend ce correctif possible. Il fallait deux informations distinctes cote natif : le relachement de l'editeur de notes passe aussi par `setSecure(false)`, et c'est justement dans cet etat qu'il faut securiser. D'ou `setSecureOnBackground`, qui transmet la PREFERENCE globale et non l'etat courant du flag. Se fier au dernier `setSecure` aurait laisse la note exposee — le defaut meme a corriger. La restauration reste a Dart (`resumeRelaxAfterBackground`) : au retour, mieux vaut un bref instant de trop securise qu'un instant de trop expose. 3. CAMOUFLAGE PARTIEL — non corrigeable `android:label` n'est pas modifiable a l'execution : Reglages > Applications affichera toujours « Pass Tech » en mode panique. Il faudrait un second APK avec un autre applicationId. Documente dans THREAT_MODEL.md §4. flutter analyze : 0 issue. 153 tests verts. Kotlin compile. A TESTER SUR APPAREIL — c'est la seule verification possible : - note en edition, bascule en arriere-plan, recents : vignette VIDE - la note doit rester copiable/collable au retour (non-regression Knox) Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
… filtres
Signale par Patrice : « tu n'as pas ajoute Email, Reseaux sociaux, Banque ? »
dans le menu Ajouter. La reponse est non — ce sont des CATEGORIES, pas des
TYPES — mais la question revele un vrai defaut d'affichage.
La rangee de filtres empile trois familles qui ne se comparent pas :
- la portee : Tous, Favoris
- le TYPE : Mot de passe, Note, Carte bancaire (decide des champs
du formulaire)
- la CATEGORIE : Web, Email, Banque, Reseaux sociaux… (simple rangement)
Toutes s'affichaient a l'identique. On lisait donc « Cartes bancaires » et
« Cartes » comme deux variantes de la meme chose, alors que la premiere filtre
un type et la seconde une categorie. Meme l'auteur de l'application n'y
retrouvait plus la distinction — un nouvel utilisateur encore moins.
Deux traits fins aux frontieres de groupe. Choisi contre les deux alternatives :
un en-tete sur une seconde rangee coute ~40 px sur la liste des entrees, soit
une entree visible en moins en permanence sur l'ecran le plus consulte ; un
libelle dans la rangee repousse les categories vers la droite et allonge le
defilement. Le trait ne coute ni hauteur ni appui.
Frontieres CALCULEES et non ecrites en dur : ajouter un type ou une categorie
les deplace toute seule.
flutter analyze : 0 issue.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Signale par Patrice : creer une note, appuyer sur Retour sans enregistrer, et tout disparait sans un mot. Sur une note longue, la perte est reelle et rien ne la laissait prevoir. `PopScope` est deja l'idiome de la maison (`heir_view_screen`, `splash_screen`) — il n'avait simplement pas ete applique au seul ecran ou l'on peut perdre du travail. Meme motif que le reste de ce chantier : la garde existe, elle n'a pas ete propagee. La boite ne s'affiche QUE si le formulaire a reellement change : une empreinte est relevee a l'ouverture, apres remplissage des controleurs, et comparee au moment du retour. Consulter une entree puis revenir en arriere ne demande rien. « Continuer la saisie » est en position sure et non destructive : c'est l'issue a privilegier quand on a appuye sur Retour par erreur. `_save()` appelle `Navigator.pop` directement, donc l'enregistrement n'est pas intercepte. Seuls le Retour systeme et la fleche de la barre de titre passent par `maybePop`. ECARTE : un dossier BROUILLON, envisage puis rejete avec Patrice. Hors du coffre, il ecrirait des secrets EN CLAIR sur le disque, survivant au verrouillage — la promesse meme de l'application. Dans le coffre, une entree a moitie saisie entrerait dans la liste, dans les sauvegardes `.ptbak` et dans l'instantane d'heritage. Il survivrait en outre au mode panique, qui n'efface que la RAM. Surtout, il creerait un SECOND endroit ou vivent des secrets, avec son propre cycle de vie et ses propres regles d'effacement. Tous les defauts de ce chantier viennent d'une garde posee a un endroit et pas a son jumeau : c'eut ete un jumeau de plus a tenir synchronise, pour toujours. Une breche permanente pour regler un probleme ponctuel. flutter analyze : 0 issue. 153 tests verts. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Le job echouait sur CHAQUE pull request, quel qu'en soit le contenu : App token exchange failed: 401 Unauthorized Claude Code is not installed on this repository Le message ne dit pas que le depot est absent — il l'est bien — mais que l'application GitHub « Claude Code » n'est pas autorisee dessus. La cle API etait presente ; c'est la seconde voie d'authentification qui manquait. Verifie dans les entrees reelles de l'action (anthropics/claude-code-action, action.yml) plutot que suppose : `github_token` y est decrit comme « optional if using GitHub App ». Les deux sont donc des voies ALTERNATIVES, et le workflow n'en fournissait aucune. `GITHUB_TOKEN` porte deja les droits necessaires — contents, pull-requests et issues sont declares dans le bloc `permissions:` du job. Installer l'app reste possible si l'on prefere que les commentaires soient signes par elle plutot que par github-actions[bot]. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
CI — deux echecs distincts sur la PR #14 : 1. `promesses` : `exodus_analyze.py: error: unrecognized arguments`. `-j` est un DRAPEAU (sortie JSON), il ne prend pas de nom de fichier ; c'est `-o` qui recoit la destination. Ecrit `-j <fichier> <apk>`, l'outil prenait le fichier pour l'APK et refusait le vrai chemin en argument surnumeraire. La forme erronee vient de `ci-et-chaine-de-publication.md` §6 — a corriger la-bas aussi. Ajout d'un controle explicite de presence du rapport, sinon `jq` echouait sur un fichier absent avec un message opaque. 2. `claude-review` : la requete etait rejetee en 440 ms pour un cout de 0 $, donc refusee avant tout traitement. L'action retenait `claude-opus-5[1m]`, variante a contexte etendu, faute de modele specifie. Desormais fixe explicitement. Laisser un defaut implicite choisir le modele en integration continue est de toute facon une mauvaise idee : le cout par revue et la disponibilite changent sans qu'aucun fichier du depot ne bouge. README passe en anglais (la version francaise suit dans README.fr.md), avec trois corrections de FOND — le traduire tel quel aurait propage des informations devenues fausses aujourd'hui : - « TOTP 2FA avec scanner QR » : le scanner a ete retire avec Google ML Kit ; - la permission `CAMERA` n'existe plus, `USE_FINGERPRINT` est documentee comme injectee par le plugin `biometric_storage` ; - `THREAT_MODEL.md` etait absent de la liste des documents. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
`README.md` est passe en anglais (public GitHub et F-Droid international),
`README.fr.md` porte le francais. Convention deja en place dans le depot pour
PRIVACY et TERMS : `.md` = anglais, `.fr.md` = francais.
Les deux fichiers ont une structure de sections IDENTIQUE et se lient
mutuellement en en-tete, pour qu'une modification de l'un signale
immediatement ce qui manque a l'autre.
Trois corrections de FOND reportees des deux cotes — traduire l'ancien texte
tel quel aurait propage des informations devenues fausses aujourd'hui :
- « TOTP 2FA avec scanner QR » : le scanner a ete retire avec Google ML Kit ;
- `CAMERA` n'est plus une permission de l'application, et `USE_FINGERPRINT`
est documentee comme injectee par le plugin `biometric_storage` ;
- `THREAT_MODEL.md` etait absent de la liste des documents.
Ajout de « Aucune bibliotheque Google » aux arguments : c'est desormais vrai et
verifie sur l'APK a chaque commit.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Le workflow reclamait `ANTHROPIC_API_KEY`. Le secret existait bel et bien, mais la requete etait rejetee en ~200 ms pour un cout de 0 $ — donc refusee a l'authentification, avant tout traitement, et cela avec deux modeles differents. Raison : l'abonnement Claude et l'API sont deux produits DISTINCTS, factures separement. Une cle API exige un compte API approvisionne, que payer l'abonnement ne fournit pas. Aucune valeur de `ANTHROPIC_API_KEY` ne pouvait donc fonctionner ici. `claude_code_oauth_token` est la voie prevue pour un abonnement ; le jeton se genere avec `claude setup-token` (« requires Claude subscription »). L'en-tete du fichier annoncait « ~0,10-0,50 EUR par review, tokens API Anthropic, paye a l'usage ». Cela n'a jamais correspondu a cette configuration, et c'est precisement ce malentendu qui a fait poser une cle API la ou il fallait un jeton d'abonnement. Corrige. `ANTHROPIC_API_KEY` peut desormais etre supprime des secrets du depot. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Audit Gemini Pro (partiel — voir plus bas). Finding confirmee, et c'est un oubli du 2026-08-03 : ce jour-la, la meme protection avait ete signalee sur les champs de NOTES, corrigee sur les trois, et les champs de CARTE laisses de cote. Exactement le motif que ce chantier passe deux jours a traquer : la garde posee sur un jumeau, pas sur l'autre. Les claviers tiers et le dictionnaire personnel apprennent ce qui est saisi, le proposent ensuite dans d'AUTRES applications et le synchronisent parfois vers le nuage de leur editeur. Un numero de carte et un nom de titulaire n'ont rien a y faire. PERIMETRE EXACT — la finding disait « les champs de carte », c'etait trop large. Verification widget par widget : _holderCtrl TextField -> corrige _numberCtrl TextField -> corrige _expiryCtrl TextField -> corrige _issuerCtrl TextField -> corrige _cvvCtrl PasswordTextField -> DEJA protege (widget, l. 82-83) _pinCtrl PasswordTextField -> DEJA protege Une premiere passe avait ajoute les drapeaux aux six : erreur de compilation sur CVV et PIN, dont le composant ne les expose pas parce qu'il les pose lui-meme. Corrige apres verification, pas apres supposition. AUDIT GEMINI PRO : ECHEC A SIGNALER. Cinq lots sur six abandonnes en HTTP 503 (modele preview congestionne), soit 68 fichiers sur 73 NON audites. Le present correctif vient du seul lot passe. Ce n'est pas la relecture externe visee, et les zones que l'on voulait faire relire — Kotlin natif, rotation de cle, flot d'authentification — n'ont pas ete couvertes. Seconde finding du lot ecartee : « FLAG_SECURE relache sur les notes », signale pour la troisieme fois par un modele, arbitre deux fois comme un compromis assume et documente (Knox bloque le presse-papier sinon). flutter analyze : 0 issue. 153 tests verts. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Relecture externe ciblee (ChatGPT) sur les 6 fichiers ecrits les 3-4 aout, que personne d'autre que moi n'avait relus. Rapport de bien meilleure qualite que les passes Gemini : citations litterales, scenarios concrets, et il dit explicitement ce qu'il ne peut pas verifier. F1 — CRITIQUE — PERTE DE DONNEES : coffre VIDE ecrit par-dessus le vrai Entre la verification du mot de passe et l'ecriture, la rotation fait un Argon2id complet — pres d'une seconde. Si `lock()` survient dans cet intervalle (arriere-plan, minuterie, ou MODE PANIQUE), il pose `_entries = []`. Or `_saveVaultV4` SERIALISE `_entries` : la rotation reecrivait le coffre avec une liste vide, chiffree sous le nouveau mot de passe. Toutes les entrees perdues, sans erreur, sans trace. La panique est le pire declencheur : elle appelle `lock()` precisement quand l'utilisateur est sous contrainte. F2 — CRITIQUE — PERTE DE DONNEES : le VRAI leurre ecrase `pt_decoy_configured` etait ecrit APRES la creation du vrai leurre, dans `setupDecoyVault` comme dans `ensureVaultLayout`. Un processus tue entre les deux laissait un vrai coffre avec un drapeau reste a 'false'. Plus tard, `_realignDummyDecoyIfSmaller` lisait exactement 'false', en concluait que `_b` etait factice, et l'ecrasait par un leurre aleatoire indechiffrable. Ma garde « le drapeau doit valoir EXACTEMENT 'false' » etait juste dans son intention et FAUSSE dans son hypothese : un 'false' PERIME est possible. On ne peut pas rendre la paire fichier + drapeau atomique — on choisit donc le sens dans lequel l'incoherence est benigne. Drapeau ecrit AVANT : le pire cas devient « l'app croit a un vrai leurre la ou il n'y en a pas », donc elle s'abstient. F4 — ELEVEE : une ouverture pouvait annuler une panique Meme fenetre, cote deverrouillage : deux Argon2id s'ecoulent avant l'application du resultat. Un `lock()` intervenu entre-temps etait ecrase, le coffre se rouvrait en memoire APRES la panique, et l'appelant recevait `success`. Le commentaire de `_lockGeneration` annoncait pourtant la regle — « toute operation longue [...] doit relever ce compteur ». Je l'avais appliquee a la verification de mot de passe et pas a l'ouverture elle-meme : un commentaire decrivant une regle que son propre fichier n'appliquait pas. Les trois relevent du meme motif que tout ce chantier, et cette fois c'est MOI qui l'ai commis : la garde posee sur un jumeau, pas sur l'autre. Restent a traiter du meme rapport : F3 (`_createSlot` sans rollback), F5 (nettoyage fail-closed non propage aux chemins biometrique et secondaire), F6 (mot de passe leurre identique au principal non refuse par le service), F7 (auto-lock si l'ancre elapsedRealtime est absente), F8 (camouflage qui se replie sur « non camoufle »), F9 (rotation commise signalee comme echouee). flutter analyze : 0 issue. 153 tests verts. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Suite du lot precedent (F1, F2, F4). Les 9 findings du rapport sont traitees. F3 — `_createSlot` posait l'etat global AVANT l'ecriture, sans rollback. Jumeau exact du defaut corrige la veille dans `changeMasterPassword` : la parade avait ete posee la-bas et pas ici. Scenario : on configure un coffre leurre depuis une session PRINCIPALE ouverte, `_saveVaultV4` echoue, et le service pretend desormais qu'un autre coffre est ouvert, vide, avec une cle qui ne correspond pas au disque. La session principale etait ecrasee en memoire par une operation qui avait echoue. Instantane + restauration. F5 — Le nettoyage fail-closed n'existait que sur le chemin principal. Les chemins biometrique et secondaire publiaient `_entries` et `_isOpen = true` AVANT des operations qui peuvent lever, et leur `catch` n'effacait que la cle. Resultat possible : « mot de passe incorrect » rendu, `_isOpen` a vrai, entrees DECHIFFREES en memoire, plus aucune cle — un etat qui se contredit lui-meme. Propage aux deux jumeaux. F6 — `setupDecoyVault` ne verifiait pas l'invariant que sa PROPRE documentation enonce (« le decoyPassword DOIT etre different du master password [...] l'appelant doit valider en amont »). L'ecran le fait, le service non. Si les deux coincident, la boucle d'ouverture garde le premier gagnant — toujours le principal — et le coffre leurre devient inatteignable. Verifie desormais par le service lui-meme. F7 — Auto-lock : mon commentaire affirmait « si le canal rend une valeur aberrante ou indisponible [...] on verrouille trop tot, jamais trop tard ». FAUX dans le cas prevu par le code : `elapsedRealtimeMs()` a `null` sautait tout le bloc, ne laissant que le `Stopwatch` — celui dont le meme fichier explique qu'il NE COMPTE PAS la veille profonde. Deux heures de sommeil, coffre toujours ouvert. Sans ancre fiable, on verrouille. F8 — `isDisguised` se repliait sur « non camoufle » en cas d'erreur, DEUX FOIS : cote Kotlin (`success(false)`) et cote Dart (`catch => false`). Le `catch` de l'appelant, qui pretendait s'abstenir « plutot que de trahir un camouflage actif », ne pouvait donc JAMAIS s'executer — l'app contactait le reseau en mode panique. Deux replis du mauvais cote, empiles, qui se masquaient l'un l'autre. La methode rend desormais `bool?` : l'incertitude est representable, et chaque appelant doit la traiter. F9 — L'ecriture du sel herite pouvait faire echouer une rotation DEJA acquise : l'ecran annoncait un echec alors que le coffre etait passe sous le nouveau mot de passe, et les deux nettoyages suivants etaient court-circuites — dont la suppression du `.bak` v3. Or si l'on change de mot de passe PARCE QUE l'ancien est compromis, laisser une sauvegarde dechiffrable avec cet ancien mot de passe annule tout le benefice. Rendue best-effort : elle n'est pas porteuse en v4. POINT DOUTEUX — tranche, ce n'est PAS une vulnerabilite. Une version forgee (99999) entre bien dans la branche v4, mais `_decryptVaultV4` teste l'egalite STRICTE et la rejette. GPT avait raison de ne pas le promouvoir. Le commentaire etait trompeur sur la portee reelle de la garde : corrige. flutter analyze : 0 issue. 153 tests verts. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Ce job n'a jamais reussi. L'authentification a ete corrigee par etapes — `github_token` fourni (le jeton d'app GitHub manquait), modele fixe explicitement, puis `claude_code_oauth_token` a la place d'une cle API que l'abonnement ne fournit pas. Chaque etape a fait progresser la requete, la duree passant de 227 ms a plus de 2 s, mais quelque chose reste refuse en aval sans message exploitable dans le journal. RAISON DU DRAPEAU, et ce n'est pas la commodite : il mettait au rouge CHAQUE pull request alors que les quatre autres jobs — analyse, 153 tests, permissions de l'APK, traceurs Exodus — sont verts. Un voyant rouge permanent finit par ne plus rien signifier : on cesse de le regarder, et le jour ou un VRAI echec arrive, il passe inapercu. C'est exactement le meme mecanisme que le `flutter test || echo "No tests yet"` retire de `ci.yml` au debut de cet audit, qui faisait passer le job au vert meme quand les tests echouaient. La revue automatique fait par ailleurs doublon avec ce qui a ete fait a la main : audit ligne a ligne, quatre passes Gemini, une relecture ChatGPT ciblee. A retirer des que le job reussit une fois. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Trouve en relisant MES PROPRES correctifs d'il y a une heure, avant meme le retour de Codex. C'est la QUATRIEME fois en deux jours que le motif « je corrige une faille et j'en cree une » se produit — et cette fois dans le correctif d'un defaut de ce motif exact. Le `finally` ajoute par f0cfea2 (audit GPT F3) restaure la session — cle, entrees, emplacement actif — quand la creation echoue. Il le faisait SANS CONDITION. Or `_createSlot` dure un Argon2id complet. Si `lock()` survient dans cet intervalle (mise en arriere-plan, minuterie d'inactivite, ou MODE PANIQUE) et que l'ecriture echoue ensuite, la restauration remettait le coffre principal OUVERT en memoire, apres la panique. Exactement le defaut que le correctif jumeau de `changeMasterPassword` corrigeait — pose la-bas, oublie ici, une heure plus tard. `_lockGeneration` est desormais releve avant l'Argon2id et compare dans le `finally`. S'il a change, on efface l'instantane et on laisse le coffre ferme : la decision de verrouiller prime sur la restauration d'une operation echouee. LECON, et elle est generale : apres avoir ecrit un correctif de securite, le relire SOI-MEME une fois de plus rapporte moins que de le faire relire par un tiers. Sur les 9 findings de l'audit GPT, 3 portaient sur des commentaires que j'avais ecrits, decrivant une regle que mon propre code n'appliquait pas. flutter analyze : 0 issue. 153 tests verts. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Cinquieme et dernier chemin long depourvu de la garde `_lockGeneration` — et paradoxalement le plus expose des quatre. Les trois autres (ouverture par mot de passe, creation d'emplacement, rotation du mot de passe maitre) durent un ou deux Argon2id, soit une a deux secondes. Ici la fenetre reste ouverte tant que l'invite biometrique attend un doigt : plusieurs dizaines de secondes, sans borne. C'est la plus grande fenetre de toute l'application pour qu'un `lock()` s'intercale — et notamment un MODE PANIQUE declenche pendant que l'invite est affichee, ce qui est exactement le scenario que la panique existe pour couvrir. Sans la garde, le deverrouillage reprenait apres la panique et rouvrait le coffre en memoire. Il rend desormais `biometricCanceled` et laisse le coffre ferme. RECENSEMENT COMPLET, pour ne pas refaire l'erreur du perimetre trop etroit. Les cinq chemins qui prennent un instantane d'etat ou appliquent un resultat apres une operation longue relevent maintenant tous la generation : vault_unlock.dart:127 _passwordMatchesPrimaryInternal vault_unlock.dart:545 _unlockWithBiometricInternal <- celui-ci vault_service.dart:575 _unlockInternalUnguarded vault_setup.dart:46 _createSlot vault_setup.dart:210 changeMasterPassword Le commentaire de `_lockGeneration` enonce la regle depuis le debut : « toute operation longue qui prend un instantane de l'etat du coffre pour le restaurer ensuite doit relever ce compteur ». Elle n'etait appliquee qu'a un endroit sur cinq quand elle a ete ecrite. Elle l'est desormais partout. flutter analyze : 0 issue. 153 tests verts. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
CRITIQUE — le cache meta manquait a l'instantane de _createSlot. _saveVaultV4 termine par _cachedSalt/_cachedWrappedDek/_cachedWrapNonce/ _cachedKdfParams = <metadonnees du slot ecrit>. Le rollback ajoute hier ne remettait que la cle, les entrees et l'emplacement actif : le cache continuait de decrire le LEURRE. Chaine complete, depuis une session PRINCIPALE ouverte : setupDecoyVault -> _createSlot(decoy) -> _saveVaultV4 reussit (cache bascule sur le leurre) -> l'ecriture du sel ou _onUnlockSuccess leve -> le finally remet la session sur le principal, cache reste leurre -> la moindre modification d'entree passe par le chemin rapide de _saveVault, qui n'exige que quatre valeurs non nulles, et reecrit le fichier PRINCIPAL en y annoncant le sel et l'enveloppe du LEURRE -> au deverrouillage suivant la derivation repart du mauvais sel et l'enveloppe est deballee avec la mauvaise KEK : le coffre principal ne s'ouvre plus jamais, avec aucun des deux mots de passe. La restauration efface les tampons sortants via _wipeUnlessSame : quand l'echec survient AVANT _saveVaultV4, le cache courant et l'instantane sont le MEME objet, et un effacement naif remettrait un sel nul en place — exactement la corruption que le correctif existe pour empecher. 7 tests couvrent cette garde. MOYENNE — setupDecoyVault ne distinguait pas « non » de « je ne sais pas ». passwordMatchesPrimary rend false dans trois cas qui ne signifient pas que les mots de passe different : emplacement actif autre que le principal, verrouillage anti-force-brute, deverrouillage concurrent. La garde ajoutee hier ne testait que la valeur de retour — donc elle reproduisait le defaut qu'elle pretendait corriger, l'invariant restant suspendu a la garde d'ECRAN. Les trois cas sont desormais explicites et bloquants, et vaultBusy est mappe a l'ecran comme il l'est deja pour le changement de mot de passe maitre. FAIBLE — un recul de l'ancre systeme etait ignore au lieu de verrouiller. C'etait le troisieme repli du bloc de verrouillage automatique et le seul a pencher du mauvais cote : ignorer revenait a ne garder que le Stopwatch, celui qui ne compte pas la veille profonde. Le cas devrait etre hors d'atteinte, mais un repli ne vaut que par le sens dans lequel il echoue. Deux points releves en verifiant : - _cachedKdfParams manquait aussi dans _tryUnlockSlot, alors que le chemin principal le pose. Les quatre champs decrivent le MEME fichier : l'omission privait ce chemin de la migration d'etiquette SEC F18, et aurait pu faire reecrire un coffre en annoncant une derivation qui n'est pas la sienne. - la documentation de MonotonicClock.elapsedRealtimeMs demandait a l'appelant de se rabattre sur nowMs. C'etait faux et dangereux a suivre : nowMs est une horloge murale, insensible aux reculs mais pas aux avances. Le seul repli correct est de traiter la duree comme inconnue. L'invariant qui rend la restauration du cache inutile dans changeMasterPassword est desormais ecrit a l'endroit ou un futur await le romprait. 160 tests (153 -> 160), flutter analyze sans issue. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
main.dart annoncait toujours, juste au-dessus du bloc de verrouillage automatique, que se rabattre sur le Stopwatch faisait « verrouiller trop tot, jamais trop tard ». Le correctif F7 refutait cette phrase quinze lignes plus bas, mais la phrase restait la, en premier dans l ordre de lecture. Le Stopwatch ne mesure que le temps eveille : s y rabattre seul RALLONGE le delai. vault_setup.dart affirmait que rien ne modifie les tampons du cache meta en place. lock() le fait — il les zeroise — et c est precisement la raison du branchement qui suit. Commentaires seuls, aucun changement de comportement. 160 tests verts. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
…ogue Signale en usage reel sur le dialogue du coffre leurre : « Mot de passe trop devinable... » passait par-dessus Annuler / Chiffrer. Ce n est pas un manque de marge, c est un DEBORDEMENT. Le contenu d un AlertDialog est enveloppe dans un Flexible ; le clavier est toujours ouvert ici (le premier champ prend le focus), donc la hauteur disponible tombe de plusieurs centaines de pixels. Le message fait alors passer la Column au-dela de sa boite, et une Column ne rogne pas : elle peint par-dessus les actions. Ajouter de la marge sous le texte aurait AGGRAVE le debordement. Parade structurelle : scrollable, la colonne recoit la hauteur restante et defile au lieu de deborder. La marge demandee vient en plus, pour que le message ne colle pas aux boutons quand tout tient. Applique AUSSI au dialogue de changement de mot de passe maitre : meme message, un champ de PLUS, donc il debordait plus tot. Ne corriger que celui qui a ete vu aurait laisse le defaut a l endroit le plus expose. setup_screen porte le meme message mais dans un Scaffold defilant : pas concerne. 160 tests verts, analyse propre. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Quatre defauts de l ecran d audit, tous corriges. 1. LE SCORE IGNORAIT LA TAILLE DU COFFRE. Il soustrayait des points en valeur absolue avec un plafond : au-dela de six mots de passe faibles la penalite s arretait. Un coffre de six entrees dont les six etaient faibles affichait 70/100, « Bon » — et un coffre de deux cents entrees avec six faibles affichait exactement la meme chose. Le score rassurait la ou il ne fallait pas. Le nouveau modele est la sante MOYENNE des mots de passe, donc proportionnel par construction. 2. LE SEUL SIGNAL FACTUEL NE COMPTAIT PAS. Le resultat HaveIBeenPwned ne servait qu a peupler une liste : un mot de passe dont on a la PREUVE qu il figure dans une fuite publique valait zero penalite, pendant que l anciennete en coutait jusqu a vingt. Une entree compromise vaut desormais zero point de sante, le plus bas du bareme. 3. L ANCIENNETE PENALISAIT, a rebours du NIST SP 800-63B qui recommande explicitement de ne pas imposer de rotation periodique. Punir un mot de passe fort, unique et non compromis parce qu il a un an pousse a l increment (MotDePasse1 -> MotDePasse2), qui est plus faible. La section reste, en ton neutre et sans effet sur le score. 4. LES DEFAUTS SE CUMULAIENT SUR UNE MEME ENTREE. Un mot de passe a la fois faible ET reutilise etait compte deux fois. Une entree vaut maintenant son pire defaut, une seule fois. Aussi : - Un coffre SANS mot de passe n a plus de score. Il affichait 100 « Excellent », ce qui revenait a feliciter un coffre vide. - Le controle 2FA couvre « Reseaux sociaux » en plus de « Banque » et « Email » (rebond vers les autres services par reinitialisation), et son libelle dit desormais qu il ne voit que les TOTP stockes DANS Pass Tech — il produisait un faux positif silencieux chez qui utilise une application separee. - Le score est annonce PARTIEL tant qu au moins un mot de passe n a pas ete confronte aux fuites. La couverture est suivie separement du resultat : une premiere version de ce correctif annoncait « complet » des qu une verification avait tourne une fois, y compris apres l ajout d un mot de passe que personne n avait jamais verifie. - Les entrees compromises affichees sont reconstruites a chaque analyse : un mot de passe corrige disparait de la liste au lieu d y rester. Le calcul est extrait dans une fonction PURE (vault_audit_score.dart) et couvert par 16 tests — il n en avait aucun, et il etait faux. 176 tests (160 -> 176), analyse propre, parite FR/EN verifiee. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Mineure et non corrective : le comportement visible change. - Le score de l audit est recalcule sur un autre modele. Il baissera chez les utilisateurs de petits coffres, non par regression mais parce qu il ne plafonne plus la penalite. - La politique de mot de passe change : 12 caracteres, symboles et chiffres ne sont plus exiges. - Google est retire de l application : plus aucune bibliotheque ni service Google embarque. Pass Tech lit sa version via PackageInfo : il n existe AUCUNE constante statique a bumper, contrairement a PDF Tech et Notes Tech. Verifie — les occurrences de 2.5.4 dans le code sont toutes des marqueurs SEC en commentaire. Changelogs fastlane FR 470 et EN 429 caracteres, sous le plafond F-Droid de 500. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Audit de sécurité complet des 2026-08-03 / 04. 46 correctifs, aucun changement de version — la 2.5.4 reste la version publiée.
Origine des findings
Sur 27 findings rapportées par les modèles, 21 se sont révélées réelles après relecture du code ; 6 ont été écartées, dont une de sévérité « haute » dont la prémisse n'existait pas dans l'application.
Les plus importantes
Stopwatchs'appuie surCLOCK_MONOTONIC, qui ne compte pas le sommeil de l'appareil : téléphone posé deux heures, coffre toujours ouvert au retour..ptbakpotentiellement corrompue : le fichier partagé était déchiqueté avant que la cible ait fini de le lire.Google entièrement retiré
mobile_scannersupprimé, et avec lui ML Kit, Play Services et le transport de télémétriedatatransport.Permissions de l'APK signé, vérifiées sur le manifeste fusionné :
ACCESS_NETWORK_STATEn'était déclarée nulle part dans le dépôt — elle arrivait par une dépendance transitive. Le scan de QR code est remplacé par le collage d'une URIotpauth://, dont le secret est extrait automatiquement.Garde-fous permanents ajoutés
android/expected-permissions.txt+.github/workflows/promesses.yml: la liste des permissions est vérifiée sur l'APK construit à chaque commit, avec un contrôle Exodus Privacy.ci.ymldurci : leflutter test || echo "No tests yet"faisait passer le job au vert même quand les tests échouaient.THREAT_MODEL.md: premier modèle de menace écrit, limites comprises.Ce que seuls les tests appareil ont trouvé
Neuf défauts qu'aucune analyse statique n'a vus, tous de la même famille : le mécanisme est correct, c'est ce qu'il communique qui ne l'est pas. Sortie du camouflage inatteignable sans le coffre, biométrie supprimée sans avertissement, annulation présentée comme un échec, boucle d'invite au lancement.
Vérifications
flutter analyze: 0 issue · 153 tests (+27) · 11 validations sur appareil (S24 FE).Deux limites assumées et documentées : le camouflage partiel dans Réglages Android (
android:labeln'est pas modifiable à l'exécution) et le blocage Knox du collage inter-applications sur une fenêtreFLAG_SECURE.Une chose non vérifiable sur appareil : la fermeture de la vue héritier au retour d'arrière-plan, qui exigerait 90 jours d'inactivité. Elle repose sur la relecture du code seule.
🤖 Generated with Claude Code