@tacet-ink/journal-core 0.2.5 → 0.3.1

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.
Files changed (49) hide show
  1. package/README.md +38 -15
  2. package/dist/client/argon-auto.d.ts +47 -0
  3. package/dist/client/argon-auto.js +226 -0
  4. package/dist/client/argon-worker.d.ts +31 -0
  5. package/dist/client/argon-worker.js +147 -0
  6. package/dist/client/argon2.d.ts +167 -0
  7. package/dist/client/argon2.js +410 -0
  8. package/dist/client/bip39.d.ts +11 -0
  9. package/dist/client/bip39.js +139 -0
  10. package/dist/client/keys.d.ts +15 -0
  11. package/dist/client/keys.js +30 -0
  12. package/dist/client/note-crypto.d.ts +203 -0
  13. package/dist/client/note-crypto.js +600 -0
  14. package/dist/client/pinlock.d.ts +38 -0
  15. package/dist/client/pinlock.js +84 -0
  16. package/dist/client/vault-error.d.ts +44 -0
  17. package/dist/client/vault-error.js +19 -0
  18. package/dist/client/vault.d.ts +107 -0
  19. package/dist/client/vault.js +391 -0
  20. package/dist/client/vendor/hash-wasm-argon2.d.ts +14 -0
  21. package/dist/client/vendor/hash-wasm-argon2.js +160 -0
  22. package/dist/client/wordlist.d.ts +7 -0
  23. package/dist/client/wordlist.js +264 -0
  24. package/dist/index.d.ts +18 -0
  25. package/dist/index.js +17 -0
  26. package/dist/server/auth.d.ts +99 -0
  27. package/dist/server/auth.js +158 -0
  28. package/dist/server/cors.d.ts +10 -0
  29. package/dist/server/cors.js +21 -0
  30. package/dist/server/env.d.ts +6 -0
  31. package/dist/server/env.js +1 -0
  32. package/dist/server/hash.d.ts +11 -0
  33. package/dist/server/hash.js +26 -0
  34. package/dist/server/ladder.d.ts +49 -0
  35. package/dist/server/ladder.js +50 -0
  36. package/dist/server/ratelimit.d.ts +32 -0
  37. package/dist/server/ratelimit.js +63 -0
  38. package/docs/format-spec.md +369 -0
  39. package/docs/vectors/bip39.json +1063 -0
  40. package/docs/vectors/families.json +594 -0
  41. package/docs/vectors/kat.json +145 -0
  42. package/package.json +68 -17
  43. package/scripts/verify-core-crypto.ts +406 -11
  44. package/src/client/argon-auto.ts +224 -0
  45. package/src/client/argon-worker.ts +147 -0
  46. package/src/client/argon2.ts +7 -2
  47. package/src/client/vault-error.ts +51 -0
  48. package/src/client/vault.ts +486 -0
  49. package/src/client/vendor/hash-wasm-argon2.ts +27 -0
