Skip to content

fix: require link possession (auth key) to fetch/delete passwordless notes - #40

Merged
emilkrebs merged 3 commits into
mainfrom
fix/require-auth-key-for-fetch-delete
Aug 8, 2026
Merged

emilkrebs merged 3 commits into
mainfrom
fix/require-auth-key-for-fetch-delete

Conversation

@emilkrebs

Copy link
Copy Markdown
Owner

The bug

vailnote delete https://vailnote.com/<id> (no #auth= fragment) deleted the note. The server never stored any verifier for the auth key — for passwordless notes, possession of the note ID alone was enough to fetch (which destroys auto-delete notes) or delete.

Root cause

Zero-knowledge means the server never sees the auth key. But it also means it can't check it — so the ID (12 hex chars, leaks from logs/proxies/history) became the de-facto capability for passwordless notes. The #auth= fragment was verified by nothing on the server.

The fix (zero-knowledge preserved)

Mirror the existing password-verifier pattern for the auth key:

  • Create: client sends authKeyHash = deterministic PBKDF2 hash of the auth key → server stores bcrypt(authKeyHash) (one-way; same as the password verifier)
  • Fetch/Delete: passwordless notes with a stored verifier now require a matching authKeyHash → 403 INVALID_PASSWORD_OR_AUTH_KEY otherwise; ID alone is never enough
  • Legacy: notes created without a verifier keep ID-addressable behavior until they expire — no breakage for existing notes or old clients
  • CLI: sends authKeyHash on create/read/delete; vailnote delete now refuses a link without #auth= (or a password) locally
  • Web client: NoteService/RemoteStorage thread the auth key from the link fragment and send the verifier on create/fetch/delete

Security notes

  • Server stores only one-way verifiers (bcrypt'd PBKDF2) — it still cannot produce plaintext or recover the auth key
  • Same attack surface as any password-verifier scheme: offline guessing only matters with a compromised server + weak auth key entropy; mitigated by PBKDF2 600k + bcrypt 12 rounds
  • Rate limiting (15 mutating req/min) already bounds online guessing

Verification

  • New test step: create with authKeyHash → fetch/delete without verifier = 403, with verifier = 200 (manual-deletion note so fetch doesn't destroy it)
  • Full suite: 28 passed (44 steps), 0 failed
  • deno check main.ts cli/main.ts clean

- llms-full.txt DELETE: passwordHash is required for password-protected
  notes (403 otherwise); only optional for unpassworded notes
- llms-full.txt .env workflow: consumers must resolve via 'vailnote env'
  before use; add rotation + expiry guidance
- README: never sends 'raw password' (only PBKDF2 hash for access control)
- README: mention 'env' command in AI-agent ready bullet
…notes

The server never stored a verifier for the auth key, so any caller who
knew only the note ID could fetch (destroying auto-delete notes) or
delete an unpassworded note - the #auth= fragment was never checked.

- Server: accept authKeyHash at create (bcrypt'd deterministic PBKDF2
  verifier), require it on fetch and delete for passwordless notes;
  legacy notes without a verifier keep ID-addressable behavior
- CLI: send authKeyHash on create/read/delete; delete now refuses
  links without #auth= (or a password) before hitting the network
- Web client: send authKeyHash on create/fetch/delete via
  NoteService/RemoteStorage (authKey threaded from the link fragment)
- Tests: new CRUD step covering 403 without verifier, 200 with it
  for both fetch and delete; full suite green (28 passed / 44 steps)
- Docs: llms-full.txt create/fetch/delete updated with authKeyHash
@emilkrebs
emilkrebs merged commit b442f1c into main Aug 8, 2026
3 of 4 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant