@mrrisega/dsh-remote 0.6.0-beta.11 → 0.6.0-beta.2
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 +0 -30
- package/clients/dsh-remote/dsh-bridge.mjs +5 -305
- package/package.json +2 -2
- package/packages/dsh-remote-web/lib/client.js +178 -467
- package/packages/dsh-remote-web/lib/index.js +4 -115
- package/packages/dsh-remote-web/test/access-key-sessions.test.mjs +11 -81
- package/packages/dsh-remote-web/test/account-switch.test.mjs +1 -1
- package/packages/dsh-remote-web/test/remote-access-ui.test.mjs +2 -174
- package/packages/dsh-remote-web/test/settings-entry.test.mjs +5 -55
- package/packages/dsh-remote-web/test/uninstall-runtime.test.mjs +2 -2
- package/clients/dsh-remote/e2ee-client.mjs +0 -711
- package/clients/dsh-remote/e2ee-shim-script.js +0 -697
- package/clients/dsh-remote/e2ee-shim.mjs +0 -158
- package/clients/dsh-remote/mobile-adapter.mjs +0 -541
- package/clients/dsh-remote/test/bridge-html-inject.test.mjs +0 -110
- package/clients/dsh-remote/test/e2ee-bridge.test.mjs +0 -630
- package/clients/dsh-remote/test/e2ee-client.test.mjs +0 -365
- package/clients/dsh-remote/test/e2ee-shim.test.mjs +0 -507
- package/clients/dsh-remote/test/e2ee-wecrypto-parity.test.mjs +0 -200
- package/clients/dsh-remote/test/lib-e2ee-shim.mjs +0 -59
- package/clients/dsh-remote/test/lib-native-e2ee.mjs +0 -92
- package/clients/dsh-remote/test/mobile-adapter.test.mjs +0 -124
- package/packages/dsh-remote-web/test/e2ee-state.test.mjs +0 -141
- package/packages/dsh-remote-web/test/feedback-account-mine.test.mjs +0 -70
- package/packages/dsh-remote-web/test/password-reset.test.mjs +0 -429
|
@@ -1,365 +0,0 @@
|
|
|
1
|
-
#!/usr/bin/env node
|
|
2
|
-
/**
|
|
3
|
-
* e2ee-client.mjs 原语契约测试(纯函数/内存,不起真实进程):
|
|
4
|
-
* - MK 派生向量与 enterprise 参考实现(e2ee.js deriveE2eeMasterKey)一致(固定口令/盐 → 固定 hex);
|
|
5
|
-
* - 信封加解密往返 / 篡改拒绝 / nonce 重放拒绝 / ws 计数重放拒绝;
|
|
6
|
-
* - 参数端点容错(非 200 / 无 e2ee / enabled=false → disabled 不回退抛错);
|
|
7
|
-
* - 桥端控制通道握手(hello→ack、探针好/坏密钥)、http 明文编解码、ws 标签解析。
|
|
8
|
-
*
|
|
9
|
-
* 用法: node --test test/e2ee-client.test.mjs
|
|
10
|
-
*/
|
|
11
|
-
import assert from "node:assert/strict";
|
|
12
|
-
import http from "node:http";
|
|
13
|
-
import test from "node:test";
|
|
14
|
-
import {
|
|
15
|
-
DEFAULT_MK_KDF,
|
|
16
|
-
E2eeError,
|
|
17
|
-
E2eeService,
|
|
18
|
-
E2eeSession,
|
|
19
|
-
HttpNonceGuard,
|
|
20
|
-
SequenceGuard,
|
|
21
|
-
decodeHttpRequestPlain,
|
|
22
|
-
decodeHttpResponsePlain,
|
|
23
|
-
decodeSalt,
|
|
24
|
-
deriveMasterKey,
|
|
25
|
-
deriveShk,
|
|
26
|
-
encodeHttpRequestPlain,
|
|
27
|
-
encodeHttpResponsePlain,
|
|
28
|
-
envelopeRequestHeaders,
|
|
29
|
-
fetchE2eeParams,
|
|
30
|
-
hasEnvelopeMarker,
|
|
31
|
-
newSessId,
|
|
32
|
-
normalizeKdf,
|
|
33
|
-
parseEnvelopeMarker,
|
|
34
|
-
parseWsE2eeParams,
|
|
35
|
-
randomB64
|
|
36
|
-
} from "../e2ee-client.mjs";
|
|
37
|
-
|
|
38
|
-
// 固定口令/盐的 MK 期望值:由 enterprise e2ee.js deriveE2eeMasterKey 计算得到
|
|
39
|
-
// (deriveE2eeMasterKey("dsh-e2ee-test-password", saltB64) 的输出,供跨端对齐校验)。
|
|
40
|
-
const SALT_B64 = "AAECAwQFBgcICQoLDA0ODw"; // 16B 0x00..0x0f
|
|
41
|
-
const EXPECTED_MK_HEX = "c73a0c5817997926c49f4902643e0203d225666b7afe60468115652ca79ec955";
|
|
42
|
-
|
|
43
|
-
test("MK 派生向量:固定口令/盐 → 与 enterprise e2ee.js 参考实现一致", () => {
|
|
44
|
-
const mk = deriveMasterKey("dsh-e2ee-test-password", SALT_B64);
|
|
45
|
-
assert.equal(mk.length, 32);
|
|
46
|
-
assert.equal(mk.toString("hex"), EXPECTED_MK_HEX);
|
|
47
|
-
});
|
|
48
|
-
|
|
49
|
-
test("MK 派生:同口令同盐稳定;口令/盐不同则不同", () => {
|
|
50
|
-
const mk1 = deriveMasterKey("dsh-e2ee-test-password", SALT_B64);
|
|
51
|
-
const mk1b = deriveMasterKey("dsh-e2ee-test-password", SALT_B64);
|
|
52
|
-
const mk2 = deriveMasterKey("dsh-e2ee-test-password", Buffer.from("fedcba9876543210fedcba9876543210", "hex").toString("base64url"));
|
|
53
|
-
const mk3 = deriveMasterKey("another-password", SALT_B64);
|
|
54
|
-
assert.deepEqual(mk1, mk1b);
|
|
55
|
-
assert.notDeepEqual(mk1, mk2);
|
|
56
|
-
assert.notDeepEqual(mk1, mk3);
|
|
57
|
-
});
|
|
58
|
-
|
|
59
|
-
test("MK 派生:口令 NFKC 归一(全角数字=半角数字),两端一致", () => {
|
|
60
|
-
const fullWidth = deriveMasterKey("正确口令123", SALT_B64);
|
|
61
|
-
const halfWidth = deriveMasterKey("正确口令123", SALT_B64);
|
|
62
|
-
assert.deepEqual(fullWidth, halfWidth);
|
|
63
|
-
assert.equal(halfWidth.length, 32);
|
|
64
|
-
});
|
|
65
|
-
|
|
66
|
-
test("MK 派生:盐/参数非法即拒绝(非 16B、坏 base64url、坏 kdf)", () => {
|
|
67
|
-
assert.equal(decodeSalt(""), null);
|
|
68
|
-
assert.equal(decodeSalt("short"), null);
|
|
69
|
-
assert.equal(decodeSalt("!!!!not-base64url!!!!"), null);
|
|
70
|
-
assert.throws(() => deriveMasterKey("pw", "short"), /salt 非法/);
|
|
71
|
-
assert.throws(() => deriveMasterKey("pw", SALT_B64, { iter: 0 }), /iter/);
|
|
72
|
-
assert.throws(() => deriveMasterKey("pw", SALT_B64, { iter: 100, dkLen: 0 }), /dkLen/);
|
|
73
|
-
assert.throws(() => deriveMasterKey("pw", SALT_B64, { iter: 100, dkLen: 32, hash: "md5" }), /hash/);
|
|
74
|
-
// 参数按服务端下发执行(不写死):iter=1 也能快速算,且与 600k 同构
|
|
75
|
-
const fast = deriveMasterKey("pw", SALT_B64, { alg: "pbkdf2-sha256", iter: 1, dkLen: 32, hash: "sha256" });
|
|
76
|
-
assert.equal(fast.length, 32);
|
|
77
|
-
});
|
|
78
|
-
|
|
79
|
-
test("normalizeKdf 兼容 iter / iterations 两种下发写法", () => {
|
|
80
|
-
assert.deepEqual(normalizeKdf({ alg: "pbkdf2-sha256", iter: 1000, dkLen: 32, hash: "sha256" }), { alg: "pbkdf2-sha256", iter: 1000, dkLen: 32, hash: "sha256" });
|
|
81
|
-
assert.equal(normalizeKdf(null).iter, DEFAULT_MK_KDF.iter);
|
|
82
|
-
assert.equal(normalizeKdf({ iterations: 42, dkLen: 16 }).iter, 42);
|
|
83
|
-
});
|
|
84
|
-
|
|
85
|
-
// ---------- 参数端点容错(本地 mock) ----------
|
|
86
|
-
|
|
87
|
-
async function withParamsServer(handler) {
|
|
88
|
-
const server = http.createServer(handler);
|
|
89
|
-
await new Promise((r) => server.listen(0, "127.0.0.1", r));
|
|
90
|
-
const base = `http://127.0.0.1:${server.address().port}/relay-api`;
|
|
91
|
-
try {
|
|
92
|
-
return { base, close: () => new Promise((r) => server.close(r)) };
|
|
93
|
-
} finally {
|
|
94
|
-
/* keep open until test done */
|
|
95
|
-
}
|
|
96
|
-
}
|
|
97
|
-
|
|
98
|
-
test("fetchE2eeParams:enabled=true 正常解析 §3.4 形状", async () => {
|
|
99
|
-
const { base, close } = await withParamsServer((req, res) => {
|
|
100
|
-
assert.equal(req.headers.authorization, "Bearer tok-1");
|
|
101
|
-
res.writeHead(200, { "content-type": "application/json" });
|
|
102
|
-
res.end(JSON.stringify({
|
|
103
|
-
e2ee: { enabled: true, profile: "pbkdf2-sha256-600k", kdf: { alg: "pbkdf2-sha256", iter: 600000, dkLen: 32, hash: "sha256" }, salt: SALT_B64, epoch: 3 }
|
|
104
|
-
}));
|
|
105
|
-
});
|
|
106
|
-
try {
|
|
107
|
-
const p = await fetchE2eeParams(base, "tok-1");
|
|
108
|
-
assert.equal(p.enabled, true);
|
|
109
|
-
assert.equal(p.salt, SALT_B64);
|
|
110
|
-
assert.equal(p.epoch, 3);
|
|
111
|
-
assert.equal(p.kdf.iter, 600000);
|
|
112
|
-
} finally {
|
|
113
|
-
await close();
|
|
114
|
-
}
|
|
115
|
-
});
|
|
116
|
-
|
|
117
|
-
test("fetchE2eeParams:401/404/5xx/无 e2ee/enabled=false → disabled(明文回退,不抛)", async () => {
|
|
118
|
-
const cases = [
|
|
119
|
-
[(res) => { res.writeHead(401); res.end("{}"); }, "server_unauthorized"],
|
|
120
|
-
[(res) => { res.writeHead(404); res.end("{}"); }, "server_no_params"],
|
|
121
|
-
[(res) => { res.writeHead(500); res.end("boom"); }, "http_500"],
|
|
122
|
-
[(res) => { res.writeHead(200, { "content-type": "application/json" }); res.end(JSON.stringify({ e2ee: { enabled: false, salt: "" } })); }, "server_disabled"],
|
|
123
|
-
[(res) => { res.writeHead(200, { "content-type": "application/json" }); res.end(JSON.stringify({ ok: true })); }, "server_no_params"],
|
|
124
|
-
[(res) => { res.writeHead(200, { "content-type": "application/json" }); res.end("not-json"); }, "bad_params"],
|
|
125
|
-
[(res) => { res.writeHead(200, { "content-type": "application/json" }); res.end(JSON.stringify({ e2ee: { enabled: true, salt: "abc" } })); }, "bad_params"]
|
|
126
|
-
];
|
|
127
|
-
for (const [handler, expectedReason] of cases) {
|
|
128
|
-
const { base, close } = await withParamsServer((req, res) => handler(res));
|
|
129
|
-
try {
|
|
130
|
-
const p = await fetchE2eeParams(base, "tok");
|
|
131
|
-
assert.equal(p.enabled, false, `case ${expectedReason}`);
|
|
132
|
-
assert.equal(p.reason, expectedReason);
|
|
133
|
-
} finally {
|
|
134
|
-
await close();
|
|
135
|
-
}
|
|
136
|
-
}
|
|
137
|
-
});
|
|
138
|
-
|
|
139
|
-
test("fetchE2eeParams:网络不可达 → params_unreachable(不抛)", async () => {
|
|
140
|
-
const p = await fetchE2eeParams("http://127.0.0.1:1", "tok", { timeoutMs: 500 });
|
|
141
|
-
assert.equal(p.enabled, false);
|
|
142
|
-
assert.equal(p.reason, "params_unreachable");
|
|
143
|
-
});
|
|
144
|
-
|
|
145
|
-
// ---------- 会话密钥 / 信封往返 ----------
|
|
146
|
-
|
|
147
|
-
function makeSession(over = {}) {
|
|
148
|
-
const mk = deriveMasterKey("unit-password", SALT_B64);
|
|
149
|
-
const a = randomB64(32);
|
|
150
|
-
const b = randomB64(32);
|
|
151
|
-
const { shk, saltH } = deriveShk(mk, a, b);
|
|
152
|
-
return new E2eeSession({ sessId: newSessId(), shk, saltH, ...over });
|
|
153
|
-
}
|
|
154
|
-
|
|
155
|
-
test("deriveShk:确定性 + 32B;a/b 非法长度拒绝", () => {
|
|
156
|
-
const mk = deriveMasterKey("unit-password", SALT_B64);
|
|
157
|
-
const r1 = deriveShk(mk, randomB64(32), randomB64(32));
|
|
158
|
-
assert.equal(r1.shk.length, 32);
|
|
159
|
-
assert.equal(r1.saltH.length, 32);
|
|
160
|
-
assert.throws(() => deriveShk(mk, "short", randomB64(32)), /32B/);
|
|
161
|
-
});
|
|
162
|
-
|
|
163
|
-
test("信封往返:http 请求(文本)seal→open 恢复明文;字段完整", () => {
|
|
164
|
-
const s = makeSession();
|
|
165
|
-
const plain = encodeHttpRequestPlain({ method: "POST", path: "/api/session/s1/messages", headers: { "content-type": "application/json" }, bodyB64: Buffer.from("你好 dsh").toString("base64"), bodyBase64: true });
|
|
166
|
-
// http 请求 key-info reqNonce 默认 = 信封自身 n(自描述),开包方无需额外状态
|
|
167
|
-
const env = s.seal({ kind: "http", dir: "p2b", counter: 7, data: plain });
|
|
168
|
-
assert.equal(env.v, 2);
|
|
169
|
-
assert.equal(env.k, "http");
|
|
170
|
-
assert.equal(env.s, s.sessId);
|
|
171
|
-
assert.equal(env.c, 7);
|
|
172
|
-
assert.equal(env.t, 0);
|
|
173
|
-
const opened = s.open({ kind: "http", dir: "p2b", env, counter: env.c });
|
|
174
|
-
const req = decodeHttpRequestPlain(opened.data);
|
|
175
|
-
assert.equal(req.method, "POST");
|
|
176
|
-
assert.equal(req.path, "/api/session/s1/messages");
|
|
177
|
-
assert.equal(Buffer.from(req.bodyB64, "base64").toString("utf8"), "你好 dsh");
|
|
178
|
-
});
|
|
179
|
-
|
|
180
|
-
function envNonce() {
|
|
181
|
-
return Buffer.from("0102030405060708090a0b0c", "hex").toString("base64url");
|
|
182
|
-
}
|
|
183
|
-
|
|
184
|
-
test("信封往返:ws 二进制(t:1)密封原样还原", () => {
|
|
185
|
-
const s = makeSession();
|
|
186
|
-
const bytes = Buffer.from([0, 1, 2, 253, 254, 255]);
|
|
187
|
-
const env = s.seal({ kind: "w", dir: "p2b", counter: 0, data: bytes, t: 1, wsLabel: "/api/events?w=deadbeef" });
|
|
188
|
-
assert.equal(env.t, 1);
|
|
189
|
-
const opened = s.open({ kind: "w", dir: "p2b", env, counter: 0, wsLabel: "/api/events?w=deadbeef" });
|
|
190
|
-
assert.equal(opened.t, 1);
|
|
191
|
-
assert.deepEqual(opened.data, bytes);
|
|
192
|
-
});
|
|
193
|
-
|
|
194
|
-
test("篡改拒绝:d 翻转一字节 → auth_failed;会话/kind/方向不符均拒绝", () => {
|
|
195
|
-
const s = makeSession();
|
|
196
|
-
const env = s.seal({ kind: "http", dir: "p2b", counter: 1, data: "payload" });
|
|
197
|
-
// 篡改密文
|
|
198
|
-
const tampered = { ...env, d: flipB64(env.d) };
|
|
199
|
-
assert.throws(() => s.open({ kind: "http", dir: "p2b", env: tampered, counter: env.c }), (e) => e instanceof E2eeError && e.code === "auth_failed");
|
|
200
|
-
// 篡改 nonce(改 key-info 域,信封自描述 reqNonce 随之变化 → 密钥不同 → 认证失败)
|
|
201
|
-
assert.throws(() => s.open({ kind: "http", dir: "p2b", env: { ...env, n: Buffer.alloc(12, 1).toString("base64url") }, counter: env.c }), (e) => e instanceof E2eeError);
|
|
202
|
-
// 错误 kind / 错误方向(密钥域分离)
|
|
203
|
-
assert.throws(() => s.open({ kind: "w", dir: "p2b", env, counter: env.c, wsLabel: "x" }), (e) => e instanceof E2eeError);
|
|
204
|
-
assert.throws(() => s.open({ kind: "http", dir: "b2p", env, counter: env.c }), (e) => e instanceof E2eeError && e.code === "auth_failed");
|
|
205
|
-
// 会话不符(直接报 bad_session,不开包)
|
|
206
|
-
const other = makeSession();
|
|
207
|
-
assert.throws(() => other.open({ kind: "http", dir: "p2b", env, counter: env.c }), (e) => e instanceof E2eeError && ["bad_session", "auth_failed"].includes(e.code));
|
|
208
|
-
});
|
|
209
|
-
|
|
210
|
-
function flipB64(b64) {
|
|
211
|
-
const buf = Buffer.from(b64, "base64url");
|
|
212
|
-
buf[0] ^= 0xff;
|
|
213
|
-
return buf.toString("base64url");
|
|
214
|
-
}
|
|
215
|
-
|
|
216
|
-
test("http 响应信封:封包→开包,enc=gzip 被提取、实体头剥离,正文还原", () => {
|
|
217
|
-
const s = makeSession();
|
|
218
|
-
const body = Buffer.from(JSON.stringify({ ok: true }), "utf8");
|
|
219
|
-
const plain = encodeHttpResponsePlain({
|
|
220
|
-
status: 200,
|
|
221
|
-
headers: { "content-type": "application/json", "content-encoding": "gzip", "content-length": "100", "x-thing": "v" },
|
|
222
|
-
bodyBuffer: body
|
|
223
|
-
});
|
|
224
|
-
const env = s.seal({ kind: "http-resp", dir: "b2p", counter: 3, data: plain, reqNonceB64: envNonce() });
|
|
225
|
-
const opened = s.open({ kind: "http-resp", dir: "b2p", env, counter: env.c, reqNonceB64: envNonce() });
|
|
226
|
-
const resp = decodeHttpResponsePlain(opened.data);
|
|
227
|
-
assert.equal(resp.status, 200);
|
|
228
|
-
assert.equal(resp.enc, "gzip");
|
|
229
|
-
assert.equal(resp.headers["content-type"], "application/json");
|
|
230
|
-
assert.equal(resp.headers["content-encoding"], undefined); // 实体头进 enc 不残留
|
|
231
|
-
assert.deepEqual(resp.bodyBuffer, body);
|
|
232
|
-
});
|
|
233
|
-
|
|
234
|
-
test("防重放:HttpNonceGuard 同 (sess,n) 重复拒绝;窗口过期后放行", () => {
|
|
235
|
-
const g = new HttpNonceGuard({ windowMs: 40_000, max: 16 });
|
|
236
|
-
assert.equal(g.check("s1", "nonce-a"), true);
|
|
237
|
-
assert.equal(g.check("s1", "nonce-a"), false); // 重放
|
|
238
|
-
assert.equal(g.check("s1", "nonce-b"), true); // 不同 nonce 放行
|
|
239
|
-
assert.equal(g.check("s2", "nonce-a"), true); // 不同会话放行
|
|
240
|
-
// 窗口 0 = 立即过期:同一 nonce 再次出现被当作新消息放行(5 分钟窗口语义)
|
|
241
|
-
const g2 = new HttpNonceGuard({ windowMs: 0, max: 16 });
|
|
242
|
-
assert.equal(g2.check("s1", "x"), true);
|
|
243
|
-
assert.equal(g2.check("s1", "x"), true);
|
|
244
|
-
assert.equal(g2.check("s1", "x"), true);
|
|
245
|
-
});
|
|
246
|
-
|
|
247
|
-
test("防重放:SequenceGuard 计数必须单调;重放/乱序拒绝", () => {
|
|
248
|
-
const g = new SequenceGuard();
|
|
249
|
-
g.check(0);
|
|
250
|
-
g.check(1);
|
|
251
|
-
g.check(5); // 跳号(网络无乱序,实际不会发生)允许? 协议要求单调 +1,这里严格递增即可
|
|
252
|
-
assert.throws(() => g.check(5), (e) => e instanceof E2eeError && e.code === "replay");
|
|
253
|
-
assert.throws(() => g.check(1), (e) => e instanceof E2eeError && e.code === "replay");
|
|
254
|
-
assert.throws(() => g.check(-1), (e) => e instanceof E2eeError && e.code === "bad_counter");
|
|
255
|
-
});
|
|
256
|
-
|
|
257
|
-
// ---------- ws 标签 / 信封标记头 ----------
|
|
258
|
-
|
|
259
|
-
test("parseWsE2eeParams:解析 e2ee/w、剥离出上游路径、保留 w 的流标签", () => {
|
|
260
|
-
const sessId = newSessId();
|
|
261
|
-
const r = parseWsE2eeParams(`/api/events.mux?a=1&e2ee=${sessId}&w=0011223344556677`);
|
|
262
|
-
assert.equal(r.sessId, sessId);
|
|
263
|
-
assert.equal(r.w, "0011223344556677");
|
|
264
|
-
assert.equal(r.upstreamPath, "/api/events.mux?a=1");
|
|
265
|
-
assert.equal(r.wsLabel, "/api/events.mux?a=1&w=0011223344556677");
|
|
266
|
-
// 无标记 / 无 query → null
|
|
267
|
-
assert.equal(parseWsE2eeParams("/api/events.mux"), null);
|
|
268
|
-
assert.equal(parseWsE2eeParams(`/api?e2ee=${sessId}`), null);
|
|
269
|
-
assert.equal(parseWsE2eeParams(`/api?e2ee=bad&w=0011`), null);
|
|
270
|
-
});
|
|
271
|
-
|
|
272
|
-
test("信封标记头:封包/识别/解析一致;content-type 与 x-dsh-e2ee 双信号", () => {
|
|
273
|
-
const sessId = newSessId();
|
|
274
|
-
const h = envelopeRequestHeaders(sessId, "http");
|
|
275
|
-
assert.equal(h["content-type"], "application/vnd.dsh.e2ee-v2");
|
|
276
|
-
assert.equal(hasEnvelopeMarker(h), true);
|
|
277
|
-
assert.equal(hasEnvelopeMarker({ "content-type": "text/html" }), false);
|
|
278
|
-
const m = parseEnvelopeMarker(h);
|
|
279
|
-
assert.equal(m.v, 2);
|
|
280
|
-
assert.equal(m.s, sessId);
|
|
281
|
-
assert.equal(m.k, "http");
|
|
282
|
-
assert.equal(parseEnvelopeMarker({}), null);
|
|
283
|
-
});
|
|
284
|
-
|
|
285
|
-
// ---------- 桥端服务:握手 / 探针 / 开关 ----------
|
|
286
|
-
|
|
287
|
-
test("E2eeService:disabled 状态 caps 为空;enabled 后 caps=['e2ee-v2']", () => {
|
|
288
|
-
const off = new E2eeService({ enabled: false, reason: "server_disabled" });
|
|
289
|
-
assert.deepEqual(off.caps(), []);
|
|
290
|
-
assert.equal(off.enabled, false);
|
|
291
|
-
const mk = deriveMasterKey("unit-password", SALT_B64);
|
|
292
|
-
const on = new E2eeService({ enabled: true, mk, salt: SALT_B64, profile: "pbkdf2-sha256-600k" });
|
|
293
|
-
assert.deepEqual(on.caps(), ["e2ee-v2"]);
|
|
294
|
-
assert.equal(on.state().reason, "ok");
|
|
295
|
-
});
|
|
296
|
-
|
|
297
|
-
test("E2eeService.init:服务端 enabled=false/401/无密码/禁用开关 → 明文回退原因", async () => {
|
|
298
|
-
// 无密码
|
|
299
|
-
const noPw = await E2eeService.init({ apiBase: "http://127.0.0.1:1", token: "t", password: "" });
|
|
300
|
-
assert.equal(noPw.enabled, false);
|
|
301
|
-
assert.equal(noPw.reason, "no_password");
|
|
302
|
-
// allowed=false(本地配置关闭)
|
|
303
|
-
const off = await E2eeService.init({ apiBase: "http://127.0.0.1:1", token: "t", password: "pw", allowed: false });
|
|
304
|
-
assert.equal(off.reason, "disabled_by_config");
|
|
305
|
-
// 401
|
|
306
|
-
const { base, close } = await withParamsServer((req, res) => { res.writeHead(401); res.end("{}"); });
|
|
307
|
-
try {
|
|
308
|
-
const unauthorized = await E2eeService.init({ apiBase: base, token: "t", password: "pw" });
|
|
309
|
-
assert.equal(unauthorized.enabled, false);
|
|
310
|
-
assert.equal(unauthorized.reason, "server_unauthorized");
|
|
311
|
-
} finally {
|
|
312
|
-
await close();
|
|
313
|
-
}
|
|
314
|
-
});
|
|
315
|
-
|
|
316
|
-
test("握手:hello→ack(b 随机 32B);salt/profile 不符拒绝;disabled 服务拒绝", async () => {
|
|
317
|
-
const mk = deriveMasterKey("unit-password", SALT_B64);
|
|
318
|
-
const svc = new E2eeService({ enabled: true, mk, salt: SALT_B64, profile: "pbkdf2-sha256-600k" });
|
|
319
|
-
const sessId = newSessId();
|
|
320
|
-
const { session, ack } = svc.handleHello({
|
|
321
|
-
v: 2, type: "e2ee-hello", role: "phone", s: sessId, a: randomB64(32), salt: SALT_B64, profile: "pbkdf2-sha256-600k", ts: Date.now()
|
|
322
|
-
});
|
|
323
|
-
assert.equal(ack.type, "e2ee-hello-ack");
|
|
324
|
-
assert.equal(ack.s, sessId);
|
|
325
|
-
assert.equal(Buffer.from(ack.b, "base64url").length, 32);
|
|
326
|
-
assert.deepEqual(ack.caps, ["e2ee-v2"]);
|
|
327
|
-
assert.equal(session.verified, false);
|
|
328
|
-
assert.equal(svc.sessionOf(sessId).sessId, sessId);
|
|
329
|
-
// salt 不一致 → 拒绝(params 域不匹配)
|
|
330
|
-
assert.throws(() => svc.handleHello({ v: 2, type: "e2ee-hello", role: "phone", s: newSessId(), a: randomB64(32), salt: Buffer.alloc(16, 1).toString("base64url"), profile: "pbkdf2-sha256-600k" }), (e) => e.code === "params");
|
|
331
|
-
// disabled 服务
|
|
332
|
-
const off = new E2eeService({ enabled: false, reason: "server_disabled" });
|
|
333
|
-
assert.throws(() => off.handleHello({ v: 2, type: "e2ee-hello", role: "phone", s: newSessId(), a: randomB64(32), salt: SALT_B64 }), (e) => e.code === "disabled");
|
|
334
|
-
});
|
|
335
|
-
|
|
336
|
-
test("探针:正确 MK 通过并置 verified;错误 MK → bad_key;未解锁会话被拒", () => {
|
|
337
|
-
const mk = deriveMasterKey("unit-password", SALT_B64);
|
|
338
|
-
const svc = new E2eeService({ enabled: true, mk, salt: SALT_B64, profile: "pbkdf2-sha256-600k" });
|
|
339
|
-
const sessId = newSessId();
|
|
340
|
-
const aB64 = randomB64(32);
|
|
341
|
-
// 手机 hello → bridge 生成 b → 手机与 bridge 各自 deriveShk(mk, a, b)
|
|
342
|
-
const bB64 = (() => {
|
|
343
|
-
const { ack } = svc.handleHello({ v: 2, type: "e2ee-hello", role: "phone", s: sessId, a: aB64, salt: SALT_B64, profile: "pbkdf2-sha256-600k" });
|
|
344
|
-
return ack.b;
|
|
345
|
-
})();
|
|
346
|
-
const client = new E2eeSession({ sessId, ...deriveShk(mk, aB64, bB64), profile: "pbkdf2-sha256-600k" });
|
|
347
|
-
// 探针(明文 {p,t,c})
|
|
348
|
-
const probe = client.seal({ kind: "ctrl", dir: "p2b", counter: 0, data: JSON.stringify({ p: "dsh-e2ee-probe-v1", t: Date.now(), c: 0 }) });
|
|
349
|
-
const { reply, session } = svc.handleProbe(sessId, probe);
|
|
350
|
-
assert.equal(reply.k, "ctrl");
|
|
351
|
-
assert.equal(reply.s, sessId);
|
|
352
|
-
assert.equal(session.verified, true);
|
|
353
|
-
// bridge 回 probe-ok → 手机可开
|
|
354
|
-
const okPlain = client.open({ kind: "ctrl", dir: "b2p", env: reply, counter: reply.c });
|
|
355
|
-
assert.equal(JSON.parse(okPlain.data.toString("utf8")).p, "dsh-e2ee-probe-ok");
|
|
356
|
-
// 重放同一探针 → 计数重放拒绝
|
|
357
|
-
assert.throws(() => svc.handleProbe(sessId, probe), (e) => e.code === "replay" || e.code === "bad_counter");
|
|
358
|
-
// 错误 MK(错误口令)的手机 → bad_key
|
|
359
|
-
const wrongMk = deriveMasterKey("wrong-password", SALT_B64);
|
|
360
|
-
const client2 = new E2eeSession({ sessId, ...deriveShk(wrongMk, aB64, bB64), profile: "pbkdf2-sha256-600k" });
|
|
361
|
-
const probe2 = client2.seal({ kind: "ctrl", dir: "p2b", counter: 0, data: JSON.stringify({ p: "dsh-e2ee-probe-v1", t: Date.now(), c: 0 }) });
|
|
362
|
-
assert.throws(() => svc.handleProbe(sessId, probe2), (e) => e.code === "bad_key" || e.code === "auth_failed");
|
|
363
|
-
// 未知会话拒绝
|
|
364
|
-
assert.throws(() => svc.handleProbe(newSessId(), probe), (e) => e.code === "unknown_session");
|
|
365
|
-
});
|