@mrrisega/dsh-remote 0.6.0-beta.7 → 0.6.0-beta.9
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 +30 -0
- package/clients/dsh-remote/dsh-bridge.mjs +291 -4
- package/clients/dsh-remote/e2ee-client.mjs +711 -0
- package/clients/dsh-remote/e2ee-shim-script.js +697 -0
- package/clients/dsh-remote/e2ee-shim.mjs +158 -0
- package/clients/dsh-remote/test/e2ee-bridge.test.mjs +630 -0
- package/clients/dsh-remote/test/e2ee-client.test.mjs +365 -0
- package/clients/dsh-remote/test/e2ee-shim.test.mjs +507 -0
- package/clients/dsh-remote/test/e2ee-wecrypto-parity.test.mjs +200 -0
- package/clients/dsh-remote/test/lib-e2ee-shim.mjs +59 -0
- package/clients/dsh-remote/test/lib-native-e2ee.mjs +92 -0
- package/package.json +2 -2
- package/packages/dsh-remote-web/lib/client.js +214 -37
- package/packages/dsh-remote-web/lib/index.js +92 -2
- package/packages/dsh-remote-web/test/access-key-sessions.test.mjs +79 -9
- package/packages/dsh-remote-web/test/e2ee-state.test.mjs +141 -0
- package/packages/dsh-remote-web/test/remote-access-ui.test.mjs +174 -2
- package/packages/dsh-remote-web/test/settings-entry.test.mjs +72 -3
|
@@ -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.
|
|
3
|
+
"version": "0.6.0-beta.9",
|
|
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"
|