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/CHANGELOG.md +29 -0
- package/README.md +187 -106
- package/README_EN.md +187 -106
- package/lib/auth/crypto.js +35 -21
- package/lib/auth/gate.js +2 -1
- package/lib/auth/token.d.ts +13 -1
- package/lib/auth/token.js +53 -20
- package/lib/client.js +171 -89
- package/lib/routes/api.js +8 -2
- package/lib/routes/login-page.js +54 -12
- package/package.json +7 -3
- package/lib/types/auth.d.ts +0 -49
- package/lib/types/auth.js +0 -4
package/lib/routes/api.js
CHANGED
|
@@ -350,14 +350,20 @@ export function createRoutes(store) {
|
|
|
350
350
|
;
|
|
351
351
|
keepAlive.unref();
|
|
352
352
|
}
|
|
353
|
-
|
|
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
|
];
|
package/lib/routes/login-page.js
CHANGED
|
@@ -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
|
|
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
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
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
|
-
|
|
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
|
|
278
|
-
return { encryptedCredential:
|
|
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
|
-
//
|
|
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.
|
|
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",
|
package/lib/types/auth.d.ts
DELETED
|
@@ -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