DH

d0n9x1n/hexo-blog-encrypt

Code review
1,1 тыс. stars Качество 50 Тренд 59

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

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

MIT

Thanks

Collaborator: xiazeyu

View this README on GitHub

Рекомендуемые инструменты

Попробуйте другой запрос или уберите фильтр.

Установка

npx skillfish add d0n9x1n/hexo-blog-encrypt