⚠️ État au 2026-08-07 — retrait de l'IA embarquée (v2.0.0). Notes Tech n'embarque plus ni moteur d'inférence Gemma, ni recherche sémantique (MiniLM / ONNX). Les entrées d'historique ci-dessous qui les mentionnent décrivent des correctifs réellement appliqués à l'époque : elles sont conservées telles quelles, réécrire un journal de sécurité serait pire que de le laisser dater. Seules les affirmations d'état courant ont été mises à jour.
Version current : v2.0.0 — Août 2026.
Retrait de la recherche sémantique (MiniLM/ONNX) et du Q&A Gemma. Surface d'attaque réduite d'autant : plus d'isolate d'inférence, plus de prompt injecté, plus de cache d'embeddings en clair à protéger. APK arm64 ~127 Mo → 26,9 Mo.
- Perte de données irréversible à la suppression d'un dossier coffre.
folders_drawer.dartdétruisait la clé Keystore PIN avant la ligne en base. Si la suppression DB échouait ensuite, le dossier et ses notes chiffrées restaient présents alors que la clé nécessaire au déverrouillage avait disparu — coffre définitivement illisible. L'ordre est inversé : base d'abord, clé ensuite, soustry/catch. - Le mode panique mentait sur son résultat. Trois étapes
(
_prefsClearWithWhitelist,_wipeExportsCache,_purgeTempDirectory) avalaient leurs échecs unitaires et l'écran final affichait « effacement terminé » alors que des artefacts survivaient. Elles comptent désormais leurs échecs et lèvent ;_wipeExportsCachepurge en outre le répertoire de cache en plus du temporaire. Le filtre_estArtefactSensibleévite l'écueil inverse — un fichier temporaire étranger à l'app ne doit pas faire déclarer la panique incomplète. - Titre d'une note de coffre écrit en clair. Le scellement était porté
par l'appelant et non par la couche de persistance : tout chemin
d'écriture qui l'oubliait laissait le titre lisible dans la colonne.
NotesRepositoryrefuse maintenant toute écriture en clair dans un dossier coffre (VaultPlaintextWriteException) et scelle lui-même. - Presse-papiers non purgé après copies successives. La minuterie d'effacement d'une copie pouvait remettre l'état à zéro alors qu'une copie plus récente était en cours, laissant son contenu indéfiniment dans le presse-papiers. Un jeton par copie ferme la course.
- Les migrations de schéma n'avaient jamais été exercées. Les tests
créaient des bases neuves :
onCreateposait le schéma courant etonUpgraden'était jamais appelé.integration_test/db_migration_test.dartconstruit désormais de vraies bases héritées (v1, v5, v7) et vérifie la migration réelle jusqu'en v9, dont la purge FTS d'une note verrouillée.
CI GitHub Actions verte de bout en bout pour la première fois : analyse, 65 tests unitaires, build, et les 5 suites d'intégration sur émulateur API 30. Scan OSV des dépendances : aucune CVE.
Changement UI sans impact sécurité : logo damier dans l'AppBar, actions
secondaires (Réglages / À propos) regroupées dans un overflow ⋮, et bouton
« Vérifier les mises à jour » dans À propos. Ce bouton délègue au navigateur
système l'ouverture de la page GitHub releases (même pattern que le
téléchargement des modèles voix) — aucune permission INTERNET ajoutée : la
promesse zéro-réseau reste vérifiable dans le manifeste (tools:node="remove").
Audit 4-agents (sécu / perf-qualité / câblage / cohérence-i18n) sur ~24,8k LOC.
Aucune faille CRITICAL/HIGH. Corrections sécu/robustesse + features anti-perte
de données. flutter analyze 0 issue, 84 tests verts.
- Purge des sidecars WAL/SHM/journal EN CLAIR lors de la migration DB
héritée plain → chiffré (
database.dart._migratePlainToEncrypted). Le modejournal_mode = WALlaissaitnotes_tech.db-wal/-shmcontenant des pages de notes en clair, survivant à la migration (fuite sur device rooté / extraction physique). Désormais purgés aprèssqlcipher_export+ checkpoint, symétriquement au wipe du.plain.bak. - Résilience du cache d'embeddings (
embeddings_dao.listByModel) : une ligne empoisonnée (blob de taille incohérente) ne condamne plus tout le chargement du cache sémantique — décodage tolérant par ligne. - Gardes
mountedajoutées (ai_chataprès l'isolate MiniLM,searchpost-frame) : plus desetStateaprèsdispose. - Signalement anti-perte vault : les notes de coffre dont la dernière
modification a été perdue (coffre verrouillé pendant la sauvegarde) sont
désormais signalées par un banner à l'ouverture — la pref
vault_lost_drafts(F11 v1.1.0) était écrite mais jamais relue. - Vidage du presse-papiers en mode panique : nouveau step
PanicStep.clipboardClear(tôt dans la séquence, aprèsvoiceCancel) qui appelleNoteActions.cancelAndClear(). Une note copiée (copyMarkdown) restait en clair dans le presse-papiers jusqu'à l'auto-clear 60 s ; en panique on n'attend plus ce délai.
- Purge de 99 clés l10n orphelines (jamais référencées) des ARB FR/EN —
parité conservée (380 clés chacune),
flutter analyze0 issue confirme qu'aucune n'était utilisée.
Suite à un audit 3-agents (sécu / perf / UX) + audit cross-files,
23 corrections livrées (F1-F14, P1-P5, U1-U11). Aucun changement
de format DB ni de format coffre. flutter analyze 0 issue, 68/68
tests verts (+5 nouveaux test/audit_v1_1_0_test.dart).
- F1 —
note_editor._moveToFolder: confirmation EXPLICITE (dialog destructifcs.errorContainer+ Cancel autofocus) avant de sortir une note d'un coffre vers un dossier ordinaire. Avant : le contenu était décrypté + persisté en clair sans signal UI, irréversible. Si l'auto-lock du coffre tombait pendant la mutation, l'utilisateur croyait l'écran fermé alors que le flush plaintext passait silencieusement. - F2 —
NotesDao.findByTitleLike: ajout du filtre SQLencrypted_content IS NULL. Avant :BacklinksService.suggestTitles(F3 v1.0.9) filtrait côté Dart, mais le DAO sous-jacent exposait toutes les notes locked à tout futur caller, et lelimitSQL était consommé par les notes vault AVANT le filtre Dart → les suggestions s'amincissaient sur les gros coffres sans raison apparente. Defense-in-depth. - F3 —
IndexingService._indexAll: skip explicite des notes vault AVANT_encodeWith(embedder, note). Avant : siknownHashes[n.id]ne matchait pas pour une note locked (hash stale), MiniLM encodait son contenu en RAM côté worker ONNX, et l'embedding n'était écarté qu'APRÈS l'encoding vialive.encryptedContent != null. Désormais aucun feed à l'embedder pour les notes locked, quel que soit le hash. - F4 —
NoteActions.copyMarkdown: MethodChannel natif Kotlincom.filestech.notes_tech/clipboard.copySensitivequi poseClipDescription.EXTRA_IS_SENSITIVE(Android 13+) + auto-clear 60 s du presse-papier côté Dart (vérifie que la valeur courante est encore celle qu'on a posée avant clear, ne touche pas un autre secret copié entretemps). Avant :Clipboard.setDatabrut exposait le plaintext d'une note vault déchiffrée à TOUT clipboard manager tiers + Knox clipboard history sans expiration. - F5 —
ai_chat_screen._resolveSource: suppression duinitialDirectory: '/storage/emulated/0/Download'. Avant : path absolu nécessitant READ_EXTERNAL_STORAGE (sinon SAF picker vide silencieusement) ET ouvrait sur Downloads d'autres apps (Telegram, WhatsApp) ouvrant la voie à un.taskmalveillant non lié au flux SAF maître. - F6 —
VoiceService._isPresentAndPlausible: TTL du cache de vérification SHA-256 réduit de 30 jours à 24 heures, et refus sicached.mtimeMs > cached.verifiedAtMs(file touché après notre dernière vérif réussie). Avant : un attaquant root pouvant écrire un Whisper trojanisé avectouch -tmatchant (size, mtime) restait validé 30 jours sans rehash. Coût utilisateur : ~3-5 s de hash strict au premierstartRecordingpost-24h. - F7 —
PanicService: nouvelle étape_wipeExportsCachequi purgegetApplicationCacheDirectory()/exports/. Avant : un ZIP d'export en cours de Share survivait à panic car_purgeTempDirectoryne couvrait quegetTemporaryDirectory(). - F8 —
RagService._sanitizeétendu : Llama2<<SYS>>, ChatML<|im_start|>/<|im_end|>, Alpaca/Vicuna### Instruction:/### Response:, Mistral[ASSISTANT]/[USER]brackets neutralisés. Avant : Gemma 3 1B (decoder generaliste pré-entraîné sur ces formats) pouvait basculer en mode chat formel si un attaquant insérait ces marqueurs dans une note contexte RAG. F13 v1.0.3 listait ces patterns comme best-effort, désormais couverts. - F10 —
FolderVaultService._unlockInProgress: Set<String>guard surunlock()/unlockWithPin(). Avant : Dart est mono-thread mais Argon2idcompute()(600-900 ms sur S24) cède l'event-loop entreawait— unTimer(_autoLockAfter)pouvait alors firer pendant le unlock et wiper lafolder_kekfraîchement assignée avant qu'elle ne soit consommée parencryptNote._autoLockSweepskip désormais les folderIds en cours de unlock. - F11 —
note_editor._flushFinalSave: si le coffre est verrouillé pendant le flush final (dispose post-auto-lock), on persiste l'iddansprefs.vault_lost_drafts. Avant : « perte acceptée » silencieuse, l'utilisateur croyait l'auto-save infaillible. Consommable par un futur écran « N modifications perdues sur des notes vault » au prochain boot. - F14 —
AppDatabase._attachSql: validation regex stricte^[A-Za-z0-9_./:\\-]+$du path AVANT l'ATTACH. Avant : le path provenait degetApplicationDocumentsDirectory(), qui peut être détourné viaLD_PRELOAD/ root setup pointant vers un chemin contenant des méta-SQL ('; DROP --). Cas extrême root-only mais c'est la « source unique de vérité » de la DB.
- P1 —
HomeScreen._reloadDebouncer(250 ms) coalesce les eventsnotes.changespendant l'auto-save continu (1 event/500 ms par frappe). Avant : un SELECT completlistAllAliveexécuté à CHAQUE event, soit ~50-200 ms SQLCipher sur 500 notes × fréquence de frappe. - P2 —
isUniversalApk = falsedansbuild.gradle.kts. Avant : générait un 4ᵉ APK universel ~294 Mo embarquant les libs natives des 3 ABIs (sqlcipher + ONNX + Whisper + MediaPipe). Économie ~70 Mo upload GitHub Releases + bandwidth user. - P3 —
BacklinksService._buildTitleIndex: cache TTL 5 s invalidé explicitement sur changement de titre. Avant :listAllAlive()re-exécuté à CHAQUE save d'une note (rafale d'auto-saves = 1 SELECT/500ms même sans mutation de titre). - P5 —
MentionsLegalesScreen._MarkdownAssetView._load: cachestatic final Map<String, String>process-wide des assets.md. Avant :rootBundle.loadStringre-exécuté à CHAQUE switch d'onglet TabBarView ou de locale.
- U1 —
HapticFeedback.selectionClick()sur copy MarkdownHapticFeedback.heavyImpact()sur déclenchement panique. Avant : 0 hitHapticFeedbackdans toutlib/— aucun feedback tactile pour les actions critiques (alignement avec Pass Tech v2.4.4 U9 / AI Tech U4).
- U2 —
SnackbarMessengerExt.showFloatingSnackaccepte désormaisforegroundColor. 2 sitesfolders_drawermis à jour :cs.errorContainer+cs.onErrorContainer(contraste WCAG AA ~13:1 en light mode vs ~3.5:1 mesuré aveccs.errorbrut surtextPriclair). - U3 — TextField titre + contenu note :
textCapitalization: TextCapitalization.sentences. Avant : saisie tactile à doigt unique sans capitalisation auto → titres avec minuscules initiales. - U11 — TextField composer AI :
textCapitalization: TextCapitalization.sentences.
Suite à un audit 3-agents (sécu / perf / UX), 11 corrections livrées.
Aucun changement de format DB (toujours v6) ni de format coffre.
flutter analyze 0 issue, tests verts.
- F1 —
FolderVaultService.unlock()(mode passphrase) bénéficie désormais du même lockout exponentiel monotonique queunlockWithPin()(M-05 v1.0.7) : compteur RAM_passFailCount+ backoff1/2/4/8/16/30 saprès 5 essais, levantVaultLockoutInProgressException. Avant : sur S24+ flagship Argon2id m=64Mo t=3 prenait ~600-900 ms → un attaquant ADB + dictionnaire 10k passphrases pouvait tester ~4 essais/s sans friction. API publiquepassphraseLockoutRemaining()exposée pour countdown UI symétrique au PIN. - F3 —
BacklinksService.suggestTitles()filtre maintenantn.isLocked. Avant : l'auto-complétion[[…]]dans une note alive proposait les titres des notes verrouillées → fuite par défaut depuis la création des coffres. Aligne sur M-01 v1.0.7 (_indexByTitle,_handleSingleChange,_reindexAllqui skippaient déjà locked). - F7 —
RagService.composePromptapplique désormais_sanitizeauuserPrompt(les titres et bodies des sources étaient déjà sanitizés). Couvre une injection arrivant via dictée vocale ou auto-paste (<|system|>, zero-width, bidi). - F8 —
note_editor_screenposeFLAG_SECURE(_ensureSecureForced) AVANTvault.decryptNote. Avant : fenêtre ~5-20 ms (channel round-trip) pendant laquelle un screenshot manuel ou MediaProjection pouvait capter le plaintext entredecryptNoteet_ensureSecureForced.
- P1.2 —
note_editor_screen._changesSubfiltre maintenant les événements (event.id != widget.noteId && !event.isBulk→ return). Avant :get(noteId)re-déclenché sur CHAQUE event (y compris ses propres saves + tous les autres éditeurs ouverts) → 1 SELECT SQLCipher/s minimum en auto-save continu (debounce 500 ms). - P1.1 (backlinks title cache) et P1.4 (notes_repository.save without
systematic
findById) reportés à v1.1 (refactors plus profonds).
- U1+U2+U11 —
PassphraseTextField(centralisé) ajouteautofillHints: const [](désactive Samsung Pass / Google Autofill),keyboardType: TextInputType.visiblePassword(neutralise SwiftKey/Gboard auto-cap), etenableInteractiveSelection: !_hidden(bloque sélection/copie quand masqué — anti clipboard manager). - U3 —
confirmDialog(helper centraliséapp_dialogs.dart) : bouton Annulerautofocus: truequand le dialog est destructif + bouton confirme viacs.errorContainer/onErrorContainerau lieu decs.errorbrut. - U4 —
about_screenicôneImage.assetaveccacheWidth: 112/cacheHeight: 112(avant : PNG 1024×1024 décodé sans borne pour afficher 56dp = ~12 Mo RAM permanent). - U9 — Empty state home :
FilledButton.tonalIcon"Nouvelle note" inline en plus du FAB (plus découvrable au premier lancement).
9 occurrences SemanticsService.announce annotées
// ignore: deprecated_member_use (migration Flutter 3.35
sendAnnouncement prévue v1.1), 2 directives_ordering
(home_screen / settings_screen imports triés), 3 prefer_const
dans panic_service_test.dart.
Tests existants tous verts (64+ assertions). Le test e2e flow
unlock → wrong passphrase × 5 → lockout est volontairement déféré
à l'instrumentation (Keystore mock non-trivial en pure Dart, cf.
folder_vault_service_test.dart).
Version précédente : v1.0.4 — Mai 2026.
Notes Tech v1.0 introduit plusieurs durcissements sécurité :
prefs.clear()panique avec whitelist (db_encrypted_v1,secure_window_enabledpréservés) conformément àPRIVACY.md.flutter_markdowncantonné aux pages légales locales (assets), aucun rendu de markdown distant.ProGuardrules complètes :files_tech_voice,flutter_markdown, sqflite. Les règles MediaPipe / ONNX / flutter_gemma sont devenues sans objet avec le retrait de l'IA embarquée (v2.0.0).
If you believe you've found a security issue in Notes Tech, please do not open a public GitHub issue. Instead, email :
Subject : [SECURITY] Notes Tech — <short summary>
Include :
- A description of the issue and its potential impact.
- Steps to reproduce (or a proof-of-concept).
- Affected version (Réglages → À propos → Notes Tech vX.Y.Z).
- Your contact for follow-up.
You'll get an acknowledgement within 72 hours. A coordinated disclosure timeline will be agreed upon if the issue is confirmed.
- Notes Tech app code (
lib/,android/) - Module sibling
files_tech_voiceif relevant - Crypto implementations (SQLCipher integration, KEK derivation / storage, panic mode irreversibility)
- Permission handling (
RECORD_AUDIO) - File handling / SAF imports / path traversal
- Dependency vulnerabilities surfaced by
health_check.sh
- Issues in third-party packages (file an issue upstream).
- Issues requiring a rooted device or pre-existing malware on the device.
- Social engineering against the user.
- Denial of service via deliberately oversized notes / payloads.
Notes Tech is designed for individuals and professionals who want local-only notes with strong cryptographic guarantees. Three adversary classes are considered :
- Confidentiality at rest : SQLCipher (AES-256) for the database, AES-256-GCM for per-folder vault notes. KEK sealed by Android Keystore (hardware-backed on modern devices).
- Per-folder vault adds a second factor (passphrase or PIN) on top of the device lockscreen.
- Panic mode : a confirmed delete-everything action that runs a deterministic ordered sequence (see below). Designed to be fast and irrecoverable under coercion.
- PIN auto-wipe : 5 failed PIN attempts on a vault wipes that vault's keys atomically (with crash-resume via prefs flag).
setUserAuthenticationRequired(false)on PIN Keystore keys : the PIN is the sole factor — adding biometric would expose the user to forced fingerprint unlock (a biometric-derived key survives reboot).
- No
INTERNETpermission means a compromised dependency cannot exfiltrate notes via the standard network path. Data exfiltration through standard channels is technically impossible without re-installing a modified APK. - No
FOREGROUND_SERVICE, noPOST_NOTIFICATIONS, noRECEIVE_BOOT_COMPLETED— minimal attack surface. FLAG_SECUREblocks Recents previews and screen recording.allowBackup=false+dataExtractionRulesblock Smart Switch / Android Backup exfiltration.
- Argon2id RFC 9106 for passphrase derivation :
m=64 MiB, t=3, p=1, 32-byte output(vault default). PIN mode uses lighter parametersm=32 MiB, t=2because the device-bound Keystore wrap is the primary defense and on-device rate-limiting prevents brute force. - AES-256-GCM for note content with AAD =
note_id(prevents ciphertext substitution between notes). - KEK wrap with AAD =
folder_id(prevents wrap reuse across folders). - HMAC verifier in constant time to detect bad passphrase / PIN without trial-decrypting every note.
- SQLCipher 4 (AES-256-CBC + HMAC-SHA512), key sealed via
flutter_secure_storage→EncryptedSharedPreferences→ Keystore.
The panic sequence is deterministic and best-effort (a step that throws does not abort the next ones) :
FLAG_SECUREforced ON, microphone capture stoppedfoldersLockAll— lock every open vault, wipe folderKek from RAMpinKeysWipe—deleteKeysWithPrefix("vault_pin_")(Kotlin)kekDestroy— destroy the master Keystore key (DB instantly unreadable)- Background workers paused (backlinks)
dbWipe— overwrite SQLCipher header (16 MiB cap) + delete.db,.db-wal,.db-shm- Whisper model file deleted, all prefs cleared, tmp purged
- Forensic recovery from a physical memory dump of an unlocked, rooted device is partially possible (Dart heap GC eventually recycles strings, but a snapshot during use can leak plaintext).
- A determined nation-state attacker with custom kernel exploits is out of scope.
- Display privacy (
FLAG_SECURE) is on by default but opt-out is possible in Settings.
We follow a 90-day disclosure window by default :
- Day 0 : your report received.
- Day 0-7 : initial triage, severity assigned.
- Day 7-60 : fix developed, tested, audited.
- Day 60-90 : release with patched version, public CVE if applicable.
- Day 90+ : you're free to publish your write-up.
Critical issues (RCE, key extraction, full data exfiltration) may be patched faster than 90 days.
Each release is checked via :
flutter analyze(lints stricts)flutter testbash j:\applications\health_check.sh notes_tech:- OSV-Scanner (CVE in dependencies)
- gitleaks (secrets in git history)
- Manifest hardening (no
debuggable, nocleartextTraffic, no excessive permissions) - Signing config (R8 enabled, no debug fallback for release)
- FileProvider (no
<root-path>, no global app-private exposure) - Crypto patterns (no MD5/SHA-1 for security, PBKDF2 ≥ 100k iter)
- Kotlin patterns (
canonicalFile,FLAG_IMMUTABLEPendingIntent)
- 4-agent audit (architecture / security / performance / coherence) for material features.
Code source : https://github.com/gitubpatrice/notes_tech Licence : Apache License 2.0
- Wipe DB header plafonné à 16 Mo : la
kekDestroyprécédente garantit déjà le secret cryptographique (la base entière devient illisible sans la clé Keystore détruite). Écraser le fichier complet n'apporte rien sur eMMC / UFS moderne avec wear-leveling : les blocs physiques ne correspondent plus aux blocs logiques. 16 Mo suffisent pour neutraliser le header SQLCipher et un préfixe raisonnable. Bénéfice marginal vs latence du panic mode → 16 Mo. setUserAuthenticationRequired(false)sur les clés Keystore PIN (ajouté en v0.9.4) : le PIN applicatif est l'unique facteur d'authentification du coffre. Le doubler par une exigence biométrique exposerait l'utilisateur à la contrainte physique (un attaquant peut forcer un doigt sur le capteur, et une clé dérivée biométrique survit au reboot). Le PIN seul, combiné au scellage Keystore device-bound et à l'auto-wipe à 5 essais, offre un meilleur compromis pour le modèle de menace « contrainte ».- AAD =
folder_id/note_id: empêche un attaquant local d'extraire un blob chiffré et de le rejouer dans le contexte d'un autre dossier ou d'une autre note (aucune confusion possible entre contextes cryptographiques distincts). - Reindex backlinks différé 2 s (v0.9.3) : évite le coût quadratique sur saisie active, tout en garantissant la cohérence de l'index avant toute fermeture / lock du coffre.