A running note from my Google Summer of Code project with Joplin.
Joplin already protects notes while they sync. This project adds a separate way to lock a note on the device too.
I am starting with individual note locking, then making the surrounding paths such as history, search, attachments, mobile, CLI, and the API handle locked notes safely.
June 26#
Another proposal cleanup day.
The old unlock timeout idea is gone now. Explicit Unlock encrypted notes / Lock encrypted notes feels easier to reason about, and there will be an optional Auto lock when switching note setting for people who want that behaviour.
June 25#
Export came up today, and it was a good catch.
If a backup keeps notes locked, restoring it later can depend on a key that might not exist anymore. So exports should write them out as normal unlocked notes after decrypting them.
June 24#
The search/indexing PR merged today.
This one also ended up covering the new AI embedding index, not just normal search. Nice to have both following the same rule.
June 23#
Good day for small PRs. The attachment indexing PR merged, and the mobile feature-flag fix merged too:
The feature-flag fix was tiny, but useful. A private flag was showing on mobile, so mobile now filters settings the same way desktop does.
June 22#
Went back through the plan with the repo in its current state. Since PR1 and PR2 are merged now, the next PRs can be smaller and independent, which should make review less tangled.
June 21#
Spent time on ResourceService review fixes. Mostly small cleanup around extracted_resource_ids.
June 20#
Kept the attachment indexing PR moving through review. The main thing was making sure saved resource IDs still go through Joplin’s existing validation instead of trusting them directly.
June 19#
Opened the third PR today. A locked note’s body is ciphertext, so the normal resource scanner cannot see its attachment links. The change rebuilds those associations from the resource ids saved before encryption.
Also tested the same path after sync with two profiles.
June 18#
The backend PR merged today. One last fix made the decrypt helper return a copy instead of changing the loaded note object.
Then finished the ResourceService change and its manual test for the next PR.
June 16#
Answered another backend review pass. Clarified why useNoteLock is opt-in: normal loads return the persisted body, which is ciphertext for a locked note, and only unlock-aware callers should request plaintext.
June 14#
Applied another set of backend review fixes. Replaced the local resource-id regex with Joplin’s existing isItemId helper and tightened the missing body and lock-state checks.
June 13#
Posted the week 3 forum update. The backend PR is now open in the main repo, targeting dev like a normal PR instead of sitting stacked on a fork branch. A week of review and cleanup, and the whole path feels much more solid than it did last Friday.
June 12#
Last round of review on the backend draft: five small comments, all fair. Applied them, restacked the branch onto the freshly merged dev so the diff is only this week’s slice, and ran the whole checklist again.
June 11#
The schema PR merged. First gsoc code in Joplin’s dev branch. Also pushed the reworked backend branch: the new naming everywhere, and the gated save stripped of its clever recovery logic.
June 10#
Simplified the gated save path after a review pass. My first version tried to recover from weird partial states, like a save missing the lock flag. But a gated save missing the lock state is really a bug in the caller, so it should fail loudly there instead of quietly saving something wrong. Less code, clearer rules.
June 9#
The schema PR got reviewed, and the naming changed for the better. is_locally_encrypted is gone, because “local” stops meaning anything once a note syncs somewhere. It is just is_locked now. That pulled the backend naming along with it: noteLock, NoteLockKey, NoteLockService. I like it more. Locked is the feature, encryption is just how it works underneath.
June 6#
Pushed the structure changes to the week 2 draft. Also did the E2EE manual pass with two clean profiles: encrypted note and attachment one way, edit back the other way. Good kind of boring.
June 5#
Posted the week 2 forum update. This week was mostly backend work: local key path, gated save/load, and making sure history does not quietly keep old readable copies after a note is locked.
June 4#
Kept the week 2 draft moving on the fork. Branch chaining is useful, but I can already see why keeping only one chained PR open matters.
June 3#
Opened the tracking issue and put the backend PR up as a draft, stacked on the schema work so the diff only shows this slice. Early feedback while it is still small.
Still all backend. No lock button yet, nothing a user can press. That comes once this path is solid underneath.
June 2#
Spent the day trying to break it. The cases I care about are the quiet ones: a damaged or half-written locked note should fail closed and leave the note and the database untouched, not save something broken.
Also the history. When a note first gets locked, the old plaintext revisions have to go, or the thing you just locked is still sitting there in its edit history. Easy to forget, bad to miss.
June 1#
Pulled the week 2 work into one backend PR instead of splitting it: the encryption service key path, the local key wrapper, and the gated note load and save. They only really make sense together.
The schema PR has not merged yet, so this one stacks on top of it as a draft. Building on a branch that is not in yet is a little awkward, but it keeps the diff small and honest about what depends on what.
May 30#
Took the schema testing a bit deeper, following a suggestion from mrjo. Checked that revisions still get created after the new column, and looked at the raw items on disk with E2EE on. The note body is an unreadable blob while is_locally_encrypted stays a plain 0 next to it.
Good way to learn where to actually look when testing a different part of the app.
May 29#
Manual pass of the PR on desktop. Two profiles syncing through a file system target, with a notebook, a few notes and an image, round-tripping both ways.
Getting the dev build to cooperate took longer than the test itself. Also posted my first weekly update on the forum, planning to do one every Friday.
May 28#
The first PR is up now. It is just the schema and sync metadata layer, which is exactly where this had to start.
It is a small start, but it feels good to have the first piece out in the open.
May 27#
Spent the day studying the schema and sync paths properly before opening the PR. A small database field does not stay small for long in an app like Joplin. It shows up in local tables, generated types, sync serialization, E2EE metadata, API responses, and server tests.
Also cleared up one detail for the upcoming encryption service work. The local-note wrapper needs the decrypted key and the key id together, but it does not need to carry the whole master key object around.
May 26#
Spent the day getting the clean dev repo into a working shape and turning the plan into an actual PR order. The goal is still small reviewable pieces, not one giant feature dump.
Also started keeping project learning notes alongside the code. I want to understand the Joplin paths as I touch them, not just move files around and hope the tests are kind.
May 25#
Coding starts today. The first thing is not the lock screen, even though that is the tempting part. It is the plumbing: schema and sync fields, old sync items mapping cleanly to the new shape, and the local note encryption key storage path.
Boring on purpose. I would rather get the data shape right before there are buttons people can press.
May 24#
Finished the last planning pass across the proposal, weekly plan, and risk assessment. The biggest cleanup was the key story. Local note encryption now has its own key path, separate from the normal sync E2EE master keys, with the encrypted key stored separately and only the decrypted key kept in memory after unlock.
Also made the feature guard more concrete: during development, the local encryption entry points should only be enabled in dev mode, so prerelease users do not run into a half-built lock feature by accident.
Also updated the diagrams so they match the text again.
May 23#
Spent most of the day on another proposal/risk pass. The annoying bug today was attachments. If a locked note hides its markdown body, Joplin can no longer see the resource links inside it, and orphan cleanup might later delete an attachment that is still used by that locked note.
The fix we settled on is pretty small: save the linked resource ids before encrypting the body, then let the resource indexer use those ids later. Small column, but it avoids a nasty data loss case.
May 22#
Went through another mentor review pass. I had one thing wrong: the lock flag should stay as cleartext sync metadata, not be hidden inside the E2EE payload. That way a synced client knows a note is locked as soon as it downloads the item.
Also shuffled a few things into better places. Null-byte failures go with conflict/leak testing, revision cleanup belongs on the save/sync transition, and plugin/API paths should fail closed instead of trying to be clever.
May 21#
Updated the proposal from the review and finished the week by week plan, then sent both off. Now starting a risk assessment for the project. This one touches a lot of the app, and it is people’s actual notes, so it is worth writing down plainly what could break, who it would hit, and how we would even know. Better to stare at the worst case now than find it later.
May 20#
Read a Joplin PR that landed about saving notes that contain a null byte. It made something click. Taking encryption off a note is not just decrypt and done, the save right after can still fail. If it does, the note and the editor cannot be left half changed. That kind of quiet data loss is exactly what I want to catch before it ever ships, so it goes straight onto the testing list.
May 19#
A week until coding starts. I am turning the plan into a week by week version, walking the proposal section by section so nothing quietly falls off. Re-reading your own work is humbling. You always find the parts you waved past the first time.
May 15#
Settled how I want to contribute. Small PRs, straight to dev. The one thing to front-load is the schema work, all of it in one go, so a prerelease never ends up holding half a schema. Easy to miss, painful to unpick later.
May 14#
First intro call today. Laurent, mrjo, the other mentors, and the selected students were all there.
Nice to finally see faces. I also had a 1-1 with mrjo after the call. He was very kind, and I left feeling glad this is the group I get to learn with.
May 7#
The sync side got simpler today, which is the kind of small thing that makes a hard week feel less hard. A full vault/private-notebook model is too much for v1. It can wait.
May 5#
Spent the day re-reading the proposal and the old forum threads, walking through the parts of the codebase this will touch. Mobile, CLI, and API are easy to forget when you live in the desktop app. They shouldn’t be.
Apr 30#
Got selected.
Happy. A little stunned. Now to actually do the work.