@tacet-ink/journal-core 0.3.2 → 0.3.4
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 +136 -36
- package/dist/client/bip39.d.ts +33 -0
- package/dist/client/bip39.js +12 -0
- package/dist/client/vault-error.d.ts +4 -1
- package/dist/client/vault-error.js +9 -2
- package/dist/client/vault.d.ts +94 -15
- package/dist/client/vault.js +155 -27
- package/dist/server/ratelimit.d.ts +1 -0
- package/dist/server/ratelimit.js +1 -0
- package/package.json +1 -6
- package/scripts/prepare-core-pkg.cjs +0 -74
- package/scripts/verify-core-crypto.ts +0 -3377
- package/src/client/argon-auto.ts +0 -224
- package/src/client/argon-worker.ts +0 -147
- package/src/client/argon2.ts +0 -532
- package/src/client/bip39.ts +0 -145
- package/src/client/keys.ts +0 -27
- package/src/client/note-crypto.ts +0 -719
- package/src/client/pinlock.ts +0 -110
- package/src/client/vault-error.ts +0 -51
- package/src/client/vault.ts +0 -486
- package/src/client/vendor/hash-wasm-argon2.ts +0 -27
- package/src/client/wordlist.ts +0 -264
- package/src/index.ts +0 -104
- package/src/server/auth.ts +0 -225
- package/src/server/cors.ts +0 -22
- package/src/server/env.ts +0 -13
- package/src/server/hash.ts +0 -28
- package/src/server/ladder.ts +0 -76
- package/src/server/ratelimit.ts +0 -123
package/src/client/argon2.ts
DELETED
|
@@ -1,532 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* argon2.ts — Argon2id 包裹原語(jr3w./jr3d.,安全路線第 2 步,2026-09-14)。
|
|
3
|
-
*
|
|
4
|
-
* 動機(安全路線定案):離線 oracle 的戰利品是 wrapped(/auth/login 回應帶 wrapped+salt,
|
|
5
|
-
* 拿到一次回應即可永久離線猜)。PBKDF2-SHA256 600k 對 GPU 友善;Argon2id 的記憶體困難
|
|
6
|
-
* 讓每猜成本上升一至兩個數量級,oracle 實際價值大幅中和。OPAQUE 降級為後續精益求精。
|
|
7
|
-
*
|
|
8
|
-
* 帶內版本化契約(不變量 5):升級 KDF = 換新前綴,禁原地改語意。
|
|
9
|
-
* jr3w. 單因子:結構與 jr1w. 同構(salt 獨立存 users.salt;wrapped = prefix + b64(iv[12] ‖ ct),
|
|
10
|
-
* ct = AES-GCM(KEK, hex(noteKey), aad='notekey')),只換 KEK 派生:
|
|
11
|
-
* KEK = Argon2id(pass, salt, m=64MiB, t=3, p=1, tag=32B) → AES-GCM key。
|
|
12
|
-
* jr3d. 雙因子(jr2w. 後繼):payload 同 jr2w.(pinSalt[16] ‖ iv[12] ‖ GCM(KEK2, hex(noteKey), aad='notekey2')),
|
|
13
|
-
* KEK2 = HKDF-SHA256( ikm = Argon2id(pass, salt1)[32] ‖ Argon2id(pinNorm, pinSalt3Prefix‖hex(pinSalt))[32],
|
|
14
|
-
* salt = pinSalt, info = 'journal-kek2-v1:' + wrapDual3, L = 32 )。
|
|
15
|
-
* 兩 payload 長度相同(皆 108B):客戶端按前綴分流(jr3w.→單因子、jr3d.→雙因子),形狀互斥由
|
|
16
|
-
* AAD 與長度把關,跨前綴呼叫一律回 null(unwrap 有前綴守衛,驗證閘有跨協議斷言)。
|
|
17
|
-
*
|
|
18
|
-
* 雙載體(同一 spec 兩端一致,交叉驗證逐位元一致):
|
|
19
|
-
* - node(驗證閘/prod smoke):node:crypto.argon2(24.7.0 落地版,26.8.1 實測)
|
|
20
|
-
* - 瀏覽器:hash-wasm argon2id(純 wasm 內嵌 base64,零網路請求,4.12.0 實測 64MiB t=3 ≈ 180ms)
|
|
21
|
-
* 標準向量:RFC 9106 無 secret/associated-data Argon2id(t=3, m=32, p=4, pwd=32B 0x01, salt=16B 0x00)
|
|
22
|
-
* = 72d2a36fd5c266bcc96121b24937bc253338cdfcbd273713655748c54b4dd503(兩載體實測 MATCH)。
|
|
23
|
-
*
|
|
24
|
-
* 遷移契約(禁強制重置):既有 jr1w./jr2w. 帳戶在下次成功 unwrap 後由客戶端靜默
|
|
25
|
-
* re-wrap 推 jr3w./jr3d.(手上有 noteKey、同一 noteKey 重包,資料零換鑰);舊前綴照解。
|
|
26
|
-
*
|
|
27
|
-
* ⚠️ 鐵律:unwrap 輸出 noteKey 一律 extractable=true(要能再包裹);KEK/KEK2 nonextractable。
|
|
28
|
-
* ⚠️ deriveArgon2id 禁 fallback(零知識):無載體即 throw,禁降級 PBKDF2 或 zeros 兜底。
|
|
29
|
-
* ⚠️ wrappedRec(復原套件)世代化:KEK_rec 派生世代(HKDF,recKekHkdf 配置面)
|
|
30
|
-
* +專用前綴 jr1r.(wrapRec)落地在 note-crypto.ts;本檔 recToken 256-bit 實體因子、
|
|
31
|
-
* KEK 強度無意義的原判不變(世代收的是派生域+契約面分離)。檢查對齊收口:wrapRec+
|
|
32
|
-
* recKekHkdf 兩欄一體(部分配置=ERR_REC_CFG_PARTIAL 拒寫);HKDF 世代前綴面專屬 jr1r.
|
|
33
|
-
* +AAD 'notekey-rec',jr1w. 前綴回歸僅 carrying passphrase-PBKDF2 派生(未配置世代
|
|
34
|
-
* wrappedRec 續走 jr1w.+PBKDF2+'notekey')。
|
|
35
|
-
*/
|
|
36
|
-
|
|
37
|
-
import { normalizePin, normalizePassphrase } from './note-crypto.ts';
|
|
38
|
-
import {
|
|
39
|
-
b64, unb64, toHex, hexToBytes, decryptWithKey, importAesGcm, openNoteKey, sealNoteKey,
|
|
40
|
-
} from './note-crypto.ts';
|
|
41
|
-
|
|
42
|
-
/** KDF 派生輸入正規化收口(mirror normalizePin 先例):v3 入口 = normalizePassphrase(pass);raw 世代入口 = 原密語。
|
|
43
|
-
* 正規化永不進共用派生本體:deriveKekArgon/deriveKek2Argon 吃的是「派生輸入」——
|
|
44
|
-
* raw 契約(jr3d./jr2w.…)不經此面(帶內版本化「契約面永不變」的結構保證)。
|
|
45
|
-
* 三面單一真相:本體**委派** normalizePassphrase(語意唯一真相在
|
|
46
|
-
* note-crypto.ts 本體,本函式只是入口收口位命名)——禁自帶 .normalize 實作(閘形錨咬住); */
|
|
47
|
-
function deriveInput(passphrase: string): string {
|
|
48
|
-
return normalizePassphrase(passphrase);
|
|
49
|
-
}
|
|
50
|
-
|
|
51
|
-
/** RFC 9106 無 secret/ad 標準向量(Argon2id v=0x13, t=3, m=32, p=4, T=32, pwd=32B 0x01, salt=16B 0x00)。 */
|
|
52
|
-
export const ARGON_RFC9106_EXPECTED =
|
|
53
|
-
'72d2a36fd5c266bcc96121b24937bc253338cdfcbd273713655748c54b4dd503';
|
|
54
|
-
|
|
55
|
-
/** RFC 9106 KAT 參數單一真相(verifyArgonKat/scripts/generate-vectors.ts 寫 kat.json/閘 [26] 對帳共用)。
|
|
56
|
-
* tagLen=派生本體固定輸出長(ARGON_TAG_LEN);pwd=passwordLen 位元組 passwordByte、salt=saltLen 位元組 0x00。 */
|
|
57
|
-
export const ARGON_RFC9106_PARAMS = Object.freeze({
|
|
58
|
-
memoryKib: 32, iterations: 3, parallelism: 4, tagLen: 32, passwordByte: 0x01, passwordLen: 32, saltLen: 16,
|
|
59
|
-
} as const);
|
|
60
|
-
|
|
61
|
-
/**
|
|
62
|
-
* PH1 v2 固定域鹽(登入憑證 Argon2id 派生,2026-09-10 安全路線第 4 步)。
|
|
63
|
-
*
|
|
64
|
-
* 固定鹽是被迫設計:per-user 鹽會摧毀 PH2 UNIQUE(同一密語必須恒生同一 ph2,
|
|
65
|
-
* 幽靈帳號機制與「同密語不可開第二帳戶」都靠它)。帶內版本化:改參數 = 換鹽尾碼
|
|
66
|
-
* (v2/v3…)重遷移,禁原地改語意。實長 12B(RFC 9106 鹽最小 8B;Task 0 探針實證)。
|
|
67
|
-
*/
|
|
68
|
-
export const PH1_V2_SALT = 'tacet-ph1-v1';
|
|
69
|
-
|
|
70
|
-
/**
|
|
71
|
-
* PH1 v2:登入憑證 = Argon2id(pass, 固定域鹽, m=64MiB, t=3, p=1) → hex64。
|
|
72
|
-
*
|
|
73
|
-
* 動機:DB 全洩後 ph2 = sha256(ph1) 是離線爆破讀日記的最後一個快雜湊面(候選密語
|
|
74
|
-
* 重算 sha256(sha256(g)) 對 ph2 命中即 g 是密語)。ph1 改 Argon2id 派生後每猜成本
|
|
75
|
-
* ×10⁴-10⁶。參數沿 jr3w. 同一組常數(單一碼路);無載體即 throw(禁 fallback 鐵律)。
|
|
76
|
-
*/
|
|
77
|
-
export async function derivePh1Argon(passphrase: string, saltArg?: Uint8Array): Promise<string> {
|
|
78
|
-
// PH1 鹽可注入:預設 PH1_V2_SALT 舊值=tacet 零行為變更;
|
|
79
|
-
// 其他產品可自選鹽但 per-product 恆固定(PH2 UNIQUE 約束——per-user 鹽會摧毀它)。
|
|
80
|
-
return toHex(await deriveArgon2id(
|
|
81
|
-
passphrase,
|
|
82
|
-
saltArg ?? new TextEncoder().encode(PH1_V2_SALT),
|
|
83
|
-
ARGON_MEMORY_KIB,
|
|
84
|
-
ARGON_ITERATIONS,
|
|
85
|
-
ARGON_PARALLELISM,
|
|
86
|
-
));
|
|
87
|
-
}
|
|
88
|
-
|
|
89
|
-
/**
|
|
90
|
-
* PH1 v3 鹽(密語正規化 v3 世代):'tacet-ph1-v2'——鹽域版本代沿
|
|
91
|
-
* PH1_V2_SALT(常數名='tacet-ph1-v1')既有先例模式:常數名=PH1 規格代、鹽域值=鹽域代。
|
|
92
|
-
* 鹽域世代分離:若續用 v2 鹽域,v3 世代(normalizePassphrase 後 'PASS'→'PASS')與
|
|
93
|
-
* v2 世代 raw 'PASS' 會派生出同一 ph2=跨世代帳戶空間混合;新鹽域把跨世代 identity
|
|
94
|
-
* 問題留在遷移層查表(v2-first 三腿查表職責),核心層零承擔。
|
|
95
|
-
* ⚠️ 鹽撞位警告(NFKC-effective 帳):`PH1_V3_SALT` 禁當 `derivePh1Argon` 的
|
|
96
|
-
* `saltArg` 餵入——v3 派生輸入吃 normalizePassphrase,同鹽域值錯面餵入=NFKC-effective
|
|
97
|
-
* 密語與 v2 raw 密語同值撞 ph2。fork 對兩面自選鹽時兩值必須互異(README PH1 節同款警告)。
|
|
98
|
-
*/
|
|
99
|
-
export const PH1_V3_SALT = 'tacet-ph1-v2';
|
|
100
|
-
|
|
101
|
-
/**
|
|
102
|
-
* PH1 v3:登入憑證 = Argon2id(normalizePassphrase(pass), 固定域鹽, m=64MiB, t=3, p=1) → hex64。
|
|
103
|
-
* 正規化收口在入口(密語 v3 契約 NFKC-only——normalizePassphrase 檔頭三面單一真相);
|
|
104
|
-
* 鹽可注入(saltArg 慣例:預設 PH1_V3_SALT;per-product 恆固定=PH2 UNIQUE 約束同 v2 母型)。
|
|
105
|
-
* 參數沿 jr3w./PH1 v2 同一組常數(單一碼路);無載體即 throw(禁 fallback 鐵律)。
|
|
106
|
-
* v2/legacy 契約面零動:derivePh1Argon 不吃正規化(舊世代 raw 派生,帶內版本化)。
|
|
107
|
-
*/
|
|
108
|
-
export async function derivePh1ArgonV3(passphrase: string, saltArg?: Uint8Array): Promise<string> {
|
|
109
|
-
return toHex(await deriveArgon2id(
|
|
110
|
-
deriveInput(passphrase),
|
|
111
|
-
saltArg ?? new TextEncoder().encode(PH1_V3_SALT),
|
|
112
|
-
ARGON_MEMORY_KIB,
|
|
113
|
-
ARGON_ITERATIONS,
|
|
114
|
-
ARGON_PARALLELISM,
|
|
115
|
-
));
|
|
116
|
-
}
|
|
117
|
-
|
|
118
|
-
/** Argon2id 參數(jr3w./jr3d. pass 段;前綴即版本,參數寫死本模組)。 */
|
|
119
|
-
export const ARGON_MEMORY_KIB = 65536; // 64 MiB(手機實測基準;瀏覽器 wasm ~180ms)
|
|
120
|
-
export const ARGON_ITERATIONS = 3; // t
|
|
121
|
-
export const ARGON_PARALLELISM = 1; // p(瀏覽器 wasm 無平行收益,定 1 兩端一致)
|
|
122
|
-
export const ARGON_TAG_LEN = 32; // 256-bit AES key
|
|
123
|
-
|
|
124
|
-
/** 雙因子 pin 段 Argon2id 參數(PIN 弱 → 記憶體補償;與 pass 段同級)。 */
|
|
125
|
-
export const ARGON_PIN_MEMORY_KIB = 65536;
|
|
126
|
-
export const ARGON_PIN_ITERATIONS = 3;
|
|
127
|
-
export const ARGON_PIN_PARALLELISM = 1;
|
|
128
|
-
|
|
129
|
-
const SALT_LEN = 16;
|
|
130
|
-
const DUAL_SALT_LEN = 16;
|
|
131
|
-
const DUAL_IV_LEN = 12;
|
|
132
|
-
|
|
133
|
-
/** jr3w 家族前綴(各 fork config 注入;未配置 = jr3w API 拒絕,兄弟 fork 行為不變)。 */
|
|
134
|
-
export interface Argon3Config {
|
|
135
|
-
/** 單因子包裹前綴,如 'jr3w.'。未配置 = 拒絕。 */
|
|
136
|
-
wrap3?: string;
|
|
137
|
-
/** 雙因子包裹前綴,如 'jr3d.'。未配置 = 拒絕。 */
|
|
138
|
-
wrapDual3?: string;
|
|
139
|
-
/** 雙因子 pin 段 Argon2id salt 前綴,如 'tacet-note-pin3:'(拼接 hex(pinSalt) 後整串當 salt)。 */
|
|
140
|
-
pinSalt3Prefix?: string;
|
|
141
|
-
/** 分享包裹前綴(jr3s.,單篇分享連結 Argon2id 版)。未配置 = 分享 API 拒絕(兄弟 fork 行為不變)。 */
|
|
142
|
-
wrapShare3?: string;
|
|
143
|
-
/** 密語正規化 v3 世代單因子包裹前綴(jr4w.,KEK=Argon2id(normalizePassphrase(pass)))。未配置 = 拒絕。 */
|
|
144
|
-
wrap4?: string;
|
|
145
|
-
/** 密語正規化 v3 世代雙因子包裹前綴(jr4d.,pass 段正規化+PIN 段照舊)。未配置 = 拒絕。 */
|
|
146
|
-
wrapDual4?: string;
|
|
147
|
-
}
|
|
148
|
-
|
|
149
|
-
export interface HashWasmArgon2id {
|
|
150
|
-
(params: {
|
|
151
|
-
password: string | Uint8Array;
|
|
152
|
-
salt: Uint8Array;
|
|
153
|
-
iterations: number;
|
|
154
|
-
parallelism: number;
|
|
155
|
-
memorySize: number;
|
|
156
|
-
hashLength: number;
|
|
157
|
-
outputType: 'binary' | 'hex' | 'encoded';
|
|
158
|
-
}): Promise<Uint8Array | string>;
|
|
159
|
-
}
|
|
160
|
-
|
|
161
|
-
export type ArgonLoader = () => Promise<{ argon2id: HashWasmArgon2id }>;
|
|
162
|
-
|
|
163
|
-
let injectedLoader: ArgonLoader | null = null;
|
|
164
|
-
|
|
165
|
-
/** 載體注入(瀏覽器端由接線層指定 hash-wasm;node 端不注入走 node:crypto 原生)。 */
|
|
166
|
-
export function setArgonLoader(loader: ArgonLoader | null): void {
|
|
167
|
-
injectedLoader = loader;
|
|
168
|
-
}
|
|
169
|
-
|
|
170
|
-
/** 現行注入載體(唯讀查詢;未注入=null)。argon-auto.ts 據此判「呼叫端覆寫優先」——0.3.1 新增 export。 */
|
|
171
|
-
export function getArgonLoader(): ArgonLoader | null {
|
|
172
|
-
return injectedLoader;
|
|
173
|
-
}
|
|
174
|
-
|
|
175
|
-
/**
|
|
176
|
-
* Argon2id 派生 32B。雙載體:injectedLoader(瀏覽器 hash-wasm)優先;
|
|
177
|
-
* 無注入時走 node:crypto 原生(僅 node 執行環境可達)。皆不可得即 throw(禁 fallback)。
|
|
178
|
-
*/
|
|
179
|
-
async function deriveArgon2id(
|
|
180
|
-
password: string | Uint8Array,
|
|
181
|
-
salt: Uint8Array,
|
|
182
|
-
memoryKib: number,
|
|
183
|
-
passes: number,
|
|
184
|
-
parallelism: number,
|
|
185
|
-
): Promise<Uint8Array> {
|
|
186
|
-
if (injectedLoader) {
|
|
187
|
-
const mod = await injectedLoader();
|
|
188
|
-
const out = await mod.argon2id({
|
|
189
|
-
password,
|
|
190
|
-
salt,
|
|
191
|
-
iterations: passes,
|
|
192
|
-
parallelism,
|
|
193
|
-
memorySize: memoryKib,
|
|
194
|
-
hashLength: ARGON_TAG_LEN,
|
|
195
|
-
outputType: 'binary',
|
|
196
|
-
});
|
|
197
|
-
const bytes = typeof out === 'string' ? hexToBytes(out) : new Uint8Array(out);
|
|
198
|
-
if (bytes.length !== ARGON_TAG_LEN) throw new Error('ERR_ARGON2_TAGLEN');
|
|
199
|
-
return bytes;
|
|
200
|
-
}
|
|
201
|
-
const nodeCrypto = (globalThis as unknown as {
|
|
202
|
-
process?: { getBuiltinModule?: (id: string) => { argon2?: NodeArgonFn } | undefined };
|
|
203
|
-
}).process?.getBuiltinModule?.('node:crypto');
|
|
204
|
-
if (!nodeCrypto?.argon2) throw new Error('ERR_ARGON2_UNAVAILABLE');
|
|
205
|
-
return new Promise<Uint8Array>((resolve, reject) => {
|
|
206
|
-
nodeCrypto.argon2!(
|
|
207
|
-
'argon2id',
|
|
208
|
-
{
|
|
209
|
-
message: password,
|
|
210
|
-
nonce: salt,
|
|
211
|
-
memory: memoryKib,
|
|
212
|
-
passes,
|
|
213
|
-
parallelism,
|
|
214
|
-
tagLength: ARGON_TAG_LEN,
|
|
215
|
-
maxmem: 2 * 1024 * 1024 * 1024, // 2GiB:node 原生預設 1GiB 上限對 m+128*p*8 記帳過緊
|
|
216
|
-
},
|
|
217
|
-
(err: Error | null, key: Uint8Array) => (err ? reject(err) : resolve(new Uint8Array(key))),
|
|
218
|
-
);
|
|
219
|
-
});
|
|
220
|
-
}
|
|
221
|
-
|
|
222
|
-
type NodeArgonFn = (
|
|
223
|
-
algorithm: 'argon2d' | 'argon2i' | 'argon2id',
|
|
224
|
-
parameters: {
|
|
225
|
-
message: string | Uint8Array;
|
|
226
|
-
nonce: Uint8Array;
|
|
227
|
-
memory: number;
|
|
228
|
-
passes: number;
|
|
229
|
-
parallelism: number;
|
|
230
|
-
tagLength: number;
|
|
231
|
-
maxmem?: number;
|
|
232
|
-
},
|
|
233
|
-
callback: (err: Error | null, key: Uint8Array) => void,
|
|
234
|
-
) => void;
|
|
235
|
-
|
|
236
|
-
/** 標準向量 KAT:驗證當前載體的 Argon2id 實作正確性(驗證閘起手式)。 */
|
|
237
|
-
export async function verifyArgonKat(): Promise<boolean> {
|
|
238
|
-
const k = ARGON_RFC9106_PARAMS;
|
|
239
|
-
const raw = await deriveArgon2id(new Uint8Array(k.passwordLen).fill(k.passwordByte), new Uint8Array(k.saltLen), k.memoryKib, k.iterations, k.parallelism);
|
|
240
|
-
return toHex(raw) === ARGON_RFC9106_EXPECTED;
|
|
241
|
-
}
|
|
242
|
-
|
|
243
|
-
// ── jr3w. 單因子包裹 ──────────────────────────────────────────────────────────
|
|
244
|
-
|
|
245
|
-
async function argonKekFromRaw(raw: Uint8Array): Promise<CryptoKey> {
|
|
246
|
-
return crypto.subtle.importKey('raw', raw as BufferSource, { name: 'AES-GCM' }, false, ['encrypt', 'decrypt']);
|
|
247
|
-
}
|
|
248
|
-
|
|
249
|
-
/** KEK = Argon2id(pass, salt, m=64MiB, t=3, p=1) → AES-GCM key(nonextractable)。 */
|
|
250
|
-
export async function deriveKekArgon(password: string, salt: Uint8Array): Promise<CryptoKey> {
|
|
251
|
-
return argonKekFromRaw(await deriveArgon2id(password, salt, ARGON_MEMORY_KIB, ARGON_ITERATIONS, ARGON_PARALLELISM));
|
|
252
|
-
}
|
|
253
|
-
|
|
254
|
-
/** jr3w. 包裹:payload = iv[12] ‖ AES-GCM(KEK, hex(noteKey), aad='notekey')(鹽外置家族,嚴格 92B);salt 不入 payload,由呼叫端存 users.salt。 */
|
|
255
|
-
export async function wrapNoteKey3(cfg: Argon3Config, noteKey: CryptoKey, passphrase: string): Promise<{ wrapped: string; salt: string }> {
|
|
256
|
-
if (!cfg.wrap3) throw new Error('ERR_JR3W_NOT_CONFIGURED');
|
|
257
|
-
const salt = crypto.getRandomValues(new Uint8Array(SALT_LEN));
|
|
258
|
-
const kek = await deriveKekArgon(passphrase, salt);
|
|
259
|
-
const wrapped = await sealNoteKey(cfg.wrap3, noteKey, kek, 'notekey');
|
|
260
|
-
return { wrapped, salt: toHex(salt) };
|
|
261
|
-
}
|
|
262
|
-
|
|
263
|
-
/** jr3w. 解包:任何不符(前綴/salt 形/長度/AAD)回 null 不拋;成功 → noteKey extractable=true(鐵律 4)。
|
|
264
|
-
* 快速前綴快檢在場(deriveKekArgon 前——t_87ef62dd;unwrapNoteKey4 母型):錯前綴 junk 免付 Argon。
|
|
265
|
-
* 收口 openNoteKey:嚴格 92B+rawHex 形+hex fail-closed 全在共用核心;本函式只做
|
|
266
|
-
* 家族守衛(wrap3 未配置回 null)、salt1 hex-形檢查與 KEK 派生——payload 形是鹽外置家族(iv‖ct),
|
|
267
|
-
* own 92B 檢已隨收口摘除(jr3s 同;鹽內嵌族 own 108B 保留)。 */
|
|
268
|
-
export async function unwrapNoteKey3(cfg: Argon3Config, wrapped: string, passphrase: string, saltHex: string): Promise<CryptoKey | null> {
|
|
269
|
-
try {
|
|
270
|
-
if (!cfg.wrap3) return null;
|
|
271
|
-
if (!wrapped.startsWith(cfg.wrap3)) return null; // 快速前綴快檢(t_87ef62dd):錯前綴 junk 免付 64 MiB Argon 成本
|
|
272
|
-
const salt = hexToBytes(saltHex);
|
|
273
|
-
if (salt.length !== SALT_LEN || !/^[0-9a-f]{32}$/.test(saltHex)) return null;
|
|
274
|
-
const kek = await deriveKekArgon(passphrase, salt);
|
|
275
|
-
return await openNoteKey(wrapped, kek, 'notekey', cfg.wrap3);
|
|
276
|
-
} catch {
|
|
277
|
-
return null;
|
|
278
|
-
}
|
|
279
|
-
}
|
|
280
|
-
|
|
281
|
-
// ── jr4w. 單因子包裹(密語正規化 v3 世代) ────────────────────
|
|
282
|
-
//
|
|
283
|
-
// 帶內版本化:密語 KEK 輸入契約改變(raw → normalizePassphrase NFKC-only)=新前綴,
|
|
284
|
-
// 舊前綴契約面永不變——jr4w. 的函式體與 jr3w. 同構(openNoteKey 共用核心、payload 嚴格
|
|
285
|
-
// 92B、AAD 'notekey'、嚴格長度全在 openNoteKey 本體),唯一差異=入口吃 normalizePassphrase(pass)
|
|
286
|
-
// 與 cfg.wrap4 前綴。舊 payload(jr1w./jr3w.)永遠可解=raw 派生;新 payload 只由 jr4w. 產生。
|
|
287
|
-
// pinSaltPrefix 沿用 cfg 既有欄(Argon3 家族 PIN 鹽域,非 jr3d. 專屬)。
|
|
288
|
-
|
|
289
|
-
/** jr4w. 包裹(v3 密語):payload = iv[12] ‖ GCM(KEK, hex(noteKey), aad='notekey')(鹽外置家族,嚴格 92B);salt 不入 payload,呼叫端存 users.salt。 */
|
|
290
|
-
export async function wrapNoteKey4(cfg: Argon3Config, noteKey: CryptoKey, passphrase: string): Promise<{ wrapped: string; salt: string }> {
|
|
291
|
-
if (!cfg.wrap4) throw new Error('ERR_JR4W_NOT_CONFIGURED');
|
|
292
|
-
const salt = crypto.getRandomValues(new Uint8Array(SALT_LEN));
|
|
293
|
-
const kek = await deriveKekArgon(deriveInput(passphrase), salt);
|
|
294
|
-
const wrapped = await sealNoteKey(cfg.wrap4, noteKey, kek, 'notekey');
|
|
295
|
-
return { wrapped, salt: toHex(salt) };
|
|
296
|
-
}
|
|
297
|
-
|
|
298
|
-
/** jr4w. 解包(v3 密語):任何不符(前綴/salt 形/長度/AAD)回 null 不拋;成功 → noteKey extractable=true(鐵律 4)。
|
|
299
|
-
* 家族守衛(wrap4 未配置回 null)+快速前綴快檢(錯前綴 junk 免付 64 MiB Argon)+salt hex-形檢+
|
|
300
|
-
* KEK 派生(normalizePassphrase 收口在入口)——payload 形檢在 openNoteKey 本體(鹽外置家族:嚴格 92B+
|
|
301
|
-
* rawHex 形+hex fail-closed)。 */
|
|
302
|
-
export async function unwrapNoteKey4(cfg: Argon3Config, wrapped: string, passphrase: string, saltHex: string): Promise<CryptoKey | null> {
|
|
303
|
-
try {
|
|
304
|
-
if (!cfg.wrap4) return null;
|
|
305
|
-
if (!wrapped.startsWith(cfg.wrap4)) return null; // 快速前綴快檢:錯前綴 junk 免付 64 MiB Argon 成本
|
|
306
|
-
const salt = hexToBytes(saltHex);
|
|
307
|
-
if (salt.length !== SALT_LEN || !/^[0-9a-f]{32}$/.test(saltHex)) return null;
|
|
308
|
-
const kek = await deriveKekArgon(deriveInput(passphrase), salt);
|
|
309
|
-
return await openNoteKey(wrapped, kek, 'notekey', cfg.wrap4);
|
|
310
|
-
} catch {
|
|
311
|
-
return null;
|
|
312
|
-
}
|
|
313
|
-
}
|
|
314
|
-
|
|
315
|
-
// ── jr3d. 雙因子合鑰(PIN 第二因子,jr2w. 的 Argon2id 版) ─────────────────────
|
|
316
|
-
//
|
|
317
|
-
// KEK2 = HKDF-SHA256( ikm = Argon2id(pass, salt1)[32] ‖ Argon2id(pinNorm, prefix‖hex(pinSalt))[32],
|
|
318
|
-
// salt = pinSalt, info = 'journal-kek2-v1:' + wrapDual3, L = 32 )
|
|
319
|
-
// info 字串與 jr2w. 家族同名(journal-kek2-v1):HKDF 合成步驟同構,兩家族 KEK2 值
|
|
320
|
-
// 因 KDF bits 不同而天然互斥(驗證閘有跨家族隔離斷言)。
|
|
321
|
-
// 兩段 Argon2 bits 用完即棄;KEK2 import 當下 nonextractable;輸出 noteKey extractable=true。
|
|
322
|
-
// HKDF info 由呼叫端帶入(hkdfInfo 參數):jr3d. 預設域 = 'journal-kek2-v1:' + cfg.wrapDual3——
|
|
323
|
-
// jr4d. 必帶自有域 'journal-kek2-v1:' + cfg.wrapDual4,KEK2 域分離由 info 承載(兩入參數同輸入
|
|
324
|
-
// 而派生域不同——同鹽同段 bits 在 NFKC-effective 密語下會同 KEK2;域即世代空間)。
|
|
325
|
-
|
|
326
|
-
async function deriveKek2Argon(
|
|
327
|
-
cfg: Argon3Config,
|
|
328
|
-
passphrase: string,
|
|
329
|
-
pinNorm: string,
|
|
330
|
-
salt1: Uint8Array,
|
|
331
|
-
pinSalt: Uint8Array,
|
|
332
|
-
hkdfInfo: string,
|
|
333
|
-
): Promise<CryptoKey> {
|
|
334
|
-
if (!cfg.pinSalt3Prefix) throw new Error('ERR_JR3W_NOT_CONFIGURED');
|
|
335
|
-
// ⚠️ 瀏覽器 hash-wasm Argon2 禁並行:純 wasm Argon2id 共享記憶體池,
|
|
336
|
-
// 兩實例並行在部分引擎靜態直 throw——這裡故意保持串行(與 jr2w. PBKDF2 版
|
|
337
|
-
// deriveKek2 的 Promise.all 並行不同:那裡並行的前提是 crypto.subtle 原生派生)。
|
|
338
|
-
const passBits = await deriveArgon2id(passphrase, salt1, ARGON_MEMORY_KIB, ARGON_ITERATIONS, ARGON_PARALLELISM);
|
|
339
|
-
const pinBits = await deriveArgon2id(
|
|
340
|
-
pinNorm,
|
|
341
|
-
new TextEncoder().encode(cfg.pinSalt3Prefix + toHex(pinSalt)),
|
|
342
|
-
ARGON_PIN_MEMORY_KIB,
|
|
343
|
-
ARGON_PIN_ITERATIONS,
|
|
344
|
-
ARGON_PIN_PARALLELISM,
|
|
345
|
-
);
|
|
346
|
-
const ikm = new Uint8Array(passBits.byteLength + pinBits.byteLength);
|
|
347
|
-
ikm.set(passBits, 0);
|
|
348
|
-
ikm.set(pinBits, passBits.byteLength);
|
|
349
|
-
const hkdfBase = await crypto.subtle.importKey('raw', ikm as BufferSource, 'HKDF', false, ['deriveKey']);
|
|
350
|
-
return crypto.subtle.deriveKey(
|
|
351
|
-
{ name: 'HKDF', hash: 'SHA-256', salt: pinSalt as BufferSource, info: new TextEncoder().encode(hkdfInfo) as BufferSource },
|
|
352
|
-
hkdfBase,
|
|
353
|
-
{ name: 'AES-GCM', length: 256 },
|
|
354
|
-
false,
|
|
355
|
-
['encrypt', 'decrypt'],
|
|
356
|
-
);
|
|
357
|
-
}
|
|
358
|
-
|
|
359
|
-
/** jr3d. 包裹:payload = pinSalt[16] ‖ iv[12] ‖ GCM(KEK2, hex(noteKey), aad='notekey2');salt1 由呼叫端存 users.salt。 */
|
|
360
|
-
export async function wrapNoteKeyDual3(cfg: Argon3Config, noteKey: CryptoKey, passphrase: string, pin: string): Promise<{ wrapped: string; salt: string }> {
|
|
361
|
-
if (!cfg.wrapDual3 || !cfg.pinSalt3Prefix) throw new Error('ERR_JR3W_NOT_CONFIGURED');
|
|
362
|
-
const pinNorm = normalizePin(pin);
|
|
363
|
-
if (!pinNorm) throw new Error('ERR_PIN_EMPTY');
|
|
364
|
-
const salt1 = crypto.getRandomValues(new Uint8Array(SALT_LEN));
|
|
365
|
-
const pinSalt = crypto.getRandomValues(new Uint8Array(DUAL_SALT_LEN));
|
|
366
|
-
const kek2 = await deriveKek2Argon(cfg, passphrase, pinNorm, salt1, pinSalt, 'journal-kek2-v1:' + cfg.wrapDual3);
|
|
367
|
-
const iv = crypto.getRandomValues(new Uint8Array(DUAL_IV_LEN));
|
|
368
|
-
const ct = new Uint8Array(await crypto.subtle.encrypt(
|
|
369
|
-
{ name: 'AES-GCM', iv, additionalData: new TextEncoder().encode('notekey2') as BufferSource },
|
|
370
|
-
kek2,
|
|
371
|
-
new TextEncoder().encode(toHex(new Uint8Array(await crypto.subtle.exportKey('raw', noteKey)))) as BufferSource,
|
|
372
|
-
));
|
|
373
|
-
const payload = new Uint8Array(DUAL_SALT_LEN + DUAL_IV_LEN + ct.byteLength);
|
|
374
|
-
payload.set(pinSalt, 0);
|
|
375
|
-
payload.set(iv, DUAL_SALT_LEN);
|
|
376
|
-
payload.set(ct, DUAL_SALT_LEN + DUAL_IV_LEN);
|
|
377
|
-
return { wrapped: cfg.wrapDual3 + b64(payload), salt: toHex(salt1) };
|
|
378
|
-
}
|
|
379
|
-
|
|
380
|
-
/** jr3d. 解包:pinSalt 內嵌自描述,salt1 取自 login 回應;任何不符回 null,不拋。
|
|
381
|
-
* 嚴格檢查為鹽內嵌族自有——payload 嚴格 108B(pinSalt 16
|
|
382
|
-
* + iv 12+ct 80)+ decryptWithKey 後 rawHex hex 形檢查(hexToBytes fail-closed),
|
|
383
|
-
* 不經 openNoteKey 本體(鹽外置 92B 形);與 openNoteKey 同嚴格度。 */
|
|
384
|
-
export async function unwrapNoteKeyDual3(
|
|
385
|
-
cfg: Argon3Config,
|
|
386
|
-
wrapped: string,
|
|
387
|
-
passphrase: string,
|
|
388
|
-
pin: string,
|
|
389
|
-
salt1Hex: string,
|
|
390
|
-
): Promise<CryptoKey | null> {
|
|
391
|
-
try {
|
|
392
|
-
if (!cfg.wrapDual3 || !cfg.pinSalt3Prefix) return null;
|
|
393
|
-
if (!wrapped.startsWith(cfg.wrapDual3)) return null;
|
|
394
|
-
const salt1 = hexToBytes(salt1Hex);
|
|
395
|
-
if (salt1.length !== SALT_LEN || !/^[0-9a-f]{32}$/.test(salt1Hex)) return null;
|
|
396
|
-
const payload = unb64(wrapped.slice(cfg.wrapDual3.length));
|
|
397
|
-
// 嚴格長度:pinSalt(16) + iv(12) + ct(hex 字串 64B + GCM tag 16B) = 108B 固定
|
|
398
|
-
if (payload.length !== DUAL_SALT_LEN + DUAL_IV_LEN + 80) return null;
|
|
399
|
-
const pinSalt = payload.slice(0, DUAL_SALT_LEN);
|
|
400
|
-
const ivPrefixedCt = payload.slice(DUAL_SALT_LEN); // decryptWithKey 契約:payload = iv[12] ‖ ct
|
|
401
|
-
const pinNorm = normalizePin(pin);
|
|
402
|
-
if (!pinNorm) return null;
|
|
403
|
-
const kek2 = await deriveKek2Argon(cfg, passphrase, pinNorm, salt1, pinSalt, 'journal-kek2-v1:' + cfg.wrapDual3);
|
|
404
|
-
const rawHex = await decryptWithKey(kek2, ivPrefixedCt, 'notekey2');
|
|
405
|
-
if (!rawHex || rawHex.length !== 64 || !/^[0-9a-f]+$/.test(rawHex)) return null;
|
|
406
|
-
return importAesGcm(hexToBytes(rawHex), true); // extractable=true:要能再包裹(鐵律)
|
|
407
|
-
} catch {
|
|
408
|
-
return null;
|
|
409
|
-
}
|
|
410
|
-
}
|
|
411
|
-
|
|
412
|
-
// ── jr4d. 雙因子合鑰(密語正規化 v3 世代=jr3d. 的 v3 後繼) ────
|
|
413
|
-
//
|
|
414
|
-
// 帶內版本化:pass 段 KEK 輸入契約改變(raw → normalizePassphrase NFKC-only)=新前綴;
|
|
415
|
-
// PIN 段契約照舊(normalizePin:NFKC→trim→lowercase,pinlock/dual 面不動)。
|
|
416
|
-
// KEK2 合成步驟與 jr3d. 同構:deriveKek2Argon 共用本體(pass 段正規化收口在 wrap/unwrap
|
|
417
|
-
// 入口層——deriveInput 收口、正規化永不進共用派生函式,jr3d. raw 契約面零動如實不經測不變);
|
|
418
|
-
// payload 嚴格 108B+rawHex 形檢在 own 本體(鹽內嵌族 openNoteKey 不覆蓋);
|
|
419
|
-
// HKDF info 沿 'journal-kek2-v1:' 合成名+hkdfInfo 呼叫端帶入(域=jr4d.:KDF 互斥由 bits+info 域承載)。
|
|
420
|
-
|
|
421
|
-
/** jr4d. 包裹(v3 密語):payload = pinSalt[16] ‖ iv[12] ‖ GCM(KEK2, hex(noteKey), aad='notekey2');salt1 呼叫端存 users.salt。 */
|
|
422
|
-
export async function wrapNoteKeyDual4(cfg: Argon3Config, noteKey: CryptoKey, passphrase: string, pin: string): Promise<{ wrapped: string; salt: string }> {
|
|
423
|
-
if (!cfg.wrapDual4 || !cfg.pinSalt3Prefix) throw new Error('ERR_JR4D_NOT_CONFIGURED');
|
|
424
|
-
const pinNorm = normalizePin(pin);
|
|
425
|
-
if (!pinNorm) throw new Error('ERR_PIN_EMPTY'); // 空 PIN ≠ 未配置(語意分離)
|
|
426
|
-
const salt1 = crypto.getRandomValues(new Uint8Array(SALT_LEN));
|
|
427
|
-
const pinSalt = crypto.getRandomValues(new Uint8Array(DUAL_SALT_LEN));
|
|
428
|
-
const kek2 = await deriveKek2Argon(cfg, deriveInput(passphrase), pinNorm, salt1, pinSalt, 'journal-kek2-v1:' + cfg.wrapDual4);
|
|
429
|
-
const iv = crypto.getRandomValues(new Uint8Array(DUAL_IV_LEN));
|
|
430
|
-
const ct = new Uint8Array(await crypto.subtle.encrypt(
|
|
431
|
-
{ name: 'AES-GCM', iv, additionalData: new TextEncoder().encode('notekey2') as BufferSource },
|
|
432
|
-
kek2,
|
|
433
|
-
new TextEncoder().encode(toHex(new Uint8Array(await crypto.subtle.exportKey('raw', noteKey)))) as BufferSource,
|
|
434
|
-
));
|
|
435
|
-
const payload = new Uint8Array(DUAL_SALT_LEN + DUAL_IV_LEN + ct.byteLength);
|
|
436
|
-
payload.set(pinSalt, 0);
|
|
437
|
-
payload.set(iv, DUAL_SALT_LEN);
|
|
438
|
-
payload.set(ct, DUAL_SALT_LEN + DUAL_IV_LEN);
|
|
439
|
-
return { wrapped: cfg.wrapDual4 + b64(payload), salt: toHex(salt1) };
|
|
440
|
-
}
|
|
441
|
-
|
|
442
|
-
// jr4d. 解包(v3 密語):pinSalt 內嵌自描述,salt1 取自 login 回應;任何不符回 null,不拋。
|
|
443
|
-
// 嚴格檢查為鹽內嵌族自有(payload 嚴格 108B+rawHex 形檢,不經 openNoteKey 92B 鹽外置本體);
|
|
444
|
-
// pass 段正規化收口在入口(deriveInput=normalizePassphrase 同語意本體收口),pinNorm 與 jr3d. 同契約。
|
|
445
|
-
// KEK2 HKDF info='journal-kek2-v1:' + cfg.wrapDual4(自有域——jr3d/jr4d 兩入參數同輸入而域分離;
|
|
446
|
-
// NFKC-effective 密語下 bits 段同值,域由 info 承載——凍結 KAT 兩 blob 互解 null 承載)。
|
|
447
|
-
export async function unwrapNoteKeyDual4(
|
|
448
|
-
cfg: Argon3Config,
|
|
449
|
-
wrapped: string,
|
|
450
|
-
passphrase: string,
|
|
451
|
-
pin: string,
|
|
452
|
-
salt1Hex: string,
|
|
453
|
-
): Promise<CryptoKey | null> {
|
|
454
|
-
try {
|
|
455
|
-
if (!cfg.wrapDual4 || !cfg.pinSalt3Prefix) return null;
|
|
456
|
-
if (!wrapped.startsWith(cfg.wrapDual4)) return null;
|
|
457
|
-
const salt1 = hexToBytes(salt1Hex);
|
|
458
|
-
if (salt1.length !== SALT_LEN || !/^[0-9a-f]{32}$/.test(salt1Hex)) return null;
|
|
459
|
-
const payload = unb64(wrapped.slice(cfg.wrapDual4.length));
|
|
460
|
-
// 嚴格長度:pinSalt(16) + iv(12) + ct(hex 字串 64B + GCM tag 16B) = 108B 固定
|
|
461
|
-
if (payload.length !== DUAL_SALT_LEN + DUAL_IV_LEN + 80) return null;
|
|
462
|
-
const pinSalt = payload.slice(0, DUAL_SALT_LEN);
|
|
463
|
-
const ivPrefixedCt = payload.slice(DUAL_SALT_LEN); // decryptWithKey 契約:payload = iv[12] ‖ ct
|
|
464
|
-
const pinNorm = normalizePin(pin);
|
|
465
|
-
if (!pinNorm) return null;
|
|
466
|
-
const kek2 = await deriveKek2Argon(cfg, deriveInput(passphrase), pinNorm, salt1, pinSalt, 'journal-kek2-v1:' + cfg.wrapDual4);
|
|
467
|
-
const rawHex = await decryptWithKey(kek2, ivPrefixedCt, 'notekey2');
|
|
468
|
-
if (!rawHex || rawHex.length !== 64 || !/^[0-9a-f]+$/.test(rawHex)) return null;
|
|
469
|
-
return importAesGcm(hexToBytes(rawHex), true); // extractable=true:要能再包裹(鐵律)
|
|
470
|
-
} catch {
|
|
471
|
-
return null;
|
|
472
|
-
}
|
|
473
|
-
}
|
|
474
|
-
|
|
475
|
-
// ── jr3s. 分享包裹(分享連結 V2,安全路線商用前清單;jrsw. 的 Argon2id 版) ───
|
|
476
|
-
//
|
|
477
|
-
// KEK_share = Argon2id(sharePass, salt, m=64MiB, t=3, p=1, tag=32B)(與 jr3w. 同級參數),
|
|
478
|
-
// payload = iv[12] ‖ GCM(KEK_share, hex(noteKey), aad='notekey-share')(與 jrsw. 同構只換 KDF),
|
|
479
|
-
// wrapped = 'jr3s.' + b64(payload),嚴格 92B;salt 由呼叫端存 shares.salt(與 jr1w./jr3w. 同形)。
|
|
480
|
-
// AAD 沿用 'notekey-share':兩代分享包裹結構同構,跨代誤用由 KDF 差異與前綴守衛雙層把關。
|
|
481
|
-
// 帶內版本化:share KDF 升級 = 換新前綴(jrsw. → jr3s.),舊前綴語意不動;讀取端兩代並行,
|
|
482
|
-
// 建立端只收 jr3s.(關閉弱 KDF 建立面)。
|
|
483
|
-
// ⚠️ unwrap 輸出 noteKey 一律 extractable=true(要能解日記密文;鐵律 4)。
|
|
484
|
-
|
|
485
|
-
/** jr3s. 包裹:salt 隨機 16 bytes(hex 由呼叫端存 shares.salt)。 */
|
|
486
|
-
export async function wrapNoteKeyShare3(cfg: Argon3Config, noteKey: CryptoKey, sharePass: string): Promise<{ wrapped: string; salt: string }> {
|
|
487
|
-
if (!cfg.wrapShare3) throw new Error('ERR_JR3S_NOT_CONFIGURED');
|
|
488
|
-
const salt = crypto.getRandomValues(new Uint8Array(SALT_LEN));
|
|
489
|
-
const kek = await deriveKekArgon(sharePass, salt);
|
|
490
|
-
const wrapped = await sealNoteKey(cfg.wrapShare3, noteKey, kek, 'notekey-share');
|
|
491
|
-
return { wrapped, salt: toHex(salt) };
|
|
492
|
-
}
|
|
493
|
-
|
|
494
|
-
/** jr3s. 解包:salt 取自 GET /shares/:hash 回應(與 jr1w 同形);任何不符回 null,不拋。
|
|
495
|
-
* 快速前綴快檢在場(deriveKekArgon 前——t_87ef62dd;unwrapNoteKey4 母型):錯前綴 junk 免付 Argon。
|
|
496
|
-
* 收口 openNoteKey:鹽外置家族(payload 嚴格 92B+rawHex 形+hex fail-closed
|
|
497
|
-
* 全在共用核心);本函式只做家族守衛、salt hex-形檢查與 KEK 派生——own 92B 檢已隨收口摘除。 */
|
|
498
|
-
export async function unwrapNoteKeyShare3(cfg: Argon3Config, wrapped: string, sharePass: string, saltHex: string): Promise<CryptoKey | null> {
|
|
499
|
-
try {
|
|
500
|
-
if (!cfg.wrapShare3) return null;
|
|
501
|
-
if (!wrapped.startsWith(cfg.wrapShare3)) return null; // 快速前綴快檢(t_87ef62dd):錯前綴 junk 免付 64 MiB Argon 成本
|
|
502
|
-
const salt = hexToBytes(saltHex);
|
|
503
|
-
if (salt.length !== SALT_LEN || !/^[0-9a-f]{32}$/.test(saltHex)) return null;
|
|
504
|
-
const kek = await deriveKekArgon(sharePass, salt);
|
|
505
|
-
return await openNoteKey(wrapped, kek, 'notekey-share', cfg.wrapShare3);
|
|
506
|
-
} catch {
|
|
507
|
-
return null;
|
|
508
|
-
}
|
|
509
|
-
}
|
|
510
|
-
|
|
511
|
-
// ── ladder 重試鹽面:unwrapNoteKeyDual4WithSalt ────
|
|
512
|
-
//
|
|
513
|
-
// 為何同構再開一支:v3 遷移(登入即重遷)會旋轉 salt1(wrapNoteKeyDual4 落新值);
|
|
514
|
-
// login 舊形面帶回的 salt(v2 世代的 salt1)在遷移後已解不開 v3 新包裹──呼叫端
|
|
515
|
-
// (tacet loginAndUnwrap 的 jr4d. 腿)以「login 回應當下帶回的新 salt」重取入參
|
|
516
|
-
// 重試 unwrap 就是本變體唯一承載面。本體與 unwrapNoteKeyDual4 完全同構(同嚴格
|
|
517
|
-
// 檢查、同 HKDF info 域、同輸出契約)──開支出零差異;命名開新支是「重取鹽」的
|
|
518
|
-
// 語意承載位(呼叫端 grep 可審計),零新增實作面。
|
|
519
|
-
//
|
|
520
|
-
// 補記(家族嚴格度):salt1 形檢/payload 108B/rawHex 形檢全在
|
|
521
|
-
// unwrapNoteKeyDual4 本體(共用);本變體純語意薄身。
|
|
522
|
-
|
|
523
|
-
/** jr4d. 解包(重取 salt1 面):pinSalt 內嵌自描述,salt1 由呼叫端重取後帶入(ladder 遷移後的新値);任何不符回 null,不拋。 */
|
|
524
|
-
export async function unwrapNoteKeyDual4WithSalt(
|
|
525
|
-
cfg: Argon3Config,
|
|
526
|
-
wrapped: string,
|
|
527
|
-
passphrase: string,
|
|
528
|
-
pin: string,
|
|
529
|
-
salt1Hex: string,
|
|
530
|
-
): Promise<CryptoKey | null> {
|
|
531
|
-
return unwrapNoteKeyDual4(cfg, wrapped, passphrase, pin, salt1Hex);
|
|
532
|
-
}
|