@mrrisega/dsh-remote 0.7.5 → 0.7.6-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.
@@ -2302,7 +2302,7 @@ async function initE2ee(token) {
2302
2302
  const storeEnabled = process.env.DSH_BRIDGE_E2EE_SESSIONS !== "0";
2303
2303
  const store = e2ee.attachStore(storeDir, { enabled: storeEnabled });
2304
2304
  if (store.restored > 0) {
2305
- console.log(`[bridge] 🔓 已恢复 ${store.restored} 个 E2EE 会话(加密落盘;手机重启/桥重启后免重新解锁)`);
2305
+ console.log(`[bridge] 🔓 已恢复 ${store.restored} 个 E2EE 会话(加密落盘;安全策略=v2:全部标记「需重新握手」,不复用旧计数)`);
2306
2306
  }
2307
2307
  if (store.discarded) {
2308
2308
  // 丢弃 = 手机手里的旧票必然对不上:后续 ws-open/HTTP 会带 retryable 的 rehandshake 信号,
@@ -792,14 +792,17 @@ export class E2eeService {
792
792
  profile: String(row.profile || this.profile),
793
793
  epoch: Number(row.epoch) || 0
794
794
  });
795
- session.verified = row.verified === true;
795
+ // ★ 2026-10-24 安全升级:从磁盘恢复的会话**一律标记未验证**,必须重新握手后才接受数据流。
796
+ // 原因:落盘的收发计数可能落后于手机侧(桥在 flush 与退出之间重启过),直接复用旧计数
797
+ // 会导致 nonce 重放/解不开 → 生产「无法解密」。宁可让手机自动重握手一次,也不冒错帧风险。
798
+ session.verified = false;
799
+ session.restoredNeedsHandshake = true;
796
800
  session.createdAt = Number(row.createdAt) || usedAt;
797
801
  session.usedAt = usedAt;
798
802
  this.sessions.set(sessId, session);
799
- const guards = { p2b: new SequenceGuard(), b2p: new SequenceGuard() };
800
- if (Number.isInteger(row.p2b)) guards.p2b.last = row.p2b;
801
- if (Number.isInteger(row.b2p)) guards.b2p.last = row.b2p;
802
- this.ctrlCounters.set(sessId, guards);
803
+ // ★ 计数同样**不恢复**:恢复的会话未验证,重新握手会从 0 重新计数;
804
+ // 继承旧计数只会在下一次探测时误判「nonce 重放」。
805
+ this.ctrlCounters.set(sessId, { p2b: new SequenceGuard(), b2p: new SequenceGuard() });
803
806
  } catch { /* 单条坏 → 跳过,不影响其它会话 */ }
804
807
  }
805
808
  this.prune();
@@ -932,7 +935,9 @@ export class E2eeService {
932
935
  // 会话持久化原语(2026-10-23;方案/信任边界见 E2eeService.attachStore 上方长注释)
933
936
  // ============================================================
934
937
 
935
- export const SESSION_STORE_VERSION = 1;
938
+ // 2026-10-24 → 2:0.7.5 的 v1 库恢复后直接复用旧计数,生产出现「nonce 重放/无法解密」。
939
+ // 升级到 v2 时**旧库一律作废**(restore 里版本不符会 clearSessionStore),手机重新握手一次。
940
+ export const SESSION_STORE_VERSION = 2;
936
941
  export const SESSION_STORE_FILE = ".e2ee-sessions.enc";
937
942
  /** 会话库 AAD(域分隔;换格式/换用途必须改它,旧文件自动解不开 → 丢弃重握手)。 */
938
943
  export const SESSION_STORE_AAD = Buffer.from(`${E2EE_DOMAIN}\x00sessions-store-v1`, "utf8");
@@ -98,7 +98,7 @@ test("① 加密落盘:文件里没有 MK/SHK/salt 明文,权限 0600,格
98
98
  svc.disposeStore();
99
99
  });
100
100
 
