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.
@@ -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
- const buffer = Buffer.from(response.frame.image, "base64");
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
- const buffer = Buffer.from(response.frame.image, "base64");
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
- /** PNG/JPEG base64(不带 data: 前缀)。*/
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
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "koishi-plugin-argus",
3
3
  "description": "百眼巨人 Argus:让群友通过 /peek 命令偷窥你电脑屏幕的 Koishi 插件。",
4
- "version": "0.1.0",
4
+ "version": "0.2.0",
5
5
  "main": "lib/index.cjs",
6
6
  "module": "lib/index.mjs",
7
7
  "typings": "lib/index.d.ts",
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