@tacet-ink/journal-core 0.1.6 → 0.2.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 +46 -12
- package/package.json +3 -2
- package/scripts/verify-core-crypto.ts +823 -1
- package/src/client/argon2.ts +170 -4
- package/src/client/note-crypto.ts +46 -4
- package/src/index.ts +15 -1
- package/src/server/auth.ts +11 -0
- package/src/server/ladder.ts +71 -0
|
@@ -27,6 +27,7 @@ import {
|
|
|
27
27
|
wrapNoteKeyDual,
|
|
28
28
|
unwrapNoteKeyDual,
|
|
29
29
|
normalizePin,
|
|
30
|
+
normalizePassphrase,
|
|
30
31
|
encryptAttach,
|
|
31
32
|
decryptAttach,
|
|
32
33
|
encryptLocal,
|
|
@@ -36,6 +37,9 @@ import {
|
|
|
36
37
|
decryptWithKey,
|
|
37
38
|
b64 as modB64,
|
|
38
39
|
unb64 as unb64Mod,
|
|
40
|
+
deriveGuestKey,
|
|
41
|
+
sealNoteKey,
|
|
42
|
+
importAesGcm,
|
|
39
43
|
} from '../src/client/note-crypto.ts';
|
|
40
44
|
import {
|
|
41
45
|
verifyArgonKat,
|
|
@@ -43,12 +47,19 @@ import {
|
|
|
43
47
|
unwrapNoteKey3,
|
|
44
48
|
wrapNoteKeyDual3,
|
|
45
49
|
unwrapNoteKeyDual3,
|
|
50
|
+
wrapNoteKey4,
|
|
51
|
+
unwrapNoteKey4,
|
|
52
|
+
wrapNoteKeyDual4,
|
|
53
|
+
unwrapNoteKeyDual4,
|
|
54
|
+
unwrapNoteKeyDual4WithSalt,
|
|
46
55
|
ARGON_MEMORY_KIB,
|
|
47
56
|
ARGON_ITERATIONS,
|
|
48
57
|
ARGON_PARALLELISM,
|
|
49
58
|
ARGON_TAG_LEN,
|
|
50
59
|
derivePh1Argon,
|
|
60
|
+
derivePh1ArgonV3,
|
|
51
61
|
PH1_V2_SALT,
|
|
62
|
+
PH1_V3_SALT,
|
|
52
63
|
type Argon3Config,
|
|
53
64
|
} from '../src/client/argon2.ts';
|
|
54
65
|
import { makeKeyStore } from '../src/client/keys.ts';
|
|
@@ -71,6 +82,15 @@ import {
|
|
|
71
82
|
import { checkRate, isRateAllowed, SQL_RATE_BUMP, type RateWindow } from '../src/server/ratelimit.ts';
|
|
72
83
|
import { generateSessionToken, timingSafeEq } from '../src/server/hash.ts';
|
|
73
84
|
import { CORS_HEADERS, corsResponse } from '../src/server/cors.ts';
|
|
85
|
+
import {
|
|
86
|
+
PH2_LADDER_TABLE,
|
|
87
|
+
PH2_LADDER_KIND_LEGACY,
|
|
88
|
+
PH2_LADDER_KIND_V2,
|
|
89
|
+
makePh2LadderStore,
|
|
90
|
+
} from '../src/server/ladder.ts';
|
|
91
|
+
|
|
92
|
+
type Row19 = { account_id: string; ph2_kind: string };
|
|
93
|
+
type Env19 = { DB: { prepare(sql: string): { bind(...params: unknown[]): { first(): Promise<Row19 | null>; run(): Promise<void> } } } };
|
|
74
94
|
|
|
75
95
|
const enc = new TextEncoder();
|
|
76
96
|
|
|
@@ -998,7 +1018,7 @@ const argon2Src14 = await srcOf('../src/client/argon2.ts');
|
|
|
998
1018
|
const corsSrc14 = await srcOf('../src/server/cors.ts');
|
|
999
1019
|
await A('[14] buildBindPayload 兩段共用 Promise.all 並行錨(wrapNoteKey+wrapNoteKeyWithRecToken)',
|
|
1000
1020
|
/await Promise\.all\(\[\r?\n\s*wrapNoteKey\(cfg, noteKey, passphrase\),\r?\n\s*wrapNoteKeyWithRecToken\(cfg, noteKey, recToken, identity\)/.test(noteCryptoSrc14));
|
|
1001
|
-
await A('[14] deriveKek2 pass
|
|
1021
|
+
await A('[14] deriveKek2 輸入順序錨(pass 段 raw 收口形+Promise.all 相鄰形——O2 毒形即本錨翻面)',
|
|
1002
1022
|
/await Promise\.all\(\[\r?\n\s*derivePbkdf2Bits\(passphrase, salt1, PBKDF2_ITERATIONS\),\r?\n\s*derivePbkdf2Bits\(pin,/.test(noteCryptoSrc14));
|
|
1003
1023
|
await A('[14] deriveKek2Argon 正向串行錨(pass/pin 兩段 deriveArgon2id 串行在場)',
|
|
1004
1024
|
/const passBits = await deriveArgon2id\(/.test(argon2Src14) && /const pinBits = await deriveArgon2id\(/.test(argon2Src14));
|
|
@@ -1124,11 +1144,813 @@ await A('[14] corsResponse 仍完整轉發四頭(corsResponse 面帶 Max-Age
|
|
|
1124
1144
|
})());
|
|
1125
1145
|
}
|
|
1126
1146
|
|
|
1147
|
+
// ── 15. 密語正規化 v3 世代(v0.2.0 批卡①:normalizePassphrase+jr4w./jr4d.+PH1 v3) ──
|
|
1148
|
+
//
|
|
1149
|
+
// 帶內版本化母型實例:密語 KEK 輸入契約改變(raw → normalizePassphrase NFKC-only)=新前綴。
|
|
1150
|
+
// 命名兩軸分離:卡D 文案「v3 家族」=PH1 規格代(derivePh1ArgonV3);前綴數字軸 jr4w./jr4d.
|
|
1151
|
+
// =KDF 世代代(jr1w→jr2w→jr3w→jr4w,README 家族表對照)。舊家族(jr1w./jr2w./jr3w./jr3d.)
|
|
1152
|
+
// 函式體零改=raw 契約永久不變——本節同字面雙家族對照向量(PASS12x/PASS12x)就是
|
|
1153
|
+
// 「免換前綴合約證明」的承載面:同組字串在舊家族恆異 KEK(raw 直派生)、在 v3 家族恆同 KEK(NFKC 收容)。
|
|
1154
|
+
|
|
1155
|
+
console.log('\n[15] 密語正規化 v3 世代(normalizePassphrase/jr4w./jr4d./PH1 v3)');
|
|
1156
|
+
|
|
1157
|
+
const noteCryptoSrc15 = await srcOf('../src/client/note-crypto.ts');
|
|
1158
|
+
const argon2Src15 = await srcOf('../src/client/argon2.ts');
|
|
1159
|
+
const TACET4: Argon3Config = { wrap4: 'jr4w.', wrapDual4: 'jr4d.', pinSalt3Prefix: 'tacet-note-pin3:' };
|
|
1160
|
+
const pass4 = 'v3-generation-passphrase-42';
|
|
1161
|
+
const passD4 = 'dual-factor-passphrase-42'; // NFKC → passD 的全形形
|
|
1162
|
+
const passD4_NFKC = normalizePassphrase(passD4); // 與 passD 恆等(NFKC 收容)——P2 毒化翻面本體(r2 CRITICAL-1:毒形即收口形計數錨翻+毒翻 = NFKC-effective 帳面翻,雙承載)
|
|
1163
|
+
|
|
1164
|
+
// 函式契約面(單一真相本體:normalizePassphrase 與 normalizePin 並存不混)
|
|
1165
|
+
await A('normalizePassphrase NFKC 契約:全形收容(pass=pass)', normalizePassphrase('pass') === 'pass');
|
|
1166
|
+
await A('normalizePassphrase NFKC 契約:結合序列合成(カ+U+3099=ガ——NFD/NFC 裝置差收容)',
|
|
1167
|
+
normalizePassphrase('カ\u3099x') === 'ガx');
|
|
1168
|
+
await A('normalizePassphrase 分離向量一:不 trim(首尾空白保留)', normalizePassphrase(' pass ') === ' pass ');
|
|
1169
|
+
await A('normalizePassphrase 分離向量二:大小寫恆異(大小寫摺疊禁絕——大小寫差=不同 KEK/ph2)',
|
|
1170
|
+
normalizePassphrase('PaSs') === 'PaSs' && normalizePassphrase('PaSs') !== normalizePassphrase('pass') && normalizePassphrase('PaSs') !== normalizePassphrase('PASS'));
|
|
1171
|
+
await A('normalizePassphrase 與 normalizePin 契約並存不混(同輸入兩形=兩輸出)',
|
|
1172
|
+
normalizePin(' Pin42 ') === 'pin42' && normalizePassphrase(' Pin42 ') === ' Pin42 ');
|
|
1173
|
+
|
|
1174
|
+
// jr4w. 單因子(v3 密語)roundtrip
|
|
1175
|
+
const w4 = await wrapNoteKey4(TACET4, noteKey, pass4);
|
|
1176
|
+
await A('wrapped 前綴 jr4w.', w4.wrapped.startsWith('jr4w.'));
|
|
1177
|
+
await A('jr4w salt = 16B hex', /^[0-9a-f]{32}$/.test(w4.salt));
|
|
1178
|
+
const unwrapped4 = await unwrapNoteKey4(TACET4, w4.wrapped, pass4, w4.salt);
|
|
1179
|
+
await A('jr4w unwrap 等值 noteKey(extractable 再 export)',
|
|
1180
|
+
unwrapped4 !== null && hex(new Uint8Array(await crypto.subtle.exportKey('raw', unwrapped4))) === origRaw);
|
|
1181
|
+
await A('jr4w 正規化等價:ASCII wrap → 全形 unwrap(unwrap 入口 NFKC 承載)', (await unwrapNoteKey4(TACET4, w4.wrapped, 'v3-generation-passphrase-42', w4.salt)) !== null);
|
|
1182
|
+
const w4Nfd = await wrapNoteKey4(TACET4, noteKey, 'カ\u3099xy');
|
|
1183
|
+
await A('jr4w NFKC 等價:NFD 形 wrap → NFC 形 unwrap', (await unwrapNoteKey4(TACET4, w4Nfd.wrapped, 'ガxy', w4Nfd.salt)) !== null);
|
|
1184
|
+
const w4Case = await wrapNoteKey4(TACET4, noteKey, 'V3-Generation-Pass-42');
|
|
1185
|
+
await A('jr4w raw 大小寫不折疊(v3 契約只收容寫法差,不收大小寫差)',
|
|
1186
|
+
(await unwrapNoteKey4(TACET4, w4Case.wrapped, 'v3-generation-pass-42', w4Case.salt)) === null);
|
|
1187
|
+
const w4Sp = await wrapNoteKey4(TACET4, noteKey, ' spaced pass ');
|
|
1188
|
+
await A('jr4w raw 首尾空白為不同密語(不 trim 契約家族面)',
|
|
1189
|
+
(await unwrapNoteKey4(TACET4, w4Sp.wrapped, 'spaced pass', w4Sp.salt)) === null);
|
|
1190
|
+
await A('jr4w 錯 pass → null', (await unwrapNoteKey4(TACET4, w4.wrapped, 'wrong-passphrase', w4.salt)) === null);
|
|
1191
|
+
await A('jr4w 錯 salt → null', (await unwrapNoteKey4(TACET4, w4.wrapped, pass4, 'ab'.repeat(16))) === null);
|
|
1192
|
+
|
|
1193
|
+
// jr4d. 雙因子(v3 密語+PIN 照舊)——wrap 端吃全形(NFKC 載體),unwrap 端兩向都咬
|
|
1194
|
+
const dual4 = await wrapNoteKeyDual4(TACET4, noteKey, passD4, pinD); // 全形 wrap=NFKC 載體(cp1=0xFF44——ASCII 化即本面 RED)
|
|
1195
|
+
await A('dual4 前綴 jr4d.', dual4.wrapped.startsWith('jr4d.'));
|
|
1196
|
+
await A('dual4 salt1 = 16B hex', /^[0-9a-f]{32}$/.test(dual4.salt));
|
|
1197
|
+
const unwrappedD4 = await unwrapNoteKeyDual4(TACET4, dual4.wrapped, passD, pinD, dual4.salt);
|
|
1198
|
+
const dual4NfkEq = unwrappedD4 !== null
|
|
1199
|
+
&& hex(new Uint8Array(await crypto.subtle.exportKey('raw', unwrappedD4))) === origRaw; // 全形 wrap → NFKC 形:unwrap 真救回(null 恆 false=normalize 摘除不可藏)
|
|
1200
|
+
await A('dual4 unwrap 等值 noteKey(extractable 再 export;全形 wrap 面 NFKC 等價自證——not-null 恆 false 恆真盲區免疫)', dual4NfkEq);
|
|
1201
|
+
const dual4a = await wrapNoteKeyDual4(TACET4, noteKey, passD, pinD); // ASCII NFKC 慣用形 wrap(passD=passD4 的 NFKC 位——上式自證)
|
|
1202
|
+
const dual4NfkRev = dual4a !== null && (await unwrapNoteKeyDual4(TACET4, dual4a.wrapped, passD4, pinD, dual4a.salt)) !== null; // ASCII 包 → 全形解(MAJOR-1 修真向:feed passD4;無 NFKC 即 null)
|
|
1203
|
+
await A('dual4 pass 段 NFKC 反向等價(ASCII wrap → 全形 unwrap;PIN 段契約不動)', dual4NfkRev === true);
|
|
1204
|
+
await A('dual4 PIN 大小寫不敏感(normalizePin 契約 v3 世代照舊)', (await unwrapNoteKeyDual4(TACET4, dual4.wrapped, passD, '2580AB', dual4.salt)) !== null);
|
|
1205
|
+
await A('dual4 PIN 全形 NFKC 等價', (await unwrapNoteKeyDual4(TACET4, dual4.wrapped, passD, '2580AB', dual4.salt)) !== null);
|
|
1206
|
+
await A('dual4 錯 PIN → null', (await unwrapNoteKeyDual4(TACET4, dual4.wrapped, passD, '999999', dual4.salt)) === null);
|
|
1207
|
+
await A('dual4 空 PIN → null(unwrap 面)', (await unwrapNoteKeyDual4(TACET4, dual4.wrapped, passD, '', dual4.salt)) === null);
|
|
1208
|
+
await A('wrapNoteKeyDual4 空 PIN → ERR_PIN_EMPTY(非 NOT_CONFIGURED——t_7710c766 分流 v3 世代照舊)', await (async () => {
|
|
1209
|
+
try { await wrapNoteKeyDual4(TACET4, noteKey, passD, ''); return false; } catch (e) { return (e as Error).message === 'ERR_PIN_EMPTY'; }
|
|
1210
|
+
})());
|
|
1211
|
+
await A('dual4 payload 嚴格 108B(截短 → null——鹽內嵌族 own 檢查在場)', await (async () => {
|
|
1212
|
+
try {
|
|
1213
|
+
const short = new Uint8Array(107);
|
|
1214
|
+
crypto.getRandomValues(short);
|
|
1215
|
+
let bin = '';
|
|
1216
|
+
for (let i = 0; i < short.byteLength; i++) bin += String.fromCharCode(short[i]);
|
|
1217
|
+
return (await unwrapNoteKeyDual4(TACET4, 'jr4d.' + btoa(bin), passD, pinD, dual4.salt)) === null;
|
|
1218
|
+
} catch { return false; }
|
|
1219
|
+
})());
|
|
1220
|
+
await A('dual4 pass 段大小寫與空白恆異(不摺疊不 trim——v3 契約 unwrap 側真行為面)',
|
|
1221
|
+
(await unwrapNoteKeyDual4(TACET4, dual4.wrapped, 'DUAL-FACTOR-PASSPHRASE-42', pinD, dual4.salt)) === null
|
|
1222
|
+
&& (await unwrapNoteKeyDual4(TACET4, dual4.wrapped, passD + ' ', pinD, dual4.salt)) === null);
|
|
1223
|
+
await A('jr4d unwrap 配置面 {wrapDual4 有、pinSalt3Prefix 無} → null(家族守衛雙腿各自承載)',
|
|
1224
|
+
(await unwrapNoteKeyDual4({ wrapDual4: 'jr4d.' }, dual4.wrapped, passD, pinD, dual4.salt)) === null);
|
|
1225
|
+
|
|
1226
|
+
// 同字面兩家族對照(帶內版本化本質向量):PASS12x/PASS12x
|
|
1227
|
+
const fwPass15 = 'PASS12x';
|
|
1228
|
+
const asciiPass15 = 'PASS12x';
|
|
1229
|
+
await A('[15] NFKC 契約向量(fwPass15 = asciiPass15 的 NFKC-effective 全形——self-proving 錨:字面漂移即本面 RED)',
|
|
1230
|
+
normalizePassphrase(fwPass15) === asciiPass15);
|
|
1231
|
+
const w4fw = await wrapNoteKey4(TACET4, noteKey, fwPass15);
|
|
1232
|
+
await A('jr4w 全形/ASCII 同密語(NFKC 收容)', (await unwrapNoteKey4(TACET4, w4fw.wrapped, asciiPass15, w4fw.salt)) !== null);
|
|
1233
|
+
const w3fw = await wrapNoteKey3(TACET3, noteKey, fwPass15); // 舊家族 raw 對照組(同字面)
|
|
1234
|
+
await A('jr3w 全形 wrap 照解(舊家族 raw 契約——全形字面恆可解)', (await unwrapNoteKey3(TACET3, w3fw.wrapped, fwPass15, w3fw.salt)) !== null);
|
|
1235
|
+
await A('jr3w 拒收 normalized 慣用形(舊家族零正規化——帶內版本化「契約面永不變」承載)', (await unwrapNoteKey3(TACET3, w3fw.wrapped, asciiPass15, w3fw.salt)) === null);
|
|
1236
|
+
await A('jr4w unwrap 拒收 jr3w 字串(家族隔離;wrap3 未配置面)', (await unwrapNoteKey4(TACET4, w3.wrapped, pass, w3.salt)) === null);
|
|
1237
|
+
await A('jr3w unwrap 拒收 jr4w 字串(家族隔離——wrap3 在場、payload 前綴不符恆 null)',
|
|
1238
|
+
(await unwrapNoteKey3(TACET3, w4.wrapped, pass4, w4.salt)) === null);
|
|
1239
|
+
await A('jr3d payload 餵 jr4d 路徑 → null(家族隔離)', (await unwrapNoteKeyDual4(TACET4, dual3.wrapped, passD, pinD, dual3.salt)) === null);
|
|
1240
|
+
await A('jr4d payload 餵 jr3d 路徑 → null(家族隔離)', (await unwrapNoteKeyDual3(TACET3, dual4.wrapped, passD, pinD, dual4.salt)) === null);
|
|
1241
|
+
|
|
1242
|
+
// 跨前綴隔離:jr4w.↔jr1w. 双向(PBKDF2 族面——鹽外置同構家族跨世代隔離;jr3↔jr4 面在下方 jr3d/jr4d 兩案)
|
|
1243
|
+
await A('jr4w unwrap 拒收 jr1w 字串(家族隔離——raw 世代 payload 餵 v3 路徑)',
|
|
1244
|
+
(await unwrapNoteKey4(TACET4, wrapped, pass, salt)) === null);
|
|
1245
|
+
await A('jr1w unwrapNoteKey 拒收 jr4w 字串(家族隔離——v3 payload 餵 raw 世代路徑)',
|
|
1246
|
+
(await unwrapNoteKey(TACET2, w4.wrapped, pass4, w4.salt)) === null);
|
|
1247
|
+
|
|
1248
|
+
// 舊家族凍結向量(PBKDF2 族面):jr1w./jr2w. 契約面永不變——全形字面 raw 恆解、normalized 慣用形恆拒
|
|
1249
|
+
//(帶內版本化承載與 jr3w. 對照組同構;jr3 面雙案在上方 fwPass15 三案)
|
|
1250
|
+
const old1fw = await wrapNoteKey(TACET, noteKey, fwPass15);
|
|
1251
|
+
await A('jr1w 凍結(raw 契約):全形 wrap 照解(raw 字面恆可解)',
|
|
1252
|
+
(await unwrapNoteKey(TACET, old1fw.wrapped, fwPass15, old1fw.salt)) !== null);
|
|
1253
|
+
await A('jr1w 凍結:normalized 慣用形解 → null(不隨 v3 正規化——帶內版本化)',
|
|
1254
|
+
(await unwrapNoteKey(TACET, old1fw.wrapped, asciiPass15, old1fw.salt)) === null);
|
|
1255
|
+
const old2fw = await wrapNoteKeyDual(TACET2, noteKey, fwPass15, pinD);
|
|
1256
|
+
await A('jr2w 凍結(pass 段 raw 契約):normalized 解 → null(pin 段 normalizePin 契約不動)',
|
|
1257
|
+
(await unwrapNoteKeyDual(TACET2, old2fw.wrapped, asciiPass15, pinD, old2fw.salt)) === null);
|
|
1258
|
+
await A('jr2w 凍結:全形 wrap 照解(raw 字面恆可解)',
|
|
1259
|
+
(await unwrapNoteKeyDual(TACET2, old2fw.wrapped, fwPass15, pinD, old2fw.salt)) !== null);
|
|
1260
|
+
// 未配置拒絕(opt-in 鐵律 v3 世代照舊)
|
|
1261
|
+
await A('未配置 wrap4 → wrapNoteKey4 拒絕', await (async () => { try { await wrapNoteKey4({}, noteKey, pass4); return false; } catch (e) { return (e as Error).message === 'ERR_JR4W_NOT_CONFIGURED'; } })());
|
|
1262
|
+
await A('未配置 wrapDual4 → wrapNoteKeyDual4 拒絕(ERR_JR4D_NOT_CONFIGURED——語意分離同 t_7710c766)', await (async () => { try { await wrapNoteKeyDual4({}, noteKey, passD, pinD); return false; } catch (e) { return (e as Error).message === 'ERR_JR4D_NOT_CONFIGURED'; } })());
|
|
1263
|
+
await A('未配置 wrap4 → unwrap 拒收 jr4w', (await unwrapNoteKey4({ wrapDual4: 'jr4d.' }, w4.wrapped, pass4, w4.salt)) === null);
|
|
1264
|
+
|
|
1265
|
+
// PH1 v3(normalizePassphrase 派生+新鹽域)
|
|
1266
|
+
const ph1v3a = await derivePh1ArgonV3('probe-v3-pass-42');
|
|
1267
|
+
await A('ph1v3 確定性(同 pass 兩跑逐位元同)', ph1v3a === (await derivePh1ArgonV3('probe-v3-pass-42')));
|
|
1268
|
+
await A('ph1v3 hex64 形', /^[0-9a-f]{64}$/.test(ph1v3a));
|
|
1269
|
+
await A('ph1v3 NFKC 等價(全形=NFKC 形同 ph2——PH2 UNIQUE 契約面)', (await derivePh1ArgonV3('probe-v3-pass-42')) === ph1v3a);
|
|
1270
|
+
await A('ph1v3 大小寫恆異(不分大小寫=不同 ph2)', (await derivePh1ArgonV3('PROBE-V3-PASS-42')) !== ph1v3a);
|
|
1271
|
+
await A('ph1v3 空白恆異(不 trim=不同 ph2)', (await derivePh1ArgonV3(' probe-v3-pass-42 ')) !== ph1v3a);
|
|
1272
|
+
await A('ph1v3 ≠ v2 形(同字面不同值——鹽域世代分離)', ph1v3a !== (await derivePh1Argon('probe-v3-pass-42')));
|
|
1273
|
+
await A('PH1_V3_SALT = "tacet-ph1-v2"(鹽域值代沿 PH1_V2_SALT 先例;與 v2 鹽域及包裹鹽前綴零重疊)',
|
|
1274
|
+
PH1_V3_SALT === 'tacet-ph1-v2' && (PH1_V3_SALT as string) !== PH1_V2_SALT && !PH1_V3_SALT.startsWith('tacet-note') && !PH1_V3_SALT.startsWith('tacet-pinlock'));
|
|
1275
|
+
await A('derivePh1ArgonV3 預設分支源碼面 = encode(PH1_V3_SALT)(鹽預設單一真相錨;同 v2 母型)',
|
|
1276
|
+
/saltArg \?\? new TextEncoder\(\)\.encode\(PH1_V3_SALT\)/.test(argon2Src15));
|
|
1277
|
+
await A('derivePh1ArgonV3 自選鹽 → 不同派生值(卡A #4 saltArg 慣例)',
|
|
1278
|
+
(await derivePh1ArgonV3('probe-v3-pass-42', new Uint8Array(12))) !== ph1v3a);
|
|
1279
|
+
await A('derivePh1Argon v2 預設零動(raw 契約——舊世代派生面零變更)', (await derivePh1Argon('probe-determinism-pass-42')) === ph1v2a);
|
|
1280
|
+
|
|
1281
|
+
// 源碼結構面(帶內版本化「舊家族零改」的錨)
|
|
1282
|
+
await A('[15] normalizePassphrase 本體=單行 NFKC(三面單一真相——函式體禁夾帶 trim/toLowerCase)',
|
|
1283
|
+
/export function normalizePassphrase\(passphrase: string\): string \{\r?\n return passphrase\.normalize\('NFKC'\);\r?\n\}/.test(noteCryptoSrc15));
|
|
1284
|
+
await A('[15] 舊家族 KEK 派生 raw 面守衛(deriveKekArgon(passphrase 恰 2=jr3w wrap/unwrap;deriveInput 形恰 2=jr4w 兩面)',
|
|
1285
|
+
argon2Src15.split('deriveKekArgon(passphrase, salt)').length === 3
|
|
1286
|
+
&& argon2Src15.split('deriveKekArgon(deriveInput(passphrase)').length === 3);
|
|
1287
|
+
await A('[15] deriveKek2Argon 零改(共用派生本體不吃 normalize——kekk2Argon 簽名與 body 兩錨在場)',
|
|
1288
|
+
/async function deriveKek2Argon\(/.test(argon2Src15) && /const passBits = await deriveArgon2id\(passphrase,/.test(argon2Src15));
|
|
1289
|
+
await A('[15] jr4w/jr4d 入口 normalize 走 deriveInput 收口(兩面恰 2——normalize 形歸零非旁路;正規化禁進共用本體)',
|
|
1290
|
+
argon2Src15.split('deriveKek2Argon(cfg, deriveInput(passphrase)').length === 3
|
|
1291
|
+
&& argon2Src15.split('deriveKekArgon(deriveInput(passphrase)').length === 3
|
|
1292
|
+
&& argon2Src15.split('deriveArgon2id(\n deriveInput(passphrase)').length === 2
|
|
1293
|
+
&& !argon2Src15.includes('deriveKekArgon(normalizePassphrase(passphrase'));
|
|
1294
|
+
await A('[15] deriveKek2Argon HKDF info 參數化(jr3d/jr4d 域由呼叫端帶入——本體 hardcoded info 域殘留=0)',
|
|
1295
|
+
/hkdfInfo: string,/.test(argon2Src15)
|
|
1296
|
+
&& argon2Src15.split("deriveKek2Argon(cfg, passphrase, pinNorm, salt1, pinSalt, 'journal-kek2-v1:' + cfg.wrapDual3)").length === 3
|
|
1297
|
+
&& argon2Src15.split("deriveKek2Argon(cfg, deriveInput(passphrase), pinNorm, salt1, pinSalt, 'journal-kek2-v1:' + cfg.wrapDual4)").length === 3
|
|
1298
|
+
&& !argon2Src15.includes("encode('journal-kek2-v1:' + cfg.wrapDual3)"));
|
|
1299
|
+
|
|
1300
|
+
// barrel 匯出面
|
|
1301
|
+
await A('index barrel 匯出 v3 世代七識別字(normalizePassphrase/jr4 家族四/derivePh1ArgonV3/PH1_V3_SALT)', await (async () => {
|
|
1302
|
+
const src = await import('../src/index.ts');
|
|
1303
|
+
return typeof src.normalizePassphrase === 'function' && typeof src.wrapNoteKey4 === 'function'
|
|
1304
|
+
&& typeof src.unwrapNoteKey4 === 'function' && typeof src.wrapNoteKeyDual4 === 'function'
|
|
1305
|
+
&& typeof src.unwrapNoteKeyDual4 === 'function' && typeof src.derivePh1ArgonV3 === 'function'
|
|
1306
|
+
&& src.PH1_V3_SALT === 'tacet-ph1-v2';
|
|
1307
|
+
})());
|
|
1308
|
+
|
|
1127
1309
|
// ── 決議 ────────────────────────────────────────────────────────────────────
|
|
1128
1310
|
|
|
1311
|
+
// ── 16. 常駐毒化矩陣(v0.2.0 批卡①自帶——對真模組行為斷言的毒化證據隨每執行重建) ──
|
|
1312
|
+
//
|
|
1313
|
+
// 母型(t_0ab6e760 閘常駐毒化面):對 /tmp 拷貝樹做突變後重跑同一組行為斷言——
|
|
1314
|
+
// 「毒化態恰翻面」meta 斷言承載外部 poison runner 的證據;毒化跑在拷貝樹=本 repo 樹零接觸。
|
|
1315
|
+
// 期望值每毒型真跑實測(r2 本 run /tmp 拷貝樹一手實測,見各案 comment 歸屬帳);還原面由
|
|
1316
|
+
// /tmp 樹的出生即棄承載(本 repo 樹 byte 不動)。每案還原後重跑回綠(還原正確性的行為帳)。
|
|
1317
|
+
//
|
|
1318
|
+
// 案期望值(r2 修正後程式碼上 /tmp 毒化重測,本 run 一手帳——還原後重跑回綠帶):
|
|
1319
|
+
// P1 摘 jr4w unwrap 入口 normalize → nfail=4 帳(正規化等價面+KAT 慣用形帳面+兩 [15] 錨跟隨)
|
|
1320
|
+
// P2 本體夾帶 lowercase → nfail=11 帳(NFKC-effective 帳面翻面集;P3/P4/P5 針毒形零匹配=全格向量化)
|
|
1321
|
+
// P3 摘 derivePh1ArgonV3 入口 normalize → nfail=2 帳(ph1v3 NFKC 等價面+[15] 收口形錨)
|
|
1322
|
+
// P4 摘 jr4w wrap 入口 normalize → nfail=4 帳(NFD 收容面+全形/ASCII 同密語面+兩 [15] 錨)
|
|
1323
|
+
// P5 摘 jr4d wrap 入口 normalize → nfail=5 帳(dual4 等值面+PIN 兩面+兩 [15] 錨)
|
|
1324
|
+
// O1 jr3w.wrap 入口接 normalize(禁手)→ nfail=4 帳(jr3w 凍結兩面+兩 [15] 錨)
|
|
1325
|
+
// O2 deriveKek2 pass 段接 normalize(禁手)→ nfail=2 帳([14] 輸入順序錨+jr2w 凍結 normalized 面)
|
|
1326
|
+
// O3 normalizePin 摘 trim/lowercase → nfail=9 帳(>=帽——MINOR-3 韌性;PIN 契約毒化真 RED)
|
|
1327
|
+
// O4 摘 jr4d unwrap 入口 normalize(反拉禁手)→ nfail=4 帳(NFKC 反向等價面+KAT 慣用形帳面+兩 [15] 錨)
|
|
1328
|
+
// O1-src/O2-src/O4-src 源碼計數錨([15] 同形複寫——防單點誤删,NIT-3):
|
|
1329
|
+
// v3 家族入口 normalize 收口形恰 3×3+PBKDF2 raw 恰 1——毒化形即計數變異,本 repo 樹直接斷言。
|
|
1330
|
+
// 每案結束後還原/清樹(出生即棄);designated 面帳寫在案例名內。
|
|
1331
|
+
|
|
1332
|
+
console.log('\n[16] 常駐毒化矩陣(v3 家族語意毒+舊家族不變毒)');
|
|
1333
|
+
{
|
|
1334
|
+
const getBuiltin = (id: string): unknown =>
|
|
1335
|
+
(globalThis as unknown as { process?: { getBuiltinModule?: (i: string) => unknown } }).process?.getBuiltinModule?.(id);
|
|
1336
|
+
const cpMod = getBuiltin('node:child_process') as { execFileSync?: (cmd: string, args: string[], opts: { cwd: string; encoding: string; stdio: unknown[]; env?: Record<string, string> }) => string } | undefined;
|
|
1337
|
+
const osMod = getBuiltin('node:os') as { tmpdir?: () => string } | undefined;
|
|
1338
|
+
const nodeEnvOk = !!cpMod?.execFileSync && !!osMod?.tmpdir && typeof (globalThis as unknown as { process?: { getBuiltinModule?: unknown } }).process?.getBuiltinModule === 'function';
|
|
1339
|
+
const processMod = (globalThis as unknown as { process?: { env?: Record<string, string | undefined> } }).process;
|
|
1340
|
+
const inner16 = processMod?.env?.POISON_GATE_INNER === '1';
|
|
1341
|
+
if (!nodeEnvOk || inner16) {
|
|
1342
|
+
await A('[16] 毒化矩陣載體就緒(node 內建模組在場;內層遞迴由 sentinel 跳過=正常;缺席=顯性 FAIL)', inner16, 'env unavailable AND not inner');
|
|
1343
|
+
} else {
|
|
1344
|
+
const exec16 = cpMod!.execFileSync!;
|
|
1345
|
+
const path16 = {
|
|
1346
|
+
join: (...parts: string[]): string => parts.join('/').replace(/\/{2,}/g, '/'),
|
|
1347
|
+
resolve: (...parts: string[]): string => parts.join('/').replace(/\/\.(?=\/|$)/g, '').replace(/\/{2,}/g, '/'),
|
|
1348
|
+
};
|
|
1349
|
+
const fs16 = getBuiltin('node:fs') as {
|
|
1350
|
+
rmSync?: (p: string, o?: { recursive: boolean; force: boolean }) => void;
|
|
1351
|
+
cpSync?: (a: string, b: string, o?: { recursive: boolean; dereference?: boolean; filter?: (src: string) => boolean }) => void;
|
|
1352
|
+
readFileSync?: (p: string, e?: string) => string;
|
|
1353
|
+
writeFileSync?: (p: string, c: string, e?: string) => void;
|
|
1354
|
+
mkdtempSync?: (p: string) => string;
|
|
1355
|
+
existsSync?: (p: string) => boolean;
|
|
1356
|
+
} | undefined;
|
|
1357
|
+
if (!fs16?.mkdtempSync || !fs16.cpSync || !fs16.readFileSync || !fs16.writeFileSync) {
|
|
1358
|
+
await A('[16] node:fs 原語在場(mkdtemp/cp/read/write)', false);
|
|
1359
|
+
} else {
|
|
1360
|
+
const treePaths16: string[] = []; // MINOR-2 修正:殘留判準只看本 run 喚出的 tree16 路徑(跨執行/跨 host 零耦合)
|
|
1361
|
+
const tmpdir16 = osMod!.tmpdir!();
|
|
1362
|
+
const coreNM = path16.resolve(new URL('.', import.meta.url).pathname, '../node_modules');
|
|
1363
|
+
const repoRoot16 = path16.resolve(new URL('.', import.meta.url).pathname, '..'); // MINOR-4:repo root 錨定(禁 cwd 依賴——自 repo 外呼叫 cp 樹也恆真)
|
|
1364
|
+
const gitIgnoreSkip = (src: string): boolean => src === '.git' || src.endsWith('/.git') || src.includes('/.git/') || src === 'node_modules' || src.endsWith('/node_modules') || src.includes('/node_modules/');
|
|
1365
|
+
let tree16 = '';
|
|
1366
|
+
const mkTree = (): void => {
|
|
1367
|
+
fs16!.rmSync!(tree16, { recursive: true, force: true });
|
|
1368
|
+
tree16 = fs16!.mkdtempSync!(path16.join(tmpdir16, 'core-poison-'));
|
|
1369
|
+
treePaths16.push(tree16); // 本 run 喚出記帳(MINOR-2:收尾殘留判準的封閉集)
|
|
1370
|
+
fs16!.cpSync!(repoRoot16, tree16, { recursive: true, filter: (src: string) => !gitIgnoreSkip(src) }); // MINOR-6+r2 MINOR-4:零 .git/零 node_modules 實拷(repo root 錨定——非 cwd;CI 冷複製成本歸零——毒化樹 node_modules 走下方 symlink)
|
|
1371
|
+
fs16!.rmSync!(path16.join(tree16, 'node_modules'), { recursive: true, force: true });
|
|
1372
|
+
// node_modules 走 symlink(worktree 慣例 cp 語意不跟隨):本閘執行樹一定有 node_modules
|
|
1373
|
+
//(@scure 對照組+typescript)——毒化樹補 symlink 即可;缺席環境(bare npm ci 後無 install)
|
|
1374
|
+
// 本閘自己在主樹就跑不起來,不是毒化矩陣的責任面。
|
|
1375
|
+
exec16('ln', ['-s', nodeEnvOk ? coreNM : '', path16.join(tree16, 'node_modules')], { cwd: tree16, encoding: 'utf8', stdio: ['ignore', 'pipe', 'pipe'] });
|
|
1376
|
+
};
|
|
1377
|
+
const runGate16 = (): { rc: number; out: string } => {
|
|
1378
|
+
// 內層 gate 執行帶 POISON_GATE_INNER=1 sentinel——[16] 矩陣在內層自我跳過=遞迴防線
|
|
1379
|
+
//(拷貝樹的閘檔含本節全文;無 sentinel 會自拷貝再自跑=18+ 連鎖行程實證)。
|
|
1380
|
+
try { return { rc: 0, out: exec16('node', ['--experimental-strip-types', 'scripts/verify-core-crypto.ts'], { cwd: tree16, encoding: 'utf8', stdio: ['ignore', 'pipe', 'pipe'], env: { ...(processMod?.env ?? {}), POISON_GATE_INNER: '1' } }) }; }
|
|
1381
|
+
catch (e: unknown) {
|
|
1382
|
+
const err = e as { stdout?: string; stderr?: string; status?: number };
|
|
1383
|
+
return { rc: err.status ?? 1, out: (err.stdout ?? '') + (err.stderr ?? '') };
|
|
1384
|
+
}
|
|
1385
|
+
};
|
|
1386
|
+
const poison16 = (rel: string, needle: string, replacement: string): { applied: boolean; restore: () => void } => {
|
|
1387
|
+
const p = path16.join(tree16, rel);
|
|
1388
|
+
const before = fs16!.readFileSync!(p, 'utf8');
|
|
1389
|
+
const cnt = before.split(needle).length - 1;
|
|
1390
|
+
fs16!.writeFileSync!(p, before.split(needle).join(replacement), 'utf8');
|
|
1391
|
+
return { applied: cnt > 0 && !(fs16!.readFileSync!(p, 'utf8').includes(needle)), restore: (): void => fs16!.writeFileSync!(p, before, 'utf8') };
|
|
1392
|
+
};
|
|
1393
|
+
const realFails = (out: string): string[] => out.split('\n').filter((l: string) => l.trimStart().startsWith('✗')).map((l: string) => l.trim());
|
|
1394
|
+
const nfail = (out: string): number => realFails(out).length;
|
|
1395
|
+
mkTree();
|
|
1396
|
+
const pristine = runGate16();
|
|
1397
|
+
|
|
1398
|
+
await A('[16] 基準:拷貝樹 pristine = 全綠(毒化矩陣前提;rc=1 即環境病非本節)', pristine.rc === 0, 'rc=' + pristine.rc + (pristine.rc === 0 ? '' : ' | stderr tail: ' + pristine.out.split('\n').filter((l: string) => l.trim()).slice(-3).join(' ;; ').slice(0, 220)));
|
|
1399
|
+
|
|
1400
|
+
// P1 摘 jr4w unwrap 入口 normalize → 行為翻 4 面帳(本 run 毒化重測:jr4w 正規化等價面+[15] raw
|
|
1401
|
+
// 守衛錨+[15] 收口形錨+[17] jr4w NFKC 慣用形帳面)——名單面 designated=正規化等價面+dual4 等值面(案內檢)。
|
|
1402
|
+
{
|
|
1403
|
+
mkTree();
|
|
1404
|
+
const mut = poison16('src/client/argon2.ts',
|
|
1405
|
+
" const kek = await deriveKekArgon(deriveInput(passphrase), salt);\n return await openNoteKey(wrapped, kek, 'notekey', cfg.wrap4);",
|
|
1406
|
+
" const kek = await deriveKekArgon(passphrase, salt);\n return await openNoteKey(wrapped, kek, 'notekey', cfg.wrap4);");
|
|
1407
|
+
const g = runGate16();
|
|
1408
|
+
// P1 名單面 designated(r2 MAJOR-2,本 run 重測帳):jr4w 正規化等價面+[17] jr4w NFKC 慣用形
|
|
1409
|
+
// 帳面兩面真翻——摘除 unwrap 入口 normalize 時 normalized 形失去收容(dual4 等值面不翻=wrap 端
|
|
1410
|
+
// normalize 仍承載,P1 針只咬 unwrap 面——P5 已咬 wrap 面;兩針分工面帳)。
|
|
1411
|
+
const p1eqRed = g.out.split('\n').some((l: string) => l.includes('jr4w 正規化等價') && l.trimStart().startsWith('✗'));
|
|
1412
|
+
const p1katRed = g.out.split('\n').some((l: string) => l.includes('NFKC-effective 全形 unwrap = 原始 noteKey(v3 契約收容自己——全形 wrap 的 KAT 慣用形)') && l.trimStart().startsWith('✗'));
|
|
1413
|
+
const red = mut.applied && g.rc === 1 && nfail(g.out) >= 2 && p1eqRed && p1katRed;
|
|
1414
|
+
await A('[16] P1 摘 jr4w unwrap 入口 normalize → 正規化等價面+KAT 慣用形帳面真翻(nfail=4 帳——非錨影)', red, 'nfail=' + nfail(g.out));
|
|
1415
|
+
mut.restore();
|
|
1416
|
+
const g2 = runGate16();
|
|
1417
|
+
fs16!.rmSync!(tree16, { recursive: true, force: true });
|
|
1418
|
+
await A('[16] P1 還原回綠(出生即棄樹上還原;P2 共樹後不可達案由出生即棄承載)', g2.rc === 0, String(g2.rc));
|
|
1419
|
+
}
|
|
1420
|
+
// P2 本體夾帶 lowercase → NFKC-effective 行為翻面集(r2 CRITICAL-1 重測:nfail=11 帳——非 crash 形)
|
|
1421
|
+
{
|
|
1422
|
+
mkTree();
|
|
1423
|
+
const mut = poison16('src/client/note-crypto.ts',
|
|
1424
|
+
"export function normalizePassphrase(passphrase: string): string {\n return passphrase.normalize('NFKC');\n}",
|
|
1425
|
+
"export function normalizePassphrase(passphrase: string): string {\n return passphrase.normalize('NFKC').toLowerCase();\n}");
|
|
1426
|
+
const g = runGate16();
|
|
1427
|
+
// P2 名單面 designated(r2 CRITICAL-1 重測帳):NFKC-effective 帳面翻面集(分離向量二+並存不混+jr4w
|
|
1428
|
+
// raw 大小寫不折疊+dual4 case/space+[15] NFKC 契約向量+ph1v3 大小寫恆異+[17] 大寫兩形 null 四面+本體
|
|
1429
|
+
// 單行形錨)=11 面帳;同 P3/P4/P5 毒形零匹配(全格向量化——毒化矩陣單點真值面)。
|
|
1430
|
+
const p2normRed = g.out.split('\n').some((l: string) => l.includes('分離向量二') && l.trimStart().startsWith('✗'));
|
|
1431
|
+
await A('[16] P2 本體夾帶 lowercase → NFKC-effective 帳面翻面集(nfail>=11 帳+分離向量二名單面)', mut.applied && g.rc === 1 && nfail(g.out) >= 11 && p2normRed, 'rc=' + g.rc + ' nfail=' + nfail(g.out));
|
|
1432
|
+
fs16!.rmSync!(tree16, { recursive: true, force: true });
|
|
1433
|
+
}
|
|
1434
|
+
// P3 摘 derivePh1ArgonV3 入口 normalize → 行為翻 2 面帳(本 run 毒化重測:ph1v3 NFKC 等價面+[15] 收口形
|
|
1435
|
+
// 錨跟隨)——名單面 designated=ph1v3 NFKC 等價面(案內檢);P3 毒形=derivePh1Argon v2 名同型不匹配(全格向量化)。
|
|
1436
|
+
{
|
|
1437
|
+
mkTree();
|
|
1438
|
+
const mut = poison16('src/client/argon2.ts',
|
|
1439
|
+
" deriveInput(passphrase),\n saltArg ?? new TextEncoder().encode(PH1_V3_SALT),",
|
|
1440
|
+
" passphrase,\n saltArg ?? new TextEncoder().encode(PH1_V3_SALT),");
|
|
1441
|
+
const g = runGate16();
|
|
1442
|
+
// P3 名單面 designated(r2 MAJOR-2):ph1v3 NFKC 等價面(全形 vs ASCII 同 ph2)真翻。
|
|
1443
|
+
const p3red = g.out.split('\n').some((l: string) => l.includes('ph1v3 NFKC 等價') && l.trimStart().startsWith('✗'));
|
|
1444
|
+
await A('[16] P3 摘 derivePh1ArgonV3 入口 normalize → ph1v3 NFKC 等價面真翻(名單級承載)', mut.applied && g.rc === 1 && nfail(g.out) >= 1 && p3red, 'nfail=' + nfail(g.out));
|
|
1445
|
+
fs16!.rmSync!(tree16, { recursive: true, force: true });
|
|
1446
|
+
}
|
|
1447
|
+
// P4 摘 jr4w wrap 入口 normalize → 行為翻 4 面帳(本 run 毒化重測:NFD wrap→NFC unwrap 面+全形/ASCII 同
|
|
1448
|
+
// 密語面+[15] raw 守衛錨+[15] 收口形錨)——名單面 designated=全形/ASCII 同密語面(案內檢)。
|
|
1449
|
+
{
|
|
1450
|
+
mkTree();
|
|
1451
|
+
const mut = poison16('src/client/argon2.ts',
|
|
1452
|
+
" const kek = await deriveKekArgon(deriveInput(passphrase), salt);\n const wrapped = await sealNoteKey(cfg.wrap4, noteKey, kek, 'notekey');",
|
|
1453
|
+
" const kek = await deriveKekArgon(passphrase, salt);\n const wrapped = await sealNoteKey(cfg.wrap4, noteKey, kek, 'notekey');");
|
|
1454
|
+
const g = runGate16();
|
|
1455
|
+
// P4 名單面 designated(r2 MAJOR-2):jr4w 全形/ASCII 同密語面(全形 wrap 的 NFKC 收容)真翻。
|
|
1456
|
+
const p4red = g.out.split('\n').some((l: string) => l.includes('jr4w 全形/ASCII 同密語') && l.trimStart().startsWith('✗'));
|
|
1457
|
+
await A('[16] P4 摘 jr4w wrap 入口 normalize → 全形/ASCII 同密語面真翻(名單級承載)', mut.applied && g.rc === 1 && nfail(g.out) >= 1 && p4red, 'nfail=' + nfail(g.out));
|
|
1458
|
+
fs16!.rmSync!(tree16, { recursive: true, force: true });
|
|
1459
|
+
}
|
|
1460
|
+
// P5 摘 jr4d wrap 入口 normalize → 行為翻 5 面帳(本 run 毒化重測:dual4 等值面+PIN 兩面+[15] 收口形
|
|
1461
|
+
// 錨+[15] HKDF info 錨)——名單面 designated=dual4 等值面(dual4red 檈);PIN 兩面翻=毒化 wrap 端
|
|
1462
|
+
// normalize 摘除的帳面(wrap 端 NFKC-effective 面倒灌 PIN 段)。
|
|
1463
|
+
{
|
|
1464
|
+
mkTree();
|
|
1465
|
+
const mut = poison16('src/client/argon2.ts',
|
|
1466
|
+
" const kek2 = await deriveKek2Argon(cfg, deriveInput(passphrase), pinNorm, salt1, pinSalt, 'journal-kek2-v1:' + cfg.wrapDual4);\n const iv = crypto.getRandomValues(new Uint8Array(DUAL_IV_LEN));\n const ct = new Uint8Array(await crypto.subtle.encrypt(",
|
|
1467
|
+
" const kek2 = await deriveKek2Argon(cfg, passphrase, pinNorm, salt1, pinSalt, 'journal-kek2-v1:' + cfg.wrapDual4);\n const iv = crypto.getRandomValues(new Uint8Array(DUAL_IV_LEN));\n const ct = new Uint8Array(await crypto.subtle.encrypt(");
|
|
1468
|
+
const g = runGate16();
|
|
1469
|
+
const dual4red = g.out.split('\n').some((l: string) => l.includes('dual4 unwrap 等值') && l.trimStart().startsWith('✗'));
|
|
1470
|
+
await A('[16] P5 摘 jr4d wrap 入口 normalize → dual4 self-proving 面翻(帳面 ✗ 計數免疫——✓-line 內嵌 ✗ 標記不入計)', mut.applied && g.rc === 1 && dual4red, 'rc=' + g.rc);
|
|
1471
|
+
fs16!.rmSync!(tree16, { recursive: true, force: true });
|
|
1472
|
+
}
|
|
1473
|
+
// O3 normalizePin 本體摘 trim/lowercase → 行為翻 ≥9 面(外部探針帳實值)
|
|
1474
|
+
{
|
|
1475
|
+
mkTree();
|
|
1476
|
+
const mut = poison16('src/client/note-crypto.ts',
|
|
1477
|
+
' return pin.normalize(\'NFKC\').trim().toLowerCase();',
|
|
1478
|
+
' return pin.toLowerCase();');
|
|
1479
|
+
const g = runGate16();
|
|
1480
|
+
await A('[16] O3 normalizePin 本體摘 trim/lowercase → 行為翻 9 面(PIN 契約毒化承載)', mut.applied && g.rc === 1 && nfail(g.out) >= 9, 'nfail=' + nfail(g.out) + ' want >=9');
|
|
1481
|
+
fs16!.rmSync!(tree16, { recursive: true, force: true });
|
|
1482
|
+
}
|
|
1483
|
+
// O2 deriveKek2 pass 段接 normalize(禁手形)→ 真翻面案(r2 MAJOR-2 名單級承載;本 run 重測帳=
|
|
1484
|
+
// [14] 輸入順序錨翻+jr2w 凍結 normalized 面翻,恰 2 面——兩面各自 designated,帳寫在案斷言內)。
|
|
1485
|
+
{
|
|
1486
|
+
mkTree();
|
|
1487
|
+
const mut = poison16('src/client/note-crypto.ts',
|
|
1488
|
+
" const [passBits, pinBits] = await Promise.all([\n derivePbkdf2Bits(passphrase, salt1, PBKDF2_ITERATIONS),",
|
|
1489
|
+
" const [passBits, pinBits] = await Promise.all([\n derivePbkdf2Bits(normalizePassphrase(passphrase), salt1, PBKDF2_ITERATIONS),");
|
|
1490
|
+
const g = runGate16();
|
|
1491
|
+
// O2 行為翻 2 面帳(本 run 毒化重測):[14] 輸入順序錨(毒形不匹配 raw 收口形恰-1 形)+jr2w 凍結
|
|
1492
|
+
// normalized 解翻面(毒化 wrap 也 normalize → ascii 形同 KEK)——名單級承載=[14] 輸入順序錨面(案內檢);
|
|
1493
|
+
// nfail 精確 2 帳=P5/P1 形的錨翻面恰補位(姊妹卡新增 PIN/dual 覆蓋時 >= 帽韌性——MINOR-3)。
|
|
1494
|
+
const o2rawRed = g.out.split('\n').some((l: string) => l.includes('[14] deriveKek2 輸入順序錨') && l.trimStart().startsWith('✗'));
|
|
1495
|
+
await A('[16] O2 deriveKek2 pass 段接 normalize(禁手形)→ raw 收口形錨翻+jr2w 凍結面翻(nfail>=2;名單級承載)',
|
|
1496
|
+
mut.applied && g.rc === 1 && nfail(g.out) >= 2 && o2rawRed, 'rc=' + g.rc + ' nfail=' + nfail(g.out) + ' fails=' + realFails(g.out).map(s => s.slice(0, 44)).join(';;'));
|
|
1497
|
+
fs16!.rmSync!(tree16, { recursive: true, force: true });
|
|
1498
|
+
}
|
|
1499
|
+
// O1 jr3w.wrap 入口接 normalize(禁手形)→ 行為翻 4 面帳(本 run 毒化重測:jr3w 全形照解面翻+normalized
|
|
1500
|
+
// 恆拒面翻+[15] raw 守衛錨+[15] 收口形錨)——行為 designated=凍結兩面(raw 入口契約復活即翻)。
|
|
1501
|
+
{
|
|
1502
|
+
mkTree();
|
|
1503
|
+
const mut = poison16('src/client/argon2.ts',
|
|
1504
|
+
" const kek = await deriveKekArgon(passphrase, salt);\n const wrapped = await sealNoteKey(cfg.wrap3, noteKey, kek, 'notekey');",
|
|
1505
|
+
" const kek = await deriveKekArgon(deriveInput(passphrase), salt);\n const wrapped = await sealNoteKey(cfg.wrap3, noteKey, kek, 'notekey');");
|
|
1506
|
+
const g = runGate16();
|
|
1507
|
+
// O1 名單面 designated(r2 MAJOR-2,本 run 重測帳):jr3w 凍結兩面(全形照解面翻+normalized 恆拒
|
|
1508
|
+
// 面翻)=raw 入口契約復活的行為齒;4 面帳=兩行為面+兩 [15] 錨跟隨。
|
|
1509
|
+
const o1fwRed = g.out.split('\n').some((l: string) => l.includes('jr3w 全形 wrap 照解') && l.trimStart().startsWith('✗'));
|
|
1510
|
+
const o1normRed = g.out.split('\n').some((l: string) => l.includes('jr3w 拒收 normalized 慣用形') && l.trimStart().startsWith('✗'));
|
|
1511
|
+
await A('[16] O1 jr3w.wrap 入口接 normalize(禁手形)→ 凍結兩面真翻(nfail=4 帳;名單級承載)',
|
|
1512
|
+
mut.applied && g.rc === 1 && nfail(g.out) >= 2 && o1fwRed && o1normRed, 'rc=' + g.rc + ' nfail=' + nfail(g.out) + ' fails=' + realFails(g.out).map(s => s.slice(0, 44)).join(';;'));
|
|
1513
|
+
fs16!.rmSync!(tree16, { recursive: true, force: true });
|
|
1514
|
+
}
|
|
1515
|
+
// O4 unwrapNoteKeyDual4 入口接 raw(v3 反拉形)→ 「毒態」行為面翻轉樣本(MAJOR-2 面修正後真有齒)
|
|
1516
|
+
{
|
|
1517
|
+
mkTree();
|
|
1518
|
+
const mut = poison16('src/client/argon2.ts',
|
|
1519
|
+
" const kek2 = await deriveKek2Argon(cfg, deriveInput(passphrase), pinNorm, salt1, pinSalt, 'journal-kek2-v1:' + cfg.wrapDual4);\n const rawHex = await decryptWithKey(kek2, ivPrefixedCt, 'notekey2');",
|
|
1520
|
+
" const kek2 = await deriveKek2Argon(cfg, passphrase, pinNorm, salt1, pinSalt, 'journal-kek2-v1:' + cfg.wrapDual4);\n const rawHex = await decryptWithKey(kek2, ivPrefixedCt, 'notekey2');");
|
|
1521
|
+
const g = runGate16();
|
|
1522
|
+
// O4 名單面 designated(r2 MAJOR-2,本 run 重測帳):dual4 pass 段 NFKC 反向等價面(MAJOR-1 修正後
|
|
1523
|
+
// unwrap 餵 passD4 的真反向)+[17] jr4d NFKC 慣用形帳面——反拉 normalize 時 unwrap 餵 normalized 形
|
|
1524
|
+
// 失去收容 = 該面 null = 行為齒(非只 rc 計數);行為翻 4 面帳=反向等價面+兩 [15] 錨+[17] 帳面。
|
|
1525
|
+
const o4red = g.out.split('\n').some((l: string) => l.includes('dual4 pass 段 NFKC 反向等價') && l.trimStart().startsWith('✗'));
|
|
1526
|
+
const o4katRed = g.out.split('\n').some((l: string) => l.includes('NFKC-effective 全形 unwrap = 原始 noteKey(v3 契約收容自己;info=jr4d 域)') && l.trimStart().startsWith('✗'));
|
|
1527
|
+
await A('[16] O4 unwrapNoteKeyDual4 入口接 raw(反拉禁手形)→ NFKC 反向等價面+KAT 慣用形帳面真翻(nfail=4 帳)',
|
|
1528
|
+
mut.applied && g.rc === 1 && nfail(g.out) >= 2 && o4red && o4katRed, 'rc=' + g.rc + ' nfail=' + nfail(g.out) + ' fails=' + realFails(g.out).map(s => s.slice(0, 44)).join(';;'));
|
|
1529
|
+
fs16!.rmSync!(tree16, { recursive: true, force: true });
|
|
1530
|
+
}
|
|
1531
|
+
// O1/O2/O4 的真防線=源碼計數錨(normalize 收口形恰 3/raw 形恰 2——毒化形即計數變異,本 repo
|
|
1532
|
+
// 樹直接斷言=毒化態「本節自身」翻而不是只翻拷貝樹):
|
|
1533
|
+
{
|
|
1534
|
+
const argonSrcNow = await srcOf('../src/client/argon2.ts');
|
|
1535
|
+
const ncSrcNow = await srcOf('../src/client/note-crypto.ts');
|
|
1536
|
+
// 與 [15] 同形計數錨=「複寫防單點誤删」(NIT-3:[16] 內層保留 O-案源碼錨——[15] 錨被未來誤删時本段仍咬)。
|
|
1537
|
+
await A('[16] O1-src 計數錨([15] 同形複寫——防單點誤删):argon2 normalize 收口入口形恰 3(禁手形任何復活=計數變異即 RED)',
|
|
1538
|
+
argonSrcNow.split('deriveKekArgon(deriveInput(passphrase)').length === 3
|
|
1539
|
+
&& argonSrcNow.split('deriveArgon2id(\n deriveInput(passphrase)').length === 2
|
|
1540
|
+
&& argonSrcNow.split('deriveKek2Argon(cfg, deriveInput(passphrase)').length === 3);
|
|
1541
|
+
await A('[16] O2-src 計數錨:note-crypto PBKDF2 家族 pass 段 raw 形恰 1 處(deriveKek2;接 normalize 即 RED——raw 錨 length 2)',
|
|
1542
|
+
ncSrcNow.split('derivePbkdf2Bits(passphrase, salt1, PBKDF2_ITERATIONS)').length === 2);
|
|
1543
|
+
await A('[16] O4-src 計數錨([15] 同形複寫):jr4d unwrap 入口 normalize 形恰 2(wrap/unwrap 對稱;摘除或反拉即變異)',
|
|
1544
|
+
argonSrcNow.split('deriveKek2Argon(cfg, deriveInput(passphrase), pinNorm').length === 3);
|
|
1545
|
+
}
|
|
1546
|
+
// 收尾:毒化樹清理自證(MINOR-2 修正:封閉集——只核對本 run 喚出的 tree16 路徑;跨執行/
|
|
1547
|
+
// 並行/host 殘留零耦合——其他 core-poison-* 不入本判準);每棵出生即棄 rmSync 強制在場。
|
|
1548
|
+
{
|
|
1549
|
+
const exists16 = fs16!.existsSync!;
|
|
1550
|
+
let residual = 0;
|
|
1551
|
+
for (const t of treePaths16) if (exists16(t)) residual++;
|
|
1552
|
+
await A('[16] 毒化樹零殘留(本 run 喚出 ' + treePaths16.length + ' 棵全清——封閉集判準)',
|
|
1553
|
+
treePaths16.length >= 10 && residual === 0, 'trees=' + treePaths16.length + ' residual=' + residual);
|
|
1554
|
+
}
|
|
1555
|
+
}
|
|
1556
|
+
}
|
|
1557
|
+
}
|
|
1558
|
+
|
|
1559
|
+
// ── 17. KAT 凍結向量:HKDF info 域世代分離(CRITICAL-1 修正承載面) ──────────────
|
|
1560
|
+
//
|
|
1561
|
+
// 凍結常數計算式構造(t_7361b68c KAT14 母型——raw 'c3'.repeat(32) 寫 c3x32 帳面):
|
|
1562
|
+
// jr3w./jr3d.=舊 raw 族凍結 blob(全形字面 RAW 派生承載;normalized 慣用形恆拒);
|
|
1563
|
+
// jr4w./jr4d.=v3 族凍結 blob(KEK 輸入契約=normalizePassphrase:normalized 慣用形可解、
|
|
1564
|
+
// raw 全形形恆拒)。KDF 派生輸入正規化收口(v3):入口 deriveInput(pass)=normalizePassphrase
|
|
1565
|
+
// 本體同語意(NFKC-only:不 trim、不分大小寫——大小寫摺疊禁絕,大小寫差=不同 KEK/ph2);
|
|
1566
|
+
// 正規化禁進共用派生本體(舊家族 raw 契約永不變)。
|
|
1567
|
+
// jr4d. 的 KEK2 HKDF info 域='journal-kek2-v1:' + cfg.wrapDual4(呼叫端帶入)——
|
|
1568
|
+
// NFKC-effective 密語下 jr3d/jr4d 兩入參數同輸入(bits 段同值),域分離由 info 承載:
|
|
1569
|
+
// 兩 blob 互解 null(下方案)+各自 ASCII/全形 unwrap=raw=KDF 域世代的真行為承載。
|
|
1570
|
+
// 凍結值:fixed rawKey 'c3'×32;jr3w/jr3d=全形 RAW 樣本 blob(舊族 raw 承載);jr4w/jr4d=
|
|
1571
|
+
// 全形 NFKC 載體 blob(v3 契約收容態);pin '2580ab';salt/pinSalt 逐值自 baked 資料(外探針實測入冊)。
|
|
1572
|
+
// 大小寫摺疊禁絕帳:ASCII 大寫與全形大寫兩形 → null(全形面上限非 NFKC-only 帳面缺口——大小寫
|
|
1573
|
+
// 摺疊是另一禁絕面,兩形各自承載)。凍結 blob 逐字面恆定(非動態重演)——驗證期重放=以 unwrap 端
|
|
1574
|
+
// KEK 派生(凍結參數)比對 blob 內 baked 密文;「重放」語意=派生確定性重驗,blob 本體恆凍結。
|
|
1575
|
+
const KAT17_PASS_NORM = pass4; // v3 段 ASCII 慣用形(NFKC-effective:normalizePassphrase(pass4)==pass4)
|
|
1576
|
+
const KAT17 = {
|
|
1577
|
+
raw: 'c3'.repeat(32),
|
|
1578
|
+
// 舊族全形凍結樣本由 ASCII 源計算式派生(同 KAT14 母型「凍結常數計算式構造」——
|
|
1579
|
+
// 帶 confusable 字面直接入冊曾實證漂移:gate literal 與真全形一位點碼差=KAT 靜默拒解)。
|
|
1580
|
+
pass3Raw: Array.from('PASS12x').map((c) => String.fromCodePoint(c.codePointAt(0)! + 0xfee0)).join(''),
|
|
1581
|
+
pass4fw: Array.from(pass4).map((c) => String.fromCodePoint(c.codePointAt(0)! + 0xfee0)).join(''),
|
|
1582
|
+
passDbRaw: Array.from(passD).map((c) => String.fromCodePoint(c.codePointAt(0)! + 0xfee0)).join(''),
|
|
1583
|
+
jr3w: 'jr3w.BeQ8F6s3DdJWY853l2jRv5dHtVEMd+tjJ3XuIisu2ymuK7IInuqIvxtwPD3WzhTkEtCRr+h7KOfWo5FM6ELGjPxwxHAp5LtM5zcEg8hmCqH3dMX9iHF7IOQl3Ak=',
|
|
1584
|
+
s3: 'f6a511fb29b131869d282d89f9eaa90a',
|
|
1585
|
+
jr3d: 'jr3d.nOB7P29euSEa+LM8qMSTRe6r32TbBX7nS2R/WyRJ7N1NO+mqXuW5u9HLiLzYObKHq3NURwiEaxD9qwli646qDoG4/5wlHD7UE9nBT485oUeEx/vYIkmOu4muJQ42avl8zX9M1PSgaP+Bk+R9',
|
|
1586
|
+
sd3: 'a06bd4b1f293b8c8ebff4b6a02f13a34',
|
|
1587
|
+
jr4w: 'jr4w.x6cZmk7UVUAf8abE3mLps22+BLTdWGTDOZpTs4DqHFUDUcz5p5VpdIqYub/9r+sE2/hYE1fPb68ABSCdDeEvJVXlGOsbDr9L//xsdFcZZYlRtrMO3APSwJQBrv0=',
|
|
1588
|
+
s4: '31a8d4ae616ccc0a6def28a9b0db4860',
|
|
1589
|
+
jr4d: 'jr4d.Dz8wgY5Lphif69Fh5fE+54J4YBlUJgfVLHBeM7RGr3HUmplys5FpcIK/AZT8zstvsUSpox2mwnpo4eQA9eqtbRK3kTowaWRyAEjTfx4kJS9eMoPQEWsr+GRRYgtdINlgAoVJxyi/NaCNqhDR',
|
|
1590
|
+
sd4: 'd6ebccb6c9bb3a0d13905183d2b2977d',
|
|
1591
|
+
pin: pinD,
|
|
1592
|
+
};
|
|
1593
|
+
{
|
|
1594
|
+
const katRawUnlock = async (key: CryptoKey | null): Promise<boolean> =>
|
|
1595
|
+
key !== null && (await hex(new Uint8Array(await crypto.subtle.exportKey('raw', key)))) === KAT17.raw;
|
|
1596
|
+
console.log('\n[17] KAT 凍結向量:HKDF info 域世代分離+v3/raw 入口契約(jr3w./jr3d./jr4w./jr4d. 四 blob)');
|
|
1597
|
+
await A('[17] jr3w 凍結 blob:全形 RAW unwrap = 原始 noteKey(舊家族 raw 契約——KAT 承載)',
|
|
1598
|
+
katRawUnlock(await unwrapNoteKey3(TACET3, KAT17.jr3w, KAT17.pass3Raw, KAT17.s3)));
|
|
1599
|
+
await A('[17] jr3w 凍結 blob:normalized 慣用形 → null(normalized 面恆拒——raw 族零正規化承載)',
|
|
1600
|
+
(await unwrapNoteKey3(TACET3, KAT17.jr3w, KAT17_PASS_NORM, KAT17.s3)) === null);
|
|
1601
|
+
await A('[17] jr3d 凍結 blob:全形 RAW unwrap = 原始 noteKey(舊家族 raw 契約——KAT 承載;info=jr3d 域)',
|
|
1602
|
+
katRawUnlock(await unwrapNoteKeyDual3(TACET3, KAT17.jr3d, KAT17.passDbRaw, KAT17.pin, KAT17.sd3)));
|
|
1603
|
+
await A('[17] jr3d 凍結 blob:normalized 慣用形 → null(normalized 面恆拒——鹽內嵌 raw 契約;info=jr3d 域)',
|
|
1604
|
+
(await unwrapNoteKeyDual3(TACET3, KAT17.jr3d, passD, KAT17.pin, KAT17.sd3)) === null);
|
|
1605
|
+
await A('[17] jr4w 凍結 blob:normalized 慣用形 unwrap = 原始 noteKey(v3 契約——KAT 承載)',
|
|
1606
|
+
katRawUnlock(await unwrapNoteKey4(TACET4, KAT17.jr4w, KAT17_PASS_NORM, KAT17.s4)));
|
|
1607
|
+
await A('[17] jr4w 凍結 blob:NFKC-effective 全形 unwrap = 原始 noteKey(v3 契約收容自己——全形 wrap 的 KAT 慣用形)',
|
|
1608
|
+
katRawUnlock(await unwrapNoteKey4(TACET4, KAT17.jr4w, KAT17.pass4fw, KAT17.s4)));
|
|
1609
|
+
await A('[17] jr4w 凍結 blob:fw 大寫形 → null(大小寫差=不同 KEK——全形載體面上的摺疊禁絕)',
|
|
1610
|
+
(await unwrapNoteKey4(TACET4, KAT17.jr4w, KAT17.pass4fw.toUpperCase(), KAT17.s4)) === null);
|
|
1611
|
+
await A('[17] jr4w 凍結 blob:ASCII 大寫形 → null(大小寫差=不同 KEK,v3 契約)',
|
|
1612
|
+
(await unwrapNoteKey4(TACET4, KAT17.jr4w, KAT17_PASS_NORM.toUpperCase(), KAT17.s4)) === null);
|
|
1613
|
+
await A('[17] jr4d 凍結 blob:normalized 慣用形 unwrap = 原始 noteKey(v3 契約;KEK2 info=jr4d 自有域)',
|
|
1614
|
+
katRawUnlock(await unwrapNoteKeyDual4(TACET4, KAT17.jr4d, passD, KAT17.pin, KAT17.sd4)));
|
|
1615
|
+
await A('[17] jr4d 凍結 blob:NFKC-effective 全形 unwrap = 原始 noteKey(v3 契約收容自己;info=jr4d 域)',
|
|
1616
|
+
katRawUnlock(await unwrapNoteKeyDual4(TACET4, KAT17.jr4d, KAT17.passDbRaw, KAT17.pin, KAT17.sd4)));
|
|
1617
|
+
await A('[17] jr4d 凍結 blob:fw 大寫形 → null(大小寫差=不同 KEK——全形載體面上的摺疊禁絕)',
|
|
1618
|
+
(await unwrapNoteKeyDual4(TACET4, KAT17.jr4d, KAT17.passDbRaw.toUpperCase(), KAT17.pin, KAT17.sd4)) === null);
|
|
1619
|
+
await A('[17] jr4d 凍結 blob:ASCII 大寫形 → null(大小寫差=不同 KEK,v3 契約)',
|
|
1620
|
+
(await unwrapNoteKeyDual4(TACET4, KAT17.jr4d, passD.toUpperCase(), KAT17.pin, KAT17.sd4)) === null);
|
|
1621
|
+
await A('[17] family isolation:jr4d blob 餵 jr3d 路徑 → null(HKDF info 域對不上——域世代分離)',
|
|
1622
|
+
(await unwrapNoteKeyDual3(TACET3, KAT17.jr4d, passD, KAT17.pin, KAT17.sd4)) === null);
|
|
1623
|
+
await A('[17] family isolation:jr3d blob 餵 jr4d 路徑 → null(KEK2 info 域分離對稱面)',
|
|
1624
|
+
(await unwrapNoteKeyDual4(TACET4, KAT17.jr3d, passD, KAT17.pin, KAT17.sd3)) === null);
|
|
1625
|
+
}
|
|
1626
|
+
|
|
1627
|
+
// ── 18. 本機包裹專用前綴(v0.2.0 批卡③:wrapLocal opt-in+讀舊寫新自癒;jr1l.) ──
|
|
1628
|
+
//
|
|
1629
|
+
// 舊實作借用 cfg.wrap(jr1w. passphrase 包裹前綴)寫本機包裹=「一個前綴一份契約」的第二
|
|
1630
|
+
// 違例(wrappedRec 專用前綴歸卡①批面)。收口三面:
|
|
1631
|
+
// ①寫面專用前綴:storeLocalWrap 寫入恆 cfg.wrapLocal(jr1l.);未配置=退場無寫入
|
|
1632
|
+
// (cipherLocal opt-in 母型——未配置面零寫入零拋)。
|
|
1633
|
+
// ②讀舊寫新自癒:loadLocalWrap 先試新前綴(本體嚴格面);命中舊形(借用期 jr1w. blob)
|
|
1634
|
+
// 回落解密後重包 cfg.wrapLocal 回寫(自癒恰一次);解不回(損壞/他機搬來)誠實 null
|
|
1635
|
+
// 不硬遷移。
|
|
1636
|
+
// ③未配置態讀取面:照走舊形 cfg.wrap(行為不變),且不自癒重寫(heal 綁在新前綴面)。
|
|
1637
|
+
// KEK 不動=deriveGuestKey(identity 派生、passphrase-free——session 期免重打密語的機制
|
|
1638
|
+
// 原樣);本卡只界「前綴」面(payload 布局的 KDF/prefix 契約由前綴界定——帶內版本化母型)。
|
|
1639
|
+
console.log('\n[18] 本機包裹專用前綴(wrapLocal=jr1l. opt-in+讀舊寫新自癒)');
|
|
1640
|
+
{
|
|
1641
|
+
// section 專用 Map store:node 端 localStorage 死位(keys.ts try/catch 恆 null)——stored 面
|
|
1642
|
+
// 行為斷言要可觀察可重放,結構 KeyStore 注入(同 cfg 注入母型;TACET 基座 fixture 不動)。
|
|
1643
|
+
const mapS18 = (() => { const m = new Map<string, string>(); return {
|
|
1644
|
+
get: (k: string): string | null => (m.has(k) ? m.get(k)! : null),
|
|
1645
|
+
set: (k: string, v: string): void => { m.set(k, v); },
|
|
1646
|
+
remove: (k: string): void => { m.delete(k); },
|
|
1647
|
+
noteKeyWrap: (soul: string): string => 't18_notekey:' + soul,
|
|
1648
|
+
}; })();
|
|
1649
|
+
const TACET_L18 = { ...TACET, wrapLocal: 'jr1l.', store: mapS18 };
|
|
1650
|
+
const { wrapLocal: _omit18, ...restN18 } = TACET_L18;
|
|
1651
|
+
const TACET18_NOLOCAL: NoteCryptoConfig = restN18; // wrap 帶、wrapLocal 無(行為不變態)
|
|
1652
|
+
const { wrap: _omitw18, ...restW18 } = TACET_L18;
|
|
1653
|
+
const TACET18_NOWRAP = restW18 as unknown as NoteCryptoConfig; // wrapLocal 帶、wrap 無(回落守衛態)
|
|
1654
|
+
|
|
1655
|
+
const RAW18 = hex(new Uint8Array(await crypto.subtle.exportKey('raw', noteKey)));
|
|
1656
|
+
const eqNoteKey18 = async (k: CryptoKey | null): Promise<boolean> =>
|
|
1657
|
+
k !== null && hex(new Uint8Array(await crypto.subtle.exportKey('raw', k))) === RAW18;
|
|
1658
|
+
const storedOf18 = (c: NoteCryptoConfig, id: string): string | null => c.store.get(c.store.noteKeyWrap(id));
|
|
1659
|
+
const guestOf18 = async (c: NoteCryptoConfig, id: string): Promise<CryptoKey> => deriveGuestKey(c, id);
|
|
1660
|
+
|
|
1661
|
+
// 舊形向量=借用期工件真本體:sealNoteKey(cfg.wrap, key, guest, 'notekey-local') 形直呼
|
|
1662
|
+
//(PRE 樹一對照實測:舊碼 storeLocalWrap 產出即此形——92B 嚴格面(hex64 字串 payload))。
|
|
1663
|
+
const guestL18 = await guestOf18(TACET_L18, 'acct-l2');
|
|
1664
|
+
const legacyL18 = await sealNoteKey(TACET.wrap, noteKey, guestL18, 'notekey-local');
|
|
1665
|
+
await A('[18] 舊形向量自洽錨:回落前直解 jr1w. blob = 原 noteKey(樣本可重放)',
|
|
1666
|
+
eqNoteKey18(await openNoteKey(legacyL18, guestL18, 'notekey-local', TACET.wrap)));
|
|
1667
|
+
|
|
1668
|
+
// ── P1 寫面:寫入恆新前綴;未配置退場 ──
|
|
1669
|
+
await storeLocalWrap(TACET_L18, 'acct-l2', noteKey);
|
|
1670
|
+
const freshL18 = storedOf18(TACET_L18, 'acct-l2');
|
|
1671
|
+
await A('[18] 寫前綴 jr1l.(store 契約——借用 cfg.wrap 舊形=反)', !!freshL18 && freshL18!.startsWith('jr1l.'));
|
|
1672
|
+
await A('[18] 寫入 payload unwrap = 原 noteKey(KEK=deriveGuestKey 原樣——本體 92B 嚴格面)',
|
|
1673
|
+
freshL18 !== null && eqNoteKey18(await openNoteKey(freshL18!, guestL18, 'notekey-local', TACET_L18.wrapLocal!)));
|
|
1674
|
+
await A('[18] 未配置 wrapLocal store 退場(noop——stored 殘留零,opt-in 母型)',
|
|
1675
|
+
(async () => { await storeLocalWrap(TACET18_NOLOCAL, 'acct-l1', noteKey); return storedOf18(TACET18_NOLOCAL, 'acct-l1') === null; })());
|
|
1676
|
+
|
|
1677
|
+
// ── P1b 讀面通路(loadLocalWrap 唯一消費點:encryptNote/decryptNote 的 held 設值面) ──
|
|
1678
|
+
const heldB18 = makeHeldKey(); heldB18.set(noteKey);
|
|
1679
|
+
const ctB18 = await encryptNote(TACET_L18, heldB18, 'plain-read-18', 'aad-18b', { current: () => 'acct-l3' });
|
|
1680
|
+
await A('[18] 樣本自洽:bound 密文前綴 jr1b.(held 預置面)', ctB18.startsWith('jr1b.'));
|
|
1681
|
+
await storeLocalWrap(TACET_L18, 'acct-l3r', noteKey);
|
|
1682
|
+
await A('[18] 空-held decrypt 經本機包裹通路 = 原文(read 通路行為面)',
|
|
1683
|
+
(await decryptNote(TACET_L18, makeHeldKey(), ctB18, 'aad-18b', { current: () => 'acct-l3r' })) === 'plain-read-18');
|
|
1684
|
+
await A('[18] 該 identity 無 stored → null(通路不無中生有)',
|
|
1685
|
+
(await decryptNote(TACET_L18, makeHeldKey(), ctB18, 'aad-18b', { current: () => 'acct-l3x' })) === null);
|
|
1686
|
+
|
|
1687
|
+
// ── P2 舊形回落+自癒回寫(讀舊寫新) ──
|
|
1688
|
+
TACET_L18.store.set(TACET_L18.store.noteKeyWrap('acct-l2'), legacyL18);
|
|
1689
|
+
await A('[18] 舊形(借用期 jr1w. blob)讀回 = 解密成立(回落面行為)',
|
|
1690
|
+
(await decryptNote(TACET_L18, makeHeldKey(), ctB18, 'aad-18b', { current: () => 'acct-l2' })) === 'plain-read-18');
|
|
1691
|
+
const healedL18 = storedOf18(TACET_L18, 'acct-l2');
|
|
1692
|
+
await A('[18] 自癒回寫恆新前綴(stored 舊形摘除——v1 借用面行為翻轉)', !!healedL18 && healedL18!.startsWith(TACET_L18.wrapLocal!));
|
|
1693
|
+
await A('[18] 自癒 blob unwrap 等值(同一 guest KEK 重包)',
|
|
1694
|
+
healedL18 !== null && eqNoteKey18(await openNoteKey(healedL18!, guestL18, 'notekey-local', TACET_L18.wrapLocal!)));
|
|
1695
|
+
await A('[18] 自癒 blob 舊形恆拒(新形不落舊前綴——家族隔離)',
|
|
1696
|
+
(await openNoteKey(healedL18!, guestL18, 'notekey-local', TACET.wrap)) === null);
|
|
1697
|
+
const beforeRe18 = storedOf18(TACET_L18, 'acct-l2');
|
|
1698
|
+
await A('[18] 自癒後二次讀回(fresh face 通路承載)',
|
|
1699
|
+
(await decryptNote(TACET_L18, makeHeldKey(), ctB18, 'aad-18b', { current: () => 'acct-l2' })) === 'plain-read-18');
|
|
1700
|
+
await A('[18] 二次讀不自癒二次(byte-identical——自癒恰一次)', storedOf18(TACET_L18, 'acct-l2') === beforeRe18);
|
|
1701
|
+
|
|
1702
|
+
// ── P2b/P3 垃圾面:回落解不回=誠實 null 零回寫;他家族/明文殘留零 crash ──
|
|
1703
|
+
const guestB18 = await guestOf18(TACET_L18, 'acct-l2b');
|
|
1704
|
+
const brokenL18 = (await sealNoteKey(TACET.wrap, noteKey, guestB18, 'notekey-local')).slice(0, -8) + 'AAAABBBB';
|
|
1705
|
+
TACET_L18.store.set(TACET_L18.store.noteKeyWrap('acct-l2b'), brokenL18);
|
|
1706
|
+
const blobBeforeBf18 = storedOf18(TACET_L18, 'acct-l2b');
|
|
1707
|
+
const heldT18 = makeHeldKey(); heldT18.set(noteKey);
|
|
1708
|
+
const ctT18 = await encryptNote(TACET_L18, heldT18, 'plain-bf-18', 'aad-18t', { current: () => 'acct-l2t' });
|
|
1709
|
+
await A('[18] 壞舊形(ct 竄改)讀回 → null(誠實降級非拋)',
|
|
1710
|
+
(await decryptNote(TACET_L18, makeHeldKey(), ctT18, 'aad-18t', { current: () => 'acct-l2b' })) === null);
|
|
1711
|
+
await A('[18] 壞舊形零回寫(自癒條款面:解不回不遷移)', storedOf18(TACET_L18, 'acct-l2b') === blobBeforeBf18);
|
|
1712
|
+
const otherK18 = await importAesGcm(crypto.getRandomValues(new Uint8Array(32)), false);
|
|
1713
|
+
TACET_L18.store.set(TACET_L18.store.noteKeyWrap('acct-l2c'), 'jr3d.' + await encryptWithKey(otherK18, 'y'.repeat(50), 'x'));
|
|
1714
|
+
await A('[18] 他家族 blob(鹽內嵌形)讀回 → null(前綴路由不串家族)',
|
|
1715
|
+
(await decryptNote(TACET_L18, makeHeldKey(), ctT18, 'aad-18t', { current: () => 'acct-l2c' })) === null);
|
|
1716
|
+
TACET_L18.store.set(TACET_L18.store.noteKeyWrap('acct-l2p'), 'plain-leftover-18');
|
|
1717
|
+
await A('[18] 明文殘留讀回 → null(零 crash——舊資料層不進包裹通路)',
|
|
1718
|
+
(await decryptNote(TACET_L18, makeHeldKey(), ctT18, 'aad-18t', { current: () => 'acct-l2p' })) === null);
|
|
1719
|
+
|
|
1720
|
+
// ── P3d cfg.wrap 缺席態(回落守衛面:無舊形可解,wrapLocal 帶著也不硬寫) ──
|
|
1721
|
+
const guestW18 = await guestOf18(TACET18_NOWRAP, 'acct-l2w');
|
|
1722
|
+
const legacyW18 = await sealNoteKey('jr1w.', noteKey, guestW18, 'notekey-local');
|
|
1723
|
+
TACET18_NOWRAP.store.set(TACET18_NOWRAP.store.noteKeyWrap('acct-l2w'), legacyW18);
|
|
1724
|
+
const blobBeforeW18 = storedOf18(TACET18_NOWRAP, 'acct-l2w');
|
|
1725
|
+
await A('[18] cfg.wrap 缺席讀舊形 → null(回落守衛面)',
|
|
1726
|
+
(await decryptNote(TACET18_NOWRAP, makeHeldKey(), ctB18, 'aad-18b', { current: () => 'acct-l2w' })) === null);
|
|
1727
|
+
await A('[18] 守衛面 stored 不變(零硬寫)', storedOf18(TACET18_NOWRAP, 'acct-l2w') === blobBeforeW18);
|
|
1728
|
+
|
|
1729
|
+
// ── P5 未配置態讀取面(wrap 帶、wrapLocal 無——v1 行為不變且不自癒) ──
|
|
1730
|
+
const guestN18 = await guestOf18(TACET18_NOLOCAL, 'acct-l2n');
|
|
1731
|
+
const legacyN18 = await sealNoteKey(TACET18_NOLOCAL.wrap, noteKey, guestN18, 'notekey-local');
|
|
1732
|
+
TACET18_NOLOCAL.store.set(TACET18_NOLOCAL.store.noteKeyWrap('acct-l2n'), legacyN18);
|
|
1733
|
+
const blobBeforeN18 = storedOf18(TACET18_NOLOCAL, 'acct-l2n');
|
|
1734
|
+
await A('[18] 未配置態讀舊形 = 解密成立(v1 行為不變——讀取面回落照走)',
|
|
1735
|
+
(await decryptNote(TACET18_NOLOCAL, makeHeldKey(), ctB18, 'aad-18b', { current: () => 'acct-l2n' })) === 'plain-read-18');
|
|
1736
|
+
await A('[18] 未配置態讀取不自癒重寫(heal 綁在新前綴面)', storedOf18(TACET18_NOLOCAL, 'acct-l2n') === blobBeforeN18);
|
|
1737
|
+
|
|
1738
|
+
// ── P6/P7 分離面+fail-open(私隱模式) ──
|
|
1739
|
+
const gCt18 = await encryptNote(TACET_L18, makeHeldKey(), 'guest-note-18', 'aad-18g', { current: () => 'acct-l5' });
|
|
1740
|
+
await A('[18] guest 期零本機觸碰(stored 殘留零+前綴 jr1g. 分離面)',
|
|
1741
|
+
storedOf18(TACET_L18, 'acct-l5') === null && gCt18.startsWith('jr1g.'));
|
|
1742
|
+
const FIXQ18 = { get: (): string | null => { throw new Error('LS_GET_DENIED'); }, set: (): void => { throw new Error('LS_SET_DENIED'); }, remove: (): void => { throw new Error('LS_RM_DENIED'); }, noteKeyWrap: (soul: string): string => 't18q_notekey:' + soul };
|
|
1743
|
+
const TACET_Q18: NoteCryptoConfig = { ...TACET_L18, store: FIXQ18 };
|
|
1744
|
+
await A('[18] 私隱模式 store 拋錯不外拋:empty-held encrypt 降級 guest 面(fail-open)',
|
|
1745
|
+
(await encryptNote(TACET_Q18, makeHeldKey(), 'plain-q-18', 'aad-18q', { current: () => 'acct-l4q' })).startsWith('jr1g.'));
|
|
1746
|
+
await A('[18] 私隱模式 get 拋錯 decrypt → null(誠實降級非外拋)',
|
|
1747
|
+
(await decryptNote(TACET_Q18, makeHeldKey(), ctB18, 'aad-18b', { current: () => 'acct-l4q' })) === null);
|
|
1748
|
+
await A('[18] guest 分支零 store 依賴(私隱模式 guest 密文照解)',
|
|
1749
|
+
(await decryptNote(TACET_Q18, makeHeldKey(), gCt18, 'aad-18g', { current: () => 'acct-l5' })) === 'guest-note-18');
|
|
1750
|
+
|
|
1751
|
+
// ── clearLocalWrap 契約原樣 ──
|
|
1752
|
+
await A('[18] clearLocalWrap 摘除面(契約原樣)', (async () => {
|
|
1753
|
+
await storeLocalWrap(TACET_L18, 'acct-l6', noteKey);
|
|
1754
|
+
const had = storedOf18(TACET_L18, 'acct-l6') !== null;
|
|
1755
|
+
clearLocalWrap(TACET_L18, 'acct-l6');
|
|
1756
|
+
return had && storedOf18(TACET_L18, 'acct-l6') === null;
|
|
1757
|
+
})());
|
|
1758
|
+
|
|
1759
|
+
// ── 源碼窗靜態錨(本地段結構——毒化形即計數/窗錨變異) ──
|
|
1760
|
+
const ncSrc18 = await srcOf('../src/client/note-crypto.ts');
|
|
1761
|
+
const localWin18 = ncSrc18.slice(ncSrc18.indexOf('── 本機包裹'), ncSrc18.indexOf('── 日記密文入口'));
|
|
1762
|
+
await A('[18] 源碼窗:本機段零 throw 面(ERR_WRAP_NOT_CONFIGURED 退場——未配置=無寫入無拋)',
|
|
1763
|
+
localWin18.length > 1900 && !localWin18.includes('ERR_WRAP_NOT_CONFIGURED'));
|
|
1764
|
+
await A('[18] 源碼窗:寫面唯 cfg.wrapLocal seal+舊借形零殘留(v1 借用面復活即反)',
|
|
1765
|
+
localWin18.split('sealNoteKey(cfg.wrapLocal').length === 2 && !localWin18.includes('sealNoteKey(cfg.wrap,'));
|
|
1766
|
+
await A('[18] 源碼窗:回落三元恰 1(收口形——自癒腿與未配置態同走單一 cfg.wrap 面;借用形復活即 2+)+自癒接線(if legacy → storeLocalWrap identity legacy 恰 1)',
|
|
1767
|
+
localWin18.split('openNoteKey(stored, guest, \'notekey-local\', cfg.wrap)').length === 2
|
|
1768
|
+
&& localWin18.split('await storeLocalWrap(cfg, identity, legacy)').length === 2
|
|
1769
|
+
&& /if \(legacy\) \{\s*await storeLocalWrap\(cfg, identity, legacy\);/.test(localWin18));
|
|
1770
|
+
await A('[18] 界面欄:wrapLocal? 宣告恰 1(note-crypto 全檔——NoteCryptoConfig 選配欄)',
|
|
1771
|
+
ncSrc18.split('wrapLocal?: string;').length === 2);
|
|
1772
|
+
// ── [18] 常駐毒化矩陣(本機段結構毒三案——母型 [16]:/tmp 拷貝突變+fresh import+出生即棄) ──
|
|
1773
|
+
{
|
|
1774
|
+
const getBuiltin18 = (id: string): unknown =>
|
|
1775
|
+
(globalThis as unknown as { process?: { getBuiltinModule?: (i: string) => unknown } }).process?.getBuiltinModule?.(id);
|
|
1776
|
+
const os18 = getBuiltin18('node:os') as { tmpdir?: () => string } | undefined;
|
|
1777
|
+
const fs18 = getBuiltin18('node:fs') as {
|
|
1778
|
+
rmSync?: (p: string, o?: { recursive: boolean; force: boolean }) => void;
|
|
1779
|
+
cpSync?: (a: string, b: string, o?: { recursive: boolean; filter?: (src: string) => boolean }) => void;
|
|
1780
|
+
readFileSync?: (p: string, e?: string) => string;
|
|
1781
|
+
writeFileSync?: (p: string, c: string, e?: string) => void;
|
|
1782
|
+
mkdtempSync?: (p: string) => string;
|
|
1783
|
+
} | undefined;
|
|
1784
|
+
const inner18 = (globalThis as unknown as { process?: { env?: Record<string, string | undefined> } }).process?.env?.POISON_GATE_INNER === '1';
|
|
1785
|
+
const ready18 = !!os18?.tmpdir && !!fs18?.mkdtempSync && !!fs18?.cpSync && !!fs18?.readFileSync && !!fs18?.writeFileSync && !!fs18?.rmSync;
|
|
1786
|
+
if (inner18 || !ready18) {
|
|
1787
|
+
await A('[18] 毒化矩陣載體就緒(node 內建模組在場;內層遞迴由 sentinel 跳過=正常;缺席=顯性 FAIL)', inner18 && !ready18 ? false : inner18, 'env unavailable AND not inner');
|
|
1788
|
+
} else {
|
|
1789
|
+
const repoRoot18 = new URL('..', import.meta.url).pathname;
|
|
1790
|
+
const skip18 = (s: string): boolean => s.endsWith('/.git') || s.includes('/.git/') || s.includes('/node_modules') || s.split('/').pop() === 'node_modules';
|
|
1791
|
+
const needlePA18 = "\n const fresh = cfg.wrapLocal\n ? await openNoteKey(stored, guest, 'notekey-local', cfg.wrapLocal) // 本體嚴格面(92B+rawHex hex 形)\n : null;\n if (fresh) return fresh;";
|
|
1792
|
+
const needlePB18 = " await storeLocalWrap(cfg, identity, legacy);\n return legacy;";
|
|
1793
|
+
const needlePC18 = "sealNoteKey(cfg.wrapLocal,";
|
|
1794
|
+
const existsSync18 = (p: string): boolean => {
|
|
1795
|
+
const fsx18 = getBuiltin18('node:fs') as { existsSync?: (p: string) => boolean } | undefined;
|
|
1796
|
+
return typeof fsx18?.existsSync === 'function' ? fsx18.existsSync(p) : false;
|
|
1797
|
+
};
|
|
1798
|
+
const trees18: string[] = [];
|
|
1799
|
+
const withPoison18 = async (applyS: (s: string) => string, restoreS: (s: string) => string, probe: (dir: string) => Promise<void>): Promise<void> => {
|
|
1800
|
+
const dir = fs18!.mkdtempSync!(os18!.tmpdir!() + '/t18-lw-')!;
|
|
1801
|
+
trees18.push(dir);
|
|
1802
|
+
try {
|
|
1803
|
+
fs18!.cpSync!(repoRoot18, dir, { recursive: true, filter: (s: string) => !skip18(s) });
|
|
1804
|
+
const p = dir + '/src/client/note-crypto.ts';
|
|
1805
|
+
const s0 = fs18!.readFileSync!(p, 'utf8');
|
|
1806
|
+
fs18!.writeFileSync!(p, applyS(s0), 'utf8');
|
|
1807
|
+
await probe(dir);
|
|
1808
|
+
} finally {
|
|
1809
|
+
fs18!.rmSync!(dir, { recursive: true, force: true });
|
|
1810
|
+
}
|
|
1811
|
+
};
|
|
1812
|
+
const freshImport18 = async (dir: string): Promise<Record<string, unknown>> => await import('file://' + dir + '/src/client/note-crypto.ts') as Record<string, unknown>;
|
|
1813
|
+
const mapStore18 = () => { const m = new Map<string, string>(); return {
|
|
1814
|
+
get: (k: string): string | null => (m.has(k) ? m.get(k)! : null),
|
|
1815
|
+
set: (k: string, v: string): void => { m.set(k, v); },
|
|
1816
|
+
remove: (k: string): void => { m.delete(k); },
|
|
1817
|
+
noteKeyWrap: (soul: string): string => 't18p_notekey:' + soul,
|
|
1818
|
+
}; };
|
|
1819
|
+
const baseCfg18 = (store: ReturnType<typeof mapStore18>): NoteCryptoConfig => ({ ...TACET, wrapLocal: 'jr1l.', store: store as unknown as NoteCryptoConfig['store'] });
|
|
1820
|
+
|
|
1821
|
+
// P18 摘 fresh-face(三元→null)→ stored jr1l. blob 通路讀死+自癒腿(舊形服務)仍活
|
|
1822
|
+
await withPoison18(
|
|
1823
|
+
(s: string) => s.replace(needlePA18, '\n const fresh = null;'),
|
|
1824
|
+
(s: string) => s.replace('\n const fresh = null;', needlePA18),
|
|
1825
|
+
async (dir: string) => {
|
|
1826
|
+
const mod = (await freshImport18(dir)) as typeof import('../src/client/note-crypto.ts');
|
|
1827
|
+
const FIX = mapStore18(); const CFG = baseCfg18(FIX);
|
|
1828
|
+
const nk = await mod.generateNoteKey();
|
|
1829
|
+
await mod.storeLocalWrap(CFG, 'acca2', nk);
|
|
1830
|
+
const hp = mod.makeHeldKey(); hp.set(nk);
|
|
1831
|
+
const ct = await mod.encryptNote(CFG, hp, 'plain-ct-18', 'aadz', { current: () => 'acca2y' });
|
|
1832
|
+
const back = await mod.decryptNote(CFG, mod.makeHeldKey(), ct, 'aadz', { current: () => 'acca2' });
|
|
1833
|
+
await A('[18] 毒化 P18 摘 fresh-face → 本機通路讀死(blob 不可讀——designated)', back === null, String(back));
|
|
1834
|
+
const guestA = await mod.deriveGuestKey(CFG, 'acca2');
|
|
1835
|
+
CFG.store.set(CFG.store.noteKeyWrap('acca2'), await mod.sealNoteKey('jr1w.', nk, guestA, 'notekey-local'));
|
|
1836
|
+
const lback = await mod.decryptNote(CFG, mod.makeHeldKey(), ct, 'aadz', { current: () => 'acca2' });
|
|
1837
|
+
await A('[18] 毒化 P18 自癒腿仍活(舊形 blob 照服務——單面隔離非互毀)', lback !== null, String(lback));
|
|
1838
|
+
});
|
|
1839
|
+
// P18b 摘自癒回寫 → 舊形讀回照解+stored 殘留 jr1w.(零重寫)
|
|
1840
|
+
await withPoison18(
|
|
1841
|
+
(s: string) => s.replace(needlePB18, ' return legacy;'),
|
|
1842
|
+
(s: string) => s.replace(' return legacy;', needlePB18),
|
|
1843
|
+
async (dir: string) => {
|
|
1844
|
+
const mod = (await freshImport18(dir)) as typeof import('../src/client/note-crypto.ts');
|
|
1845
|
+
const FIX = mapStore18(); const CFG = baseCfg18(FIX);
|
|
1846
|
+
const nk = await mod.generateNoteKey();
|
|
1847
|
+
const guest = await mod.deriveGuestKey(CFG, 'accb2');
|
|
1848
|
+
const legacy = await mod.sealNoteKey('jr1w.', nk, guest, 'notekey-local');
|
|
1849
|
+
CFG.store.set(CFG.store.noteKeyWrap('accb2'), legacy);
|
|
1850
|
+
const hp = mod.makeHeldKey(); hp.set(nk);
|
|
1851
|
+
const ct = await mod.encryptNote(CFG, hp, 'plain-ct-18', 'aadz', { current: () => 'accb2y' });
|
|
1852
|
+
const back = await mod.decryptNote(CFG, mod.makeHeldKey(), ct, 'aadz', { current: () => 'accb2' });
|
|
1853
|
+
await A('[18] 毒化 P18b 摘自癒回寫 → 舊形讀回照解(讀面不依賴回寫線)', back !== null, String(back));
|
|
1854
|
+
await A('[18] 毒化 P18b stored 殘留 jr1w.(零重寫——heal 條款面是唯一重寫者)',
|
|
1855
|
+
typeof legacy === 'string' && legacy.startsWith('jr1w.') && (CFG.store.get(CFG.store.noteKeyWrap('accb2')) ?? '') === legacy);
|
|
1856
|
+
});
|
|
1857
|
+
// P18c 寫面接回借用形(禁手)→ 寫面 fallback jr1w.(v1 借用面復活的行為翻轉)
|
|
1858
|
+
await withPoison18(
|
|
1859
|
+
(s: string) => s.replace(needlePC18, 'sealNoteKey(cfg.wrap,'),
|
|
1860
|
+
(s: string) => s.replace('sealNoteKey(cfg.wrap,', needlePC18),
|
|
1861
|
+
async (dir: string) => {
|
|
1862
|
+
const mod = (await freshImport18(dir)) as typeof import('../src/client/note-crypto.ts');
|
|
1863
|
+
const FIX = mapStore18(); const CFG = baseCfg18(FIX);
|
|
1864
|
+
const nk = await mod.generateNoteKey();
|
|
1865
|
+
await mod.storeLocalWrap(CFG, 'accc2', nk);
|
|
1866
|
+
const blob = CFG.store.get(CFG.store.noteKeyWrap('accc2'));
|
|
1867
|
+
await A('[18] 毒化 P18c 寫面接回借用形 → 寫面 fallback jr1w.(designated——寫面契約行為翻轉)',
|
|
1868
|
+
typeof blob === 'string' && blob.startsWith('jr1w.'), String(blob ?? '').slice(0, 8));
|
|
1869
|
+
});
|
|
1870
|
+
// 還原自證:真樹 needle 計數在所有毒化後仍恰 1×3(byte-exact 還原的行程帳)
|
|
1871
|
+
const srcAfter18 = await srcOf('../src/client/note-crypto.ts');
|
|
1872
|
+
await A('[18] 毒化還原自證(真樹 needle 恰 1×3——毒化零殘留本樹)',
|
|
1873
|
+
srcAfter18.split(needlePA18).length === 2 && srcAfter18.split(needlePB18).length === 2 && srcAfter18.split(needlePC18).length === 2);
|
|
1874
|
+
await A('[18] 毒化樹零殘留(本 run 喚出 ' + trees18.length + ' 棵全清——封閉集判準)',
|
|
1875
|
+
trees18.length === 3 && trees18.every((t) => !fs18!.rmSync || !existsSync18(t)));
|
|
1876
|
+
}
|
|
1877
|
+
}
|
|
1878
|
+
}
|
|
1879
|
+
|
|
1880
|
+
// ── 19. ladder 表與重取鹽面(v0.2.0 批卡② t_ce216a63;(a+) 案) ──────────────
|
|
1881
|
+
//
|
|
1882
|
+
// 驗證策略(真 SQLite,node:sqlite):①ph2_ladder DDL(FK CASCADE/UNIQUE account_id);
|
|
1883
|
+
// ②重取鹽面 unwrapNoteKeyDual4WithSalt=雙腿重試形為(ladder 遷移旋轉 salt1 後,呼叫端
|
|
1884
|
+
// 以 login 回帶的新 salt 重試 jr4d 重包──新面 unwrap 必中、陳舊 salt 必 null 兩面承載);
|
|
1885
|
+
// ③ladder 表語意面(kinds 常數/表名單一真相)+值主權威 upsert 行為面(真 node:sqlite
|
|
1886
|
+
// 直驅 makePh2LadderStore:upsert 恰一列、同一舊值最新持有者勝、無 DELETE 語句面)。
|
|
1887
|
+
// 表名/kind 常數的契約面(PH2_LADDER_TABLE='ph2_ladder'、KIND legacy/v2)由本段斷言直接咬住
|
|
1888
|
+
//(fork migration 0011 與 core 原語同字面——漂移=fork schema 與 core 原語脫鉤=表缺席 fail-open 面)。
|
|
1889
|
+
console.log('\n[19] ladder 表與重取鹽面(unwrapNoteKeyDual4WithSalt+ph2_ladder)');
|
|
1890
|
+
{
|
|
1891
|
+
const { DatabaseSync } = (globalThis as unknown as {
|
|
1892
|
+
process?: { getBuiltinModule?: (id: string) => { DatabaseSync?: unknown } };
|
|
1893
|
+
}).process?.getBuiltinModule?.('node:sqlite') ?? {};
|
|
1894
|
+
if (!DatabaseSync) throw new Error('ERR_SQLITE_UNAVAILABLE');
|
|
1895
|
+
await A('[19] ladder 表名契約:PH2_LADDER_TABLE = ph2_ladder(fork migration 同字面)', PH2_LADDER_TABLE === 'ph2_ladder');
|
|
1896
|
+
await A('[19] ladder kinds 契約:legacy/v2 兩形(常數面)', PH2_LADDER_KIND_LEGACY === 'legacy' && PH2_LADDER_KIND_V2 === 'v2');
|
|
1897
|
+
// 值主權威行為面(真 node:sqlite 直驅本體):建表走 fork migration 同形(PK=ph2)。
|
|
1898
|
+
{
|
|
1899
|
+
const DatabaseSync19 = DatabaseSync as new (p: string) => { exec: (s: string) => void; prepare: (s: string) => { run: (...p: unknown[]) => { changes: number | bigint }; get: (...p: unknown[]) => unknown } };
|
|
1900
|
+
const db19 = new DatabaseSync19(':memory:');
|
|
1901
|
+
db19.exec('CREATE TABLE users (account_id TEXT PRIMARY KEY, ph2 TEXT)');
|
|
1902
|
+
db19.exec("INSERT INTO users (account_id, ph2) VALUES ('acc19', 'ph2-new')");
|
|
1903
|
+
db19.exec("INSERT INTO users (account_id, ph2) VALUES ('ghost19', 'ph2-ghost')");
|
|
1904
|
+
const migSrc19 = await srcOf('../migrations/0011-ph2-ladder.sql');
|
|
1905
|
+
const ddlPos19 = migSrc19.indexOf('CREATE TABLE IF NOT EXISTS ph2_ladder');
|
|
1906
|
+
const ddl19 = migSrc19.slice(ddlPos19, migSrc19.indexOf(';', ddlPos19) + 1);
|
|
1907
|
+
db19.exec(ddl19.replace(/IF NOT EXISTS /g, ''));
|
|
1908
|
+
const env19 = {
|
|
1909
|
+
DB: {
|
|
1910
|
+
prepare(sql: string) {
|
|
1911
|
+
return {
|
|
1912
|
+
bind(...params: unknown[]) {
|
|
1913
|
+
return {
|
|
1914
|
+
async first() { return db19.prepare(sql).get(...params) as Row19 | null; },
|
|
1915
|
+
async run() { db19.prepare(sql).run(...params); return { changes: 1 }; },
|
|
1916
|
+
};
|
|
1917
|
+
},
|
|
1918
|
+
};
|
|
1919
|
+
},
|
|
1920
|
+
},
|
|
1921
|
+
} as unknown as Env19;
|
|
1922
|
+
const ladder19 = makePh2LadderStore(env19 as unknown as Parameters<typeof makePh2LadderStore>[0]);
|
|
1923
|
+
await ladder19.insert('acc19', 'old-19', PH2_LADDER_KIND_V2);
|
|
1924
|
+
await ladder19.insert('acc19', 'old-19b', PH2_LADDER_KIND_V2); // 同帳戶第二個舊值歷列
|
|
1925
|
+
await ladder19.insert('acc19', 'old-19', PH2_LADDER_KIND_LEGACY); // 同舊值 upsert(值主權威覆蓋)
|
|
1926
|
+
await ladder19.insert('ghost19', 'old-19', PH2_LADDER_KIND_V2); // 幽靈帳同舊值=最新持有者勝
|
|
1927
|
+
const r19a = await ladder19.findByOldPh2('old-19');
|
|
1928
|
+
await A('[19] ladder upsert 值主權威:同一舊值恰一列(PK=ph2)且最新持有者勝',
|
|
1929
|
+
r19a !== null && r19a.accountId === 'ghost19' && r19a.ph2Kind === 'v2',
|
|
1930
|
+
JSON.stringify(r19a));
|
|
1931
|
+
const cnt19 = db19.prepare('SELECT COUNT(*) AS n FROM ph2_ladder').get() as { n: number };
|
|
1932
|
+
await A('[19] ladder upsert 恆一列(三案同表恰 2 列——值唯一+多舊值歷列並存)', cnt19.n === 2, String(cnt19.n));
|
|
1933
|
+
const r19b = await ladder19.findByOldPh2('old-19b');
|
|
1934
|
+
await A('[19] ladder 多舊值歷列:第二舊值仍在場(同帳戶兩段遷移各存)', r19b !== null && r19b.accountId === 'acc19');
|
|
1935
|
+
await A('[19] ladder miss 面:查無列回 null', (await ladder19.findByOldPh2('nope-19')) === null);
|
|
1936
|
+
}
|
|
1937
|
+
|
|
1938
|
+
// 重取鹽面:wrap 吃 passD4 全形(NFKC 載體)→ unwrap 以新 salt(ladder 遷移後重取)必中
|
|
1939
|
+
const rewrapD4 = await wrapNoteKeyDual4(TACET4, noteKey, passD4, pinD);
|
|
1940
|
+
const withSalt = await unwrapNoteKeyDual4WithSalt(TACET4, rewrapD4.wrapped, passD, pinD, rewrapD4.salt);
|
|
1941
|
+
await A('[19] WithSalt 重試面:重取 salt1(ladder 遷移後新值)→ unwrap = 原 noteKey',
|
|
1942
|
+
withSalt !== null && hex(new Uint8Array(await crypto.subtle.exportKey('raw', withSalt))) === origRaw);
|
|
1943
|
+
await A('[19] WithSalt 陳舊鹽面:wrap 期的 salt1(已旋轉面)→ null(鹽旋轉後舊鹽解不開)',
|
|
1944
|
+
(await unwrapNoteKeyDual4WithSalt(TACET4, rewrapD4.wrapped, passD, pinD, dual4.salt)) === null);
|
|
1945
|
+
await A('[19] WithSalt 錯 passphrase → null(同 unwrapNoteKeyDual4 本體契約)',
|
|
1946
|
+
(await unwrapNoteKeyDual4WithSalt(TACET4, rewrapD4.wrapped, 'wrong-passphrase', pinD, rewrapD4.salt)) === null);
|
|
1947
|
+
await A('[19] WithSalt 未配置 cfg(wrapDual4 缺)→ null(opt-in 鐵律同向)',
|
|
1948
|
+
(await unwrapNoteKeyDual4WithSalt({ pinSalt3Prefix: 'tacet-note-pin3:' }, rewrapD4.wrapped, passD, pinD, rewrapD4.salt)) === null);
|
|
1949
|
+
}
|
|
1129
1950
|
console.log(`\n${passed} 斷言全綠` + (failures.length ? `;${failures.length} 失敗` : ''));
|
|
1130
1951
|
if (failures.length) {
|
|
1131
1952
|
console.error('失敗項:', failures);
|
|
1132
1953
|
process.exit(1);
|
|
1133
1954
|
}
|
|
1955
|
+
|
|
1134
1956
|
console.log('CORE-CRYPTO-VERIFY-OK');
|