Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
17 commits
Select commit Hold shift + click to select a range
2e412c8
feat(import): one Markdown reader for the share sheet and the import
gitubpatrice Oct 29, 2026
fa44054
feat(share): "Share -> Notes Tech" creates notes in the Inbox
gitubpatrice Oct 29, 2026
1e4d1d7
fix(share): the security review of the share feature, all five findings
gitubpatrice Oct 29, 2026
2f01e5f
docs(3.2.0): a generated backup code by default, a chosen phrase as a…
gitubpatrice Oct 29, 2026
a1a7b1a
feat(editor): "Find in note", highlighted, from the menu and from a s…
gitubpatrice Oct 29, 2026
d683f03
feat(settings): import Markdown or text files, or a ZIP of them
gitubpatrice Oct 29, 2026
83189de
Merge remote-tracking branch 'origin/main' into feat/3.2.0
gitubpatrice Oct 29, 2026
ebb828d
feat(backup): the .ntbackup file, version 1, and its secret
gitubpatrice Oct 29, 2026
574eeae
feat(backup): write a backup, read it back, restore it in two passes
gitubpatrice Oct 29, 2026
27b3f1a
feat(backup): the Settings' encrypted backup, written before the picker
gitubpatrice Oct 29, 2026
d7ba5ee
fix(backup): what the GPT-5.6 review of the backup code found
gitubpatrice Oct 29, 2026
5bebff2
fix(editor): write the body in Android's own text field
gitubpatrice Oct 30, 2026
673ba1b
fix(preview): draw a note shown as written in pieces
gitubpatrice Oct 30, 2026
4fc4752
fix(import): bring in notes of at most 500 000 characters
gitubpatrice Oct 30, 2026
976b293
fix(editor): a long body scrolls under the finger, and flings
gitubpatrice Oct 30, 2026
187f595
fix: what the 3.2.0 pre-release audit and i18n check found
gitubpatrice Oct 30, 2026
e64347b
release: 3.2.0 (503) — changelogs, store listing, legal, READMEs
gitubpatrice Oct 30, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
32 changes: 30 additions & 2 deletions README.fr.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@

🇬🇧 [English version](README.md)