@@ -0,0 +1,139 @@
1
+ /**
2
+ * bip39.ts — 復原套件轉寫層(BIP39 英文 24 詞 ⇄ entropy 32B ⇄ recToken hex64)。
3
+ *
4
+ * 契約裁定(2026-09-10 復原套件三件):
5
+ * - **轉寫層,非 KDF 升級**:24 詞 ⇄ hex64 可逆互轉;線上契約(rec_hash = SHA-256(hex64)、
6
+ * wrappedRec = jr1w. + GCM(PBKDF2(recToken, ...)))完全不變,伺服器零改動。
7
+ * 熵等價:24 詞 = 256-bit entropy + 8-bit checksum(BIP39 標準);與舊 hex64 同熵。
8
+ * - **checksum 是轉寫品質閘**:單詞打錯 255/256 機率當場被抓(64 hex 轉寫無此偵測);
9
+ * 解析失敗一律回 null(不洩漏哪類錯:字數/詞表外/checksum 統一,UI 統一訊息)。
10
+ * - **只收 24 詞**:12 詞是降級禁用(security-roadmap 裁定);15/18/21 不收(契約面單一)。
11
+ * - **相容層永久**:舊帳戶 hex64 紙本照走現行鏈;recTokenToWords 供既有套件顯示用。
12
+ * - **零依賴**:SHA-256 用 WebCrypto(全 async API);bits 打包自製(閘 [9] 以
13
+ * @scure/bip39 參照實作 200 組雙向對照守證)。禁用 @scure 的 mnemonicToSeed
14
+ * (PBKDF2-SHA512 seed 派生與 Tacet 契約無關,不可混入)。
15
+ */
16
+ import { wordlist } from './wordlist.js';
17
+ const HEX64_RE = /^[0-9a-f]{64}$/;
18
+ async function sha256(data) {
19
+ return new Uint8Array(await crypto.subtle.digest('SHA-256', data));
20
+ }
21
+ function hexToBytes(hexStr) {
22
+ const out = new Uint8Array(hexStr.length / 2);
23
+ for (let i = 0; i < out.length; i++)
24
+ out[i] = parseInt(hexStr.slice(i * 2, i * 2 + 2), 16);
25
+ return out;
26
+ }
27
+ function bytesToHex(bytes) {
28
+ return Array.from(bytes).map(b => b.toString(16).padStart(2, '0')).join('');
29
+ }
30
+ /** 輸入正規化:NFKC → 換行/全形空白/多空白收斂單半形空格 → 小寫(wordlist 全小寫,輸入容錯)。 */
31
+ function normalizeWords(input) {
32
+ return input.normalize('NFKC').trim().toLowerCase().split(/\s+/).filter(w => w.length > 0);
33
+ }
34
+ /** 11-bit 打包(BIP39 標準位元累積器;carry 恆 < 19 bits,索引 = 低位遮罩 0x7ff)。 */
35
+ function entropyToIndexes(entropy, checksum) {
36
+ const bytes = new Uint8Array(entropy.length + 1);
37
+ bytes.set(entropy);
38
+ bytes[entropy.length] = checksum; // 32B entropy → checksum 8 bits = SHA-256 首位元組全值
39
+ const indexes = [];
40
+ let carry = 0;
41
+ let bits = 0;
42
+ for (const byte of bytes) {
43
+ carry = (carry << 8) | byte;
44
+ bits += 8;
45
+ if (bits >= 11) {
46
+ bits -= 11;
47
+ indexes.push((carry >>> bits) & 0x7ff);
48
+ carry &= (1 << bits) - 1;
49
+ }
50
+ }
51
+ return indexes;
52
+ }
53
+ /** 11-bit 逆打包:24 個索引 → 33 bytes(entropy 32B + checksum 1B)。 */
54
+ function indexesToBytes(indexes) {
55
+ const out = new Uint8Array(33);
56
+ let carry = 0;
57
+ let bits = 0;
58
+ let pos = 0;
59
+ for (const idx of indexes) {
60
+ carry = (carry << 11) | idx;
61
+ bits += 11;
62
+ while (bits >= 8) {
63
+ bits -= 8;
64
+ out[pos++] = (carry >>> bits) & 0xff;
65
+ carry &= (1 << bits) - 1;
66
+ }
67
+ }
68
+ return out; // 24 詞 = 264 bits = 33 bytes 整除,無餘位(11-bit 打包自洽)
69
+ }
70
+ /** 產生 24 詞套件(entropy 32B 隨機 → +8-bit checksum → 24 詞)。
71
+ * entropyBytes 可注入(驗證閘固定種子樣本用;產品面不傳=真隨機契約不變)。 */
72
+ export async function generateBip39Words(entropyBytes) {
73
+ const entropy = entropyBytes ?? crypto.getRandomValues(new Uint8Array(32));
74
+ const checksum = (await sha256(entropy))[0];
75
+ return entropyToIndexes(entropy, checksum).map(idx => wordlist[idx]);
76
+ }
77
+ // ── 驗證閘固定種子樣本(flake 歸零)──────────────────────────────
78
+ //
79
+ // 背景:checksum 統計面「錯一詞 64 樣本 ≥63 被抓」用真隨機取樣時,P(漏 ≥2)≈0.4%,
80
+ // 偶發 62/64=閘 flake(2026-09-19 session 實證)。治本是固定種子:xorshift32 以
81
+ // 常數 seed 產生確定性位元組流(種子決定樣本集=向量可重放、閘輸出逐輪恆定)。
82
+ // 樣本集僅供驗證閘使用(checksum 是 BIP39 公開數學,非機密;產品面
83
+ // generateBip39Words() 不帶參數=crypto.getRandomValues 真隨機契約不變)。
84
+ function xorshift32Bytes(seed, len) {
85
+ let s = seed >>> 0;
86
+ if (s === 0)
87
+ s = 0x9e3779b9; // xorshift 全零態死鎖防護
88
+ const out = new Uint8Array(len);
89
+ for (let i = 0; i < len; i++) {
90
+ s ^= s << 13;
91
+ s >>>= 0;
92
+ s ^= s >>> 17;
93
+ s ^= s << 5;
94
+ s >>>= 0;
95
+ out[i] = s & 0xff;
96
+ }
97
+ return out;
98
+ }
99
+ /** 固定種子樣本集:seed 給定 → 32B 確定(xorshift32 常數種子流)。 */
100
+ export function seededSampleBytes(seed) {
101
+ return xorshift32Bytes(seed, 32);
102
+ }
103
+ /** 24 詞 → recToken hex64。任何不符(字數/詞表外/checksum)回 null,不洩漏哪類錯。 */
104
+ export async function wordsToRecToken(input) {
105
+ const words = typeof input === 'string' ? normalizeWords(input) : input.map(w => w.normalize('NFKC').trim().toLowerCase());
106
+ if (words.length !== 24)
107
+ return null;
108
+ const indexes = [];
109
+ for (const w of words) {
110
+ const idx = wordlist.indexOf(w);
111
+ if (idx < 0)
112
+ return null;
113
+ indexes.push(idx);
114
+ }
115
+ const bytes = indexesToBytes(indexes);
116
+ const entropy = bytes.slice(0, 32);
117
+ const checksum = bytes[32];
118
+ const expected = (await sha256(entropy))[0];
119
+ if (checksum !== expected)
120
+ return null;
121
+ return bytesToHex(entropy);
122
+ }
123
+ /** recToken hex64 → 24 詞(既有套件顯示需求;非 hex64 或格式不符回 null)。 */
124
+ export async function recTokenToWords(recToken) {
125
+ if (!HEX64_RE.test(recToken))
126
+ return null;
127
+ const entropy = hexToBytes(recToken);
128
+ const checksum = (await sha256(entropy))[0];
129
+ return entropyToIndexes(entropy, checksum).map(idx => wordlist[idx]);
130
+ }
131
+ /** 抄寫抽驗位置:24 詞中抽 4 個不重複位置(0-23),排序回傳(顯示順序穩定)。 */
132
+ export function spotCheckIndexes() {
133
+ const pool = Array.from({ length: 24 }, (_, i) => i);
134
+ for (let i = pool.length - 1; i > 0; i--) {
135
+ const j = crypto.getRandomValues(new Uint32Array(1))[0] % (i + 1);
136
+ [pool[i], pool[j]] = [pool[j], pool[i]];
137
+ }
138
+ return pool.slice(0, 4).sort((a, b) => a - b);
139
+ }
@@ -0,0 +1,15 @@
1
+ /**
2
+ * keys.ts — storage key 命名空間。各產品以 config 指定前綴(如 journal 的 <brand>_)。
3
+ * 理由:fork diff 證實 noteCrypto 差異一半在 localStorage key 前綴——參數化收進 core。
4
+ */
5
+ export interface KeyStoreConfig {
6
+ /** 產品前綴,如 'tacet'(不含底線)。 */
7
+ brand: string;
8
+ }
9
+ export declare function makeKeyStore(config: KeyStoreConfig): {
10
+ get: (k: string) => string | null;
11
+ set: (k: string, v: string) => void;
12
+ remove: (k: string) => void;
13
+ noteKeyWrap: (soulKey: string) => string;
14
+ };
15
+ export type KeyStore = ReturnType<typeof makeKeyStore>;
@@ -0,0 +1,30 @@
1
+ /**
2
+ * keys.ts — storage key 命名空間。各產品以 config 指定前綴(如 journal 的 <brand>_)。
3
+ * 理由:fork diff 證實 noteCrypto 差異一半在 localStorage key 前綴——參數化收進 core。
4
+ */
5
+ export function makeKeyStore(config) {
6
+ const wrapKey = (soulKey) => `${config.brand}_notekey:${soulKey}`;
7
+ return {
8
+ get: (k) => {
9
+ try {
10
+ return localStorage.getItem(k);
11
+ }
12
+ catch {
13
+ return null;
14
+ }
15
+ },
16
+ set: (k, v) => {
17
+ try {
18
+ localStorage.setItem(k, v);
19
+ }
20
+ catch { /* private mode:不阻擋主流程 */ }
21
+ },
22
+ remove: (k) => {
23
+ try {
24
+ localStorage.removeItem(k);
25
+ }
26
+ catch { /* noop */ }
27
+ },
28
+ noteKeyWrap: wrapKey,
29
+ };
30
+ }
@@ -0,0 +1,203 @@
1
+ /**
2
+ * note-crypto.ts — 自由書寫 e2e 加密,品牌參數化核心。
3
+ *
4
+ * 血統:抽取自姊妹專案的日記加密模組(37 斷言驗證閘母型);
5
+ * fork diff 已證實:除品牌前綴(guest KDF 前綴、密文前綴家族、KDF salt 前綴)
6
+ * 與產品附加機制外,密碼學本體在多個產品間完全一致——本檔承載該本體。
7
+ *
8
+ * 兩時代金鑰模型(不變量,勿破壞):
9
+ * 時代 1(未綁定 guest):K_u = SHA-256(guestKdfPrefix ‖ identity) — 純客戶端派生,混淆級
10
+ * 時代 2(綁定後):隨機 256-bit noteKey;日常加解密只用 noteKey,passphrase 以
11
+ * KEK = PBKDF2(pass, salt, 600000, SHA-256) 包裹成 wrapped 上傳;復原套件 wrappedRec
12
+ * = 同款包裹在 KEK_rec = PBKDF2(recToken, salt = recSaltPrefix ‖ identity) 下。
13
+ * passphrase 從此不過線:線上憑證送 PH1(雜湊形,派生方式由各產品自定義),伺服器存 PH2。
14
+ * 復原套件 v0.2.0 世代:前綴專用化(wrapRec=jr1r.)+KEK_rec 派生
15
+ * 世代化(recKekHkdf 配置=HKDF-SHA256,info='journal-kek-rec-v1:'+recKekHkdf)+
16
+ * AAD 帶內切換('notekey'→'notekey-rec');新世代讀舊寫新——unwrap 新面先行、回落
17
+ * 舊契約面(PBKDF2+AAD 'notekey')雙試(過渡保護承諾:舊 blob 永遠可解;摘雙試
18
+ * 時點=遷移率裁定非日曆)。
19
+ *
20
+ * 前綴全部可配置(帶內版本化:升級 KDF 參數 = 換新前綴)。AAD 由呼叫端傳入——
21
+ * 例如綁 `noteId`(跨列搬移必解密失敗)、`lifeId:day`(逐日日記)或留言綁 `tst:<lifeId>`。
22
+ *
23
+ * ⚠️ extractable 鐵律:所有要被 exportKey/wrap 的 key,
24
+ * import 當下就必須 extractable=true。noteKey 全部產生路徑(generate/unwrap×3)
25
+ * 一律顯式 true;KEK/guest key 保持 nonextractable(只加解密、永不 export)。
26
+ */
27
+ export interface NoteCryptoConfig {
28
+ /** guest 時代 KDF 前綴,如 'journal-note-u1'。 */
29
+ guestKdfPrefix: string;
30
+ /** 復原包裹 KDF salt 前綴,如 'journal-note-rec1:'。 */
31
+ recSaltPrefix: string;
32
+ /** 密文前綴:guest 時代,如 'jr1g.'。未配置 = guest API 拒絕(未配置行為不變;2026-09-19 轉選配)。 */
33
+ cipherGuest?: string;
34
+ /** 密文前綴:綁定時代,如 'jr1b.'。 */
35
+ cipherBound: string;
36
+ /** 金鑰包裹前綴,如 'jr1w.'。 */
37
+ wrap: string;
38
+ /** 本機包裹前綴(jr1l.:本機包裹不再借用 cfg.wrap——「一個前綴一份契約」
39
+ * 的第二違例收口)。未配置 = storeLocalWrap/loadLocalWrap 退場(無寫入/讀取照走
40
+ * 舊形回落,零資料損失——cipherLocal opt-in 母型;寫面功能退場——v1 可用者升級後
41
+ * 需配置 wrapLocal 才保留本機包裹寫入)。 */
42
+ wrapLocal?: string;
43
+ /** 復原套件專用前綴(jr1r.:wrappedRec 不再借用 cfg.wrap——同前例收口)。
44
+ * AAD 亦帶內切換 'notekey'→'notekey-rec'(共用 AAD 收口)。與 recKekHkdf **兩欄一體**
45
+ * (同缺同在;部分配置=ERR_REC_CFG_PARTIAL 拒寫——世代配對門,fail-closed 禁寫出
46
+ * 「前綴面 jr1r.×派生面 PBKDF2」跨契約 blob)。兩欄皆缺席 = wrap/unwrap 復原套件面
47
+ * 照走舊契約(cfg.wrap+PBKDF2+AAD 'notekey')=行為不變態(配置形正確
48
+ * 時舊 wrappedRec blob 永遠可解——HKDF 腿誤配拋點整面 fail-closed 既有契約)。 */
49
+ wrapRec?: string;
50
+ /** 復原套件「KEK 派生」世代開關:KEK_rec = HKDF-SHA256(
51
+ * ikm = recToken(256-bit 實體因子), salt = recKekSalt?:16B hex(未配置=零鹽),
52
+ * info = 'journal-kek-rec-v1:' + recKekHkdf, L = 32 )——info 合成名沿 argon2 家族
53
+ * 慣例(呼叫端帶入;KEK 派生世代空間由前綴+info 承載)。HKDF 世代 identity 不入
54
+ * KEK(identity-free by design——256-bit 實體因子足綁;identity 綁域由
55
+ * 舊契約面 recSaltPrefix‖identity 專屬承載,兩世代綁域刻意分離)。與 wrapRec 兩欄
56
+ * 一體(同缺同在;部分配置=ERR_REC_CFG_PARTIAL 拒寫)。未配置 = PBKDF2 舊世代
57
+ * (裁定:HKDF 強化屬 v0.2.0 世代;強度差異對 256-bit recToken 無意義,
58
+ * 收的是「派生域+前綴契約面分離」的語意收口)。 */
59
+ recKekHkdf?: string;
60
+ /** HKDF salt:16B hex 專屬鹽域(與 PBKDF2 面 recSaltPrefix 前綴域分離
61
+ * ——KEK 派生世代鹽)。未配置 = HKDF 零鹽(HKDF salt 欄位本就 optional;語意不變,
62
+ * 域由 info 承載)。 */
63
+ recKekSalt?: string;
64
+ /** 雙因子合鑰包裹前綴(jr2w.,PIN 第二因子)。未配置 = dual API 拒絕(未配置行為不變)。 */
65
+ wrapDual?: string;
66
+ /** 雙因子 pin 段 PBKDF2 salt 前綴,如 'journal-note-pin1:'(pinSalt 內嵌 payload 後拼接)。 */
67
+ pinSaltPrefix?: string;
68
+ /** 分享包裹前綴(jrsw.,單篇分享連結 V1)。未配置 = share API 拒絕(未配置行為不變)。 */
69
+ wrapShare?: string;
70
+ /** 附件密文前綴(jr1c.,image attachments)。未配置 = attach API 拒絕(未配置行為不變)。 */
71
+ cipherAttach?: string;
72
+ /** 本機 IDB 密文前綴(jr1d.,notes store stored 形)。未配置 = local API 拒絕(未配置行為不變)。 */
73
+ cipherLocal?: string;
74
+ /** localStorage key store(品牌前綴由 keys.ts 管理)。 */
75
+ store: import('./keys').KeyStore;
76
+ }
77
+ export declare const PBKDF2_ITERATIONS = 600000;
78
+ export declare const PIN_PBKDF2_ITERATIONS = 2000000;
79
+ export declare const IV_LEN = 12;
80
+ export declare function b64(bytes: Uint8Array): string;
81
+ export declare function unb64(text: string): Uint8Array;
82
+ export declare function toHex(bytes: Uint8Array): string;
83
+ export declare function hexToBytes(hex: string): Uint8Array;
84
+ /** SHA-256 hex(分享定位鍵雜湊等呼叫端雜湊用;與伺服器端 sha256Hex 同構)。 */
85
+ export declare function sha256HexExport(text: string): Promise<string>;
86
+ export declare function importAesGcm(raw: Uint8Array, extractable?: boolean): Promise<CryptoKey>;
87
+ export declare function deriveGuestKey(cfg: NoteCryptoConfig, identity: string): Promise<CryptoKey>;
88
+ /** PIN 正規化契約:NFKC → trim → lowercase(不分大小寫;正規化後內含空白由強度檢拒絕)。 */
89
+ export declare function normalizePin(pin: string): string;
90
+ /**
91
+ * 密語正規化契約 v3(NFKC-only 裁定):NFKC——**大小寫摺疊禁絕:區分大小寫
92
+ * (大小寫差=不同 KEK/ph2)+首尾空白保留(不 trim)**。
93
+ * 消費面只有 v3 世代:jr4w./jr4d. 包裹家族與 PH1 v3(derivePh1ArgonV3)——
94
+ * 舊前綴家族(jr1w./jr3w./jr2w./jr3d.)契約面永不變(raw pass 派生,帶內版本化=
95
+ * KEK 輸入契約改變=換新前綴,禁原地改語意),禁把本函式接進舊家族入口。
96
+ * 與 normalizePin 契約並存不混(NFKC→trim→lowercase,pinlock/dual 面不動):
97
+ * 兩函式是正交契約,閘有「同輸入不同輸出」行為向量承載分離。
98
+ * 三面單一真相:本體(此處)+README 家族表+驗證閘 [15] 行為向量——改 NFKC 語意=帶內版本化(換世代),禁原地改。
99
+ */
100
+ export declare function normalizePassphrase(passphrase: string): string;
101
+ /** 雙因子包裹:payload = pinSalt[16] ‖ iv[12] ‖ GCM(KEK2, hex(noteKey));salt1 隨機由呼叫端存 users.salt。 */
102
+ export declare function wrapNoteKeyDual(cfg: NoteCryptoConfig, noteKey: CryptoKey, passphrase: string, pin: string): Promise<{
103
+ wrapped: string;
104
+ salt: string;
105
+ }>;
106
+ /** 雙因子解包:pinSalt 內嵌自描述,salt1 取自 login 回應(與 jr1w 同形);任何不符回 null,不拋。 */
107
+ export declare function unwrapNoteKeyDual(cfg: NoteCryptoConfig, wrapped: string, passphrase: string, pin: string, salt1Hex: string): Promise<CryptoKey | null>;
108
+ export declare function encryptWithKey(key: CryptoKey, plaintext: string, aad: string): Promise<string>;
109
+ export declare function decryptWithKey(key: CryptoKey, payload: Uint8Array, aad: string): Promise<string | null>;
110
+ export declare function encryptAttach(cfg: NoteCryptoConfig, key: CryptoKey, plaintext: string, aad: string): Promise<string>;
111
+ export declare function decryptAttach(cfg: NoteCryptoConfig, key: CryptoKey, cipher: string, aad: string): Promise<string | null>;
112
+ export declare function encryptLocal(cfg: NoteCryptoConfig, key: CryptoKey, plaintext: string, aad: string): Promise<string>;
113
+ export declare function decryptLocal(cfg: NoteCryptoConfig, key: CryptoKey, cipher: string, aad: string): Promise<string | null>;
114
+ /** 綁定當下生成:random 256-bit AES-GCM key。必須 extractable(包裹靠 exportKey)。 */
115
+ export declare function generateNoteKey(): Promise<CryptoKey>;
116
+ /** 登入憑證:PH1 = SHA-256(pass)——pass 明文從此不過線(伺服器存 PH2)。 */
117
+ export declare function ph1Of(passphrase: string): Promise<string>;
118
+ /** 復原種子:32 bytes hex,只顯示一次;伺服器只收 SHA-256。 */
119
+ export declare function generateRecToken(): string;
120
+ export declare function recTokenHash(recToken: string): Promise<string>;
121
+ /**
122
+ * 共用包裹核心(收口):prefix + b64(iv[12] ‖ GCM(KEK, hex(noteKey), aad))。
123
+ * 鹽外置家族字首家系清點(七呼叫端):jr1w.(wrapNoteKey)/jrsw.
124
+ *(wrapNoteKeyShare)/jr3w.(wrapNoteKey3)/jr4w.(wrapNoteKey4)/jr3s.
125
+ *(wrapNoteKeyShare3)/jr1l.(storeLocalWrap)/jr1r.+jr1w.(wrapNoteKeyWithRecToken——
126
+ * HKDF 世代專用前綴+AAD 'notekey-rec'、未配置世代 legacy 面=cfg.wrap+AAD 'notekey')
127
+ * 同一本體;KDF 差異在呼叫端 KEK、payload 形狀(salt 外置 vs 內嵌)由各家族維持。
128
+ * prefix null/undefined=未配置拒絕(呼叫端亦可先行拒絕,帶自家錯誤碼形;雙防線)。
129
+ */
130
+ export declare function sealNoteKey(prefix: string | null | undefined, noteKey: CryptoKey, kek: CryptoKey, aad: string): Promise<string>;
131
+ /**
132
+ * 共用解包核心(收口):任何不符——前綴、base64 形、payload 長度、
133
+ * GCM(金鑰/AAD/密文損壞)、rawHex 長度/hex 形——恆回 null 不拋。
134
+ * 修正紀錄:atob 對非法字元會拋、原 docstring「不拋」與實作不符——
135
+ * 本體吞收 null(公開原語誠實契約;呼叫端 unwrap 家族 try/catch 是雙防線非依賴面)。
136
+ * 家族嚴格度收口實況(校正+殘餘校正;
137
+ * 七呼叫端帳同步——鹽外置家族字首家系清點,6 點帳已過期):
138
+ * 本體嚴格 92B 終結七呼叫端=note-crypto 四點(unwrapNoteKey=jr1w/
139
+ * unwrapNoteKeyShare=jrsw/unwrapNoteKeyWithRecToken(legacy 面 jr1w./HKDF 面 jr1r.)
140
+ * /loadLocalWrap=jr1l.)
141
+ * +argon2 三點(unwrapNoteKey3=jr3w/unwrapNoteKey4=jr4w/unwrapNoteKeyShare3=jr3s;
142
+ * own 92B 檢已隨收口摘除);
143
+ * 鹽內嵌 3 點(jr2w/jr3d/jr1p)own 108B+rawHex 形檢——與本體同嚴格度、刻意不經本體
144
+ * (pinSalt 前綴不在鹽外置形契約內)。維護本體時鹽內嵌三族不隨行。
145
+ */
146
+ export declare function openNoteKey(wrapped: string, kek: CryptoKey, aad: string, prefix: string): Promise<CryptoKey | null>;
147
+ /** passphrase 包裹:salt 隨機 16 bytes(hex 存於 users.salt)。 */
148
+ export declare function wrapNoteKey(cfg: NoteCryptoConfig, noteKey: CryptoKey, passphrase: string): Promise<{
149
+ wrapped: string;
150
+ salt: string;
151
+ }>;
152
+ export declare function unwrapNoteKey(cfg: NoteCryptoConfig, wrapped: string, passphrase: string, saltHex: string): Promise<CryptoKey | null>;
153
+ /** 分享包裹:salt 隨機 16 bytes(hex 由呼叫端存 shares.salt)。 */
154
+ export declare function wrapNoteKeyShare(cfg: NoteCryptoConfig, noteKey: CryptoKey, sharePass: string): Promise<{
155
+ wrapped: string;
156
+ salt: string;
157
+ }>;
158
+ /** 分享解包:salt 取自 GET /shares/:hash 回應(與 jr1w 同形);任何不符回 null,不拋。 */
159
+ export declare function unwrapNoteKeyShare(cfg: NoteCryptoConfig, wrapped: string, sharePass: string, saltHex: string): Promise<CryptoKey | null>;
160
+ export declare function wrapNoteKeyWithRecToken(cfg: NoteCryptoConfig, noteKey: CryptoKey, recToken: string, identity: string): Promise<string>;
161
+ export declare function unwrapNoteKeyWithRecToken(cfg: NoteCryptoConfig, wrapped: string, recToken: string, identity: string): Promise<CryptoKey | null>;
162
+ /**
163
+ * 綁定請求的金鑰包裹三件套(bind 用)+ recovery_token_hash。
164
+ * 兩段 600k PBKDF2 相互獨立(pass 鹽隨機、rec 鹽=recSaltPrefix‖identity 定值)=並行
165
+ *(node 實測 133-152ms→68-84ms,與閘執行帳同源)。
166
+ * AES-GCM derive 佔 600k 迴圈 99%,兩段 wrapped 各自隨機 iv(pass 段另隨機鹽 16B;rec 腿
167
+ * KEK 鹽定值、回傳 salt 來自 pass 腿 w1)——逐位等值帳由
168
+ * 「兩段各自與 wrapNoteKey 直呼相等」承載。
169
+ */
170
+ export declare function buildBindPayload(cfg: NoteCryptoConfig, noteKey: CryptoKey, passphrase: string, recToken: string, identity: string): Promise<{
171
+ wrapped: string;
172
+ salt: string;
173
+ wrappedRec: string;
174
+ recTokenHash: string;
175
+ }>;
176
+ /**
177
+ * 本機包裹:KEK = deriveGuestKey(identity 派生、passphrase-free——
178
+ * session 期免重打密語的機制原樣),payload 改走專用前綴 cfg.wrapLocal(jr1l.)。
179
+ * 舊實作借用 cfg.wrap(jr1w. passphrase 包裹前綴)寫本機包裹=「一個前綴一份契約」
180
+ * 的第二違例(wrappedRec 專用前綴歸 v0.2.0 世代收口):本機包裹的 KDF 與 caller 不同、
181
+ * 儲存面(localStorage)不同、生命週期不同(clearLocalWrap 隨時摘除)——共前綴讓
182
+ * 「本機字串是否可能誤入 server 包裹欄」對帳失真。新前綴=讀舊寫新自癒:
183
+ * loadLocalWrap 先試 jr1l.(本體嚴格面),命中舊形(jr1w.)回落解密後即重包
184
+ * cfg.wrapLocal 回寫(自癒一次),寫入恆 jr1l.。
185
+ */
186
+ export declare function storeLocalWrap(cfg: NoteCryptoConfig, identity: string, noteKey: CryptoKey): Promise<void>;
187
+ export declare function clearLocalWrap(cfg: NoteCryptoConfig, identity: string): void;
188
+ export interface HeldKey {
189
+ get(): CryptoKey | null;
190
+ set(key: CryptoKey | null): void;
191
+ }
192
+ export declare function makeHeldKey(): HeldKey;
193
+ export interface IdentityProvider {
194
+ /** 當前身份字串(guest 派生與本機包裹的命名空間,如 account_id)。 */
195
+ current(): string;
196
+ }
197
+ /** 加密一則:綁定時代持有 noteKey → cipherBound;否則 guest key → cipherGuest
198
+ * (guest 前綴選配:未配置=ERR_GUEST_NOT_CONFIGURED,bound 路徑不受牽連)。
199
+ * held 為空時先試本機包裹(reload / PWA 續存期免重打密語)。 */
200
+ export declare function encryptNote(cfg: NoteCryptoConfig, held: HeldKey, plaintext: string, aad: string, idp: IdentityProvider): Promise<string>;
201
+ /** 解密一則:按前綴選鑰匙;無前綴 = 舊版明文(相容層)原樣返回(guest 前綴未配置時
202
+ * 不明字串一律 null=guest 家族整面拒絕,跨前綴鐵律不降級);失敗回 null。 */
203
+ export declare function decryptNote(cfg: NoteCryptoConfig, held: HeldKey, cipher: string, aad: string, idp: IdentityProvider): Promise<string | null>;