koishi-plugin-argus 0.1.0 → 0.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/crypto.d.ts +11 -0
- package/lib/index.cjs +53 -1
- package/lib/index.mjs +58 -1
- package/lib/types.d.ts +7 -1
- package/package.json +1 -1
- package/readme.md +8 -0
package/lib/crypto.d.ts
ADDED
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* AES-256-GCM 加密。返回 base64(iv | tag | ciphertext)。
|
|
3
|
+
*/
|
|
4
|
+
export declare function encryptBuffer(plain: Buffer, token: string): string;
|
|
5
|
+
/**
|
|
6
|
+
* 解密 encryptBuffer 的输出。token 不对 / 数据被改 / 格式不匹配都会抛错。
|
|
7
|
+
*/
|
|
8
|
+
export declare function decryptBuffer(payloadBase64: string, token: string): Buffer;
|
|
9
|
+
/** 协议字段:标识 image 字段使用何种加密。当前只用一种。 */
|
|
10
|
+
export declare const ENC_ALGO: "aes-256-gcm";
|
|
11
|
+
export type EncAlgo = typeof ENC_ALGO;
|
package/lib/index.cjs
CHANGED
|
@@ -393,6 +393,37 @@ function formatRemaining(ms) {
|
|
|
393
393
|
}
|
|
394
394
|
__name(formatRemaining, "formatRemaining");
|
|
395
395
|
|
|
396
|
+
// src/crypto.ts
|
|
397
|
+
var import_node_crypto = require("node:crypto");
|
|
398
|
+
var ALGO = "aes-256-gcm";
|
|
399
|
+
var IV_LEN = 12;
|
|
400
|
+
var TAG_LEN = 16;
|
|
401
|
+
var KEY_LEN = 32;
|
|
402
|
+
var SALT = Buffer.from("argus.peek.v1", "utf8");
|
|
403
|
+
var keyCache = /* @__PURE__ */ new Map();
|
|
404
|
+
function deriveKey(token) {
|
|
405
|
+
let key = keyCache.get(token);
|
|
406
|
+
if (key) return key;
|
|
407
|
+
key = (0, import_node_crypto.scryptSync)(token, SALT, KEY_LEN);
|
|
408
|
+
keyCache.set(token, key);
|
|
409
|
+
return key;
|
|
410
|
+
}
|
|
411
|
+
__name(deriveKey, "deriveKey");
|
|
412
|
+
function decryptBuffer(payloadBase64, token) {
|
|
413
|
+
const buf = Buffer.from(payloadBase64, "base64");
|
|
414
|
+
if (buf.length < IV_LEN + TAG_LEN) {
|
|
415
|
+
throw new Error("ciphertext_too_short");
|
|
416
|
+
}
|
|
417
|
+
const iv = buf.subarray(0, IV_LEN);
|
|
418
|
+
const tag = buf.subarray(IV_LEN, IV_LEN + TAG_LEN);
|
|
419
|
+
const ct = buf.subarray(IV_LEN + TAG_LEN);
|
|
420
|
+
const key = deriveKey(token);
|
|
421
|
+
const decipher = (0, import_node_crypto.createDecipheriv)(ALGO, key, iv);
|
|
422
|
+
decipher.setAuthTag(tag);
|
|
423
|
+
return Buffer.concat([decipher.update(ct), decipher.final()]);
|
|
424
|
+
}
|
|
425
|
+
__name(decryptBuffer, "decryptBuffer");
|
|
426
|
+
|
|
396
427
|
// src/commands.ts
|
|
397
428
|
function applyCommands(ctx, server, config, cache) {
|
|
398
429
|
const cmd = ctx.command(
|
|
@@ -449,7 +480,18 @@ function applyCommands(ctx, server, config, cache) {
|
|
|
449
480
|
cache.set(cacheKey, { busy: response.frame });
|
|
450
481
|
return formatBusy(session, client.name, response.frame);
|
|
451
482
|
}
|
|
452
|
-
|
|
483
|
+
let buffer;
|
|
484
|
+
try {
|
|
485
|
+
buffer = decodeImagePayload(response.frame, config.token);
|
|
486
|
+
} catch (err) {
|
|
487
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
488
|
+
ctx.logger.warn(
|
|
489
|
+
"argus decrypt failed for %s: %s",
|
|
490
|
+
client.name,
|
|
491
|
+
message
|
|
492
|
+
);
|
|
493
|
+
return session.text(".failed", ["decrypt_failed"]);
|
|
494
|
+
}
|
|
453
495
|
const output = await blurImage(buffer, {
|
|
454
496
|
radius,
|
|
455
497
|
mode: config.blurMode
|
|
@@ -564,6 +606,16 @@ function range(start, end) {
|
|
|
564
606
|
return out;
|
|
565
607
|
}
|
|
566
608
|
__name(range, "range");
|
|
609
|
+
function decodeImagePayload(frame, token) {
|
|
610
|
+
if (!frame.enc || frame.enc === "none") {
|
|
611
|
+
return Buffer.from(frame.image, "base64");
|
|
612
|
+
}
|
|
613
|
+
if (frame.enc === "aes-256-gcm") {
|
|
614
|
+
return decryptBuffer(frame.image, token);
|
|
615
|
+
}
|
|
616
|
+
throw new Error(`unsupported_enc:${frame.enc}`);
|
|
617
|
+
}
|
|
618
|
+
__name(decodeImagePayload, "decodeImagePayload");
|
|
567
619
|
|
|
568
620
|
// src/index.ts
|
|
569
621
|
var name = "argus";
|
package/lib/index.mjs
CHANGED
|
@@ -370,6 +370,42 @@ function formatRemaining(ms) {
|
|
|
370
370
|
}
|
|
371
371
|
__name(formatRemaining, "formatRemaining");
|
|
372
372
|
|
|
373
|
+
// src/crypto.ts
|
|
374
|
+
import {
|
|
375
|
+
createCipheriv,
|
|
376
|
+
createDecipheriv,
|
|
377
|
+
randomBytes,
|
|
378
|
+
scryptSync
|
|
379
|
+
} from "node:crypto";
|
|
380
|
+
var ALGO = "aes-256-gcm";
|
|
381
|
+
var IV_LEN = 12;
|
|
382
|
+
var TAG_LEN = 16;
|
|
383
|
+
var KEY_LEN = 32;
|
|
384
|
+
var SALT = Buffer.from("argus.peek.v1", "utf8");
|
|
385
|
+
var keyCache = /* @__PURE__ */ new Map();
|
|
386
|
+
function deriveKey(token) {
|
|
387
|
+
let key = keyCache.get(token);
|
|
388
|
+
if (key) return key;
|
|
389
|
+
key = scryptSync(token, SALT, KEY_LEN);
|
|
390
|
+
keyCache.set(token, key);
|
|
391
|
+
return key;
|
|
392
|
+
}
|
|
393
|
+
__name(deriveKey, "deriveKey");
|
|
394
|
+
function decryptBuffer(payloadBase64, token) {
|
|
395
|
+
const buf = Buffer.from(payloadBase64, "base64");
|
|
396
|
+
if (buf.length < IV_LEN + TAG_LEN) {
|
|
397
|
+
throw new Error("ciphertext_too_short");
|
|
398
|
+
}
|
|
399
|
+
const iv = buf.subarray(0, IV_LEN);
|
|
400
|
+
const tag = buf.subarray(IV_LEN, IV_LEN + TAG_LEN);
|
|
401
|
+
const ct = buf.subarray(IV_LEN + TAG_LEN);
|
|
402
|
+
const key = deriveKey(token);
|
|
403
|
+
const decipher = createDecipheriv(ALGO, key, iv);
|
|
404
|
+
decipher.setAuthTag(tag);
|
|
405
|
+
return Buffer.concat([decipher.update(ct), decipher.final()]);
|
|
406
|
+
}
|
|
407
|
+
__name(decryptBuffer, "decryptBuffer");
|
|
408
|
+
|
|
373
409
|
// src/commands.ts
|
|
374
410
|
function applyCommands(ctx, server, config, cache) {
|
|
375
411
|
const cmd = ctx.command(
|
|
@@ -426,7 +462,18 @@ function applyCommands(ctx, server, config, cache) {
|
|
|
426
462
|
cache.set(cacheKey, { busy: response.frame });
|
|
427
463
|
return formatBusy(session, client.name, response.frame);
|
|
428
464
|
}
|
|
429
|
-
|
|
465
|
+
let buffer;
|
|
466
|
+
try {
|
|
467
|
+
buffer = decodeImagePayload(response.frame, config.token);
|
|
468
|
+
} catch (err) {
|
|
469
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
470
|
+
ctx.logger.warn(
|
|
471
|
+
"argus decrypt failed for %s: %s",
|
|
472
|
+
client.name,
|
|
473
|
+
message
|
|
474
|
+
);
|
|
475
|
+
return session.text(".failed", ["decrypt_failed"]);
|
|
476
|
+
}
|
|
430
477
|
const output = await blurImage(buffer, {
|
|
431
478
|
radius,
|
|
432
479
|
mode: config.blurMode
|
|
@@ -541,6 +588,16 @@ function range(start, end) {
|
|
|
541
588
|
return out;
|
|
542
589
|
}
|
|
543
590
|
__name(range, "range");
|
|
591
|
+
function decodeImagePayload(frame, token) {
|
|
592
|
+
if (!frame.enc || frame.enc === "none") {
|
|
593
|
+
return Buffer.from(frame.image, "base64");
|
|
594
|
+
}
|
|
595
|
+
if (frame.enc === "aes-256-gcm") {
|
|
596
|
+
return decryptBuffer(frame.image, token);
|
|
597
|
+
}
|
|
598
|
+
throw new Error(`unsupported_enc:${frame.enc}`);
|
|
599
|
+
}
|
|
600
|
+
__name(decodeImagePayload, "decodeImagePayload");
|
|
544
601
|
|
|
545
602
|
// src/index.ts
|
|
546
603
|
var name = "argus";
|
package/lib/types.d.ts
CHANGED
|
@@ -30,8 +30,14 @@ export interface PeekRequestFrame {
|
|
|
30
30
|
export interface PeekResultFrame {
|
|
31
31
|
type: 'peek_result';
|
|
32
32
|
id: string;
|
|
33
|
-
/**
|
|
33
|
+
/**
|
|
34
|
+
* 图片 buffer,base64 编码。
|
|
35
|
+
* `enc` 为 `aes-256-gcm` 时为 base64(iv|tag|ciphertext),token 派生 key 解密。
|
|
36
|
+
* `enc` 缺省 / 为 `none` 时为图片原始字节的 base64(向后兼容旧 CLI)。
|
|
37
|
+
*/
|
|
34
38
|
image: string;
|
|
39
|
+
/** 加密算法。新版客户端始终发 `aes-256-gcm`。 */
|
|
40
|
+
enc?: 'aes-256-gcm' | 'none';
|
|
35
41
|
mime?: string;
|
|
36
42
|
width?: number;
|
|
37
43
|
height?: number;
|
package/package.json
CHANGED
package/readme.md
CHANGED
|
@@ -5,6 +5,14 @@
|
|
|
5
5
|
插件本体作为 WebSocket 服务,配套 CLI 客户端 [`argus-eye`](https://www.npmjs.com/package/argus-eye)
|
|
6
6
|
连接上来后,群里就能通过命令拉取实时模糊截图。
|
|
7
7
|
|
|
8
|
+
## 安全说明
|
|
9
|
+
|
|
10
|
+
- 客户端连接时必须带 `token`,错的直接踢。
|
|
11
|
+
- 截图 buffer 在 CLI 端用 `AES-256-GCM` 加密后才上 WebSocket,
|
|
12
|
+
key 由 token 通过 scrypt 派生,IV 每帧随机。换句话说**抓包只能拿到密文**,
|
|
13
|
+
插件这边解出来再做模糊处理。token 写错或者被改包都会被 GCM 校验拒绝。
|
|
14
|
+
- 默认 `authority: 1`,可调高到只允许特定群友 peek。
|
|
15
|
+
|
|
8
16
|
## 安装
|
|
9
17
|
|
|
10
18
|
```bash
|