@zwave-js/cc 14.2.0 → 14.3.1

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.
@@ -32,7 +32,7 @@ var __runInitializers = (this && this.__runInitializers) || function (thisArg, i
32
32
  }
33
33
  return useValue ? value : void 0;
34
34
  };
35
- import { CommandClasses, MPANState, MessagePriority, SECURITY_S2_AUTH_TAG_LENGTH, SPANState, SecurityClass, TransmitOptions, ZWaveError, ZWaveErrorCodes, decryptAES128CCM, encodeBitMask, encryptAES128CCM, getCCName, highResTimestamp, isLongRangeNodeId, isTransmissionError, isZWaveError, parseBitMask, parseCCList, securityClassIsS2, securityClassOrder, validatePayload, } from "@zwave-js/core";
35
+ import { CommandClasses, MPANState, MessagePriority, SPANState, SecurityClass, TransmitOptions, ZWaveError, ZWaveErrorCodes, decryptAES128CCMAsync, decryptAES128CCMSync, encodeBitMask, encryptAES128CCMAsync, encryptAES128CCMSync, getCCName, highResTimestamp, isLongRangeNodeId, isTransmissionError, isZWaveError, parseBitMask, parseCCList, securityClassIsS2, securityClassOrder, validatePayload, } from "@zwave-js/core";
36
36
  import { EncapsulationFlags, NODE_ID_BROADCAST, NODE_ID_BROADCAST_LR, encodeCCList, } from "@zwave-js/core/safe";
37
37
  import { Bytes } from "@zwave-js/shared/safe";
38
38
  import { buffer2hex, getEnumMemberName, pick } from "@zwave-js/shared/safe";
@@ -56,6 +56,7 @@ function bitMaskToSecurityClass(buffer, offset) {
56
56
  validatePayload(keys.length === 1);
57
57
  return keys[0];
58
58
  }
59
+ const SECURITY_S2_AUTH_TAG_LENGTH = 8;
59
60
  function getAuthenticationData(sendingNodeId, destination, homeId, commandLength, unencryptedPayload) {
60
61
  const nodeIdSize = isLongRangeNodeId(sendingNodeId) || isLongRangeNodeId(destination)
61
62
  ? 2
@@ -110,17 +111,20 @@ function assertSecurityTX(ctx, destination) {
110
111
  }
111
112
  return ret;
112
113
  }
