Skip to content

Security: gitubpatrice/notes_tech

Security

SECURITY.md

Security policy — Notes Tech

⚠️ É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.

v2.0.0 — Retrait de l'IA + durcissement coffre/panique (2026-08-07)

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.

Failles corrigées

  • Perte de données irréversible à la suppression d'un dossier coffre. folders_drawer.dart dé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, sous try/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 ; _wipeExportsCache purge 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. NotesRepository refuse 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 : onCreate posait le schéma courant et onUpgrade n'était jamais appelé. integration_test/db_migration_test.dart construit 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.

Vérification

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.

v1.1.6 — Logo & réorganisation barre du haut (2026-07-09)

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").

v1.1.5 — Audit expert post-v1.1.4 (2026-07-07)

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.

Sécurité / robustesse

  • Purge des sidecars WAL/SHM/journal EN CLAIR lors de la migration DB héritée plain → chiffré (database.dart._migratePlainToEncrypted). Le mode journal_mode = WAL laissait notes_tech.db-wal/-shm contenant des pages de notes en clair, survivant à la migration (fuite sur device rooté / extraction physique). Désormais purgés après sqlcipher_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 mounted ajoutées (ai_chat après l'isolate MiniLM, search post-frame) : plus de setState après dispose.
  • 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ès voiceCancel) qui appelle NoteActions.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.

Maintenance

  • Purge de 99 clés l10n orphelines (jamais référencées) des ARB FR/EN — parité conservée (380 clés chacune), flutter analyze 0 issue confirme qu'aucune n'était utilisée.

v1.1.0 — Audit expert post-v1.0.9 (2026-05-14)

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).

Sécurité

  • F1note_editor._moveToFolder : confirmation EXPLICITE (dialog destructif cs.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.
  • F2NotesDao.findByTitleLike : ajout du filtre SQL encrypted_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 le limit SQL était consommé par les notes vault AVANT le filtre Dart → les suggestions s'amincissaient sur les gros coffres sans raison apparente. Defense-in-depth.
  • F3IndexingService._indexAll : skip explicite des notes vault AVANT _encodeWith(embedder, note). Avant : si knownHashes[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 via live.encryptedContent != null. Désormais aucun feed à l'embedder pour les notes locked, quel que soit le hash.
  • F4NoteActions.copyMarkdown : MethodChannel natif Kotlin com.filestech.notes_tech/clipboard.copySensitive qui pose ClipDescription.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.setData brut exposait le plaintext d'une note vault déchiffrée à TOUT clipboard manager tiers + Knox clipboard history sans expiration.
  • F5ai_chat_screen._resolveSource : suppression du initialDirectory: '/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 .task malveillant non lié au flux SAF maître.
  • F6VoiceService._isPresentAndPlausible : TTL du cache de vérification SHA-256 réduit de 30 jours à 24 heures, et refus si cached.mtimeMs > cached.verifiedAtMs (file touché après notre dernière vérif réussie). Avant : un attaquant root pouvant écrire un Whisper trojanisé avec touch -t matchant (size, mtime) restait validé 30 jours sans rehash. Coût utilisateur : ~3-5 s de hash strict au premier startRecording post-24h.
  • F7PanicService : nouvelle étape _wipeExportsCache qui purge getApplicationCacheDirectory()/exports/. Avant : un ZIP d'export en cours de Share survivait à panic car _purgeTempDirectory ne couvrait que getTemporaryDirectory().
  • F8RagService._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.
  • F10FolderVaultService._unlockInProgress: Set<String> guard sur unlock() / unlockWithPin(). Avant : Dart est mono-thread mais Argon2id compute() (600-900 ms sur S24) cède l'event-loop entre await — un Timer(_autoLockAfter) pouvait alors firer pendant le unlock et wiper la folder_kek fraîchement assignée avant qu'elle ne soit consommée par encryptNote. _autoLockSweep skip désormais les folderIds en cours de unlock.
  • F11note_editor._flushFinalSave : si le coffre est verrouillé pendant le flush final (dispose post-auto-lock), on persiste l'id dans prefs.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.
  • F14AppDatabase._attachSql : validation regex stricte ^[A-Za-z0-9_./:\\-]+$ du path AVANT l'ATTACH. Avant : le path provenait de getApplicationDocumentsDirectory(), qui peut être détourné via LD_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.

Performance

  • P1HomeScreen._reloadDebouncer (250 ms) coalesce les events notes.changes pendant l'auto-save continu (1 event/500 ms par frappe). Avant : un SELECT complet listAllAlive exécuté à CHAQUE event, soit ~50-200 ms SQLCipher sur 500 notes × fréquence de frappe.
  • P2isUniversalApk = false dans build.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.
  • P3BacklinksService._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).
  • P5MentionsLegalesScreen._MarkdownAssetView._load : cache static final Map<String, String> process-wide des assets .md. Avant : rootBundle.loadString re-exécuté à CHAQUE switch d'onglet TabBarView ou de locale.

UX / a11y

  • U1HapticFeedback.selectionClick() sur copy Markdown
    • HapticFeedback.heavyImpact() sur déclenchement panique. Avant : 0 hit HapticFeedback dans tout lib/ — aucun feedback tactile pour les actions critiques (alignement avec Pass Tech v2.4.4 U9 / AI Tech U4).
  • U2SnackbarMessengerExt.showFloatingSnack accepte désormais foregroundColor. 2 sites folders_drawer mis à jour : cs.errorContainer + cs.onErrorContainer (contraste WCAG AA ~13:1 en light mode vs ~3.5:1 mesuré avec cs.error brut sur textPri clair).
  • 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.

v1.0.9 — Audit expert post-v1.0.8 (2026-05-13)

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.

Sécurité

  • F1FolderVaultService.unlock() (mode passphrase) bénéficie désormais du même lockout exponentiel monotonique que unlockWithPin() (M-05 v1.0.7) : compteur RAM _passFailCount + backoff 1/2/4/8/16/30 s après 5 essais, levant VaultLockoutInProgressException. 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 publique passphraseLockoutRemaining() exposée pour countdown UI symétrique au PIN.
  • F3BacklinksService.suggestTitles() filtre maintenant n.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, _reindexAll qui skippaient déjà locked).
  • F7RagService.composePrompt applique désormais _sanitize au userPrompt (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).
  • F8note_editor_screen pose FLAG_SECURE (_ensureSecureForced) AVANT vault.decryptNote. Avant : fenêtre ~5-20 ms (channel round-trip) pendant laquelle un screenshot manuel ou MediaProjection pouvait capter le plaintext entre decryptNote et _ensureSecureForced.

