@mega-yfue/eufy-sdk 0.5.0-beta.69 → 0.5.0-beta.70
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/dist/core/contracts.d.ts +5 -0
- package/dist/index.js +146 -54
- package/dist/index.js.map +4 -4
- package/dist/model/capabilities/index.d.ts +5 -1
- package/dist/model/capabilities/smart-drop.d.ts +20 -0
- package/dist/model/types.d.ts +1 -1
- package/dist/transport/p2p/codec.d.ts +16 -0
- package/dist/transport/p2p/command-router.d.ts +10 -7
- package/dist/transport/p2p/live-trace.d.ts +11 -2
- package/dist/transport/p2p/p2p-session.d.ts +38 -11
- package/package.json +1 -1
package/dist/core/contracts.d.ts
CHANGED
|
@@ -329,6 +329,11 @@ export type Command = {
|
|
|
329
329
|
channel: number;
|
|
330
330
|
mValue3?: number;
|
|
331
331
|
form?: ScalarForm;
|
|
332
|
+
/**
|
|
333
|
+
* `"set-payload"`: a level-2 key that seals only this envelope (an RSA cipher's AES-128-ECB key)
|
|
334
|
+
* may seal it. Absent, the send waits for the key every level-2 sender uses.
|
|
335
|
+
*/
|
|
336
|
+
level2Sender?: "set-payload";
|
|
332
337
|
} | {
|
|
333
338
|
kind: "p2p-privacy-burst";
|
|
334
339
|
enabled: boolean;
|
package/dist/index.js
CHANGED
|
@@ -11097,6 +11097,30 @@ var LOCATE = {
|
|
|
11097
11097
|
}
|
|
11098
11098
|
};
|
|
11099
11099
|
|
|
11100
|
+
// dist/model/capabilities/smart-drop.js
|
|
11101
|
+
var OPEN_LID_CMD = 2100;
|
|
11102
|
+
var SMART_DROP_MEMBERS = {
|
|
11103
|
+
/**
|
|
11104
|
+
* Opens the lid with `SET_PAYLOAD` 2100 at level 2. Verified on a standalone T8790, whose cipher has
|
|
11105
|
+
* only an RSA key: the frame sealed AES-128-ECB opened the lid, the same frame at level 1 did not.
|
|
11106
|
+
*/
|
|
11107
|
+
openLid: method(({ ctx, sink }) => () => sink.dispatch({
|
|
11108
|
+
kind: "set-payload",
|
|
11109
|
+
cmd: OPEN_LID_CMD,
|
|
11110
|
+
payload: { isOpen: 1, userIndex: 0 },
|
|
11111
|
+
channel: ctx.channel,
|
|
11112
|
+
mValue3: 0,
|
|
11113
|
+
level2Sender: "set-payload"
|
|
11114
|
+
}), "Open the SmartDrop lid.")
|
|
11115
|
+
};
|
|
11116
|
+
var SMART_DROP = {
|
|
11117
|
+
capability: "smart_drop",
|
|
11118
|
+
description: "SmartDrop parcel box lid control.",
|
|
11119
|
+
members: SMART_DROP_MEMBERS,
|
|
11120
|
+
properties: propertiesOf(SMART_DROP_MEMBERS),
|
|
11121
|
+
detection: { detect: (record) => record.model === "T8790" && record.parentSn === void 0 }
|
|
11122
|
+
};
|
|
11123
|
+
|
|
11100
11124
|
// dist/model/capabilities/info.js
|
|
11101
11125
|
var MANUFACTURER = "eufy";
|
|
11102
11126
|
function buildDeviceInfo(ctx) {
|
|
@@ -11178,6 +11202,7 @@ var MODULES = [
|
|
|
11178
11202
|
SUCTION,
|
|
11179
11203
|
MOWER,
|
|
11180
11204
|
LOCATE,
|
|
11205
|
+
SMART_DROP,
|
|
11181
11206
|
DISPLAY,
|
|
11182
11207
|
INFO
|
|
11183
11208
|
];
|
|
@@ -14980,7 +15005,7 @@ import { setTimeout as sleep2 } from "node:timers/promises";
|
|
|
14980
15005
|
// dist/transport/p2p/p2p-session.js
|
|
14981
15006
|
import { EventEmitter as EventEmitter2 } from "node:events";
|
|
14982
15007
|
import dgram from "node:dgram";
|
|
14983
|
-
import { constants, createCipheriv as createCipheriv3, createDecipheriv as createDecipheriv4, generateKeyPairSync, privateDecrypt, randomBytes as randomBytes2 } from "node:crypto";
|
|
15008
|
+
import { constants as constants2, createCipheriv as createCipheriv3, createDecipheriv as createDecipheriv4, generateKeyPairSync, privateDecrypt as privateDecrypt2, randomBytes as randomBytes2 } from "node:crypto";
|
|
14984
15009
|
|
|
14985
15010
|
// dist/transport/p2p/codec.js
|
|
14986
15011
|
var codec_exports = {};
|
|
@@ -15005,6 +15030,7 @@ __export(codec_exports, {
|
|
|
15005
15030
|
decodeP2PCloudIPs: () => decodeP2PCloudIPs,
|
|
15006
15031
|
decryptP2PData: () => decryptP2PData,
|
|
15007
15032
|
deriveLevel2KeyFromGatewayInfo: () => deriveLevel2KeyFromGatewayInfo,
|
|
15033
|
+
deriveLevel2KeyFromRsaGatewayInfo: () => deriveLevel2KeyFromRsaGatewayInfo,
|
|
15008
15034
|
eciesUnwrap: () => eciesUnwrap,
|
|
15009
15035
|
encryptP2PData: () => encryptP2PData,
|
|
15010
15036
|
frameMessage: () => frameMessage,
|
|
@@ -15018,7 +15044,7 @@ __export(codec_exports, {
|
|
|
15018
15044
|
readNullTerminatedString: () => readNullTerminatedString,
|
|
15019
15045
|
stringWithLength: () => stringWithLength
|
|
15020
15046
|
});
|
|
15021
|
-
import { createCipheriv as createCipheriv2, createDecipheriv as createDecipheriv3, createECDH as createECDH2, createHmac as createHmac2, timingSafeEqual } from "node:crypto";
|
|
15047
|
+
import { constants, createCipheriv as createCipheriv2, createDecipheriv as createDecipheriv3, createECDH as createECDH2, createHmac as createHmac2, createPrivateKey, privateDecrypt, timingSafeEqual } from "node:crypto";
|
|
15022
15048
|
var MAGIC_WORD = "XZYH";
|
|
15023
15049
|
var RequestMessageType = {
|
|
15024
15050
|
LOCAL_LOOKUP: Buffer.from([241, 48]),
|
|
@@ -15162,6 +15188,27 @@ function deriveLevel2KeyFromGatewayInfo(gwPayload, eccPrivateKeyHex) {
|
|
|
15162
15188
|
const key = plain.subarray(0, 32);
|
|
15163
15189
|
return key.length === 32 ? Buffer.from(key) : void 0;
|
|
15164
15190
|
}
|
|
15191
|
+
function deriveLevel2KeyFromRsaGatewayInfo(gwPayload, rsaPrivateKey) {
|
|
15192
|
+
try {
|
|
15193
|
+
const key = createPrivateKey({ key: Buffer.from(rsaPrivateKey, "base64"), format: "der", type: "pkcs8" });
|
|
15194
|
+
const modulusBits = key.asymmetricKeyDetails?.modulusLength;
|
|
15195
|
+
if (!modulusBits)
|
|
15196
|
+
return void 0;
|
|
15197
|
+
const len = Math.ceil(modulusBits / 8);
|
|
15198
|
+
if (gwPayload.length < 4 + len)
|
|
15199
|
+
return void 0;
|
|
15200
|
+
const block = privateDecrypt({ key, padding: constants.RSA_NO_PADDING }, gwPayload.subarray(4, 4 + len));
|
|
15201
|
+
if (block[0] !== 0 || block[1] !== 2)
|
|
15202
|
+
return void 0;
|
|
15203
|
+
const separator = block.indexOf(0, 2);
|
|
15204
|
+
if (separator < 10)
|
|
15205
|
+
return void 0;
|
|
15206
|
+
const sessionKey = block.subarray(separator + 1);
|
|
15207
|
+
return sessionKey.length === 16 ? Buffer.from(sessionKey) : void 0;
|
|
15208
|
+
} catch {
|
|
15209
|
+
return void 0;
|
|
15210
|
+
}
|
|
15211
|
+
}
|
|
15165
15212
|
function buildLocalLookupPayload() {
|
|
15166
15213
|
return Buffer.from([0, 0]);
|
|
15167
15214
|
}
|
|
@@ -16023,6 +16070,12 @@ var P2PSession = class _P2PSession extends EventEmitter2 {
|
|
|
16023
16070
|
level1Key;
|
|
16024
16071
|
/** Negotiated 32-byte level-2/gateway key (AES-256-GCM). Set via setLevel2Key once known. */
|
|
16025
16072
|
level2Key;
|
|
16073
|
+
/**
|
|
16074
|
+
* Negotiated 16-byte level-2 key of a cipher with no ECC key (RSA-encrypted in `CMD_GATEWAYINFO`). Only
|
|
16075
|
+
* {@link sendControlLevel2} seals with it, AES-128-ECB in the level-1 layout at encryption type 2; never
|
|
16076
|
+
* held beside {@link level2Key}.
|
|
16077
|
+
*/
|
|
16078
|
+
level2EcbKey;
|
|
16026
16079
|
level2Seq = 0;
|
|
16027
16080
|
rsaPrivateKey;
|
|
16028
16081
|
rsaModulusHex;
|
|
@@ -16103,12 +16156,22 @@ var P2PSession = class _P2PSession extends EventEmitter2 {
|
|
|
16103
16156
|
this.level2Key = key;
|
|
16104
16157
|
this.settleLevel2();
|
|
16105
16158
|
}
|
|
16106
|
-
/** Whether the level-2 session key has been negotiated/set. */
|
|
16159
|
+
/** Whether the AES-256-GCM level-2 session key has been negotiated/set. */
|
|
16107
16160
|
get hasLevel2Key() {
|
|
16108
16161
|
return !!this.level2Key;
|
|
16109
16162
|
}
|
|
16163
|
+
/**
|
|
16164
|
+
* Whether the held level-2 key can frame for `sender`: every level-2 sender needs the AES-256-GCM key,
|
|
16165
|
+
* and `"set-payload"` ({@link sendControlLevel2}) also takes the AES-128-ECB key of an RSA cipher.
|
|
16166
|
+
*/
|
|
16167
|
+
level2KeyServes(sender) {
|
|
16168
|
+
return !!this.level2Key || sender === "set-payload" && !!this.level2EcbKey;
|
|
16169
|
+
}
|
|
16110
16170
|
/**
|
|
16111
16171
|
* Resolve with whether a level-2 key is available, waiting only while waiting can still change that.
|
|
16172
|
+
* `sender` names who will frame with it: by default every level-2 sender, which needs the AES-256-GCM
|
|
16173
|
+
* key; `"set-payload"` is {@link sendControlLevel2} alone, which also takes the AES-128-ECB key of an RSA
|
|
16174
|
+
* cipher, so `true` for it does not imply {@link hasLevel2Key}.
|
|
16112
16175
|
*
|
|
16113
16176
|
* A `"session"` grace is measured once, not restarted per call. Best-effort media uses it because every
|
|
16114
16177
|
* egress asks this same session and can proceed without the key on an own-session camera; a per-call
|
|
@@ -16133,12 +16196,12 @@ var P2PSession = class _P2PSession extends EventEmitter2 {
|
|
|
16133
16196
|
* `terminal` outcome is the one already stated where the negotiation concluded, since that is where the
|
|
16134
16197
|
* cipher and the cause are known, and re-stating it here would double every settled negotiation.
|
|
16135
16198
|
*/
|
|
16136
|
-
async awaitLevel2Key(graceMs, graceFrom = "call") {
|
|
16199
|
+
async awaitLevel2Key(graceMs, graceFrom = "call", sender = "every") {
|
|
16137
16200
|
if (this.closed) {
|
|
16138
16201
|
this.trace({ phase: "level2-unavailable", reason: "session-closed" });
|
|
16139
16202
|
return false;
|
|
16140
16203
|
}
|
|
16141
|
-
if (this.
|
|
16204
|
+
if (this.level2KeyServes(sender))
|
|
16142
16205
|
return true;
|
|
16143
16206
|
if (!this.level2Pending) {
|
|
16144
16207
|
this.trace({ phase: "level2-unavailable", reason: "not-negotiating" });
|
|
@@ -16172,16 +16235,17 @@ var P2PSession = class _P2PSession extends EventEmitter2 {
|
|
|
16172
16235
|
deadline.unref?.();
|
|
16173
16236
|
waiters.push(wake);
|
|
16174
16237
|
});
|
|
16238
|
+
const served = outcome === "key" || outcome === "terminal" && this.level2KeyServes(sender);
|
|
16175
16239
|
if (outcome === "timeout") {
|
|
16176
16240
|
this.logger.debug(`[p2p] ${this.cfg.stationSn} level-2 key did not arrive within its grace`);
|
|
16177
16241
|
this.trace({ phase: "level2-unavailable", reason: "grace-elapsed", waitedMs: remaining });
|
|
16178
|
-
} else if (outcome === "terminal") {
|
|
16242
|
+
} else if (outcome === "terminal" && !served) {
|
|
16179
16243
|
this.logger.debug(`[p2p] ${this.cfg.stationSn} level-2 negotiation concluded without a key`);
|
|
16180
16244
|
} else if (outcome === "closed") {
|
|
16181
16245
|
this.logger.debug(`[p2p] ${this.cfg.stationSn} session closed before the level-2 key arrived`);
|
|
16182
16246
|
this.trace({ phase: "level2-unavailable", reason: "session-closed" });
|
|
16183
16247
|
}
|
|
16184
|
-
return
|
|
16248
|
+
return served;
|
|
16185
16249
|
}
|
|
16186
16250
|
/**
|
|
16187
16251
|
* Ask the station for its gateway info a second time, re-opening a negotiation that concluded without a key.
|
|
@@ -16199,7 +16263,7 @@ var P2PSession = class _P2PSession extends EventEmitter2 {
|
|
|
16199
16263
|
* use this: re-prompting on their behalf would be noise for an answer they do not need.
|
|
16200
16264
|
*/
|
|
16201
16265
|
repromptLevel2Key() {
|
|
16202
|
-
if (this.closed || this.
|
|
16266
|
+
if (this.closed || this.level2KeyServes("set-payload") || this.level2Reprompted)
|
|
16203
16267
|
return false;
|
|
16204
16268
|
if (!this.cfg.resolveCipherKey || !this.connectAddress)
|
|
16205
16269
|
return false;
|
|
@@ -16218,9 +16282,12 @@ var P2PSession = class _P2PSession extends EventEmitter2 {
|
|
|
16218
16282
|
}
|
|
16219
16283
|
/**
|
|
16220
16284
|
* Negotiate the level-2 session key from the decrypted CMD_GATEWAYINFO payload: read its
|
|
16221
|
-
* `cipher_id`, resolve that cipher's
|
|
16222
|
-
* `resolveCipherKey`), run the ECIES unwrap, and `setLevel2Key()`.
|
|
16223
|
-
*
|
|
16285
|
+
* `cipher_id`, resolve that cipher's private keys once (cloud `get_ciphers`, via the configured
|
|
16286
|
+
* `resolveCipherKey`), run the ECIES unwrap with the ECC key, and `setLevel2Key()`. A cipher with no
|
|
16287
|
+
* ECC key uses its RSA key instead and holds the RSA-decrypted 16-byte key as the AES-128-ECB level-2
|
|
16288
|
+
* key. One-shot; emits `level2Ready` only when the AES-256-GCM key is negotiated, traces
|
|
16289
|
+
* `level2-set-payload-only` instead for the AES-128-ECB key, and emits `error` on failure
|
|
16290
|
+
* (non-fatal — level-1 traffic keeps working regardless).
|
|
16224
16291
|
*
|
|
16225
16292
|
* Every outcome settles the wait in {@link awaitLevel2Key}, because being one-shot is what makes a
|
|
16226
16293
|
* failure final for this connection generation: nothing will retry it there, so a later command must be
|
|
@@ -16233,26 +16300,36 @@ var P2PSession = class _P2PSession extends EventEmitter2 {
|
|
|
16233
16300
|
this.trace({ phase: "level2-negotiating", cipherId });
|
|
16234
16301
|
void (async () => {
|
|
16235
16302
|
try {
|
|
16236
|
-
const
|
|
16303
|
+
const resolved = await this.cfg.resolveCipherKey?.(cipherId);
|
|
16237
16304
|
if (this.closed || generation !== this.connectionGeneration)
|
|
16238
16305
|
return;
|
|
16239
|
-
|
|
16240
|
-
|
|
16306
|
+
const keys = typeof resolved === "string" ? { ecc: resolved } : resolved;
|
|
16307
|
+
const eccPrivHex = keys?.ecc;
|
|
16308
|
+
const rsaPrivKey = eccPrivHex ? void 0 : keys?.rsa;
|
|
16309
|
+
if (!eccPrivHex && !rsaPrivKey) {
|
|
16310
|
+
this.logger.debug(`[p2p] ${this.cfg.stationSn} no ECC or RSA key for cipher_id ${cipherId}`);
|
|
16241
16311
|
this.trace({ phase: "level2-unavailable", reason: "no-cipher-key", cipherId });
|
|
16242
16312
|
this.settleLevel2();
|
|
16243
16313
|
return;
|
|
16244
16314
|
}
|
|
16245
|
-
const key = deriveLevel2KeyFromGatewayInfo(gwPayload, eccPrivHex);
|
|
16315
|
+
const key = eccPrivHex ? deriveLevel2KeyFromGatewayInfo(gwPayload, eccPrivHex) : deriveLevel2KeyFromRsaGatewayInfo(gwPayload, rsaPrivKey);
|
|
16246
16316
|
if (!key) {
|
|
16247
16317
|
this.trace({ phase: "level2-unavailable", reason: "derivation-failed", cipherId });
|
|
16248
16318
|
this.settleLevel2();
|
|
16249
16319
|
this.emit("error", new Error(`level-2 key derivation failed (cipher_id ${cipherId})`));
|
|
16250
16320
|
return;
|
|
16251
16321
|
}
|
|
16252
|
-
|
|
16253
|
-
|
|
16254
|
-
|
|
16255
|
-
|
|
16322
|
+
if (eccPrivHex) {
|
|
16323
|
+
this.setLevel2Key(key);
|
|
16324
|
+
this.logger.debug(`[p2p] ${this.cfg.stationSn} level-2 key negotiated (cipher_id ${cipherId})`);
|
|
16325
|
+
this.trace({ phase: "level2-ready", cipherId });
|
|
16326
|
+
this.emit("level2Ready", { cipherId });
|
|
16327
|
+
} else {
|
|
16328
|
+
this.level2EcbKey = key;
|
|
16329
|
+
this.logger.debug(`[p2p] ${this.cfg.stationSn} level-2 set-payload key negotiated (cipher_id ${cipherId})`);
|
|
16330
|
+
this.trace({ phase: "level2-set-payload-only", cipherId });
|
|
16331
|
+
this.settleLevel2();
|
|
16332
|
+
}
|
|
16256
16333
|
} catch (e) {
|
|
16257
16334
|
if (this.closed || generation !== this.connectionGeneration)
|
|
16258
16335
|
return;
|
|
@@ -16322,7 +16399,7 @@ var P2PSession = class _P2PSession extends EventEmitter2 {
|
|
|
16322
16399
|
this.closed = false;
|
|
16323
16400
|
this.connectionGeneration += 1;
|
|
16324
16401
|
this.resetInboundSequencing();
|
|
16325
|
-
if (!this.
|
|
16402
|
+
if (!this.level2KeyServes("set-payload")) {
|
|
16326
16403
|
this.level2Pending = this.cfg.resolveCipherKey !== void 0;
|
|
16327
16404
|
this.level2Negotiating = false;
|
|
16328
16405
|
this.level2Reprompted = false;
|
|
@@ -16805,18 +16882,25 @@ var P2PSession = class _P2PSession extends EventEmitter2 {
|
|
|
16805
16882
|
* mechanism proven for camera selection in media start. Use for control commands routed through a
|
|
16806
16883
|
* HomeBase (where level-1 ECB is rejected). Returns `false` if the level-2 key isn't negotiated
|
|
16807
16884
|
* yet (caller can fall back to {@link sendStringPayloadCommand}).
|
|
16885
|
+
*
|
|
16886
|
+
* On a session holding only the AES-128-ECB level-2 key of an RSA cipher, the frame is the level-1
|
|
16887
|
+
* layout ({@link sendRawLevel1Bytes}) sealed under that key at encryption type 2: the encryption-type
|
|
16888
|
+
* byte names the key the body is sealed with (1 = level-1 key, 2 = the negotiated key).
|
|
16808
16889
|
*/
|
|
16809
16890
|
sendControlLevel2(cmd, channel, accountId, payload, mValue3 = cmd) {
|
|
16810
|
-
if (!this.connectAddress || !this.
|
|
16811
|
-
return false;
|
|
16812
|
-
const value = JSON.stringify({ account_id: accountId, cmd, mChannel: channel, mValue3, payload });
|
|
16813
|
-
const body = this.encryptLevel2(Buffer.from(value, "utf-8"));
|
|
16814
|
-
if (!body)
|
|
16891
|
+
if (!this.connectAddress || !this.level2KeyServes("set-payload"))
|
|
16815
16892
|
return false;
|
|
16816
|
-
const
|
|
16817
|
-
|
|
16818
|
-
|
|
16819
|
-
|
|
16893
|
+
const value = Buffer.from(JSON.stringify({ account_id: accountId, cmd, mChannel: channel, mValue3, payload }), "utf-8");
|
|
16894
|
+
let sealed;
|
|
16895
|
+
if (this.level2Key) {
|
|
16896
|
+
const body = this.encryptLevel2(value);
|
|
16897
|
+
if (!body)
|
|
16898
|
+
return false;
|
|
16899
|
+
sealed = buildRawCommandPayload(body, channel, 8, [8, 0], 0);
|
|
16900
|
+
} else {
|
|
16901
|
+
sealed = buildRawCommandPayload(encryptP2PData(paddingP2PData(value), this.level2EcbKey), channel, 2);
|
|
16902
|
+
}
|
|
16903
|
+
const data = Buffer.concat([buildCommandHeader(this.seqNumber, CMD_SET_PAYLOAD), sealed]);
|
|
16820
16904
|
this.seqNumber = this.seqNumber + 1 & 65535;
|
|
16821
16905
|
this.send(this.connectAddress, RequestMessageType.DATA, data);
|
|
16822
16906
|
return true;
|
|
@@ -17152,7 +17236,7 @@ var P2PSession = class _P2PSession extends EventEmitter2 {
|
|
|
17152
17236
|
if (!this.rsaPrivateKey)
|
|
17153
17237
|
return void 0;
|
|
17154
17238
|
try {
|
|
17155
|
-
return
|
|
17239
|
+
return privateDecrypt2({ key: this.rsaPrivateKey, padding: constants2.RSA_PKCS1_PADDING }, wrapped);
|
|
17156
17240
|
} catch {
|
|
17157
17241
|
return void 0;
|
|
17158
17242
|
}
|
|
@@ -17621,7 +17705,7 @@ var P2PSession = class _P2PSession extends EventEmitter2 {
|
|
|
17621
17705
|
} catch {
|
|
17622
17706
|
}
|
|
17623
17707
|
}
|
|
17624
|
-
if (header.commandId === CMD_GATEWAYINFO && header.signCode <= 1 && !this.
|
|
17708
|
+
if (header.commandId === CMD_GATEWAYINFO && header.signCode <= 1 && !this.level2KeyServes("set-payload") && !this.level2Negotiating && this.cfg.resolveCipherKey) {
|
|
17625
17709
|
this.negotiateLevel2Key(data);
|
|
17626
17710
|
}
|
|
17627
17711
|
if (header.commandId === CMD_DATABASE && header.signCode > 0) {
|
|
@@ -17684,6 +17768,7 @@ var P2PSession = class _P2PSession extends EventEmitter2 {
|
|
|
17684
17768
|
this.stopPunchProbes();
|
|
17685
17769
|
this.connectionGeneration += 1;
|
|
17686
17770
|
this.level2Key = void 0;
|
|
17771
|
+
this.level2EcbKey = void 0;
|
|
17687
17772
|
this.level2Seq = 0;
|
|
17688
17773
|
this.settleLevel2("closed");
|
|
17689
17774
|
this.connected = false;
|
|
@@ -21565,7 +21650,7 @@ var P2PCommandRouter = class _P2PCommandRouter {
|
|
|
21565
21650
|
* shared — see {@link P2PCommandRouter.openTalkback}.
|
|
21566
21651
|
*/
|
|
21567
21652
|
talkbacks = /* @__PURE__ */ new Map();
|
|
21568
|
-
/** cipher_id → ECC private
|
|
21653
|
+
/** cipher_id → its ECC and RSA private keys (one eufylife get_ciphers call per cipher), shared across (re)opens. */
|
|
21569
21654
|
cipherKeyCache = /* @__PURE__ */ new Map();
|
|
21570
21655
|
/** The recording download in flight per station; a station serves one at a time. */
|
|
21571
21656
|
recordingDownloads = /* @__PURE__ */ new Map();
|
|
@@ -21757,7 +21842,7 @@ var P2PCommandRouter = class _P2PCommandRouter {
|
|
|
21757
21842
|
* Build + wire a {@link P2PSession} for a station (NOT yet connected — the caller awaits `connect()`).
|
|
21758
21843
|
*
|
|
21759
21844
|
* `resolveCipherKey` auto-negotiates the level-2 session key from `CMD_GATEWAYINFO` by resolving the
|
|
21760
|
-
* cipher's
|
|
21845
|
+
* cipher's private keys via cloud `get_ciphers`, so signCode 2/8 frames decrypt live; results are
|
|
21761
21846
|
* cached on the router instance so a lazy re-open (after idle-detach) reuses the lookup. Only a
|
|
21762
21847
|
* SUCCESSFUL lookup is cached — caching `undefined` after a transient failure would permanently
|
|
21763
21848
|
* disable level-2 for the session's life.
|
|
@@ -21781,7 +21866,7 @@ var P2PCommandRouter = class _P2PCommandRouter {
|
|
|
21781
21866
|
localAddress,
|
|
21782
21867
|
dskKey,
|
|
21783
21868
|
noBroadcast: this.deps.noBroadcast,
|
|
21784
|
-
resolveCipherKey: (cipherId) => this.
|
|
21869
|
+
resolveCipherKey: (cipherId) => this.cipherKeysFor(session, cipherId, adminUserId, stationSn),
|
|
21785
21870
|
logger: this.deps.logger ?? noopLogger
|
|
21786
21871
|
});
|
|
21787
21872
|
session.on("error", (e) => this.reportError(e));
|
|
@@ -21806,19 +21891,20 @@ var P2PCommandRouter = class _P2PCommandRouter {
|
|
|
21806
21891
|
return session;
|
|
21807
21892
|
}
|
|
21808
21893
|
/**
|
|
21809
|
-
* The ECC private
|
|
21810
|
-
*
|
|
21811
|
-
*
|
|
21894
|
+
* The ECC and RSA private keys of `cipherId` (`ecc_private_key`, `private_key`), from the router's cache
|
|
21895
|
+
* or one `get_ciphers` call. When the cloud answers with another cipher that carries an ECC key and the
|
|
21896
|
+
* asked one does not, that one is used and traced on `session`. Only a successful lookup — one that
|
|
21897
|
+
* yields either key — is cached.
|
|
21812
21898
|
*/
|
|
21813
|
-
async
|
|
21899
|
+
async cipherKeysFor(session, cipherId, userId, stationSn) {
|
|
21814
21900
|
if (this.cipherKeyCache.has(cipherId))
|
|
21815
21901
|
return this.cipherKeyCache.get(cipherId);
|
|
21816
|
-
let
|
|
21902
|
+
let cipher;
|
|
21817
21903
|
try {
|
|
21818
21904
|
const ciphers = await this.deps.mega.getCiphers([cipherId], userId, stationSn);
|
|
21819
|
-
|
|
21820
|
-
if (
|
|
21821
|
-
|
|
21905
|
+
cipher = ciphers.find((c) => Number(c.cipher_id) === cipherId);
|
|
21906
|
+
if (cipher?.ecc_private_key === void 0 && ciphers[0]?.ecc_private_key !== void 0) {
|
|
21907
|
+
cipher = ciphers[0];
|
|
21822
21908
|
this.traceOnStation(session, {
|
|
21823
21909
|
phase: "cipher-fallback",
|
|
21824
21910
|
cipherId,
|
|
@@ -21828,9 +21914,11 @@ var P2PCommandRouter = class _P2PCommandRouter {
|
|
|
21828
21914
|
} catch (e) {
|
|
21829
21915
|
this.deps.onError(e instanceof Error ? e : new Error(String(e)));
|
|
21830
21916
|
}
|
|
21831
|
-
if (
|
|
21832
|
-
|
|
21833
|
-
|
|
21917
|
+
if (cipher?.ecc_private_key === void 0 && cipher?.private_key === void 0)
|
|
21918
|
+
return void 0;
|
|
21919
|
+
const keys = { ecc: cipher.ecc_private_key, rsa: cipher.private_key };
|
|
21920
|
+
this.cipherKeyCache.set(cipherId, keys);
|
|
21921
|
+
return keys;
|
|
21834
21922
|
}
|
|
21835
21923
|
/**
|
|
21836
21924
|
* Open (or reuse) a station's P2P session and await its completed handshake. An optional abort only
|
|
@@ -21906,7 +21994,7 @@ var P2PCommandRouter = class _P2PCommandRouter {
|
|
|
21906
21994
|
await this.sendJsonRaw(sn, cmd.cmd, cmd.data, cmd.channel);
|
|
21907
21995
|
return;
|
|
21908
21996
|
case "set-payload":
|
|
21909
|
-
await this.sendSetPayloadEnvelope(sn, cmd.cmd, cmd.payload, cmd.channel, cmd.mValue3, void 0, cmd.form);
|
|
21997
|
+
await this.sendSetPayloadEnvelope(sn, cmd.cmd, cmd.payload, cmd.channel, cmd.mValue3, void 0, cmd.form, cmd.level2Sender);
|
|
21910
21998
|
return;
|
|
21911
21999
|
case "p2p-station-scalar":
|
|
21912
22000
|
await this.sendStationScalar(sn, cmd.cmd, cmd.value, cmd.channel);
|
|
@@ -22050,7 +22138,7 @@ var P2PCommandRouter = class _P2PCommandRouter {
|
|
|
22050
22138
|
const path = homeBase2RecordingPath(channel, opts.recording);
|
|
22051
22139
|
if (!path)
|
|
22052
22140
|
throw new RecordingDownloadError("invalid-recording", `no recording named ${opts.recording}`);
|
|
22053
|
-
const eccKey = await this.
|
|
22141
|
+
const eccKey = (await this.cipherKeysFor(session, opts.cipherId, accountId, parentSn))?.ecc;
|
|
22054
22142
|
if (!eccKey) {
|
|
22055
22143
|
throw new RecordingDownloadError("key-unavailable", `cipher ${opts.cipherId} could not be obtained`);
|
|
22056
22144
|
}
|
|
@@ -22791,9 +22879,10 @@ var P2PCommandRouter = class _P2PCommandRouter {
|
|
|
22791
22879
|
const required = opts.waitLevel2 !== "soft" || opts.requireLevel2ForAttached === true && homeBaseAttached;
|
|
22792
22880
|
if (!required)
|
|
22793
22881
|
return { session, parentSn, channel, accountId, homeBaseAttached };
|
|
22794
|
-
|
|
22882
|
+
const sender = opts.level2Sender ?? "every";
|
|
22883
|
+
let ready = await abortable(session.awaitLevel2Key(LEVEL2_GRACE_MS, "call", sender), opts.signal);
|
|
22795
22884
|
if (!ready && session.repromptLevel2Key()) {
|
|
22796
|
-
ready = await abortable(session.awaitLevel2Key(LEVEL2_GRACE_MS, "call"), opts.signal);
|
|
22885
|
+
ready = await abortable(session.awaitLevel2Key(LEVEL2_GRACE_MS, "call", sender), opts.signal);
|
|
22797
22886
|
}
|
|
22798
22887
|
if (!ready)
|
|
22799
22888
|
throw new StationKeyUnavailableError(parentSn);
|
|
@@ -22868,7 +22957,9 @@ var P2PCommandRouter = class _P2PCommandRouter {
|
|
|
22868
22957
|
* HomeBase-attached device takes the GCM signCode-8 form, a standalone one the level-1 form. A
|
|
22869
22958
|
* standalone camera never negotiates a level-2 key, so pinning this to level 2 makes the envelope
|
|
22870
22959
|
* unreachable on exactly the devices that serve their own RTSP stream. Verified live: a standalone
|
|
22871
|
-
* camera accepts the level-1 form. With no `form` (default) it stays level-2 only
|
|
22960
|
+
* camera accepts the level-1 form. With no `form` (default) it stays level-2 only and waits for the
|
|
22961
|
+
* key every level-2 sender uses; `level2Sender: "set-payload"` also takes an RSA cipher's AES-128-ECB
|
|
22962
|
+
* key (see {@link P2PSession.sendControlLevel2}). A `resolved` session keeps the wait its caller gave it.
|
|
22872
22963
|
*
|
|
22873
22964
|
* Both seals REPLAY the frame {@link DIRECT_CMD_SENDS}× at 200ms, as every other fire-and-forget
|
|
22874
22965
|
* control on this router does: these are unacknowledged datagrams, and a level-1 device is the one
|
|
@@ -22877,7 +22968,7 @@ var P2PCommandRouter = class _P2PCommandRouter {
|
|
|
22877
22968
|
* throwing (`sendSetPayload` throws when the session has no address) rather than by returning a
|
|
22878
22969
|
* boolean, so the first pass carries the failure and the rest are repeats.
|
|
22879
22970
|
*/
|
|
22880
|
-
async sendSetPayloadEnvelope(sn, cmd, payload, channel, mValue3, resolved, form) {
|
|
22971
|
+
async sendSetPayloadEnvelope(sn, cmd, payload, channel, mValue3, resolved, form, level2Sender) {
|
|
22881
22972
|
if (form === "auto") {
|
|
22882
22973
|
await this.sendBySessionLevel(sn, {
|
|
22883
22974
|
l1: async ({ session, accountId }) => {
|
|
@@ -22894,7 +22985,7 @@ var P2PCommandRouter = class _P2PCommandRouter {
|
|
|
22894
22985
|
});
|
|
22895
22986
|
return;
|
|
22896
22987
|
}
|
|
22897
|
-
await this.replayLevel2Send(sn, `set-payload cmd ${cmd}`, ({ session, accountId }) => session.sendControlLevel2(cmd, channel, accountId, payload, mValue3 ?? cmd), resolved);
|
|
22988
|
+
await this.replayLevel2Send(sn, `set-payload cmd ${cmd}`, ({ session, accountId }) => session.sendControlLevel2(cmd, channel, accountId, payload, mValue3 ?? cmd), resolved ?? await this.resolveSession(sn, { waitLevel2: true, level2Sender }));
|
|
22898
22989
|
}
|
|
22899
22990
|
/**
|
|
22900
22991
|
* **`ff09-actuate` intent** — build the `ff09` AES-128-CBC frame ({@link buildFf09Frame}, shared with the
|
|
@@ -26016,7 +26107,7 @@ var HmacSigner = class {
|
|
|
26016
26107
|
var TUYA_CHKEY = "7cbfe6d8";
|
|
26017
26108
|
|
|
26018
26109
|
// dist/transport/tuya/client.js
|
|
26019
|
-
import { randomBytes as randomBytes5, createHash as createHash6, createPublicKey, publicEncrypt, constants as
|
|
26110
|
+
import { randomBytes as randomBytes5, createHash as createHash6, createPublicKey, publicEncrypt, constants as constants3 } from "node:crypto";
|
|
26020
26111
|
|
|
26021
26112
|
// dist/transport/tuya/et3.js
|
|
26022
26113
|
import { createCipheriv as createCipheriv6, createDecipheriv as createDecipheriv8, createHmac as createHmac5, randomBytes as randomBytes4 } from "node:crypto";
|
|
@@ -26183,7 +26274,7 @@ function rsaEncryptPassword(password, publicKeyDecimal, exponentDecimal) {
|
|
|
26183
26274
|
format: "jwk"
|
|
26184
26275
|
});
|
|
26185
26276
|
const md5Hex2 = createHash6("md5").update(password).digest("hex");
|
|
26186
|
-
return publicEncrypt({ key: rsaKey, padding:
|
|
26277
|
+
return publicEncrypt({ key: rsaKey, padding: constants3.RSA_PKCS1_PADDING }, Buffer.from(md5Hex2)).toString("hex");
|
|
26187
26278
|
}
|
|
26188
26279
|
var TuyaClient = class {
|
|
26189
26280
|
signer;
|
|
@@ -31558,6 +31649,7 @@ export {
|
|
|
31558
31649
|
SECURITY_PARAMS,
|
|
31559
31650
|
SERVER_STATIC_PUBLIC_KEY_HEX,
|
|
31560
31651
|
SIREN_MEMBERS,
|
|
31652
|
+
SMART_DROP_MEMBERS,
|
|
31561
31653
|
SMART_LIGHT_MEMBERS,
|
|
31562
31654
|
SMOKE_MEMBERS,
|
|
31563
31655
|
SOLIX_ENERGY_METER_MEMBERS,
|