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 +24 -0
- package/README.md +6 -1
- package/dist/index.d.ts +1 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1 -1
- package/dist/index.js.map +1 -1
- package/dist/protocol/pairing.d.ts +11 -1
- package/dist/protocol/pairing.d.ts.map +1 -1
- package/dist/protocol/pairing.js +110 -3
- package/dist/protocol/pairing.js.map +1 -1
- package/package.json +1 -1
- package/src/index.ts +1 -0
- package/src/protocol/pairing.ts +109 -3
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';
|
package/dist/index.d.ts.map
CHANGED
|
@@ -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":"
|
|
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"}
|
package/dist/protocol/pairing.js
CHANGED
|
@@ -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
|
-
|
|
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(
|
|
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
|
|
1
|
+
{"version":3,"file":"pairing.js","sourceRoot":"","sources":["../../src/protocol/pairing.ts"],"names":[],"mappings":"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"}
|
package/package.json
CHANGED
package/src/index.ts
CHANGED
package/src/protocol/pairing.ts
CHANGED
|
@@ -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
|
-
|
|
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(
|
|
179
|
+
raw = JSON.parse(jsonText)
|
|
74
180
|
} catch {
|
|
75
181
|
return null
|
|
76
182
|
}
|