dsh-remote-mobile 1.1.0 → 1.2.0

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/lib/routes/api.js CHANGED
@@ -350,14 +350,20 @@ export function createRoutes(store) {
350
350
  ;
351
351
  keepAlive.unref();
352
352
  }
353
- req.on('close', () => {
353
+ let cleaned = false;
354
+ const cleanup = () => {
355
+ if (cleaned)
356
+ return;
357
+ cleaned = true;
354
358
  clearInterval(keepAlive);
355
359
  store.off('device-connected', onConnected);
356
360
  store.off('device-online', onOnline);
357
361
  store.off('device-revoked', onRevoked);
358
362
  store.off('ip-security-updated', onSecurityUpdated);
359
363
  store.off('ip-security-alert', onSecurityAlert);
360
- });
364
+ };
365
+ req.on('close', cleanup);
366
+ res.on('close', cleanup);
361
367
  },
362
368
  },
363
369
  ];
@@ -11,7 +11,7 @@ export function getLoginPageHtml(publicKeyPem, lang = 'zh') {
11
11
  headerDesc: isEn ? 'Enter 6-digit dynamic pairing code or persistent password' : '请输入 6 位动态配对码或长期访问密码',
12
12
  placeholder: isEn ? '6-digit code or password' : '6 位临时配对码 / 长期密码',
13
13
  submitBtn: isEn ? 'Authorize & Connect' : '立即授权连接',
14
- footerSecurity: isEn ? '🔒 Protected by 2048-bit RSA-OAEP end-to-end encryption' : '🔒 全程受 2048 位 RSA-OAEP 端到端加密保护',
14
+ footerSecurity: isEn ? '🔒 Protected by 2048-bit RSA encryption' : '🔒 全程受 2048 位 RSA 加密保护',
15
15
  verifying: isEn ? '🔐 Encrypting & verifying...' : '🔐 正在 RSA 加密与验证...',
16
16
  success: isEn ? 'Authorization successful! Entering workspace...' : '授权成功!正在进入工作区...',
17
17
  networkErr: isEn ? 'Network connection error, please try again' : '网络连接异常,请检查网络',
@@ -191,7 +191,7 @@ export function getLoginPageHtml(publicKeyPem, lang = 'zh') {
191
191
  const statusMsg = document.getElementById('statusMsg');
192
192
  const submitBtn = document.getElementById('submitBtn');
193
193
 
194
- // 解析 SPKI 格式公钥并提取 n 和 e
194
+ // 解析 SPKI 格式公钥并提取 n 和 e(用于纯 JS 垫片计算)
195
195
  function parseSpkiKey(pemStr) {
196
196
  const b64 = pemStr.replace(/-----BEGIN PUBLIC KEY-----/g, '').replace(/-----END PUBLIC KEY-----/g, '').replace(/\\s/g, '');
197
197
  const binary = window.atob(b64);
@@ -241,15 +241,25 @@ export function getLoginPageHtml(publicKeyPem, lang = 'zh') {
241
241
  return res;
242
242
  }
243
243
 
244
+ // 纯 JS RSA 垫片加密:使用 CSPRNG (crypto.getRandomValues) 填充
244
245
  function rsaEncryptPureJs(plainText, pubPem) {
245
246
  const { n, e, keyLen } = parseSpkiKey(pubPem);
246
247
  const msgBytes = new TextEncoder().encode(plainText);
247
248
  const padLen = keyLen - 3 - msgBytes.length;
248
- const pad = [];
249
- while (pad.length < padLen) {
250
- const r = Math.floor(Math.random() * 255) + 1;
251
- pad.push(r);
249
+ if (padLen < 8) throw new Error('Message too long');
250
+
251
+ const pad = new Uint8Array(padLen);
252
+ if (window.crypto && typeof window.crypto.getRandomValues === 'function') {
253
+ window.crypto.getRandomValues(pad);
254
+ for (let i = 0; i < padLen; i++) {
255
+ if (pad[i] === 0) pad[i] = (Math.floor(Math.random() * 254) + 1);
256
+ }
257
+ } else {
258
+ for (let i = 0; i < padLen; i++) {
259
+ pad[i] = Math.floor(Math.random() * 255) + 1;
260
+ }
252
261
  }
262
+
253
263
  const em = new Uint8Array(keyLen);
254
264
  em[0] = 0x00;
255
265
  em[1] = 0x02;
@@ -272,22 +282,54 @@ export function getLoginPageHtml(publicKeyPem, lang = 'zh') {
272
282
  return window.btoa(binary);
273
283
  }
274
284
 
275
- function encryptCredential(plainText) {
285
+ // 尝试使用 Web Crypto API 原生 RSA-OAEP SHA-256 加密(安全上下文可用)
286
+ async function trySubtleEncrypt(plainText, pubPem) {
287
+ if (!window.crypto || !window.crypto.subtle) return null;
288
+ const b64 = pubPem.replace(/-----BEGIN PUBLIC KEY-----/g, '').replace(/-----END PUBLIC KEY-----/g, '').replace(/\\s/g, '');
289
+ const binary = window.atob(b64);
290
+ const bytes = new Uint8Array(binary.length);
291
+ for (let i = 0; i < binary.length; i++) bytes[i] = binary.charCodeAt(i);
292
+
293
+ const cryptoKey = await window.crypto.subtle.importKey(
294
+ 'spki',
295
+ bytes.buffer,
296
+ { name: 'RSA-OAEP', hash: 'SHA-256' },
297
+ false,
298
+ ['encrypt']
299
+ );
300
+
301
+ const encBuffer = await window.crypto.subtle.encrypt(
302
+ { name: 'RSA-OAEP' },
303
+ cryptoKey,
304
+ new TextEncoder().encode(plainText)
305
+ );
306
+
307
+ const encBytes = new Uint8Array(encBuffer);
308
+ let binaryEnc = '';
309
+ for (let i = 0; i < encBytes.length; i++) binaryEnc += String.fromCharCode(encBytes[i]);
310
+ return window.btoa(binaryEnc);
311
+ }
312
+
313
+ async function encryptCredential(plainText) {
314
+ try {
315
+ const subtleB64 = await trySubtleEncrypt(plainText, SERVER_RSA_KEY);
316
+ if (subtleB64) return { encryptedCredential: subtleB64 };
317
+ } catch (e) {}
318
+
276
319
  try {
277
- const encryptedB64 = rsaEncryptPureJs(plainText, SERVER_RSA_KEY);
278
- return { encryptedCredential: encryptedB64 };
320
+ const pureB64 = rsaEncryptPureJs(plainText, SERVER_RSA_KEY);
321
+ return { encryptedCredential: pureB64 };
279
322
  } catch (err) {
280
323
  console.error('RSA encrypt error:', err);
281
324
  return { credential: plainText };
282
325
  }
283
326
  }
284
327
 
285
- // 自动检测 URL 参数里的 token 快速登录
328
+ // 检测 URL 参数里的 token,仅回填输入框,不自动触发验证
286
329
  const urlParams = new URLSearchParams(window.location.search);
287
330
  const queryToken = urlParams.get('token');
288
331
  if (queryToken) {
289
332
  input.value = queryToken;
290
- doVerify(queryToken);
291
333
  }
292
334
 
293
335
  form.addEventListener('submit', (e) => {
@@ -304,7 +346,7 @@ export function getLoginPageHtml(publicKeyPem, lang = 'zh') {
304
346
  statusMsg.style.display = 'none';
305
347
 
306
348
  try {
307
- const payload = encryptCredential(credential);
349
+ const payload = await encryptCredential(credential);
308
350
  const res = await fetch('/api/remote-mobile/verify', {
309
351
  method: 'POST',
310
352
  headers: { 'Content-Type': 'application/json' },
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "dsh-remote-mobile",
3
- "version": "1.1.0",
4
- "description": "DeepSeek Hub (DSH) Mobile & Remote Access Security Guard Plugin: Tailscale & LAN bypass, QR scan auth, RSA encryption, brute-force protection, and realtime SSE push.",
3
+ "version": "1.2.0",
4
+ "description": "DeepSeek Hub 移动端与远程访问安全网关插件 | DeepSeek Hub (DSH) Mobile & Remote Access Security Guard Plugin: Tailscale & LAN bypass, QR scan auth, RSA encryption, brute-force protection, and realtime SSE push.",
5
5
  "keywords": [
6
6
  "dsh",
7
7
  "dsh-plugin",
@@ -23,8 +23,11 @@
23
23
  "type": "module",
24
24
  "main": "lib/index.js",
25
25
  "types": "lib/index.d.ts",
26
+ "engines": {
27
+ "node": ">=18.0.0"
28
+ },
26
29
  "scripts": {
27
- "build": "tsc && node scripts/build-client.js",
30
+ "build": "rm -rf lib && tsc && node scripts/build-client.js",
28
31
  "build:server": "tsc",
29
32
  "build:client": "node scripts/build-client.js",
30
33
  "typecheck": "tsc --noEmit",
@@ -66,6 +69,7 @@
66
69
  "cordis.patch.yml",
67
70
  "README.md",
68
71
  "README_EN.md",
72
+ "CHANGELOG.md",
69
73
  "LICENSE"
70
74
  ],
71
75
  "license": "MIT",
@@ -1,49 +0,0 @@
1
- /**
2
- * 移动端与远程访问安全认证 - 核心类型定义
3
- */
4
- export interface PluginConfig {
5
- allowTailscale?: boolean;
6
- allowLan?: boolean;
7
- secret?: string;
8
- secretHash?: string;
9
- maxVisitsPerMinute?: number;
10
- maxFailedAttempts?: number;
11
- lockDurationMs?: number;
12
- }
13
- export interface SessionInfo {
14
- token: string;
15
- ip: string;
16
- createdAt: number;
17
- lastSeenAt: number;
18
- userAgent?: string;
19
- deviceName?: string;
20
- authType: string;
21
- isBypass?: boolean;
22
- }
23
- export interface ShortCodeInfo {
24
- code: string;
25
- token: string;
26
- createdAt: number;
27
- expiresAt: number;
28
- }
29
- export interface IpSecurityStat {
30
- ip: string;
31
- authVisits: number;
32
- firstSeenAt: number;
33
- lastSeenAt: number;
34
- failedAttempts: number;
35
- totalAttempts: number;
36
- lockedUntil?: number;
37
- lockCount: number;
38
- authType?: string;
39
- deviceName?: string;
40
- userAgent?: string;
41
- }
42
- export type DeviceEventType = 'device-connected' | 'device-online' | 'device-revoked' | 'ip-security-alert' | 'ip-security-updated';
43
- export interface DeviceEventPayload {
44
- type: DeviceEventType;
45
- device?: SessionInfo;
46
- ip?: string;
47
- reason?: string;
48
- stat?: IpSecurityStat;
49
- }
package/lib/types/auth.js DELETED
@@ -1,4 +0,0 @@
1
- /**
2
- * 移动端与远程访问安全认证 - 核心类型定义
3
- */
4
- export {};