113
- function decryptSinglecast(ctx, securityManager, sendingNodeId, curSequenceNumber, prevSequenceNumber, ciphertext, authData, authTag, spanState, extensions) {
114
+ /** @deprecated Use {@link decryptSinglecastAsync} instead */
115
+ function decryptSinglecastSync(ctx, securityManager, sendingNodeId, curSequenceNumber, prevSequenceNumber, ciphertext, authData, authTag, spanState, extensions) {
114
116
  const decryptWithNonce = (nonce) => {
115
117
  const { keyCCM: key } = securityManager.getKeysForNode(sendingNodeId);
116
118
  const iv = nonce;
117
119
  return {
118
120
  key,
119
121
  iv,
120
- ...decryptAES128CCM(key, iv, ciphertext, authData, authTag),
122
+ // eslint-disable-next-line @typescript-eslint/no-deprecated
123
+ ...decryptAES128CCMSync(key, iv, ciphertext, authData, authTag),
121
124
  };
122
125
  };
123
126
  const getNonceAndDecrypt = () => {
127
+ // eslint-disable-next-line @typescript-eslint/no-deprecated
124
128
  const iv = securityManager.nextNonce(sendingNodeId);
125
129
  return decryptWithNonce(iv);
126
130
  };
@@ -172,6 +176,7 @@ function decryptSinglecast(ctx, securityManager, sendingNodeId, curSequenceNumbe
172
176
  const isBootstrappingNode = securityManager.tempKeys.has(sendingNodeId);
173
177
  if (isBootstrappingNode) {
174
178
  // We're currently bootstrapping the node, it might be using a temporary key
179
+ // eslint-disable-next-line @typescript-eslint/no-deprecated
175
180
  securityManager.initializeTempSPAN(sendingNodeId, senderEI, receiverEI);
176
181
  const ret = getNonceAndDecrypt();
177
182
  // Decryption with the temporary key worked
@@ -203,6 +208,7 @@ function decryptSinglecast(ctx, securityManager, sendingNodeId, curSequenceNumbe
203
208
  continue;
204
209
  }
205
210
  // Initialize an SPAN with that security class
211
+ // eslint-disable-next-line @typescript-eslint/no-deprecated
206
212
  securityManager.initializeSPAN(sendingNodeId, secClass, senderEI, receiverEI);
207
213
  const ret = getNonceAndDecrypt();
208
214
  // It worked, return the result
@@ -230,17 +236,212 @@ function decryptSinglecast(ctx, securityManager, sendingNodeId, curSequenceNumbe
230
236
  securityClass: undefined,
231
237
  };
232
238
  }
233
- function decryptMulticast(sendingNodeId, securityManager, groupId, ciphertext, authData, authTag) {
239
+ async function decryptSinglecastAsync(ctx, securityManager, sendingNodeId, curSequenceNumber, prevSequenceNumber, ciphertext, authData, authTag, spanState, extensions) {
240
+ const decryptWithNonce = async (nonce) => {
241
+ const { keyCCM: key } = securityManager.getKeysForNode(sendingNodeId);
242
+ const iv = nonce;
243
+ return {
244
+ key,
245
+ iv,
246
+ ...(await decryptAES128CCMAsync(ciphertext, key, iv, authData, authTag)),
247
+ };
248
+ };
249
+ const getNonceAndDecrypt = async () => {
250
+ const iv = await securityManager.nextNonceAsync(sendingNodeId);
251
+ return decryptWithNonce(iv);
252
+ };
253
+ if (spanState.type === SPANState.SPAN) {
254
+ // There SHOULD be a shared SPAN between both parties. But experience has shown that both could have
255
+ // sent a command at roughly the same time, using the same SPAN for encryption.
256
+ // To avoid a nasty desync and both nodes trying to resync at the same time, causing message loss,
257
+ // we accept commands encrypted with the previous SPAN under very specific circumstances:
258
+ if (
259
+ // The previous SPAN is still known, i.e. the node didn't send another command that was successfully decrypted
260
+ !!spanState.currentSPAN
261
+ // it is still valid
262
+ && spanState.currentSPAN.expires > highResTimestamp()
263
+ // The received command is exactly the next, expected one
264
+ && prevSequenceNumber != undefined
265
+ && curSequenceNumber === ((prevSequenceNumber + 1) & 0xff)
266
+ // And in case of a mock-based test, do this only on the controller
267
+ && !ctx.__internalIsMockNode) {
268
+ const nonce = spanState.currentSPAN.nonce;
269
+ spanState.currentSPAN = undefined;
270
+ // If we could decrypt this way, we're done...
271
+ const result = await decryptWithNonce(nonce);
272
+ if (result.authOK) {
273
+ return {
274
+ ...result,
275
+ securityClass: spanState.securityClass,
276
+ };
277
+ }
278
+ // ...otherwise, we need to try the normal way
279
+ }
280
+ else {
281
+ // forgetting the current SPAN shouldn't be necessary but better be safe than sorry
282
+ spanState.currentSPAN = undefined;
283
+ }
284
+ // This can only happen if the security class is known
285
+ return {
286
+ ...(await getNonceAndDecrypt()),
287
+ securityClass: spanState.securityClass,
288
+ };
289
+ }
290
+ else if (spanState.type === SPANState.LocalEI) {
291
+ // We've sent the other our receiver's EI and received its sender's EI,
292
+ // meaning we can now establish an SPAN
293
+ const senderEI = getSenderEI(extensions);
294
+ if (!senderEI)
295
+ failNoSPAN();
296
+ const receiverEI = spanState.receiverEI;
297
+ // How we do this depends on whether we know the security class of the other node
298
+ const isBootstrappingNode = securityManager.tempKeys.has(sendingNodeId);
299
+ if (isBootstrappingNode) {
300
+ // We're currently bootstrapping the node, it might be using a temporary key
301
+ await securityManager.initializeTempSPANAsync(sendingNodeId, senderEI, receiverEI);
302
+ const ret = await getNonceAndDecrypt();
303
+ // Decryption with the temporary key worked
304
+ if (ret.authOK) {
305
+ return {
306
+ ...ret,
307
+ securityClass: SecurityClass.Temporary,
308
+ };
309
+ }
310
+ // Reset the SPAN state and try with the recently granted security class
311
+ securityManager.setSPANState(sendingNodeId, spanState);
312
+ }
313
+ // When ending up here, one of two situations has occured:
314
+ // a) We've taken over an existing network and do not know the node's security class
315
+ // b) We know the security class, but we're about to establish a new SPAN. This may happen at a lower
316
+ // security class than the one the node normally uses, e.g. when we're being queried for securely
317
+ // supported CCs.
318
+ // In both cases, we should simply try decoding with multiple security classes, starting from the highest one.
319
+ // If this fails, we restore the previous (partial) SPAN state.
320
+ // Try all security classes where we do not definitely know that it was not granted
321
+ // While bootstrapping a node, we consider the key that is being exchanged (including S0) to be the highest. No need to look at others
322
+ const possibleSecurityClasses = isBootstrappingNode
323
+ ? [ctx.getHighestSecurityClass(sendingNodeId)]
324
+ : securityClassOrder.filter((s) => ctx.hasSecurityClass(sendingNodeId, s)
325
+ !== false);
326
+ for (const secClass of possibleSecurityClasses) {
327
+ // Skip security classes we don't have keys for
328
+ if (!securityManager.hasKeysForSecurityClass(secClass)) {
329
+ continue;
330
+ }
331
+ // Initialize an SPAN with that security class
332
+ await securityManager.initializeSPANAsync(sendingNodeId, secClass, senderEI, receiverEI);
333
+ const ret = await getNonceAndDecrypt();
334
+ // It worked, return the result
335
+ if (ret.authOK) {
336
+ // Also if we weren't sure before, we now know that the security class is granted
337
+ if (ctx.hasSecurityClass(sendingNodeId, secClass)
338
+ === undefined) {
339
+ ctx.setSecurityClass(sendingNodeId, secClass, true);
340
+ }
341
+ return {
342
+ ...ret,
343
+ securityClass: secClass,
344
+ };
345
+ }
346
+ else {
347
+ // Reset the SPAN state and try with the next security class
348
+ securityManager.setSPANState(sendingNodeId, spanState);
349
+ }
350
+ }
351
+ }
352
+ // Nothing worked, fail the decryption
353
+ return {
354
+ plaintext: new Uint8Array(),
355
+ authOK: false,
356
+ securityClass: undefined,
357
+ };
358
+ }
359
+ /** @deprecated Use {@link decryptMulticastAsync} instead */
360
+ function decryptMulticastSync(sendingNodeId, securityManager, groupId, ciphertext, authData, authTag) {
361
+ // eslint-disable-next-line @typescript-eslint/no-deprecated
234
362
  const iv = securityManager.nextPeerMPAN(sendingNodeId, groupId);
235
363
  const { keyCCM: key } = securityManager.getKeysForNode(sendingNodeId);
236
364
  return {
237
365
  key,
238
366
  iv,
239
- ...decryptAES128CCM(key, iv, ciphertext, authData, authTag),
367
+ // eslint-disable-next-line @typescript-eslint/no-deprecated
368
+ ...decryptAES128CCMSync(key, iv, ciphertext, authData, authTag),
369
+ // The security class is irrelevant when decrypting multicast commands
370
+ securityClass: undefined,
371
+ };
372
+ }
373
+ async function decryptMulticastAsync(sendingNodeId, securityManager, groupId, ciphertext, authData, authTag) {
374
+ const iv = await securityManager.nextPeerMPANAsync(sendingNodeId, groupId);
375
+ const { keyCCM: key } = securityManager.getKeysForNode(sendingNodeId);
376
+ return {
377
+ key,
378
+ iv,
379
+ ...(await decryptAES128CCMAsync(ciphertext, key, iv, authData, authTag)),
240
380
  // The security class is irrelevant when decrypting multicast commands
241
381
  securityClass: undefined,
242
382
  };
243
383
  }
384
+ function parseExtensions(buffer, wasEncrypted) {
385
+ const extensions = [];
386
+ let mustDiscardCommand = false;
387
+ let offset = 0;
388
+ parsing: while (true) {
389
+ if (buffer.length < offset + 2) {
390
+ // An S2 extension was expected, but the buffer is too short
391
+ mustDiscardCommand = true;
392
+ break parsing;
393
+ }
394
+ // The length field could be too large, which would cause part of the actual ciphertext
395
+ // to be ignored. Try to avoid this for known extensions by checking the actual and expected length.
396
+ const { actual: actualLength, expected: expectedLength } = Security2Extension
397
+ .getExtensionLength(buffer.subarray(offset));
398
+ // Parse the extension using the expected length if possible
399
+ const extensionLength = expectedLength ?? actualLength;
400
+ if (extensionLength < 2) {
401
+ // An S2 extension was expected, but the length is too short
402
+ mustDiscardCommand = true;
403
+ break parsing;
404
+ }
405
+ else if (extensionLength
406
+ > buffer.length
407
+ - offset
408
+ - (wasEncrypted
409
+ ? 0
410
+ : SECURITY_S2_AUTH_TAG_LENGTH)) {
411
+ // The supposed length is longer than the space the extensions may occupy
412
+ mustDiscardCommand = true;
413
+ break parsing;
414
+ }
415
+ const extensionData = buffer.subarray(offset, offset + extensionLength);
416
+ offset += extensionLength;
417
+ const ext = Security2Extension.parse(extensionData);
418
+ switch (validateS2Extension(ext, wasEncrypted)) {
419
+ case ValidateS2ExtensionResult.OK:
420
+ if (expectedLength != undefined
421
+ && actualLength !== expectedLength) {
422
+ // The extension length field does not match, ignore the extension
423
+ }
424
+ else {
425
+ extensions.push(ext);
426
+ }
427
+ break;
428
+ case ValidateS2ExtensionResult.DiscardExtension:
429
+ // Do nothing
430
+ break;
431
+ case ValidateS2ExtensionResult.DiscardCommand:
432
+ mustDiscardCommand = true;
433
+ break;
434
+ }
435
+ // Check if that was the last extension
436
+ if (!ext.moreToFollow)
437
+ break parsing;
438
+ }
439
+ return {
440
+ extensions,
441
+ mustDiscardCommand,
442
+ bytesRead: offset,
443
+ };
444
+ }
244
445
  function getDestinationIDTX() {
245
446
  if (this.isSinglecast())
246
447
  return this.nodeId;
@@ -302,7 +503,7 @@ let Security2CCAPI = (() => {
302
503
  if (!securityManager) {
303
504
  throw new ZWaveError(`Nonces can only be sent if secure communication is set up!`, ZWaveErrorCodes.Driver_NoSecurity);
304
505
  }
305
- const receiverEI = securityManager.generateNonce(this.endpoint.nodeId);
506
+ const receiverEI = await securityManager.generateNonceAsync(this.endpoint.nodeId);
306
507
  const cc = new Security2CCNonceReport({
307
508
  nodeId: this.endpoint.nodeId,
308
509
  endpointIndex: this.endpoint.index,
@@ -935,61 +1136,12 @@ let Security2CCMessageEncapsulation = (() => {
935
1136
  let offset = 2;
936
1137
  const extensions = [];
937
1138
  let mustDiscardCommand = false;
938
- const parseExtensions = (buffer, wasEncrypted) => {
939
- while (true) {
940
- if (buffer.length < offset + 2) {
941
- // An S2 extension was expected, but the buffer is too short
942
- mustDiscardCommand = true;
943
- return;
944
- }
945
- // The length field could be too large, which would cause part of the actual ciphertext
946
- // to be ignored. Try to avoid this for known extensions by checking the actual and expected length.
947
- const { actual: actualLength, expected: expectedLength } = Security2Extension
948
- .getExtensionLength(buffer.subarray(offset));
949
- // Parse the extension using the expected length if possible
950
- const extensionLength = expectedLength ?? actualLength;
951
- if (extensionLength < 2) {
952
- // An S2 extension was expected, but the length is too short
953
- mustDiscardCommand = true;
954
- return;
955
- }
956
- else if (extensionLength
957
- > buffer.length
958
- - offset
959
- - (wasEncrypted
960
- ? 0
961
- : SECURITY_S2_AUTH_TAG_LENGTH)) {
962
- // The supposed length is longer than the space the extensions may occupy
963
- mustDiscardCommand = true;
964
- return;
965
- }
966
- const extensionData = buffer.subarray(offset, offset + extensionLength);
967
- offset += extensionLength;
968
- const ext = Security2Extension.parse(extensionData);
969
- switch (validateS2Extension(ext, wasEncrypted)) {
970
- case ValidateS2ExtensionResult.OK:
971
- if (expectedLength != undefined
972
- && actualLength !== expectedLength) {
973
- // The extension length field does not match, ignore the extension
974
- }
975
- else {
976
- extensions.push(ext);
977
- }
978
- break;
979
- case ValidateS2ExtensionResult.DiscardExtension:
980
- // Do nothing
981
- break;
982
- case ValidateS2ExtensionResult.DiscardCommand:
983
- mustDiscardCommand = true;
984
- break;
985
- }
986
- // Check if that was the last extension
987
- if (!ext.moreToFollow)
988
- break;
989
- }
990
- };
991
- if (hasExtensions)
992
- parseExtensions(raw.payload, false);
1139
+ if (hasExtensions) {
1140
+ const parseResult = parseExtensions(raw.payload.subarray(offset), false);
1141
+ extensions.push(...parseResult.extensions);
1142
+ offset += parseResult.bytesRead;
1143
+ mustDiscardCommand = parseResult.mustDiscardCommand;
1144
+ }
993
1145
  const mcctx = (() => {
994
1146
  const multicastGroupId = getMulticastGroupId(extensions);
995
1147
  if (ctx.frameType === "multicast" || ctx.frameType === "broadcast") {
@@ -1009,9 +1161,11 @@ let Security2CCMessageEncapsulation = (() => {
1009
1161
  // we still need to increment the SPAN or MPAN state
1010
1162
  if (mustDiscardCommand) {
1011
1163
  if (mcctx.isMulticast) {
1164
+ // eslint-disable-next-line @typescript-eslint/no-deprecated
1012
1165
  securityManager.nextPeerMPAN(ctx.sourceNodeId, mcctx.groupId);
1013
1166
  }
1014
1167
  else {
1168
+ // eslint-disable-next-line @typescript-eslint/no-deprecated
1015
1169
  securityManager.nextNonce(ctx.sourceNodeId);
1016
1170
  }
1017
1171
  validatePayload.fail("Invalid S2 extension");
@@ -1044,7 +1198,9 @@ let Security2CCMessageEncapsulation = (() => {
1044
1198
  securityManager.storePeerMPAN(ctx.sourceNodeId, mcctx.groupId, { type: MPANState.OutOfSync });
1045
1199
  failNoMPAN();
1046
1200
  }
1047
- decrypt = () => decryptMulticast(ctx.sourceNodeId, securityManager, mcctx.groupId, ciphertext, authData, authTag);
1201
+ decrypt = () =>
1202
+ // eslint-disable-next-line @typescript-eslint/no-deprecated
1203
+ decryptMulticastSync(ctx.sourceNodeId, securityManager, mcctx.groupId, ciphertext, authData, authTag);
1048
1204
  }
1049
1205
  else {
1050
1206
  // Decrypt payload and verify integrity
@@ -1058,7 +1214,9 @@ let Security2CCMessageEncapsulation = (() => {
1058
1214
  // For now, do the same as if we didn't have any EI
1059
1215
  failNoSPAN();
1060
1216
  }
1061
- decrypt = () => decryptSinglecast(ctx, securityManager, ctx.sourceNodeId, sequenceNumber, prevSequenceNumber, ciphertext, authData, authTag, spanState, extensions);
1217
+ decrypt = () =>
1218
+ // eslint-disable-next-line @typescript-eslint/no-deprecated
1219
+ decryptSinglecastSync(ctx, securityManager, ctx.sourceNodeId, sequenceNumber, prevSequenceNumber, ciphertext, authData, authTag, spanState, extensions);
1062
1220
  }
1063
1221
  let plaintext;
1064
1222
  let authOK = false;
@@ -1109,8 +1267,12 @@ let Security2CCMessageEncapsulation = (() => {
1109
1267
  // Remember which security class was used to decrypt this message, so we can discard it later
1110
1268
  const securityClass = decryptionSecurityClass;
1111
1269
  offset = 0;
1112
- if (hasEncryptedExtensions)
1113
- parseExtensions(plaintext, true);
1270
+ if (hasEncryptedExtensions) {
1271
+ const parseResult = parseExtensions(plaintext, true);
1272
+ extensions.push(...parseResult.extensions);
1273
+ offset += parseResult.bytesRead;
1274
+ mustDiscardCommand = parseResult.mustDiscardCommand;
1275
+ }
1114
1276
  // Before we can continue, check if the command must be discarded
1115
1277
  if (mustDiscardCommand) {
1116
1278
  validatePayload.fail("Invalid extension");
@@ -1162,61 +1324,12 @@ let Security2CCMessageEncapsulation = (() => {
1162
1324
  let offset = 2;
1163
1325
  const extensions = [];
1164
1326
  let mustDiscardCommand = false;
1165
- const parseExtensions = (buffer, wasEncrypted) => {
1166
- while (true) {
1167
- if (buffer.length < offset + 2) {
1168
- // An S2 extension was expected, but the buffer is too short
1169
- mustDiscardCommand = true;
1170
- return;
1171
- }
1172
- // The length field could be too large, which would cause part of the actual ciphertext
1173
- // to be ignored. Try to avoid this for known extensions by checking the actual and expected length.
1174
- const { actual: actualLength, expected: expectedLength } = Security2Extension
1175
- .getExtensionLength(buffer.subarray(offset));
1176
- // Parse the extension using the expected length if possible
1177
- const extensionLength = expectedLength ?? actualLength;
1178
- if (extensionLength < 2) {
1179
- // An S2 extension was expected, but the length is too short
1180
- mustDiscardCommand = true;
1181
- return;
1182
- }
1183
- else if (extensionLength
1184
- > buffer.length
1185
- - offset
1186
- - (wasEncrypted
1187
- ? 0
1188
- : SECURITY_S2_AUTH_TAG_LENGTH)) {
1189
- // The supposed length is longer than the space the extensions may occupy
1190
- mustDiscardCommand = true;
1191
- return;
1192
- }
1193
- const extensionData = buffer.subarray(offset, offset + extensionLength);
1194
- offset += extensionLength;
1195
- const ext = Security2Extension.parse(extensionData);
1196
- switch (validateS2Extension(ext, wasEncrypted)) {
1197
- case ValidateS2ExtensionResult.OK:
1198
- if (expectedLength != undefined
1199
- && actualLength !== expectedLength) {
1200
- // The extension length field does not match, ignore the extension
1201
- }
1202
- else {
1203
- extensions.push(ext);
1204
- }
1205
- break;
1206
- case ValidateS2ExtensionResult.DiscardExtension:
1207
- // Do nothing
1208
- break;
1209
- case ValidateS2ExtensionResult.DiscardCommand:
1210
- mustDiscardCommand = true;
1211
- break;
1212
- }
1213
- // Check if that was the last extension
1214
- if (!ext.moreToFollow)
1215
- break;
1216
- }
1217
- };
1218
- if (hasExtensions)
1219
- parseExtensions(raw.payload, false);
1327
+ if (hasExtensions) {
1328
+ const parseResult = parseExtensions(raw.payload.subarray(offset), false);
1329
+ extensions.push(...parseResult.extensions);
1330
+ offset += parseResult.bytesRead;
1331
+ mustDiscardCommand = parseResult.mustDiscardCommand;
1332
+ }
1220
1333
  const mcctx = (() => {
1221
1334
  const multicastGroupId = getMulticastGroupId(extensions);
1222
1335
  if (ctx.frameType === "multicast" || ctx.frameType === "broadcast") {
@@ -1236,10 +1349,10 @@ let Security2CCMessageEncapsulation = (() => {
1236
1349
  // we still need to increment the SPAN or MPAN state
1237
1350
  if (mustDiscardCommand) {
1238
1351
  if (mcctx.isMulticast) {
1239
- securityManager.nextPeerMPAN(ctx.sourceNodeId, mcctx.groupId);
1352
+ await securityManager.nextPeerMPANAsync(ctx.sourceNodeId, mcctx.groupId);
1240
1353
  }
1241
1354
  else {
1242
- securityManager.nextNonce(ctx.sourceNodeId);
1355
+ await securityManager.nextNonceAsync(ctx.sourceNodeId);
1243
1356
  }
1244
1357
  validatePayload.fail("Invalid S2 extension");
1245
1358
  }
@@ -1271,7 +1384,7 @@ let Security2CCMessageEncapsulation = (() => {
1271
1384
  securityManager.storePeerMPAN(ctx.sourceNodeId, mcctx.groupId, { type: MPANState.OutOfSync });
1272
1385
  failNoMPAN();
1273
1386
  }
1274
- decrypt = () => decryptMulticast(ctx.sourceNodeId, securityManager, mcctx.groupId, ciphertext, authData, authTag);
1387
+ decrypt = () => decryptMulticastAsync(ctx.sourceNodeId, securityManager, mcctx.groupId, ciphertext, authData, authTag);
1275
1388
  }
1276
1389
  else {
1277
1390
  // Decrypt payload and verify integrity
@@ -1285,7 +1398,7 @@ let Security2CCMessageEncapsulation = (() => {
1285
1398
  // For now, do the same as if we didn't have any EI
1286
1399
  failNoSPAN();
1287
1400
  }
1288
- decrypt = () => decryptSinglecast(ctx, securityManager, ctx.sourceNodeId, sequenceNumber, prevSequenceNumber, ciphertext, authData, authTag, spanState, extensions);
1401
+ decrypt = () => decryptSinglecastAsync(ctx, securityManager, ctx.sourceNodeId, sequenceNumber, prevSequenceNumber, ciphertext, authData, authTag, spanState, extensions);
1289
1402
  }
1290
1403
  let plaintext;
1291
1404
  let authOK = false;
@@ -1310,7 +1423,7 @@ let Security2CCMessageEncapsulation = (() => {
1310
1423
  key,
1311
1424
  iv,
1312
1425
  securityClass: decryptionSecurityClass,
1313
- } = decrypt());
1426
+ } = await decrypt());
1314
1427
  if (!!authOK && !!plaintext)
1315
1428
  break;
1316
1429
  // No need to try further SPANs if we just got the sender's EI
@@ -1336,8 +1449,12 @@ let Security2CCMessageEncapsulation = (() => {
1336
1449
  // Remember which security class was used to decrypt this message, so we can discard it later
1337
1450
  const securityClass = decryptionSecurityClass;
1338
1451
  offset = 0;
1339
- if (hasEncryptedExtensions)
1340
- parseExtensions(plaintext, true);
1452
+ if (hasEncryptedExtensions) {
1453
+ const parseResult = parseExtensions(plaintext, true);
1454
+ extensions.push(...parseResult.extensions);
1455
+ offset += parseResult.bytesRead;
1456
+ mustDiscardCommand = parseResult.mustDiscardCommand;
1457
+ }
1341
1458
  // Before we can continue, check if the command must be discarded
1342
1459
  if (mustDiscardCommand) {
1343
1460
  validatePayload.fail("Invalid extension");
@@ -1415,7 +1532,8 @@ let Security2CCMessageEncapsulation = (() => {
1415
1532
  getMulticastGroupId() {
1416
1533
  return getMulticastGroupId(this.extensions);
1417
1534
  }
1418
- maybeAddSPANExtension(ctx, securityManager) {
1535
+ /** @deprecated Use {@link maybeAddSPANExtensionAsync} instead */
1536
+ maybeAddSPANExtensionSync(ctx, securityManager) {
1419
1537
  if (!this.isSinglecast())
1420
1538
  return;
1421
1539
  const receiverNodeId = this.nodeId;
@@ -1428,12 +1546,14 @@ let Security2CCMessageEncapsulation = (() => {
1428
1546
  else if (spanState.type === SPANState.RemoteEI) {
1429
1547
  // We have the receiver's EI, generate our input and send it over
1430
1548
  // With both, we can create an SPAN
1549
+ // eslint-disable-next-line @typescript-eslint/no-deprecated
1431
1550
  const senderEI = securityManager.generateNonce(undefined);
1432
1551
  const receiverEI = spanState.receiverEI;
1433
1552
  // While bootstrapping a node, prefer the temporary key, unless the
1434
1553
  // specific command specifies a security class
1435
1554
  if (this.securityClass == undefined
1436
1555
  && securityManager.tempKeys.has(receiverNodeId)) {
1556
+ // eslint-disable-next-line @typescript-eslint/no-deprecated
1437
1557
  securityManager.initializeTempSPAN(receiverNodeId, senderEI, receiverEI);
1438
1558
  }
1439
1559
  else {
@@ -1442,6 +1562,7 @@ let Security2CCMessageEncapsulation = (() => {
1442
1562
  if (securityClass == undefined) {
1443
1563
  throw new ZWaveError("No security class defined for this command!", ZWaveErrorCodes.Security2CC_NoSPAN);
1444
1564
  }
1565
+ // eslint-disable-next-line @typescript-eslint/no-deprecated
1445
1566
  securityManager.initializeSPAN(receiverNodeId, securityClass, senderEI, receiverEI);
1446
1567
  }
1447
1568
  // Add or update the SPAN extension
@@ -1455,12 +1576,53 @@ let Security2CCMessageEncapsulation = (() => {
1455
1576
  }
1456
1577
  }
1457
1578
  }
1579
+ async maybeAddSPANExtensionAsync(ctx, securityManager) {
1580
+ if (!this.isSinglecast())
1581
+ return;
1582
+ const receiverNodeId = this.nodeId;
1583
+ const spanState = securityManager.getSPANState(receiverNodeId);
1584
+ if (spanState.type === SPANState.None
1585
+ || spanState.type === SPANState.LocalEI) {
1586
+ // Can't do anything here if we don't have the receiver's EI
1587
+ throw new ZWaveError(`Security S2 CC requires the receiver's nonce to be sent!`, ZWaveErrorCodes.Security2CC_NoSPAN);
1588
+ }
1589
+ else if (spanState.type === SPANState.RemoteEI) {
1590
+ // We have the receiver's EI, generate our input and send it over
1591
+ // With both, we can create an SPAN
1592
+ const senderEI = await securityManager.generateNonceAsync(undefined);
1593
+ const receiverEI = spanState.receiverEI;
1594
+ // While bootstrapping a node, prefer the temporary key, unless the
1595
+ // specific command specifies a security class
1596
+ if (this.securityClass == undefined
1597
+ && securityManager.tempKeys.has(receiverNodeId)) {
1598
+ await securityManager.initializeTempSPANAsync(receiverNodeId, senderEI, receiverEI);
1599
+ }
1600
+ else {
1601
+ const securityClass = this.securityClass
1602
+ ?? ctx.getHighestSecurityClass(receiverNodeId);
1603
+ if (securityClass == undefined) {
1604
+ throw new ZWaveError("No security class defined for this command!", ZWaveErrorCodes.Security2CC_NoSPAN);
1605
+ }
1606
+ await securityManager.initializeSPANAsync(receiverNodeId, securityClass, senderEI, receiverEI);
1607
+ }
1608
+ // Add or update the SPAN extension
1609
+ let spanExtension = this.extensions.find((e) => e instanceof SPANExtension);
1610
+ if (spanExtension) {
1611
+ spanExtension.senderEI = senderEI;
1612
+ }
1613
+ else {
1614
+ spanExtension = new SPANExtension({ senderEI });
1615
+ this.extensions.push(spanExtension);
1616
+ }
1617
+ }
1618
+ }
1458
1619
  /** @deprecated Use {@link serializeAsync} instead */
1459
1620
  serialize(ctx) {
1460
1621
  const securityManager = assertSecurityTX(ctx, this.nodeId);
1461
1622
  this.ensureSequenceNumber(securityManager);
1462
1623
  // Include Sender EI in the command if we only have the receiver's EI
1463
- this.maybeAddSPANExtension(ctx, securityManager);
1624
+ // eslint-disable-next-line @typescript-eslint/no-deprecated
1625
+ this.maybeAddSPANExtensionSync(ctx, securityManager);
1464
1626
  const unencryptedExtensions = this.extensions.filter((e) => !e.isEncrypted());
1465
1627
  const encryptedExtensions = this.extensions.filter((e) => e.isEncrypted());
1466
1628
  const unencryptedPayload = Bytes.concat([
@@ -1489,6 +1651,7 @@ let Security2CCMessageEncapsulation = (() => {
1489
1651
  // Singlecast:
1490
1652
  // Generate a nonce for encryption, and remember it to attempt decryption
1491
1653
  // of potential in-flight messages from the target node.
1654
+ // eslint-disable-next-line @typescript-eslint/no-deprecated
1492
1655
  iv = securityManager.nextNonce(this.nodeId, true);
1493
1656
  const { keyCCM } =
1494
1657
  // Prefer the overridden security class if it was given
@@ -1499,11 +1662,13 @@ let Security2CCMessageEncapsulation = (() => {
1499
1662
  }
1500
1663
  else {
1501
1664
  // Multicast:
1665
+ // eslint-disable-next-line @typescript-eslint/no-deprecated
1502
1666
  const keyAndIV = securityManager.getMulticastKeyAndIV(destinationTag);
1503
1667
  key = keyAndIV.key;
1504
1668
  iv = keyAndIV.iv;
1505
1669
  }
1506
- const { ciphertext: ciphertextPayload, authTag } = encryptAES128CCM(key, iv, plaintextPayload, authData, SECURITY_S2_AUTH_TAG_LENGTH);
1670
+ // eslint-disable-next-line @typescript-eslint/no-deprecated
1671
+ const { ciphertext: ciphertextPayload, authTag } = encryptAES128CCMSync(key, iv, plaintextPayload, authData, SECURITY_S2_AUTH_TAG_LENGTH);
1507
1672
  // Remember key and IV for debugging purposes
1508
1673
  this.key = key;
1509
1674
  this.iv = iv;
@@ -1522,7 +1687,7 @@ let Security2CCMessageEncapsulation = (() => {
1522
1687
  const securityManager = assertSecurityTX(ctx, this.nodeId);
1523
1688
  this.ensureSequenceNumber(securityManager);
1524
1689
  // Include Sender EI in the command if we only have the receiver's EI
1525
- this.maybeAddSPANExtension(ctx, securityManager);
1690
+ await this.maybeAddSPANExtensionAsync(ctx, securityManager);
1526
1691
  const unencryptedExtensions = this.extensions.filter((e) => !e.isEncrypted());
1527
1692
  const encryptedExtensions = this.extensions.filter((e) => e.isEncrypted());
1528
1693
  const unencryptedPayload = Bytes.concat([
@@ -1550,7 +1715,7 @@ let Security2CCMessageEncapsulation = (() => {
1550
1715
  // Singlecast:
1551
1716
  // Generate a nonce for encryption, and remember it to attempt decryption
1552
1717
  // of potential in-flight messages from the target node.
1553
- iv = securityManager.nextNonce(this.nodeId, true);
1718
+ iv = await securityManager.nextNonceAsync(this.nodeId, true);
1554
1719
  const { keyCCM } =
1555
1720
  // Prefer the overridden security class if it was given
1556
1721
  this.securityClass != undefined
@@ -1560,11 +1725,11 @@ let Security2CCMessageEncapsulation = (() => {
1560
1725
  }
1561
1726
  else {
1562
1727
  // Multicast:
1563
- const keyAndIV = securityManager.getMulticastKeyAndIV(destinationTag);
1728
+ const keyAndIV = await securityManager.getMulticastKeyAndIVAsync(destinationTag);
1564
1729
  key = keyAndIV.key;
1565
1730
  iv = keyAndIV.iv;
1566
1731
  }
1567
- const { ciphertext: ciphertextPayload, authTag } = encryptAES128CCM(key, iv, plaintextPayload, authData, SECURITY_S2_AUTH_TAG_LENGTH);
1732
+ const { ciphertext: ciphertextPayload, authTag } = await encryptAES128CCMAsync(plaintextPayload, key, iv, authData, SECURITY_S2_AUTH_TAG_LENGTH);
1568
1733
  // Remember key and IV for debugging purposes
1569
1734
  this.key = key;
1570
1735
  this.iv = iv;