Version française : SECURITY.fr.md
Current version: v3.1.0 — October 2026. Notes Tech 3.0 is a rewrite in Kotlin. The security log
of the Flutter versions 1.x and 2.x is kept as it was, in
SECURITY.md at tag v2.0.9.
- Trashing or erasing a vault note needs no key — the row goes, sealed blob and all. So the long press on a note, and the editor's new "Delete permanently", check the vault at execution, on a fresh read of the note: with its vault closed, the gesture asks for the secret and runs once it is open. Proved by sabotage: without the check, the refusal tests fail.
- A note's colour is stored outside the vault's envelope, like its tags (in the SQLCipher database). The lists show it on a vault note only while its vault is open, and hide it again at the automatic lock. The privacy policy now says it, for the tags too.
- Reading mode keeps a vault note's protections (FLAG_SECURE, session, automatic lock), opens no keyboard, and its rendering is not selectable — no copy path around the protected clipboard.
- Schema 10: one nullable column (
color_id), added by a migration replayed in tests on a Flutter base and on a base adopted by 3.0.0; no note or sealed blob changes. - Reviewed per change by two external models and a three-axis pre-release audit (no Critical, no High; one Medium fixed: a gesture waiting for a vault now dies with its unlock sheet).
The port was audited before release by a researcher-and-adversarial-panel method: five researchers,
one component and one angle each, then three verifiers per candidate whose job was to refute it.
No critical and no high finding. 3 medium, 13 low — every one fixed, each with a test and a
negative control (the defect put back to check that the test catches it). The full report, in
French, is in audits/securite-2026-09-26.md.
The three medium findings:
- V1 — closing a vault left the decrypted note on screen in the editor. The editor now clears it and asks for the secret again.
- K1 — deleted text, and text moved into a vault, stayed in the full-text index. FTS5 now runs in
secure-deletemode, SQLite withsecure_delete = ON, and existing indexes are purged once. - K4 — before Android 9, a PIN vault's key cannot be bound to the phone's unlock, so the five-attempt limit does not hold against a seized phone. No new PIN vault is created there any more; an existing one still opens and says so.
Only the latest published release receives security fixes: the app has no network access and never updates itself, so a fix only ships in a new version.
If you believe you've found a security issue in Notes Tech, please do not open a public GitHub issue. Use GitHub private reporting — the repository's Security tab, then Report a vulnerability (https://github.com/gitubpatrice/notes_tech/security/advisories/new) — or 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 (Settings → About Notes Tech).
- 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.
- The app code (
app/src/main/), including the JNI bridgeapp/src/main/cpp/notes_stt_jni.cpp - Crypto: database key handling, vault and app lock keys, panic mode irreversibility
- Permission handling (
RECORD_AUDIO, biometrics) - File handling: model import, exports, path traversal
- Issues in third-party code — including the vendored whisper.cpp sources — unless the app's use of it creates the issue (please report upstream too).
- Issues requiring a rooted device or pre-existing malware with elevated privileges.
- Social engineering against the user.
- Denial of service through deliberately oversized notes or files.
Notes Tech is designed for people 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 the notes of a vault. The database key is sealed by the Android Keystore (hardware-backed on modern devices).
- A vault adds a second factor (passphrase or PIN) on top of the phone's screen lock; the app lock adds one in front of the whole app.
- Panic mode: a confirmed wipe that runs an ordered, deterministic sequence (below).
- PIN auto-wipe: 5 failed PIN attempts on a vault delete that vault's key, atomically, resumed at the next launch if interrupted.
- No biometric factor on vault keys (
setUserAuthenticationRequired(false)): the PIN is the only factor — a finger can be forced onto a sensor. Biometrics exist only for the app lock, and they are off by default.
- No
INTERNETpermission: a compromised dependency has no standard network path to send notes. - No foreground service, no notification, no boot receiver: minimal attack surface.
FLAG_SECURE(on by default) blocks screenshots and the recent-apps preview.allowBackup=falseanddataExtractionRulesblock Android backup and device-transfer copies.- In a vault note, copying goes through a clipboard entry marked sensitive, cleared after 60 seconds,
and the keyboard is asked not to learn (
IME_FLAG_NO_PERSONALIZED_LEARNING).
- Argon2id (RFC 9106, Bouncy Castle):
m = 64 MiB, t = 3, p = 1, 32-byte output for a passphrase;m = 32 MiB, t = 2for a vault PIN and for the app lock PIN, whose Keystore keys and attempt limits are the main defence. - AES-256-GCM for note content with AAD = note id, and for the vault key wrap with AAD = folder id: an encrypted blob cannot be replayed in another note or folder.
- Constant-time verifier to detect a wrong passphrase or PIN without trial-decrypting the notes.
- Database key (KEK): 32 random bytes, sealed by an
AndroidKeyStoreAES-256-GCM key. SQLCipher 4 (AES-256, HMAC-SHA512). - Vault PIN keys: one
AndroidKeyStorekey per vault, bound to the phone's unlock (setUnlockedDeviceRequired, Android 9+). - App lock: the PIN goes through Argon2id, then an HMAC-SHA256 key held by the Keystore; five free
attempts, then delays doubling from 30 seconds up to one hour. The optional biometric unlock uses
its own Keystore key, usable only through a strong (class 3) biometric in a
CryptoObject.
Settings → Panic mode, confirmed by typing the word it asks for (WIPE in English). The sequence is deterministic and best-effort: a
failing step does not stop the next ones, it is recorded, and the final screen reports an incomplete
wipe. A journal written before the first step makes an interrupted sequence resume at the next
launch.
- Secure window —
FLAG_SECUREforced on - Dictation — recording forbidden and the current one abandoned, before anything else
- Clipboard — cleared
- Vaults — every open vault locked, keys erased from memory
- Vault PIN keys — every
vault_pin_*Keystore key deleted, orphans included - App lock keys — its PIN verifier key and its biometric key deleted
- Database key — destroyed: from here on the database is noise
- Exports — archives holding the full text of notes erased
- Recordings — dictation recordings erased
- Database — closed, header overwritten (16 MiB), file and sidecars deleted
- Voice model — the imported model deleted
- Legacy models — model files left by versions ≤ 1.1.6
- Preferences — cleared, except
secure_window_enabled,db_encrypted_v1and the panic journal - Cache — previews, temporary files, library leftovers
- The database key is usable while the phone is locked, by code running as the app. It is deliberately not bound to the phone's unlock: Android deletes such a key when the screen lock is removed, which would destroy the database with it. Vaults keep their own secret.
- A copy of the database key kept by the 2.x versions (
flutter_secure_storage) is left in place: on Android 12 and later it is what recovers the database when Android has deleted the key the 2.x bridge created. Only panic mode removes it. - Removing the phone's screen lock deletes the keys of PIN vaults (Android does this to keys bound to the unlock): their notes become unreadable. The mode chooser warns before a PIN vault is created; a passphrase vault does not depend on the screen lock.
- A PIN vault created before Android 9 (by a 2.x version) does not hold its five-attempt limit against a seized phone. The app says so on that vault.
- A memory dump of an unlocked, rooted phone during use can reveal plaintext.
- A determined attacker with custom kernel exploits is out of scope.
FLAG_SECUREis on by default but can be turned off 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 (key extraction, full data exfiltration) may be patched faster.
- Unit tests (JVM) and instrumented tests on real phones (Android 10 and 16)
- Android lint, detekt, ktlint
tools/check-manifest-permissions.pyon the merged release manifest: no network permission may appear- Security audit with an adversarial panel for material changes, and external reviews
- Database header wipe capped at 16 MiB: destroying the Keystore key just before already makes the whole database unreadable. Overwriting the full file brings nothing on flash storage with wear-leveling, where physical blocks no longer match logical ones.
- No user authentication on vault PIN keys: the in-app PIN is the vault's only factor; a biometric requirement would expose the user to a forced fingerprint, and a biometric-bound key survives a reboot.
- AAD = folder id / note id: no confusion is possible between distinct cryptographic contexts.
- No unlocked-device binding on the database key — see Accepted limits.
Source code: https://github.com/gitubpatrice/notes_tech License: Apache License 2.0