@tacet-ink/journal-core 0.2.4 → 0.2.5
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 +4 -4
- package/package.json +1 -1
- package/scripts/verify-core-crypto.ts +226 -73
- package/src/server/auth.ts +3 -2
- package/src/server/ladder.ts +3 -2
package/README.md
CHANGED
|
@@ -18,7 +18,7 @@
|
|
|
18
18
|
```sh
|
|
19
19
|
git clone https://github.com/tacet-ink/journal-core.git && cd journal-core
|
|
20
20
|
npm ci --ignore-scripts
|
|
21
|
-
npm run verify #
|
|
21
|
+
npm run verify # 491 assertions, all green, run against the real modules (no mocks)
|
|
22
22
|
```
|
|
23
23
|
|
|
24
24
|
```ts
|
|
@@ -119,7 +119,7 @@ jr3d/jr4d 兩入參數同輸入,域分離由 info 承載;凍結 KAT 兩 blob
|
|
|
119
119
|
|
|
120
120
|
```sh
|
|
121
121
|
npm ci --ignore-scripts
|
|
122
|
-
npm run verify #
|
|
122
|
+
npm run verify # 491 斷言對真模組(禁鏡像;限流單句 UPSERT…RETURNING 直載真 SQLite):roundtrip/AAD 防搬移/
|
|
123
123
|
# extractable/時代隔離/跨前綴家族隔離/payload 竄改/RFC 9106 KAT/BIP39 @scure 對照 200 組/
|
|
124
124
|
# server-side(inboundCipher/validWrappedKey/pickKeyPackage/checkRate/timingSafeEq)+ pinlock 全函式
|
|
125
125
|
# + 密語正規化 v3 世代(normalizePassphrase/jr4w./jr4d./PH1 v3 鹽域分離+帶內版本化舊契約向量)
|
|
@@ -131,7 +131,7 @@ npm run verify # 475 斷言對真模組(禁鏡像;限流單句 UPSERT…RE
|
|
|
131
131
|
# +KAT 凍結向量 [17](HKDF info 域世代分離+v3/raw 入口契約四 blob)
|
|
132
132
|
# +包裹前綴快檢 [13-2](錯前綴試探免付 KDF:五入口行為+計時帽+計數錨)
|
|
133
133
|
# +CI 供應鏈面 [21](npm ci --ignore-scripts+dependabot runner fromJSON 分流+腳本摘除+NIT-1 錨+r2 MINOR-1 URL 直傳;
|
|
134
|
-
#
|
|
134
|
+
# 491 帳=repo 樹——發行包內無 .github=[21] 四條 workflow 錨收斂為 1 顯性 SKIP 行→480,SKIP 顯形非靜默)
|
|
135
135
|
# +發行 tarball 治理面 [22](pack 白名單零 open-card-pr.sh)
|
|
136
136
|
```
|
|
137
137
|
|
|
@@ -311,7 +311,7 @@ PH1→PH2 login flow, inbound cipher/package validation and paired key-package s
|
|
|
311
311
|
(`auth.ts`; PH2-UNIQUE conflict and session revocation are caller-owned wiring), per-IP
|
|
312
312
|
fixed-window rate limiting on D1 (`ratelimit.ts`), shared CORS/hash utilities.
|
|
313
313
|
|
|
314
|
-
**Verification.** `npm run verify` runs
|
|
314
|
+
**Verification.** `npm run verify` runs 491 assertions against the real modules (no mocks):
|
|
315
315
|
roundtrips, AAD tamper-evidence, extractability rules, era isolation, cross-prefix family
|
|
316
316
|
isolation, payload tampering, RFC 9106 KAT, a 200-vector BIP39 cross-check, and the
|
|
317
317
|
login ladder-guard behavior suite (external audit #8).
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@tacet-ink/journal-core",
|
|
3
|
-
"version": "0.2.
|
|
3
|
+
"version": "0.2.5",
|
|
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": {
|
|
@@ -1,7 +1,10 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* verify-core-crypto.ts — core 抽取的驗證閘(對真模組,禁鏡像——extractable 教訓)。
|
|
3
3
|
* 執行:node --experimental-strip-types scripts/verify-core-crypto.ts
|
|
4
|
-
* 全綠輸出 CORE-CRYPTO-OK
|
|
4
|
+
* 全綠輸出 CORE-CRYPTO-VERIFY-OK(N assertions)(整行=唯一消費錨——下游驗收條款禁前綴子字串
|
|
5
|
+
* 比對,防段數漂移誤判;--only 態改 CORE-CRYPTO-VERIFY-OK(N assertions)(--ONLY 16-18) 顯形);任何失敗 exit 1。
|
|
6
|
+
* [24] 段級運行(2026-10-09):`--only 16,17,18`/`--only 16-18`(或混形)=只跑指定段;setup(頂層向量群+段標記行)恆跑;
|
|
7
|
+
* 段依賴常數由 setup 重生(無跨段狀態依賴——各段消費自己的區塊常數)。省時是這卡的真價值:審查輪重跑 [16]-[18] 不再等 KDF 群全套。
|
|
5
8
|
*
|
|
6
9
|
* 2026-10-09(0.2.4):[23] loginRouteCore ladder 查表守衛段(外審 #8 幽靈帳)——真 node:sqlite
|
|
7
10
|
* shim 直驅七情境(現值 hit/ladder hit 回舊帳/兩面 miss 建幽靈/未配置單查零變/fail-open)
|
|
@@ -129,32 +132,148 @@ let passed = 0;
|
|
|
129
132
|
const failures: string[] = [];
|
|
130
133
|
async function A(name: string, cond: boolean | Promise<boolean>, detail = ''): Promise<void> {
|
|
131
134
|
const ok = cond instanceof Promise ? await cond : cond;
|
|
132
|
-
if (ok) { passed++; console.log(` ✓ ${name}`); }
|
|
133
|
-
else { failures.push(name); console.error(` ✗ ${name}${detail ? ' — ' + detail : ''}`); }
|
|
135
|
+
if (ok) { passed++; secRan.set(secCur, (secRan.get(secCur) ?? 0) + 1); console.log(` ✓ ${name}`); }
|
|
136
|
+
else { failures.push(name); secRan.set(secCur, (secRan.get(secCur) ?? 0) + 1); console.error(` ✗ ${name}${detail ? ' — ' + detail : ''}`); }
|
|
134
137
|
}
|
|
135
138
|
|
|
139
|
+
// ── [24] runner:段級 --only 運行(2026-10-09,t_3ec0936e)——段號語法契約+帳面誠實契約的主場 ──
|
|
140
|
+
// 語法契約([24] parse 面自證):--only <CSV>|<a-b>|<混形>;輸出單調遞增去重(16,17,18 ≡ 16-18);
|
|
141
|
+
// 空值/非數字/未定段/倒序範圍=ERR_ONLY_* fail-closed exit 1。--only 缺席=預設恆全跑(CI/deploy 契約不動)。
|
|
142
|
+
// 帳面契約:--only 態總帳顯形「本輪 X/選段帳 N(N 案非本輪載)」+OK 標記帶 (--ONLY …) 尾碼;skip 段帳面恆列「未跑非通過」;[24]/[25] runner 自證段隨每輪在場。
|
|
143
|
+
// 段標記 helper(secOpen):全跑態輸出位元組恆等原 console.log('\n[N] …');--only 態僅選中段輸出段頭。
|
|
144
|
+
// 毒化矩陣內層 rerun(execFileSync+POISON_GATE_INNER=1)不傳遞 --only=內層恆全帳(POISON_GATE_INNER 語意不變)。
|
|
145
|
+
// 段帳=執行帳(healthy-repo 全跑 runtime 真值,2026-10-10 實測 483);帳面語意契約(逐段「案 X/帳」
|
|
146
|
+
// 對 secRan 執行值顯形):執行 < 帳 的段=本環境條件態未行使(非 skip 態),--only 期望算術對齊本帳。
|
|
147
|
+
// [23] 執行 18 帳 21=[23]④ lookup 未配置(0.2.3 單查恆等)/[23]⑤ fail-open(表缺席)兩情境
|
|
148
|
+
// 在本機樹由 [8] PH1_v2_SALT 訊號面真行使(try/catch 異常面)——車道分流非帳面虛報。
|
|
149
|
+
// secEnter 跑行為脫鉤 secCases;secCases 鍵只承載段存在/帳面/--only 判準的登記義務
|
|
150
|
+
//(secOpen(22,…) 無段標記行=慣例 skip 段)。缺帳防線(teeth):secRan 實跑段必已登記——
|
|
151
|
+
// for (const [k] of secRan) if (!secCases[k] && k !== 22) FAIL;反向缺席段=帳面誠實「案 0/N · skip」。
|
|
152
|
+
// secCases 只餘 [24]/[25] self-account 與 --only 段存在面+teeth 三載體(段帳由 secRan 執行自記)。
|
|
153
|
+
const secCases: Record<number, number> = { 1: 3, 2: 7, 3: 9, 4: 4, 5: 3, 6: 20, 7: 27, 8: 8, 9: 22, 10: 5, 11: 41, 12: 50, 13: 39, 14: 18, 15: 59, 16: 15, 17: 14, 18: 39, 19: 17, 20: 47, 21: 10, 23: 18, 24: 5, 25: 3 };
|
|
154
|
+
function parseOnly(spec: string): { err: string | null; set: number[] } {
|
|
155
|
+
const set = new Set<number>();
|
|
156
|
+
const specStr = spec.trim();
|
|
157
|
+
if (specStr === '') return { err: 'ERR_ONLY_SPEC_EMPTY', set: [] };
|
|
158
|
+
for (const rawPart of specStr.split(',')) {
|
|
159
|
+
const part = rawPart.trim();
|
|
160
|
+
if (part === '') return { err: 'ERR_ONLY_SPEC_EMPTY_TOKEN', set: [] };
|
|
161
|
+
const span = part.match(/^([0-9]+)-([0-9]+)$/);
|
|
162
|
+
if (span) {
|
|
163
|
+
const lo = Number(span[1]); const hi = Number(span[2]);
|
|
164
|
+
if (!(hi >= lo) || !secCases[lo] || !secCases[hi]) return { err: 'ERR_ONLY_SPEC_RANGE', set: [] };
|
|
165
|
+
for (let s = lo; s <= hi; s++) if (secCases[s]) set.add(s);
|
|
166
|
+
continue;
|
|
167
|
+
}
|
|
168
|
+
if (!/^[0-9]+$/.test(part)) return { err: 'ERR_ONLY_SPEC_TOKEN', set: [] };
|
|
169
|
+
const num = Number(part);
|
|
170
|
+
if (!secCases[num]) return { err: 'ERR_ONLY_SPEC_UNKNOWN_SEC', set: [] };
|
|
171
|
+
set.add(num);
|
|
172
|
+
}
|
|
173
|
+
return { err: null, set: [...set].sort((a, b) => a - b) };
|
|
174
|
+
}
|
|
175
|
+
const argvArr = (globalThis as unknown as { process?: { argv?: string[] } }).process?.argv ?? [];
|
|
176
|
+
const argvOnlyIdx = argvArr.indexOf('--only');
|
|
177
|
+
const onlySpecRaw = argvOnlyIdx >= 0 ? argvArr[argvOnlyIdx + 1] ?? '' : null;
|
|
178
|
+
const onlyParsed = onlySpecRaw !== null ? parseOnly(onlySpecRaw) : null;
|
|
179
|
+
if (onlyParsed?.err) {
|
|
180
|
+
console.error('FAIL: ' + onlyParsed.err + ' — --only 段號語法契約:--only 16,17,18(CSV)/--only 16-18(範圍)/混形 16,17-19;--only 缺席=全跑。');
|
|
181
|
+
process.exit(1);
|
|
182
|
+
}
|
|
183
|
+
const onlySet = new Set<number>(onlyParsed?.set ?? []);
|
|
184
|
+
const onlyMode = (): boolean => onlySet.size > 0;
|
|
185
|
+
// runner 自證段(24/25)隨每輪在場(毫秒級;only 態帳面顯形契約的行使面——非選段制)。
|
|
186
|
+
// 全跑態 secEnter 恆 true(n ≥1 且 n ≠ 22——secOpen 行在場即實段;secCases 人工帳冊退役=
|
|
187
|
+
// 靜默不跑面收口:新段只加 secOpen 即跑,段帳由 secRan 自記)。22=慣例 skip 段(bannerless
|
|
188
|
+
// 斷言群帳入 [13]的慣例面;[24] 契約明文在案);n < 1=非法(非段位)。--only 態照選段集+自證段。
|
|
189
|
+
const secEnter = (n: number): boolean =>
|
|
190
|
+
(!onlyMode() ? (n >= 1 && n !== 22) : (onlySet.has(n) || n === 24 || n === 25));
|
|
191
|
+
const onlySpecDisp = (): string => (onlyMode() ? (onlyParsed?.set ?? []).join(',') : '');
|
|
192
|
+
const rangeCompress = (nums: number[]): string => {
|
|
193
|
+
const out: string[] = [];
|
|
194
|
+
let i = 0;
|
|
195
|
+
while (i < nums.length) {
|
|
196
|
+
let j = i;
|
|
197
|
+
while (j + 1 < nums.length && nums[j + 1] === nums[j] + 1) j++;
|
|
198
|
+
out.push(nums[i] === nums[j] ? String(nums[i]) : nums[i] + '-' + nums[j]);
|
|
199
|
+
i = j + 1;
|
|
200
|
+
}
|
|
201
|
+
return out.join(',');
|
|
202
|
+
};
|
|
203
|
+
const onlyTag = (): string => (onlyMode() ? '(--ONLY ' + rangeCompress(onlyParsed?.set ?? []) + ')' : '');
|
|
204
|
+
let secCur = 0;
|
|
205
|
+
const secRan = new Map<number, number>();
|
|
206
|
+
const secOpen = (n: number, text: string): void => {
|
|
207
|
+
secCur = n;
|
|
208
|
+
if (onlyMode() ? onlySet.has(n) : true) console.log(onlyMode() ? text : '\n' + text);
|
|
209
|
+
};
|
|
210
|
+
|
|
211
|
+
// ── setup 恆跑向量群(--only 態也重生——跨段消費常數上移;值與原段位逐位等值,僅求值時點上移)──
|
|
212
|
+
// 依賴帳(--only 分段的承載面——每行原段位標註):note/identityA/held/noteKey/cipherGuest/cipherBound ←[2];
|
|
213
|
+
// pass/recToken/wrapped/salt/wrappedRec/unwrapped/recUnwrapped/localIdentity ←[3];origRaw ←[4];
|
|
214
|
+
// TACET2/passD/pinD/dual ←[6];TACET3/w3/dual3 ←[7];ph1v2a ←[8];bip ←[9];
|
|
215
|
+
// TACET_ATTACH/attachAad/attachPayload/attachCt ←[10];TACET4/pass4/passD4 ←[15]([17]KAT17/[19] 跨段消費)。
|
|
216
|
+
const note = '今天寫了一點東西。';
|
|
217
|
+
const identityA = 'acct-aaaaaaaaaaaaaaaa';
|
|
218
|
+
const held = makeHeldKey();
|
|
219
|
+
const noteKey = await generateNoteKey();
|
|
220
|
+
const cipherGuest = await encryptNote(TACET, held, note, 'noteId:n1', { current: () => identityA }); // held 空態=guest 時代(原 [2] 順序——guest 密文先於 held 設值)
|
|
221
|
+
held.set(noteKey);
|
|
222
|
+
const cipherBound = await encryptNote(TACET, held, note, 'noteId:n1', { current: () => identityA });
|
|
223
|
+
const pass = 'correct-horse-battery-staple-42';
|
|
224
|
+
const recToken = generateRecToken();
|
|
225
|
+
const { wrapped, salt } = await wrapNoteKey(TACET, noteKey, pass);
|
|
226
|
+
const wrappedRec = await (await import('../src/client/note-crypto.ts')).wrapNoteKeyWithRecToken(TACET, noteKey, recToken, identityA);
|
|
227
|
+
const unwrapped = await unwrapNoteKey(TACET, wrapped, pass, salt);
|
|
228
|
+
const recUnwrapped = await unwrapNoteKeyWithRecToken(TACET, wrappedRec, recToken, identityA);
|
|
229
|
+
const origRaw = hex(new Uint8Array(await crypto.subtle.exportKey('raw', noteKey)));
|
|
230
|
+
const localIdentity = 'acct-localwrap-test';
|
|
231
|
+
const TACET2: NoteCryptoConfig = { ...TACET, wrapDual: 'jr2w.', pinSaltPrefix: 'tacet-note-pin1:' };
|
|
232
|
+
const passD = 'dual-factor-passphrase-42';
|
|
233
|
+
const pinD = '2580ab';
|
|
234
|
+
const dual = await wrapNoteKeyDual(TACET2, noteKey, passD, pinD);
|
|
235
|
+
const TACET3: Argon3Config = { wrap3: 'jr3w.', wrapDual3: 'jr3d.', pinSalt3Prefix: 'tacet-note-pin3:' };
|
|
236
|
+
const w3 = await wrapNoteKey3(TACET3, noteKey, pass);
|
|
237
|
+
const dual3 = await wrapNoteKeyDual3(TACET3, noteKey, passD, pinD);
|
|
238
|
+
const ph1v2a = await derivePh1Argon('probe-determinism-pass-42');
|
|
239
|
+
const bip = await import('../src/client/bip39.ts');
|
|
240
|
+
const TACET_ATTACH: NoteCryptoConfig = { ...TACET, cipherAttach: 'jr1c.' };
|
|
241
|
+
const attachAad = 'jr1a:note-abc:att-001';
|
|
242
|
+
const attachPayload = JSON.stringify({ v: 1, kind: 'img', mime: 'image/jpeg', w: 2048, h: 1365, b64: b64(new Uint8Array(2048)) });
|
|
243
|
+
const attachCt = await encryptAttach(TACET_ATTACH, noteKey, attachPayload, attachAad);
|
|
244
|
+
const TACET4: Argon3Config = { wrap4: 'jr4w.', wrapDual4: 'jr4d.', pinSalt3Prefix: 'tacet-note-pin3:' };
|
|
245
|
+
const guest = await crypto.subtle.importKey('raw', enc.encode('seed').slice(0), 'PBKDF2', false, ['deriveKey']); // 型別哨兵:guest 非此路徑——跨段消費([5]/[11]/[13]/[18])
|
|
246
|
+
void guest;
|
|
247
|
+
const pass4 = 'v3-generation-passphrase-42';
|
|
248
|
+
const passD4 = 'dual-factor-passphrase-42'; // NFKC → passD 的全形形
|
|
249
|
+
// srcOf:源碼窗讀取 helper(原 [13] 段內宣告——跨 [14]-[23] 消費=setup 恆跑群;2026-10-09 段級化揚升)
|
|
250
|
+
async function srcOf(rel: string): Promise<string> {
|
|
251
|
+
// node 內建模組動態存取(structured type,零 node types 依賴——argon2.ts getBuiltinModule 母型同構;
|
|
252
|
+
// TS2591 types 帽下 import('node:fs') 靜態/動態皆炸=此繞法)
|
|
253
|
+
const fs = (globalThis as unknown as {
|
|
254
|
+
process?: { getBuiltinModule?: (id: string) => { readFileSync?: (p: string | URL, enc: string) => string } | undefined };
|
|
255
|
+
}).process?.getBuiltinModule?.('node:fs');
|
|
256
|
+
if (!fs?.readFileSync) throw new Error('ERR_FS_UNAVAILABLE');
|
|
257
|
+
return fs.readFileSync(new URL(rel, import.meta.url), 'utf8');
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
const TACET_GUESTOK: NoteCryptoConfig = { ...TACET }; // cipherGuest='jr1g.' 在場,專測 identity 面
|
|
261
|
+
const emptyIdp = { current: () => '' };
|
|
262
|
+
const dual4 = await wrapNoteKeyDual4(TACET4, noteKey, passD4, pinD); // 全形 wrap=NFKC 載體(cp1=0xFF44——ASCII 化即本面 RED)
|
|
136
263
|
// ── 1. 基礎向量 ─────────────────────────────────────────────────────────────
|
|
137
264
|
|
|
138
|
-
|
|
265
|
+
secOpen(1, '[1] PH1 / PBKDF2 / hash-ladder 基礎'); if (secEnter(1)) {
|
|
139
266
|
await A('PH1 = SHA-256(pass) 穩定', (await ph1Of('test-pass-123')) === (await ph1Of('test-pass-123')));
|
|
140
267
|
await A('PH1 hex64 格式', /^[0-9a-f]{64}$/.test(await ph1Of('x')));
|
|
141
268
|
await A('PBKDF2_ITERATIONS = 600k', PBKDF2_ITERATIONS === 600_000);
|
|
142
269
|
|
|
143
270
|
// ── 2. roundtrip(guest 時代 + 綁定時代,AAD 由呼叫端) ───────────────────────
|
|
144
271
|
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
const identityA = 'acct-aaaaaaaaaaaaaaaa';
|
|
148
|
-
const guest = await crypto.subtle.importKey('raw', enc.encode('seed').slice(0), 'PBKDF2', false, ['deriveKey']); // 型別哨兵:guest 非此路徑
|
|
149
|
-
void guest;
|
|
150
|
-
const held = makeHeldKey();
|
|
151
|
-
const cipherGuest = await encryptNote(TACET, held, note, 'noteId:n1', { current: () => identityA });
|
|
272
|
+
};
|
|
273
|
+
secOpen(2, '[2] roundtrip / AAD 防搬移'); if (secEnter(2)) {
|
|
152
274
|
await A('guest 時代前綴 jr1g.', cipherGuest.startsWith('jr1g.'));
|
|
153
275
|
await A('guest roundtrip', (await decryptNote(TACET, held, cipherGuest, 'noteId:n1', { current: () => identityA })) === note);
|
|
154
276
|
|
|
155
|
-
const noteKey = await generateNoteKey();
|
|
156
|
-
held.set(noteKey);
|
|
157
|
-
const cipherBound = await encryptNote(TACET, held, note, 'noteId:n1', { current: () => identityA });
|
|
158
277
|
await A('綁定時代前綴 jr1b.', cipherBound.startsWith('jr1b.'));
|
|
159
278
|
await A('綁定 roundtrip', (await decryptNote(TACET, held, cipherBound, 'noteId:n1', { current: () => identityA })) === note);
|
|
160
279
|
|
|
@@ -170,24 +289,19 @@ await A('舊明文原樣返回', (await decryptNote(TACET, held, '純舊明文',
|
|
|
170
289
|
// ── 3. 包裹三件套(pass / rec / 本機;2026-09-24 效能節——buildBindPayload 兩段
|
|
171
290
|
// PBKDF2 600k 已收 Promise.all 並行,node 實測 133-152ms→68-84ms) ──
|
|
172
291
|
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
const recToken = generateRecToken();
|
|
176
|
-
const { wrapped, salt } = await wrapNoteKey(TACET, noteKey, pass);
|
|
177
|
-
const wrappedRec = await (await import('../src/client/note-crypto.ts')).wrapNoteKeyWithRecToken(TACET, noteKey, recToken, identityA);
|
|
292
|
+
};
|
|
293
|
+
secOpen(3, '[3] 金鑰包裹(passphrase / 復原 / 本機)'); if (secEnter(3)) {
|
|
178
294
|
const recTokenHashValue = await recTokenHash(recToken);
|
|
179
295
|
await A('wrapped 前綴 jr1w.', wrapped.startsWith('jr1w.'));
|
|
180
296
|
await A('salt = 16B hex', /^[0-9a-f]{32}$/.test(salt));
|
|
181
297
|
await A('wrappedRec 前綴 jr1w.', wrappedRec.startsWith('jr1w.'));
|
|
182
298
|
await A('recTokenHash hex64', /^[0-9a-f]{64}$/.test(recTokenHashValue));
|
|
183
299
|
|
|
184
|
-
const unwrapped = await unwrapNoteKey(TACET, wrapped, pass, salt);
|
|
185
300
|
await A('pass unwrap 救回 noteKey(同 pass 同 salt)',
|
|
186
301
|
unwrapped !== null && hex(new Uint8Array(await crypto.subtle.exportKey('raw', unwrapped))) ===
|
|
187
302
|
hex(new Uint8Array(await crypto.subtle.exportKey('raw', noteKey))));
|
|
188
303
|
await A('錯誤 pass unwrap → null', (await unwrapNoteKey(TACET, wrapped, 'wrong-pass', salt)) === null);
|
|
189
304
|
|
|
190
|
-
const recUnwrapped = await unwrapNoteKeyWithRecToken(TACET, wrappedRec, recToken, identityA);
|
|
191
305
|
await A('rec unwrap 救回 noteKey',
|
|
192
306
|
recUnwrapped !== null && hex(new Uint8Array(await crypto.subtle.exportKey('raw', recUnwrapped))) ===
|
|
193
307
|
hex(new Uint8Array(await crypto.subtle.exportKey('raw', noteKey))));
|
|
@@ -202,14 +316,14 @@ await A('改密語:新包裹解得回同 noteKey',
|
|
|
202
316
|
hex(new Uint8Array(await crypto.subtle.exportKey('raw', noteKey))));
|
|
203
317
|
|
|
204
318
|
// 本機包裹
|
|
205
|
-
const localIdentity = 'acct-localwrap-test';
|
|
206
319
|
const heldLocal = makeHeldKey();
|
|
207
320
|
heldLocal.set(noteKey);
|
|
208
321
|
await storeLocalWrap(TACET, localIdentity, noteKey);
|
|
209
322
|
|
|
210
323
|
// ── 4. extractable 鐵律(2026-09-06 iOS 實機炸點回歸) ────────────────────────
|
|
211
324
|
|
|
212
|
-
|
|
325
|
+
};
|
|
326
|
+
secOpen(4, '[4] noteKey extractable 鐵律'); if (secEnter(4)) {
|
|
213
327
|
const rawNote = crypto.getRandomValues(new Uint8Array(32));
|
|
214
328
|
const nonExtractable = await crypto.subtle.importKey('raw', rawNote, { name: 'AES-GCM' }, false, ['encrypt', 'decrypt']);
|
|
215
329
|
let threwNotExtractable = false;
|
|
@@ -224,7 +338,6 @@ const extractable = await crypto.subtle.importKey('raw', rawNote, { name: 'AES-G
|
|
|
224
338
|
await A('修法機制:extractable key exportKey 等值 roundtrip',
|
|
225
339
|
hex(new Uint8Array(await crypto.subtle.exportKey('raw', extractable))) === hex(rawNote));
|
|
226
340
|
// unwrap 出的 key(全部三路徑)都要能再包裹:與 noteKey 原值比對(非 rawNote——那是本段新建隨機值)
|
|
227
|
-
const origRaw = hex(new Uint8Array(await crypto.subtle.exportKey('raw', noteKey)));
|
|
228
341
|
await A('unwrap 出的 noteKey 可再 exportKey(等值 noteKey)',
|
|
229
342
|
hex(new Uint8Array(await crypto.subtle.exportKey('raw', unwrapped!))) === origRaw);
|
|
230
343
|
await A('rec unwrap 出的 noteKey 可再 exportKey(等值 noteKey)',
|
|
@@ -232,7 +345,8 @@ await A('rec unwrap 出的 noteKey 可再 exportKey(等值 noteKey)',
|
|
|
232
345
|
|
|
233
346
|
// ── 5. 時代隔離:guest 與 bound 前綴互不誤判 ─────────────────────────────────
|
|
234
347
|
|
|
235
|
-
|
|
348
|
+
};
|
|
349
|
+
secOpen(5, '[5] 時代隔離'); if (secEnter(5)) {
|
|
236
350
|
await A('guest cipher 不以 jr1b. 開頭', !cipherGuest.startsWith('jr1b.'));
|
|
237
351
|
await A('bound cipher 不以 jr1g. 開頭', !cipherBound.startsWith('jr1g.'));
|
|
238
352
|
await A('無 held key 的 bound 解密 → null',
|
|
@@ -240,11 +354,8 @@ await A('無 held key 的 bound 解密 → null',
|
|
|
240
354
|
|
|
241
355
|
// ── 6. 雙因子合鑰(jr2w.,2026-09-09 PIN 第二因子) ─────────────────────────
|
|
242
356
|
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
const passD = 'dual-factor-passphrase-42';
|
|
246
|
-
const pinD = '2580ab';
|
|
247
|
-
const dual = await wrapNoteKeyDual(TACET2, noteKey, passD, pinD);
|
|
357
|
+
};
|
|
358
|
+
secOpen(6, '[6] 雙因子合鑰 KEK2(jr2w.)'); if (secEnter(6)) {
|
|
248
359
|
await A('wrapped2 前綴 jr2w.', dual.wrapped.startsWith('jr2w.'));
|
|
249
360
|
await A('dual salt1 = 16B hex', /^[0-9a-f]{32}$/.test(dual.salt));
|
|
250
361
|
const unwrapped2 = await unwrapNoteKeyDual(TACET2, dual.wrapped, passD, pinD, dual.salt);
|
|
@@ -293,13 +404,12 @@ await A('未配置 wrapDual 拒絕 dual 包裹', (await unwrapNoteKeyDual(TACET,
|
|
|
293
404
|
// Argon2id 記憶體困難(64MiB)使每猜成本上升 1-2 個數量級。遷移契約 = 成功 unwrap
|
|
294
405
|
// 後客戶端靜默 re-wrap,舊前綴照解(下面有並存斷言)。
|
|
295
406
|
|
|
296
|
-
|
|
297
|
-
|
|
407
|
+
};
|
|
408
|
+
secOpen(7, '[7] Argon2id 包裹(jr3w./jr3d.)'); if (secEnter(7)) {
|
|
298
409
|
await A('RFC 9106 無 secret/ad 標準向量(當前載體正確性)', await verifyArgonKat());
|
|
299
410
|
await A('Argon 參數契約 m=64MiB t=3 p=1 tag=32',
|
|
300
411
|
ARGON_MEMORY_KIB === 65536 && ARGON_ITERATIONS === 3 && ARGON_PARALLELISM === 1 && ARGON_TAG_LEN === 32);
|
|
301
412
|
|
|
302
|
-
const w3 = await wrapNoteKey3(TACET3, noteKey, pass);
|
|
303
413
|
await A('wrapped 前綴 jr3w.', w3.wrapped.startsWith('jr3w.'));
|
|
304
414
|
await A('jr3w salt = 16B hex', /^[0-9a-f]{32}$/.test(w3.salt));
|
|
305
415
|
const unwrapped3 = await unwrapNoteKey3(TACET3, w3.wrapped, pass, w3.salt);
|
|
@@ -323,7 +433,6 @@ await A('jr3w payload 竄改 iv 區 → null', await tampered3(3));
|
|
|
323
433
|
await A('jr3w payload 竄改 ct 尾 → null', await tampered3(payload3.length - 1));
|
|
324
434
|
|
|
325
435
|
// jr3d 雙因子(PIN 第二因子,Argon2id 版)
|
|
326
|
-
const dual3 = await wrapNoteKeyDual3(TACET3, noteKey, passD, pinD);
|
|
327
436
|
await A('dual3 前綴 jr3d.', dual3.wrapped.startsWith('jr3d.'));
|
|
328
437
|
await A('dual3 salt1 = 16B hex', /^[0-9a-f]{32}$/.test(dual3.salt));
|
|
329
438
|
const unwrappedD3 = await unwrapNoteKeyDual3(TACET3, dual3.wrapped, passD, pinD, dual3.salt);
|
|
@@ -360,9 +469,9 @@ await A('未配置 wrapDual3 → unwrap 拒收 jr3d',
|
|
|
360
469
|
// 每猜成本從 ~ns(SHA-256×2)升到 ~0.1-0.18s CPU=×10⁴-10⁶。
|
|
361
470
|
// 固定域鹽是被迫設計(per-user 鹽會摧毀 PH2 UNIQUE=幽靈帳號機制基礎);改參數 = 換鹽尾碼(v2/v3)重遷移。
|
|
362
471
|
|
|
363
|
-
|
|
472
|
+
};
|
|
473
|
+
secOpen(8, '[8] PH1 v2(登入憑證 Argon2id 派生)'); if (secEnter(8)) {
|
|
364
474
|
await A('RFC 9106 KAT 先行(載體正確性,本節所有 Argon 斷言的前提)', await verifyArgonKat());
|
|
365
|
-
const ph1v2a = await derivePh1Argon('probe-determinism-pass-42');
|
|
366
475
|
await A('ph1v2 確定性(同 pass 兩跑逐位元同)', ph1v2a === (await derivePh1Argon('probe-determinism-pass-42')));
|
|
367
476
|
await A('ph1v2 hex64 形(validHash64 可收)', /^[0-9a-f]{64}$/.test(ph1v2a));
|
|
368
477
|
// 與 legacy ph1(SHA-256 形)必然不同值=雙查表兩鍵語意成立
|
|
@@ -384,8 +493,8 @@ await A('ph1v2 非恆等(pass ≠ 輸出)', ph1v2a !== 'probe-determinism-pa
|
|
|
384
493
|
|
|
385
494
|
// ── 9. BIP39 復原套件契約(words↔hex64 轉寫層;線上契約 recToken hex64 不變) ──
|
|
386
495
|
|
|
387
|
-
|
|
388
|
-
|
|
496
|
+
};
|
|
497
|
+
secOpen(9, '[9] BIP39 復原套件(24 詞 ⇄ entropy 32B ⇄ hex64)'); if (secEnter(9)) {
|
|
389
498
|
const wl = await import('../src/client/wordlist.ts');
|
|
390
499
|
|
|
391
500
|
const kitWords = await bip.generateBip39Words();
|
|
@@ -483,16 +592,13 @@ await A('words 派生 hex64 unwrap 救回 noteKey',
|
|
|
483
592
|
|
|
484
593
|
// ── 10. 附件密文(jr1c.,image attachments;opt-in 未配置即拒) ───────────────
|
|
485
594
|
|
|
486
|
-
|
|
595
|
+
};
|
|
596
|
+
secOpen(10, '[10] 附件密文(jr1c. cipherAttach opt-in)'); if (secEnter(10)) {
|
|
487
597
|
const bareCfgAttach: NoteCryptoConfig = { ...TACET }; // 無 cipherAttach 欄
|
|
488
598
|
let attachCfgThrow = '';
|
|
489
599
|
try { await encryptAttach(bareCfgAttach, noteKey, '{"v":1}', 'jr1a:n1:a1'); } catch (e) { attachCfgThrow = (e as Error).message; }
|
|
490
600
|
await A('未配置 cipherAttach → encryptAttach throw', attachCfgThrow === 'ERR_ATTACH_NOT_CONFIGURED', attachCfgThrow);
|
|
491
601
|
|
|
492
|
-
const TACET_ATTACH: NoteCryptoConfig = { ...TACET, cipherAttach: 'jr1c.' };
|
|
493
|
-
const attachAad = 'jr1a:note-abc:att-001';
|
|
494
|
-
const attachPayload = JSON.stringify({ v: 1, kind: 'img', mime: 'image/jpeg', w: 2048, h: 1365, b64: b64(new Uint8Array(2048)) });
|
|
495
|
-
const attachCt = await encryptAttach(TACET_ATTACH, noteKey, attachPayload, attachAad);
|
|
496
602
|
await A('附件前綴 jr1c.', attachCt.startsWith('jr1c.'));
|
|
497
603
|
await A('附件 roundtrip(顯式 noteKey)',
|
|
498
604
|
(await decryptAttach(TACET_ATTACH, noteKey, attachCt, attachAad)) === attachPayload);
|
|
@@ -503,7 +609,8 @@ await A('附件錯鑰匙 → null',
|
|
|
503
609
|
|
|
504
610
|
// ── 11. 本機 IDB 密文(jr1d. cipherLocal opt-in)+ guest 選配拒絕面 ──────────
|
|
505
611
|
|
|
506
|
-
|
|
612
|
+
};
|
|
613
|
+
secOpen(11, '[11] 本機 IDB 密文(jr1d. cipherLocal opt-in)+ guest 選配拒絕面'); if (secEnter(11)) {
|
|
507
614
|
// 分享包裹(jrsw.,wrapShare 選配)+ jr3s. roundtrip(閘本有 roundtrip 斷言缺席)
|
|
508
615
|
const TACET_SHARE: NoteCryptoConfig = { ...TACET, wrapShare: 'jrsw.' };
|
|
509
616
|
const shareWrapped = await wrapNoteKeyShare(TACET_SHARE, noteKey, 'share-pass-42');
|
|
@@ -612,8 +719,6 @@ await A('既有 guest 配置 roundtrip 不受選配化影響',
|
|
|
612
719
|
(await decryptNote(TACET, makeHeldKey(), cipherGuest, 'noteId:n1', { current: () => identityA })) === note);
|
|
613
720
|
|
|
614
721
|
// guest 空 identity 拒絕面:K_u = SHA-256(prefix ‖ '') 靜默產出「空帳號金鑰」是誤配炸彈
|
|
615
|
-
const TACET_GUESTOK: NoteCryptoConfig = { ...TACET }; // cipherGuest='jr1g.' 在場,專測 identity 面
|
|
616
|
-
const emptyIdp = { current: () => '' };
|
|
617
722
|
let emptyIdMsg: string | null = null;
|
|
618
723
|
try { await encryptNote(TACET_GUESTOK, makeHeldKey(), note, 'noteId:n1', emptyIdp); } catch (e) { emptyIdMsg = (e as Error).message; }
|
|
619
724
|
await A("guest 加密面空 identity → throw ERR_NO_IDENTITY(falsy 一體:空字串/undefined/null)",
|
|
@@ -643,7 +748,8 @@ await A("畸形 guest 前綴 '' → decryptNote 舊明文相容層同「未配
|
|
|
643
748
|
// ── 12. 伺服器端原語(makeInboundCipher/validWrappedKey/pickKeyPackage/
|
|
644
749
|
// checkRate/timingSafeEq)+ 本機鎖定全函式(pinlock jr1p.) ──────────
|
|
645
750
|
|
|
646
|
-
|
|
751
|
+
};
|
|
752
|
+
secOpen(12, '[12] 伺服器端原語(auth/ratelimit/hash)+ 本機鎖定(pinlock)'); if (secEnter(12)) {
|
|
647
753
|
// ratelimit 行為面(UPSERT…RETURNING 單句設計收口):真 node:sqlite 直載 UPSERT…RETURNING 單句,
|
|
648
754
|
// 行為斷言全數「真生產函式 checkRate 直驅」——D1 形 shim 包 node:sqlite 只做 D1 run() 的
|
|
649
755
|
// {results} 回帶形,無測試端 glue 副本。表名直插 SQL=注入面驗證;放行判準 count<=max
|
|
@@ -875,7 +981,8 @@ await A('jr1p 跨家族:jr1p. 字串餵 jr1w unwrap → null',
|
|
|
875
981
|
|
|
876
982
|
// ── 13. 新契約面(hex fail-closed/salt hex 形收口/PH1 鹽注入/錯誤碼語意分離) ──
|
|
877
983
|
|
|
878
|
-
|
|
984
|
+
};
|
|
985
|
+
secOpen(13, '[13] unwrap 驗證統一新契約(fail-closed + 語意分離)'); if (secEnter(13)) {
|
|
879
986
|
// hexToBytes fail-closed(note-crypto 與 bip39 同步——bip39 面是 HEX64_RE 前置自守)
|
|
880
987
|
await A('hexToBytes 合法輸出不變(舊碼同值;新向量)', hexToBytes('00ff10').join(',') === '0,255,16');
|
|
881
988
|
await A('hexToBytes 大小寫正規化(新行為,舊碼同值)', (() => { const a = Array.from(hexToBytes('AB')); const b = Array.from(hexToBytes('ab')); return a.length === b.length && a.every((v, i) => v === b[i]); })());
|
|
@@ -1033,15 +1140,6 @@ await A('wrapNoteKeyDual3 未配置 wrapDual3 → ERR_JR3W_NOT_CONFIGURED(契
|
|
|
1033
1140
|
// PH1 鹽注入:預設舊值零行為變更
|
|
1034
1141
|
await A('derivePh1Argon 預設鹽零行為變更(同 pass 同值)',
|
|
1035
1142
|
(await derivePh1Argon('probe-determinism-pass-42')) === ph1v2a);
|
|
1036
|
-
async function srcOf(rel: string): Promise<string> {
|
|
1037
|
-
// node 內建模組動態存取(structured type,零 node types 依賴——argon2.ts getBuiltinModule 母型同構;
|
|
1038
|
-
// TS2591 types 帽下 import('node:fs') 靜態/動態皆炸=此繞法)
|
|
1039
|
-
const fs = (globalThis as unknown as {
|
|
1040
|
-
process?: { getBuiltinModule?: (id: string) => { readFileSync?: (p: string | URL, enc: string) => string } | undefined };
|
|
1041
|
-
}).process?.getBuiltinModule?.('node:fs');
|
|
1042
|
-
if (!fs?.readFileSync) throw new Error('ERR_FS_UNAVAILABLE');
|
|
1043
|
-
return fs.readFileSync(new URL(rel, import.meta.url), 'utf8');
|
|
1044
|
-
}
|
|
1045
1143
|
await A('derivePh1Argon 預設分支源碼面 = encode(PH1_V2_SALT)(鹽預設單一真相錨;同-pass 同值斷言對預設值漂移無承載力)',
|
|
1046
1144
|
/saltArg \?\? new TextEncoder\(\)\.encode\(PH1_V2_SALT\)/.test(await srcOf('../src/client/argon2.ts')));
|
|
1047
1145
|
await A('derivePh1Argon 自選鹽 → 不同派生值', (await derivePh1Argon('probe-determinism-pass-42', new Uint8Array(12))) !== ph1v2a);
|
|
@@ -1095,7 +1193,8 @@ await A('[22] 發行 tarball 零 open-card-pr.sh(治理掃蕩面——pack 白
|
|
|
1095
1193
|
// buildBindPayload 串行 133-152ms→並行 68-84ms;dual 串行 234→並行 179ms median;
|
|
1096
1194
|
// b64 4MiB 132-181ms→18ms。瀏覽器實測 222ms→27ms。
|
|
1097
1195
|
|
|
1098
|
-
|
|
1196
|
+
};
|
|
1197
|
+
secOpen(14, '[14] 效能形契約(buildBindPayload 並行/b64 分塊/argon 禁並行/CORS Max-Age/sideEffects)'); if (secEnter(14)) {
|
|
1099
1198
|
|
|
1100
1199
|
const noteCryptoSrc14 = await srcOf('../src/client/note-crypto.ts');
|
|
1101
1200
|
const argon2Src14 = await srcOf('../src/client/argon2.ts');
|
|
@@ -1236,13 +1335,11 @@ await A('[14] corsResponse 仍完整轉發四頭(corsResponse 面帶 Max-Age
|
|
|
1236
1335
|
// 函式體零改=raw 契約永久不變——本節同字面雙家族對照向量(PASS12x/PASS12x)就是
|
|
1237
1336
|
// 「免換前綴合約證明」的承載面:同組字串在舊家族恆異 KEK(raw 直派生)、在 v3 家族恆同 KEK(NFKC 收容)。
|
|
1238
1337
|
|
|
1239
|
-
|
|
1338
|
+
};
|
|
1339
|
+
secOpen(15, '[15] 密語正規化 v3 世代(normalizePassphrase/jr4w./jr4d./PH1 v3)'); if (secEnter(15)) {
|
|
1240
1340
|
|
|
1241
1341
|
const noteCryptoSrc15 = await srcOf('../src/client/note-crypto.ts');
|
|
1242
1342
|
const argon2Src15 = await srcOf('../src/client/argon2.ts');
|
|
1243
|
-
const TACET4: Argon3Config = { wrap4: 'jr4w.', wrapDual4: 'jr4d.', pinSalt3Prefix: 'tacet-note-pin3:' };
|
|
1244
|
-
const pass4 = 'v3-generation-passphrase-42';
|
|
1245
|
-
const passD4 = 'dual-factor-passphrase-42'; // NFKC → passD 的全形形
|
|
1246
1343
|
const passD4_NFKC = normalizePassphrase(passD4); // 與 passD 恆等(NFKC 收容)——P2 毒化翻面本體(毒形即收口形計數錨翻+毒翻 = NFKC-effective 帳面翻,雙承載)
|
|
1247
1344
|
|
|
1248
1345
|
// 函式契約面(單一真相本體:normalizePassphrase 與 normalizePin 並存不混)
|
|
@@ -1275,7 +1372,6 @@ await A('jr4w 錯 pass → null', (await unwrapNoteKey4(TACET4, w4.wrapped, 'wro
|
|
|
1275
1372
|
await A('jr4w 錯 salt → null', (await unwrapNoteKey4(TACET4, w4.wrapped, pass4, 'ab'.repeat(16))) === null);
|
|
1276
1373
|
|
|
1277
1374
|
// jr4d. 雙因子(v3 密語+PIN 照舊)——wrap 端吃全形(NFKC 載體),unwrap 端兩向都咬
|
|
1278
|
-
const dual4 = await wrapNoteKeyDual4(TACET4, noteKey, passD4, pinD); // 全形 wrap=NFKC 載體(cp1=0xFF44——ASCII 化即本面 RED)
|
|
1279
1375
|
await A('dual4 前綴 jr4d.', dual4.wrapped.startsWith('jr4d.'));
|
|
1280
1376
|
await A('dual4 salt1 = 16B hex', /^[0-9a-f]{32}$/.test(dual4.salt));
|
|
1281
1377
|
const unwrappedD4 = await unwrapNoteKeyDual4(TACET4, dual4.wrapped, passD, pinD, dual4.salt);
|
|
@@ -1413,7 +1509,8 @@ await A('index barrel 匯出 v3 世代七識別字(normalizePassphrase/jr4
|
|
|
1413
1509
|
// v3 家族入口 normalize 收口形恰 3×3+PBKDF2 raw 恰 1——毒化形即計數變異,本 repo 樹直接斷言。
|
|
1414
1510
|
// 每案結束後還原/清樹(出生即棄);designated 面帳寫在案例名內。
|
|
1415
1511
|
|
|
1416
|
-
|
|
1512
|
+
};
|
|
1513
|
+
secOpen(16, '[16] 常駐毒化矩陣(v3 家族語意毒+舊家族不變毒)'); if (secEnter(16)) {
|
|
1417
1514
|
{
|
|
1418
1515
|
const getBuiltin = (id: string): unknown =>
|
|
1419
1516
|
(globalThis as unknown as { process?: { getBuiltinModule?: (i: string) => unknown } }).process?.getBuiltinModule?.(id);
|
|
@@ -1641,6 +1738,7 @@ console.log('\n[16] 常駐毒化矩陣(v3 家族語意毒+舊家族不變毒
|
|
|
1641
1738
|
}
|
|
1642
1739
|
}
|
|
1643
1740
|
}
|
|
1741
|
+
};
|
|
1644
1742
|
|
|
1645
1743
|
// ── 17. KAT 凍結向量:HKDF info 域世代分離(修正承載面) ──────────────
|
|
1646
1744
|
//
|
|
@@ -1679,7 +1777,7 @@ const KAT17 = {
|
|
|
1679
1777
|
{
|
|
1680
1778
|
const katRawUnlock = async (key: CryptoKey | null): Promise<boolean> =>
|
|
1681
1779
|
key !== null && (await hex(new Uint8Array(await crypto.subtle.exportKey('raw', key)))) === KAT17.raw;
|
|
1682
|
-
|
|
1780
|
+
secOpen(17, '[17] KAT 凍結向量:HKDF info 域世代分離+v3/raw 入口契約(jr3w./jr3d./jr4w./jr4d. 四 blob)'); if (secEnter(17)) {
|
|
1683
1781
|
await A('[17] jr3w 凍結 blob:全形 RAW unwrap = 原始 noteKey(舊家族 raw 契約——KAT 承載)',
|
|
1684
1782
|
katRawUnlock(await unwrapNoteKey3(TACET3, KAT17.jr3w, KAT17.pass3Raw, KAT17.s3)));
|
|
1685
1783
|
await A('[17] jr3w 凍結 blob:normalized 慣用形 → null(normalized 面恆拒——raw 族零正規化承載)',
|
|
@@ -1708,6 +1806,7 @@ const KAT17 = {
|
|
|
1708
1806
|
(await unwrapNoteKeyDual3(TACET3, KAT17.jr4d, passD, KAT17.pin, KAT17.sd4)) === null);
|
|
1709
1807
|
await A('[17] family isolation:jr3d blob 餵 jr4d 路徑 → null(KEK2 info 域分離對稱面)',
|
|
1710
1808
|
(await unwrapNoteKeyDual4(TACET4, KAT17.jr3d, passD, KAT17.pin, KAT17.sd3)) === null);
|
|
1809
|
+
};
|
|
1711
1810
|
}
|
|
1712
1811
|
|
|
1713
1812
|
// ── 18. 本機包裹專用前綴(wrapLocal opt-in+讀舊寫新自癒;jr1l.) ──
|
|
@@ -1722,7 +1821,7 @@ const KAT17 = {
|
|
|
1722
1821
|
// ③未配置態讀取面:照走舊形 cfg.wrap(行為不變),且不自癒重寫(heal 綁在新前綴面)。
|
|
1723
1822
|
// KEK 不動=deriveGuestKey(identity 派生、passphrase-free——session 期免重打密語的機制
|
|
1724
1823
|
// 原樣);本段只界「前綴」面(payload 布局的 KDF/prefix 契約由前綴界定——帶內版本化母型)。
|
|
1725
|
-
|
|
1824
|
+
secOpen(18, '[18] 本機包裹專用前綴(wrapLocal=jr1l. opt-in+讀舊寫新自癒)'); if (secEnter(18)) {
|
|
1726
1825
|
{
|
|
1727
1826
|
// section 專用 Map store:node 端 localStorage 死位(keys.ts try/catch 恆 null)——stored 面
|
|
1728
1827
|
// 行為斷言要可觀察可重放,結構 KeyStore 注入(同 cfg 注入母型;TACET 基座 fixture 不動)。
|
|
@@ -1984,6 +2083,7 @@ console.log('\n[18] 本機包裹專用前綴(wrapLocal=jr1l. opt-in+讀舊
|
|
|
1984
2083
|
}
|
|
1985
2084
|
}
|
|
1986
2085
|
}
|
|
2086
|
+
};
|
|
1987
2087
|
|
|
1988
2088
|
// ── 19. ladder 表與重取鹽面((a+) 案) ──────────────
|
|
1989
2089
|
//
|
|
@@ -1994,7 +2094,7 @@ console.log('\n[18] 本機包裹專用前綴(wrapLocal=jr1l. opt-in+讀舊
|
|
|
1994
2094
|
// 直驅 makePh2LadderStore:upsert 恰一列、同一舊值最新持有者勝、無 DELETE 語句面)。
|
|
1995
2095
|
// 表名/kind 常數的契約面(PH2_LADDER_TABLE='ph2_ladder'、KIND legacy/v2)由本段斷言直接咬住
|
|
1996
2096
|
//(fork migration 0011 與 core 原語同字面——漂移=fork schema 與 core 原語脫鉤=表缺席 fail-open 面)。
|
|
1997
|
-
|
|
2097
|
+
secOpen(19, '[19] ladder 表與重取鹽面(unwrapNoteKeyDual4WithSalt+ph2_ladder)'); if (secEnter(19)) {
|
|
1998
2098
|
{
|
|
1999
2099
|
const { DatabaseSync } = (globalThis as unknown as {
|
|
2000
2100
|
process?: { getBuiltinModule?: (id: string) => { DatabaseSync?: unknown } };
|
|
@@ -2087,6 +2187,7 @@ console.log('\n[19] ladder 表與重取鹽面(unwrapNoteKeyDual4WithSalt+ph2
|
|
|
2087
2187
|
await A('[19] WithSalt 未配置 cfg(wrapDual4 缺)→ null(opt-in 鐵律同向)',
|
|
2088
2188
|
(await unwrapNoteKeyDual4WithSalt({ pinSalt3Prefix: 'tacet-note-pin3:' }, rewrapD4.wrapped, passD, pinD, rewrapD4.salt)) === null);
|
|
2089
2189
|
}
|
|
2190
|
+
};
|
|
2090
2191
|
// ── 20. 復原套件專用前綴+HKDF 派生世代 ──
|
|
2091
2192
|
//
|
|
2092
2193
|
// 裁定:採搭車——wrappedRec 專用前綴+KEK_rec 改
|
|
@@ -2108,7 +2209,7 @@ console.log('\n[19] ladder 表與重取鹽面(unwrapNoteKeyDual4WithSalt+ph2
|
|
|
2108
2209
|
// 涵蓋;摘雙試=遷移率裁定非日曆)。兩欄皆缺席(舊 cfg)=單試舊面(行為不變)。
|
|
2109
2210
|
// ⑤四象限 write→read 矩陣(檢查對齊 M1):每象限「自 wrap 可自 unwrap 或 wrap 拒絕」
|
|
2110
2211
|
// +超集 cfg 可讀子集 blob(升級路徑保護)+B01 歷史缺陷形恆拒(升 KDF=換前綴)。
|
|
2111
|
-
|
|
2212
|
+
secOpen(20, '[20] 復原套件專用前綴(wrapRec=jr1r. opt-in)+KEK_rec HKDF 世代(recKekHkdf opt-in,兩欄一體配對門)'); if (secEnter(20)) {
|
|
2112
2213
|
{
|
|
2113
2214
|
const recSalt20 = 'a1b2c3d4e5f60718293a4b5c6d7e8f90';
|
|
2114
2215
|
const TACET5: NoteCryptoConfig = { ...TACET, wrapRec: 'jr1r.', recKekHkdf: 'jr1r.', recKekSalt: recSalt20 };
|
|
@@ -2392,6 +2493,7 @@ console.log('\n[20] 復原套件專用前綴(wrapRec=jr1r. opt-in)+KEK_rec
|
|
|
2392
2493
|
trees20.length === 6 && trees20.every((t) => !existsSync20(t)));
|
|
2393
2494
|
}
|
|
2394
2495
|
}
|
|
2496
|
+
};
|
|
2395
2497
|
|
|
2396
2498
|
// ── 21. CI 供應鏈面(npm ci --ignore-scripts+Dependabot runner 分流)──
|
|
2397
2499
|
//
|
|
@@ -2399,7 +2501,7 @@ console.log('\n[20] 復原套件專用前綴(wrapRec=jr1r. opt-in)+KEK_rec
|
|
|
2399
2501
|
// --ignore-scripts 是安裝面防線(postinstall/preinstall/prepare 全免執行);
|
|
2400
2502
|
// Dependabot PR 走 GitHub-hosted runner 隔離(self-hosted runner 不接機器人分支)。
|
|
2401
2503
|
// 靜態錨面:workflow YAML 本體的接線(job-if C1 母型+actor 分流+runs-on fromJSON 陣列形)。
|
|
2402
|
-
|
|
2504
|
+
secOpen(21, '[21] CI 供應鏈面(npm ci --ignore-scripts+dependabot runner 分流)'); if (secEnter(21)) {
|
|
2403
2505
|
{
|
|
2404
2506
|
// 環境鍵=.github 目錄存在(存在性鍵非檔名硬讀——r1 MINOR-3):閘 src 檔在 files 白名單
|
|
2405
2507
|
// 而 .github 不出包——tarball 消費樹硬讀 workflows=未守衛 ENOENT,rc=1 尾段全吞。
|
|
@@ -2422,7 +2524,7 @@ console.log('\n[21] CI 供應鏈面(npm ci --ignore-scripts+dependabot runne
|
|
|
2422
2524
|
await A('[21] publish.yml:npm ci --ignore-scripts(tag 驅動無 actor 面;typecheck+verify 先行=腳本面零在場)',
|
|
2423
2525
|
srcPy21.includes('npm ci --ignore-scripts'));
|
|
2424
2526
|
} else {
|
|
2425
|
-
await A('[21] CI 源碼面:.github 缺席環境(tarball 發行樹常態)=顯性 SKIP(workflow 斷言 4 收斂為 1 SKIP 行——消費端計數帳
|
|
2527
|
+
await A('[21] CI 源碼面:.github 缺席環境(tarball 發行樹常態)=顯性 SKIP(workflow 斷言 4 收斂為 1 SKIP 行——消費端計數帳 479)',
|
|
2426
2528
|
true, 'no .github dir — consumption tree face');
|
|
2427
2529
|
}
|
|
2428
2530
|
// 摘除自證(t_760f44e8 治理掃蕩補漏;fs 原語缺席環境=顯性 FAIL 形自守衛——非 silent true)
|
|
@@ -2447,8 +2549,9 @@ console.log('\n[21] CI 供應鏈面(npm ci --ignore-scripts+dependabot runne
|
|
|
2447
2549
|
&& !/readFileSync\(new URL\([^)]*\)\.pathname/.test(gateSrc21));
|
|
2448
2550
|
}
|
|
2449
2551
|
}
|
|
2552
|
+
};
|
|
2450
2553
|
|
|
2451
|
-
|
|
2554
|
+
secOpen(23, '[23] loginRouteCore ladder 查表守衛(外審 #8 幽靈帳——現值 miss → ladder → 兩面 miss 才 createUser)'); if (secEnter(23)) {
|
|
2452
2555
|
{
|
|
2453
2556
|
// getBuiltin23:node 內建模組動態存取(本段自備 helper——[19]/[21] 同構;structured type 零 node types 依賴)
|
|
2454
2557
|
const getBuiltin23 = (id: string): unknown =>
|
|
@@ -2660,7 +2763,7 @@ console.log('\n[23] loginRouteCore ladder 查表守衛(外審 #8 幽靈帳—
|
|
|
2660
2763
|
const storeP = {
|
|
2661
2764
|
findByIdentityQuery: async () => null,
|
|
2662
2765
|
createUser: async () => 'acct-ghost-23',
|
|
2663
|
-
getByUserKey: async () => null,
|
|
2766
|
+
getByUserKey: async (key: string) => (tP.db.prepare(`SELECT ${USER_COLS23} FROM users WHERE account_id = ?`).get(key) as unknown) || null,
|
|
2664
2767
|
revokeAllSessions: async () => {},
|
|
2665
2768
|
insertSession: async () => {},
|
|
2666
2769
|
ladderLookup: async () => 'acct-23old',
|
|
@@ -2685,10 +2788,60 @@ console.log('\n[23] loginRouteCore ladder 查表守衛(外審 #8 幽靈帳—
|
|
|
2685
2788
|
}
|
|
2686
2789
|
}
|
|
2687
2790
|
|
|
2791
|
+
};
|
|
2792
|
+
secOpen(24, '[24] runner 契約自證(--only 解析契約 fail-closed+段級帳面顯形)'); if (secEnter(24)) {
|
|
2793
|
+
await A('[24] --only 解析契約(全跑態):spec 缺席=onlySet 空集=secEnter 恆真(CI 形契約不變——npm run verify 全帳;--only 態本面恆真跳過=帳面由次面承載)',
|
|
2794
|
+
!onlyMode() ? (onlySpecRaw === null && parseOnly('').err === 'ERR_ONLY_SPEC_EMPTY' && secEnter(1) && secEnter(23)) : true);
|
|
2795
|
+
await A('[24] --only 解析契約(CSV/範圍/混形三形同集合):16,17,18 ≡ 16-18(同集合);16,17-19 展開 16..19;單調遞增去重 18,16-17,16 → [16,17,18]',
|
|
2796
|
+
JSON.stringify(parseOnly('16,17,18').set) === JSON.stringify([16, 17, 18])
|
|
2797
|
+
&& JSON.stringify(parseOnly('16-18').set) === JSON.stringify([16, 17, 18])
|
|
2798
|
+
&& JSON.stringify(parseOnly('16,17-19').set) === JSON.stringify([16, 17, 18, 19])
|
|
2799
|
+
&& JSON.stringify(parseOnly('18,16-17,16').set) === JSON.stringify([16, 17, 18]));
|
|
2800
|
+
await A('[24] --only 解析契約(fail-closed 分流):空值/非數字 token/慣例 skip 段 22/未定段(0/99)/倒序範圍=ERR_ONLY_*(exit 1 由頂層守衛承載——parse 面證 err 分流真值)',
|
|
2801
|
+
parseOnly('').err === 'ERR_ONLY_SPEC_EMPTY' && parseOnly('abc').err === 'ERR_ONLY_SPEC_TOKEN'
|
|
2802
|
+
&& parseOnly('0').err === 'ERR_ONLY_SPEC_UNKNOWN_SEC' && parseOnly('22').err === 'ERR_ONLY_SPEC_UNKNOWN_SEC'
|
|
2803
|
+
&& parseOnly('99').err === 'ERR_ONLY_SPEC_UNKNOWN_SEC' && parseOnly('2-1').err === 'ERR_ONLY_SPEC_RANGE'
|
|
2804
|
+
&& parseOnly('16,,17').err === 'ERR_ONLY_SPEC_EMPTY_TOKEN');
|
|
2805
|
+
await A('[24] 段級帳面顯形契約:全跑態 onlySpecDisp 空+onlyTag 光禿;--only 態 spec CSV 遞增+(--ONLY …) 尾碼形+secRan 本段已入帳',
|
|
2806
|
+
onlyMode() ? ((secRan.get(24) ?? 0) >= 3 && onlySpecDisp() === (onlyParsed?.set ?? []).join(',') && onlyTag().startsWith('(--ONLY ')) : (onlySpecDisp() === '' && onlyTag() === ''));
|
|
2807
|
+
await A('[24] skip 慣例面:secEnter(22)=false(22 慣例 skip 段——secOpen 行缺席+bannerless 斷言群帳入 [13] 的慣例面;secEnter 全跑態 n≥1 且 n≠22 即真=段落帳冊退役)',
|
|
2808
|
+
secEnter(22) === false);
|
|
2809
|
+
}
|
|
2810
|
+
|
|
2811
|
+
secOpen(25, '[25] 段級運行樣本:secEnter 真值表+tag 形帳+secCases 段帳'); if (secEnter(25)) {
|
|
2812
|
+
await A('[25] secEnter 真值表:bannerless 段全跑態行為(26 未在 secOrder 段帳=secRan 零位面——帳面逐段只列 secOrder;n=0 恆 false);全跑態實段 1/15/23 全真',
|
|
2813
|
+
!onlyMode() ? (secEnter(0) === false && (secRan.get(26) ?? 0) === 0 && secEnter(1) && secEnter(15) && secEnter(23)) : true);
|
|
2814
|
+
await A('[25] tag/spec 形帳:--only 態 (--ONLY …) 括形+spec 遞增 CSV 非空;全跑態 spec 空+tag 裸',
|
|
2815
|
+
onlyMode() ? (onlyTag().startsWith('(--ONLY ') && onlyTag().endsWith(')') && onlySpecDisp().length > 0)
|
|
2816
|
+
: (onlySpecDisp() === '' && onlyTag() === ''));
|
|
2817
|
+
await A('[25] secCases 段帳登記義務(負向斷言——teeth):secRan 實跑段(本機樹 [23]⑤ fail-open 表缺席異常面照行)必已登記;secCases 缺鍵且非 22 段=RED(secCases 全摘禁令的行為面承載)',
|
|
2818
|
+
(() => { for (const [k25] of secRan) if (!secCases[k25] && k25 !== 22) return false; return true; })());
|
|
2819
|
+
await A('[25] self-account:secCases[25] = 3 恰此三案(段帳面 self-proving——帳漂移即本面 RED;secCases 載體面:[24]/[25] 自證段+--only 段存在面+teeth 三載體)',
|
|
2820
|
+
secCases[25] === 3 && (secRan.get(25) ?? 0) === 3);
|
|
2821
|
+
}
|
|
2822
|
+
|
|
2823
|
+
// ── 段級帳面(誠實帳——skip 段恆列帳「未跑非通過」;全跑態逐段案數對 secCases 帳面)──
|
|
2824
|
+
const secOrder = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 23, 24, 25];
|
|
2825
|
+
const secTotal = secOrder.reduce((a, n) => a + (secCases[n] ?? 0), 0);
|
|
2826
|
+
const ranSecs = secOrder.filter((n) => (secRan.get(n) ?? 0) > 0);
|
|
2827
|
+
const selTotal = secOrder.filter((n) => !onlyMode() || onlySet.has(n) || n === 24 || n === 25).reduce((a, n) => a + (secCases[n] ?? 0), 0);
|
|
2828
|
+
const skipSecs = secOrder.filter((n) => !ranSecs.includes(n));
|
|
2829
|
+
console.log('\n──── 段級帳面 ────');
|
|
2830
|
+
for (const nSec of secOrder) {
|
|
2831
|
+
const ranN = secRan.get(nSec) ?? 0;
|
|
2832
|
+
const rowLabel = ranSecs.includes(nSec)
|
|
2833
|
+
? (failures.length === 0 ? 'ok' : '見 ✗(失敗 ' + failures.length + '——詳上)')
|
|
2834
|
+
: 'skip(--only 態未選段——本輪未跑非通過)';
|
|
2835
|
+
console.log('[' + nSec + '] 案 ' + ranN + '/' + (secCases[nSec] ?? 0) + ' · ' + rowLabel);
|
|
2836
|
+
}
|
|
2837
|
+
|
|
2688
2838
|
console.log(`\n${passed} 斷言全綠` + (failures.length ? `;${failures.length} 失敗` : ''));
|
|
2839
|
+
if (onlyMode()) {
|
|
2840
|
+
console.log('--only 態:段 ' + onlySpecDisp() + ' 運行,本輪 ' + passed + '/' + selTotal + '(含 runner 自證段 ' + ((secCases[24] ?? 0) + (secCases[25] ?? 0)) + ';總帳 ' + secTotal + ' 案非本輪載;skip ' + skipSecs.length + ' 段——本輪未跑非通過)');
|
|
2841
|
+
}
|
|
2689
2842
|
if (failures.length) {
|
|
2690
2843
|
console.error('失敗項:', failures);
|
|
2691
2844
|
process.exit(1);
|
|
2692
2845
|
}
|
|
2693
2846
|
|
|
2694
|
-
console.log('CORE-CRYPTO-VERIFY-OK');
|
|
2847
|
+
console.log('CORE-CRYPTO-VERIFY-OK(' + passed + ' assertions)' + onlyTag());
|
package/src/server/auth.ts
CHANGED
|
@@ -145,8 +145,9 @@ export interface AuthStore {
|
|
|
145
145
|
* ladder hit → getByUserKey → 可續 buildSession),兩面皆 miss 才 createUser。
|
|
146
146
|
* 未配置(optional undefined)=查表面退場(現行單查行為零變——向後相容:
|
|
147
147
|
* fork 未配 ladder(0011 未上)時行為與 0.2.3 恆等,sennight/vestige 未來接 core 也零炸)。
|
|
148
|
-
* 錯誤處理照 tacet store.ts 母型:
|
|
149
|
-
|
|
148
|
+
* 錯誤處理照 tacet store.ts 母型:lookup 拋任何錯(0011 未上的 ladder 表缺席、ladder store
|
|
149
|
+
* 停機故障)=fail-open 視同 miss,不炸登入(ladder store 停機中登入=建幽靈——fail-open
|
|
150
|
+
* 語意的已知代價;接線端 ladderGuard 統一 catch 只包 lookup)+非 lookup 錯誤照常外拋。
|
|
150
151
|
*/
|
|
151
152
|
ladderLookup?(oldPh2: string): Promise<string | null>;
|
|
152
153
|
/** ladder 入表(遷移線用;upsert冪等)。 */
|
package/src/server/ladder.ts
CHANGED
|
@@ -19,8 +19,9 @@
|
|
|
19
19
|
* 零知識照舊:ladder 只存 ph2 hash 與 account_id,零 PII、零密語材料。
|
|
20
20
|
*
|
|
21
21
|
* 職責分工(fork 對位面):表名/migration 由 fork migrations 承載(0011-ph2-ladder.sql,
|
|
22
|
-
* PK=ph2 值主權威形);本層提供「以舊 ph2
|
|
23
|
-
* (現值 miss → ladder →
|
|
22
|
+
* PK=ph2 值主權威形);本層提供「以舊 ph2 查列」與「遷移時入表」原語。查表守衛自 0.2.4 起
|
|
23
|
+
* 內建 loginRouteCore(現值 miss → ladder → 兩面 miss 才建幽靈;lookup 未配置=零呼叫
|
|
24
|
+
* 行為零變)——自帶 login route 的 fork(tacet loginRoute,v2-first 裁定)續走自身接線。
|
|
24
25
|
*/
|
|
25
26
|
|
|
26
27
|
import type { Env } from './env.ts';
|