Yet, just another hexo plugin for security.
개요
Password-protect individual Hexo posts. Content is encrypted at build time with AES-256-GCM and decrypted in the reader's browser with the Web Crypto API. No server, no plaintext in the generated HTML. - Theme gallery - Decrypt callback - Script loading order and cached keys Requires Node.js 18 or later and Hexo 5 or later. The package is published to npm with provenance and mirrored to GitHub Packages as @d0n9x1n/hexo-blog-encrypt. Add a password to a post's front matter: The post body is replaced by a password form. Index pages show the abstract text instead of the content. A front-matter password takes precedence over a tag password. When autoSave reads a cached key, it compares the cached salt with the current page salt. A nonce change alone does not delete the cache up front; the browser tries the cached key with the current page nonce and ciphertext. If AES-GCM authentication fails, the cache entry is cleared and the visitor is asked for the password again.
README
hexo-blog-encrypt
Password-protect individual Hexo posts. Content is encrypted at build time with AES-256-GCM and decrypted in the reader’s browser with the Web Crypto API. No server, no plaintext in the generated HTML.
▶ Live demo: every demo password is hello.
English · 中文说明 · Wiki · Releases
Contents
- Highlights
- Live demo
- Install
- Quick start
- Configuration
- Theme integration
- Browser support
- Contributing
- License
Highlights
| Authenticated encryption | PBKDF2-SHA256 key derivation and AES-256-GCM. The GCM tag rejects wrong passwords and tampered ciphertext. |
| Native crypto only | Node.js crypto at build time, the browser Web Crypto API at read time. No runtime dependencies. |
| Per-post keys | Each post has its own 32-byte salt and 12-byte nonce, so one cracked post does not speed up attacks on the others. |
| Front-matter or tag passwords | Set password: on a post, or map tags to passwords in _config.yml. |
| Eight themes | default, blink, flip, shrink, surge, up, wave, xray. Add your own by dropping in one HTML file. |
| Scripts keep working | Scripts inside a decrypted post run in document order. A hexo-blog-decrypt event fires when the post is revealed. |
| Privacy by default | Passwords and keys are not stored unless you opt in with autoSave. |
| Claude Code skill | hexo-encrypt-toc fixes any theme’s table of contents for encrypted posts without leaking headings. |
| Tested | 100% server line, branch and function coverage, plus Playwright end-to-end tests across every theme. |
Live demo
Open the demo site. All passwords are hello.
Install
npm install --save hexo-blog-encrypt
Requires Node.js 18 or later and Hexo 5 or later. The package is published to
npm with provenance
and mirrored to GitHub Packages as @d0n9x1n/hexo-blog-encrypt.
Quick start
Add a password to a post’s front matter:
---
title: Hello World
date: 2016-03-30 21:18:02
password: hello
---
Then rebuild and preview:
hexo clean && hexo generate && hexo server
The post body is replaced by a password form. Index pages show the abstract
text instead of the content.
Configuration
Settings are resolved in this order:
post front matter > _config.yml encrypt: block > built-in defaults
A front-matter password takes precedence over a tag password.
Front matter
---
title: Hello World
tags:
- encryptAsDiary
date: 2016-03-30 21:12:21
password: mikemessi
abstract: Here's something encrypted, password is required to continue reading.
message: Hey, password is required here.
wrong_pass_message: Oh, this is an invalid password. Check and try again, please.
theme: xray
---
Site-wide _config.yml
encrypt: # hexo-blog-encrypt
abstract: Here's something encrypted, password is required to continue reading.
message: Hey, password is required here.
wrong_pass_message: Oh, this is an invalid password. Check and try again, please.
theme: default
silent: false
# Encrypt every post that carries one of these tags.
tags:
- {name: encryptAsDiary, password: passwordA}
- {name: encryptAsTips, password: passwordB}
# Show a visible "Decrypt" button next to the password field.
# Default: true.
decryptButton:
show: true
text: Decrypt
# Cache the derived AES key in localStorage so reloads skip the password
# prompt. Default: false. Enable globally here or per post in front matter.
# Entries are stored under `hbe.v4.`.
autoSave: false
# Keep the PBKDF2 salt stable across clean builds for the same post
# permalink. Default: false. Useful with autoSave on Cloudflare Pages,
# Vercel, Netlify, GitHub Actions, and other platforms that rebuild
# generated HTML from a clean environment on every deploy.
#
# Only the salt is stable. The AES-GCM nonce stays random for every
# encryption to avoid nonce reuse. If the post permalink changes, the
# stable salt changes too, and the page's existing localStorage cache is
# invalidated.
stableSalt: false
# PBKDF2 iteration count. Floor: 100_000. Recommended: 600_000 or more
# (OWASP 2023 for PBKDF2-SHA256). Values below the recommendation log a
# build warning. Default: 250_000.
kdf:
iterations: 250000
When autoSave reads a cached key, it compares the cached salt with the
current page salt. A nonce change alone does not delete the cache up front;
the browser tries the cached key with the current page nonce and ciphertext.
If AES-GCM authentication fails, the cache entry is cleared and the visitor is
asked for the password again.
Saved keys and their exposure. A saved key lives in localStorage under
hbe.v4. and unlocks that post on this device until it is removed.
Any script running on the same origin (your theme, analytics, comment
widgets, or an injected script) can read it, so enable autoSave only when
you trust every script on the site. After a post is revealed with a saved
key, a Forget saved key button above it deletes the entry. Turning
autoSave off for a post deletes that post’s saved key on the next visit,
and leftover v3 hexo-blog-encrypt:#… entries are removed automatically.
Option reference
| Option | Default | Description |
|---|---|---|
password |
none | Encrypts the post. "" disables encryption, including tag encryption. A matching tag entry without a password fails the build. |
password: { env: NAME } |
none | Reads the password from environment variable NAME at build time (front matter, encrypt.password or encrypt.tags[].password). A missing or empty variable fails the build; the value is never logged or rendered. This keeps passwords out of config files, not out of plaintext Markdown you commit publicly. |
abstract |
Here's something encrypted… |
Excerpt shown on index and archive pages. |
message |
Hey, password is required here. |
Label of the password field. |
wrong_pass_message |
Oh, this is an invalid password… |
Inline error after a failed attempt. |
theme |
default |
One of the eight bundled themes. Unknown names fall back to default with a warning. |
tags |
[] |
_config.yml only. {name, password} pairs for tag encryption. |
decryptButton.show / .text |
true / Decrypt |
Visible submit button and its label. |
autoSave |
false |
Cache the derived key in localStorage. |
stableSalt |
false |
Derive the salt from the permalink so cached keys survive clean rebuilds. |
kdf.iterations |
250000 |
PBKDF2 iterations. Must be an integer of at least 100,000. |
silent |
false |
Suppress build-time info logs. Warnings and errors still print. |
wrong_hash_message |
wrong_pass_message |
Deprecated alias. AES-GCM reports wrong passwords and tampering the same way. |
template |
none | Deprecated alias of theme. |
Boolean options (autoSave, stableSalt, silent, decryptButton.show) must be true or false. Quoted "true"/"false" are accepted with a warning; any other value fails the build.
The default abstract, message, wrong_pass_message and decryptButton.text follow the post’s lang/language, else the site language. zh, zh-CN and zh-Hans use Simplified Chinese; everything else uses English. Explicit values always win.
Disable tag encryption for one post
Set an empty password in that post’s front matter:
---
title: Public post with an encrypted tag
tags:
- A Tag should be encrypted
password: ""
---
Theme integration
After-decrypt event
The plugin dispatches hexo-blog-decrypt once on window after each post is
revealed. event.detail.mode is manual (password entered) or cached
(saved key), and event.detail.element is the revealed container (useful when
an index page shows several encrypted posts). Listen for it to re-run anything
that scans the post body:
window.addEventListener('hexo-blog-decrypt', function (event) {
// Reinitialize your theme's table of contents, syntax highlighting, etc.
console.log('Decrypted:', event.detail.mode, event.detail.element);
});
PJAX and SPA themes
The bundle initializes every encrypted post on the page once. It re-scans
automatically on pjax:complete, pjax:success, swup:contentReplaced and
turbo:load (dispatched on document). For other routers, call the idempotent
initializer after swapping content:
window.hexoBlogEncrypt.init(); // whole document
window.hexoBlogEncrypt.init(container); // only a subtree
Scripts inside encrypted posts
Scripts in a post run after it is decrypted. Since 4.0.3, ordinary external
scripts load in document order before later inline scripts run, so
library-dependent code such as DPlayer initializes correctly. The decrypt event
waits until restored external scripts load, fail, or reach a 15-second
per-script deadline. A timeout does not cancel a script; it can still execute
later. Existing onload and onerror handlers are preserved. Explicit async
scripts still load independently, and restored defer scripts do not recreate a
document parsing phase. Inline type="module" scripts and asynchronous work
inside scripts keep native scheduling and may finish after the decrypt event.
Table of contents
Never render an encrypted post’s post.origin into generated HTML. It
holds plaintext for legacy Hexo integrations. A TOC hidden with
display:none still publishes its headings. Search, feed and sitemap
integrations must also exclude plaintext fields from encrypted posts.
Render a server-side TOC only for unencrypted posts:
For encrypted posts, build the TOC in the browser inside the
hexo-blog-decrypt handler from the revealed #hexo-blog-encrypt content.
Let Claude Code fix your theme
The hexo-encrypt-toc skill does this for any Hexo theme: it finds
the theme’s TOC, removes plaintext sources, rebuilds the TOC after decryption,
and runs a leak check on the generated site. It is a single file. Save it as a
Claude Code skill:
mkdir -p ~/.claude/skills/hexo-encrypt-toc
curl -fsSL https://raw.githubusercontent.com/D0n9X1n/hexo-blog-encrypt/master/skills/hexo-encrypt-toc/SKILL.md \
-o ~/.claude/skills/hexo-encrypt-toc/SKILL.md
Then, in your blog’s directory, ask Claude Code to “fix the TOC for encrypted
posts in my theme” or run /hexo-encrypt-toc. Details:
wiki: Encrypted TOC.
Encryption themes
Pick a theme per post with theme: in front matter, or for the whole site in
_config.yml. Preview each one:
default · blink · flip · shrink · surge · up · wave · xray
To add a theme, see Theme Authoring.
Browser support
Decryption uses Web Crypto (crypto.subtle), which requires a secure
context: HTTPS or http://localhost.
| Browser | Minimum | Tested |
|---|---|---|
| Chromium / Edge | 92+ | Automated: Playwright Chromium in CI, Hexo 7 and 8 |
| Firefox | 90+ | Automated: Playwright Firefox in CI |
| Safari (macOS) | 14+ | Automated: Playwright WebKit in CI (WebKit engine, not Safari itself) |
| Safari (iOS) | 14+ | Manual only |
Minimum versions come from Web Crypto and language feature support; CI runs the current Playwright builds, not these minimums.
On plain HTTP from a non-localhost host, crypto.subtle is undefined, so
decryption cannot run; the post shows an “open this page over HTTPS” message
instead of the wrong-password error. Always serve production sites over HTTPS.
Contributing
Issues and pull requests are welcome. Please read CONTRIBUTING.md and include reproduction steps, logs and your configuration when reporting a bug.
License
Thanks
Collaborator: xiazeyu
추천 도구
다른 키워드를 입력하거나 필터를 제거해 보세요.
설치
npx skillfish add d0n9x1n/hexo-blog-encrypt