Version française : SECURITY.fr.md
Current version: v3.2.2 — 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.
Compose foundation 1.12 adds to the selection toolbar of its text fields every app visible to Notes
Tech that declares an ACTION_PROCESS_TEXT action — any app may, under any name — and hands it the
selected text. Before Android 11 every installed app is visible. The note body, a native field,
already kept editing entries only; the title, search and vault fields did not. MainActivity now
turns ComposeFoundationFlags.isSmartSelectionEnabled off before the first composition, on every
Android version. The text classifier ("Translate") has been off since 3.1.1. Permissions, storage
and encryption are those of 3.2.1.
A navigation fix: two quick taps on a note's back arrow left an empty screen, a double tap on "New note" created a second note. Permissions, storage and encryption are those of 3.2.0.
- Encrypted backup (
.ntbackupv1): a 48-byte header authenticated as associated data (format version, Argon2id parameters — t = 3, m = 64 MiB, p = 1, refused before any derivation if different —, salt, nonce), then AES-256-GCM in streaming mode; the content is read to the end of its stream, so the tag is always checked. Bounded to 64 MiB on what is really read. The file is written in the app's cache BEFORE the system picker opens — every vault locks when the app goes to the background —, then copied and read back, compared by SHA-256; the cache copy is deleted on every way out, at each start and by the panic mode. Restoring is a trial pass, then a writing pass in one Room transaction, bound to the trial's bytes by SHA-256; the Keystore keys a failed pass created for PIN vaults are deleted only when their vault is not in the database. The panic mode cannot reach a backup saved outside the app, and the screen says so. Two external reviews (GPT-5.6), of the design and of the code; every finding handled. - Import and incoming share: strict UTF-8, at most 500,000 characters per note; 5,000 files and
100 MiB inflated per import (a ZIP bomb stops it), 20 files and 16 MiB per share, read one file at a
time; never into a vault. The share activity is exported but has no interface: it bounds what it
receives and hands it in memory to the app.
MainActivitystill drops every extra, and the note is created only after the app lock. - The note's body is an
EditText— Compose draws every line of a text at each frame, and a long note could not be written in. What anEditTextopens, closed: the selection toolbar keeps cut, copy, paste and select all (no Share, no Autofill, noPROCESS_TEXTapp — Android 10 and below list them all); Copy and Cut go through a vault note's protected clipboard, and a Cut removes its text only once written; no autofill, no content capture (Android 11 and later; a vault note's window isFLAG_SECURE), no spell checker, nothing in the saved state. A vault note asks the keyboard not to learn, and its selection cannot be dragged to another app. The keyboard reads 5,000 characters around the cursor, never the whole note — a long selection, nothing. Reviewed by GPT-5.6; every new defence seen failing when removed.
- 3.1.0 closed on every long press in the editor, so no note could be pasted into, copied from or
selected with the toolbar (#36). The cause is
the protected clipboard of the editor's fields (audit of 2026-09-26, F1): a plain Compose
Clipboard, which Compose 1.12, shipped with 3.1.0, refuses before it shows the selection toolbar. It is now anAndroidClipboard. What a vault note copies still goes the protected way (marked, cleared after 60 s, purged by the panic mode): the F1 tests are unchanged and pass. - An availability defect, not a leak: the app closed before anything was copied. New tests long-press a real field, ordinary note and vault note; they fail on the 3.1.0 code.
- The selection toolbar offered Android's "Translate", added by the system's text classifier; one
tap handed the selected text to another app — on the test emulator, the Google app, which searched it
online. A vault note's text left that way too, outside the protected clipboard: the family the audit
of 2026-09-26 closed by removing the
PROCESS_TEXTqueries, reached through another door. Likely present since 3.0.0: Compose 1.11 already carries these items (not measured); 3.1.0 hid them by crashing first. The activity now setsTextClassifier.NO_OPon itsTextClassificationManager, the one Compose reads for every field of every window, dialogs and sheets included. Lost: smart selection and the classifier's actions. Tested on the real activity, with a control; measured on the release build. - The Whisper Tiny model could never be imported: its SHA-256 in
SttModelCataloguematched no file ever published. The check itself held — it refused the genuine file. The hash is corrected from the file's LFS oid on Hugging Face, cross-checked against the SHA-1 the repository's README lists; the file has not changed upstream since 2023. Whisper Base's hash was right.
- 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