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