virlen-remote 0.1.2 → 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/CHANGELOG.md CHANGED
@@ -3,6 +3,29 @@
3
3
  本文件记录对外可见的变更(协议 / 导出面 / 行为)。格式参考 [Keep a Changelog](https://keepachangelog.com/zh-CN/1.1.0/),
4
4
  版本号遵循 [语义化版本](https://semver.org/lang/zh-CN/)。
5
5
 
6
+ ## [0.2.0] - 2026-09-30
7
+
8
+ ### Changed
9
+
10
+ - **配对串(二维码 / 手工输入的"配对码")不再是明文 JSON**:`encodePairingPayload` 现在输出
11
+ `vrp1:` + `Base64URL(UTF-8(JSON) ⊕ 固定盐)`。目的只是让二维码文字 / 界面上的排查文本
12
+ **不是一眼可读**(此前是 `{"v":2,"host":"dk-…","ticket":"…"}` 原文),并避免在复制粘贴 / 日志里
13
+ 直接暴露 ticket、设备 key、信令基址。
14
+ - `parsePairingPayload` **兼容两种输入**:带 `vrp1:` 前缀走解码;否则按旧版明文 JSON 解析 ——
15
+ 已生成 / 已截图的旧二维码、以及旧版本手抄的配对串仍能识别,**升级不会让旧码失效**。
16
+
17
+ ### Added
18
+
19
+ - `PAIRING_OBFUSCATION_PREFIX`(`'vrp1:'`):混淆串前缀,供消费方识别 / 展示。
20
+
21
+ ### Notes
22
+
23
+ - ⚠️ **这是混淆,不是加密**:本包发布到公开 npm,固定盐在源码里可见,拿到源码即可解;
24
+ 且它**挡不住有人对着屏幕拍照**(二维码本就是给人扫的,App 会解回来)。
25
+ 真正的安全边界仍是「一次性 ticket + 电脑端确认弹窗 + 授权凭证」。
26
+ - 向后兼容:新版能解旧码;**旧版解不了新码**(消费方需同步升级到本版或更高)。
27
+ - 零依赖:自带 Base64URL 实现,不依赖 `btoa` / `atob` / `Buffer`。
28
+
6
29
  ## [0.1.2] - 2026-09-28
7
30
 
8
31
  ### Changed
@@ -70,3 +93,4 @@
70
93
  [0.1.0]: https://github.com/WeiChens/virlen-remote/releases/tag/v0.1.0
71
94
  [0.1.1]: https://github.com/WeiChens/virlen-remote/releases/tag/v0.1.1
72
95
  [0.1.2]: https://github.com/WeiChens/virlen-remote/releases/tag/v0.1.2
96
+ [0.2.0]: https://github.com/WeiChens/virlen-remote/releases/tag/v0.2.0
package/README.md CHANGED
@@ -112,11 +112,16 @@ import {
112
112
  } from 'virlen-remote'
113
113
 
114
114
  const payload = buildPairingPayload({ host: deviceKey, name: '我的电脑', ticket, signal: signalUrl })
115
- const qrText = encodePairingPayload(payload) // 画进二维码
115
+ const qrText = encodePairingPayload(payload) // 画进二维码 —— 输出形如 'vrp1:xxxx'(混淆后,非明文 JSON)
116
116
  const back = parsePairingPayload(qrText) // 手机端解析(无法识别时返回 null)
117
117
  if (back) await connect(roomOfPayload(back), back.host, back.ticket)
118
118
  ```
119
119
 
120
+ > **关于混淆**:`encodePairingPayload` 输出 `vrp1:` + `Base64URL(JSON ⊕ 固定盐)`,只是让二维码文字 /
121
+ > 界面上的排查文本**不是一眼可读**,**不是加密** —— 固定盐在源码里,拿到源码即可解;它也
122
+ > 挡不住有人对着屏幕拍照。真正的安全边界是「一次性 ticket + 电脑端确认弹窗 + 授权凭证」。
123
+ > `parsePairingPayload` **同时接受**混淆串与旧版明文 JSON(向后兼容)。前缀常量见 `PAIRING_OBFUSCATION_PREFIX`。
124
+
120
125
  ## ICE / TURN 配置
121
126
 
122
127
  **本包与两个客户端里都没有任何 TURN 凭证。** 默认值由**服务端**提供:
package/dist/index.d.ts CHANGED
@@ -18,7 +18,7 @@ export type { ErrorCode, WireError, BridgeErrorOptions } from './protocol/errors
18
18
  export { newRequestId } from './protocol/ids.js';
19
19
  export { HOST_KEY_PREFIX, MOBILE_KEY_PREFIX, GRANT_PREFIX, ROOM_PREFIX, GRANT_TTL_MS, GRANT_MAX_LIFETIME_MS, randomKey, newDeviceKey, newGrantToken, isDeviceKey, roomFor, hostKeyFromRoom, issueGrant, isGrantExpired, renewGrant, checkGrant, describeGrantRemaining, } from './protocol/identity.js';
20
20
  export type { DeviceKind, GrantRecord, CredentialRejectReason, RoomStatus } from './protocol/identity.js';
21
- export { PAIRING_PAYLOAD_VERSION, PAIRING_TICKET_TTL_MS, buildPairingPayload, encodePairingPayload, parsePairingPayload, roomOfPayload, } from './protocol/pairing.js';
21
+ export { PAIRING_PAYLOAD_VERSION, PAIRING_TICKET_TTL_MS, PAIRING_OBFUSCATION_PREFIX, buildPairingPayload, encodePairingPayload, parsePairingPayload, roomOfPayload, } from './protocol/pairing.js';
22
22
  export type { PairingPayload } from './protocol/pairing.js';
23
23
  export { normalizeChoiceAnswer, answerActionError, CHOICE_JOINER } from './protocol/answer.js';
24
24
  export type { ChoiceAnswer } from './protocol/answer.js';
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"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"}
package/dist/index.js CHANGED
@@ -18,7 +18,7 @@ export { newRequestId } from './protocol/ids.js';
18
18
  // ── 设备身份与授权凭证(M6,§30)──
19
19
  export { HOST_KEY_PREFIX, MOBILE_KEY_PREFIX, GRANT_PREFIX, ROOM_PREFIX, GRANT_TTL_MS, GRANT_MAX_LIFETIME_MS, randomKey, newDeviceKey, newGrantToken, isDeviceKey, roomFor, hostKeyFromRoom, issueGrant, isGrantExpired, renewGrant, checkGrant, describeGrantRemaining, } from './protocol/identity.js';
20
20
  // ── 配对载荷(二维码内容,两端同一份)──
21
- export { PAIRING_PAYLOAD_VERSION, PAIRING_TICKET_TTL_MS, buildPairingPayload, encodePairingPayload, parsePairingPayload, roomOfPayload, } from './protocol/pairing.js';
21
+ export { PAIRING_PAYLOAD_VERSION, PAIRING_TICKET_TTL_MS, PAIRING_OBFUSCATION_PREFIX, buildPairingPayload, encodePairingPayload, parsePairingPayload, roomOfPayload, } from './protocol/pairing.js';
22
22
  export { normalizeChoiceAnswer, answerActionError, CHOICE_JOINER } from './protocol/answer.js';
23
23
  export { negotiate, intersectCapabilities } from './protocol/hello.js';
24
24
  export { Endpoint } from './protocol/endpoint.js';
package/dist/index.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;GAWG;AAEH,iBAAiB;AACjB,OAAO,EAAE,gBAAgB,EAAE,WAAW,EAAE,yBAAyB,EAAE,SAAS,EAAE,MAAM,kBAAkB,CAAA;AACtG,OAAO,EAAE,YAAY,EAAE,YAAY,EAAE,SAAS,EAAE,YAAY,EAAE,WAAW,EAAE,MAAM,kBAAkB,CAAA;AAGnG,OAAO,EAAE,WAAW,EAAE,aAAa,EAAE,MAAM,mBAAmB,CAAA;AAG9D,OAAO,EAAE,YAAY,EAAE,MAAM,gBAAgB,CAAA;AAE7C,yBAAyB;AACzB,OAAO,EACL,eAAe,EACf,iBAAiB,EACjB,YAAY,EACZ,WAAW,EACX,YAAY,EACZ,qBAAqB,EACrB,SAAS,EACT,YAAY,EACZ,aAAa,EACb,WAAW,EACX,OAAO,EACP,eAAe,EACf,UAAU,EACV,cAAc,EACd,UAAU,EACV,UAAU,EACV,sBAAsB,GACvB,MAAM,qBAAqB,CAAA;AAG5B,yBAAyB;AACzB,OAAO,EACL,uBAAuB,EACvB,qBAAqB,EACrB,mBAAmB,EACnB,oBAAoB,EACpB,mBAAmB,EACnB,aAAa,GACd,MAAM,oBAAoB,CAAA;AAG3B,OAAO,EAAE,qBAAqB,EAAE,iBAAiB,EAAE,aAAa,EAAE,MAAM,mBAAmB,CAAA;AAG3F,OAAO,EAAE,SAAS,EAAE,qBAAqB,EAAE,MAAM,kBAAkB,CAAA;AAEnE,OAAO,EAAE,QAAQ,EAAE,MAAM,qBAAqB,CAAA;AAU9C,OAAO,EAAE,YAAY,EAAE,gBAAgB,EAAE,gBAAgB,EAAE,MAAM,gBAAgB,CAAA;AACjF,OAAO,EAAE,6BAA6B,EAAE,kBAAkB,EAAE,MAAM,gBAAgB,CAAA;AAsClF,OAAO,EAAE,oBAAoB,EAAE,MAAM,iBAAiB,CAAA;AAItD,OAAO,EAAE,eAAe,EAAE,gBAAgB,EAAE,MAAM,oBAAoB,CAAA;AAEtE,OAAO,EAAE,kBAAkB,EAAE,mBAAmB,EAAE,MAAM,uBAAuB,CAAA;AAC/E,OAAO,EAAE,kBAAkB,EAAE,eAAe,EAAE,kBAAkB,EAAE,MAAM,uBAAuB,CAAA;AAS/F,OAAO,EAAE,YAAY,EAAE,MAAM,iBAAiB,CAAA;AAG9C,mCAAmC;AACnC,OAAO,EACL,iBAAiB,EACjB,gBAAgB,EAChB,aAAa,EACb,iBAAiB,EACjB,aAAa,EACb,eAAe,GAChB,MAAM,uBAAuB,CAAA;AAQ9B,yCAAyC;AACzC,OAAO,EACL,kBAAkB,EAClB,YAAY,EACZ,sBAAsB,EACtB,sBAAsB,EACtB,gBAAgB,EAChB,oBAAoB,EACpB,kBAAkB,EAClB,YAAY,EACZ,gBAAgB,EAChB,iBAAiB,EACjB,eAAe,EACf,iBAAiB,EACjB,iBAAiB,EACjB,kBAAkB,GACnB,MAAM,iBAAiB,CAAA"}
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;GAWG;AAEH,iBAAiB;AACjB,OAAO,EAAE,gBAAgB,EAAE,WAAW,EAAE,yBAAyB,EAAE,SAAS,EAAE,MAAM,kBAAkB,CAAA;AACtG,OAAO,EAAE,YAAY,EAAE,YAAY,EAAE,SAAS,EAAE,YAAY,EAAE,WAAW,EAAE,MAAM,kBAAkB,CAAA;AAGnG,OAAO,EAAE,WAAW,EAAE,aAAa,EAAE,MAAM,mBAAmB,CAAA;AAG9D,OAAO,EAAE,YAAY,EAAE,MAAM,gBAAgB,CAAA;AAE7C,yBAAyB;AACzB,OAAO,EACL,eAAe,EACf,iBAAiB,EACjB,YAAY,EACZ,WAAW,EACX,YAAY,EACZ,qBAAqB,EACrB,SAAS,EACT,YAAY,EACZ,aAAa,EACb,WAAW,EACX,OAAO,EACP,eAAe,EACf,UAAU,EACV,cAAc,EACd,UAAU,EACV,UAAU,EACV,sBAAsB,GACvB,MAAM,qBAAqB,CAAA;AAG5B,yBAAyB;AACzB,OAAO,EACL,uBAAuB,EACvB,qBAAqB,EACrB,0BAA0B,EAC1B,mBAAmB,EACnB,oBAAoB,EACpB,mBAAmB,EACnB,aAAa,GACd,MAAM,oBAAoB,CAAA;AAG3B,OAAO,EAAE,qBAAqB,EAAE,iBAAiB,EAAE,aAAa,EAAE,MAAM,mBAAmB,CAAA;AAG3F,OAAO,EAAE,SAAS,EAAE,qBAAqB,EAAE,MAAM,kBAAkB,CAAA;AAEnE,OAAO,EAAE,QAAQ,EAAE,MAAM,qBAAqB,CAAA;AAU9C,OAAO,EAAE,YAAY,EAAE,gBAAgB,EAAE,gBAAgB,EAAE,MAAM,gBAAgB,CAAA;AACjF,OAAO,EAAE,6BAA6B,EAAE,kBAAkB,EAAE,MAAM,gBAAgB,CAAA;AAsClF,OAAO,EAAE,oBAAoB,EAAE,MAAM,iBAAiB,CAAA;AAItD,OAAO,EAAE,eAAe,EAAE,gBAAgB,EAAE,MAAM,oBAAoB,CAAA;AAEtE,OAAO,EAAE,kBAAkB,EAAE,mBAAmB,EAAE,MAAM,uBAAuB,CAAA;AAC/E,OAAO,EAAE,kBAAkB,EAAE,eAAe,EAAE,kBAAkB,EAAE,MAAM,uBAAuB,CAAA;AAS/F,OAAO,EAAE,YAAY,EAAE,MAAM,iBAAiB,CAAA;AAG9C,mCAAmC;AACnC,OAAO,EACL,iBAAiB,EACjB,gBAAgB,EAChB,aAAa,EACb,iBAAiB,EACjB,aAAa,EACb,eAAe,GAChB,MAAM,uBAAuB,CAAA;AAQ9B,yCAAyC;AACzC,OAAO,EACL,kBAAkB,EAClB,YAAY,EACZ,sBAAsB,EACtB,sBAAsB,EACtB,gBAAgB,EAChB,oBAAoB,EACpB,kBAAkB,EAClB,YAAY,EACZ,gBAAgB,EAChB,iBAAiB,EACjB,eAAe,EACf,iBAAiB,EACjB,iBAAiB,EACjB,kBAAkB,GACnB,MAAM,iBAAiB,CAAA"}
@@ -7,6 +7,16 @@ export declare const PAIRING_PAYLOAD_VERSION = 2;
7
7
  * 到期自动重生成**(见 `phoneControlStore`)三管齐下,真机上基本不可能再扫到过期码。
8
8
  */
9
9
  export declare const PAIRING_TICKET_TTL_MS: number;
10
+ /**
11
+ * 配对串混淆前缀。
12
+ *
13
+ * - **带此前缀**:内容是 `Base64URL(UTF-8(JSON) ⊕ 固定盐)`,走解码;
14
+ * - **不带**:当作旧版明文 JSON 解析(**向后兼容** —— 线上已生成 / 已截图的旧二维码、
15
+ * 以及老版本手抄的配对串仍能识别,升级后不会突然「无法识别」)。
16
+ *
17
+ * 前缀选 `vrp1:`(virlen pairing v1)—— 明文 JSON 必以 `{` 开头,不可能撞上前缀。
18
+ */
19
+ export declare const PAIRING_OBFUSCATION_PREFIX = "vrp1:";
10
20
  export interface PairingPayload {
11
21
  v: number;
12
22
  /** 电脑设备 key(也是房间号的来源)。 */
@@ -27,7 +37,7 @@ export declare function buildPairingPayload(input: {
27
37
  ticket: string;
28
38
  signal?: string;
29
39
  }): PairingPayload;
30
- /** 序列化为二维码内容。 */
40
+ /** 序列化为二维码内容(**混淆后的串**,不再是明文 JSON)。 */
31
41
  export declare function encodePairingPayload(payload: PairingPayload): string;
32
42
  /**
33
43
  * 解析二维码 / 配对串。无法识别返回 `null`(调用方给「不是 Virlen 配对码」的文案)。
@@ -1 +1 @@
1
- {"version":3,"file":"pairing.d.ts","sourceRoot":"","sources":["../../src/protocol/pairing.ts"],"names":[],"mappings":"AAcA,sEAAsE;AACtE,eAAO,MAAM,uBAAuB,IAAI,CAAA;AAExC;;;;;GAKG;AACH,eAAO,MAAM,qBAAqB,QAAgB,CAAA;AAElD,MAAM,WAAW,cAAc;IAC7B,CAAC,EAAE,MAAM,CAAA;IACT,0BAA0B;IAC1B,IAAI,EAAE,MAAM,CAAA;IACZ,sBAAsB;IACtB,IAAI,EAAE,MAAM,CAAA;IACZ,eAAe;IACf,MAAM,EAAE,MAAM,CAAA;IACd,4CAA4C;IAC5C,MAAM,CAAC,EAAE,MAAM,CAAA;IACf,wCAAwC;IACxC,IAAI,CAAC,EAAE,MAAM,CAAA;CACd;AAED,iDAAiD;AACjD,wBAAgB,mBAAmB,CAAC,KAAK,EAAE;IACzC,IAAI,EAAE,MAAM,CAAA;IACZ,IAAI,EAAE,MAAM,CAAA;IACZ,MAAM,EAAE,MAAM,CAAA;IACd,MAAM,CAAC,EAAE,MAAM,CAAA;CAChB,GAAG,cAAc,CAQjB;AAED,iBAAiB;AACjB,wBAAgB,oBAAoB,CAAC,OAAO,EAAE,cAAc,GAAG,MAAM,CAEpE;AAED;;;;;;;GAOG;AACH,wBAAgB,mBAAmB,CAAC,IAAI,EAAE,MAAM,GAAG,cAAc,GAAG,IAAI,CAqBvE;AAED,2CAA2C;AAC3C,wBAAgB,aAAa,CAAC,OAAO,EAAE,cAAc,GAAG,MAAM,CAE7D"}
1
+ {"version":3,"file":"pairing.d.ts","sourceRoot":"","sources":["../../src/protocol/pairing.ts"],"names":[],"mappings":"AAsBA,sEAAsE;AACtE,eAAO,MAAM,uBAAuB,IAAI,CAAA;AAExC;;;;;GAKG;AACH,eAAO,MAAM,qBAAqB,QAAgB,CAAA;AAElD;;;;;;;;GAQG;AACH,eAAO,MAAM,0BAA0B,UAAU,CAAA;AAcjD,MAAM,WAAW,cAAc;IAC7B,CAAC,EAAE,MAAM,CAAA;IACT,0BAA0B;IAC1B,IAAI,EAAE,MAAM,CAAA;IACZ,sBAAsB;IACtB,IAAI,EAAE,MAAM,CAAA;IACZ,eAAe;IACf,MAAM,EAAE,MAAM,CAAA;IACd,4CAA4C;IAC5C,MAAM,CAAC,EAAE,MAAM,CAAA;IACf,wCAAwC;IACxC,IAAI,CAAC,EAAE,MAAM,CAAA;CACd;AAED,iDAAiD;AACjD,wBAAgB,mBAAmB,CAAC,KAAK,EAAE;IACzC,IAAI,EAAE,MAAM,CAAA;IACZ,IAAI,EAAE,MAAM,CAAA;IACZ,MAAM,EAAE,MAAM,CAAA;IACd,MAAM,CAAC,EAAE,MAAM,CAAA;CAChB,GAAG,cAAc,CAQjB;AAwED,uCAAuC;AACvC,wBAAgB,oBAAoB,CAAC,OAAO,EAAE,cAAc,GAAG,MAAM,CAGpE;AAED;;;;;;;GAOG;AACH,wBAAgB,mBAAmB,CAAC,IAAI,EAAE,MAAM,GAAG,cAAc,GAAG,IAAI,CAyBvE;AAED,2CAA2C;AAC3C,wBAAgB,aAAa,CAAC,OAAO,EAAE,cAAc,GAAG,MAAM,CAE7D"}
@@ -9,6 +9,14 @@
9
9
  * - `ticket`:**一次性**配对票据(不是长期凭证!兑换后电脑端签发 `grant` 回传,见 §30.3)。
10
10
  * - `signal`:信令基址;缺省 = 同源 Broadcast 联调(生产必有)。
11
11
  * - `room`:信令房间号;**缺省由 `roomFor(host)` 派生**。保留字段只为兼容旧码。
12
+ *
13
+ * **线上的码是混淆过的**(见 `PAIRING_OBFUSCATION_PREFIX`):`encodePairingPayload` 输出
14
+ * `vrp1:` + Base64URL(UTF-8(JSON) ⊕ 固定盐),使二维码文字 / 界面上的排查文本不再是明文 JSON;
15
+ * `parsePairingPayload` 仍**兼容旧明文码**(无前缀即按原 JSON 解析),已生成 / 已截图的旧码不会失效。
16
+ *
17
+ * ⚠️ 这是**混淆,不是加密**:本包是公开 npm 包、固定盐也在源码里,拿到源码即可解;
18
+ * 且它**挡不住有人对着屏幕拍照**(二维码本就是给人扫的)。真正的安全边界是
19
+ * 「一次性 ticket + 电脑端确认弹窗 + 授权凭证」(见 `identity.ts`)。
12
20
  */
13
21
  import { roomFor } from './identity.js';
14
22
  /** 当前载荷版本。v1 与 v2 形状相同(差异在语义:v2 起 `host` 是设备 key、`ticket` 是一次性票据)。 */
@@ -20,6 +28,26 @@ export const PAIRING_PAYLOAD_VERSION = 2;
20
28
  * 到期自动重生成**(见 `phoneControlStore`)三管齐下,真机上基本不可能再扫到过期码。
21
29
  */
22
30
  export const PAIRING_TICKET_TTL_MS = 5 * 60 * 1000;
31
+ /**
32
+ * 配对串混淆前缀。
33
+ *
34
+ * - **带此前缀**:内容是 `Base64URL(UTF-8(JSON) ⊕ 固定盐)`,走解码;
35
+ * - **不带**:当作旧版明文 JSON 解析(**向后兼容** —— 线上已生成 / 已截图的旧二维码、
36
+ * 以及老版本手抄的配对串仍能识别,升级后不会突然「无法识别」)。
37
+ *
38
+ * 前缀选 `vrp1:`(virlen pairing v1)—— 明文 JSON 必以 `{` 开头,不可能撞上前缀。
39
+ */
40
+ export const PAIRING_OBFUSCATION_PREFIX = 'vrp1:';
41
+ const textEncoder = new TextEncoder();
42
+ const textDecoder = new TextDecoder();
43
+ /**
44
+ * 混淆盐(UTF-8 字节)。
45
+ *
46
+ * ⚠️ **这不是密钥,是固定盐**:本包发布到公开 npm,字符串必然可见、拿到源码即可解。
47
+ * 它只解决「配对串在界面上 / 复制粘贴时不是一眼可读的 JSON」,不提供任何机密性 ——
48
+ * 真正的安全边界是「一次性 ticket + 电脑端确认弹窗 + 授权凭证」(见 `identity.ts`)。
49
+ */
50
+ const OBFUSCATION_SALT = textEncoder.encode('virlen/pairing/obfuscation/v1');
23
51
  /** 构造载荷(只填必要字段;`room` 交给两端各自派生,避免又多一处可漂移的冗余)。 */
24
52
  export function buildPairingPayload(input) {
25
53
  return {
@@ -30,9 +58,83 @@ export function buildPairingPayload(input) {
30
58
  ...(input.signal ? { signal: input.signal } : {}),
31
59
  };
32
60
  }
33
- /** 序列化为二维码内容。 */
61
+ // ── 混淆编解码(零依赖:自带 Base64URL,不依赖 btoa / atob / Buffer —— 两端 + Node 环境一致)──
62
+ const B64_ALPHABET = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-_';
63
+ /** Base64URL 反向查表(ASCII 0–127;其余一律视为非法字符)。 */
64
+ const B64_LOOKUP = (() => {
65
+ const table = new Int16Array(128).fill(-1);
66
+ for (let i = 0; i < B64_ALPHABET.length; i += 1)
67
+ table[B64_ALPHABET.charCodeAt(i)] = i;
68
+ return table;
69
+ })();
70
+ /** 逐字节与盐做 XOR(盐循环使用)—— 可逆,用于混淆。 */
71
+ function xorSalt(bytes) {
72
+ const out = new Uint8Array(bytes.length);
73
+ const salt = OBFUSCATION_SALT;
74
+ for (let i = 0; i < bytes.length; i += 1)
75
+ out[i] = bytes[i] ^ salt[i % salt.length];
76
+ return out;
77
+ }
78
+ /** Base64URL 编码(**无填充**,`-` / `_` 代替 `+` / `/`)。 */
79
+ function base64UrlEncode(bytes) {
80
+ let out = '';
81
+ for (let i = 0; i < bytes.length; i += 3) {
82
+ const b0 = bytes[i];
83
+ const b1 = i + 1 < bytes.length ? bytes[i + 1] : undefined;
84
+ const b2 = i + 2 < bytes.length ? bytes[i + 2] : undefined;
85
+ out += B64_ALPHABET[b0 >> 2];
86
+ out += B64_ALPHABET[((b0 & 0x03) << 4) | ((b1 ?? 0) >> 4)];
87
+ if (b1 === undefined)
88
+ break;
89
+ out += B64_ALPHABET[((b1 & 0x0f) << 2) | ((b2 ?? 0) >> 6)];
90
+ if (b2 === undefined)
91
+ break;
92
+ out += B64_ALPHABET[b2 & 0x3f];
93
+ }
94
+ return out;
95
+ }
96
+ /** Base64URL 解码;含非法字符或长度非法(`len % 4 === 1`)时返回 `null`。 */
97
+ function base64UrlDecode(text) {
98
+ if (!text)
99
+ return null;
100
+ const values = [];
101
+ for (const ch of text) {
102
+ const code = ch.charCodeAt(0);
103
+ const v = code < 128 ? B64_LOOKUP[code] : -1;
104
+ if (v < 0)
105
+ return null;
106
+ values.push(v);
107
+ }
108
+ if (values.length % 4 === 1)
109
+ return null;
110
+ const out = new Uint8Array((values.length * 6) >> 3);
111
+ let o = 0;
112
+ for (let i = 0; i < values.length; i += 4) {
113
+ const c0 = values[i];
114
+ const c1 = values[i + 1];
115
+ const c2 = values[i + 2];
116
+ const c3 = values[i + 3];
117
+ out[o++] = (c0 << 2) | ((c1 ?? 0) >> 4);
118
+ if (c2 === undefined)
119
+ break;
120
+ out[o++] = (((c1 ?? 0) & 0x0f) << 4) | (c2 >> 2);
121
+ if (c3 === undefined)
122
+ break;
123
+ out[o++] = ((c2 & 0x03) << 6) | c3;
124
+ }
125
+ return out.subarray(0, o);
126
+ }
127
+ /** 把 `vrp1:` 串还原成 JSON 文本;任何异常(非法字符 / 长度 / 解码失败)都返回 `null`。 */
128
+ function decodeObfuscated(text) {
129
+ const bytes = base64UrlDecode(text.slice(PAIRING_OBFUSCATION_PREFIX.length));
130
+ if (!bytes)
131
+ return null;
132
+ return textDecoder.decode(xorSalt(bytes));
133
+ }
134
+ /** 序列化为二维码内容(**混淆后的串**,不再是明文 JSON)。 */
34
135
  export function encodePairingPayload(payload) {
35
- return JSON.stringify(payload);
136
+ const json = textEncoder.encode(JSON.stringify(payload));
137
+ return PAIRING_OBFUSCATION_PREFIX + base64UrlEncode(xorSalt(json));
36
138
  }
37
139
  /**
38
140
  * 解析二维码 / 配对串。无法识别返回 `null`(调用方给「不是 Virlen 配对码」的文案)。
@@ -45,9 +147,14 @@ export function encodePairingPayload(payload) {
45
147
  export function parsePairingPayload(text) {
46
148
  if (typeof text !== 'string' || !text.trim())
47
149
  return null;
150
+ const trimmed = text.trim();
151
+ // 带前缀 → 先解混淆;否则按旧版明文 JSON 解析(向后兼容,见 PAIRING_OBFUSCATION_PREFIX)
152
+ const jsonText = trimmed.startsWith(PAIRING_OBFUSCATION_PREFIX) ? decodeObfuscated(trimmed) : trimmed;
153
+ if (jsonText === null)
154
+ return null;
48
155
  let raw;
49
156
  try {
50
- raw = JSON.parse(text);
157
+ raw = JSON.parse(jsonText);
51
158
  }
52
159
  catch {
53
160
  return null;
@@ -1 +1 @@
1
- {"version":3,"file":"pairing.js","sourceRoot":"","sources":["../../src/protocol/pairing.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;GAWG;AACH,OAAO,EAAE,OAAO,EAAE,MAAM,YAAY,CAAA;AAEpC,sEAAsE;AACtE,MAAM,CAAC,MAAM,uBAAuB,GAAG,CAAC,CAAA;AAExC;;;;;GAKG;AACH,MAAM,CAAC,MAAM,qBAAqB,GAAG,CAAC,GAAG,EAAE,GAAG,IAAI,CAAA;AAgBlD,iDAAiD;AACjD,MAAM,UAAU,mBAAmB,CAAC,KAKnC;IACC,OAAO;QACL,CAAC,EAAE,uBAAuB;QAC1B,IAAI,EAAE,KAAK,CAAC,IAAI;QAChB,IAAI,EAAE,KAAK,CAAC,IAAI;QAChB,MAAM,EAAE,KAAK,CAAC,MAAM;QACpB,GAAG,CAAC,KAAK,CAAC,MAAM,CAAC,CAAC,CAAC,EAAE,MAAM,EAAE,KAAK,CAAC,MAAM,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC;KAClD,CAAA;AACH,CAAC;AAED,iBAAiB;AACjB,MAAM,UAAU,oBAAoB,CAAC,OAAuB;IAC1D,OAAO,IAAI,CAAC,SAAS,CAAC,OAAO,CAAC,CAAA;AAChC,CAAC;AAED;;;;;;;GAOG;AACH,MAAM,UAAU,mBAAmB,CAAC,IAAY;IAC9C,IAAI,OAAO,IAAI,KAAK,QAAQ,IAAI,CAAC,IAAI,CAAC,IAAI,EAAE;QAAE,OAAO,IAAI,CAAA;IACzD,IAAI,GAAY,CAAA;IAChB,IAAI,CAAC;QACH,GAAG,GAAG,IAAI,CAAC,KAAK,CAAC,IAAI,CAAC,CAAA;IACxB,CAAC;IAAC,MAAM,CAAC;QACP,OAAO,IAAI,CAAA;IACb,CAAC;IACD,IAAI,CAAC,GAAG,IAAI,OAAO,GAAG,KAAK,QAAQ;QAAE,OAAO,IAAI,CAAA;IAChD,MAAM,GAAG,GAAG,GAA8B,CAAA;IAC1C,IAAI,OAAO,GAAG,CAAC,IAAI,KAAK,QAAQ,IAAI,CAAC,GAAG,CAAC,IAAI,CAAC,IAAI,EAAE;QAAE,OAAO,IAAI,CAAA;IACjE,IAAI,OAAO,GAAG,CAAC,MAAM,KAAK,QAAQ,IAAI,CAAC,GAAG,CAAC,MAAM,CAAC,IAAI,EAAE;QAAE,OAAO,IAAI,CAAA;IACrE,MAAM,OAAO,GAAmB;QAC9B,CAAC,EAAE,OAAO,GAAG,CAAC,CAAC,KAAK,QAAQ,CAAC,CAAC,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC;QACxC,IAAI,EAAE,GAAG,CAAC,IAAI,CAAC,IAAI,EAAE;QACrB,IAAI,EAAE,OAAO,GAAG,CAAC,IAAI,KAAK,QAAQ,IAAI,GAAG,CAAC,IAAI,CAAC,IAAI,EAAE,CAAC,CAAC,CAAC,GAAG,CAAC,IAAI,CAAC,IAAI,EAAE,CAAC,CAAC,CAAC,GAAG,CAAC,IAAI,CAAC,IAAI,EAAE;QACzF,MAAM,EAAE,GAAG,CAAC,MAAM,CAAC,IAAI,EAAE;KAC1B,CAAA;IACD,IAAI,OAAO,GAAG,CAAC,MAAM,KAAK,QAAQ,IAAI,GAAG,CAAC,MAAM,CAAC,IAAI,EAAE;QAAE,OAAO,CAAC,MAAM,GAAG,GAAG,CAAC,MAAM,CAAC,IAAI,EAAE,CAAA;IAC3F,IAAI,OAAO,GAAG,CAAC,IAAI,KAAK,QAAQ,IAAI,GAAG,CAAC,IAAI,CAAC,IAAI,EAAE;QAAE,OAAO,CAAC,IAAI,GAAG,GAAG,CAAC,IAAI,CAAC,IAAI,EAAE,CAAA;IACnF,OAAO,OAAO,CAAA;AAChB,CAAC;AAED,2CAA2C;AAC3C,MAAM,UAAU,aAAa,CAAC,OAAuB;IACnD,OAAO,OAAO,CAAC,IAAI,IAAI,OAAO,CAAC,OAAO,CAAC,IAAI,CAAC,CAAA;AAC9C,CAAC"}
1
+ {"version":3,"file":"pairing.js","sourceRoot":"","sources":["../../src/protocol/pairing.ts"],"names":[],"mappings":"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"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "virlen-remote",
3
- "version": "0.1.2",
3
+ "version": "0.2.0",
4
4
  "description": "Virlen 手机控制 · 协议栈共享包 —— 帧 / RPC / 错误 / 能力协商 / 设备身份与授权凭证 / 配对载荷 / ICE 解析 / Transport(WebRTC / 内存 / Broadcast)",
5
5
  "license": "MIT",
6
6
  "author": "WeiChens",
package/src/index.ts CHANGED
@@ -47,6 +47,7 @@ export type { DeviceKind, GrantRecord, CredentialRejectReason, RoomStatus } from
47
47
  export {
48
48
  PAIRING_PAYLOAD_VERSION,
49
49
  PAIRING_TICKET_TTL_MS,
50
+ PAIRING_OBFUSCATION_PREFIX,
50
51
  buildPairingPayload,
51
52
  encodePairingPayload,
52
53
  parsePairingPayload,
@@ -9,6 +9,14 @@
9
9
  * - `ticket`:**一次性**配对票据(不是长期凭证!兑换后电脑端签发 `grant` 回传,见 §30.3)。
10
10
  * - `signal`:信令基址;缺省 = 同源 Broadcast 联调(生产必有)。
11
11
  * - `room`:信令房间号;**缺省由 `roomFor(host)` 派生**。保留字段只为兼容旧码。
12
+ *
13
+ * **线上的码是混淆过的**(见 `PAIRING_OBFUSCATION_PREFIX`):`encodePairingPayload` 输出
14
+ * `vrp1:` + Base64URL(UTF-8(JSON) ⊕ 固定盐),使二维码文字 / 界面上的排查文本不再是明文 JSON;
15
+ * `parsePairingPayload` 仍**兼容旧明文码**(无前缀即按原 JSON 解析),已生成 / 已截图的旧码不会失效。
16
+ *
17
+ * ⚠️ 这是**混淆,不是加密**:本包是公开 npm 包、固定盐也在源码里,拿到源码即可解;
18
+ * 且它**挡不住有人对着屏幕拍照**(二维码本就是给人扫的)。真正的安全边界是
19
+ * 「一次性 ticket + 电脑端确认弹窗 + 授权凭证」(见 `identity.ts`)。
12
20
  */
13
21
  import { roomFor } from './identity'
14
22
 
@@ -23,6 +31,29 @@ export const PAIRING_PAYLOAD_VERSION = 2
23
31
  */
24
32
  export const PAIRING_TICKET_TTL_MS = 5 * 60 * 1000
25
33
 
34
+ /**
35
+ * 配对串混淆前缀。
36
+ *
37
+ * - **带此前缀**:内容是 `Base64URL(UTF-8(JSON) ⊕ 固定盐)`,走解码;
38
+ * - **不带**:当作旧版明文 JSON 解析(**向后兼容** —— 线上已生成 / 已截图的旧二维码、
39
+ * 以及老版本手抄的配对串仍能识别,升级后不会突然「无法识别」)。
40
+ *
41
+ * 前缀选 `vrp1:`(virlen pairing v1)—— 明文 JSON 必以 `{` 开头,不可能撞上前缀。
42
+ */
43
+ export const PAIRING_OBFUSCATION_PREFIX = 'vrp1:'
44
+
45
+ const textEncoder = new TextEncoder()
46
+ const textDecoder = new TextDecoder()
47
+
48
+ /**
49
+ * 混淆盐(UTF-8 字节)。
50
+ *
51
+ * ⚠️ **这不是密钥,是固定盐**:本包发布到公开 npm,字符串必然可见、拿到源码即可解。
52
+ * 它只解决「配对串在界面上 / 复制粘贴时不是一眼可读的 JSON」,不提供任何机密性 ——
53
+ * 真正的安全边界是「一次性 ticket + 电脑端确认弹窗 + 授权凭证」(见 `identity.ts`)。
54
+ */
55
+ const OBFUSCATION_SALT = textEncoder.encode('virlen/pairing/obfuscation/v1')
56
+
26
57
  export interface PairingPayload {
27
58
  v: number
28
59
  /** 电脑设备 key(也是房间号的来源)。 */
@@ -53,9 +84,80 @@ export function buildPairingPayload(input: {
53
84
  }
54
85
  }
55
86
 
56
- /** 序列化为二维码内容。 */
87
+ // ── 混淆编解码(零依赖:自带 Base64URL,不依赖 btoa / atob / Buffer —— 两端 + Node 环境一致)──
88
+
89
+ const B64_ALPHABET = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-_'
90
+
91
+ /** Base64URL 反向查表(ASCII 0–127;其余一律视为非法字符)。 */
92
+ const B64_LOOKUP: Int16Array = (() => {
93
+ const table = new Int16Array(128).fill(-1)
94
+ for (let i = 0; i < B64_ALPHABET.length; i += 1) table[B64_ALPHABET.charCodeAt(i)] = i
95
+ return table
96
+ })()
97
+
98
+ /** 逐字节与盐做 XOR(盐循环使用)—— 可逆,用于混淆。 */
99
+ function xorSalt(bytes: Uint8Array): Uint8Array {
100
+ const out = new Uint8Array(bytes.length)
101
+ const salt = OBFUSCATION_SALT
102
+ for (let i = 0; i < bytes.length; i += 1) out[i] = bytes[i]! ^ salt[i % salt.length]!
103
+ return out
104
+ }
105
+
106
+ /** Base64URL 编码(**无填充**,`-` / `_` 代替 `+` / `/`)。 */
107
+ function base64UrlEncode(bytes: Uint8Array): string {
108
+ let out = ''
109
+ for (let i = 0; i < bytes.length; i += 3) {
110
+ const b0 = bytes[i]!
111
+ const b1 = i + 1 < bytes.length ? bytes[i + 1]! : undefined
112
+ const b2 = i + 2 < bytes.length ? bytes[i + 2]! : undefined
113
+ out += B64_ALPHABET[b0 >> 2]
114
+ out += B64_ALPHABET[((b0 & 0x03) << 4) | ((b1 ?? 0) >> 4)]
115
+ if (b1 === undefined) break
116
+ out += B64_ALPHABET[((b1 & 0x0f) << 2) | ((b2 ?? 0) >> 6)]
117
+ if (b2 === undefined) break
118
+ out += B64_ALPHABET[b2 & 0x3f]
119
+ }
120
+ return out
121
+ }
122
+
123
+ /** Base64URL 解码;含非法字符或长度非法(`len % 4 === 1`)时返回 `null`。 */
124
+ function base64UrlDecode(text: string): Uint8Array | null {
125
+ if (!text) return null
126
+ const values: number[] = []
127
+ for (const ch of text) {
128
+ const code = ch.charCodeAt(0)
129
+ const v = code < 128 ? B64_LOOKUP[code]! : -1
130
+ if (v < 0) return null
131
+ values.push(v)
132
+ }
133
+ if (values.length % 4 === 1) return null
134
+ const out = new Uint8Array((values.length * 6) >> 3)
135
+ let o = 0
136
+ for (let i = 0; i < values.length; i += 4) {
137
+ const c0 = values[i]!
138
+ const c1 = values[i + 1]
139
+ const c2 = values[i + 2]
140
+ const c3 = values[i + 3]
141
+ out[o++] = (c0 << 2) | ((c1 ?? 0) >> 4)
142
+ if (c2 === undefined) break
143
+ out[o++] = (((c1 ?? 0) & 0x0f) << 4) | (c2 >> 2)
144
+ if (c3 === undefined) break
145
+ out[o++] = ((c2 & 0x03) << 6) | c3
146
+ }
147
+ return out.subarray(0, o)
148
+ }
149
+
150
+ /** 把 `vrp1:` 串还原成 JSON 文本;任何异常(非法字符 / 长度 / 解码失败)都返回 `null`。 */
151
+ function decodeObfuscated(text: string): string | null {
152
+ const bytes = base64UrlDecode(text.slice(PAIRING_OBFUSCATION_PREFIX.length))
153
+ if (!bytes) return null
154
+ return textDecoder.decode(xorSalt(bytes))
155
+ }
156
+
157
+ /** 序列化为二维码内容(**混淆后的串**,不再是明文 JSON)。 */
57
158
  export function encodePairingPayload(payload: PairingPayload): string {
58
- return JSON.stringify(payload)
159
+ const json = textEncoder.encode(JSON.stringify(payload))
160
+ return PAIRING_OBFUSCATION_PREFIX + base64UrlEncode(xorSalt(json))
59
161
  }
60
162
 
61
163
  /**
@@ -68,9 +170,13 @@ export function encodePairingPayload(payload: PairingPayload): string {
68
170
  */
69
171
  export function parsePairingPayload(text: string): PairingPayload | null {
70
172
  if (typeof text !== 'string' || !text.trim()) return null
173
+ const trimmed = text.trim()
174
+ // 带前缀 → 先解混淆;否则按旧版明文 JSON 解析(向后兼容,见 PAIRING_OBFUSCATION_PREFIX)
175
+ const jsonText = trimmed.startsWith(PAIRING_OBFUSCATION_PREFIX) ? decodeObfuscated(trimmed) : trimmed
176
+ if (jsonText === null) return null
71
177
  let raw: unknown
72
178
  try {
73
- raw = JSON.parse(text)
179
+ raw = JSON.parse(jsonText)
74
180
  } catch {
75
181
  return null
76
182
  }