给纯静态网站(GitHub Pages 这类没有后端的托管)加“密码门禁”的小工具。核心就一句:整站可以被任何人下载,但没有密码就读不出正文;密码本身是单向的、不落盘、不上传;正文用的是可逆的对称加密,有授权就能看。
三套语言实现,同一套算法、互相能加解密:JavaScript(浏览器用 WebCrypto,Node 也能跑)· Python · C++(OpenSSL)。
同一份代码库、四个 payload 版本。版本号即算法标识,四个版本互不兼容:
| 变体 | payload | 基础 | 一句话 |
|---|---|---|---|
| SSMPL-EA | ssmpl:1 |
— | 基础方案:密本 = P ⊕ K。最简、开销最小、三语言互通最成熟 |
| SSMPL-BD | ssmpl:2 |
— | 后防改型:用户名参与派生,payload 里不存任何秘密 |
| SSMPL-TEA | ssmpl:3 |
EA | 时基检验:抓下来的副本过一阵子自己失效 |
| SSMPL-TBD | ssmpl:4 |
BD | 时基检验 + 用户名绑定 |
T 系列(
ssmpl:3/ssmpl:4)自带有效期与代码混淆,属于混淆强度而非密码学强度,单独成篇 → docs/SSMPL-T.md
| 文档 | 内容 |
|---|---|
| docs/SPEC.md | EA 协议规格:在线载荷格式、API、边界 |
| docs/SPEC-BD.md | BD 协议规格:派生链、决策记录、不变量、诚实边界 |
| docs/BD-测试报告.md | BD 构建 / 攻击 / 可用性测试报告(268 条断言) |
| docs/SSMPL-T.md | T 系列设计文档(时基检验 / 自校验算子 / 混淆管线,285 条断言) |
| docs/原理解析与优势.md | 原方案逐行走读 |
| cpp/BUILD-NOTES.md | C++ 构建说明(含"只有 libcrypto DLL、没有开发头文件"的 Windows 路线) |
- 纯静态就能跑。 不需要服务器、不需要数据库,也就没有服务器要运维、没有后端可被黑。
- 是真加密,不是糊弄。 正文用 AES-256-GCM(或 AES-256-CBC + HMAC),不是 base64,也不是写死一个密钥的异或。
- 多密码 / 换密码都不用重加密。 文章用随机密钥 K 加密,密码只通过「密本」去包 K。所以加一个新密码或换密码,只要重算一遍密本
P ⊕ K,文章密文一个字节都不用动。 - 密码不会离开用户的浏览器。 密码只在本地做单向派生(PBKDF2),不落盘、不上传,也就没有可被拖库的后端。
- 载荷很省。 每篇只存一份密文,是 O(篇数),不是 O(用户数 × 篇数)。换一次钥的成本就是一次异或。
- 关键参数都能调。 KDF 哈希(SHA-256/512)、迭代次数、加密方式(GCM / CBC+HMAC)都能改。
存: 正文 --AES-256-GCM(随机密钥 K)--> 密文 (双向,能还原)
密码:密码 --PBKDF2-HMAC(盐, 迭代n)--> P (32字节) (单向,拿不回密码)
密本:密本 = P ⊕ K (“加密后的密码”与“密钥”的差,公开存)
读: 输入密码 -> P -> K = P ⊕ 密本 -> AES 解密
如果这篇有多个密本(多密码),解密失败就试下一个
- 一篇正文只用一个随机 K 加密;密码只负责“包住”K。
- 加/换密码:给同一篇换个密码,再算一个新密本塞进
books数组即可,正文不动。 - 所以密本依赖的只有密码的强弱和迭代次数,跟正文密文长度无关。
两者共用同一套「随机 K + 可逆密本」骨架,区别只在凭证怎么来:
EA(ssmpl:1) |
BD(ssmpl:2) |
|
|---|---|---|
| 凭证来源 | 单级 KDF:P = PBKDF2(pw, salt, iters) |
多级派生链(见下) |
| 用户名 | 不参与 | 参与,且不进 payload |
| 同一密码的两个用户 | 撞车:凭证相同、密本字节相同 —— 分不开,也无法单独撤销 | 互不影响 |
| payload 泄露的用户信息 | 每篇有几条密本(= 每篇几个被授权人) | 连"有几个用户"都看不出来 |
| 校验值 / 用户清单 | 无 | 无(明确不引入) |
| 包装 | book = P ⊕ K |
book = C ⊕ K(同一个骨架) |
| AAD | 无 | 绑定篇名 |
| 载荷规模 | 密文 O(篇数)、密本 O(授权条数) | 相同 |
BD 的派生链(‖ 为拼接;全链只有第一环不可逆):
EP = PBKDF2-HMAC(grow(pw), salt, iterations, 32) ← 唯一不可逆环
LP = HMAC-SHA256(EP, "SSMPL-BD|LP|" ‖ salt)
Ku = HKDF-SHA256(EP, salt, "SSMPL-BD|uname", 32)
Eu = HMAC-SHA256(Ku, "SSMPL-BD|uname|" ‖ norm(u))
C = HMAC-SHA256(LP, "SSMPL-BD|C|" ‖ Eu) ← 32 字节凭证,永不落盘
三条要点:
- BD 修掉的是 EA 唯一的"功能性"局限 —— 旧版 README 把"每个用户必须用不同密码"列为限制,BD 之后不再需要。
- BD 的 payload 里不存任何秘密:没有用户清单、没有校验值、没有加密用户名。于是"入侵数据库"本身无利可图 —— 没有用户自己的密码,拿到的东西全是乱码。
- 两者都不存在通用凭证:
K每篇独立随机且从不出现,想全开就得拥有每一篇对应的凭证。
命名:本项目的基础方案(原 v1)现在叫 SSMPL-EA,它的代码一个字节都没改;SSMPL-BD 是后来新增的并列实现。两者必须按
ssmpl字段分流,不能靠空值/默认值兼容(会派生出不同的凭证,老密码直接打不开)。
| 你的情况 | 推荐 |
|---|---|
| 一个人或少数几个人用,只要"没密码读不出正文" | EA —— 最简、开销最小、三语言实现最成熟 |
| 多用户共享一个静态站,要"按篇、按人"给权限与撤销 | BD —— 同密码的不同用户不再撞车,能单独撤销 |
| 不想在公开载荷里暴露"有几个用户" | BD |
| 需要"抓下来的副本过一阵子失效" | TEA / TBD → docs/SSMPL-T.md |
| 需要真正的账号鉴权、服务器端吊销 | 以上都不是 —— 纯静态做不到,需要后端 |
四者的正文加密完全相同(AES-256-GCM,每篇一个随机 K),差别只在派生链长度:BD 比 EA 多一次 HKDF + 三次 HMAC(微秒级),可以忽略。
每篇文章的 books(密本列表)是彼此独立的,同一个人在不同篇里可以有不同的密本或没有密本:
- 授权某人看某篇:
add_password(那篇item, 那人的密码, 管理员密码, cfg)—— 往这一篇塞一个该用户的密本。 - 不让某人看某篇:那一篇的
books里不放他的密本 —— 他算不出 K,读不了这篇。 - 收回某人的某篇:
remove_password(那篇item, 那人的密码, cfg)—— 只删这一篇里属于他的那条密本,正文不动。
所以一个公开的 JSON 就能做到「张三能看 A、B,李四只能看 B,管理员全看」:
blob.items.A = await removePassword(await addPassword(itemA, 'zhang', 'admin', cfg), 'li', cfg);
blob.items.B = await removePassword(await addPassword(itemB, 'li', 'admin', cfg), 'zhang', cfg);硬要挑刺的边界(照样写清楚):
- 每个“用户”得用不同的密码(不然两个用户共用同一个密本,分不开)。
- 收回来的是密本,不是把密文改掉——所以重新托管 JSON 后,之前已经拿到并保存的密本的人仍能看(静态站没法“远程踢人”)。
- 用户之间可以转借密码/密本,静态站拦不住。
- 依然是离线可穷举弱密码,强口令 + 高迭代才是安全感来源。
| 字段 | 值 | 说明 |
|---|---|---|
hash |
SHA-256 / SHA-512 |
密码 KDF 的哈希 |
iterations |
整数 | KDF 代价,越大越抗离线爆破 |
cipher |
AES-256-GCM / AES-256-CBC-HMAC-SHA256 |
正文加密方式(推荐 GCM,带认证) |
salt |
bytes | 公开盐,一个 lock 固定一份 |
JavaScript(js/src/ssmpl.js,浏览器 / Node ≥18 通用)
import { Lock, encryptItem, addPassword, b64e } from './src/ssmpl.js';
// 后端:加密 + 再加一个密码
const salt = crypto.getRandomValues(new Uint8Array(16));
const cfg = { hash:'SHA-256', iterations:250000, cipher:'AES-256-GCM', salt };
let item = await encryptItem('正文…', '密码A', cfg);
item = await addPassword(item, '密码B', '密码A', cfg); // 不重加密
const blob = { ssmpl:1, cipher:cfg.cipher, kdf:'PBKDF2-HMAC', hash:cfg.hash,
iterations:cfg.iterations, salt:b64e(salt), items:{ story:item } };
// 前端:使用者解锁并解密
const lock = new Lock(JSON.parse(JSON.stringify(blob)));
await lock.unlock('密码B'); // 密码错则返回 false
const text = await lock.decrypt('story');Python(python/ssmpl/)
from ssmpl import Config, encrypt_item, add_password, Lock
cfg = Config(hash="SHA-256", iterations=250000, cipher="AES-256-GCM", salt=bytes(range(16)))
item = encrypt_item("正文…", "密码A", cfg)
item = add_password(item, "密码B", "密码A", cfg)
lock = Lock({"ssmpl":1,"cipher":cfg.cipher,"kdf":"PBKDF2-HMAC","hash":cfg.hash,
"iterations":cfg.iterations,"salt":b64e(cfg.salt),"items":{"story":item}})
lock.unlock("密码B"); print(lock.decrypt("story"))C++(cpp/,用 CMake 构建,需 OpenSSL)
cd cpp && cmake -B build && cmake --build build
./build/test_ssmpl # 自测:往返 + 多密码 + GCM/CBC
./build/ssmpl_cli encrypt 密码A article.txt
./build/ssmpl_cli addpass ssmpl-lock.json article.txt 密码B 密码A
./build/ssmpl_cli decrypt ssmpl-lock.json article.txt 密码Bnode js/test/test.js # JS 16 用例
py -3.9 python/tests/test_ssmpl.py # Python 16 用例
node js/test/interop.mjs # JS <-> Python 互通(GCM/CBC 双向)- SSMPL-EA —— 本项目的基础方案(payload ssmpl: 1),就是上面这套「密本 = P ⊕ K」。
- SSMPL-BD —— 后防改型(payload ssmpl: 2),见下。
在原方案背后加一条"后防"链。要点是:payload 里不存放任何秘密——没有用户清单、没有校验值、 没有加密用户名,只有密文和密本。于是"入侵数据库"本身无利可图:拿到的东西没有用户自己的密码就全是乱码。
norm(s) = UTF-8(NFC(s)) grow(b) = uint32be(len(b)) || b
EP = PBKDF2(grow(pw), salt, iterations, 32) ← 全链唯一不可逆环
LP = HMAC(EP, "SSMPL-BD|LP|" || salt)
Ku = HKDF(EP, salt, "SSMPL-BD|uname")
Eu = HMAC(Ku, "SSMPL-BD|uname|"|| norm(u)) ← 加密用户名:秘密,不落盘
C = HMAC(LP, "SSMPL-BD|C|" || Eu) ← 凭证 32B:秘密,不落盘
K_i = 32B 随机(每篇一个) book = C ⊕ K_i ← 公开、可逆
读:算出 C → K_i = C ⊕ book → AES-256-GCM(用 ABDD tag 判定对错)
-
修掉了 v1 的一条旧局限:同一密码的不同用户不再撞车(用户名参与派生);
-
payload 里连"有哪些用户"都看不出来(比 v1 更严:v1 至少泄露每篇有几个被授权人);
-
K_i每篇独立随机且从不出现 ⇒ 不存在"一把钥匙开全部"的通用凭证。 -
测试报告:docs/BD-测试报告.md(三语言 268 条断言全部通过)
node js/test/test-bd.js # JS 功能 46
node js/test/attack-bd.js # JS 攻击 42
node js/test/gen-vectors-bd.js # 生成跨语言测试向量(可复现)
node js/test/interop-bd.mjs # Python -> JS 反向验证
python python/tests/test_bd.py # Python 功能 45
python python/tests/attack_bd.py # Python 攻击 41
python python/tests/interop_bd.py # JS -> Python 验证(并产出反向验证用的 payload)
cpp/是按标准 OpenSSL 写的。本仓库已在 MSVC + OpenSSL 3.5.7 运行时 DLL 上编译并跑通test_ssmpl_bd(35 条断言,与 JS/Python 的测试向量逐字节一致)。 只有运行时 DLL、没有开发头文件时的构建办法见 cpp/BUILD-NOTES.md。
给纯静态站点加有效期:抓下来的副本会在一个时间窗之后自己失效。
- 时间窗并入慢 KDF 的盐 —— 每试一个窗口都要付一次完整 PBKDF2,不是一次异或;
- payload 里没有任何时间字段 —— 与普通 EA/BD payload 逐字节同构,从数据本身看不出用了时基;
- 自校验算子 —— 附在解密算子最后,无分支、无报错,把自身代码的 SHA-256 直接喂进派生链: 代码被改 → 密钥不对 → 静默解不开;
- JS 代码混淆作为构建步骤(控制流平坦化 + 字符串数组 + 标识符改名,
seed=0可复现构建)。
- 设计文档:docs/SSMPL-T.md
- 测试:285 条断言(功能 55+45 / 攻击 35+32 / 互通 32+17 / C++ 47 / 混淆产物 22)
node js/test/test-t.js
node js/test/attack-t.js
python python/tests/test_t.py
python python/tests/attack_t.py
python python/tests/interop_t.py && node js/test/interop-t.mjs
cd js && npm install && npm run build && node test/smoke-dist-t.js # 混淆产物 + 自校验这是静态站的访问门禁 + 口令级权限:能用「这篇的 books 里有没有某人的密本」做到按文章、按用户给权限(见上一节)。但它不是服务器式账号系统——没有登录态,不能“远程踢掉”已经拿到密本的人,也拦不住用户把密码/密本转借他人。载荷公开意味着弱密码可被离线穷举,安全感 = 密码强度 × 迭代次数。要真正的账户鉴权 + 服务器端吊销,那得用后端。
详见 docs/SPEC.md。
A small toolkit that adds password gates to a fully static site (GitHub Pages and similar — no backend). The gist: anyone can download the whole site, but without a valid password the content is unreadable. The password is one-way, never stored, never uploaded; the content uses reversible symmetric encryption, so an authorized reader gets it back.
Three implementations of the same, interoperable spec: JavaScript (WebCrypto in browsers, also Node) · Python · C++ (OpenSSL).
One codebase, four payload versions. The version number is the algorithm identifier — they are not interchangeable:
| Variant | payload | Built on | One-liner |
|---|---|---|---|
| SSMPL-EA | ssmpl:1 |
— | Base scheme: cipher-book = P ⊕ K. Simplest, cheapest, most mature across languages |
| SSMPL-BD | ssmpl:2 |
— | Backdefense variant: the username joins the derivation, the payload stores no secret at all |
| SSMPL-TEA | ssmpl:3 |
EA | Time gate: a scraped copy expires by itself |
| SSMPL-TBD | ssmpl:4 |
BD | Time gate + username binding |
The T series ships an expiry window and code obfuscation — obfuscation grade, not cryptographic grade. Documented separately → docs/SSMPL-T.md
| Doc | What it covers |
|---|---|
| docs/SPEC.md | EA protocol spec: hosted payload, API, limits |
| docs/SPEC-BD.md | BD protocol spec: derivation chain, decisions, invariants, honest limits |
| docs/BD-测试报告.md | BD build / attack / usability report (268 assertions) |
| docs/SSMPL-T.md | T-series design doc (time gate / self-check operator / obfuscation pipeline, 285 assertions) |
| cpp/BUILD-NOTES.md | C++ build notes, incl. the "libcrypto DLL but no dev headers" route |
- Runs on static hosting. No server, no database — nothing to operate, nothing on the server side to breach.
- Real crypto, not obfuscation. AES-256-GCM (or AES-256-CBC + HMAC) for the content — not base64, not a hard-coded XOR key.
- Add or change a password without re-encrypting. Content is encrypted with a random key K; passwords only wrap K via the cipher-book (密本). Add a password or change one by recomputing
cipher-book = P ⊕ K— the ciphertext doesn't change at all. - The password never leaves the browser. It's only hashed locally (PBKDF2), nothing is stored or transmitted, so there's no backend to leak from.
- Cheap payload. One ciphertext per item → O(items), not O(users × items). A re-key costs one XOR.
- Configurable. KDF hash (SHA-256/512), iteration count, and cipher (GCM / CBC+HMAC) are all settable.
store: plaintext --AES-256-GCM(random key K)--> ciphertext (two-way, reversible)
password: password --PBKDF2-HMAC(salt, iters)--> P (32 bytes) (one-way, irreversible)
book: cipher-book = P ⊕ K (difference of the hashed password and the content key)
read: type password -> P -> K = P ⊕ cipher-book -> AES decrypt
if the item has several books (multi-password), a failed key tries the next
- One random K encrypts each piece of content; a password only "wraps" K.
- Add/change a password: recompute a new
cipher-bookfor the same K and push it intobooks— the content stays untouched. - Security therefore depends on password strength and KDF cost, not on the ciphertext length.
Both share the same skeleton — a random per-item K wrapped by a reversible cipher-book. Only the credential changes:
EA (ssmpl:1) |
BD (ssmpl:2) |
|
|---|---|---|
| Credential | single KDF: P = PBKDF2(pw, salt, iters) |
a derivation chain (below) |
| Username | not used | used, and never stored in the payload |
| Two users, same password | collide: identical credential, byte-identical books — indistinguishable, cannot be revoked separately | independent |
| What the payload leaks | how many books each article has (= how many readers) | not even how many users exist |
| Verifier / user list | none | none (deliberately) |
| Wrapping | book = P ⊕ K |
book = C ⊕ K (same skeleton) |
| AAD | none | binds the item id |
| Payload size | ciphertext O(items), books O(grants) | identical |
EP = PBKDF2-HMAC(grow(pw), salt, iterations, 32) <- the only one-way step
LP = HMAC-SHA256(EP, "SSMPL-BD|LP|" ‖ salt)
Ku = HKDF-SHA256(EP, salt, "SSMPL-BD|uname", 32)
Eu = HMAC-SHA256(Ku, "SSMPL-BD|uname|" ‖ norm(u))
C = HMAC-SHA256(LP, "SSMPL-BD|C|" ‖ Eu) <- 32-byte credential, never stored
Naming: the base scheme (formerly v1) is now called SSMPL-EA and its code is unchanged; SSMPL-BD is a parallel implementation added later. Route by the
ssmplfield — never by defaults, or you will derive a different credential and old passwords will simply stop working.
| Your situation | Pick |
|---|---|
| One or a few people; you only need "no password, no content" | EA — simplest, cheapest, most mature |
| Many users on one static site, with per-article grants and revocation | BD — same password no longer collides, revoke individually |
| You do not want the public payload to reveal how many users exist | BD |
| You need scraped copies to expire | TEA / TBD → docs/SSMPL-T.md |
| You need real account auth or server-side revocation | None of these — that needs a backend |
Each item's books (cipher-books) is independent — the same person can have a 密本 in one article and no 密本 in another.
- Grant a user an article:
add_password(thatItem, thatUserPassword, adminPassword, cfg)— put one of that user's cipher-books into that article. - Deny a user an article: just don't put their cipher-book in that article's
books— they can't recover K, so they can't read it. - Revoke a user's article:
remove_password(thatItem, thatUserPassword, cfg)— drop only their cipher-book from that item; the content stays untouched.
So one public JSON can express "张三 reads A/B, 李四 reads only B, admin reads all":
blob.items.A = await removePassword(await addPassword(itemA, 'zhang', 'admin', cfg), 'li', cfg);
blob.items.B = await removePassword(await addPassword(itemB, 'li', 'admin', cfg), 'zhang', cfg);Honest limits, stated plainly:
- Each "user" needs a distinct password (otherwise two users share the same cipher-book and can't be told apart).
- You revoke the cipher-book, not the ciphertext — so anyone who already downloaded a cipher-book keeps reading; a static site can't "kick" someone remotely.
- Users can hand their password / cipher-book to someone else; a static site can't stop it.
- Weak passwords are still offline-attackable; a strong password + high iteration count is what buys real safety.
{
"ssmpl": 1,
"cipher": "AES-256-GCM", // or "AES-256-CBC-HMAC-SHA256"
"kdf": "PBKDF2-HMAC",
"hash": "SHA-256", // or "SHA-512"
"iterations": 250000,
"salt": "<base64>",
"items": {
"<name>": { "iv": "…", "ct": "…", "tag": "…" or "mac": "…",
"books": ["<base64 cipher-book>", …] } // one per authorized password
}
}| field | values | notes |
|---|---|---|
hash |
SHA-256 / SHA-512 |
one-way password KDF hash |
iterations |
integer | KDF cost; higher = slower offline brute-force |
cipher |
AES-256-GCM / AES-256-CBC-HMAC-SHA256 |
content cipher (GCM is authenticated, recommended) |
salt |
bytes | public salt, fixed per lock |
JavaScript (js/src/ssmpl.js, browser / Node ≥18)
import { Lock, encryptItem, addPassword, b64e } from './src/ssmpl.js';
const salt = crypto.getRandomValues(new Uint8Array(16));
const cfg = { hash:'SHA-256', iterations:250000, cipher:'AES-256-GCM', salt };
let item = await encryptItem('body…', 'passwordA', cfg);
item = await addPassword(item, 'passwordB', 'passwordA', cfg); // no re-encryption
const blob = { ssmpl:1, cipher:cfg.cipher, kdf:'PBKDF2-HMAC', hash:cfg.hash,
iterations:cfg.iterations, salt:b64e(salt), items:{ story:item } };
const lock = new Lock(JSON.parse(JSON.stringify(blob)));
await lock.unlock('passwordB'); // false if wrong
const text = await lock.decrypt('story');Python (python/ssmpl/)
from ssmpl import Config, encrypt_item, add_password, Lock
cfg = Config(hash="SHA-256", iterations=250000, cipher="AES-256-GCM", salt=bytes(range(16)))
item = encrypt_item("body…", "passwordA", cfg)
item = add_password(item, "passwordB", "passwordA", cfg)
lock = Lock({"ssmpl":1,"cipher":cfg.cipher,"kdf":"PBKDF2-HMAC","hash":cfg.hash,
"iterations":cfg.iterations,"salt":b64e(cfg.salt),"items":{"story":item}})
lock.unlock("passwordB"); print(lock.decrypt("story"))C++ (cpp/, CMake + OpenSSL)
cd cpp && cmake -B build && cmake --build build
./build/test_ssmpl # round-trip + multi-password + GCM/CBC
./build/ssmpl_cli encrypt passwordA article.txt
./build/ssmpl_cli addpass ssmpl-lock.json article.txt passwordB passwordA
./build/ssmpl_cli decrypt ssmpl-lock.json article.txt passwordBnode js/test/test.js # JS 16 cases
py -3.9 python/tests/test_ssmpl.py # Python 16 cases
node js/test/interop.mjs # JS <-> Python, GCM + CBC, both directions- SSMPL-EA — the base scheme (payload
ssmpl: 1), i.e. the 密本 = P XOR K construction above. - SSMPL-BD — the backdefense variant (payload
ssmpl: 2); see below.
A second line of defence behind the original scheme. The point: the payload stores no secret at all — no user list, no verifier, no encrypted username, only ciphertexts and cipher-books. Breaking into "the database" is therefore pointless: without the user's own password, everything you steal is noise.
norm(s) = UTF-8(NFC(s)) grow(b) = uint32be(len(b)) || b
EP = PBKDF2(grow(pw), salt, iterations, 32) <- the only one-way step in the whole chain
LP = HMAC(EP, "SSMPL-BD|LP|" || salt)
Ku = HKDF(EP, salt, "SSMPL-BD|uname")
Eu = HMAC(Ku, "SSMPL-BD|uname|"|| norm(u)) <- encrypted username: secret, never stored
C = HMAC(LP, "SSMPL-BD|C|" || Eu) <- credential, 32B: secret, never stored
K_i = random 32B per item book = C XOR K_i <- public, reversible
read: derive C -> K_i = C XOR book -> AES-256-GCM (the ABDD tag decides right/wrong)
-
Fixes a documented v1 limitation: two users sharing a password no longer collide (the username takes part in the derivation);
-
the payload does not even reveal how many users exist (stricter than v1, which leaks the number of authorised readers per article);
-
a per-item random
K_ithat never appears => there is no universal credential. -
Spec: docs/SPEC-BD.md
-
Test report: docs/BD-测试报告.md — 268 assertions across three languages, all passing
node js/test/test-bd.js # JS functional 46
node js/test/attack-bd.js # JS attack 42
node js/test/gen-vectors-bd.js # regenerate cross-language vectors (byte-identical)
node js/test/interop-bd.mjs # Python -> JS direction
python python/tests/test_bd.py # Python functional 45
python python/tests/attack_bd.py # Python attack 41
python python/tests/interop_bd.py # JS -> Python direction
cpp/targets standard OpenSSL. It has been compiled and run on MSVC + the OpenSSL 3.5.7 runtime DLL:test_ssmpl_bdpasses 35 assertions, byte-identical to the JS/Python vectors. For building with the DLL but no development headers, see cpp/BUILD-NOTES.md.
Adds an expiry window to a fully static site: a scraped copy stops working after one period.
- the time window goes into the salt of the slow KDF (each candidate window costs a full PBKDF2);
- the payload carries no time field at all — byte-for-byte isomorphic to a plain EA/BD payload;
- a self-check operator appended after the decrypt routine: no branches, no errors — it folds the SHA-256 of its own code into the derivation, so tampering yields a wrong key and a silent failure;
- JS code obfuscation as a build step (
seed=0, reproducible).
- Design doc: docs/SSMPL-T.md · 285 assertions, all passing.
This is access gating + password-level permissions on a static site: you can grant/deny
per-article, per-user by whether that article's books contains a user's cipher-book (see above).
It is not a server-style account system — no session, no way to remotely revoke someone who
already holds a cipher-book, and no way to stop people sharing passwords / cipher-books. The
payload is public, so weak passwords are offline-attackable; safety = password strength × KDF
cost. For real account auth + server-side revocation you'd need a backend.
See docs/SPEC.md.
MIT © 2026 LLYlab
{ "ssmpl": 1, "cipher": "AES-256-GCM", // 或 "AES-256-CBC-HMAC-SHA256" "kdf": "PBKDF2-HMAC", "hash": "SHA-256", // 或 "SHA-512" "iterations": 250000, "salt": "<base64>", "items": { "<名称>": { "iv": "...", "ct": "...", "tag": "..." 或 "mac": "...", "books": ["<base64 密本>", ...] } // 一个密码对应一个密本 } }