Skip to content

feat: encrypt profile entries with passphrase-derived key, migrate to typescript#7

Open
MoustaphaCamara wants to merge 77 commits into
masterfrom
mouss/260507-step6-encryption
Open

feat: encrypt profile entries with passphrase-derived key, migrate to typescript#7
MoustaphaCamara wants to merge 77 commits into
masterfrom
mouss/260507-step6-encryption

Conversation

@MoustaphaCamara
Copy link
Copy Markdown
Contributor

@MoustaphaCamara MoustaphaCamara commented May 7, 2026

small tweaks

  • moved css to dedicated folder
  • separated in files to optimize usage
  • style.css -> src/css/main.css
  • subfiles in src/css/buttons.css, css/inputs.css etc.
  • helpers in utils/helpers.ts
  • migrate to typescript

fix #2
fix #8 86745a6
fix #11 5bfa1b2
fix #12 5a10e8d
fix #14

encryption

Encrypt local profile entries with passphrase-derived keys

  • Replace stored profile passphrase hashes with passphrase-derived local encryption
  • generate an Ed25519 keypair when creating a profile
  • store the public key, key salt, and encrypted private key in profile index
  • derive anunlock key from the passphrase and use it to decrypt the private key
  • Encrypt entry title/body before saving to SQLite
  • Decrypt entry title/body after reading from SQLite
  • Clear in-memory profile keys on logout!

note: currently, the SQLite database file is still readable as a SQLite file, but entry content is encrypted. Table names, row counts, and timestamps are not encrypted in this PR. I'm not sure if we wanted to encrypt only the content or also the db itself, but files cant be read even via db. e.g.,

PROFILE=enter a profile uuid from index.json
DB="$HOME/.config/elabftw-desktop/profiles/$PROFILE/data.sqlite3"
sqlite3 "$DB"
.tables
# output: entries
.schema entries
# output: the schema of the entries table

Now try to read some

select id, title, body FROM entries;
# expected output: 1 | blablablahashed-title-long-random-looking-base64|blablablahashed-body-same-rdm-looking
.quit

Steps to reproduce & test

  • Create a new profile with a passphrase
  • Verify wrong passphrase does not unlock the profile
  • Create entries and confirm title/body are not readable in data.sqlite3
  • Verify entries decrypt correctly after unlocking with the right passphrase :)

Summary by CodeRabbit

  • New Features

    • Profiles now use per-profile cryptographic keys with in-memory unlock/lock and encrypted profile secrets.
    • Entry titles and bodies are encrypted at rest.
    • New profile selector UI with create, unlock, and delete flows.
  • Bug Fixes

    • Improved form submission, entry save/load flow, and error handling.
  • Style

    • Added global styles for alerts, buttons, inputs, profiles, and core theme tokens.
  • Chores

    • Tooling and dependency updates.

Review Change Stack

active profile set to null
app-state back to select-profile
did not know the ci in private repos is limited in minutes, monthly
so this one should be trigerred only on workflow dispatch, not on every push. Keeping the lint for every push/pr but not this one
Encrypt local profile entries with passphrase-derived keys

- Replace stored profile passphrase hashes with passphrase-derived local encryption
- generate an Ed25519 keypair when creating a profile
- store the public key, key salt, and encrypted private key in profile index
- derive anunlock key from the passphrase and use it to decrypt the private key
- Encrypt entry title/body before saving to SQLite
- Decrypt entry title/body after reading from SQLite
- Clear in-memory profile keys on logout!

note: currently, the SQLite database file is still readable as a SQLite file, but entry content is encrypted. Table names, row counts, and timestamps are not encrypted in this PR.
I'm not sure if we wanted to encrypt only the content or also the db itself, but files cant be read even via db.

Steps to reproduce & test

- Create a new profile with a passphrase
- Verify wrong passphrase does not unlock the profile
- Create entries and confirm title/body are not readable in `data.sqlite3`
- Verify entries decrypt correctly after unlocking with the right passphrase :)
- Replace stored profile passphrase hashes with passphrase-derived local encryption
- Generate a random per-profile salt when creating a profile
- Derive a local encryption key from the passphrase using Argon2id
- Store the salt and an encrypted verifier in the profile index
- Verify the passphrase by deriving the same key and decrypting the verifier
- Encrypt entry title/body before saving to SQLite
- Decrypt entry title/body after reading from SQLite
- Clear the in-memory profile key on logout
@MoustaphaCamara MoustaphaCamara changed the title step5feat: encrypt profile entries with passphrase-derived key step5feat: encrypt profile entries with passphrase-derived key, migrate to typescript May 13, 2026
@MoustaphaCamara MoustaphaCamara marked this pull request as ready for review May 13, 2026 10:27
@MoustaphaCamara MoustaphaCamara mentioned this pull request May 13, 2026
now it goes
- generate ed25519 keypair when creating a profile
- store the public key, salt, and encrypted private key in the profile index (it is needed ! to have something persistent that proves the passphrase is correct after the app restarts, while recovering the same Ed25519 identity)
- derive an unlock key from the passphrase using Argon2id
- Use the derived key to decrypt the encrypted private key when unlocking a profile
- Treat private key decryption failure as an invalid passphrase
- Encrypt entry title/body before saving to SQLite
- Decrypt entry title/body after reading from SQLite
- Clear in-memory profile keys on logout
@MoustaphaCamara MoustaphaCamara changed the title step5feat: encrypt profile entries with passphrase-derived key, migrate to typescript feat: encrypt profile entries with passphrase-derived key, migrate to typescript May 18, 2026
Copy link
Copy Markdown
Contributor

@NicolasCARPi NicolasCARPi left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

on create new entry, reset state (title), instead of asking other components to remember to do it

remove all ed25519 code: we don't sign anything.

crypto is:

passphrase + salt (stored in clear in json profile) = master key

use the master key to decrypt data, because it's authenticated, you'll get an error if it's not the correct key.

Comment thread frontend/wailsjs/go/models.ts Outdated
Comment thread frontend/src/utils/helpers.ts Outdated
Comment thread frontend/src/components/ProfileSelector/ProfileSelector.svelte Outdated
Comment thread frontend/src/components/ProfileSelector/ProfileSelectorList.svelte Outdated
Comment thread frontend/src/components/MainApp.svelte Outdated
Comment thread frontend/src/css/buttons.css Outdated
Comment thread frontend/src/css/inputs.css
@@ -0,0 +1,321 @@
.app-shell {
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

try and refactor this, remove many things

Comment thread frontend/src/css/profiles.css Outdated
@@ -0,0 +1,30 @@
import type { Action } from 'svelte/action';
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

try and use Attachments

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

2 participants