homebridge-tuya-plus 3.3.0 → 3.4.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/Readme.MD +1 -0
- package/config.schema.json +12 -0
- package/index.js +3 -1
- package/lib/TuyaAccessory.js +251 -38
- package/lib/TuyaDiscovery.js +268 -165
- package/lib/VerticalBlindsWithTilt.js +431 -0
- package/package.json +1 -1
- package/wiki/Supported-Device-Types.md +30 -0
package/Readme.MD
CHANGED
|
@@ -44,6 +44,7 @@ A community-maintained Homebridge plugin for controlling Tuya devices locally ov
|
|
|
44
44
|
* Oil Diffusers<sup>[13](https://github.com/adrianjagielak/homebridge-tuya-plus/blob/main/wiki/Supported-Device-Types.md)</sup>
|
|
45
45
|
* Outlets<sup>[14](https://github.com/adrianjagielak/homebridge-tuya-plus/blob/main/wiki/Supported-Device-Types.md#outlets)</sup>
|
|
46
46
|
* Switches<sup>[15](https://github.com/adrianjagielak/homebridge-tuya-plus/blob/main/wiki/Supported-Device-Types.md)</sup>
|
|
47
|
+
* Vertical Blinds<sup>[16](https://github.com/adrianjagielak/homebridge-tuya-plus/blob/main/wiki/Supported-Device-Types.md#vertical-blinds-with-tilt)</sup>
|
|
47
48
|
|
|
48
49
|
Note: Motion, and other sensor types don't behave well with responce requests, so they will not be added.
|
|
49
50
|
|
package/config.schema.json
CHANGED
|
@@ -78,6 +78,10 @@
|
|
|
78
78
|
"title": "Simple Blinds",
|
|
79
79
|
"enum": ["SimpleBlinds"]
|
|
80
80
|
},
|
|
81
|
+
{
|
|
82
|
+
"title": "Vertical Blinds with Tilt",
|
|
83
|
+
"enum": ["VerticalBlindsWithTilt"]
|
|
84
|
+
},
|
|
81
85
|
{
|
|
82
86
|
"title": "Smart Plug w/ White and Color Lights",
|
|
83
87
|
"enum": ["RGBTWOutlet"]
|
|
@@ -498,6 +502,14 @@
|
|
|
498
502
|
"functionBody": "return model.devices && model.devices[arrayIndices] && ['SimpleBlinds'].includes(model.devices[arrayIndices].type);"
|
|
499
503
|
}
|
|
500
504
|
},
|
|
505
|
+
"timeToClose": {
|
|
506
|
+
"type": "integer",
|
|
507
|
+
"placeholder": "30",
|
|
508
|
+
"description": "Time in seconds for blinds to fully open or close",
|
|
509
|
+
"condition": {
|
|
510
|
+
"functionBody": "return model.devices && model.devices[arrayIndices] && ['VerticalBlindsWithTilt'].includes(model.devices[arrayIndices].type);"
|
|
511
|
+
}
|
|
512
|
+
},
|
|
501
513
|
"dpLight": {
|
|
502
514
|
"type": "integer",
|
|
503
515
|
"placeholder": 1,
|
package/index.js
CHANGED
|
@@ -23,6 +23,7 @@ const SwitchAccessory = require('./lib/SwitchAccessory');
|
|
|
23
23
|
const ValveAccessory = require('./lib/ValveAccessory');
|
|
24
24
|
const OilDiffuserAccessory = require('./lib/OilDiffuserAccessory');
|
|
25
25
|
const DoorbellAccessory = require('./lib/DoorbellAccessory');
|
|
26
|
+
const VerticalBlindsWithTilt = require('./lib/VerticalBlindsWithTilt');
|
|
26
27
|
|
|
27
28
|
const PLUGIN_NAME = 'homebridge-tuya';
|
|
28
29
|
const PLATFORM_NAME = 'TuyaLan';
|
|
@@ -50,7 +51,8 @@ const CLASS_DEF = {
|
|
|
50
51
|
fanlight: SimpleFanLightAccessory,
|
|
51
52
|
watervalve: ValveAccessory,
|
|
52
53
|
oildiffuser: OilDiffuserAccessory,
|
|
53
|
-
doorbell: DoorbellAccessory
|
|
54
|
+
doorbell: DoorbellAccessory,
|
|
55
|
+
verticalblindswithtilt: VerticalBlindsWithTilt
|
|
54
56
|
};
|
|
55
57
|
|
|
56
58
|
let Characteristic, PlatformAccessory, Service, Categories, AdaptiveLightingController, UUID;
|
package/lib/TuyaAccessory.js
CHANGED
|
@@ -17,12 +17,20 @@ class TuyaAccessory extends EventEmitter {
|
|
|
17
17
|
|
|
18
18
|
this.log = props.log;
|
|
19
19
|
|
|
20
|
-
this.context = {
|
|
20
|
+
this.context = {
|
|
21
|
+
version: '3.5', // Every pre-3.5 device includes its version in its broadcast
|
|
22
|
+
port: 6668,
|
|
23
|
+
...props,
|
|
24
|
+
};
|
|
21
25
|
|
|
22
26
|
this.state = {};
|
|
23
27
|
this._cachedBuffer = Buffer.allocUnsafe(0);
|
|
24
28
|
|
|
25
|
-
this._msgQueue = async.queue(this[
|
|
29
|
+
this._msgQueue = async.queue(this[
|
|
30
|
+
this.context.version < 3.2 ? '_msgHandler_3_1' :
|
|
31
|
+
this.context.version === '3.3' ? '_msgHandler_3_3' :
|
|
32
|
+
this.context.version === '3.4' ? '_msgHandler_3_4' : '_msgHandler_3_5'
|
|
33
|
+
].bind(this), 1);
|
|
26
34
|
|
|
27
35
|
if (this.context.version >= 3.2) {
|
|
28
36
|
this.context.pingGap = Math.min(this.context.pingGap || 9, 9);
|
|
@@ -102,7 +110,7 @@ class TuyaAccessory extends EventEmitter {
|
|
|
102
110
|
};
|
|
103
111
|
|
|
104
112
|
this._socket.on('connect', () => {
|
|
105
|
-
if (this.context.version !== '3.4') {
|
|
113
|
+
if (this.context.version !== '3.4' && this.context.version !== '3.5') {
|
|
106
114
|
clearTimeout(this._socket._connTimeout);
|
|
107
115
|
|
|
108
116
|
this.connected = true;
|
|
@@ -122,13 +130,13 @@ class TuyaAccessory extends EventEmitter {
|
|
|
122
130
|
if (this.context.intro === false) return;
|
|
123
131
|
this.connected = true;
|
|
124
132
|
|
|
125
|
-
if (this.context.version === '3.4') {
|
|
133
|
+
if (this.context.version === '3.4' || this.context.version === '3.5') {
|
|
126
134
|
this._tmpLocalKey = crypto.randomBytes(16);
|
|
127
135
|
const payload = {
|
|
128
136
|
data: this._tmpLocalKey,
|
|
129
137
|
encrypted: true,
|
|
130
138
|
cmd: 3 //CommandType.BIND
|
|
131
|
-
}
|
|
139
|
+
};
|
|
132
140
|
this._send(payload);
|
|
133
141
|
} else {
|
|
134
142
|
this.update();
|
|
@@ -139,18 +147,28 @@ class TuyaAccessory extends EventEmitter {
|
|
|
139
147
|
this._cachedBuffer = Buffer.concat([this._cachedBuffer, msg]);
|
|
140
148
|
|
|
141
149
|
do {
|
|
142
|
-
let startingIndex
|
|
150
|
+
let startingIndex;
|
|
151
|
+
if (this.context.version === '3.5') {
|
|
152
|
+
startingIndex = this._cachedBuffer.indexOf('00006699', 'hex');
|
|
153
|
+
} else {
|
|
154
|
+
startingIndex = this._cachedBuffer.indexOf('000055aa', 'hex');
|
|
155
|
+
}
|
|
143
156
|
if (startingIndex === -1) {
|
|
144
157
|
this._cachedBuffer = Buffer.allocUnsafe(0);
|
|
145
158
|
break;
|
|
146
159
|
}
|
|
147
160
|
if (startingIndex !== 0) this._cachedBuffer = this._cachedBuffer.slice(startingIndex);
|
|
148
161
|
|
|
149
|
-
let endingIndex
|
|
162
|
+
let endingIndex;
|
|
163
|
+
if (this.context.version === '3.5') {
|
|
164
|
+
endingIndex = this._cachedBuffer.indexOf('00009966', 'hex');
|
|
165
|
+
if (endingIndex !== -1) endingIndex += 4;
|
|
166
|
+
} else {
|
|
167
|
+
endingIndex = this._cachedBuffer.indexOf('0000aa55', 'hex');
|
|
168
|
+
if (endingIndex !== -1) endingIndex += 4;
|
|
169
|
+
}
|
|
150
170
|
if (endingIndex === -1) break;
|
|
151
171
|
|
|
152
|
-
endingIndex += 4;
|
|
153
|
-
|
|
154
172
|
this._msgQueue.push({msg: this._cachedBuffer.slice(0, endingIndex)});
|
|
155
173
|
|
|
156
174
|
this._cachedBuffer = this._cachedBuffer.slice(endingIndex);
|
|
@@ -489,6 +507,125 @@ class TuyaAccessory extends EventEmitter {
|
|
|
489
507
|
callback();
|
|
490
508
|
}
|
|
491
509
|
|
|
510
|
+
/*
|
|
511
|
+
* 3.5 Protocol message handler
|
|
512
|
+
*/
|
|
513
|
+
_msgHandler_3_5(task, callback) {
|
|
514
|
+
if (!(task.msg instanceof Buffer)) return callback();
|
|
515
|
+
|
|
516
|
+
const len = task.msg.length;
|
|
517
|
+
if (len < 22 ||
|
|
518
|
+
task.msg.readUInt32BE(0) !== 0x00006699 ||
|
|
519
|
+
task.msg.readUInt32BE(len - 4) !== 0x00009966) return callback();
|
|
520
|
+
|
|
521
|
+
const declaredLen = task.msg.readUInt32BE(14);
|
|
522
|
+
if ((len - 22) < declaredLen) return callback();
|
|
523
|
+
|
|
524
|
+
const cmd = task.msg.readUInt32BE(10);
|
|
525
|
+
|
|
526
|
+
if (cmd === 7 || cmd === 13) return callback(); // ignoring
|
|
527
|
+
if (cmd === 9) {
|
|
528
|
+
if (this._socket._pinger) clearTimeout(this._socket._pinger);
|
|
529
|
+
this._socket._pinger = setTimeout(() => {
|
|
530
|
+
this._socket._ping();
|
|
531
|
+
}, (this.context.pingGap || 20) * 1000);
|
|
532
|
+
return callback();
|
|
533
|
+
}
|
|
534
|
+
|
|
535
|
+
const iv = task.msg.slice(18, 30);
|
|
536
|
+
const encrypted = task.msg.slice(30, len - 20); // remove tag + footer
|
|
537
|
+
const tag = task.msg.slice(len - 20, len - 4);
|
|
538
|
+
const aad = task.msg.slice(4, 18); // UUUU + seq + cmd + len (14 bytes)
|
|
539
|
+
|
|
540
|
+
let decrypted;
|
|
541
|
+
try {
|
|
542
|
+
const decipher = crypto.createDecipheriv('aes-128-gcm', this.session_key ?? this.context.key, iv);
|
|
543
|
+
decipher.setAAD(aad);
|
|
544
|
+
decipher.setAuthTag(tag);
|
|
545
|
+
decrypted = Buffer.concat([decipher.update(encrypted), decipher.final()]);
|
|
546
|
+
} catch(ex) {
|
|
547
|
+
this.log.info(`Failed to decrypt message from ${this.context.name} (${this.context.version}) with command ${cmd}:`, ex);
|
|
548
|
+
return callback();
|
|
549
|
+
}
|
|
550
|
+
|
|
551
|
+
// strip return code if present (first 4 bytes)
|
|
552
|
+
let payload = decrypted;
|
|
553
|
+
if (decrypted.length > 4) {
|
|
554
|
+
try {
|
|
555
|
+
JSON.parse(decrypted.toString('utf8'));
|
|
556
|
+
} catch(_) {
|
|
557
|
+
payload = decrypted.slice(4);
|
|
558
|
+
}
|
|
559
|
+
}
|
|
560
|
+
|
|
561
|
+
// remove leading version header
|
|
562
|
+
if (payload.indexOf(this.context.version) === 0) payload = payload.slice(15);
|
|
563
|
+
|
|
564
|
+
// Attempt JSON parse
|
|
565
|
+
let parsedPayload;
|
|
566
|
+
try {
|
|
567
|
+
parsedPayload = JSON.parse(payload.toString('utf8'));
|
|
568
|
+
if (parsedPayload && parsedPayload.data) {
|
|
569
|
+
const tmp = parsedPayload;
|
|
570
|
+
parsedPayload = tmp.data;
|
|
571
|
+
parsedPayload.t = tmp.t;
|
|
572
|
+
}
|
|
573
|
+
} catch(_) {
|
|
574
|
+
parsedPayload = payload; // may be buffer during key exchange
|
|
575
|
+
}
|
|
576
|
+
|
|
577
|
+
// ---- Handle 3‑way key exchange ----
|
|
578
|
+
if (cmd === 4 && Buffer.isBuffer(parsedPayload)) {
|
|
579
|
+
this._tmpRemoteKey = parsedPayload.subarray(0, 16);
|
|
580
|
+
const calcLocalHmac = hmac(this._tmpLocalKey, this.context.key).toString('hex');
|
|
581
|
+
const expLocalHmac = parsedPayload.slice(16, 48).toString('hex');
|
|
582
|
+
if (calcLocalHmac !== expLocalHmac) {
|
|
583
|
+
throw new Error(`HMAC mismatch(keys): expected ${expLocalHmac}, was ${calcLocalHmac}. ${parsedPayload.toString('hex')}`);
|
|
584
|
+
}
|
|
585
|
+
|
|
586
|
+
const payloadToSend = {
|
|
587
|
+
data: hmac(this._tmpRemoteKey, this.context.key),
|
|
588
|
+
encrypted: true,
|
|
589
|
+
cmd: 5 // CommandType.RENAME_DEVICE equivalent
|
|
590
|
+
};
|
|
591
|
+
this._send(payloadToSend);
|
|
592
|
+
clearTimeout(this._socket._connTimeout);
|
|
593
|
+
|
|
594
|
+
// derive session key
|
|
595
|
+
let tmpKey = xorBuffers(this._tmpLocalKey, this._tmpRemoteKey);
|
|
596
|
+
this.session_key = encrypt35(tmpKey, this.context.key, this._tmpLocalKey.slice(0, 12));
|
|
597
|
+
|
|
598
|
+
this.connected = true;
|
|
599
|
+
this.update();
|
|
600
|
+
this.emit('connect');
|
|
601
|
+
if (this._socket._pinger) clearTimeout(this._socket._pinger);
|
|
602
|
+
this._socket._pinger = setTimeout(() => this._socket._ping(), 1000);
|
|
603
|
+
return callback();
|
|
604
|
+
}
|
|
605
|
+
|
|
606
|
+
if (cmd === 10 && parsedPayload === 'json obj data unvalid') {
|
|
607
|
+
this.log.info(`${this.context.name} (${this.context.version}) didn't respond with its current state.`);
|
|
608
|
+
this.emit('change', {}, this.state);
|
|
609
|
+
return callback();
|
|
610
|
+
}
|
|
611
|
+
|
|
612
|
+
switch (cmd) {
|
|
613
|
+
case 8:
|
|
614
|
+
case 10:
|
|
615
|
+
case 16:
|
|
616
|
+
if (parsedPayload && parsedPayload.dps) {
|
|
617
|
+
this._change(parsedPayload.dps);
|
|
618
|
+
} else {
|
|
619
|
+
this.log.info(`Malformed message from ${this.context.name} with command ${cmd}:`, payload.toString('utf8'));
|
|
620
|
+
}
|
|
621
|
+
break;
|
|
622
|
+
default:
|
|
623
|
+
this.log.info(`Odd message from ${this.context.name} with command ${cmd}:`, payload.toString('utf8'));
|
|
624
|
+
}
|
|
625
|
+
|
|
626
|
+
callback();
|
|
627
|
+
}
|
|
628
|
+
|
|
492
629
|
update(o) {
|
|
493
630
|
const dps = {};
|
|
494
631
|
let hasDataPoint = false;
|
|
@@ -514,25 +651,25 @@ class TuyaAccessory extends EventEmitter {
|
|
|
514
651
|
t,
|
|
515
652
|
dps
|
|
516
653
|
};
|
|
517
|
-
const data = this.context.version === '3.4'
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
654
|
+
const data = (this.context.version === '3.4' || this.context.version === '3.5')
|
|
655
|
+
? {
|
|
656
|
+
data: {
|
|
657
|
+
...payload,
|
|
658
|
+
ctype: 0,
|
|
659
|
+
t: undefined
|
|
660
|
+
},
|
|
661
|
+
protocol: 5,
|
|
662
|
+
t
|
|
663
|
+
}
|
|
664
|
+
: payload;
|
|
528
665
|
result = this._send({
|
|
529
666
|
data,
|
|
530
|
-
cmd: this.context.version === '3.4' ? 13 : 7
|
|
667
|
+
cmd: (this.context.version === '3.4' || this.context.version === '3.5') ? 13 : 7
|
|
531
668
|
});
|
|
532
669
|
if (result !== true) this.log.info(" Result", result);
|
|
533
670
|
if (this.context.sendEmptyUpdate) {
|
|
534
671
|
//this.log.info(" Sending", this.context.name, 'empty signature');
|
|
535
|
-
this._send({cmd: this.context.version === '3.4' ? 13 : 7});
|
|
672
|
+
this._send({cmd: (this.context.version === '3.4' || this.context.version === '3.5') ? 13 : 7});
|
|
536
673
|
}
|
|
537
674
|
} else {
|
|
538
675
|
//this.log.info(`Sending first query to ${this.context.name} (${this.context.version})`);
|
|
@@ -541,7 +678,7 @@ class TuyaAccessory extends EventEmitter {
|
|
|
541
678
|
gwId: this.context.id,
|
|
542
679
|
devId: this.context.id
|
|
543
680
|
},
|
|
544
|
-
cmd: this.context.version === '3.4' ? 16 : 10
|
|
681
|
+
cmd: (this.context.version === '3.4' || this.context.version === '3.5') ? 16 : 10
|
|
545
682
|
});
|
|
546
683
|
}
|
|
547
684
|
|
|
@@ -570,7 +707,8 @@ class TuyaAccessory extends EventEmitter {
|
|
|
570
707
|
|
|
571
708
|
if (this.context.version < 3.2) return this._send_3_1(o);
|
|
572
709
|
if (this.context.version === '3.3') return this._send_3_3(o);
|
|
573
|
-
return this._send_3_4(o);
|
|
710
|
+
if (this.context.version === '3.4') return this._send_3_4(o);
|
|
711
|
+
return this._send_3_5(o);
|
|
574
712
|
}
|
|
575
713
|
|
|
576
714
|
_send_3_1(o) {
|
|
@@ -645,16 +783,6 @@ class TuyaAccessory extends EventEmitter {
|
|
|
645
783
|
return this._socket.write(payload);
|
|
646
784
|
}
|
|
647
785
|
|
|
648
|
-
_fakeUpdate(dps) {
|
|
649
|
-
this.log.info('Fake update:', JSON.stringify(dps));
|
|
650
|
-
Object.keys(dps).forEach(dp => {
|
|
651
|
-
this.state[dp] = dps[dp];
|
|
652
|
-
});
|
|
653
|
-
setTimeout(() => {
|
|
654
|
-
this.emit('change', dps, this.state);
|
|
655
|
-
}, 1000);
|
|
656
|
-
}
|
|
657
|
-
|
|
658
786
|
_send_3_4(o) {
|
|
659
787
|
let {cmd, data} = {...o};
|
|
660
788
|
|
|
@@ -713,19 +841,105 @@ class TuyaAccessory extends EventEmitter {
|
|
|
713
841
|
|
|
714
842
|
return this._socket.write(buffer);
|
|
715
843
|
}
|
|
844
|
+
|
|
845
|
+
/*
|
|
846
|
+
* 3.5 Protocol send
|
|
847
|
+
*/
|
|
848
|
+
_send_3_5(o) {
|
|
849
|
+
let {cmd, data} = {...o};
|
|
850
|
+
// data processing similar to 3.4
|
|
851
|
+
if (!data) data = Buffer.allocUnsafe(0);
|
|
852
|
+
if (!(data instanceof Buffer)) {
|
|
853
|
+
if (typeof data !== 'string') data = JSON.stringify(data);
|
|
854
|
+
data = Buffer.from(data);
|
|
855
|
+
}
|
|
856
|
+
|
|
857
|
+
if (cmd !== 10 && cmd !== 9 && cmd !== 16 && cmd !== 3 && cmd !== 5 && cmd !== 18) {
|
|
858
|
+
const buffer = Buffer.alloc(data.length + 15);
|
|
859
|
+
Buffer.from('3.5').copy(buffer, 0);
|
|
860
|
+
data.copy(buffer, 15);
|
|
861
|
+
data = buffer;
|
|
862
|
+
}
|
|
863
|
+
|
|
864
|
+
const iv = crypto.randomBytes(12);
|
|
865
|
+
|
|
866
|
+
// Increment sequence counter unless empty dp-update
|
|
867
|
+
if ((cmd !== 7 && cmd !== 13) || data.length) this._sendCounter++;
|
|
868
|
+
|
|
869
|
+
const unknownBuf = Buffer.alloc(2); // always 0x0000
|
|
870
|
+
const seqBuf = Buffer.alloc(4);
|
|
871
|
+
seqBuf.writeUInt32BE(this._sendCounter, 0);
|
|
872
|
+
const cmdBuf = Buffer.alloc(4);
|
|
873
|
+
cmdBuf.writeUInt32BE(cmd, 0);
|
|
874
|
+
const lenField = 12 + data.length + 16; // iv + payload + tag
|
|
875
|
+
const lenBuf = Buffer.alloc(4);
|
|
876
|
+
lenBuf.writeUInt32BE(lenField, 0);
|
|
877
|
+
|
|
878
|
+
const aad = Buffer.concat([unknownBuf, seqBuf, cmdBuf, lenBuf]);
|
|
879
|
+
|
|
880
|
+
const key = this.session_key ?? this.context.key;
|
|
881
|
+
const cipher = crypto.createCipheriv('aes-128-gcm', key, iv);
|
|
882
|
+
cipher.setAAD(aad);
|
|
883
|
+
const encrypted = Buffer.concat([cipher.update(data), cipher.final()]);
|
|
884
|
+
const tag = cipher.getAuthTag();
|
|
885
|
+
|
|
886
|
+
const prefixBuf = Buffer.from('00006699', 'hex');
|
|
887
|
+
const footerBuf = Buffer.from('00009966', 'hex');
|
|
888
|
+
|
|
889
|
+
const packet = Buffer.concat([
|
|
890
|
+
prefixBuf,
|
|
891
|
+
unknownBuf,
|
|
892
|
+
seqBuf,
|
|
893
|
+
cmdBuf,
|
|
894
|
+
lenBuf,
|
|
895
|
+
iv,
|
|
896
|
+
encrypted,
|
|
897
|
+
tag,
|
|
898
|
+
footerBuf
|
|
899
|
+
]);
|
|
900
|
+
|
|
901
|
+
return this._socket.write(packet);
|
|
902
|
+
}
|
|
903
|
+
|
|
904
|
+
_fakeUpdate(dps) {
|
|
905
|
+
this.log.info('Fake update:', JSON.stringify(dps));
|
|
906
|
+
Object.keys(dps).forEach(dp => {
|
|
907
|
+
this.state[dp] = dps[dp];
|
|
908
|
+
});
|
|
909
|
+
setTimeout(() => {
|
|
910
|
+
this.emit('change', dps, this.state);
|
|
911
|
+
}, 1000);
|
|
912
|
+
}
|
|
716
913
|
}
|
|
717
914
|
|
|
915
|
+
/* ------------------ Helper utilities ------------------- */
|
|
718
916
|
const encrypt34 = (data, encryptKey) => {
|
|
719
917
|
const cipher = crypto.createCipheriv('aes-128-ecb', encryptKey, null);
|
|
720
918
|
cipher.setAutoPadding(false);
|
|
721
919
|
let encrypted = cipher.update(data);
|
|
722
920
|
cipher.final();
|
|
723
921
|
return encrypted;
|
|
724
|
-
}
|
|
922
|
+
};
|
|
923
|
+
|
|
924
|
+
// 3.5 encryption helper (AES‑128‑GCM). Returns ciphertext buffer.
|
|
925
|
+
const encrypt35 = (data, encryptKey, iv) => {
|
|
926
|
+
const cipher = crypto.createCipheriv('aes-128-gcm', encryptKey, iv);
|
|
927
|
+
cipher.setAAD(Buffer.alloc(0));
|
|
928
|
+
const encrypted = Buffer.concat([cipher.update(data), cipher.final()]);
|
|
929
|
+
/* auth tag is cipher.getAuthTag() but not required here */
|
|
930
|
+
return encrypted;
|
|
931
|
+
};
|
|
725
932
|
|
|
726
933
|
const hmac = (data, hmacKey) => {
|
|
727
|
-
return crypto.createHmac('sha256',hmacKey).update(data, 'utf8').digest();
|
|
728
|
-
}
|
|
934
|
+
return crypto.createHmac('sha256', hmacKey).update(data, 'utf8').digest();
|
|
935
|
+
};
|
|
936
|
+
|
|
937
|
+
const xorBuffers = (a, b) => {
|
|
938
|
+
const len = Math.min(a.length, b.length);
|
|
939
|
+
const out = Buffer.alloc(len);
|
|
940
|
+
for (let i = 0; i < len; i++) out[i] = a[i] ^ b[i];
|
|
941
|
+
return out;
|
|
942
|
+
};
|
|
729
943
|
|
|
730
944
|
const crc32LookupTable = [];
|
|
731
945
|
(() => {
|
|
@@ -742,5 +956,4 @@ const getCRC32 = buffer => {
|
|
|
742
956
|
return ~crc;
|
|
743
957
|
};
|
|
744
958
|
|
|
745
|
-
|
|
746
959
|
module.exports = TuyaAccessory;
|