@lyrify/znl 0.5.2 → 0.5.6
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/README.md +17 -2
- package/package.json +1 -1
- package/src/ZNL.js +281 -39
package/README.md
CHANGED
|
@@ -15,7 +15,8 @@
|
|
|
15
15
|
- `true`:签名 + 防重放 + payload 透明加密(AES-256-GCM)
|
|
16
16
|
- 可选关闭 payload 摘要校验(`enablePayloadDigest=false`)以提升性能
|
|
17
17
|
- 建议 master/slave 两端保持一致配置,避免认证不一致
|
|
18
|
-
- `authKey` 仅在 `encrypted=true` 时必填
|
|
18
|
+
- `authKey` 或 `authKeyMap` 仅在 `encrypted=true` 时必填
|
|
19
|
+
- `authKeyMap` 支持 master 按 `slaveId` 配置不同 key(未命中时会回退到 `authKey`)
|
|
19
20
|
- Payload 支持 `string`、`Buffer`、`Uint8Array` 及其数组(多帧)
|
|
20
21
|
|
|
21
22
|
## 安装
|
|
@@ -103,6 +104,7 @@ new ZNL({
|
|
|
103
104
|
},
|
|
104
105
|
maxPending: 1000,
|
|
105
106
|
authKey: "",
|
|
107
|
+
authKeyMap: { "slave-001": "k1", "slave-002": "k2" },
|
|
106
108
|
heartbeatInterval: 3000,
|
|
107
109
|
heartbeatTimeoutMs: 0,
|
|
108
110
|
encrypted: false,
|
|
@@ -118,7 +120,8 @@ new ZNL({
|
|
|
118
120
|
| `id` | ✓ | 节点唯一标识;slave 端同时作为 ZMQ `routingId` |
|
|
119
121
|
| `endpoints.router` | | ROUTER 端点,默认 `tcp://127.0.0.1:6003` |
|
|
120
122
|
| `maxPending` | | 最大并发 RPC 请求数,默认 `1000`;`0` 表示不限制 |
|
|
121
|
-
| `authKey` | | 共享认证 Key
|
|
123
|
+
| `authKey` | | 共享认证 Key;与 `authKeyMap` 二选一(`encrypted=true` 时至少提供一个) |
|
|
124
|
+
| `authKeyMap` | | master 侧 slaveId → authKey 映射;未命中时回退到 `authKey` |
|
|
122
125
|
| `heartbeatInterval` | | 心跳间隔(毫秒),默认 `3000`,`0` 表示禁用心跳 |
|
|
123
126
|
| `heartbeatTimeoutMs` | | 心跳超时时间(毫秒),默认 `0` 表示使用 `heartbeatInterval × 3` |
|
|
124
127
|
| `encrypted` | | 是否启用加密:`false`(默认,明文) / `true`(签名+防重放+透明加密) |
|
|
@@ -158,6 +161,18 @@ new ZNL({
|
|
|
158
161
|
- `identityOrHandler` 为函数时:注册 master 侧自动回复处理器(Slave 发来 RPC 请求时触发)
|
|
159
162
|
- `identityOrHandler` 为 identity(slave ID)时:Master 主动向指定 Slave 发送 RPC 请求并等待响应,返回 `Promise<Buffer | Array>`
|
|
160
163
|
|
|
164
|
+
### `addAuthKey(slaveId, authKey)`
|
|
165
|
+
|
|
166
|
+
**Master 侧调用:**
|
|
167
|
+
|
|
168
|
+
- 动态添加/更新某个 slave 的 authKey(立即生效)
|
|
169
|
+
|
|
170
|
+
### `removeAuthKey(slaveId)`
|
|
171
|
+
|
|
172
|
+
**Master 侧调用:**
|
|
173
|
+
|
|
174
|
+
- 移除某个 slave 的 authKey(立即生效),并触发 `slave_disconnected`
|
|
175
|
+
|
|
161
176
|
### `options.timeoutMs`
|
|
162
177
|
|
|
163
178
|
单次 RPC 请求超时时间,默认 `5000` ms。
|
package/package.json
CHANGED
package/src/ZNL.js
CHANGED
|
@@ -127,13 +127,19 @@ export class ZNL extends EventEmitter {
|
|
|
127
127
|
/** @type {boolean} 是否启用 payload 摘要校验(安全模式用) */
|
|
128
128
|
#enablePayloadDigest = DEFAULT_ENABLE_PAYLOAD_DIGEST;
|
|
129
129
|
|
|
130
|
+
/** @type {Map<string, string>} master 侧 authKey 配置表(按 slaveId) */
|
|
131
|
+
#authKeyMap = new Map();
|
|
132
|
+
|
|
133
|
+
/** @type {Map<string, { signKey: Buffer, encryptKey: Buffer }>} master 侧派生密钥缓存 */
|
|
134
|
+
#slaveKeyCache = new Map();
|
|
135
|
+
|
|
130
136
|
// ─── PUB/SUB 状态 ────────────────────────────────────────────────────────
|
|
131
137
|
|
|
132
138
|
/**
|
|
133
139
|
* master 侧:已注册的在线 slave 表
|
|
134
140
|
* key = slaveId(字符串)
|
|
135
|
-
* value = { identity: Buffer, lastSeen: number }
|
|
136
|
-
* @type {Map<string, { identity: Buffer, lastSeen: number }>}
|
|
141
|
+
* value = { identity: Buffer, lastSeen: number, signKey: Buffer|null, encryptKey: Buffer|null }
|
|
142
|
+
* @type {Map<string, { identity: Buffer, lastSeen: number, signKey: Buffer|null, encryptKey: Buffer|null }>}
|
|
137
143
|
*/
|
|
138
144
|
#slaves = new Map();
|
|
139
145
|
|
|
@@ -185,6 +191,7 @@ export class ZNL extends EventEmitter {
|
|
|
185
191
|
* endpoints? : { router?: string },
|
|
186
192
|
* maxPending? : number,
|
|
187
193
|
* authKey? : string,
|
|
194
|
+
* authKeyMap? : Record<string, string>,
|
|
188
195
|
* heartbeatInterval? : number,
|
|
189
196
|
* heartbeatTimeoutMs? : number,
|
|
190
197
|
* encrypted? : boolean,
|
|
@@ -199,6 +206,7 @@ export class ZNL extends EventEmitter {
|
|
|
199
206
|
endpoints = {},
|
|
200
207
|
maxPending = DEFAULT_MAX_PENDING,
|
|
201
208
|
authKey = "",
|
|
209
|
+
authKeyMap = null,
|
|
202
210
|
heartbeatInterval = DEFAULT_HEARTBEAT_INTERVAL,
|
|
203
211
|
heartbeatTimeoutMs = DEFAULT_HEARTBEAT_TIMEOUT_MS,
|
|
204
212
|
encrypted = false,
|
|
@@ -220,16 +228,35 @@ export class ZNL extends EventEmitter {
|
|
|
220
228
|
this.endpoints = { ...DEFAULT_ENDPOINTS, ...endpoints };
|
|
221
229
|
this.authKey = authKey == null ? "" : String(authKey);
|
|
222
230
|
|
|
231
|
+
if (this.role === "master") {
|
|
232
|
+
if (authKeyMap != null) {
|
|
233
|
+
if (typeof authKeyMap !== "object") {
|
|
234
|
+
throw new TypeError(
|
|
235
|
+
"`authKeyMap` 必须是对象(slaveId -> authKey)。",
|
|
236
|
+
);
|
|
237
|
+
}
|
|
238
|
+
for (const [slaveId, key] of Object.entries(authKeyMap)) {
|
|
239
|
+
if (key == null) continue;
|
|
240
|
+
this.#authKeyMap.set(String(slaveId), String(key));
|
|
241
|
+
}
|
|
242
|
+
}
|
|
243
|
+
}
|
|
244
|
+
|
|
223
245
|
this.encrypted = Boolean(encrypted);
|
|
224
246
|
this.#secureEnabled = this.encrypted;
|
|
225
247
|
|
|
226
248
|
if (this.#secureEnabled) {
|
|
227
|
-
|
|
228
|
-
|
|
249
|
+
const hasAuthMap = this.role === "master" && authKeyMap != null;
|
|
250
|
+
if (!this.authKey && !hasAuthMap) {
|
|
251
|
+
throw new Error(
|
|
252
|
+
"启用 encrypted=true 时,必须提供非空 authKey 或 authKeyMap。",
|
|
253
|
+
);
|
|
254
|
+
}
|
|
255
|
+
if (this.authKey) {
|
|
256
|
+
const { signKey, encryptKey } = deriveKeys(this.authKey);
|
|
257
|
+
this.#signKey = signKey;
|
|
258
|
+
this.#encryptKey = encryptKey;
|
|
229
259
|
}
|
|
230
|
-
const { signKey, encryptKey } = deriveKeys(this.authKey);
|
|
231
|
-
this.#signKey = signKey;
|
|
232
|
-
this.#encryptKey = encryptKey;
|
|
233
260
|
}
|
|
234
261
|
|
|
235
262
|
this.#pending = new PendingManager(maxPending);
|
|
@@ -356,19 +383,34 @@ export class ZNL extends EventEmitter {
|
|
|
356
383
|
if (this.#slaves.size === 0) return;
|
|
357
384
|
|
|
358
385
|
const topicText = String(topic);
|
|
359
|
-
const payloadFrames = this.#sealPayloadFrames(
|
|
360
|
-
"publish",
|
|
361
|
-
topicText,
|
|
362
|
-
payload,
|
|
363
|
-
);
|
|
364
|
-
const authProof = this.#secureEnabled
|
|
365
|
-
? this.#createAuthProof("publish", topicText, payloadFrames)
|
|
366
|
-
: "";
|
|
367
|
-
|
|
368
|
-
const frames = buildPublishFrames(topicText, payloadFrames, authProof);
|
|
369
386
|
|
|
370
387
|
for (const [slaveId, entry] of this.#slaves) {
|
|
388
|
+
const keys = this.#secureEnabled ? this.#resolveSlaveKeys(slaveId) : null;
|
|
389
|
+
if (this.#secureEnabled && !keys) {
|
|
390
|
+
if (this.#slaves.delete(slaveId)) {
|
|
391
|
+
this.emit("slave_disconnected", slaveId);
|
|
392
|
+
}
|
|
393
|
+
continue;
|
|
394
|
+
}
|
|
395
|
+
|
|
396
|
+
const payloadFrames = this.#sealPayloadFrames(
|
|
397
|
+
"publish",
|
|
398
|
+
topicText,
|
|
399
|
+
payload,
|
|
400
|
+
keys?.encryptKey ?? null,
|
|
401
|
+
);
|
|
402
|
+
const authProof = this.#secureEnabled
|
|
403
|
+
? this.#createAuthProof(
|
|
404
|
+
"publish",
|
|
405
|
+
topicText,
|
|
406
|
+
payloadFrames,
|
|
407
|
+
keys.signKey,
|
|
408
|
+
)
|
|
409
|
+
: "";
|
|
410
|
+
|
|
411
|
+
const frames = buildPublishFrames(topicText, payloadFrames, authProof);
|
|
371
412
|
const idFrame = identityToBuffer(entry.identity);
|
|
413
|
+
|
|
372
414
|
this.#sendQueue
|
|
373
415
|
.enqueue("router", () => socket.send([idFrame, ...frames]))
|
|
374
416
|
.catch(() => {
|
|
@@ -424,6 +466,63 @@ export class ZNL extends EventEmitter {
|
|
|
424
466
|
return [...this.#slaves.keys()];
|
|
425
467
|
}
|
|
426
468
|
|
|
469
|
+
/**
|
|
470
|
+
* 【Master 侧】动态添加/更新某个 slave 的 authKey(立即生效)
|
|
471
|
+
* @param {string} slaveId
|
|
472
|
+
* @param {string} authKey
|
|
473
|
+
* @returns {this}
|
|
474
|
+
*/
|
|
475
|
+
addAuthKey(slaveId, authKey) {
|
|
476
|
+
if (this.role !== "master") {
|
|
477
|
+
throw new Error("addAuthKey() 只能在 master 侧调用。");
|
|
478
|
+
}
|
|
479
|
+
const id = String(slaveId ?? "");
|
|
480
|
+
if (!id) {
|
|
481
|
+
throw new Error("slaveId 不能为空。");
|
|
482
|
+
}
|
|
483
|
+
const key = authKey == null ? "" : String(authKey);
|
|
484
|
+
if (!key) {
|
|
485
|
+
throw new Error("authKey 不能为空。");
|
|
486
|
+
}
|
|
487
|
+
|
|
488
|
+
this.#authKeyMap.set(id, key);
|
|
489
|
+
|
|
490
|
+
if (this.#secureEnabled) {
|
|
491
|
+
const derived = deriveKeys(key);
|
|
492
|
+
this.#slaveKeyCache.set(id, derived);
|
|
493
|
+
const entry = this.#slaves.get(id);
|
|
494
|
+
if (entry) {
|
|
495
|
+
entry.signKey = derived.signKey;
|
|
496
|
+
entry.encryptKey = derived.encryptKey;
|
|
497
|
+
}
|
|
498
|
+
}
|
|
499
|
+
return this;
|
|
500
|
+
}
|
|
501
|
+
|
|
502
|
+
/**
|
|
503
|
+
* 【Master 侧】移除某个 slave 的 authKey(立即生效)
|
|
504
|
+
* @param {string} slaveId
|
|
505
|
+
* @returns {this}
|
|
506
|
+
*/
|
|
507
|
+
removeAuthKey(slaveId) {
|
|
508
|
+
if (this.role !== "master") {
|
|
509
|
+
throw new Error("removeAuthKey() 只能在 master 侧调用。");
|
|
510
|
+
}
|
|
511
|
+
const id = String(slaveId ?? "");
|
|
512
|
+
if (!id) {
|
|
513
|
+
throw new Error("slaveId 不能为空。");
|
|
514
|
+
}
|
|
515
|
+
|
|
516
|
+
this.#authKeyMap.delete(id);
|
|
517
|
+
this.#slaveKeyCache.delete(id);
|
|
518
|
+
|
|
519
|
+
if (this.#slaves.has(id)) {
|
|
520
|
+
this.#slaves.delete(id);
|
|
521
|
+
this.emit("slave_disconnected", id);
|
|
522
|
+
}
|
|
523
|
+
return this;
|
|
524
|
+
}
|
|
525
|
+
|
|
427
526
|
// ═══════════════════════════════════════════════════════════════════════════
|
|
428
527
|
// 生命周期内部实现
|
|
429
528
|
// ═══════════════════════════════════════════════════════════════════════════
|
|
@@ -486,6 +585,7 @@ export class ZNL extends EventEmitter {
|
|
|
486
585
|
this.#sockets = {};
|
|
487
586
|
this.#sendQueue.clear();
|
|
488
587
|
this.#slaves.clear();
|
|
588
|
+
this.#slaveKeyCache.clear();
|
|
489
589
|
this.#masterNodeId = null;
|
|
490
590
|
this.#replayGuard.clear();
|
|
491
591
|
}
|
|
@@ -498,7 +598,7 @@ export class ZNL extends EventEmitter {
|
|
|
498
598
|
* 初始化 Master 侧 ROUTER socket(bind 模式),并启动读循环和死节点扫描定时器
|
|
499
599
|
*/
|
|
500
600
|
async #startMasterSockets() {
|
|
501
|
-
const router = new zmq.Router();
|
|
601
|
+
const router = new zmq.Router({ handover: true });
|
|
502
602
|
await router.bind(this.endpoints.router);
|
|
503
603
|
this.#sockets.router = router;
|
|
504
604
|
|
|
@@ -555,17 +655,34 @@ export class ZNL extends EventEmitter {
|
|
|
555
655
|
// ── 心跳 ────────────────────────────────────────────────────────────────
|
|
556
656
|
if (parsed.kind === "heartbeat") {
|
|
557
657
|
if (this.#secureEnabled) {
|
|
658
|
+
const keys = this.#resolveSlaveKeys(identityText);
|
|
659
|
+
if (!keys) {
|
|
660
|
+
this.#emitAuthFailed(event, "未配置该 slave 的 authKey。");
|
|
661
|
+
if (this.#slaves.delete(identityText)) {
|
|
662
|
+
this.emit("slave_disconnected", identityText);
|
|
663
|
+
}
|
|
664
|
+
return;
|
|
665
|
+
}
|
|
666
|
+
|
|
558
667
|
const v = this.#verifyIncomingProof({
|
|
559
668
|
kind: "heartbeat",
|
|
560
669
|
proofToken: parsed.authProof,
|
|
561
670
|
requestId: "",
|
|
562
671
|
payloadFrames: [],
|
|
563
672
|
expectedNodeId: identityText,
|
|
673
|
+
signKey: keys.signKey,
|
|
564
674
|
});
|
|
565
675
|
if (!v.ok) {
|
|
566
676
|
this.#emitAuthFailed(event, v.error);
|
|
567
677
|
return;
|
|
568
678
|
}
|
|
679
|
+
|
|
680
|
+
this.#ensureSlaveOnline(identityText, identity, {
|
|
681
|
+
touch: true,
|
|
682
|
+
signKey: keys.signKey,
|
|
683
|
+
encryptKey: keys.encryptKey,
|
|
684
|
+
});
|
|
685
|
+
return;
|
|
569
686
|
}
|
|
570
687
|
|
|
571
688
|
// 心跳视为在线确认:必要时自动补注册
|
|
@@ -576,20 +693,42 @@ export class ZNL extends EventEmitter {
|
|
|
576
693
|
// ── 注册 ────────────────────────────────────────────────────────────────
|
|
577
694
|
if (parsed.kind === "register") {
|
|
578
695
|
if (this.#secureEnabled) {
|
|
696
|
+
const keys = this.#resolveSlaveKeys(identityText);
|
|
697
|
+
if (!keys) {
|
|
698
|
+
this.#emitAuthFailed(event, "未配置该 slave 的 authKey。");
|
|
699
|
+
if (this.#slaves.delete(identityText)) {
|
|
700
|
+
this.emit("slave_disconnected", identityText);
|
|
701
|
+
}
|
|
702
|
+
return;
|
|
703
|
+
}
|
|
704
|
+
|
|
579
705
|
const v = this.#verifyIncomingProof({
|
|
580
706
|
kind: "register",
|
|
581
707
|
proofToken: parsed.authKey, // register 复用 authKey 字段承载 proof
|
|
582
708
|
requestId: "",
|
|
583
709
|
payloadFrames: [],
|
|
584
710
|
expectedNodeId: identityText,
|
|
711
|
+
signKey: keys.signKey,
|
|
585
712
|
});
|
|
586
713
|
if (!v.ok) {
|
|
587
714
|
this.#emitAuthFailed(event, v.error);
|
|
588
715
|
return;
|
|
589
716
|
}
|
|
717
|
+
|
|
718
|
+
this.#ensureSlaveOnline(identityText, identity, {
|
|
719
|
+
touch: true,
|
|
720
|
+
signKey: keys.signKey,
|
|
721
|
+
encryptKey: keys.encryptKey,
|
|
722
|
+
});
|
|
723
|
+
return;
|
|
590
724
|
}
|
|
591
725
|
|
|
592
|
-
this.#slaves.set(identityText, {
|
|
726
|
+
this.#slaves.set(identityText, {
|
|
727
|
+
identity,
|
|
728
|
+
lastSeen: Date.now(),
|
|
729
|
+
signKey: null,
|
|
730
|
+
encryptKey: null,
|
|
731
|
+
});
|
|
593
732
|
this.emit("slave_connected", identityText);
|
|
594
733
|
return;
|
|
595
734
|
}
|
|
@@ -607,12 +746,22 @@ export class ZNL extends EventEmitter {
|
|
|
607
746
|
let finalFrames = parsed.payloadFrames;
|
|
608
747
|
|
|
609
748
|
if (this.#secureEnabled) {
|
|
749
|
+
const keys = this.#resolveSlaveKeys(identityText);
|
|
750
|
+
if (!keys) {
|
|
751
|
+
this.#emitAuthFailed(event, "未配置该 slave 的 authKey。");
|
|
752
|
+
if (this.#slaves.delete(identityText)) {
|
|
753
|
+
this.emit("slave_disconnected", identityText);
|
|
754
|
+
}
|
|
755
|
+
return;
|
|
756
|
+
}
|
|
757
|
+
|
|
610
758
|
const v = this.#verifyIncomingProof({
|
|
611
759
|
kind: "request",
|
|
612
760
|
proofToken: parsed.authKey, // request 复用 authKey 字段承载 proof
|
|
613
761
|
requestId: parsed.requestId,
|
|
614
762
|
payloadFrames: parsed.payloadFrames,
|
|
615
763
|
expectedNodeId: identityText,
|
|
764
|
+
signKey: keys.signKey,
|
|
616
765
|
});
|
|
617
766
|
if (!v.ok) {
|
|
618
767
|
this.#emitAuthFailed(event, v.error);
|
|
@@ -620,7 +769,11 @@ export class ZNL extends EventEmitter {
|
|
|
620
769
|
}
|
|
621
770
|
|
|
622
771
|
// 认证通过后允许补注册(避免 master 重启后丢失在线表)
|
|
623
|
-
this.#ensureSlaveOnline(identityText, identity, {
|
|
772
|
+
this.#ensureSlaveOnline(identityText, identity, {
|
|
773
|
+
touch: true,
|
|
774
|
+
signKey: keys.signKey,
|
|
775
|
+
encryptKey: keys.encryptKey,
|
|
776
|
+
});
|
|
624
777
|
|
|
625
778
|
if (this.encrypted) {
|
|
626
779
|
try {
|
|
@@ -629,6 +782,7 @@ export class ZNL extends EventEmitter {
|
|
|
629
782
|
parsed.requestId,
|
|
630
783
|
parsed.payloadFrames,
|
|
631
784
|
identityText,
|
|
785
|
+
keys.encryptKey,
|
|
632
786
|
);
|
|
633
787
|
} catch (error) {
|
|
634
788
|
this.#emitAuthFailed(
|
|
@@ -663,12 +817,22 @@ export class ZNL extends EventEmitter {
|
|
|
663
817
|
let finalFrames = parsed.payloadFrames;
|
|
664
818
|
|
|
665
819
|
if (this.#secureEnabled) {
|
|
820
|
+
const keys = this.#resolveSlaveKeys(identityText);
|
|
821
|
+
if (!keys) {
|
|
822
|
+
this.#emitAuthFailed(event, "未配置该 slave 的 authKey。");
|
|
823
|
+
if (this.#slaves.delete(identityText)) {
|
|
824
|
+
this.emit("slave_disconnected", identityText);
|
|
825
|
+
}
|
|
826
|
+
return;
|
|
827
|
+
}
|
|
828
|
+
|
|
666
829
|
const v = this.#verifyIncomingProof({
|
|
667
830
|
kind: "response",
|
|
668
831
|
proofToken: parsed.authProof,
|
|
669
832
|
requestId: parsed.requestId,
|
|
670
833
|
payloadFrames: parsed.payloadFrames,
|
|
671
834
|
expectedNodeId: identityText,
|
|
835
|
+
signKey: keys.signKey,
|
|
672
836
|
});
|
|
673
837
|
if (!v.ok) {
|
|
674
838
|
this.#emitAuthFailed(event, v.error);
|
|
@@ -676,7 +840,11 @@ export class ZNL extends EventEmitter {
|
|
|
676
840
|
}
|
|
677
841
|
|
|
678
842
|
// 认证通过后允许补注册(避免 master 重启后丢失在线表)
|
|
679
|
-
this.#ensureSlaveOnline(identityText, identity, {
|
|
843
|
+
this.#ensureSlaveOnline(identityText, identity, {
|
|
844
|
+
touch: true,
|
|
845
|
+
signKey: keys.signKey,
|
|
846
|
+
encryptKey: keys.encryptKey,
|
|
847
|
+
});
|
|
680
848
|
|
|
681
849
|
if (this.encrypted) {
|
|
682
850
|
try {
|
|
@@ -685,6 +853,7 @@ export class ZNL extends EventEmitter {
|
|
|
685
853
|
parsed.requestId,
|
|
686
854
|
parsed.payloadFrames,
|
|
687
855
|
identityText,
|
|
856
|
+
keys.encryptKey,
|
|
688
857
|
);
|
|
689
858
|
} catch (error) {
|
|
690
859
|
this.#emitAuthFailed(
|
|
@@ -927,13 +1096,21 @@ export class ZNL extends EventEmitter {
|
|
|
927
1096
|
identityText,
|
|
928
1097
|
);
|
|
929
1098
|
|
|
1099
|
+
const keys = this.#secureEnabled
|
|
1100
|
+
? this.#resolveSlaveKeys(identityText)
|
|
1101
|
+
: null;
|
|
1102
|
+
if (this.#secureEnabled && !keys) {
|
|
1103
|
+
throw new Error(`未配置该 slave 的 authKey:${identityText}`);
|
|
1104
|
+
}
|
|
1105
|
+
|
|
930
1106
|
const payloadFrames = this.#sealPayloadFrames(
|
|
931
1107
|
"request",
|
|
932
1108
|
requestId,
|
|
933
1109
|
payload,
|
|
1110
|
+
keys?.encryptKey ?? null,
|
|
934
1111
|
);
|
|
935
1112
|
const proofOrAuthKey = this.#secureEnabled
|
|
936
|
-
? this.#createAuthProof("request", requestId, payloadFrames)
|
|
1113
|
+
? this.#createAuthProof("request", requestId, payloadFrames, keys.signKey)
|
|
937
1114
|
: "";
|
|
938
1115
|
|
|
939
1116
|
const frames = buildRequestFrames(requestId, payloadFrames, proofOrAuthKey);
|
|
@@ -981,15 +1158,29 @@ export class ZNL extends EventEmitter {
|
|
|
981
1158
|
*/
|
|
982
1159
|
async #replyTo(identity, requestId, payload) {
|
|
983
1160
|
const socket = this.#requireSocket("router", "ROUTER");
|
|
1161
|
+
const identityText = identityToString(identity);
|
|
984
1162
|
const idFrame = identityToBuffer(identity);
|
|
985
1163
|
|
|
1164
|
+
const keys = this.#secureEnabled
|
|
1165
|
+
? this.#resolveSlaveKeys(identityText)
|
|
1166
|
+
: null;
|
|
1167
|
+
if (this.#secureEnabled && !keys) {
|
|
1168
|
+
throw new Error(`未配置该 slave 的 authKey:${identityText}`);
|
|
1169
|
+
}
|
|
1170
|
+
|
|
986
1171
|
const payloadFrames = this.#sealPayloadFrames(
|
|
987
1172
|
"response",
|
|
988
1173
|
requestId,
|
|
989
1174
|
payload,
|
|
1175
|
+
keys?.encryptKey ?? null,
|
|
990
1176
|
);
|
|
991
1177
|
const authProof = this.#secureEnabled
|
|
992
|
-
? this.#createAuthProof(
|
|
1178
|
+
? this.#createAuthProof(
|
|
1179
|
+
"response",
|
|
1180
|
+
requestId,
|
|
1181
|
+
payloadFrames,
|
|
1182
|
+
keys.signKey,
|
|
1183
|
+
)
|
|
993
1184
|
: "";
|
|
994
1185
|
|
|
995
1186
|
const frames = buildResponseFrames(requestId, payloadFrames, authProof);
|
|
@@ -1061,8 +1252,9 @@ export class ZNL extends EventEmitter {
|
|
|
1061
1252
|
* @param {Buffer[]} payloadFrames
|
|
1062
1253
|
* @returns {string}
|
|
1063
1254
|
*/
|
|
1064
|
-
#createAuthProof(kind, requestId, payloadFrames) {
|
|
1065
|
-
|
|
1255
|
+
#createAuthProof(kind, requestId, payloadFrames, signKeyOverride = null) {
|
|
1256
|
+
const signKey = signKeyOverride ?? this.#signKey;
|
|
1257
|
+
if (!this.#secureEnabled || !signKey) return "";
|
|
1066
1258
|
|
|
1067
1259
|
const envelope = {
|
|
1068
1260
|
kind: String(kind),
|
|
@@ -1079,7 +1271,7 @@ export class ZNL extends EventEmitter {
|
|
|
1079
1271
|
// 保留 canonical 文本,便于后续排障(签名实际在 token 内完成)
|
|
1080
1272
|
canonicalSignInput(envelope);
|
|
1081
1273
|
|
|
1082
|
-
return encodeAuthProofToken(
|
|
1274
|
+
return encodeAuthProofToken(signKey, envelope);
|
|
1083
1275
|
}
|
|
1084
1276
|
|
|
1085
1277
|
/**
|
|
@@ -1100,9 +1292,12 @@ export class ZNL extends EventEmitter {
|
|
|
1100
1292
|
requestId,
|
|
1101
1293
|
payloadFrames,
|
|
1102
1294
|
expectedNodeId = null,
|
|
1295
|
+
signKey = null,
|
|
1103
1296
|
}) {
|
|
1104
1297
|
if (!this.#secureEnabled) return { ok: true, envelope: null };
|
|
1105
|
-
|
|
1298
|
+
|
|
1299
|
+
const verifyKey = signKey ?? this.#signKey;
|
|
1300
|
+
if (!verifyKey) {
|
|
1106
1301
|
return { ok: false, error: "签名密钥未初始化。" };
|
|
1107
1302
|
}
|
|
1108
1303
|
|
|
@@ -1110,7 +1305,7 @@ export class ZNL extends EventEmitter {
|
|
|
1110
1305
|
return { ok: false, error: "缺少认证证明(proofToken)。" };
|
|
1111
1306
|
}
|
|
1112
1307
|
|
|
1113
|
-
const decoded = decodeAuthProofToken(
|
|
1308
|
+
const decoded = decodeAuthProofToken(verifyKey, proofToken, {
|
|
1114
1309
|
maxSkewMs: this.#maxTimeSkewMs,
|
|
1115
1310
|
now: Date.now(),
|
|
1116
1311
|
});
|
|
@@ -1167,13 +1362,14 @@ export class ZNL extends EventEmitter {
|
|
|
1167
1362
|
* @param {*} payload
|
|
1168
1363
|
* @returns {Buffer[]}
|
|
1169
1364
|
*/
|
|
1170
|
-
#sealPayloadFrames(kind, requestId, payload) {
|
|
1365
|
+
#sealPayloadFrames(kind, requestId, payload, encryptKeyOverride = null) {
|
|
1171
1366
|
if (!this.encrypted) {
|
|
1172
1367
|
// 非加密模式保持历史行为:沿用协议层的原始帧类型(string/Buffer)
|
|
1173
1368
|
return normalizeFrames(payload);
|
|
1174
1369
|
}
|
|
1175
1370
|
|
|
1176
|
-
|
|
1371
|
+
const encryptKey = encryptKeyOverride ?? this.#encryptKey;
|
|
1372
|
+
if (!encryptKey) {
|
|
1177
1373
|
throw new Error("加密密钥未初始化。");
|
|
1178
1374
|
}
|
|
1179
1375
|
|
|
@@ -1183,11 +1379,7 @@ export class ZNL extends EventEmitter {
|
|
|
1183
1379
|
"utf8",
|
|
1184
1380
|
);
|
|
1185
1381
|
|
|
1186
|
-
const { iv, ciphertext, tag } = encryptFrames(
|
|
1187
|
-
this.#encryptKey,
|
|
1188
|
-
rawFrames,
|
|
1189
|
-
aad,
|
|
1190
|
-
);
|
|
1382
|
+
const { iv, ciphertext, tag } = encryptFrames(encryptKey, rawFrames, aad);
|
|
1191
1383
|
|
|
1192
1384
|
// 用统一信封包装:version + iv + tag + ciphertext
|
|
1193
1385
|
return [Buffer.from(SECURITY_ENVELOPE_VERSION), iv, tag, ciphertext];
|
|
@@ -1204,9 +1396,16 @@ export class ZNL extends EventEmitter {
|
|
|
1204
1396
|
* @param {string} senderNodeId
|
|
1205
1397
|
* @returns {Buffer[]}
|
|
1206
1398
|
*/
|
|
1207
|
-
#openPayloadFrames(
|
|
1399
|
+
#openPayloadFrames(
|
|
1400
|
+
kind,
|
|
1401
|
+
requestId,
|
|
1402
|
+
payloadFrames,
|
|
1403
|
+
senderNodeId,
|
|
1404
|
+
encryptKeyOverride = null,
|
|
1405
|
+
) {
|
|
1208
1406
|
if (!this.encrypted) return payloadFrames;
|
|
1209
|
-
|
|
1407
|
+
const encryptKey = encryptKeyOverride ?? this.#encryptKey;
|
|
1408
|
+
if (!encryptKey) throw new Error("加密密钥未初始化。");
|
|
1210
1409
|
|
|
1211
1410
|
if (!Array.isArray(payloadFrames) || payloadFrames.length !== 4) {
|
|
1212
1411
|
throw new Error("加密信封格式非法:期望 4 帧。");
|
|
@@ -1222,7 +1421,7 @@ export class ZNL extends EventEmitter {
|
|
|
1222
1421
|
"utf8",
|
|
1223
1422
|
);
|
|
1224
1423
|
|
|
1225
|
-
return decryptFrames(
|
|
1424
|
+
return decryptFrames(encryptKey, iv, ciphertext, tag, aad);
|
|
1226
1425
|
}
|
|
1227
1426
|
|
|
1228
1427
|
/**
|
|
@@ -1247,7 +1446,11 @@ export class ZNL extends EventEmitter {
|
|
|
1247
1446
|
* @param {Buffer|string|Uint8Array} identity
|
|
1248
1447
|
* @param {{ touch?: boolean }} [options]
|
|
1249
1448
|
*/
|
|
1250
|
-
#ensureSlaveOnline(
|
|
1449
|
+
#ensureSlaveOnline(
|
|
1450
|
+
identityText,
|
|
1451
|
+
identity,
|
|
1452
|
+
{ touch = true, signKey = null, encryptKey = null } = {},
|
|
1453
|
+
) {
|
|
1251
1454
|
const id = String(identityText ?? "");
|
|
1252
1455
|
if (!id) return;
|
|
1253
1456
|
|
|
@@ -1255,16 +1458,55 @@ export class ZNL extends EventEmitter {
|
|
|
1255
1458
|
const entry = this.#slaves.get(id);
|
|
1256
1459
|
if (entry) {
|
|
1257
1460
|
if (touch) entry.lastSeen = now;
|
|
1461
|
+
if (signKey) entry.signKey = signKey;
|
|
1462
|
+
if (encryptKey) entry.encryptKey = encryptKey;
|
|
1258
1463
|
return;
|
|
1259
1464
|
}
|
|
1260
1465
|
|
|
1261
1466
|
this.#slaves.set(id, {
|
|
1262
1467
|
identity: identityToBuffer(identity),
|
|
1263
1468
|
lastSeen: now,
|
|
1469
|
+
signKey: signKey ?? null,
|
|
1470
|
+
encryptKey: encryptKey ?? null,
|
|
1264
1471
|
});
|
|
1265
1472
|
this.emit("slave_connected", id);
|
|
1266
1473
|
}
|
|
1267
1474
|
|
|
1475
|
+
/**
|
|
1476
|
+
* master 侧解析某个 slave 的签名/加密密钥
|
|
1477
|
+
* - 优先匹配 authKeyMap
|
|
1478
|
+
* - 未命中时回退到 this.authKey(若提供)
|
|
1479
|
+
*
|
|
1480
|
+
* @param {string} slaveId
|
|
1481
|
+
* @returns {{ signKey: Buffer, encryptKey: Buffer }|null}
|
|
1482
|
+
*/
|
|
1483
|
+
#resolveSlaveKeys(slaveId) {
|
|
1484
|
+
if (!this.#secureEnabled) return null;
|
|
1485
|
+
|
|
1486
|
+
const id = String(slaveId ?? "");
|
|
1487
|
+
if (!id) return null;
|
|
1488
|
+
|
|
1489
|
+
const cached = this.#slaveKeyCache.get(id);
|
|
1490
|
+
if (cached) return cached;
|
|
1491
|
+
|
|
1492
|
+
let key = this.#authKeyMap.get(id);
|
|
1493
|
+
if (!key && this.authKey) {
|
|
1494
|
+
key = this.authKey;
|
|
1495
|
+
}
|
|
1496
|
+
if (!key) return null;
|
|
1497
|
+
|
|
1498
|
+
const derived = deriveKeys(key);
|
|
1499
|
+
this.#slaveKeyCache.set(id, derived);
|
|
1500
|
+
|
|
1501
|
+
const entry = this.#slaves.get(id);
|
|
1502
|
+
if (entry) {
|
|
1503
|
+
entry.signKey = derived.signKey;
|
|
1504
|
+
entry.encryptKey = derived.encryptKey;
|
|
1505
|
+
}
|
|
1506
|
+
|
|
1507
|
+
return derived;
|
|
1508
|
+
}
|
|
1509
|
+
|
|
1268
1510
|
// ═══════════════════════════════════════════════════════════════════════════
|
|
1269
1511
|
// 工具方法
|
|
1270
1512
|
// ═══════════════════════════════════════════════════════════════════════════
|