Safe Diary Encryption Report
Safe Diary

How your diary stays unreadable to us

Safe Diary encrypts every entry in your browser before it ever reaches a server. This page documents exactly how that works, what our database can still see, and how to check the claims yourself.

Published 6 September 2026 Applies to web app, key schedule v2 Review type internal, by the developer

The short version

Your password never leaves your device, and neither does the key it makes.

When you unlock Safe Diary, your browser turns your password into an encryption key and holds it in memory for that session only. Entries are encrypted with that key before upload and decrypted after download. The server receives ciphertext and stores ciphertext.

There is no key escrow, no recovery key, no support tool that can open your journal. That is a deliberate trade: it also means that if you forget your password, your entries are gone permanently. We cannot reset it, and neither can anyone who compels us to try.

What the database holds

Stored, and not stored

Being honest about metadata matters more than a blanket claim. Here is every field attached to an entry.

Visible to the server

  • encrypted_dataAES-GCM ciphertext. Unreadable without your key.
  • ivThe random 96-bit nonce for that entry.
  • created_at / updated_atWhen an entry was written and last edited.
  • idA random identifier, bound into the ciphertext.
  • Approximate lengthCiphertext size roughly tracks entry length.

Never sent, never stored

  • Your passwordIt is never transmitted in any form.
  • Your encryption keyNon-extractable, memory only, discarded on lock.
  • Titles and body textEncrypted inside the blob, not separate columns.
  • Tags and moodsAlso inside the blob — tag names are not searchable server-side.
  • Analytics of any kindNo trackers, no third-party scripts, no telemetry.

So a full database breach reveals how many entries you keep and when you wrote them — not a single word of what you wrote.

Key schedule v2

From password to ciphertext, in four steps

The order matters here, so the steps are numbered. Every one of them runs inside your browser, using the platform’s own Web Crypto implementation — no hand-rolled cryptography, no third-party crypto library.

STEP 01

Stretch the password

Your password is run through PBKDF2-HMAC-SHA256 with a per-account random salt and 600,000 iterations — deliberately slow, so guessing is expensive.

PBKDF2(pw, salt, 600000) → 256 bits
STEP 02

Split into two keys

Those bits are expanded by HKDF into two separate keys with different labels: one that encrypts your entries, one that proves to the server you know the password.

HKDF → enc key ∥ auth key
STEP 03

Encrypt each entry

AES-GCM with a 256-bit key and a fresh random nonce per save. The entry’s own identifier is authenticated alongside it, so ciphertext cannot be swapped between records.

AES-GCM-256, new IV, AAD = entry id
STEP 04

Upload the ciphertext

Only the encrypted blob and its nonce are sent. The encryption key is marked non-extractable by the browser, so no script — ours included — can read it back out.

POST { id, encrypted_data, iv }
Key derivation
PBKDF2-HMAC-SHA256, 600,000 iterations — meets current OWASP guidance
Domain separation
HKDF-SHA256, distinct labels for encryption and authentication
Content cipher
AES-GCM, 256-bit key, 96-bit random nonce per save
Integrity
GCM authentication tag, with the entry identifier as additional authenticated data
Key handling
Non-extractable CryptoKey, in memory only — never written to storage of any kind
Unlock cost
≈0.45 s on a typical desktop — opening an entry afterwards takes under a millisecond
Minimum password
12 characters, with a passphrase recommended
Transport
HTTPS with HSTS; strict Content-Security-Policy; no third-party scripts

Threat model

What this protects you from, and what it doesn’t

It protects against the server. A stolen database, a compromised hosting account, a rogue administrator, or a legal order served on us yields ciphertext and timestamps. We cannot produce your entries because we cannot read them.

It does not protect a compromised device. Malware, a keylogger, a shoulder-surfer, or someone using your unlocked browser sees exactly what you see. Encryption at rest cannot help once the diary is open in front of an attacker.

It does not rescue a weak password. Six hundred thousand iterations make each guess expensive, but a short or reused password is still the cheapest way in. Use a long passphrase you have never used anywhere else.

The limitation every web app shares

Safe Diary delivers its own cryptography as JavaScript from our servers. Anyone who could compromise our hosting or domain could, in principle, serve a modified version of the app to future visitors and capture keys as they are typed. No browser-based encrypted service can eliminate this — the honest answer is that it narrows the attack from “read the database” to “successfully tamper with a signed deploy without anyone noticing”, which is a far harder and far more visible thing to do. We publish this rather than let “zero-knowledge” imply otherwise.

Don’t take our word for it

Three checks you can run in two minutes

Open Safe Diary in a desktop browser, press F12 for developer tools, and see for yourself.

1. Watch an entry leave your browser

In the Network tab, save an entry and inspect the request body. You should see an encrypted blob and a nonce — no title, no text, no tags.

{ "id": "…", "encrypted_data": "qJ8f2…", "iv": "SVaoD4lXJQ6Eayp9" }

2. Check what is kept on your device

In the Application tab, look at Local Storage. You will find a salt, a session token and a user record — and no password, no key material, nothing readable from your diary.

Application → Local Storage → https://safediary.app

3. Confirm nothing third-party can run

Our Content-Security-Policy allows scripts only from our own origin and network connections only to our own API. Paste this in the Console to read the policy the page is enforcing.

document.querySelector('meta[http-equiv="Content-Security-Policy"]').content

Security review — September 2026

What we found, and what we changed

A review of the client cryptography, session handling, storage and HTTP headers. Findings and their current status are listed below; existing accounts were migrated to the new key schedule with their entries re-encrypted in the browser.

Fixed

Key stretching raised to 600,000 iterations

PBKDF2-HMAC-SHA256 was running at 310,000 iterations. Raised to 600,000 to match current OWASP guidance, roughly doubling the cost of every password guess.

Fixed

Encryption and login keys separated

The login verifier previously shared a derivation with the encryption key. The two are now expanded from the master secret through HKDF under distinct labels, so the value held by our server has no useful relationship to the key that opens your entries.

Fixed

Entries bound to their own identity

Each entry’s identifier is now authenticated as part of its ciphertext, so a stored blob cannot be moved to another record without the app detecting it.

Fixed

Login attempts rate-limited

Failed unlock attempts are throttled per account and per address with escalating delays, and the verifier is compared in constant time. Failure messages never reveal whether an account exists.

Fixed

Minimum password length raised to 12

New accounts and password changes require at least 12 characters, with passphrases encouraged. Since there is no recovery path, password strength is the whole of your security.

Ongoing

Independent review

This report documents an internal review by the developer. It is not a third-party audit, and we do not claim one. If and when an external review is commissioned, its results will be published here whatever they say.

Reporting a problem

If you believe you have found a security flaw in Safe Diary, please write to security@safediary.app with enough detail to reproduce it. We will confirm receipt, keep you updated while we fix it, and credit you publicly if you would like us to. Please do not test against other people’s accounts.

Last updated 6 September 2026. This page describes the web application as deployed on that date and will be revised whenever the cryptography changes.

Scroll to Top