@tacet-ink/journal-core 0.1.4 → 0.1.6
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +87 -12
- package/package.json +5 -3
- package/scripts/verify-core-crypto.ts +628 -10
- package/src/client/argon2.ts +32 -37
- package/src/client/note-crypto.ts +137 -42
- package/src/client/pinlock.ts +13 -8
- package/src/index.ts +3 -0
- package/src/server/auth.ts +47 -19
- package/src/server/cors.ts +4 -0
- package/src/server/ratelimit.ts +50 -34
package/README.md
CHANGED
|
@@ -8,24 +8,26 @@
|
|
|
8
8
|
零知識日記核心:端對端加密、密語即身份的 auth、傳輸與限流原語。
|
|
9
9
|
伺服器看不到任何一個字:passphrase 永不過線,密鑰包裹與密文都在客戶端完成。
|
|
10
10
|
|
|
11
|
+
> **English**: Zero-knowledge journaling core — E2E-encrypted notes, passphrase-as-identity auth, transport & rate-limiting primitives. The server never sees a single word: the passphrase never crosses the wire, and all key wrapping and encryption happen client-side. See the [English summary](#english-summary) below.
|
|
12
|
+
|
|
11
13
|
這是 [默·Tacet(tacet.ink)](https://tacet.ink) 的客戶端/伺服端核心原語層,
|
|
12
14
|
從多個姊妹產品共用的密碼學本體抽取而成,以品牌前綴參數化(前綴契約見下表)。
|
|
13
15
|
|
|
14
|
-
##
|
|
16
|
+
## 設計(Design)
|
|
15
17
|
|
|
16
18
|
- **兩時代金鑰模型**:未綁定(guest)時代以 `K_u = SHA-256(guestKdfPrefix ‖ identity)`
|
|
17
19
|
純客戶端派生(混淆級);綁定後改用隨機 256-bit noteKey 加密,
|
|
18
20
|
noteKey 再以 passphrase(KEK)與復原套件(KEK_rec)雙重包裹上傳。
|
|
19
21
|
此後 passphrase 不再過線:線上只送 PH1(雜湊形),伺服器存 PH2 = SHA-256(PH1)。
|
|
20
22
|
- **帶內版本化**:升級 KDF 參數 = 換新前綴(舊前綴照解、原地改語意是禁手)。
|
|
21
|
-
前綴全部可配置,AAD
|
|
23
|
+
前綴全部可配置,AAD(防搬移綁結)由呼叫端傳入。
|
|
22
24
|
- **extractable 鐵律**:要被 exportKey/wrap 的 key(noteKey 全部產生路徑),
|
|
23
25
|
import 當下就必須 `extractable=true`;KEK/guest key 恆 nonextractable。
|
|
24
26
|
- **opt-in 原語**:選配契約(cipherGuest/wrapDual/wrapShare/pinLock/cipherAttach/cipherLocal)
|
|
25
|
-
|
|
26
|
-
解密面 guest
|
|
27
|
+
未配置即拒絕,各 fork 未選用的原語行為不受影響(cipherGuest 未配置時 bound 路徑不受牽連,
|
|
28
|
+
解密面 guest 家族整面拒絕、不明字串與畸形空字串配置不當明文顯示)。
|
|
27
29
|
|
|
28
|
-
##
|
|
30
|
+
## 模組(Modules)
|
|
29
31
|
|
|
30
32
|
| 模組 | 內容 |
|
|
31
33
|
| --- | --- |
|
|
@@ -34,8 +36,8 @@
|
|
|
34
36
|
| `src/client/pinlock.ts` | 本機 PIN 鎖定包裹(jr1p.,開啟時鎖定) |
|
|
35
37
|
| `src/client/bip39.ts` | 復原套件 24 詞 ⇄ hex64 轉寫層(BIP39,零依賴自製) |
|
|
36
38
|
| `src/client/keys.ts` | 品牌前綴 localStorage 命名空間 |
|
|
37
|
-
| `src/server/auth.ts` | 零知識 auth 核心:PH1
|
|
38
|
-
| `src/server/ratelimit.ts` | per-IP
|
|
39
|
+
| `src/server/auth.ts` | 零知識 auth 核心:PH1→PH2 login 動線、入庫格式驗證、包裹欄組成對(pair 檢查 pickKeyPackage);PH2 UNIQUE 衝突與 session 撤銷=呼叫端職責 |
|
|
40
|
+
| `src/server/ratelimit.ts` | per-IP fixed-window 限流(單句 UPSERT…RETURNING;D1 計數,跨 isolate 有效) |
|
|
39
41
|
| `src/server/cors.ts`/`hash.ts` | 共用 CORS/雜湊工具(`src/server/env.ts` 為內部 Env 介面,不入 exports) |
|
|
40
42
|
|
|
41
43
|
## 前綴契約(家族表:10 個資料前綴+閘對照組 jr1g.)
|
|
@@ -59,19 +61,20 @@ Tacet 部署實例(config 傳入):
|
|
|
59
61
|
guest 密文前綴在閘對照組另驗 `jr1g.`(閘自造前綴,驗證品牌參數化本身;tacet 部署實例 guest 用 `jr1u.`)。
|
|
60
62
|
跨前綴呼叫一律回 null(前綴守衛+AAD+長度把關),不拋、不降級。
|
|
61
63
|
|
|
62
|
-
##
|
|
64
|
+
## 驗證(Verification)
|
|
63
65
|
|
|
64
66
|
```sh
|
|
65
|
-
npm run verify #
|
|
66
|
-
#
|
|
67
|
+
npm run verify # 249 斷言對真模組(禁鏡像;限流單句 UPSERT…RETURNING 直載真 SQLite):roundtrip/AAD 防搬移/
|
|
68
|
+
# extractable/時代隔離/跨前綴家族隔離/payload 竄改/RFC 9106 KAT/BIP39 @scure 對照 200 組/
|
|
69
|
+
# server-side(inboundCipher/validWrappedKey/pickKeyPackage/checkRate/timingSafeEq)+ pinlock 全函式
|
|
67
70
|
```
|
|
68
71
|
|
|
69
72
|
- 產品碼**零執行時依賴**(WebCrypto 原語);devDependencies 僅閘用(typescript、@scure/bip39 對照、workers types)。
|
|
70
|
-
- Argon2id 雙載體:node 走 `node:crypto`(Node
|
|
73
|
+
- Argon2id 雙載體:node 走 `node:crypto`(Node 24.7+ 原生)、瀏覽器走 hash-wasm(wasm 內嵌),
|
|
71
74
|
RFC 9106 標準向量逐位元一致;無載體即 throw(禁 fallback 鐵律)。
|
|
72
75
|
- BIP39 原語自製零依賴,與 @scure/bip39 參照 200 組雙向對照(僅 entropy↔words 轉寫層,禁用其 seed 派生)。
|
|
73
76
|
|
|
74
|
-
##
|
|
77
|
+
## 使用(Usage)
|
|
75
78
|
|
|
76
79
|
TypeScript 原始碼發行(exports 直指 .ts)。**注意**:Node 的 strip-types 不適用於
|
|
77
80
|
node_modules 內的檔案,npm 安裝的消費者請走打包器;源碼 clone 可直接 node --experimental-strip-types。
|
|
@@ -85,6 +88,7 @@ import { makeKeyStore, makeHeldKey, generateNoteKey, encryptNote, decryptNote }
|
|
|
85
88
|
import { generateNoteKey, encryptNote, decryptNote, makeHeldKey,
|
|
86
89
|
wrapNoteKey, unwrapNoteKey } from '@tacet-ink/journal-core/client/note-crypto';
|
|
87
90
|
import { makeKeyStore } from '@tacet-ink/journal-core/client/keys';
|
|
91
|
+
import { derivePh1Argon } from '@tacet-ink/journal-core/client/argon2';
|
|
88
92
|
|
|
89
93
|
const cfg = {
|
|
90
94
|
guestKdfPrefix: 'myapp-note-u1',
|
|
@@ -101,6 +105,23 @@ const identity = { current: () => 'acct-xxxxxxxxxxxxxxxx' };
|
|
|
101
105
|
const cipher = await encryptNote(cfg, held, '今天寫了一點東西。', 'noteId:n1', identity);
|
|
102
106
|
```
|
|
103
107
|
|
|
108
|
+
PH1 派生(登入憑證):`derivePh1Argon(pass, saltArg?)` 預設鹽 `PH1_V2_SALT`
|
|
109
|
+
(`'tacet-ph1-v1'`,Tacet 實例)。其他產品可自選**固定域鹽**傳入 `saltArg`——
|
|
110
|
+
鹽一經選定即 per-product 恆固定(PH2 UNIQUE 約束:同一密語必須恆生同一 PH2;
|
|
111
|
+
per-user 鹽會摧毀幽靈帳號機制)。改鹽=帶內版本化換鹽尾碼重遷移,禁原地改語意。
|
|
112
|
+
|
|
113
|
+
### 例外契約(錯誤碼語意)
|
|
114
|
+
|
|
115
|
+
- 空 identity(guest 派生):加密面拋 `ERR_NO_IDENTITY`、解密面回 `null`
|
|
116
|
+
(K_u = SHA-256(prefix ‖ '') 會靜默產出「空帳號金鑰」=誤配炸彈,兩面皆 fail-closed)。
|
|
117
|
+
- 空 PIN(`wrapNoteKeyDual`/`wrapNoteKeyDual3`):拋 `ERR_PIN_EMPTY`——與
|
|
118
|
+
「功能未配置」(`ERR_DUAL_NOT_CONFIGURED`/`ERR_JR3W_NOT_CONFIGURED`)語意分離。
|
|
119
|
+
- `hexToBytes` fail-closed:非法 hex(奇數長度/非 hex 字元/空字串)拋 `ERR_BAD_HEX`,
|
|
120
|
+
不再 parseInt 靜默歸零(假金鑰生產器)。unwrap 家族的 try/catch 承接=跨前綴鐵律
|
|
121
|
+
(回 null 不拋)不變。
|
|
122
|
+
- `PinLockConfig`:解包輸出的 noteKey 恆 `extractable=true`(鐵律,非可調選項)——
|
|
123
|
+
config 只帶三個前綴欄(pinLock/pinLockSaltPrefix/pinLockAad),缺任一即拒。
|
|
124
|
+
|
|
104
125
|
打包器(vite/esbuild)可直接 alias 到源碼目錄使用;Argon2id 瀏覽器載體需另裝
|
|
105
126
|
hash-wasm 並以 `setArgonLoader()` 注入(詳 `src/client/argon2.ts` 檔頭)。
|
|
106
127
|
|
|
@@ -114,3 +135,57 @@ hash-wasm 並以 `setArgonLoader()` 注入(詳 `src/client/argon2.ts` 檔頭
|
|
|
114
135
|
## 授權
|
|
115
136
|
|
|
116
137
|
MIT。安全問題聯絡 tacetink.csd@gmail.com。
|
|
138
|
+
|
|
139
|
+
## English Summary
|
|
140
|
+
|
|
141
|
+
**What this is.** `@tacet-ink/journal-core` is the cryptographic and server-primitive layer powering
|
|
142
|
+
[默·Tacet (tacet.ink)](https://tacet.ink), a zero-knowledge journal: the server stores only ciphertext
|
|
143
|
+
and salted hashes, and never learns your passphrase or the content of any note.
|
|
144
|
+
|
|
145
|
+
**Security model.**
|
|
146
|
+
|
|
147
|
+
- **Two key eras.** Before binding an account, notes are encrypted with a client-derived key
|
|
148
|
+
`K_u = SHA-256(guestKdfPrefix ‖ identity)` (obfuscation-grade, not true E2E). After binding,
|
|
149
|
+
notes are encrypted with a random 256-bit noteKey. The noteKey is wrapped twice — with the
|
|
150
|
+
passphrase (KEK) and with a recovery package (KEK_rec) — before upload. From that point the
|
|
151
|
+
passphrase never crosses the wire: only PH1 (a client-side hash form) is transmitted, and the
|
|
152
|
+
server stores only `PH2 = SHA-256(PH1)`.
|
|
153
|
+
- **In-band versioning.** Payload layout and KDF are defined by configurable cipher prefixes
|
|
154
|
+
(e.g. `jr1b.` for bound-era ciphertext, `jr3w.` for Argon2id-wrapped note keys). Upgrading KDF
|
|
155
|
+
parameters always means a new prefix; old prefixes keep decrypting forever (no in-place
|
|
156
|
+
semantic changes). Cross-prefix calls always return `null` — prefix guard + AAD + length
|
|
157
|
+
checks; they never throw or fall back.
|
|
158
|
+
- **Tamper-evidence.** Every AES-GCM operation binds additional authenticated data (AAD) —
|
|
159
|
+
e.g. ciphertext is bound to its note id, so records cannot be shuffled between notes.
|
|
160
|
+
- **Key hygiene.** Any key that must be exported/wrapped is imported with `extractable=true`;
|
|
161
|
+
KEK and guest keys are permanently non-extractable.
|
|
162
|
+
|
|
163
|
+
**Opt-in primitives.** Optional contracts (`cipherGuest` / `wrapDual` / `wrapShare` / `pinLock` /
|
|
164
|
+
`cipherAttach` / `cipherLocal`) are rejected unless explicitly configured, so forks that don't use
|
|
165
|
+
a primitive are unaffected. Unknown or malformed strings never decrypt to plaintext.
|
|
166
|
+
|
|
167
|
+
**Modules.** Client: two-era crypto (`note-crypto.ts`), Argon2id wrapping (`argon2.ts`, dual
|
|
168
|
+
carrier: `node:crypto` ≥ Node 24.7 / hash-wasm in browsers, RFC 9106 test vectors), local PIN lock
|
|
169
|
+
(`pinlock.ts`), recovery-package BIP39 codec (`bip39.ts`, zero-dependency, cross-checked against
|
|
170
|
+
@scure/bip39), brand-namespaced key storage (`keys.ts`). Server: zero-knowledge auth core with a
|
|
171
|
+
PH1→PH2 login flow, inbound cipher/package validation and paired key-package selection
|
|
172
|
+
(`auth.ts`; PH2-UNIQUE conflict and session revocation are caller-owned wiring), per-IP
|
|
173
|
+
fixed-window rate limiting on D1 (`ratelimit.ts`), shared CORS/hash utilities.
|
|
174
|
+
|
|
175
|
+
**Verification.** `npm run verify` runs 249 assertions against the real modules (no mocks):
|
|
176
|
+
roundtrips, AAD tamper-evidence, extractability rules, era isolation, cross-prefix family
|
|
177
|
+
isolation, payload tampering, RFC 9106 KAT, and a 200-vector BIP39 cross-check.
|
|
178
|
+
|
|
179
|
+
**Usage.** TypeScript source is published (exports point at `.ts`). If you install from npm you
|
|
180
|
+
must bundle it (vite/esbuild etc.); Node's strip-types does not apply inside `node_modules`.
|
|
181
|
+
See the code sample in the 使用 section above.
|
|
182
|
+
|
|
183
|
+
**Honest limits.** The guest era is obfuscation-grade, not E2E. PH1 derivation is defined by each
|
|
184
|
+
product (fast hash or Argon2id); this layer only validates its hex64 form. Product concerns
|
|
185
|
+
(routing, quotas, subscriptions, GC, webhooks) live in the product forks. No third-party audit
|
|
186
|
+
yet. A lost passphrase is unrecoverable by design — that is the price of server-side zero
|
|
187
|
+
knowledge.
|
|
188
|
+
|
|
189
|
+
## License
|
|
190
|
+
|
|
191
|
+
MIT. Security contact: tacetink.csd@gmail.com.
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@tacet-ink/journal-core",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.6",
|
|
4
4
|
"description": "Zero-knowledge journal core — e2e crypto, passphrase-only auth, transport, ratelimit. The server never sees plaintext.",
|
|
5
5
|
"homepage": "https://tacet.ink",
|
|
6
6
|
"repository": {
|
|
@@ -13,6 +13,7 @@
|
|
|
13
13
|
"type": "module",
|
|
14
14
|
"main": "src/index.ts",
|
|
15
15
|
"types": "src/index.ts",
|
|
16
|
+
"sideEffects": false,
|
|
16
17
|
"exports": {
|
|
17
18
|
".": "./src/index.ts",
|
|
18
19
|
"./client/note-crypto": "./src/client/note-crypto.ts",
|
|
@@ -34,10 +35,11 @@
|
|
|
34
35
|
"LICENSE"
|
|
35
36
|
],
|
|
36
37
|
"engines": {
|
|
37
|
-
"node": ">=
|
|
38
|
+
"node": ">=24.7.0"
|
|
38
39
|
},
|
|
39
40
|
"scripts": {
|
|
40
|
-
"verify": "node --experimental-strip-types scripts/verify-core-crypto.ts"
|
|
41
|
+
"verify": "node --experimental-strip-types scripts/verify-core-crypto.ts",
|
|
42
|
+
"typecheck": "tsc -p ."
|
|
41
43
|
},
|
|
42
44
|
"devDependencies": {
|
|
43
45
|
"@cloudflare/workers-types": "^5.20260831.1",
|