@mrrisega/dsh-remote 0.6.0-beta.7 → 0.6.0-beta.8

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.
@@ -0,0 +1,200 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * e2ee-wecrypto-parity.test.mjs — 手机端(浏览器 WebCrypto,native.html 抽取)与
4
+ * 桥端(node:crypto e2ee-client.mjs)的**字节级一致性**对拍(纯函数/内存,不起进程):
5
+ * - MK KAT:同一固定口令/盐 → 同一 32B hex(node 与 enterprise e2ee.js 三方一致);
6
+ * - SHK/子密钥:同一 MK+a/b → 同一 hex;
7
+ * - 信封(seal/open):显式 nonce/计数/方向/kind 完全一致 → node 与 wc 产出逐字段相同
8
+ * (http / http-resp / w 文本+二进制 / ctrl);交叉 open 成功、篡改拒绝;
9
+ * - http/ws 明文编解码与信封标记头、ws 标签解析一致。
10
+ *
11
+ * 用法: node --test test/e2ee-wecrypto-parity.test.mjs
12
+ */
13
+ import assert from "node:assert/strict";
14
+ import test from "node:test";
15
+ import {
16
+ E2eeSession,
17
+ decodeHttpRequestPlain as nodeDecodeReq,
18
+ decodeHttpResponsePlain as nodeDecodeResp,
19
+ deriveMasterKey,
20
+ deriveShk,
21
+ encodeHttpRequestPlain as nodeEncodeReq,
22
+ encodeHttpResponsePlain as nodeEncodeResp,
23
+ envelopeRequestHeaders,
24
+ newSessId,
25
+ parseEnvelopeMarker,
26
+ parseWsE2eeParams,
27
+ randomB64
28
+ } from "../e2ee-client.mjs";
29
+ import { loadNativeWcCore } from "./lib-native-e2ee.mjs";
30
+
31
+ // 与 e2ee-client.test.mjs 相同的 KAT 常量(enterprise deriveE2eeMasterKey 三方一致)
32
+ const SALT_B64 = "AAECAwQFBgcICQoLDA0ODw"; // 16B 0x00..0x0f
33
+ const EXPECTED_MK_HEX = "c73a0c5817997926c49f4902643e0203d225666b7afe60468115652ca79ec955";
34
+
35
+ const wc = await loadNativeWcCore();
36
+ const hex = (u8) => Buffer.from(u8).toString("hex");
37
+
38
+ function nodeSession(over = {}) {
39
+ const mk = deriveMasterKey("parity-password", SALT_B64);
40
+ const { shk, saltH } = deriveShk(mk, randomB64(32), randomB64(32));
41
+ return new E2eeSession({ sessId: newSessId(), shk, saltH, ...over });
42
+ }
43
+
44
+ test("MK 派生:WebCrypto 与 node 同口令同盐逐字节一致(含 KAT)", async () => {
45
+ const mkWc = await wc.wcDeriveMasterKey("dsh-e2ee-test-password", SALT_B64);
46
+ assert.equal(mkWc.length, 32);
47
+ assert.equal(hex(mkWc), EXPECTED_MK_HEX); // KAT(enterprise e2ee.js 参考实现)
48
+ const mkNode = deriveMasterKey("dsh-e2ee-test-password", SALT_B64);
49
+ assert.equal(hex(mkWc), mkNode.toString("hex"));
50
+ // NFKC 归一(全角数字=半角)两端一致
51
+ const fullWc = await wc.wcDeriveMasterKey("正确口令123", SALT_B64);
52
+ const halfWc = await wc.wcDeriveMasterKey("正确口令123", SALT_B64);
53
+ assert.equal(hex(fullWc), hex(halfWc));
54
+ const fullNode = deriveMasterKey("正确口令123", SALT_B64);
55
+ assert.equal(hex(fullWc), fullNode.toString("hex"));
56
+ // 盐非法:wc 与 node 同样拒绝
57
+ await assert.rejects(() => wc.wcDeriveMasterKey("pw", "short"), /salt 非法/);
58
+ // 参数按服务端下发执行(iter=1 同构可算)
59
+ const fast = await wc.wcDeriveMasterKey("pw", SALT_B64, { alg: "pbkdf2-sha256", iter: 1, dkLen: 32, hash: "sha256" });
60
+ assert.equal(fast.length, 32);
61
+ });
62
+
63
+ test("SHK/子密钥:WebCrypto 与 node 在相同 a/b/MK 下逐字节一致", async () => {
64
+ const mkNode = deriveMasterKey("parity-password", SALT_B64);
65
+ const aB64 = randomB64(32);
66
+ const bB64 = randomB64(32);
67
+ const nodeShk = deriveShk(mkNode, aB64, bB64);
68
+ const wcMk = await wc.wcDeriveMasterKey("parity-password", SALT_B64);
69
+ const wcShk = await wc.wcDeriveShk(wcMk, aB64, bB64);
70
+ assert.equal(hex(wcShk.saltH), nodeShk.saltH.toString("hex"));
71
+ assert.equal(hex(wcShk.shk), nodeShk.shk.toString("hex"));
72
+ });
73
+
74
+ /** 同参数在 node 与 wc 各建一个会话(显式 a/b → SHK 一致)。 */
75
+ async function twinSessions() {
76
+ const mkNode = deriveMasterKey("parity-password", SALT_B64);
77
+ const aB64 = randomB64(32);
78
+ const bB64 = randomB64(32);
79
+ const sessId = newSessId();
80
+ const wcMk = await wc.wcDeriveMasterKey("parity-password", SALT_B64);
81
+ const { shk: wcShk, saltH: wcSaltH } = await wc.wcDeriveShk(wcMk, aB64, bB64);
82
+ const nodeS = new E2eeSession({ sessId, ...deriveShk(mkNode, aB64, bB64) });
83
+ const wcS = new wc.WcE2eeSession({ sessId, shk: wcShk, saltH: wcSaltH });
84
+ return { nodeS, wcS, sessId };
85
+ }
86
+
87
+ const NONCE_12 = () => Buffer.from("00112233445566778899aabb", "hex");
88
+ const NONCE_12_B64 = "ABEiM0RVZneImaq7";
89
+
90
+ test("信封逐字段一致:http 请求 / http 响应 / w(文本·二进制)/ ctrl", async () => {
91
+ const { nodeS, wcS } = await twinSessions();
92
+
93
+ // http 请求(node key-info reqNonce 默认 = 信封 n)
94
+ const reqPlain = nodeEncodeReq({ method: "POST", path: "/api/session/s1/messages?b=2", headers: { "content-type": "application/json", "x-test": "v" }, bodyB64: Buffer.from("你好 世界").toString("base64") });
95
+ const envNode = nodeS.seal({ kind: "http", dir: "p2b", counter: 7, data: reqPlain, nonce: NONCE_12() });
96
+ const envWc = await wcS.seal({ kind: "http", dir: "p2b", counter: 7, data: reqPlain, nonceBytes: NONCE_12() });
97
+ assert.deepEqual(envWc, envNode, "http p2b 信封应与 node 完全一致");
98
+ // 交叉 open 成功
99
+ const openedByWc = await wcS.open({ kind: "http", dir: "p2b", env: envNode, counter: envNode.c });
100
+ assert.equal(Buffer.from(openedByWc.data).toString(), Buffer.from(reqPlain).toString());
101
+
102
+ // http 响应(http-resp key-info reqNonce = 请求 n;n 为响应 nonce)
103
+ const respPlain = nodeEncodeResp({ status: 200, headers: { "content-type": "application/json", "content-encoding": "gzip", "content-length": "10", "x-a": "1" }, bodyBuffer: Buffer.from("hello-gzip-body") });
104
+ const envRespN = nodeS.seal({ kind: "http-resp", dir: "b2p", counter: 3, data: respPlain, reqNonceB64: NONCE_12_B64, nonce: NONCE_12() });
105
+ const envRespW = await wcS.seal({ kind: "http-resp", dir: "b2p", counter: 3, data: respPlain, reqNonceB64: NONCE_12_B64, nonceBytes: NONCE_12() });
106
+ assert.deepEqual(envRespW, envRespN, "http-resp b2p 信封应与 node 完全一致");
107
+ const openedResp = await wcS.open({ kind: "http-resp", dir: "b2p", env: envRespN, counter: envRespN.c, reqNonceB64: NONCE_12_B64 });
108
+ const rp = nodeDecodeResp(Buffer.from(openedResp.data));
109
+ assert.equal(rp.status, 200);
110
+ assert.equal(rp.enc, "gzip");
111
+ assert.equal(rp.headers["content-type"], "application/json");
112
+ assert.equal(rp.headers["content-encoding"], undefined);
113
+
114
+ // w 文本 + 二进制
115
+ const wsLabel = "/api/events.mux?w=0011223344556677";
116
+ for (const [data, t] of [["ping-你好", undefined], [Buffer.from([0, 1, 2, 253, 254, 255]), 1]]) {
117
+ const envWN = nodeS.seal({ kind: "w", dir: "p2b", counter: 0, data, t, wsLabel, nonce: NONCE_12() });
118
+ const envWW = await wcS.seal({ kind: "w", dir: "p2b", counter: 0, data: Buffer.isBuffer(data) ? new Uint8Array(data) : data, t, wsLabel, nonceBytes: NONCE_12() });
119
+ assert.deepEqual(envWW, envWN, "w 信封应与 node 完全一致");
120
+ const back = await wcS.open({ kind: "w", dir: "p2b", env: envWN, counter: envWN.c, wsLabel });
121
+ assert.equal(back.t, t === 1 ? 1 : 0);
122
+ if (t === 1) assert.deepEqual(Buffer.from(back.data), data);
123
+ else assert.equal(Buffer.from(back.data).toString("utf8"), data);
124
+ }
125
+
126
+ // ctrl 探针
127
+ const probePlain = JSON.stringify({ p: "dsh-e2ee-probe-v1", t: 1700000000000, c: 0 });
128
+ const envCN = nodeS.seal({ kind: "ctrl", dir: "p2b", counter: 0, data: probePlain, nonce: NONCE_12() });
129
+ const envCW = await wcS.seal({ kind: "ctrl", dir: "p2b", counter: 0, data: probePlain, nonceBytes: NONCE_12() });
130
+ assert.deepEqual(envCW, envCN, "ctrl 信封应与 node 完全一致");
131
+ });
132
+
133
+ test("交叉开包 + 篡改拒绝(方向/kind/密文被改 → wc 与 node 同判)", async () => {
134
+ const { nodeS, wcS } = await twinSessions();
135
+ const plain = nodeEncodeReq({ method: "GET", path: "/api/x", headers: {}, bodyB64: "" });
136
+ const envN = nodeS.seal({ kind: "http", dir: "p2b", counter: 1, data: plain, nonce: NONCE_12() });
137
+ const envW = await wcS.seal({ kind: "http", dir: "p2b", counter: 1, data: plain, nonceBytes: NONCE_12() });
138
+
139
+ // 双向交叉开包(不同实现、同一会话材料)
140
+ const n2w = await wcS.open({ kind: "http", dir: "p2b", env: envN, counter: envN.c });
141
+ const w2n = nodeS.open({ kind: "http", dir: "p2b", env: envW, counter: envW.c });
142
+ assert.equal(Buffer.from(n2w.data).toString("utf8"), Buffer.from(plain).toString("utf8"));
143
+ assert.equal(Buffer.from(w2n.data).toString("utf8"), Buffer.from(plain).toString("utf8"));
144
+
145
+ // 篡改密文:node 拒 + wc 拒(auth_failed)
146
+ const tampered = { ...envW, d: (() => { const b = Buffer.from(envW.d, "base64url"); b[3] ^= 0xff; return b.toString("base64url"); })() };
147
+ assert.throws(() => nodeS.open({ kind: "http", dir: "p2b", env: tampered, counter: tampered.c }), (e) => e.code === "auth_failed");
148
+ await assert.rejects(() => wcS.open({ kind: "http", dir: "p2b", env: tampered, counter: tampered.c }), (e) => e && e.code === "auth_failed");
149
+ // 错误方向(wc 侧密钥域分离 → auth 失败)
150
+ await assert.rejects(() => wcS.open({ kind: "http", dir: "b2p", env: envW, counter: envW.c }), (e) => e && e.code === "auth_failed");
151
+ // 会话不符 → bad_session
152
+ const other = new E2eeSession({ sessId: newSessId(), shk: Buffer.alloc(32, 9), saltH: Buffer.alloc(32, 9) });
153
+ await assert.rejects(() => wcS.open({ kind: "http", dir: "p2b", env: { ...envW, s: other.sessId }, counter: envW.c }), (e) => e && e.code === "bad_session");
154
+ });
155
+
156
+ test("信封标记头 / 明文编解码 / ws 标签:浏览器与 node 同构", async () => {
157
+ const sessId = newSessId();
158
+ const hNode = envelopeRequestHeaders(sessId, "http");
159
+ const hWc = wc.wcEnvelopeHeaders(sessId, "http");
160
+ assert.deepEqual(hWc, hNode);
161
+ assert.deepEqual(wc.wcParseEnvelopeMarker(hWc), parseEnvelopeMarker(hNode));
162
+ assert.equal(wc.wcHasEnvelopeMarker(hWc), true);
163
+ assert.equal(wc.wcHasEnvelopeMarker({ "content-type": "text/html" }), false);
164
+
165
+ const req = { method: "POST", path: "/api/echo?b=2", headers: { "content-type": "application/json", "x-test": "abc" }, bodyB64: Buffer.from("payload").toString("base64") };
166
+ const bNode = Buffer.from(nodeEncodeReq(req));
167
+ const bWc = wc.wcEncodeHttpRequestPlain(req);
168
+ assert.deepEqual(Buffer.from(bWc), bNode, "请求明文 JSON 应逐字节一致");
169
+ const decWc = wc.wcDecodeHttpRequestPlain(bWc);
170
+ const decNode = nodeDecodeReq(bNode);
171
+ assert.equal(decWc.method, decNode.method);
172
+ assert.equal(decWc.path, decNode.path);
173
+ assert.deepEqual(decWc.headers, decNode.headers);
174
+ assert.equal(decWc.bodyB64, decNode.bodyB64);
175
+
176
+ const respNode = nodeEncodeResp({ status: 200, headers: { "content-type": "application/json", "content-encoding": "gzip" }, bodyBuffer: Buffer.from("aGk=", "base64") });
177
+ const respWc = wc.wcEncodeHttpResponsePlain({ status: 200, headers: { "content-type": "application/json", "content-encoding": "gzip" }, bodyB64: "aGk=" });
178
+ assert.deepEqual(Buffer.from(respWc), Buffer.from(respNode));
179
+ const dResp = wc.wcDecodeHttpResponsePlain(respWc);
180
+ assert.equal(dResp.enc, "gzip");
181
+ assert.equal(dResp.bodyB64, "aGk=");
182
+
183
+ const path = `/api/events.mux?a=1&e2ee=${sessId}&w=0011223344556677`;
184
+ assert.deepEqual(wc.wcParseWsE2eeParams(path), parseWsE2eeParams(path));
185
+ assert.equal(wc.wcParseWsE2eeParams("/api/events.mux"), null);
186
+ });
187
+
188
+ test("随机化往返:wc 自洽(seal→open)与 node 交叉 20 轮", async () => {
189
+ for (let i = 0; i < 20; i++) {
190
+ const { nodeS, wcS } = await twinSessions();
191
+ const wsLabel = `/stream?w=${wc.wcRandomHex(8)}`;
192
+ const msg = "round-" + i + "-你好-" + wc.wcRandomHex(6);
193
+ const envW = await wcS.seal({ kind: "w", dir: "p2b", counter: i, data: msg, wsLabel });
194
+ const opened = nodeS.open({ kind: "w", dir: "p2b", env: envW, counter: i, wsLabel });
195
+ assert.equal(Buffer.from(opened.data).toString("utf8"), msg);
196
+ const envN = nodeS.seal({ kind: "w", dir: "b2p", counter: i, data: Buffer.from([i, i + 1, i + 2]), t: 1, wsLabel });
197
+ const back = await wcS.open({ kind: "w", dir: "b2p", env: envN, counter: i, wsLabel });
198
+ assert.deepEqual(Buffer.from(back.data), Buffer.from([i, i + 1, i + 2]));
199
+ }
200
+ });
@@ -0,0 +1,59 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * lib-e2ee-shim.mjs — 测试共享:从 clients/dsh-remote/e2ee-shim-script.js 抽取
4
+ * 「DSH-E2EE-SHIM-CORE」纯区间(浏览器 WebCrypto + 信封 + fetch/WS 包装逻辑),
5
+ * 包装为 ESM 后 import(node ≥22 自带 crypto.subtle / atob / btoa / fetch / WS 所需全局)。
6
+ *
7
+ * 用途:镜像页 shim(Phase-4,注入到经隧道回传的官方 dsh web 文档)与桥端 e2ee-client.mjs、
8
+ * native.html WC-CORE 的字节级一致性对拍 / KAT / mock 与真实 router+bridge 互通,全部用
9
+ * 注入脚本里的真实代码,而不是在测试里再抄一份。
10
+ */
11
+ import { readFileSync } from "node:fs";
12
+ import path from "node:path";
13
+ import { fileURLToPath } from "node:url";
14
+
15
+ export const SHIM_SCRIPT_PATH = path.join(
16
+ path.dirname(fileURLToPath(import.meta.url)),
17
+ "..", "e2ee-shim-script.js"
18
+ );
19
+
20
+ export const SHIM_CORE_START = "/* ===DSH-E2EE-SHIM-CORE-START===";
21
+ export const SHIM_CORE_END = "/* ===DSH-E2EE-SHIM-CORE-END===";
22
+
23
+ export function readShimScript() {
24
+ return readFileSync(SHIM_SCRIPT_PATH, "utf8");
25
+ }
26
+
27
+ /** 从 e2ee-shim-script.js 抽出纯核心区间(文本)。 */
28
+ export function extractShimCore(script = readShimScript()) {
29
+ const s = script.indexOf(SHIM_CORE_START);
30
+ const e = script.indexOf(SHIM_CORE_END);
31
+ if (s === -1 || e === -1 || e <= s) throw new Error("e2ee-shim-script.js 缺少 DSH-E2EE-SHIM-CORE 区间");
32
+ const bodyStart = script.indexOf("\n", s) + 1;
33
+ return script.slice(bodyStart, e).replace(/\n\s*$/, "");
34
+ }
35
+
36
+ /** 需要暴露给测试的核心名字(与 e2ee-shim-script.js 内声明一致)。 */
37
+ export const SHIM_EXPORTS = [
38
+ "SE_CT", "SE_HDR", "SE_DIR_P2B", "SE_DIR_B2P", "SE_NONCE_BYTES", "SE_TAG_BYTES", "SE_KEY_BYTES", "SE_VERSION",
39
+ "DSH_E2EE_HANDOVER_KEY",
40
+ "seErr", "seUtf8", "seUtf8Decode", "seB64ToBytes", "seBytesToB64",
41
+ "seRandomBytes", "seRandomHex", "seHkdf", "SeSession", "seFailureHook", "seNotifyFail",
42
+ "seEncodeHttpReqPlain", "seDecodeHttpRespPlain", "seEnvelopeHeaders", "seIsEnvelopeResponse", "seGunzip",
43
+ "seUpstreamPathOf", "seShouldEncryptHttp", "seShouldEncryptWs", "seWsLabelParts", "seBuildWsTarget",
44
+ "seHandoverDecode", "seSessionOfHandover",
45
+ "seFetchWrapper", "SeE2eeWs", "seMakeWsCtor", "seBuildResponse", "seErrorResponse"
46
+ ];
47
+
48
+ let cached = null;
49
+
50
+ /** 抽取 + import(幂等缓存)。 */
51
+ export async function loadShimCore() {
52
+ if (cached) return cached;
53
+ const core = extractShimCore();
54
+ const src = core + "\nexport { " + SHIM_EXPORTS.join(", ") + " };\n";
55
+ const b64 = Buffer.from(src, "utf8").toString("base64");
56
+ const mod = await import("data:text/javascript;base64," + b64);
57
+ cached = mod;
58
+ return mod;
59
+ }
@@ -0,0 +1,92 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * lib-native-e2ee.mjs — 测试共享:从 clients/dsh-web/native.html 抽取
4
+ * 「DSH-E2EE-WC-CORE」纯 WebCrypto 区间,包装为 ESM 后 import(node ≥22 自带
5
+ * globalThis.crypto.subtle / TextEncoder / atob / btoa / DecompressionStream)。
6
+ *
7
+ * 用途:手机端(浏览器 WebCrypto)与桥端(node:crypto e2ee-client.mjs)的
8
+ * 字节级一致性对拍 / KAT / 端到端互通,全部用 native.html 里的真实代码,
9
+ * 而不是在测试里再抄一份。
10
+ */
11
+ import { readFileSync } from "node:fs";
12
+ import path from "node:path";
13
+ import { fileURLToPath } from "node:url";
14
+
15
+ export const NATIVE_HTML_PATH = path.join(
16
+ path.dirname(fileURLToPath(import.meta.url)),
17
+ "..", "..", "dsh-web", "native.html"
18
+ );
19
+
20
+ export const CORE_START = "/* ===DSH-E2EE-WC-CORE-START===";
21
+ export const CORE_END = "/* ===DSH-E2EE-WC-CORE-END===";
22
+
23
+ /** 从 native.html 抽出纯原语区间(文本)。 */
24
+ export function extractNativeWcCore(html = readNativeHtml()) {
25
+ const s = html.indexOf(CORE_START);
26
+ const e = html.indexOf(CORE_END);
27
+ if (s === -1 || e === -1 || e <= s) throw new Error("native.html 缺少 DSH-E2EE-WC-CORE 区间");
28
+ const bodyStart = html.indexOf("\n", s) + 1;
29
+ return html.slice(bodyStart, e).replace(/\n\s*$/, "");
30
+ }
31
+
32
+ // ---------- Phase-4 一次性交接单(DH-CORE)抽取 ----------
33
+ export const HANDOVER_START = "/* ===DSH-E2EE-HANDOVER-CORE-START===";
34
+ export const HANDOVER_END = "/* ===DSH-E2EE-HANDOVER-CORE-END===";
35
+
36
+ /** 从 native.html 抽出交接单纯函数区间(文本;依赖 wcBytesToB64,需与 WC-CORE 同模块)。 */
37
+ export function extractNativeHandoverCore(html = readNativeHtml()) {
38
+ const s = html.indexOf(HANDOVER_START);
39
+ const e = html.indexOf(HANDOVER_END);
40
+ if (s === -1 || e === -1 || e <= s) throw new Error("native.html 缺少 DSH-E2EE-HANDOVER-CORE 区间");
41
+ const bodyStart = html.indexOf("\n", s) + 1;
42
+ return html.slice(bodyStart, e).replace(/\n\s*$/, "");
43
+ }
44
+
45
+ export function readNativeHtml() {
46
+ return readFileSync(NATIVE_HTML_PATH, "utf8");
47
+ }
48
+
49
+ /** 需要暴露给测试/桥端互通的浏览器端名字(与 native.html 内声明一致)。 */
50
+ export const WC_EXPORTS = [
51
+ "WC_ENVELOPE_CONTENT_TYPE", "WC_ENVELOPE_HEADER", "WC_E2EE_CAP", "WC_DIR_P2B", "WC_DIR_B2P",
52
+ "WC_MK_BYTES", "WC_NONCE_BYTES", "WC_TAG_BYTES", "WC_E2EE_VERSION",
53
+ "wcNormalizePassword", "wcB64ToBytes", "wcBytesToB64", "wcUtf8", "wcUtf8Decode",
54
+ "wcDecodeSalt", "wcNormalizeKdf", "wcDeriveMasterKey", "wcHkdf", "wcDeriveShk",
55
+ "wcNewSessId", "wcRandomB64", "wcRandomHex", "wcSha256Bytes",
56
+ "WcE2eeSession", "wcErr",
57
+ "wcDecodeHttpRequestPlain", "wcEncodeHttpRequestPlain",
58
+ "wcDecodeHttpResponsePlain", "wcEncodeHttpResponsePlain", "wcGunzip",
59
+ "wcParseWsE2eeParams", "wcEnvelopeHeaders", "wcParseEnvelopeMarker", "wcHasEnvelopeMarker", "wcHeaderValueOf"
60
+ ];
61
+
62
+ let cached = null;
63
+
64
+ /** 抽取 + import(幂等缓存)。 */
65
+ export async function loadNativeWcCore(html) {
66
+ if (cached) return cached;
67
+ const core = extractNativeWcCore(html);
68
+ const src = core + "\nexport { " + WC_EXPORTS.join(", ") + " };\n";
69
+ const b64 = Buffer.from(src, "utf8").toString("base64");
70
+ const mod = await import("data:text/javascript;base64," + b64);
71
+ cached = mod;
72
+ return mod;
73
+ }
74
+
75
+ export const HANDOVER_EXPORTS = [
76
+ "DSH_E2EE_HANDOVER_KEY",
77
+ "dshE2eeHandoverEncode",
78
+ "dshE2eeHandoverWrite",
79
+ "dshE2eeHandoverClear"
80
+ ];
81
+
82
+ /** 抽取 native 交接单纯函数 + import(与 WC-CORE 同模块以复用 wcBytesToB64;幂等缓存)。 */
83
+ export async function loadNativeHandoverCore(html) {
84
+ const core = extractNativeWcCore(html);
85
+ const handover = extractNativeHandoverCore(html);
86
+ const src =
87
+ core + "\n" + handover +
88
+ "\nexport { wcBytesToB64, " + HANDOVER_EXPORTS.join(", ") + " };\n";
89
+ const b64 = Buffer.from(src, "utf8").toString("base64");
90
+ const mod = await import("data:text/javascript;base64," + b64);
91
+ return mod;
92
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@mrrisega/dsh-remote",
3
- "version": "0.6.0-beta.7",
3
+ "version": "0.6.0-beta.8",
4
4
  "description": "手机远程控制 DeepSeek Harness · Remote control DeepSeek Harness (dsh web) from any phone browser — 100% 全功能 App 级体验:发消息、看工具执行、审批权限、改设置、管凭据,含特权操作,免内网穿透。一条命令安装 npx @mrrisega/dsh-remote。Mobile remote control for DSH, self-host or SaaS, no server needed on LAN.",
5
5
  "keywords": [
6
6
  "deepseek-harness",
@@ -52,7 +52,7 @@
52
52
  "test:router": "node --test packages/relay-router/test/native-device-selection.test.mjs packages/relay-router/test/quotas.test.mjs",
53
53
  "test:plugin": "node --test --test-concurrency=1 packages/dsh-remote-web/test/*.test.mjs",
54
54
  "test:bridge": "node --test clients/dsh-remote/test/*.test.mjs",
55
- "check": "node --check clients/dsh-remote/dsh-bridge.mjs && node --check dsh-setup.mjs && node --check packages/dsh-remote-web/lib/index.js && node --check packages/dsh-remote-web/lib/client.js && bash -n deploy/install-open.sh"
55
+ "check": "node --check clients/dsh-remote/dsh-bridge.mjs && node --check clients/dsh-remote/e2ee-shim.mjs && node --check dsh-setup.mjs && node --check packages/dsh-remote-web/lib/index.js && node --check packages/dsh-remote-web/lib/client.js && bash -n deploy/install-open.sh"
56
56
  },
57
57
  "dependencies": {
58
58
  "ws": "^8.18.0"
@@ -146,6 +146,9 @@ window.__ModuleLoader__.load({
146
146
  ".dru-dev-sub{font-size:11px;color:#8c959f;margin-top:3px}",
147
147
  ".dru-dev-tag{font-size:10.5px;color:#8c959f;border:1px solid #d0d7de;border-radius:999px;padding:0 7px;flex:none;white-space:nowrap}",
148
148
  ".dru-dev-tag-off{color:#cf222e;border-color:#ffb3b6;background:#fff0f1}",
149
+ // Phase-5:端到端加密(E2EE)状态行 —— 启用=绿字绿点,未启用=灰字(纯文字状态行,不打扰)
150
+ ".dru-e2ee-line{display:flex;align-items:center;gap:7px;margin-top:6px;font-size:12px;line-height:1.5;color:#57606a}",
151
+ ".dru-e2ee-line.ok{color:#1a7f37}",
149
152
  // 左下角「远程访问」快捷小手机图标(安装后常驻;首次点击前带红点)
150
153
  ".dru-fab{position:fixed;left:18px;bottom:18px;z-index:2147482100;width:46px;height:46px;border-radius:50%;background:#0969da;color:#fff;border:none;display:flex;align-items:center;justify-content:center;font-size:22px;line-height:1;box-shadow:0 6px 18px rgba(0,0,0,.28);cursor:pointer;font-family:inherit;padding:0}",
151
154
  ".dru-fab:hover{background:#0860bd}",
@@ -984,6 +987,26 @@ window.__ModuleLoader__.load({
984
987
  );
985
988
  }
986
989
 
990
+ // ── 端到端加密(E2EE,Phase-5):bridge 状态 → 可读文案映射 ─────────────
991
+ // bridge 启停时把开关结果写入 <relayDir>/.e2ee-state.json({enabled,reason,profile,epoch,caps},
992
+ // 见 clients/dsh-remote/e2ee-client.mjs),node 半随 /dsh-remote/status 以 service.e2ee 下发;
993
+ // 此处只做“可读文案”映射(协议 docs/e2ee-protocol.md §2.3/§7.3)。
994
+ var E2EE_DISABLED_COPY = {
995
+ server_disabled: "等待服务端开启 E2EE(灰度中,当前为加密准备)",
996
+ params_unreachable: "当前为普通安全连接(HTTPS)",
997
+ disabled_by_config: "当前为普通安全连接(HTTPS)",
998
+ derive_failed: "账号密码已变更,需在「🔑 账号」重新登录后恢复端到端加密(当前为普通安全连接(HTTPS))",
999
+ };
1000
+ var E2EE_DISABLED_FALLBACK = "当前为普通安全连接(HTTPS)";
1001
+ /** 把 service.e2ee 归一化为可读状态行;e2ee 缺失/旧 host 未下发 → null(不打扰)。 */
1002
+ function describeE2ee(e2ee) {
1003
+ if (!e2ee || typeof e2ee !== "object") return null;
1004
+ if (e2ee.enabled === true) return { kind: "ok", text: "🔒 端到端加密已启用(手机解锁后生效)" };
1005
+ var reason = typeof e2ee.reason === "string" && e2ee.reason ? e2ee.reason : "";
1006
+ var text = E2EE_DISABLED_COPY[reason] || E2EE_DISABLED_FALLBACK; // reason 未知 → 兜底
1007
+ return { kind: "off", text: text };
1008
+ }
1009
+
987
1010
  // ── 面板主体(渲染于设置页 settings.section 栏目内) ─────────────────────
988
1011
  function RemoteControlSection(props) {
989
1012
 
@@ -1477,11 +1500,23 @@ window.__ModuleLoader__.load({
1477
1500
  ? "请先登录(下方账号卡片)后启用远程访问"
1478
1501
  : serviceRunning ? "已连接(可远程访问)" : "等待设备连接";
1479
1502
  var dotCls = "dru-dot " + (loggedInSaaS && serviceRunning ? "dru-dot-on" : "dru-dot-off");
1503
+ // Phase-5:端到端加密(E2EE)状态行 —— 未登录/旧 host 未下发 e2ee 一律不渲染
1504
+ // (桌面宽屏与手机镜像共用同一面板:纯文字状态行、不弹层不打扰);启用=绿点绿字,
1505
+ // 未启用=灰字 + 原因映射(server_disabled 等待灰度开启 / 其余回退普通 HTTPS)。
1506
+ function renderE2eeBadge() {
1507
+ if (!loggedInSaaS) return null;
1508
+ var e = describeE2ee(st && st.service && st.service.e2ee);
1509
+ if (!e) return null;
1510
+ return h("div", { className: "dru-e2ee-line" + (e.kind === "ok" ? " ok" : "") },
1511
+ h("span", { className: "dru-dot " + (e.kind === "ok" ? "dru-dot-on" : "dru-dot-off") }),
1512
+ h("span", null, e.text));
1513
+ }
1480
1514
  return card("📱 远程访问", [
1481
1515
  h("div", { className: "dru-status-line" },
1482
1516
  h("span", { className: dotCls }),
1483
1517
  h("span", null, statusTxt)
1484
1518
  ),
1519
+ renderE2eeBadge(),
1485
1520
  akeyMsg ? h("div", { className: "dru-msg dru-msg-" + akeyMsg.kind, style: { marginTop: 8 } }, akeyMsg.text) : null,
1486
1521
  hasKey ? h("div", null, [
1487
1522
  h("div", { className: "dru-url big", style: { marginTop: 8 } },
@@ -1692,7 +1727,7 @@ window.__ModuleLoader__.load({
1692
1727
  h("div", { className: "dru-hint", style: { marginBottom: 6 } }, "📱 远程访问:用手机或另一台电脑的浏览器,随时随地使用同一份 dsh web——人在哪都能用(免公网 IP、免内网穿透);官方托管中继,4G/5G 即用,也可自建服务。"),
1693
1728
  h("div", { className: "dru-hint", style: { marginBottom: 6 } }, "🛠 电脑端一键安装:bridge 与「远程访问」面板一次到位——云端/自建切换、账号登录、bridge 启停、一次性扫码访问、已授权设备管理、意见反馈都在这里。"),
1694
1729
  h("div", { className: "dru-hint", style: { marginBottom: 6 } }, "🔒 安全与通道:HTTP / WebSocket 全量透传,一次性访问密钥认证,面板实时显示设备与已授权设备列表;服务端可配置流量配额。"),
1695
- h("div", { className: "dru-hint" }, "🛡 端到端流量保护:可选对通道做端到端加密保护,传输全程不暴露本机公网 IP(详见项目 README「安全」说明)。")
1730
+ h("div", { className: "dru-hint" }, "🛡 端到端加密(灰度开启中):服务端开启后,手机↔电脑之间的消息内容用「你的账号密码派生密钥」端到端加密——密钥与密码不落服务端(仅存校验值),中继只可见路径/大小/时间(详见 README「安全与隐私」)。")
1696
1731
  ]),
1697
1732
  // 版本与更新(自管理:检测新版 / 一键在线更新 / 彻底卸载)
1698
1733
  h(SelfManageCard, null),
@@ -804,6 +804,31 @@ async function proxyRevokeMobileSession(relayDir, req, res) {
804
804
 
805
805
  // ---------- 综合状态 ----------
806
806
 
807
+ /**
808
+ * 读取 bridge 写入的 E2EE 开关状态(<relayDir>/.e2ee-state.json,写入方见
809
+ * clients/dsh-remote/e2ee-client.mjs 的 writeE2eeStateFile:{enabled, reason, profile, epoch, caps, at})。
810
+ * 归一化后只透出固定展示字段(enabled/reason/profile/epoch/caps),at 等调试字段不外泄;
811
+ * 文件缺失/损坏一律视为「未启用(明文回退)」——面板据此显示普通安全连接。
812
+ */
813
+ export function readE2eeStateFile(relayDir) {
814
+ const none = () => ({ enabled: false, reason: "no_state_file", profile: "", epoch: 0, caps: [] });
815
+ try {
816
+ const f = join(relayDir, ".e2ee-state.json");
817
+ if (!existsSync(f)) return none();
818
+ const s = JSON.parse(readFileSync(f, "utf8"));
819
+ if (!s || typeof s !== "object") return none();
820
+ return {
821
+ enabled: s.enabled === true,
822
+ reason: typeof s.reason === "string" && s.reason ? s.reason : "",
823
+ profile: typeof s.profile === "string" ? s.profile : "",
824
+ epoch: Number.isInteger(s.epoch) && s.epoch >= 0 ? s.epoch : 0,
825
+ caps: Array.isArray(s.caps) ? s.caps.filter((c) => typeof c === "string") : [],
826
+ };
827
+ } catch {
828
+ return none();
829
+ }
830
+ }
831
+
807
832
  async function composeStatus(relayDir) {
808
833
  const cfg = loadConfig(relayDir);
809
834
  const launchd = launchdStatus();
@@ -838,6 +863,9 @@ async function composeStatus(relayDir) {
838
863
  manual,
839
864
  running: launchd.running || manual.bridge.length > 0,
840
865
  bindError,
866
+ // E2EE 开关状态(bridge 写 .e2ee-state.json;文件缺失 = 未启用明文):
867
+ // {enabled, reason, profile, epoch, caps},供面板「📱 远程访问」卡展示加密状态。
868
+ e2ee: readE2eeStateFile(relayDir),
841
869
  },
842
870
  host: hostname(),
843
871
  };
@@ -946,7 +974,7 @@ const PLUGIN_ID = "dsh-remote-web";
946
974
  const PLUGIN_LEGACY_IDS = ["dsh-remote-ui"];
947
975
  const PLUGIN_ALL_IDS = [PLUGIN_ID, ...PLUGIN_LEGACY_IDS];
948
976
  /** 插件自身发布版本(与 dsh-remote 根包同步递增)。 */
949
- const PLUGIN_VERSION = "0.6.0-beta.7";
977
+ const PLUGIN_VERSION = "0.6.0-beta.8";
950
978
  const UPDATE_LOG = ".dsh-update.log";
951
979
  const UPDATE_MARKER = ".dsh-update-running";
952
980
 
@@ -0,0 +1,141 @@
1
+ // 插件 node 半 E2EE 状态读取(Phase-5):composeStatus 读取 relayDir/.e2ee-state.json →
2
+ // /dsh-remote/status 的 service.e2ee = {enabled, reason, profile, epoch, caps};文件缺失/损坏=未启用明文。
3
+ // 覆盖:readE2eeStateFile 归一化单测 + 真实 apply 路由集成(写状态文件 → status 带回;删文件 → 默认明文)。
4
+ import assert from "node:assert/strict";
5
+ import http from "node:http";
6
+ import { chmod, mkdtemp, mkdir, rm, writeFile } from "node:fs/promises";
7
+ import os from "node:os";
8
+ import path from "node:path";
9
+ import test from "node:test";
10
+ import { apply, readE2eeStateFile } from "../lib/index.js";
11
+
12
+ const E2EE_DEFAULT = { enabled: false, reason: "no_state_file", profile: "", epoch: 0, caps: [] };
13
+
14
+ test("readE2eeStateFile:缺失/损坏 → 默认明文;有效状态归一化(at 等调试字段不外泄)", async () => {
15
+ const tempDir = await mkdtemp(path.join(os.tmpdir(), "dsh-e2ee-read-"));
16
+ try {
17
+ // 1) 文件缺失 → 默认(未启用明文)
18
+ assert.deepEqual(readE2eeStateFile(tempDir), E2EE_DEFAULT);
19
+
20
+ // 2) 损坏 JSON / 非对象 → 默认
21
+ await writeFile(path.join(tempDir, ".e2ee-state.json"), "{oops", "utf8");
22
+ assert.deepEqual(readE2eeStateFile(tempDir), E2EE_DEFAULT);
23
+ await writeFile(path.join(tempDir, ".e2ee-state.json"), "42", "utf8");
24
+ assert.deepEqual(readE2eeStateFile(tempDir), E2EE_DEFAULT);
25
+
26
+ // 3) 有效状态:只透出固定展示字段,enabled/caps 等按布尔/字符串严格归一化
27
+ await writeFile(path.join(tempDir, ".e2ee-state.json"), JSON.stringify({
28
+ enabled: true,
29
+ reason: "ok",
30
+ profile: "pbkdf2-sha256-600k",
31
+ epoch: 3,
32
+ caps: ["e2ee-v2", 7, null, "e2ee-http"],
33
+ at: Date.now(), // 调试字段
34
+ }), "utf8");
35
+ assert.deepEqual(readE2eeStateFile(tempDir), {
36
+ enabled: true,
37
+ reason: "ok",
38
+ profile: "pbkdf2-sha256-600k",
39
+ epoch: 3,
40
+ caps: ["e2ee-v2", "e2ee-http"],
41
+ });
42
+
43
+ // 4) 畸形字段兜底:enabled 非布尔→false;reason 非字符串→空;epoch 负数/字符串→0
44
+ await writeFile(path.join(tempDir, ".e2ee-state.json"), JSON.stringify({
45
+ enabled: "yes",
46
+ reason: 123,
47
+ profile: null,
48
+ epoch: -1,
49
+ caps: "e2ee-v2",
50
+ }), "utf8");
51
+ assert.deepEqual(readE2eeStateFile(tempDir), {
52
+ enabled: false,
53
+ reason: "",
54
+ profile: "",
55
+ epoch: 0,
56
+ caps: [],
57
+ });
58
+ } finally {
59
+ await rm(tempDir, { recursive: true, force: true });
60
+ }
61
+ });
62
+
63
+ /** 假账号 API:只回 public-config(composeStatus 取 app_url 用),其余 404。 */
64
+ function startFakeRelay() {
65
+ const srv = http.createServer((req, res) => {
66
+ const url = new URL(req.url, "http://x");
67
+ res.writeHead(200, { "content-type": "application/json" });
68
+ if (url.pathname === "/api/public-config") {
69
+ res.end(JSON.stringify({ app_url: "https://app.test/app/", api_url: `http://127.0.0.1:${srv.address().port}/relay-api` }));
70
+ return;
71
+ }
72
+ res.end(JSON.stringify({ error: { code: "not_found" } }));
73
+ });
74
+ return new Promise((resolve) => srv.listen(0, "127.0.0.1", () => resolve(srv)));
75
+ }
76
+
77
+ /** 装载插件路由 + 假 launchctl/pgrep/ps(避免触碰真实系统服务),返回 host http server 与 base。 */
78
+ async function bootPlugin(relayDir) {
79
+ const fakeBin = path.join(relayDir, "fakebin");
80
+ await mkdir(fakeBin, { recursive: true });
81
+ const noop = "#!/bin/sh\nexit 0\n";
82
+ for (const name of ["launchctl", "pgrep", "ps"]) {
83
+ const p = path.join(fakeBin, name);
84
+ await writeFile(p, noop);
85
+ await chmod(p, 0o755);
86
+ }
87
+ const oldPath = process.env.PATH;
88
+ process.env.PATH = `${fakeBin}:${oldPath}`;
89
+
90
+ const routes = new Map();
91
+ apply({
92
+ webServer: { register(route) { routes.set(route.path, route.handler); return () => {}; } },
93
+ effect(register) { return register(); },
94
+ logger: { info() {}, warn() {} },
95
+ }, { relayDir });
96
+ const host = http.createServer((req, res) => {
97
+ const handler = routes.get(new URL(req.url, "http://x").pathname);
98
+ (handler || ((_r, rs) => { rs.writeHead(404); rs.end(); }))(req, res);
99
+ });
100
+ await new Promise((resolve) => host.listen(0, "127.0.0.1", resolve));
101
+ return { host, base: `http://127.0.0.1:${host.address().port}`, fakeBin, oldPath };
102
+ }
103
+
104
+ test("composeStatus:.e2ee-state.json 启用 → /dsh-remote/status 的 service.e2ee;删除 → 默认明文", async () => {
105
+ const relay = await startFakeRelay();
106
+ const tempDir = await mkdtemp(path.join(os.tmpdir(), "dsh-e2ee-status-"));
107
+ const stateFile = path.join(tempDir, ".e2ee-state.json");
108
+ await writeFile(path.join(tempDir, ".dsh-config.json"), JSON.stringify({
109
+ phone: "13800000000",
110
+ device_id: "dev-e2ee",
111
+ api_url: `http://127.0.0.1:${relay.address().port}/relay-api`,
112
+ }));
113
+ const boot = await bootPlugin(tempDir);
114
+ try {
115
+ // 1) bridge 已启用 E2EE(写状态文件)→ status 携带归一化 service.e2ee
116
+ await writeFile(stateFile, JSON.stringify({
117
+ enabled: true, reason: "ok", profile: "pbkdf2-sha256-600k", epoch: 2, caps: ["e2ee-v2"], at: Date.now(),
118
+ }));
119
+ let status = await (await fetch(`${boot.base}/dsh-remote/status`)).json();
120
+ assert.equal(status.ok, true);
121
+ assert.deepEqual(status.service.e2ee, {
122
+ enabled: true, reason: "ok", profile: "pbkdf2-sha256-600k", epoch: 2, caps: ["e2ee-v2"],
123
+ });
124
+
125
+ // 2) bridge 未启用(服务端灰度关)→ 原样上报 reason,供面板映射可读文案
126
+ await writeFile(stateFile, JSON.stringify({ enabled: false, reason: "server_disabled", profile: "", epoch: 0, caps: [] }));
127
+ status = await (await fetch(`${boot.base}/dsh-remote/status`)).json();
128
+ assert.equal(status.service.e2ee.enabled, false);
129
+ assert.equal(status.service.e2ee.reason, "server_disabled");
130
+
131
+ // 3) 文件缺失 = 未启用明文(默认兜底)
132
+ await rm(stateFile, { force: true });
133
+ status = await (await fetch(`${boot.base}/dsh-remote/status`)).json();
134
+ assert.deepEqual(status.service.e2ee, E2EE_DEFAULT);
135
+ } finally {
136
+ boot.host.close();
137
+ relay.close();
138
+ process.env.PATH = boot.oldPath;
139
+ await rm(tempDir, { recursive: true, force: true });
140
+ }
141
+ });