homebridge-midea-platform 1.1.2-beta.9 → 1.2.0-beta.2
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/.husky/pre-commit +0 -0
- package/CHANGELOG.md +4 -2
- package/config.schema.json +46 -1
- package/dist/accessory/AccessoryFactory.d.ts +12 -12
- package/dist/accessory/AccessoryFactory.js +31 -31
- package/dist/accessory/AirConditionerAccessory.d.ts +92 -89
- package/dist/accessory/AirConditionerAccessory.d.ts.map +1 -1
- package/dist/accessory/AirConditionerAccessory.js +607 -584
- package/dist/accessory/AirConditionerAccessory.js.map +1 -1
- package/dist/accessory/BaseAccessory.d.ts +11 -11
- package/dist/accessory/BaseAccessory.js +21 -21
- package/dist/accessory/DehumidifierAccessory.d.ts +45 -45
- package/dist/accessory/DehumidifierAccessory.js +344 -344
- package/dist/accessory/DishwasherAccessory.d.ts +31 -0
- package/dist/accessory/DishwasherAccessory.d.ts.map +1 -0
- package/dist/accessory/DishwasherAccessory.js +64 -0
- package/dist/accessory/DishwasherAccessory.js.map +1 -0
- package/dist/accessory/ElectricWaterHeaterAccessory.d.ts +44 -44
- package/dist/accessory/ElectricWaterHeaterAccessory.js +176 -176
- package/dist/accessory/FanAccessory.d.ts +39 -39
- package/dist/accessory/FanAccessory.js +123 -123
- package/dist/accessory/FrontLoadWasherAccessory.d.ts +30 -30
- package/dist/accessory/FrontLoadWasherAccessory.d.ts.map +1 -1
- package/dist/accessory/FrontLoadWasherAccessory.js +66 -60
- package/dist/accessory/FrontLoadWasherAccessory.js.map +1 -1
- package/dist/accessory/GasWaterHeaterAccessory.d.ts +51 -51
- package/dist/accessory/GasWaterHeaterAccessory.js +216 -216
- package/dist/core/MideaCloud.d.ts +35 -35
- package/dist/core/MideaCloud.js +350 -350
- package/dist/core/MideaConstants.d.ts +50 -49
- package/dist/core/MideaConstants.d.ts.map +1 -1
- package/dist/core/MideaConstants.js +58 -57
- package/dist/core/MideaConstants.js.map +1 -1
- package/dist/core/MideaDevice.d.ts +76 -76
- package/dist/core/MideaDevice.js +409 -409
- package/dist/core/MideaDiscover.d.ts +35 -35
- package/dist/core/MideaDiscover.js +212 -212
- package/dist/core/MideaMessage.d.ts +75 -75
- package/dist/core/MideaMessage.js +184 -184
- package/dist/core/MideaPacketBuilder.d.ts +10 -10
- package/dist/core/MideaPacketBuilder.js +60 -60
- package/dist/core/MideaSecurity.d.ts +63 -63
- package/dist/core/MideaSecurity.js +251 -251
- package/dist/core/MideaUtils.d.ts +32 -32
- package/dist/core/MideaUtils.js +181 -181
- package/dist/devices/DeviceFactory.d.ts +13 -12
- package/dist/devices/DeviceFactory.d.ts.map +1 -1
- package/dist/devices/DeviceFactory.js +36 -33
- package/dist/devices/DeviceFactory.js.map +1 -1
- package/dist/devices/a1/MideaA1Device.d.ts +76 -76
- package/dist/devices/a1/MideaA1Device.d.ts.map +1 -1
- package/dist/devices/a1/MideaA1Device.js +145 -136
- package/dist/devices/a1/MideaA1Device.js.map +1 -1
- package/dist/devices/a1/MideaA1Message.d.ts +40 -40
- package/dist/devices/a1/MideaA1Message.js +198 -198
- package/dist/devices/ac/MideaACDevice.d.ts +104 -100
- package/dist/devices/ac/MideaACDevice.d.ts.map +1 -1
- package/dist/devices/ac/MideaACDevice.js +384 -381
- package/dist/devices/ac/MideaACDevice.js.map +1 -1
- package/dist/devices/ac/MideaACMessage.d.ts +94 -92
- package/dist/devices/ac/MideaACMessage.d.ts.map +1 -1
- package/dist/devices/ac/MideaACMessage.js +611 -589
- package/dist/devices/ac/MideaACMessage.js.map +1 -1
- package/dist/devices/db/MideaDBDevice.d.ts +29 -29
- package/dist/devices/db/MideaDBDevice.d.ts.map +1 -1
- package/dist/devices/db/MideaDBDevice.js +100 -88
- package/dist/devices/db/MideaDBDevice.js.map +1 -1
- package/dist/devices/db/MideaDBMessage.d.ts +32 -32
- package/dist/devices/db/MideaDBMessage.d.ts.map +1 -1
- package/dist/devices/db/MideaDBMessage.js +101 -101
- package/dist/devices/db/MideaDBMessage.js.map +1 -1
- package/dist/devices/e1/MideaE1Device.d.ts +57 -0
- package/dist/devices/e1/MideaE1Device.d.ts.map +1 -0
- package/dist/devices/e1/MideaE1Device.js +129 -0
- package/dist/devices/e1/MideaE1Device.js.map +1 -0
- package/dist/devices/e1/MideaE1Message.d.ts +29 -0
- package/dist/devices/e1/MideaE1Message.d.ts.map +1 -0
- package/dist/devices/e1/MideaE1Message.js +108 -0
- package/dist/devices/e1/MideaE1Message.js.map +1 -0
- package/dist/devices/e2/MideaE2Device.d.ts +44 -44
- package/dist/devices/e2/MideaE2Device.d.ts.map +1 -1
- package/dist/devices/e2/MideaE2Device.js +129 -119
- package/dist/devices/e2/MideaE2Device.js.map +1 -1
- package/dist/devices/e2/MideaE2Message.d.ts +33 -33
- package/dist/devices/e2/MideaE2Message.js +132 -132
- package/dist/devices/e3/MideaE3Device.d.ts +43 -43
- package/dist/devices/e3/MideaE3Device.d.ts.map +1 -1
- package/dist/devices/e3/MideaE3Device.js +137 -125
- package/dist/devices/e3/MideaE3Device.js.map +1 -1
- package/dist/devices/e3/MideaE3Message.d.ts +51 -51
- package/dist/devices/e3/MideaE3Message.js +136 -136
- package/dist/devices/fa/MideaFADevice.d.ts +36 -36
- package/dist/devices/fa/MideaFADevice.d.ts.map +1 -1
- package/dist/devices/fa/MideaFADevice.js +106 -92
- package/dist/devices/fa/MideaFADevice.js.map +1 -1
- package/dist/devices/fa/MideaFAMessage.d.ts +38 -38
- package/dist/devices/fa/MideaFAMessage.js +98 -98
- package/dist/index.d.ts +6 -6
- package/dist/index.js +6 -6
- package/dist/platform.d.ts +60 -60
- package/dist/platform.js +212 -212
- package/dist/platformUtils.d.ts +106 -99
- package/dist/platformUtils.d.ts.map +1 -1
- package/dist/platformUtils.js +101 -97
- package/dist/platformUtils.js.map +1 -1
- package/dist/settings.d.ts +8 -8
- package/dist/settings.js +11 -11
- package/docs/e1.md +3 -0
- package/homebridge-ui/server.js +3 -0
- package/package.json +1 -1
|
@@ -1,252 +1,252 @@
|
|
|
1
|
-
"use strict";
|
|
2
|
-
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
-
exports.LocalSecurity = exports.ArtisonClimaSecurity = exports.NetHomePlusSecurity = exports.SimpleSecurity = exports.MeijuCloudSecurity = exports.MSmartHomeCloudSecurity = exports.ProxiedSecurity = exports.CloudSecurity = void 0;
|
|
4
|
-
const crypto_1 = require("crypto");
|
|
5
|
-
const MideaConstants_1 = require("./MideaConstants");
|
|
6
|
-
const MideaUtils_1 = require("./MideaUtils");
|
|
7
|
-
const querystring_1 = require("querystring");
|
|
8
|
-
function unescape_plus(str) {
|
|
9
|
-
return (0, querystring_1.unescape)(str.replace(/\+/g, ' '));
|
|
10
|
-
}
|
|
11
|
-
class CloudSecurity {
|
|
12
|
-
constructor(login_key, iot_key, hmac_key) {
|
|
13
|
-
this.LOGIN_KEY = login_key;
|
|
14
|
-
if (hmac_key) {
|
|
15
|
-
this.HMAC_KEY = Buffer.from(hmac_key.toString(16), 'hex').toString();
|
|
16
|
-
}
|
|
17
|
-
if (iot_key) {
|
|
18
|
-
this.IOT_KEY = Buffer.from(iot_key.toString(16), 'hex').toString();
|
|
19
|
-
}
|
|
20
|
-
}
|
|
21
|
-
// Generate a HMAC signature for the provided data and random data.
|
|
22
|
-
sign(data, random) {
|
|
23
|
-
const message = `${this.IOT_KEY}${data}${random}`;
|
|
24
|
-
return (0, crypto_1.createHmac)('sha256', this.HMAC_KEY).update(message).digest('hex');
|
|
25
|
-
}
|
|
26
|
-
// Encrypt the password for cloud API password.
|
|
27
|
-
encrpytPassword(loginId, password) {
|
|
28
|
-
const m1 = (0, crypto_1.createHash)('sha256').update(password);
|
|
29
|
-
const login_hash = `${loginId}${m1.digest('hex')}${this.LOGIN_KEY}`;
|
|
30
|
-
const m2 = (0, crypto_1.createHash)('sha256').update(login_hash);
|
|
31
|
-
return m2.digest('hex');
|
|
32
|
-
}
|
|
33
|
-
static getUDPID(device_id_buf) {
|
|
34
|
-
const data = (0, crypto_1.createHash)('sha256').update(device_id_buf).digest();
|
|
35
|
-
const output = Buffer.alloc(16);
|
|
36
|
-
for (let i = 0; i < 16; i++) {
|
|
37
|
-
output[i] = data[i] ^ data[i + 16];
|
|
38
|
-
}
|
|
39
|
-
return output.toString('hex');
|
|
40
|
-
}
|
|
41
|
-
}
|
|
42
|
-
exports.CloudSecurity = CloudSecurity;
|
|
43
|
-
class ProxiedSecurity extends CloudSecurity {
|
|
44
|
-
getAppKeyAndIv() {
|
|
45
|
-
const hash = (0, crypto_1.createHash)('sha256').update(Buffer.from(this.APP_KEY, 'utf8')).digest('hex');
|
|
46
|
-
return {
|
|
47
|
-
appKey: Buffer.from(hash.substring(0, 16)),
|
|
48
|
-
iv: Buffer.from(hash.substring(16, 32)),
|
|
49
|
-
};
|
|
50
|
-
}
|
|
51
|
-
encryptAESAppKey(data) {
|
|
52
|
-
const { appKey, iv } = this.getAppKeyAndIv();
|
|
53
|
-
const cipher = (0, crypto_1.createCipheriv)('aes-128-cbc', appKey, iv);
|
|
54
|
-
return Buffer.concat([cipher.update(data), cipher.final()]);
|
|
55
|
-
}
|
|
56
|
-
decryptAESAppKey(data) {
|
|
57
|
-
const { appKey, iv } = this.getAppKeyAndIv();
|
|
58
|
-
const decipher = (0, crypto_1.createDecipheriv)('aes-128-cbc', appKey, iv);
|
|
59
|
-
return Buffer.concat([decipher.update(data), decipher.final()]);
|
|
60
|
-
}
|
|
61
|
-
}
|
|
62
|
-
exports.ProxiedSecurity = ProxiedSecurity;
|
|
63
|
-
class MSmartHomeCloudSecurity extends ProxiedSecurity {
|
|
64
|
-
constructor() {
|
|
65
|
-
super(MSmartHomeCloudSecurity._LOGIN_KEY, BigInt('7882822598523843940'), BigInt('117390035944627627450677220413733956185864939010425'));
|
|
66
|
-
this.APP_KEY = 'ac21b9f9cbfe4ca5a88562ef25e2b768';
|
|
67
|
-
}
|
|
68
|
-
encrpytIAMPassword(loginId, password) {
|
|
69
|
-
const m1 = (0, crypto_1.createHash)('md5').update(password);
|
|
70
|
-
const m2 = (0, crypto_1.createHash)('md5').update(m1.digest('hex'));
|
|
71
|
-
const login_hash = `${loginId}${m2.digest('hex')}${this.LOGIN_KEY}`;
|
|
72
|
-
const sha = (0, crypto_1.createHash)('sha256').update(login_hash);
|
|
73
|
-
return sha.digest('hex');
|
|
74
|
-
}
|
|
75
|
-
}
|
|
76
|
-
exports.MSmartHomeCloudSecurity = MSmartHomeCloudSecurity;
|
|
77
|
-
MSmartHomeCloudSecurity._LOGIN_KEY = 'ac21b9f9cbfe4ca5a88562ef25e2b768';
|
|
78
|
-
class MeijuCloudSecurity extends ProxiedSecurity {
|
|
79
|
-
constructor() {
|
|
80
|
-
super(MeijuCloudSecurity._LOGIN_KEY, BigInt('9795516279659324117647275084689641883661667'), BigInt('117390035944627627450677220413733956185864939010425'));
|
|
81
|
-
this.APP_KEY = 'ac21b9f9cbfe4ca5a88562ef25e2b768';
|
|
82
|
-
}
|
|
83
|
-
encrpytIAMPassword(_loginId, password) {
|
|
84
|
-
const m1 = (0, crypto_1.createHash)('md5').update(password);
|
|
85
|
-
const m2 = (0, crypto_1.createHash)('md5').update(m1.digest('hex'));
|
|
86
|
-
return m2.digest('hex');
|
|
87
|
-
}
|
|
88
|
-
}
|
|
89
|
-
exports.MeijuCloudSecurity = MeijuCloudSecurity;
|
|
90
|
-
MeijuCloudSecurity._LOGIN_KEY = 'ad0ee21d48a64bf49f4fb583ab76e799';
|
|
91
|
-
class SimpleSecurity extends CloudSecurity {
|
|
92
|
-
constructor(login_key) {
|
|
93
|
-
super(login_key);
|
|
94
|
-
}
|
|
95
|
-
encrpytIAMPassword() {
|
|
96
|
-
return '';
|
|
97
|
-
}
|
|
98
|
-
sign(url, query) {
|
|
99
|
-
const parsedUrl = new URL(url);
|
|
100
|
-
const path = parsedUrl.pathname;
|
|
101
|
-
return (0, crypto_1.createHash)('sha256')
|
|
102
|
-
.update(Buffer.from(`${path}${unescape_plus(query)}${this.LOGIN_KEY}`, 'ascii'))
|
|
103
|
-
.digest('hex');
|
|
104
|
-
}
|
|
105
|
-
}
|
|
106
|
-
exports.SimpleSecurity = SimpleSecurity;
|
|
107
|
-
class NetHomePlusSecurity extends SimpleSecurity {
|
|
108
|
-
constructor() {
|
|
109
|
-
super(NetHomePlusSecurity._LOGIN_KEY);
|
|
110
|
-
}
|
|
111
|
-
}
|
|
112
|
-
exports.NetHomePlusSecurity = NetHomePlusSecurity;
|
|
113
|
-
NetHomePlusSecurity._LOGIN_KEY = '3742e9e5842d4ad59c2db887e12449f9';
|
|
114
|
-
class ArtisonClimaSecurity extends SimpleSecurity {
|
|
115
|
-
constructor() {
|
|
116
|
-
super(ArtisonClimaSecurity._LOGIN_KEY);
|
|
117
|
-
}
|
|
118
|
-
}
|
|
119
|
-
exports.ArtisonClimaSecurity = ArtisonClimaSecurity;
|
|
120
|
-
ArtisonClimaSecurity._LOGIN_KEY = '434a209a5ce141c3b726de067835d7f0';
|
|
121
|
-
class LocalSecurity {
|
|
122
|
-
constructor() {
|
|
123
|
-
this.aes_key = Buffer.from(BigInt('141661095494369103254425781617665632877').toString(16), 'hex');
|
|
124
|
-
this.salt = Buffer.from(BigInt('233912452794221312800602098970898185176935770387238278451789080441632479840061417076563').toString(16), 'hex');
|
|
125
|
-
this.iv = Buffer.alloc(16);
|
|
126
|
-
this.request_count = 0;
|
|
127
|
-
this.response_count = 0;
|
|
128
|
-
this.tcp_key = Buffer.alloc(0);
|
|
129
|
-
}
|
|
130
|
-
aes_cbc_encrpyt(raw, key) {
|
|
131
|
-
const cipher = (0, crypto_1.createCipheriv)('aes-256-cbc', key, this.iv);
|
|
132
|
-
cipher.setAutoPadding(false);
|
|
133
|
-
return Buffer.concat([cipher.update(raw), cipher.final()]);
|
|
134
|
-
}
|
|
135
|
-
aes_cbc_decrypt(raw, key) {
|
|
136
|
-
const decipher = (0, crypto_1.createDecipheriv)('aes-256-cbc', key, this.iv);
|
|
137
|
-
decipher.setAutoPadding(false);
|
|
138
|
-
return Buffer.concat([decipher.update(raw), decipher.final()]);
|
|
139
|
-
}
|
|
140
|
-
aes_encrypt(data) {
|
|
141
|
-
const cipher = (0, crypto_1.createCipheriv)('aes-128-ecb', this.aes_key, null);
|
|
142
|
-
return Buffer.concat([cipher.update(data), cipher.final()]);
|
|
143
|
-
}
|
|
144
|
-
aes_decrypt(data) {
|
|
145
|
-
const decipher = (0, crypto_1.createDecipheriv)('aes-128-ecb', this.aes_key, null);
|
|
146
|
-
return Buffer.concat([decipher.update(data), decipher.final()]);
|
|
147
|
-
}
|
|
148
|
-
encode32_data(raw) {
|
|
149
|
-
return (0, crypto_1.createHash)('md5')
|
|
150
|
-
.update(Buffer.concat([raw, this.salt]))
|
|
151
|
-
.digest();
|
|
152
|
-
}
|
|
153
|
-
tcp_key_from_resp(response, key) {
|
|
154
|
-
if (response.toString() === 'ERROR') {
|
|
155
|
-
throw Error('Authentication response is ERROR, cannot get TCP key.');
|
|
156
|
-
}
|
|
157
|
-
if (response.length !== 64) {
|
|
158
|
-
throw Error('Authentication response has unexpected data length, cannot get TCP key..');
|
|
159
|
-
}
|
|
160
|
-
const payload = response.subarray(0, 32);
|
|
161
|
-
const sign = response.subarray(32, response.length);
|
|
162
|
-
const plain = this.aes_cbc_decrypt(payload, key);
|
|
163
|
-
if ((0, crypto_1.createHash)('sha256').update(plain).digest().compare(sign) !== 0) {
|
|
164
|
-
throw Error('Authentication sign does not match, cannot get TCP key.');
|
|
165
|
-
}
|
|
166
|
-
this.tcp_key = (0, MideaUtils_1.strxor)(plain, key);
|
|
167
|
-
this.request_count = 0;
|
|
168
|
-
this.response_count = 0;
|
|
169
|
-
return this.tcp_key;
|
|
170
|
-
}
|
|
171
|
-
encode_8370(data, message_type) {
|
|
172
|
-
if (message_type !== MideaConstants_1.TCPMessageType.HANDSHAKE_REQUEST &&
|
|
173
|
-
message_type !== MideaConstants_1.TCPMessageType.HANDSHAKE_RESPONSE &&
|
|
174
|
-
this.tcp_key.length === 0) {
|
|
175
|
-
throw new Error('TCP key is not set.');
|
|
176
|
-
}
|
|
177
|
-
let header = Buffer.from([0x83, 0x70]);
|
|
178
|
-
let size = data.length;
|
|
179
|
-
let padding = 0;
|
|
180
|
-
if (message_type === MideaConstants_1.TCPMessageType.ENCRYPTED_REQUEST || message_type === MideaConstants_1.TCPMessageType.ENCRYPTED_RESPONSE) {
|
|
181
|
-
if ((size + 2) % 16 !== 0) {
|
|
182
|
-
padding = 16 - ((size + 2) & 0xf);
|
|
183
|
-
size += padding + 32;
|
|
184
|
-
data = Buffer.concat([data, (0, crypto_1.randomBytes)(padding)]);
|
|
185
|
-
}
|
|
186
|
-
}
|
|
187
|
-
header = Buffer.concat([header, (0, MideaUtils_1.numberToUint8Array)(size, 2, MideaConstants_1.Endianness.Big)]);
|
|
188
|
-
header = Buffer.concat([header, Buffer.from([0x20, (padding << 4) | message_type])]);
|
|
189
|
-
data = Buffer.concat([(0, MideaUtils_1.numberToUint8Array)(this.request_count, 2, MideaConstants_1.Endianness.Big), data]);
|
|
190
|
-
this.request_count += 1;
|
|
191
|
-
if (this.request_count >= 0xffff) {
|
|
192
|
-
this.request_count = 0;
|
|
193
|
-
}
|
|
194
|
-
if (message_type === MideaConstants_1.TCPMessageType.ENCRYPTED_REQUEST || message_type === MideaConstants_1.TCPMessageType.ENCRYPTED_RESPONSE) {
|
|
195
|
-
const sign = (0, crypto_1.createHash)('sha256')
|
|
196
|
-
.update(Buffer.concat([header, data]))
|
|
197
|
-
.digest();
|
|
198
|
-
data = Buffer.concat([this.aes_cbc_encrpyt(data, this.tcp_key), sign]);
|
|
199
|
-
}
|
|
200
|
-
return Buffer.concat([header, data]);
|
|
201
|
-
}
|
|
202
|
-
decode_8370(data) {
|
|
203
|
-
if (data.length < 6) {
|
|
204
|
-
return [[], data];
|
|
205
|
-
}
|
|
206
|
-
if (this.tcp_key.length === 0) {
|
|
207
|
-
throw new Error('TCP key is not set.');
|
|
208
|
-
}
|
|
209
|
-
const header = data.subarray(0, 6);
|
|
210
|
-
if (header[0] !== 0x83 || header[1] !== 0x70) {
|
|
211
|
-
throw new Error('Not an 8370 message.');
|
|
212
|
-
}
|
|
213
|
-
const size = header.subarray(2, 4).readUInt16BE() + 8;
|
|
214
|
-
let leftover = Buffer.alloc(0);
|
|
215
|
-
if (data.length < size) {
|
|
216
|
-
return [[], data];
|
|
217
|
-
}
|
|
218
|
-
else if (data.length > size) {
|
|
219
|
-
leftover = data.subarray(size, data.length);
|
|
220
|
-
data = data.subarray(0, size);
|
|
221
|
-
}
|
|
222
|
-
if (header[4] !== 0x20) {
|
|
223
|
-
throw new Error('Missing byte 4');
|
|
224
|
-
}
|
|
225
|
-
const padding = header[5] >> 4;
|
|
226
|
-
const message_type_received = header[5] & 0xf;
|
|
227
|
-
data = data.subarray(6, data.length);
|
|
228
|
-
if ([MideaConstants_1.TCPMessageType.ENCRYPTED_RESPONSE, MideaConstants_1.TCPMessageType.ENCRYPTED_REQUEST].includes(message_type_received)) {
|
|
229
|
-
const sign = data.subarray(data.length - 32, data.length);
|
|
230
|
-
data = data.subarray(0, data.length - 32);
|
|
231
|
-
data = this.aes_cbc_decrypt(data, this.tcp_key);
|
|
232
|
-
if ((0, crypto_1.createHash)('sha256')
|
|
233
|
-
.update(Buffer.concat([header, data]))
|
|
234
|
-
.digest()
|
|
235
|
-
.compare(sign) !== 0) {
|
|
236
|
-
throw new Error('Sign does not match');
|
|
237
|
-
}
|
|
238
|
-
if (padding) {
|
|
239
|
-
data = data.subarray(0, data.length - padding);
|
|
240
|
-
}
|
|
241
|
-
}
|
|
242
|
-
this.response_count = data.subarray(0, 2).readUInt16BE();
|
|
243
|
-
data = data.subarray(2, data.length);
|
|
244
|
-
if (leftover.length > 0) {
|
|
245
|
-
const [packets, incomplete] = this.decode_8370(leftover);
|
|
246
|
-
return [[data, ...packets], incomplete];
|
|
247
|
-
}
|
|
248
|
-
return [[data], leftover];
|
|
249
|
-
}
|
|
250
|
-
}
|
|
251
|
-
exports.LocalSecurity = LocalSecurity;
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.LocalSecurity = exports.ArtisonClimaSecurity = exports.NetHomePlusSecurity = exports.SimpleSecurity = exports.MeijuCloudSecurity = exports.MSmartHomeCloudSecurity = exports.ProxiedSecurity = exports.CloudSecurity = void 0;
|
|
4
|
+
const crypto_1 = require("crypto");
|
|
5
|
+
const MideaConstants_1 = require("./MideaConstants");
|
|
6
|
+
const MideaUtils_1 = require("./MideaUtils");
|
|
7
|
+
const querystring_1 = require("querystring");
|
|
8
|
+
function unescape_plus(str) {
|
|
9
|
+
return (0, querystring_1.unescape)(str.replace(/\+/g, ' '));
|
|
10
|
+
}
|
|
11
|
+
class CloudSecurity {
|
|
12
|
+
constructor(login_key, iot_key, hmac_key) {
|
|
13
|
+
this.LOGIN_KEY = login_key;
|
|
14
|
+
if (hmac_key) {
|
|
15
|
+
this.HMAC_KEY = Buffer.from(hmac_key.toString(16), 'hex').toString();
|
|
16
|
+
}
|
|
17
|
+
if (iot_key) {
|
|
18
|
+
this.IOT_KEY = Buffer.from(iot_key.toString(16), 'hex').toString();
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
// Generate a HMAC signature for the provided data and random data.
|
|
22
|
+
sign(data, random) {
|
|
23
|
+
const message = `${this.IOT_KEY}${data}${random}`;
|
|
24
|
+
return (0, crypto_1.createHmac)('sha256', this.HMAC_KEY).update(message).digest('hex');
|
|
25
|
+
}
|
|
26
|
+
// Encrypt the password for cloud API password.
|
|
27
|
+
encrpytPassword(loginId, password) {
|
|
28
|
+
const m1 = (0, crypto_1.createHash)('sha256').update(password);
|
|
29
|
+
const login_hash = `${loginId}${m1.digest('hex')}${this.LOGIN_KEY}`;
|
|
30
|
+
const m2 = (0, crypto_1.createHash)('sha256').update(login_hash);
|
|
31
|
+
return m2.digest('hex');
|
|
32
|
+
}
|
|
33
|
+
static getUDPID(device_id_buf) {
|
|
34
|
+
const data = (0, crypto_1.createHash)('sha256').update(device_id_buf).digest();
|
|
35
|
+
const output = Buffer.alloc(16);
|
|
36
|
+
for (let i = 0; i < 16; i++) {
|
|
37
|
+
output[i] = data[i] ^ data[i + 16];
|
|
38
|
+
}
|
|
39
|
+
return output.toString('hex');
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
exports.CloudSecurity = CloudSecurity;
|
|
43
|
+
class ProxiedSecurity extends CloudSecurity {
|
|
44
|
+
getAppKeyAndIv() {
|
|
45
|
+
const hash = (0, crypto_1.createHash)('sha256').update(Buffer.from(this.APP_KEY, 'utf8')).digest('hex');
|
|
46
|
+
return {
|
|
47
|
+
appKey: Buffer.from(hash.substring(0, 16)),
|
|
48
|
+
iv: Buffer.from(hash.substring(16, 32)),
|
|
49
|
+
};
|
|
50
|
+
}
|
|
51
|
+
encryptAESAppKey(data) {
|
|
52
|
+
const { appKey, iv } = this.getAppKeyAndIv();
|
|
53
|
+
const cipher = (0, crypto_1.createCipheriv)('aes-128-cbc', appKey, iv);
|
|
54
|
+
return Buffer.concat([cipher.update(data), cipher.final()]);
|
|
55
|
+
}
|
|
56
|
+
decryptAESAppKey(data) {
|
|
57
|
+
const { appKey, iv } = this.getAppKeyAndIv();
|
|
58
|
+
const decipher = (0, crypto_1.createDecipheriv)('aes-128-cbc', appKey, iv);
|
|
59
|
+
return Buffer.concat([decipher.update(data), decipher.final()]);
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
exports.ProxiedSecurity = ProxiedSecurity;
|
|
63
|
+
class MSmartHomeCloudSecurity extends ProxiedSecurity {
|
|
64
|
+
constructor() {
|
|
65
|
+
super(MSmartHomeCloudSecurity._LOGIN_KEY, BigInt('7882822598523843940'), BigInt('117390035944627627450677220413733956185864939010425'));
|
|
66
|
+
this.APP_KEY = 'ac21b9f9cbfe4ca5a88562ef25e2b768';
|
|
67
|
+
}
|
|
68
|
+
encrpytIAMPassword(loginId, password) {
|
|
69
|
+
const m1 = (0, crypto_1.createHash)('md5').update(password);
|
|
70
|
+
const m2 = (0, crypto_1.createHash)('md5').update(m1.digest('hex'));
|
|
71
|
+
const login_hash = `${loginId}${m2.digest('hex')}${this.LOGIN_KEY}`;
|
|
72
|
+
const sha = (0, crypto_1.createHash)('sha256').update(login_hash);
|
|
73
|
+
return sha.digest('hex');
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
exports.MSmartHomeCloudSecurity = MSmartHomeCloudSecurity;
|
|
77
|
+
MSmartHomeCloudSecurity._LOGIN_KEY = 'ac21b9f9cbfe4ca5a88562ef25e2b768';
|
|
78
|
+
class MeijuCloudSecurity extends ProxiedSecurity {
|
|
79
|
+
constructor() {
|
|
80
|
+
super(MeijuCloudSecurity._LOGIN_KEY, BigInt('9795516279659324117647275084689641883661667'), BigInt('117390035944627627450677220413733956185864939010425'));
|
|
81
|
+
this.APP_KEY = 'ac21b9f9cbfe4ca5a88562ef25e2b768';
|
|
82
|
+
}
|
|
83
|
+
encrpytIAMPassword(_loginId, password) {
|
|
84
|
+
const m1 = (0, crypto_1.createHash)('md5').update(password);
|
|
85
|
+
const m2 = (0, crypto_1.createHash)('md5').update(m1.digest('hex'));
|
|
86
|
+
return m2.digest('hex');
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
exports.MeijuCloudSecurity = MeijuCloudSecurity;
|
|
90
|
+
MeijuCloudSecurity._LOGIN_KEY = 'ad0ee21d48a64bf49f4fb583ab76e799';
|
|
91
|
+
class SimpleSecurity extends CloudSecurity {
|
|
92
|
+
constructor(login_key) {
|
|
93
|
+
super(login_key);
|
|
94
|
+
}
|
|
95
|
+
encrpytIAMPassword() {
|
|
96
|
+
return '';
|
|
97
|
+
}
|
|
98
|
+
sign(url, query) {
|
|
99
|
+
const parsedUrl = new URL(url);
|
|
100
|
+
const path = parsedUrl.pathname;
|
|
101
|
+
return (0, crypto_1.createHash)('sha256')
|
|
102
|
+
.update(Buffer.from(`${path}${unescape_plus(query)}${this.LOGIN_KEY}`, 'ascii'))
|
|
103
|
+
.digest('hex');
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
exports.SimpleSecurity = SimpleSecurity;
|
|
107
|
+
class NetHomePlusSecurity extends SimpleSecurity {
|
|
108
|
+
constructor() {
|
|
109
|
+
super(NetHomePlusSecurity._LOGIN_KEY);
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
exports.NetHomePlusSecurity = NetHomePlusSecurity;
|
|
113
|
+
NetHomePlusSecurity._LOGIN_KEY = '3742e9e5842d4ad59c2db887e12449f9';
|
|
114
|
+
class ArtisonClimaSecurity extends SimpleSecurity {
|
|
115
|
+
constructor() {
|
|
116
|
+
super(ArtisonClimaSecurity._LOGIN_KEY);
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
exports.ArtisonClimaSecurity = ArtisonClimaSecurity;
|
|
120
|
+
ArtisonClimaSecurity._LOGIN_KEY = '434a209a5ce141c3b726de067835d7f0';
|
|
121
|
+
class LocalSecurity {
|
|
122
|
+
constructor() {
|
|
123
|
+
this.aes_key = Buffer.from(BigInt('141661095494369103254425781617665632877').toString(16), 'hex');
|
|
124
|
+
this.salt = Buffer.from(BigInt('233912452794221312800602098970898185176935770387238278451789080441632479840061417076563').toString(16), 'hex');
|
|
125
|
+
this.iv = Buffer.alloc(16);
|
|
126
|
+
this.request_count = 0;
|
|
127
|
+
this.response_count = 0;
|
|
128
|
+
this.tcp_key = Buffer.alloc(0);
|
|
129
|
+
}
|
|
130
|
+
aes_cbc_encrpyt(raw, key) {
|
|
131
|
+
const cipher = (0, crypto_1.createCipheriv)('aes-256-cbc', key, this.iv);
|
|
132
|
+
cipher.setAutoPadding(false);
|
|
133
|
+
return Buffer.concat([cipher.update(raw), cipher.final()]);
|
|
134
|
+
}
|
|
135
|
+
aes_cbc_decrypt(raw, key) {
|
|
136
|
+
const decipher = (0, crypto_1.createDecipheriv)('aes-256-cbc', key, this.iv);
|
|
137
|
+
decipher.setAutoPadding(false);
|
|
138
|
+
return Buffer.concat([decipher.update(raw), decipher.final()]);
|
|
139
|
+
}
|
|
140
|
+
aes_encrypt(data) {
|
|
141
|
+
const cipher = (0, crypto_1.createCipheriv)('aes-128-ecb', this.aes_key, null);
|
|
142
|
+
return Buffer.concat([cipher.update(data), cipher.final()]);
|
|
143
|
+
}
|
|
144
|
+
aes_decrypt(data) {
|
|
145
|
+
const decipher = (0, crypto_1.createDecipheriv)('aes-128-ecb', this.aes_key, null);
|
|
146
|
+
return Buffer.concat([decipher.update(data), decipher.final()]);
|
|
147
|
+
}
|
|
148
|
+
encode32_data(raw) {
|
|
149
|
+
return (0, crypto_1.createHash)('md5')
|
|
150
|
+
.update(Buffer.concat([raw, this.salt]))
|
|
151
|
+
.digest();
|
|
152
|
+
}
|
|
153
|
+
tcp_key_from_resp(response, key) {
|
|
154
|
+
if (response.toString() === 'ERROR') {
|
|
155
|
+
throw Error('Authentication response is ERROR, cannot get TCP key.');
|
|
156
|
+
}
|
|
157
|
+
if (response.length !== 64) {
|
|
158
|
+
throw Error('Authentication response has unexpected data length, cannot get TCP key..');
|
|
159
|
+
}
|
|
160
|
+
const payload = response.subarray(0, 32);
|
|
161
|
+
const sign = response.subarray(32, response.length);
|
|
162
|
+
const plain = this.aes_cbc_decrypt(payload, key);
|
|
163
|
+
if ((0, crypto_1.createHash)('sha256').update(plain).digest().compare(sign) !== 0) {
|
|
164
|
+
throw Error('Authentication sign does not match, cannot get TCP key.');
|
|
165
|
+
}
|
|
166
|
+
this.tcp_key = (0, MideaUtils_1.strxor)(plain, key);
|
|
167
|
+
this.request_count = 0;
|
|
168
|
+
this.response_count = 0;
|
|
169
|
+
return this.tcp_key;
|
|
170
|
+
}
|
|
171
|
+
encode_8370(data, message_type) {
|
|
172
|
+
if (message_type !== MideaConstants_1.TCPMessageType.HANDSHAKE_REQUEST &&
|
|
173
|
+
message_type !== MideaConstants_1.TCPMessageType.HANDSHAKE_RESPONSE &&
|
|
174
|
+
this.tcp_key.length === 0) {
|
|
175
|
+
throw new Error('TCP key is not set.');
|
|
176
|
+
}
|
|
177
|
+
let header = Buffer.from([0x83, 0x70]);
|
|
178
|
+
let size = data.length;
|
|
179
|
+
let padding = 0;
|
|
180
|
+
if (message_type === MideaConstants_1.TCPMessageType.ENCRYPTED_REQUEST || message_type === MideaConstants_1.TCPMessageType.ENCRYPTED_RESPONSE) {
|
|
181
|
+
if ((size + 2) % 16 !== 0) {
|
|
182
|
+
padding = 16 - ((size + 2) & 0xf);
|
|
183
|
+
size += padding + 32;
|
|
184
|
+
data = Buffer.concat([data, (0, crypto_1.randomBytes)(padding)]);
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
header = Buffer.concat([header, (0, MideaUtils_1.numberToUint8Array)(size, 2, MideaConstants_1.Endianness.Big)]);
|
|
188
|
+
header = Buffer.concat([header, Buffer.from([0x20, (padding << 4) | message_type])]);
|
|
189
|
+
data = Buffer.concat([(0, MideaUtils_1.numberToUint8Array)(this.request_count, 2, MideaConstants_1.Endianness.Big), data]);
|
|
190
|
+
this.request_count += 1;
|
|
191
|
+
if (this.request_count >= 0xffff) {
|
|
192
|
+
this.request_count = 0;
|
|
193
|
+
}
|
|
194
|
+
if (message_type === MideaConstants_1.TCPMessageType.ENCRYPTED_REQUEST || message_type === MideaConstants_1.TCPMessageType.ENCRYPTED_RESPONSE) {
|
|
195
|
+
const sign = (0, crypto_1.createHash)('sha256')
|
|
196
|
+
.update(Buffer.concat([header, data]))
|
|
197
|
+
.digest();
|
|
198
|
+
data = Buffer.concat([this.aes_cbc_encrpyt(data, this.tcp_key), sign]);
|
|
199
|
+
}
|
|
200
|
+
return Buffer.concat([header, data]);
|
|
201
|
+
}
|
|
202
|
+
decode_8370(data) {
|
|
203
|
+
if (data.length < 6) {
|
|
204
|
+
return [[], data];
|
|
205
|
+
}
|
|
206
|
+
if (this.tcp_key.length === 0) {
|
|
207
|
+
throw new Error('TCP key is not set.');
|
|
208
|
+
}
|
|
209
|
+
const header = data.subarray(0, 6);
|
|
210
|
+
if (header[0] !== 0x83 || header[1] !== 0x70) {
|
|
211
|
+
throw new Error('Not an 8370 message.');
|
|
212
|
+
}
|
|
213
|
+
const size = header.subarray(2, 4).readUInt16BE() + 8;
|
|
214
|
+
let leftover = Buffer.alloc(0);
|
|
215
|
+
if (data.length < size) {
|
|
216
|
+
return [[], data];
|
|
217
|
+
}
|
|
218
|
+
else if (data.length > size) {
|
|
219
|
+
leftover = data.subarray(size, data.length);
|
|
220
|
+
data = data.subarray(0, size);
|
|
221
|
+
}
|
|
222
|
+
if (header[4] !== 0x20) {
|
|
223
|
+
throw new Error('Missing byte 4');
|
|
224
|
+
}
|
|
225
|
+
const padding = header[5] >> 4;
|
|
226
|
+
const message_type_received = header[5] & 0xf;
|
|
227
|
+
data = data.subarray(6, data.length);
|
|
228
|
+
if ([MideaConstants_1.TCPMessageType.ENCRYPTED_RESPONSE, MideaConstants_1.TCPMessageType.ENCRYPTED_REQUEST].includes(message_type_received)) {
|
|
229
|
+
const sign = data.subarray(data.length - 32, data.length);
|
|
230
|
+
data = data.subarray(0, data.length - 32);
|
|
231
|
+
data = this.aes_cbc_decrypt(data, this.tcp_key);
|
|
232
|
+
if ((0, crypto_1.createHash)('sha256')
|
|
233
|
+
.update(Buffer.concat([header, data]))
|
|
234
|
+
.digest()
|
|
235
|
+
.compare(sign) !== 0) {
|
|
236
|
+
throw new Error('Sign does not match');
|
|
237
|
+
}
|
|
238
|
+
if (padding) {
|
|
239
|
+
data = data.subarray(0, data.length - padding);
|
|
240
|
+
}
|
|
241
|
+
}
|
|
242
|
+
this.response_count = data.subarray(0, 2).readUInt16BE();
|
|
243
|
+
data = data.subarray(2, data.length);
|
|
244
|
+
if (leftover.length > 0) {
|
|
245
|
+
const [packets, incomplete] = this.decode_8370(leftover);
|
|
246
|
+
return [[data, ...packets], incomplete];
|
|
247
|
+
}
|
|
248
|
+
return [[data], leftover];
|
|
249
|
+
}
|
|
250
|
+
}
|
|
251
|
+
exports.LocalSecurity = LocalSecurity;
|
|
252
252
|
//# sourceMappingURL=MideaSecurity.js.map
|
|
@@ -1,33 +1,33 @@
|
|
|
1
|
-
/// <reference types="node" />
|
|
2
|
-
/***********************************************************************
|
|
3
|
-
* Homebridge-midea-platform miscellaneous support functions.
|
|
4
|
-
*
|
|
5
|
-
* Copyright (c) 2023 Kovalovszky Patrik, https://github.com/kovapatrik
|
|
6
|
-
* Portions Copyright (c) 2023 David Kerr, https://github.com/dkerr64
|
|
7
|
-
*
|
|
8
|
-
* Includes very basic implementation of a promise-wrapped Socket class.
|
|
9
|
-
*
|
|
10
|
-
*/
|
|
11
|
-
import { Logger } from 'homebridge';
|
|
12
|
-
import { Endianness } from './MideaConstants';
|
|
13
|
-
export declare function numberToUint8Array(num: number, byte_length: number, endianness: Endianness): Uint8Array;
|
|
14
|
-
export declare function strxor(a: Buffer, b: Buffer): Buffer;
|
|
15
|
-
export declare function calculate(data: Buffer): number;
|
|
16
|
-
/*********************************************************************
|
|
17
|
-
* PromiseSocket
|
|
18
|
-
* A very basic implementation of promise-wrapped Socket
|
|
19
|
-
*
|
|
20
|
-
*/
|
|
21
|
-
export declare class PromiseSocket {
|
|
22
|
-
private readonly logger;
|
|
23
|
-
private readonly logerror;
|
|
24
|
-
private innerSok;
|
|
25
|
-
destroyed: boolean;
|
|
26
|
-
constructor(logger: Logger, logerror: boolean);
|
|
27
|
-
connect(port: number, host: string): Promise<void>;
|
|
28
|
-
setTimeout(t: number): void;
|
|
29
|
-
destroy(): void;
|
|
30
|
-
write(data: string | Buffer, encoding?: BufferEncoding): Promise<void>;
|
|
31
|
-
read(): Promise<Buffer>;
|
|
32
|
-
}
|
|
1
|
+
/// <reference types="node" />
|
|
2
|
+
/***********************************************************************
|
|
3
|
+
* Homebridge-midea-platform miscellaneous support functions.
|
|
4
|
+
*
|
|
5
|
+
* Copyright (c) 2023 Kovalovszky Patrik, https://github.com/kovapatrik
|
|
6
|
+
* Portions Copyright (c) 2023 David Kerr, https://github.com/dkerr64
|
|
7
|
+
*
|
|
8
|
+
* Includes very basic implementation of a promise-wrapped Socket class.
|
|
9
|
+
*
|
|
10
|
+
*/
|
|
11
|
+
import { Logger } from 'homebridge';
|
|
12
|
+
import { Endianness } from './MideaConstants';
|
|
13
|
+
export declare function numberToUint8Array(num: number, byte_length: number, endianness: Endianness): Uint8Array;
|
|
14
|
+
export declare function strxor(a: Buffer, b: Buffer): Buffer;
|
|
15
|
+
export declare function calculate(data: Buffer): number;
|
|
16
|
+
/*********************************************************************
|
|
17
|
+
* PromiseSocket
|
|
18
|
+
* A very basic implementation of promise-wrapped Socket
|
|
19
|
+
*
|
|
20
|
+
*/
|
|
21
|
+
export declare class PromiseSocket {
|
|
22
|
+
private readonly logger;
|
|
23
|
+
private readonly logerror;
|
|
24
|
+
private innerSok;
|
|
25
|
+
destroyed: boolean;
|
|
26
|
+
constructor(logger: Logger, logerror: boolean);
|
|
27
|
+
connect(port: number, host: string): Promise<void>;
|
|
28
|
+
setTimeout(t: number): void;
|
|
29
|
+
destroy(): void;
|
|
30
|
+
write(data: string | Buffer, encoding?: BufferEncoding): Promise<void>;
|
|
31
|
+
read(): Promise<Buffer>;
|
|
32
|
+
}
|
|
33
33
|
//# sourceMappingURL=MideaUtils.d.ts.map
|