• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

codetheweb / tuyapi / 10869511015

15 Sep 2024 08:29AM UTC coverage: 58.841% (-0.03%) from 58.866%
10869511015

push

github

web-flow
Merge pull request #657 from Apollon77/35upd

Updates to make testing happy

200 of 352 branches covered (56.82%)

Branch coverage included in aggregate %.

21 of 55 new or added lines in 3 files covered. (38.18%)

8 existing lines in 3 files now uncovered.

379 of 632 relevant lines covered (59.97%)

114.3 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

39.02
/lib/cipher.js
1
const crypto = require('crypto');
30✔
2
/**
3
* Low-level class for encrypting and decrypting payloads.
4
* @class
5
* @param {Object} options - Options for the cipher.
6
* @param {String} options.key localKey of cipher
7
* @param {Number} options.version protocol version
8
* @example
9
* const cipher = new TuyaCipher({key: 'xxxxxxxxxxxxxxxx', version: 3.1})
10
*/
11
class TuyaCipher {
12
  constructor(options) {
13
    this.sessionKey = null;
168✔
14
    this.key = options.key;
168✔
15
    this.version = options.version.toString();
168✔
16
  }
17

18
  /**
19
   * Sets the session key used for Protocol 3.4, 3.5
20
   * @param {Buffer} sessionKey Session key
21
   */
22
  setSessionKey(sessionKey) {
23
    this.sessionKey = sessionKey;
48✔
24
  }
25

26
  /**
27
  * Encrypts data.
28
  * @param {Object} options Options for encryption
29
  * @param {String} options.data data to encrypt
30
  * @param {Boolean} [options.base64=true] `true` to return result in Base64
31
  * @example
32
  * TuyaCipher.encrypt({data: 'hello world'})
33
  * @returns {Buffer|String} returns Buffer unless options.base64 is true
34
  */
35
  encrypt(options) {
36
    if (this.version === '3.4') {
36!
37
      return this._encrypt34(options);
×
38
    }
39

40
    if (this.version === '3.5') {
36!
41
      return this._encrypt35(options);
×
42
    }
43

44
    return this._encryptPre34(options);
36✔
45
  }
46

47
  /**
48
   * Encrypt data for protocol 3.3 and before
49
   * @param {Object} options Options for encryption
50
   * @param {String} options.data data to encrypt
51
   * @param {Boolean} [options.base64=true] `true` to return result in Base64
52
   * @returns {Buffer|String} returns Buffer unless options.base64 is true
53
   */
54
  _encryptPre34(options) {
55
    const cipher = crypto.createCipheriv('aes-128-ecb', this.getKey(), '');
36✔
56

57
    let encrypted = cipher.update(options.data, 'utf8', 'base64');
36✔
58
    encrypted += cipher.final('base64');
36✔
59

60
    // Default base64 enable
61
    if (options.base64 === false) {
36✔
62
      return Buffer.from(encrypted, 'base64');
18✔
63
    }
64

65
    return encrypted;
18✔
66
  }
67

68
  /**
69
   * Encrypt data for protocol 3.4
70
   * @param {Object} options Options for encryption
71
   * @param {String} options.data data to encrypt
72
   * @param {Boolean} [options.base64=true] `true` to return result in Base64
73
   * @returns {Buffer|String} returns Buffer unless options.base64 is true
74
   */
75
  _encrypt34(options) {
76
    const cipher = crypto.createCipheriv('aes-128-ecb', this.getKey(), null);
×
77
    cipher.setAutoPadding(false);
×
78
    const encrypted = cipher.update(options.data);
×
79
    cipher.final();
×
80

81
    // Default base64 enable TODO: check if this is needed?
82
    // if (options.base64 === false) {
83
    //   return Buffer.from(encrypted, 'base64');
84
    // }
85

86
    return encrypted;
×
87
  }
88

89
  /**
90
   * Encrypt data for protocol 3.5
91
   * @param {Object} options Options for encryption
92
   * @param {String} options.data data to encrypt
93
   * @param {Boolean} [options.base64=true] `true` to return result in Base64
94
   * @returns {Buffer|String} returns Buffer unless options.base64 is true
95
   */
96
  _encrypt35(options) {
97
    let encrypted;
NEW
98
    let localIV = Buffer.from((Date.now() * 10).toString().slice(0, 12));
×
NEW
99
    if (options.iv !== undefined) {
×
NEW
100
      localIV = options.iv.slice(0, 12);
×
101
    }
102

103
    const cipher = crypto.createCipheriv('aes-128-gcm', this.getKey(), localIV);
×
NEW
104
    if (options.aad === undefined) {
×
NEW
105
      encrypted = Buffer.concat([cipher.update(options.data), cipher.final()]);
×
106
    } else {
107
      cipher.setAAD(options.aad);
×
108
      encrypted = Buffer.concat([localIV, cipher.update(options.data), cipher.final(), cipher.getAuthTag(), Buffer.from([0x00, 0x00, 0x99, 0x66])]);
×
109
    }
110

UNCOV
111
    return encrypted;
×
112
  }
113

114
  /**
115
  * Decrypts data.
116
  * @param {String|Buffer} data to decrypt
117
  * @returns {Object|String}
118
  * returns object if data is JSON, else returns string
119
  */
120
  decrypt(data) {
121
    if (this.version === '3.4') {
186!
122
      return this._decrypt34(data);
×
123
    }
124

125
    if (this.version === '3.5') {
186!
126
      return this._decrypt35(data);
×
127
    }
128

129
    return this._decryptPre34(data);
186✔
130
  }
131

132
  /**
133
   * Decrypts data for protocol 3.3 and before
134
   * @param {String|Buffer} data to decrypt
135
   * @returns {Object|String}
136
   * returns object if data is JSON, else returns string
137
   */
138
  _decryptPre34(data) {
139
    // Incoming data format
140
    let format = 'buffer';
186✔
141

142
    if (data.indexOf(this.version) === 0) {
186✔
143
      if (this.version === '3.3' || this.version === '3.2') {
18✔
144
        // Remove 3.3/3.2 header
145
        data = data.slice(15);
6✔
146
      } else {
147
        // Data has version number and is encoded in base64
148

149
        // Remove prefix of version number and MD5 hash
150
        data = data.slice(19).toString();
12✔
151
        // Decode incoming data as base64
152
        format = 'base64';
12✔
153
      }
154
    }
155

156
    // Decrypt data
157
    let result;
158
    try {
186✔
159
      const decipher = crypto.createDecipheriv('aes-128-ecb', this.getKey(), '');
186✔
160
      result = decipher.update(data, format, 'utf8');
186✔
161
      result += decipher.final('utf8');
186✔
162
    } catch (_) {
163
      throw new Error('Decrypt failed');
156✔
164
    }
165

166
    // Try to parse data as JSON,
167
    // otherwise return as string.
168
    try {
30✔
169
      return JSON.parse(result);
30✔
170
    } catch (_) {
171
      return result;
6✔
172
    }
173
  }
174

175
  /**
176
   * Decrypts data for protocol 3.4
177
   * @param {String|Buffer} data to decrypt
178
   * @returns {Object|String}
179
   * returns object if data is JSON, else returns string
180
   */
181
  _decrypt34(data) {
182
    let result;
183
    try {
×
184
      const decipher = crypto.createDecipheriv('aes-128-ecb', this.getKey(), null);
×
185
      decipher.setAutoPadding(false);
×
186
      result = decipher.update(data);
×
187
      decipher.final();
×
188
      // Remove padding
189
      result = result.slice(0, (result.length - result[result.length - 1]));
×
190
    } catch (_) {
191
      throw new Error('Decrypt failed');
×
192
    }
193

194
    // Try to parse data as JSON,
195
    // otherwise return as string.
196
    // 3.4 protocol
197
    // {"protocol":4,"t":1632405905,"data":{"dps":{"101":true},"cid":"00123456789abcde"}}
198
    try {
×
199
      if (result.indexOf(this.version) === 0) {
×
200
        result = result.slice(15);
×
201
      }
202

203
      const res = JSON.parse(result);
×
204
      if ('data' in res) {
×
205
        const resData = res.data;
×
206
        resData.t = res.t;
×
207
        return resData; // Or res.data // for compatibility with tuya-mqtt
×
208
      }
209

210
      return res;
×
211
    } catch (_) {
212
      return result;
×
213
    }
214
  }
215

216
  /**
217
   * Decrypts data for protocol 3.5
218
   * @param {String|Buffer} data to decrypt
219
   * @returns {Object|String}
220
   * returns object if data is JSON, else returns string
221
   */
222
  _decrypt35(data) {
223
    let result;
NEW
224
    const header = data.slice(0, 14);
×
NEW
225
    const iv = data.slice(14, 26);
×
NEW
226
    const tag = data.slice(data.length - 16);
×
UNCOV
227
    data = data.slice(26, data.length - 16);
×
228

229
    try {
×
230
      const decipher = crypto.createDecipheriv('aes-128-gcm', this.getKey(), iv);
×
231
      decipher.setAuthTag(tag);
×
232
      decipher.setAAD(header);
×
233

234
      result = Buffer.concat([decipher.update(data), decipher.final()]);
×
NEW
235
      result = result.slice(4); // Remove 32bit return code
×
236
    } catch (_) {
237
      throw new Error('Decrypt failed');
×
238
    }
239

240
    // Try to parse data as JSON, otherwise return as string.
241
    // 3.5 protocol
242
    // {"protocol":4,"t":1632405905,"data":{"dps":{"101":true},"cid":"00123456789abcde"}}
243
    try {
×
244
      if (result.indexOf(this.version) === 0) {
×
245
        result = result.slice(15);
×
246
      }
247

248
      const res = JSON.parse(result);
×
249
      if ('data' in res) {
×
250
        const resData = res.data;
×
251
        resData.t = res.t;
×
252
        return resData; // Or res.data // for compatibility with tuya-mqtt
×
253
      }
254

255
      return res;
×
256
    } catch (_) {
257
      return result;
×
258
    }
259
  }
260

261
  /**
262
  * Calculates a MD5 hash.
263
  * @param {String} data to hash
264
  * @returns {String} characters 8 through 16 of hash of data
265
  */
266
  md5(data) {
267
    const md5hash = crypto.createHash('md5').update(data, 'utf8').digest('hex');
18✔
268
    return md5hash.slice(8, 24);
18✔
269
  }
270

271
  /**
272
   * Gets the key used for encryption/decryption
273
   * @returns {String} sessionKey (if set for protocol 3.4, 3.5) or key
274
   */
275
  getKey() {
276
    return this.sessionKey === null ? this.key : this.sessionKey;
222!
277
  }
278

279
  /**
280
   * Returns the HMAC for the current key (sessionKey if set for protocol 3.4, 3.5 or key)
281
   * @param {Buffer} data data to hash
282
   * @returns {Buffer} HMAC
283
   */
284
  hmac(data) {
285
    return crypto.createHmac('sha256', this.getKey()).update(data, 'utf8').digest(); // .digest('hex');
×
286
  }
287

288
  /**
289
   * Returns 16 random bytes
290
   * @returns {Buffer} Random bytes
291
   */
292
  random() {
293
    return crypto.randomBytes(16);
×
294
  }
295
}
296
module.exports = TuyaCipher;
30✔
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc