-
Notifications
You must be signed in to change notification settings - Fork 106
Security Model
Plain English: what hexo-blog-encrypt is and isn't designed to protect against.
| Designed for | Hiding the content of specific posts from passive readers of your public site. |
| Not designed for | Protecting against motivated attackers who can run code on the reader's device, MITM the connection, or coerce you. |
| Crypto strength | AES-256-GCM with a key derived via PBKDF2-SHA256 (≥ 100 000 iterations, 250 000 default, 600 000 recommended). The crypto itself is solid. The weakest link is your password choice and your hosting setup. |
After hexo generate, the post's HTML on disk and on your CDN contains only ciphertext + a salt + a nonce. There is no plaintext stored anywhere. Anyone who can read your public/ directory cannot read the locked post.
AES-GCM produces an authentication tag along with the ciphertext. The browser bundle verifies the tag during decryption. Any modification to the ciphertext — even a single bit — fails authentication and surfaces the same wrong_pass_message as a wrong-password attempt. An attacker who can edit your hosted HTML cannot substitute their own content under your password.
Per-post salt and nonce mean:
- Two posts encrypted with the same password produce different ciphertexts.
- Knowing the plaintext of post A doesn't help you decrypt post B even if both use the same password.
Your _config.yml tags[].password values never appear in the published HTML. They're only used during hexo generate to derive keys.
This plugin doesn't help you choose, store, or transmit passwords. Picking hello is not secure regardless of how strong AES-GCM is.
Title, date, tags, abstract, the password prompt, and any unencrypted text above the <!-- more --> cut are all plaintext in the published HTML. If the title is "My password to bank.example.com is hunter2" — sorry, AES-GCM can't help.
- Browser extensions, malware, screen scrapers — once the reader decrypts, the plaintext lives in the DOM. Anything running in the reader's browser can read it.
-
Shared devices — if you turn on
autoSave, the derived key (not the password) is stored inlocalStorage. Anyone with subsequent access to that profile can re-decrypt the same post on reload. WithautoSave: false(default) this risk is gone. - Memory dumps / DevTools — the password is briefly in JS memory during decryption. A motivated reader (or attacker who controls the reader's machine) can extract it.
-
HTTPS is required — Web Crypto (
crypto.subtle) is only available in a secure context (HTTPS or localhost). On plain HTTP from a non-localhost host, decryption silently fails and your reader sees nothing. - Without HTTPS, an MITM could replace your encrypted page with their own JavaScript that steals the password as the reader types it. The plugin can't defend against this — TLS is your only protection.
This plugin doesn't help if you're compelled (by law, by employer, by anyone with leverage over you) to reveal the password. There is no panic mode, no plausible deniability, no remote wipe. If you're in a threat model that includes coercion, you need a different tool.
- Encrypted post content is invisible to search engines. That's the point but worth saying.
- Anything above the
<!-- more -->cut (the abstract / preview text) is fully indexed. Don't put secrets there.
-
KDF: PBKDF2-HMAC-SHA256, 250 000 iterations by default, 100 000 floor, 600 000+ recommended (OWASP 2023 for PBKDF2-SHA256). Salt is 32 random bytes per post, generated at build time via Node's
crypto.randomBytes— unlessstableSaltis enabled (see below). - Cipher: AES-256-GCM. Key is the 32-byte PBKDF2 output. Nonce is 12 random bytes per post, generated at build time. Auth tag is 16 bytes, verified by the browser via Web Crypto's built-in GCM authentication.
-
Wire format: version
4. Browser refuses to decrypt other versions (so a v3 page won't be silently mis-handled by a v4 reader, and vice-versa). - No HMAC layer. GCM provides authenticated encryption (AEAD), so a separate HMAC step is unnecessary and was removed in v4.
By default every build mints a fresh random salt per post, so an attacker's
PBKDF2 precomputation against one build is worthless against the next. When
you enable stableSalt, the salt
is instead derived from the post permalink and stays the same across
rebuilds. The tradeoff:
- The salt was already public (it ships in
data-salteither way), so this discloses nothing new. What changes is that the salt — and therefore the derived key for a given password — is now stable over time rather than rotating each build. An attacker's offline dictionary work against a target post stays valid until you change the password or the permalink. - The nonce is still random per encryption, so there is no AES-GCM nonce reuse regardless of this setting.
- It is off by default. Only enable it when you specifically want
autoSavecaches to survive clean rebuilds (Cloudflare Pages, Vercel, Netlify, GitHub Actions). Strong passwords and highkdf.iterationsremain your real defense — the same as withoutstableSalt.
- Strong, unique passwords. A passphrase of 4+ unrelated words is a reasonable starting point.
- Serve over HTTPS. Always.
-
Raise
kdf.iterationsif your threat model includes offline brute-force attempts and your readers can tolerate ~120 ms of decrypt time:kdf: { iterations: 1000000 }. -
Don't enable
autoSaveon a shared device. Default-off is safer. Opt in only on private devices. -
Don't put secrets above
<!-- more -->. That's the public preview text. -
Audit your theme. Some themes leak post content into structured data (
<meta>, JSON-LD, RSS) before encryption runs. Check the rendered HTML. -
Rotate passwords if you suspect compromise. By default the salt and nonce change on every build, so old leaked HTML is irrelevant once you regenerate. Note: with
stableSaltenabled the salt is intentionally stable across rebuilds, so rotating the password (or changing the permalink) — not merely rebuilding — is what invalidates an old derived key.
If you find a real vulnerability, please open a GitHub issue OR contact the maintainers via the contact info in the repository. Don't post exploit details publicly until a fix is released.