Performance

  • P1.2note_editor_screen._changesSub filtre 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).

UX / a11y

  • U1+U2+U11PassphraseTextField (centralisé) ajoute autofillHints: const [] (désactive Samsung Pass / Google Autofill), keyboardType: TextInputType.visiblePassword (neutralise SwiftKey/Gboard auto-cap), et enableInteractiveSelection: !_hidden (bloque sélection/copie quand masqué — anti clipboard manager).
  • U3confirmDialog (helper centralisé app_dialogs.dart) : bouton Annuler autofocus: true quand le dialog est destructif + bouton confirme via cs.errorContainer/onErrorContainer au lieu de cs.error brut.
  • U4about_screen icône Image.asset avec cacheWidth: 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).

Info-only nettoyés (analyze 0 issue)

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

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_enabled préservés) conformément à PRIVACY.md.
  • flutter_markdown cantonné aux pages légales locales (assets), aucun rendu de markdown distant.
  • ProGuard rules 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).

Reporting a vulnerability

If you believe you've found a security issue in Notes Tech, please do not open a public GitHub issue. Instead, email :

📧 contact@files-tech.com

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.

Scope

In scope

  • Notes Tech app code (lib/, android/)
  • Module sibling files_tech_voice if 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

Out of scope

  • 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.

Threat model

Notes Tech is designed for individuals and professionals who want local-only notes with strong cryptographic guarantees. Three adversary classes are considered :

1. Loss / theft (lost or stolen unlocked device)

  • 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.

2. Coercion (search, "give me your phone", border check)

  • 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).

3. Sandbox malware on the same device

  • No INTERNET permission 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, no POST_NOTIFICATIONS, no RECEIVE_BOOT_COMPLETED — minimal attack surface.
  • FLAG_SECURE blocks Recents previews and screen recording.
  • allowBackup=false + dataExtractionRules block Smart Switch / Android Backup exfiltration.

Crypto building blocks

  • Argon2id RFC 9106 for passphrase derivation : m=64 MiB, t=3, p=1, 32-byte output (vault default). PIN mode uses lighter parameters m=32 MiB, t=2 because 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_storageEncryptedSharedPreferences → Keystore.

Panic mode — ordered multi-step

The panic sequence is deterministic and best-effort (a step that throws does not abort the next ones) :

  1. FLAG_SECURE forced ON, microphone capture stopped
  2. foldersLockAll — lock every open vault, wipe folderKek from RAM
  3. pinKeysWipedeleteKeysWithPrefix("vault_pin_") (Kotlin)
  4. kekDestroy — destroy the master Keystore key (DB instantly unreadable)
  5. Background workers paused (backlinks)
  6. dbWipe — overwrite SQLCipher header (16 MiB cap) + delete .db, .db-wal, .db-shm
  7. Whisper model file deleted, all prefs cleared, tmp purged

Accepted limits

  • 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.

Responsible disclosure

We follow a 90-day disclosure window by default :

  1. Day 0 : your report received.
  2. Day 0-7 : initial triage, severity assigned.
  3. Day 7-60 : fix developed, tested, audited.
  4. Day 60-90 : release with patched version, public CVE if applicable.
  5. 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.

Security audits run on each release

Each release is checked via :

  • flutter analyze (lints stricts)
  • flutter test
  • bash j:\applications\health_check.sh notes_tech :
    • OSV-Scanner (CVE in dependencies)
    • gitleaks (secrets in git history)
    • Manifest hardening (no debuggable, no cleartextTraffic, 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_IMMUTABLE PendingIntent)
  • 4-agent audit (architecture / security / performance / coherence) for material features.

Code source : https://github.com/gitubpatrice/notes_tech Licence : Apache License 2.0

Décisions de design

  • Wipe DB header plafonné à 16 Mo : la kekDestroy pré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.

There aren't any published security advisories