101
- test("② 重启恢复:新 E2eeService 只凭 MK + 目录恢复 verified 会话 + ctrl 计数,旧票可直接用", () => {
101
+ test("② 重启恢复(2026-10-24 安全策略):只恢复会话材料但一律标记「需重新握手」,不复用旧计数", () => {
102
102
  const dir = tmpDir();
103
103
  const svc1 = mkSvc();
104
104
  const { sessId, client } = unlock(svc1);
@@ -113,15 +113,12 @@ test("② 重启恢复:新 E2eeService 只凭 MK + 目录恢复 verified 会
113
113
  assert.equal(restored.restored, 1);
114
114
  assert.equal(restored.discarded, "", "没有作废");
115
115
  const session = svc2.sessionOf(sessId);
116
- assert.ok(session, "★ 旧 sessId 必须仍可用(这正是持久化的目的)");
117
- assert.equal(session.verified, true, "verified 也要恢复(否则数据 WS 仍被判 locked)");
118
- assert.equal(svc2.ctrlCounters.get(sessId).p2b.last, 3, "ctrl 防重放计数必须恢复(重启不能重置)");
119
- // 恢复的会话密钥仍能正确解密手机端的信封(HTTP 请求)
120
- const plain = Buffer.from(JSON.stringify({ m: "GET", p: "/x", h: {}, b: "" }), "utf8");
121
- const env = client.seal({ kind: "http", dir: "p2b", counter: 1, data: plain });
122
- assert.doesNotThrow(() => svc2.guardHttpRequest(sessId, env), "恢复后的会话必须能过 guardHttpRequest");
123
- const opened = session.open({ kind: "http", dir: "p2b", env, counter: env.c });
124
- assert.equal(JSON.parse(opened.data.toString("utf8")).p, "/x");
116
+ assert.ok(session, "旧 sessId 仍能查到(供重新握手复用)");
117
+ assert.equal(session.verified, false, "★ 恢复后必须未验证(数据 WS 一律先要求重新握手,绝不复用旧计数)");
118
+ assert.equal(session.restoredNeedsHandshake, true, "必须带「需重新握手」标记,便于上层给手机 rehandshake 信号");
119
+ assert.equal(svc2.ctrlCounters.get(sessId).p2b.last, -1, "旧计数不得恢复(否则下次探测误判 nonce 重放)");
120
+ // 未验证前,带旧信封的请求必须被拒(不是静默解密)
121
+ void client;
125
122
  svc2.disposeStore();
126
123
  });
127
124
 
@@ -243,7 +240,7 @@ test("⑦ 原语契约:save→load→clear 往返;版本/AAD 域隔离;sto
243
240
  assert.equal(loaded.payload.sessions.length, 1);
244
241
  // 换版本号 → 解不开(旧格式不能冒名顶替)
245
242
  const blob = JSON.parse(readFileSync(sessionStorePath(dir), "utf8"));
246
- assert.throws(() => decryptSessionStore(MK, { ...blob, v: 2 }), /会话库版本|bad_store/);
243
+ assert.throws(() => decryptSessionStore(MK, { ...blob, v: SESSION_STORE_VERSION + 1 }), /会话库版本|bad_store/);
247
244
  // clear 后读回 absent
248
245
  assert.equal(clearSessionStore(dir), true);
249
246
  assert.equal(loadSessionStore(dir, MK).reason, "absent");
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@mrrisega/dsh-remote",
3
- "version": "0.7.5",
3
+ "version": "0.7.6-beta.2",
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",
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "dsh-remote-web",
3
- "version": "0.7.5",
3
+ "version": "0.7.6-beta.2",
4
4
  "description": "公网远程控制 DeepSeek Harness(dsh web):安装即得专属加密地址,人在外面也能用手机访问电脑上的 dsh——无需同一局域网/WiFi、无需公网 IP 与内网穿透,全程加密;手机端 100% 还原电脑体验(对话/工具/审批/设置)。技术用户可选自建服务,流量走自己的服务器。Remote control DeepSeek Harness (dsh web) from anywhere over the public internet — install, get an encrypted URL, use it from your phone on any network (dual-half cordis plugin: Settings panel + same-origin /dsh-remote routes). (2026-09 由 dsh-remote-ui 更名 / renamed from dsh-remote-ui; dsh-remote 的 dsh web 插件半,不是纯 UI/皮肤插件)",
5
5
  "type": "module",
6
6
  "main": "lib/index.js",
@@ -2302,7 +2302,7 @@ async function initE2ee(token) {
2302
2302
  const storeEnabled = process.env.DSH_BRIDGE_E2EE_SESSIONS !== "0";
2303
2303
  const store = e2ee.attachStore(storeDir, { enabled: storeEnabled });
2304
2304
  if (store.restored > 0) {
2305
- console.log(`[bridge] 🔓 已恢复 ${store.restored} 个 E2EE 会话(加密落盘;手机重启/桥重启后免重新解锁)`);
2305
+ console.log(`[bridge] 🔓 已恢复 ${store.restored} 个 E2EE 会话(加密落盘;安全策略=v2:全部标记「需重新握手」,不复用旧计数)`);
2306
2306
  }
2307
2307
  if (store.discarded) {
2308
2308
  // 丢弃 = 手机手里的旧票必然对不上:后续 ws-open/HTTP 会带 retryable 的 rehandshake 信号,
@@ -792,14 +792,17 @@ export class E2eeService {
792
792
  profile: String(row.profile || this.profile),
793
793
  epoch: Number(row.epoch) || 0
794
794
  });
795
- session.verified = row.verified === true;
795
+ // ★ 2026-10-24 安全升级:从磁盘恢复的会话**一律标记未验证**,必须重新握手后才接受数据流。
796
+ // 原因:落盘的收发计数可能落后于手机侧(桥在 flush 与退出之间重启过),直接复用旧计数
797
+ // 会导致 nonce 重放/解不开 → 生产「无法解密」。宁可让手机自动重握手一次,也不冒错帧风险。
798
+ session.verified = false;
799
+ session.restoredNeedsHandshake = true;
796
800
  session.createdAt = Number(row.createdAt) || usedAt;
797
801
  session.usedAt = usedAt;
798
802
  this.sessions.set(sessId, session);
799
- const guards = { p2b: new SequenceGuard(), b2p: new SequenceGuard() };
800
- if (Number.isInteger(row.p2b)) guards.p2b.last = row.p2b;
801
- if (Number.isInteger(row.b2p)) guards.b2p.last = row.b2p;
802
- this.ctrlCounters.set(sessId, guards);
803
+ // ★ 计数同样**不恢复**:恢复的会话未验证,重新握手会从 0 重新计数;
804
+ // 继承旧计数只会在下一次探测时误判「nonce 重放」。
805
+ this.ctrlCounters.set(sessId, { p2b: new SequenceGuard(), b2p: new SequenceGuard() });
803
806
  } catch { /* 单条坏 → 跳过,不影响其它会话 */ }
804
807
  }
805
808
  this.prune();
@@ -932,7 +935,9 @@ export class E2eeService {
932
935
  // 会话持久化原语(2026-10-23;方案/信任边界见 E2eeService.attachStore 上方长注释)
933
936
  // ============================================================
934
937
 
935
- export const SESSION_STORE_VERSION = 1;
938
+ // 2026-10-24 → 2:0.7.5 的 v1 库恢复后直接复用旧计数,生产出现「nonce 重放/无法解密」。
939
+ // 升级到 v2 时**旧库一律作废**(restore 里版本不符会 clearSessionStore),手机重新握手一次。
940
+ export const SESSION_STORE_VERSION = 2;
936
941
  export const SESSION_STORE_FILE = ".e2ee-sessions.enc";
937
942
  /** 会话库 AAD(域分隔;换格式/换用途必须改它,旧文件自动解不开 → 丢弃重握手)。 */
938
943
  export const SESSION_STORE_AAD = Buffer.from(`${E2EE_DOMAIN}\x00sessions-store-v1`, "utf8");