**v3.1.1 — octobre 2026** · [Politique de confidentialité](PRIVACY.md) · [Conditions d'utilisation](TERMS.md) · [Sécurité](SECURITY.fr.md)
**v3.2.0 — octobre 2026** · [Politique de confidentialité](PRIVACY.md) · [Conditions d'utilisation](TERMS.md) · [Sécurité](SECURITY.fr.md)

Application de prise de notes Markdown chiffrée pour Android, écrite en **Kotlin** avec Jetpack
Compose. **100 % locale, sans permission Internet.** Interface en **anglais, français, allemand,
Expand All @@ -21,6 +21,26 @@ vérifier.**

---

## Nouveautés de la 3.2.0

- **Sauvegarde chiffrée** (Réglages → Sauvegarde chiffrée) : vos notes et dossiers, corbeille comprise,
dans un seul fichier `.ntbackup`, chiffré (Argon2id, AES-256-GCM) par un code généré ou une
passphrase de votre choix. Un coffre à passphrase voyage tel quel, toujours scellé par sa propre
passphrase ; un coffre à code ouvert voyage dans le chiffrement de la sauvegarde et reçoit un nouveau
code à la restauration. Restaurer ajoute et n'écrase jamais : tout le fichier est vérifié d'abord,
puis écrit en une seule transaction.
- **Import** de fichiers Markdown ou texte, ou d'une archive ZIP de ces fichiers — un export de Notes
Tech, un coffre Obsidian : dossiers conservés, frontmatter lu, jamais dans un coffre. 500 000
caractères au plus par note.
- **Rechercher dans la note** : occurrences surlignées, « 3 / 12 », précédente et suivante, en lecture
comme en écriture ; une note ouverte depuis un résultat de recherche s'ouvre avec sa recherche.
- **Partager vers Notes Tech** depuis une autre appli : un texte devient une note dans la Boîte de
réception, des fichiers texte sont importés — après le verrou de l'appli, s'il est activé.
- **Notes longues** : le corps s'écrit désormais dans le champ de texte d'Android, qui ne dessine que
les lignes à l'écran — sur un Galaxy S9, d'environ 1 s par frappe à 100 000 caractères à environ
20 ms. Le clavier ne lit que le texte autour du curseur, le défilement glisse sur sa lancée, et la
lecture d'une très longue note ne fige plus l'appli.

## Nouveautés de la 3.1.1

- **Corrigé** : en 3.1.0, un appui long dans une note en cours d'édition fermait l'appli ; impossible de
Expand Down Expand Up @@ -132,6 +152,14 @@ avec sa propre clé. Une copie installée depuis F-Droid reçoit la 3.0.0 par F-
### Recherche
- Recherche plein texte **FTS5** instantanée (analyseur `unicode61`, accents ignorés). Le texte
supprimé est aussi effacé de l'index (`secure-delete`).
- **Rechercher dans la note** : les occurrences d'une recherche surlignées dans la note à l'écran, l'une
après l'autre.

### Sauvegarde, import et partage
- **Sauvegarde chiffrée** dans un fichier `.ntbackup` que vous gardez, restaurée sur ce téléphone ou un
autre.
- **Import** de fichiers Markdown ou texte et d'archives ZIP ; **partage** de textes ou de fichiers depuis
d'autres applis.

### Dictée Whisper
- **whisper.cpp 1.8.3**, compilé dans l'application à partir de ses sources et exécuté sur l'appareil.
Expand Down Expand Up @@ -163,7 +191,7 @@ avec sa propre clé. Une copie installée depuis F-Droid reçoit la 3.0.0 par F-
## Installation

1. **APK publié** — depuis les [Releases GitHub](https://github.com/gitubpatrice/notes_tech/releases),
l'APK universel (`notes-tech-universel-3.1.1.apk`, tous téléphones), ou le fichier plus léger de
l'APK universel (`notes-tech-universel-3.2.0.apk`, tous téléphones), ou le fichier plus léger de
votre appareil (`arm64-v8a` convient à presque tous les téléphones depuis 2016). Vérifiez l'empreinte SHA-256 publiée dans les notes de version.
2. **F-Droid** — [f-droid.org/packages/com.filestech.notes_tech](https://f-droid.org/packages/com.filestech.notes_tech/),
construite et signée par F-Droid.
Expand Down
27 changes: 25 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@

🇫🇷 [Version française](README.fr.md)

**v3.1.1 — October 2026** · [Privacy policy](PRIVACY.md) · [Terms of use](TERMS.md) · [Security](SECURITY.md)
**v3.2.0 — October 2026** · [Privacy policy](PRIVACY.md) · [Terms of use](TERMS.md) · [Security](SECURITY.md)

Encrypted Markdown note-taking app for Android, written in **Kotlin** with Jetpack Compose.
**100% local, no Internet permission.** Interface in **English, French, German, Italian and
Expand All @@ -20,6 +20,24 @@ technically unable to send anything, and you can check that in its manifest.**

---

## What's new in 3.2.0

- **Encrypted backup** (Settings → Encrypted backup): your notes and folders, trash included, in one
`.ntbackup` file, encrypted (Argon2id, AES-256-GCM) by a generated code or a passphrase you choose.
A passphrase vault travels as it is, still sealed by its own passphrase; an open PIN vault travels
inside the backup's encryption and gets a new PIN when restored. Restoring adds and never
overwrites: the whole file is checked first, then written in one transaction.
- **Import** Markdown or text files, or a ZIP of them — a Notes Tech export, an Obsidian vault:
folders kept, frontmatter read, never into a vault. At most 500,000 characters per note.
- **Find in note**: matches highlighted, "3 / 12", previous and next, while reading and writing; a
note opened from a search result opens with its search.
- **Share to Notes Tech** from another app: a text becomes a note in the Inbox, text files are
imported — after the app lock, when it is on.
- **Long notes**: the body is now written in Android's own text field, which draws only the lines on
screen — on a Galaxy S9, from about 1 s per keystroke at 100,000 characters to about 20 ms. The
keyboard reads only the text around the cursor, scrolling flings, and reading a very long note no
longer freezes.

## What's new in 3.1.1

- **Fixed**: in 3.1.0, a long press inside a note being edited closed the app, so text could not be
Expand Down Expand Up @@ -127,6 +145,11 @@ A copy installed from F-Droid receives the 3.0.0 through F-Droid.
### Search
- Instant **FTS5** full-text search (`unicode61` tokenizer, diacritics folded). Deleted text is
erased from the index too (`secure-delete`).
- **Find in note**: the matches of a search highlighted in the note on screen, one after the other.

### Backup, import and sharing
- **Encrypted backup** to a `.ntbackup` file you keep, restored on this phone or another one.
- **Import** of Markdown or text files and ZIP archives; **share** text or files from other apps.

### Whisper voice dictation
- **whisper.cpp 1.8.3**, compiled into the app from source and run on the device.
Expand Down Expand Up @@ -157,7 +180,7 @@ A copy installed from F-Droid receives the 3.0.0 through F-Droid.
## Installation

1. **Published APK** — from [GitHub Releases](https://github.com/gitubpatrice/notes_tech/releases),
the universal APK (`notes-tech-universel-3.1.1.apk`, any phone), or the lighter one for your device
the universal APK (`notes-tech-universel-3.2.0.apk`, any phone), or the lighter one for your device
(`arm64-v8a` fits almost every phone since 2016). Check the SHA-256 published in the release notes.
2. **F-Droid** — [f-droid.org/packages/com.filestech.notes_tech](https://f-droid.org/packages/com.filestech.notes_tech/),
built and signed by F-Droid.
Expand Down
31 changes: 30 additions & 1 deletion SECURITY.fr.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,10 +2,39 @@

*English version: [SECURITY.md](SECURITY.md)*

**Version actuelle : v3.1.1 — octobre 2026.** Notes Tech 3.0 est une réécriture en Kotlin. Le journal
**Version actuelle : v3.2.0 — octobre 2026.** Notes Tech 3.0 est une réécriture en Kotlin. Le journal
de sécurité des versions Flutter 1.x et 2.x est conservé tel quel, dans
[`SECURITY.fr.md` au tag v2.0.9](https://github.com/gitubpatrice/notes_tech/blob/v2.0.9/SECURITY.fr.md).

## v3.2.0 — sauvegarde chiffrée, import, partage entrant, le champ du corps (2026-10-30)

- **Sauvegarde chiffrée** (`.ntbackup` v1) : un en-tête de 48 octets authentifié comme données associées
(version du format, paramètres Argon2id — t = 3, m = 64 Mio, p = 1, refusés avant toute dérivation
s'ils diffèrent —, sel, nonce), puis AES-256-GCM en flux ; le contenu est lu jusqu'à la fin de son
flux, donc l'étiquette est toujours vérifiée. Bornée à 64 Mio sur ce qui est réellement lu. Le fichier
est écrit dans le cache de l'appli AVANT l'ouverture du sélecteur — tout coffre se verrouille quand
l'appli passe en arrière-plan —, puis copié et relu, comparé par SHA-256 ; la copie du cache est
effacée à chaque sortie, à chaque démarrage et par le mode panique. Restaurer, c'est une passe d'essai
puis une passe d'écriture dans une seule transaction Room, liée aux octets de l'essai par SHA-256 ; les
clés Keystore qu'une passe échouée a créées pour des coffres à code ne sont supprimées que si leur
coffre n'est pas en base. Le mode panique n'atteint pas une sauvegarde rangée hors de l'appli, et
l'écran le dit. Deux relectures externes (GPT-5.6), de la conception et du code ; chaque constat traité.
- **Import et partage entrant** : UTF-8 strict, 500 000 caractères au plus par note ; 5 000 fichiers et
100 Mio décompressés par import (une bombe ZIP l'arrête), 20 fichiers et 16 Mio par partage, lus un
fichier à la fois ; jamais dans un coffre. L'activité de partage est exportée mais sans interface :
elle borne ce qu'elle reçoit et le confie en mémoire à l'appli. `MainActivity` jette toujours tous
les extras, et la note n'est créée qu'après le verrou de l'appli.
- **Le corps d'une note est un `EditText`** — Compose dessine toutes les lignes d'un texte à chaque
image, et une longue note devenait impossible à écrire. Ce qu'un `EditText` ouvre, refermé : la barre
de sélection garde couper, copier, coller et tout sélectionner (ni Partager, ni Saisie automatique, ni
appli `PROCESS_TEXT` — Android 10 et avant les listent toutes) ; Copier et Couper passent par le
presse-papiers protégé d'une note de coffre, et une coupe n'efface son texte qu'une fois copié ; ni
saisie automatique, ni capture de contenu (Android 11 et suivants ; la fenêtre d'une note de coffre est
en `FLAG_SECURE`), ni correcteur orthographique, rien dans l'état sauvegardé. Une note de coffre
demande au clavier de ne pas apprendre, et sa sélection ne peut pas être glissée vers une autre appli.
Le clavier lit 5 000 caractères autour du curseur, jamais la note entière — une longue sélection, rien.
Relu par GPT-5.6 ; chaque nouvelle défense vue échouer une fois retirée.

## v3.1.1 — l'appui long qui fermait l'éditeur (2026-10-29)

- **La 3.1.0 se fermait à chaque appui long dans l'éditeur** : impossible de coller, de copier ou de
Expand Down
30 changes: 29 additions & 1 deletion SECURITY.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,10 +2,38 @@

*Version française : [SECURITY.fr.md](SECURITY.fr.md)*

**Current version: v3.1.1 — October 2026.** Notes Tech 3.0 is a rewrite in Kotlin. The security log
**Current version: v3.2.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](https://github.com/gitubpatrice/notes_tech/blob/v2.0.9/SECURITY.md).

## v3.2.0 — encrypted backup, import, incoming share, the body field (2026-10-30)

- **Encrypted backup** (`.ntbackup` v1): 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. `MainActivity` still 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 an `EditText` opens, closed: the selection toolbar keeps cut, copy,
paste and select all (no Share, no Autofill, no `PROCESS_TEXT` app — 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 is
`FLAG_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.

## v3.1.1 — the long press that closed the editor (2026-10-29)

- **3.1.0 closed on every long press in the editor**, so no note could be pasted into, copied from or
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,6 @@ package com.filestech.notes_tech.data.export
import android.content.Context
import androidx.test.ext.junit.runners.AndroidJUnit4
import androidx.test.platform.app.InstrumentationRegistry
import com.filestech.notes_tech.core.crypto.SecretBytes
import com.filestech.notes_tech.data.local.DatabaseProvider
import com.filestech.notes_tech.data.local.LegacyDatabaseFixture
import com.filestech.notes_tech.data.local.LegacyDatabaseLocation
Expand All @@ -15,12 +14,8 @@ import com.filestech.notes_tech.data.repository.NotesRepository
import com.filestech.notes_tech.security.kek.KekRepository
import com.filestech.notes_tech.security.kek.WritableKekSource
import com.filestech.notes_tech.security.vault.FolderVaultService
import com.filestech.notes_tech.security.vault.KeystoreUnavailableException
import com.filestech.notes_tech.security.vault.KeystoreEnMemoire
import com.filestech.notes_tech.security.vault.MonotonicClock
import com.filestech.notes_tech.security.vault.SealedByKeystore
import com.filestech.notes_tech.security.vault.VaultCrypto
import com.filestech.notes_tech.security.vault.VaultKeystore
import com.filestech.notes_tech.security.vault.VaultParams
import com.filestech.notes_tech.security.vault.VaultSessions
import com.filestech.notes_tech.security.vault.VaultWipeJournal
import com.google.common.truth.Truth.assertThat
Expand Down Expand Up @@ -340,39 +335,6 @@ class NoteExporteurTest {
override fun elapsedMillis(): Long = 0L
}

private class KeystoreEnMemoire : VaultKeystore {
private val cles = mutableMapOf<String, ByteArray>()

override fun createKey(alias: String): Boolean {
if (cles.containsKey(alias)) return false
cles[alias] = SecretBytes.randomBytes(VaultParams.FOLDER_KEY_BYTES)
return true
}

override fun seal(alias: String, plaintext: ByteArray): SealedByKeystore {
val cle = cles[alias] ?: throw KeystoreUnavailableException()
val nonce = VaultCrypto.newNonce()
return SealedByKeystore(VaultCrypto.seal(cle, nonce, plaintext, ByteArray(0)), nonce)
}

override fun open(alias: String, sealed: SealedByKeystore): ByteArray {
val cle = cles[alias] ?: throw KeystoreUnavailableException()
return VaultCrypto.open(cle, sealed.nonce, sealed.ciphertext, ByteArray(0))
}

override fun deleteKey(alias: String) {
cles.remove(alias)
}

override fun deleteKeysWithPrefix(prefix: String): Int {
val vises = cles.keys.filter { it.startsWith(prefix) }
vises.forEach(cles::remove)
return vises.size
}

override fun hasKey(alias: String): Boolean = cles.containsKey(alias)
}

private companion object {
const val DOSSIER_NOM = "Coffre"
const val PHRASE = "une phrase secrete de test 2026"
Expand Down
Loading
Loading