@zwave-js/cc 10.8.0 → 10.11.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.
Files changed (105) hide show
  1. package/build/cc/AlarmSensorCC.d.ts +1 -1
  2. package/build/cc/AlarmSensorCC.d.ts.map +1 -1
  3. package/build/cc/AlarmSensorCC.js +11 -9
  4. package/build/cc/AlarmSensorCC.js.map +1 -1
  5. package/build/cc/AssociationCC.d.ts +4 -4
  6. package/build/cc/AssociationGroupInfoCC.d.ts +4 -4
  7. package/build/cc/BarrierOperatorCC.d.ts +1 -1
  8. package/build/cc/BarrierOperatorCC.d.ts.map +1 -1
  9. package/build/cc/BarrierOperatorCC.js +4 -0
  10. package/build/cc/BarrierOperatorCC.js.map +1 -1
  11. package/build/cc/BasicCC.d.ts +1 -1
  12. package/build/cc/BatteryCC.d.ts +22 -2
  13. package/build/cc/BatteryCC.d.ts.map +1 -1
  14. package/build/cc/BatteryCC.js +58 -20
  15. package/build/cc/BatteryCC.js.map +1 -1
  16. package/build/cc/BinarySensorCC.d.ts +1 -1
  17. package/build/cc/BinarySensorCC.d.ts.map +1 -1
  18. package/build/cc/BinarySensorCC.js +4 -0
  19. package/build/cc/BinarySensorCC.js.map +1 -1
  20. package/build/cc/BinarySwitchCC.d.ts +1 -1
  21. package/build/cc/BinarySwitchCC.d.ts.map +1 -1
  22. package/build/cc/BinarySwitchCC.js.map +1 -1
  23. package/build/cc/CRC16CC.d.ts.map +1 -1
  24. package/build/cc/CRC16CC.js +1 -0
  25. package/build/cc/CRC16CC.js.map +1 -1
  26. package/build/cc/CentralSceneCC.d.ts +3 -3
  27. package/build/cc/ColorSwitchCC.d.ts +2 -2
  28. package/build/cc/ColorSwitchCC.d.ts.map +1 -1
  29. package/build/cc/ColorSwitchCC.js +4 -0
  30. package/build/cc/ColorSwitchCC.js.map +1 -1
  31. package/build/cc/ConfigurationCC.d.ts +2 -2
  32. package/build/cc/DoorLockCC.d.ts +4 -4
  33. package/build/cc/DoorLockCC.js +1 -1
  34. package/build/cc/DoorLockCC.js.map +1 -1
  35. package/build/cc/DoorLockLoggingCC.d.ts +1 -1
  36. package/build/cc/EntryControlCC.d.ts +3 -3
  37. package/build/cc/FirmwareUpdateMetaDataCC.d.ts +4 -4
  38. package/build/cc/HumidityControlModeCC.d.ts +1 -1
  39. package/build/cc/HumidityControlSetpointCC.d.ts +1 -1
  40. package/build/cc/IndicatorCC.d.ts +2 -2
  41. package/build/cc/IndicatorCC.js +1 -1
  42. package/build/cc/IndicatorCC.js.map +1 -1
  43. package/build/cc/IrrigationCC.d.ts +4 -4
  44. package/build/cc/ManufacturerSpecificCC.d.ts +3 -3
  45. package/build/cc/MeterCC.d.ts +4 -4
  46. package/build/cc/MultiChannelAssociationCC.d.ts +4 -4
  47. package/build/cc/MultiChannelCC.d.ts +8 -8
  48. package/build/cc/MultiChannelCC.d.ts.map +1 -1
  49. package/build/cc/MultiChannelCC.js +2 -0
  50. package/build/cc/MultiChannelCC.js.map +1 -1
  51. package/build/cc/MultiCommandCC.d.ts.map +1 -1
  52. package/build/cc/MultiCommandCC.js +3 -2
  53. package/build/cc/MultiCommandCC.js.map +1 -1
  54. package/build/cc/MultilevelSensorCC.d.ts +2 -2
  55. package/build/cc/MultilevelSwitchCC.d.ts +3 -3
  56. package/build/cc/NodeNamingCC.d.ts +2 -2
  57. package/build/cc/NotificationCC.d.ts +5 -5
  58. package/build/cc/NotificationCC.d.ts.map +1 -1
  59. package/build/cc/NotificationCC.js +27 -0
  60. package/build/cc/NotificationCC.js.map +1 -1
  61. package/build/cc/ProtectionCC.d.ts +4 -4
  62. package/build/cc/SceneActivationCC.d.ts +1 -1
  63. package/build/cc/ScheduleEntryLockCC.d.ts +3 -3
  64. package/build/cc/ScheduleEntryLockCC.d.ts.map +1 -1
  65. package/build/cc/ScheduleEntryLockCC.js +12 -10
  66. package/build/cc/ScheduleEntryLockCC.js.map +1 -1
  67. package/build/cc/Security2CC.d.ts +20 -3
  68. package/build/cc/Security2CC.d.ts.map +1 -1
  69. package/build/cc/Security2CC.js +329 -133
  70. package/build/cc/Security2CC.js.map +1 -1
  71. package/build/cc/SupervisionCC.d.ts +1 -1
  72. package/build/cc/SupervisionCC.d.ts.map +1 -1
  73. package/build/cc/SupervisionCC.js +1 -0
  74. package/build/cc/SupervisionCC.js.map +1 -1
  75. package/build/cc/ThermostatFanModeCC.d.ts +1 -1
  76. package/build/cc/ThermostatModeCC.d.ts +1 -1
  77. package/build/cc/ThermostatSetpointCC.d.ts +3 -3
  78. package/build/cc/TransportServiceCC.d.ts +10 -0
  79. package/build/cc/TransportServiceCC.d.ts.map +1 -1
  80. package/build/cc/TransportServiceCC.js +13 -4
  81. package/build/cc/TransportServiceCC.js.map +1 -1
  82. package/build/cc/UserCodeCC.d.ts +11 -11
  83. package/build/cc/VersionCC.d.ts +3 -3
  84. package/build/cc/WakeUpCC.d.ts +2 -2
  85. package/build/cc/ZWavePlusCC.d.ts +5 -5
  86. package/build/index.d.ts +1 -0
  87. package/build/index.d.ts.map +1 -1
  88. package/build/index.js +3 -1
  89. package/build/index.js.map +1 -1
  90. package/build/lib/API.d.ts +4 -2
  91. package/build/lib/API.d.ts.map +1 -1
  92. package/build/lib/API.js +23 -4
  93. package/build/lib/API.js.map +1 -1
  94. package/build/lib/CommandClass.d.ts +12 -4
  95. package/build/lib/CommandClass.d.ts.map +1 -1
  96. package/build/lib/CommandClass.js +44 -15
  97. package/build/lib/CommandClass.js.map +1 -1
  98. package/build/lib/Security2/Extension.d.ts.map +1 -1
  99. package/build/lib/Security2/Extension.js +6 -3
  100. package/build/lib/Security2/Extension.js.map +1 -1
  101. package/build/lib/Values.d.ts +1 -1
  102. package/build/lib/Values.d.ts.map +1 -1
  103. package/build/lib/Values.js +4 -0
  104. package/build/lib/Values.js.map +1 -1
  105. package/package.json +12 -12
@@ -50,7 +50,7 @@ function assertSecurity(options) {
50
50
  throw new core_1.ZWaveError(`Secure commands (S2) can only be ${verb} when the controller's node id is known!`, core_1.ZWaveErrorCodes.Driver_NotReady);
51
51
  }
52
52
  else if (!this.host.securityManager2) {
53
- throw new core_1.ZWaveError(`Secure commands (S2) can only be ${verb} when the network keys for the applHost are set!`, core_1.ZWaveErrorCodes.Driver_NoSecurity);
53
+ throw new core_1.ZWaveError(`Secure commands (S2) can only be ${verb} when the network keys are configured!`, core_1.ZWaveErrorCodes.Driver_NoSecurity);
54
54
  }
55
55
  }
56
56
  const DECRYPT_ATTEMPTS = 5;
@@ -106,6 +106,78 @@ let Security2CCAPI = class Security2CCAPI extends API_1.CCAPI {
106
106
  }
107
107
  return true;
108
108
  }
109
+ /** Notifies the target node that the MPAN state is out of sync */
110
+ async sendMOS() {
111
+ this.assertSupportsCommand(_Types_1.Security2Command, _Types_1.Security2Command.NonceReport);
112
+ this.assertPhysicalEndpoint(this.endpoint);
113
+ const cc = new Security2CCNonceReport(this.applHost, {
114
+ nodeId: this.endpoint.nodeId,
115
+ endpoint: this.endpoint.index,
116
+ SOS: false,
117
+ MOS: true,
118
+ });
119
+ try {
120
+ await this.applHost.sendCommand(cc, {
121
+ ...this.commandOptions,
122
+ // Seems we need these options or some nodes won't accept the nonce
123
+ transmitOptions: core_1.TransmitOptions.ACK | core_1.TransmitOptions.AutoRoute,
124
+ // Only try sending a nonce once
125
+ maxSendAttempts: 1,
126
+ // Nonce requests must be handled immediately
127
+ priority: core_1.MessagePriority.Nonce,
128
+ // We don't want failures causing us to treat the node as asleep or dead
129
+ changeNodeStatusOnMissingACK: false,
130
+ });
131
+ }
132
+ catch (e) {
133
+ if ((0, core_1.isTransmissionError)(e)) {
134
+ return false;
135
+ }
136
+ else {
137
+ // Pass other errors through
138
+ throw e;
139
+ }
140
+ }
141
+ return true;
142
+ }
143
+ /** Sends the given MPAN to the node */
144
+ async sendMPAN(groupId, innerMPANState) {
145
+ this.assertSupportsCommand(_Types_1.Security2Command, _Types_1.Security2Command.MessageEncapsulation);
146
+ this.assertPhysicalEndpoint(this.endpoint);
147
+ const cc = new Security2CCMessageEncapsulation(this.applHost, {
148
+ nodeId: this.endpoint.nodeId,
149
+ endpoint: this.endpoint.index,
150
+ extensions: [
151
+ new Extension_1.MPANExtension({
152
+ groupId,
153
+ innerMPANState,
154
+ }),
155
+ ],
156
+ });
157
+ try {
158
+ await this.applHost.sendCommand(cc, {
159
+ ...this.commandOptions,
160
+ // Seems we need these options or some nodes won't accept the nonce
161
+ transmitOptions: core_1.TransmitOptions.ACK | core_1.TransmitOptions.AutoRoute,
162
+ // Only try sending a nonce once
163
+ maxSendAttempts: 1,
164
+ // Nonce requests must be handled immediately
165
+ priority: core_1.MessagePriority.Nonce,
166
+ // We don't want failures causing us to treat the node as asleep or dead
167
+ changeNodeStatusOnMissingACK: false,
168
+ });
169
+ }
170
+ catch (e) {
171
+ if ((0, core_1.isTransmissionError)(e)) {
172
+ return false;
173
+ }
174
+ else {
175
+ // Pass other errors through
176
+ throw e;
177
+ }
178
+ }
179
+ return true;
180
+ }
109
181
  /**
110
182
  * Queries the securely supported commands for the current security class
111
183
  * @param securityClass Can be used to overwrite the security class to use. If this doesn't match the current one, new nonces will need to be exchanged.
@@ -122,7 +194,7 @@ let Security2CCAPI = class Security2CCAPI extends API_1.CCAPI {
122
194
  if (MultiChannelCC_1.MultiChannelCC.requiresEncapsulation(cc)) {
123
195
  cc = MultiChannelCC_1.MultiChannelCC.encapsulate(this.applHost, cc);
124
196
  }
125
- cc = Security2CC.encapsulate(this.applHost, cc, securityClass);
197
+ cc = Security2CC.encapsulate(this.applHost, cc, { securityClass });
126
198
  const response = await this.applHost.sendCommand(cc, {
127
199
  ...this.commandOptions,
128
200
  autoEncapsulate: false,
@@ -422,11 +494,23 @@ let Security2CC = Security2CC_1 = class Security2CC extends CommandClass_1.Comma
422
494
  return true;
423
495
  }
424
496
  /** Encapsulates a command that should be sent encrypted */
425
- static encapsulate(host, cc, securityClass) {
497
+ static encapsulate(host, cc, options) {
498
+ // Determine which extensions must be used on the command
499
+ const extensions = [];
500
+ if (options?.multicastOutOfSync) {
501
+ extensions.push(new Extension_1.MOSExtension());
502
+ }
503
+ if (options?.multicastGroupId != undefined) {
504
+ extensions.push(new Extension_1.MGRPExtension({ groupId: options.multicastGroupId }));
505
+ }
506
+ // Make sure that S2 multicast uses broadcasts. While the specs mention that both multicast and broadcast
507
+ // are possible, it has been found that devices treat multicasts like singlecast followups and respond incorrectly.
508
+ const nodeId = cc.isMulticast() ? safe_1.NODE_ID_BROADCAST : cc.nodeId;
426
509
  const ret = new Security2CCMessageEncapsulation(host, {
427
- nodeId: cc.nodeId,
510
+ nodeId,
428
511
  encapsulated: cc,
429
- securityClass,
512
+ securityClass: options?.securityClass,
513
+ extensions,
430
514
  });
431
515
  // Copy the encapsulation flags from the encapsulated command
432
516
  // but omit Security, since we're doing that right now
@@ -457,6 +541,12 @@ function testCCResponseForMessageEncapsulation(sent, received) {
457
541
  return received.SOS && !!received.receiverEI;
458
542
  }
459
543
  }
544
+ function failNoSPAN() {
545
+ throw core_1.validatePayload.fail(core_1.ZWaveErrorCodes.Security2CC_NoSPAN);
546
+ }
547
+ function failNoMPAN() {
548
+ throw core_1.validatePayload.fail(core_1.ZWaveErrorCodes.Security2CC_NoMPAN);
549
+ }
460
550
  let Security2CCMessageEncapsulation = class Security2CCMessageEncapsulation extends Security2CC {
461
551
  constructor(host, options) {
462
552
  super(host, options);
@@ -465,14 +555,9 @@ let Security2CCMessageEncapsulation = class Security2CCMessageEncapsulation exte
465
555
  if ((0, CommandClass_1.gotDeserializationOptions)(options)) {
466
556
  (0, core_1.validatePayload)(this.payload.length >= 2);
467
557
  // Check the sequence number to avoid duplicates
468
- // TODO: distinguish between multicast and singlecast
469
558
  this._sequenceNumber = this.payload[0];
470
559
  const sendingNodeId = this.nodeId;
471
- // Don't accept duplicate commands
472
- const prevSequenceNumber = validateSequenceNumber.call(this, this._sequenceNumber);
473
560
  // Ensure the node has a security class
474
- // const node = this.getNode()!;
475
- // validatePayload.withReason("The node is not included")(!!node);
476
561
  const securityClass = this.host.getHighestSecurityClass(sendingNodeId);
477
562
  core_1.validatePayload.withReason("No security class granted")(securityClass !== core_1.SecurityClass.None);
478
563
  const hasExtensions = !!(this.payload[1] & 0b1);
@@ -495,111 +580,66 @@ let Security2CCMessageEncapsulation = class Security2CCMessageEncapsulation exte
495
580
  };
496
581
  if (hasExtensions)
497
582
  parseExtensions(this.payload);
583
+ const ctx = (() => {
584
+ const multicastGroupId = this.getMulticastGroupId();
585
+ if (options.frameType === "multicast" ||
586
+ options.frameType === "broadcast") {
587
+ if (multicastGroupId == undefined) {
588
+ throw core_1.validatePayload.fail("Multicast frames without MGRP extension");
589
+ }
590
+ return {
591
+ isMulticast: true,
592
+ groupId: multicastGroupId,
593
+ };
594
+ }
595
+ else {
596
+ return { isMulticast: false, groupId: multicastGroupId };
597
+ }
598
+ })();
599
+ let prevSequenceNumber;
600
+ let mpanState;
601
+ if (ctx.isMulticast) {
602
+ mpanState = this.host.securityManager2.getPeerMPAN(sendingNodeId, ctx.groupId);
603
+ }
604
+ else {
605
+ // Don't accept duplicate Singlecast commands
606
+ prevSequenceNumber = validateSequenceNumber.call(this, this._sequenceNumber);
607
+ // When a node removes a singlecast message after a multicast group was marked out of sync,
608
+ // it must forget about the group.
609
+ if (ctx.groupId == undefined) {
610
+ this.host.securityManager2.resetOutOfSyncMPANs(sendingNodeId);
611
+ }
612
+ }
498
613
  const unencryptedPayload = this.payload.slice(0, offset);
499
614
  const ciphertext = this.payload.slice(offset, -core_1.SECURITY_S2_AUTH_TAG_LENGTH);
500
615
  const authTag = this.payload.slice(-core_1.SECURITY_S2_AUTH_TAG_LENGTH);
616
+ this.authTag = authTag;
501
617
  const messageLength = super.computeEncapsulationOverhead() + this.payload.length;
502
618
  const authData = getAuthenticationData(sendingNodeId, this.getDestinationIDRX(), this.host.homeId, messageLength, unencryptedPayload);
503
- // Decrypt payload and verify integrity
504
- const spanState = this.host.securityManager2.getSPANState(sendingNodeId);
505
- const failNoSPAN = () => {
506
- return core_1.validatePayload.fail(core_1.ZWaveErrorCodes.Security2CC_NoSPAN);
507
- };
508
- // If we are not able to establish an SPAN yet, fail the decryption
509
- if (spanState.type === core_1.SPANState.None) {
510
- return failNoSPAN();
511
- }
512
- else if (spanState.type === core_1.SPANState.RemoteEI) {
513
- // TODO: The specs are not clear how to handle this case
514
- // For now, do the same as if we didn't have any EI
515
- return failNoSPAN();
619
+ let decrypt;
620
+ if (ctx.isMulticast) {
621
+ // For incoming multicast commands, make sure we have an MPAN
622
+ if (mpanState?.type !== core_1.MPANState.MPAN) {
623
+ // If we don't, mark the MPAN as out of sync, so we can respond accordingly on the singlecast followup
624
+ this.host.securityManager2.storePeerMPAN(sendingNodeId, ctx.groupId, { type: core_1.MPANState.OutOfSync });
625
+ throw failNoMPAN();
626
+ }
627
+ decrypt = () => this.decryptMulticast(sendingNodeId, ctx.groupId, ciphertext, authData, authTag);
516
628
  }
517
- const decrypt = () => {
518
- const decryptWithNonce = (nonce) => {
519
- const { keyCCM: key } = this.host.securityManager2.getKeysForNode(sendingNodeId);
520
- const iv = nonce;
521
- return {
522
- key,
523
- iv,
524
- ...(0, core_1.decryptAES128CCM)(key, iv, ciphertext, authData, authTag),
525
- };
526
- };
527
- const getNonceAndDecrypt = () => {
528
- const iv = this.host.securityManager2.nextNonce(sendingNodeId);
529
- return decryptWithNonce(iv);
530
- };
531
- if (spanState.type === core_1.SPANState.SPAN) {
532
- // There SHOULD be a shared SPAN between both parties. But experience has shown that both could have
533
- // sent a command at roughly the same time, using the same SPAN for encryption.
534
- // To avoid a nasty desync and both nodes trying to resync at the same time, causing message loss,
535
- // we accept commands encrypted with the previous SPAN under very specific circumstances:
536
- if (
537
- // The previous SPAN is still known, i.e. the node didn't send another command that was successfully decrypted
538
- !!spanState.currentSPAN &&
539
- // it is still valid
540
- spanState.currentSPAN.expires > (0, core_1.highResTimestamp)() &&
541
- // The received command is exactly the next, expected one
542
- prevSequenceNumber != undefined &&
543
- this._sequenceNumber ===
544
- ((prevSequenceNumber + 1) & 0xff) &&
545
- // And in case of a mock-based test, do this only on the controller
546
- !this.host.__internalIsMockNode) {
547
- const nonce = spanState.currentSPAN.nonce;
548
- spanState.currentSPAN = undefined;
549
- return decryptWithNonce(nonce);
550
- }
551
- else {
552
- // forgetting the current SPAN shouldn't be necessary but better be safe than sorry
553
- spanState.currentSPAN = undefined;
554
- }
555
- // This can only happen if the security class is known
556
- return getNonceAndDecrypt();
629
+ else {
630
+ // Decrypt payload and verify integrity
631
+ const spanState = this.host.securityManager2.getSPANState(sendingNodeId);
632
+ // If we are not able to establish an SPAN yet, fail the decryption
633
+ if (spanState.type === core_1.SPANState.None) {
634
+ throw failNoSPAN();
557
635
  }
558
- else if (spanState.type === core_1.SPANState.LocalEI) {
559
- // We've sent the other our receiver's EI and received its sender's EI,
560
- // meaning we can now establish an SPAN
561
- const senderEI = this.getSenderEI();
562
- if (!senderEI)
563
- return failNoSPAN();
564
- const receiverEI = spanState.receiverEI;
565
- // How we do this depends on whether we know the security class of the other node
566
- if (this.host.securityManager2.tempKeys.has(sendingNodeId)) {
567
- // We're currently bootstrapping the node, it might be using a temporary key
568
- this.host.securityManager2.initializeTempSPAN(sendingNodeId, senderEI, receiverEI);
569
- const ret = getNonceAndDecrypt();
570
- // Decryption with the temporary key worked
571
- if (ret.authOK)
572
- return ret;
573
- // Reset the SPAN state and try with the recently granted security class
574
- this.host.securityManager2.setSPANState(sendingNodeId, spanState);
575
- }
576
- if (securityClass != undefined) {
577
- this.host.securityManager2.initializeSPAN(sendingNodeId, securityClass, senderEI, receiverEI);
578
- return getNonceAndDecrypt();
579
- }
580
- else {
581
- // Not knowing it can happen if we just took over an existing network
582
- // Try multiple security classes
583
- const possibleSecurityClasses = core_1.securityClassOrder
584
- .filter((s) => (0, core_1.securityClassIsS2)(s))
585
- .filter((s) => this.host.hasSecurityClass(sendingNodeId, s) !== false);
586
- for (const secClass of possibleSecurityClasses) {
587
- // Initialize an SPAN with that security class
588
- this.host.securityManager2.initializeSPAN(sendingNodeId, secClass, senderEI, receiverEI);
589
- const ret = getNonceAndDecrypt();
590
- // It worked, return the result and remember the security class
591
- if (ret.authOK) {
592
- this.host.setSecurityClass(sendingNodeId, secClass, true);
593
- return ret;
594
- }
595
- // Reset the SPAN state and try with the next security class
596
- this.host.securityManager2.setSPANState(sendingNodeId, spanState);
597
- }
598
- }
636
+ else if (spanState.type === core_1.SPANState.RemoteEI) {
637
+ // TODO: The specs are not clear how to handle this case
638
+ // For now, do the same as if we didn't have any EI
639
+ throw failNoSPAN();
599
640
  }
600
- // Nothing worked, fail the decryption
601
- return { plaintext: Buffer.from([]), authOK: false };
602
- };
641
+ decrypt = () => this.decryptSinglecast(sendingNodeId, prevSequenceNumber, ciphertext, authData, authTag, securityClass, spanState);
642
+ }
603
643
  let plaintext;
604
644
  let authOK = false;
605
645
  let key;
@@ -615,7 +655,18 @@ let Security2CCMessageEncapsulation = class Security2CCMessageEncapsulation exte
615
655
  // If authentication fails, do so with an error code that instructs the
616
656
  // applHost to tell the node we have no nonce
617
657
  if (!authOK || !plaintext) {
618
- return core_1.validatePayload.fail(core_1.ZWaveErrorCodes.Security2CC_CannotDecode);
658
+ if (ctx.isMulticast) {
659
+ // Mark the MPAN as out of sync
660
+ this.host.securityManager2.storePeerMPAN(sendingNodeId, ctx.groupId, { type: core_1.MPANState.OutOfSync });
661
+ throw core_1.validatePayload.fail(core_1.ZWaveErrorCodes.Security2CC_CannotDecodeMulticast);
662
+ }
663
+ else {
664
+ throw core_1.validatePayload.fail(core_1.ZWaveErrorCodes.Security2CC_CannotDecode);
665
+ }
666
+ }
667
+ else if (!ctx.isMulticast && ctx.groupId != undefined) {
668
+ // After reception of a singlecast followup, the MPAN state must be increased
669
+ this.host.securityManager2.tryIncrementPeerMPAN(sendingNodeId, ctx.groupId);
619
670
  }
620
671
  offset = 0;
621
672
  if (hasEncryptedExtensions)
@@ -630,15 +681,21 @@ let Security2CCMessageEncapsulation = class Security2CCMessageEncapsulation exte
630
681
  data: decryptedCCBytes,
631
682
  fromEncapsulation: true,
632
683
  encapCC: this,
684
+ frameType: options.frameType,
633
685
  });
634
686
  }
635
687
  this.key = key;
636
688
  this.iv = iv;
637
689
  }
638
690
  else {
691
+ if (!options.encapsulated && !options.extensions?.length) {
692
+ throw new core_1.ZWaveError("Security S2 encapsulation requires an encapsulated CC and/or extensions", core_1.ZWaveErrorCodes.Argument_Invalid);
693
+ }
639
694
  this._securityClass = options.securityClass;
640
- this.encapsulated = options.encapsulated;
641
- options.encapsulated.encapsulatingCC = this;
695
+ if (options.encapsulated) {
696
+ this.encapsulated = options.encapsulated;
697
+ options.encapsulated.encapsulatingCC = this;
698
+ }
642
699
  this.extensions = options.extensions ?? [];
643
700
  if (typeof this.nodeId !== "number" &&
644
701
  !this.extensions.some((e) => e instanceof Extension_1.MGRPExtension)) {
@@ -654,37 +711,51 @@ let Security2CCMessageEncapsulation = class Security2CCMessageEncapsulation exte
654
711
  */
655
712
  get sequenceNumber() {
656
713
  if (this._sequenceNumber == undefined) {
657
- this._sequenceNumber =
658
- this.host.securityManager2.nextSequenceNumber(this.nodeId);
714
+ if (this.isSinglecast()) {
715
+ this._sequenceNumber =
716
+ this.host.securityManager2.nextSequenceNumber(this.nodeId);
717
+ }
718
+ else {
719
+ const groupId = this.getDestinationIDTX();
720
+ return this.host.securityManager2.nextMulticastSequenceNumber(groupId);
721
+ }
659
722
  }
660
723
  return this._sequenceNumber;
661
724
  }
662
- unsetSequenceNumber() {
725
+ prepareRetransmission() {
726
+ super.prepareRetransmission();
663
727
  this._sequenceNumber = undefined;
664
728
  }
665
729
  getDestinationIDTX() {
730
+ if (this.isSinglecast())
731
+ return this.nodeId;
666
732
  const mgrpExtension = this.extensions.find((e) => e instanceof Extension_1.MGRPExtension);
667
733
  if (mgrpExtension)
668
734
  return mgrpExtension.groupId;
669
- else if (typeof this.nodeId === "number")
670
- return this.nodeId;
671
735
  throw new core_1.ZWaveError("Multicast Security S2 encapsulation requires the MGRP extension", core_1.ZWaveErrorCodes.Security2CC_MissingExtension);
672
736
  }
673
737
  getDestinationIDRX() {
674
- const mgrpExtension = this.extensions.find((e) => e instanceof Extension_1.MGRPExtension);
675
- if (mgrpExtension)
676
- return mgrpExtension.groupId;
677
- return this.host.ownNodeId;
738
+ return this.getMulticastGroupId() ?? this.host.ownNodeId;
739
+ }
740
+ getMGRPExtension() {
741
+ return this.extensions.find((e) => e instanceof Extension_1.MGRPExtension);
742
+ }
743
+ getMulticastGroupId() {
744
+ const mgrpExtension = this.getMGRPExtension();
745
+ return mgrpExtension?.groupId;
746
+ }
747
+ hasMOSExtension() {
748
+ return this.extensions.some((e) => e instanceof Extension_1.MOSExtension);
678
749
  }
679
750
  /** Returns the Sender's Entropy Input if this command contains an SPAN extension */
680
751
  getSenderEI() {
681
752
  const spanExtension = this.extensions.find((e) => e instanceof Extension_1.SPANExtension);
682
753
  return spanExtension?.senderEI;
683
754
  }
684
- serialize() {
685
- // TODO: Support Multicast
686
- // Include Sender EI in the command if we only have the receiver's EI
687
- const receiverNodeId = this.getDestinationIDTX();
755
+ maybeAddSPANExtension() {
756
+ if (!this.isSinglecast())
757
+ return;
758
+ const receiverNodeId = this.nodeId;
688
759
  const spanState = this.host.securityManager2.getSPANState(receiverNodeId);
689
760
  if (spanState.type === core_1.SPANState.None ||
690
761
  spanState.type === core_1.SPANState.LocalEI) {
@@ -719,6 +790,10 @@ let Security2CCMessageEncapsulation = class Security2CCMessageEncapsulation exte
719
790
  this.extensions.push(spanExtension);
720
791
  }
721
792
  }
793
+ }
794
+ serialize() {
795
+ // Include Sender EI in the command if we only have the receiver's EI
796
+ this.maybeAddSPANExtension();
722
797
  const unencryptedExtensions = this.extensions.filter((e) => !e.isEncrypted());
723
798
  const encryptedExtensions = this.extensions.filter((e) => e.isEncrypted());
724
799
  const unencryptedPayload = Buffer.concat([
@@ -735,20 +810,36 @@ let Security2CCMessageEncapsulation = class Security2CCMessageEncapsulation exte
735
810
  serializedCC,
736
811
  ]);
737
812
  // Generate the authentication data for CCM encryption
813
+ const destinationTag = this.getDestinationIDTX();
738
814
  const messageLength = this.computeEncapsulationOverhead() + serializedCC.length;
739
- const authData = getAuthenticationData(this.host.ownNodeId, receiverNodeId, this.host.homeId, messageLength, unencryptedPayload);
740
- // Generate a nonce for encryption, and remember it to attempt decryption
741
- // of potential in-flight messages from the target node.
742
- const iv = this.host.securityManager2.nextNonce(receiverNodeId, true);
743
- const { keyCCM: key } =
744
- // Prefer the overridden security class if it was given
745
- this._securityClass != undefined
746
- ? this.host.securityManager2.getKeysForSecurityClass(this._securityClass)
747
- : this.host.securityManager2.getKeysForNode(receiverNodeId);
815
+ const authData = getAuthenticationData(this.host.ownNodeId, destinationTag, this.host.homeId, messageLength, unencryptedPayload);
816
+ let key;
817
+ let iv;
818
+ if (this.isSinglecast()) {
819
+ // Singlecast:
820
+ // Generate a nonce for encryption, and remember it to attempt decryption
821
+ // of potential in-flight messages from the target node.
822
+ iv = this.host.securityManager2.nextNonce(this.nodeId, true);
823
+ const { keyCCM } =
824
+ // Prefer the overridden security class if it was given
825
+ this._securityClass != undefined
826
+ ? this.host.securityManager2.getKeysForSecurityClass(this._securityClass)
827
+ : this.host.securityManager2.getKeysForNode(this.nodeId);
828
+ key = keyCCM;
829
+ }
830
+ else {
831
+ // Multicast:
832
+ const keyAndIV = this.host.securityManager2.getMulticastKeyAndIV(destinationTag);
833
+ key = keyAndIV.key;
834
+ iv = keyAndIV.iv;
835
+ }
748
836
  const { ciphertext: ciphertextPayload, authTag } = (0, core_1.encryptAES128CCM)(key, iv, plaintextPayload, authData, core_1.SECURITY_S2_AUTH_TAG_LENGTH);
749
837
  // Remember key and IV for debugging purposes
750
838
  this.key = key;
751
839
  this.iv = iv;
840
+ this.authData = authData;
841
+ this.authTag = authTag;
842
+ this.ciphertext = ciphertextPayload;
752
843
  this.payload = Buffer.concat([
753
844
  unencryptedPayload,
754
845
  ciphertextPayload,
@@ -780,19 +871,124 @@ let Security2CCMessageEncapsulation = class Security2CCMessageEncapsulation exte
780
871
  .join("");
781
872
  }
782
873
  // Log the used keys in integration tests
783
- if (process.env.NODE_ENV === "test") {
874
+ if (process.env.NODE_ENV === "test" ||
875
+ process.env.NODE_ENV === "development") {
784
876
  if (this.key) {
785
877
  message.key = (0, safe_2.buffer2hex)(this.key);
786
878
  }
787
879
  if (this.iv) {
788
880
  message.IV = (0, safe_2.buffer2hex)(this.iv);
789
881
  }
882
+ if (this.ciphertext) {
883
+ message.ciphertext = (0, safe_2.buffer2hex)(this.ciphertext);
884
+ }
885
+ if (this.authData) {
886
+ message["auth data"] = (0, safe_2.buffer2hex)(this.authData);
887
+ }
888
+ if (this.authTag) {
889
+ message["auth tag"] = (0, safe_2.buffer2hex)(this.authTag);
890
+ }
790
891
  }
791
892
  return {
792
893
  ...super.toLogEntry(applHost),
793
894
  message,
794
895
  };
795
896
  }
897
+ decryptSinglecast(sendingNodeId, prevSequenceNumber, ciphertext, authData, authTag, securityClass, spanState) {
898
+ const decryptWithNonce = (nonce) => {
899
+ const { keyCCM: key } = this.host.securityManager2.getKeysForNode(sendingNodeId);
900
+ const iv = nonce;
901
+ return {
902
+ key,
903
+ iv,
904
+ ...(0, core_1.decryptAES128CCM)(key, iv, ciphertext, authData, authTag),
905
+ };
906
+ };
907
+ const getNonceAndDecrypt = () => {
908
+ const iv = this.host.securityManager2.nextNonce(sendingNodeId);
909
+ return decryptWithNonce(iv);
910
+ };
911
+ if (spanState.type === core_1.SPANState.SPAN) {
912
+ // There SHOULD be a shared SPAN between both parties. But experience has shown that both could have
913
+ // sent a command at roughly the same time, using the same SPAN for encryption.
914
+ // To avoid a nasty desync and both nodes trying to resync at the same time, causing message loss,
915
+ // we accept commands encrypted with the previous SPAN under very specific circumstances:
916
+ if (
917
+ // The previous SPAN is still known, i.e. the node didn't send another command that was successfully decrypted
918
+ !!spanState.currentSPAN &&
919
+ // it is still valid
920
+ spanState.currentSPAN.expires > (0, core_1.highResTimestamp)() &&
921
+ // The received command is exactly the next, expected one
922
+ prevSequenceNumber != undefined &&
923
+ this["_sequenceNumber"] === ((prevSequenceNumber + 1) & 0xff) &&
924
+ // And in case of a mock-based test, do this only on the controller
925
+ !this.host.__internalIsMockNode) {
926
+ const nonce = spanState.currentSPAN.nonce;
927
+ spanState.currentSPAN = undefined;
928
+ return decryptWithNonce(nonce);
929
+ }
930
+ else {
931
+ // forgetting the current SPAN shouldn't be necessary but better be safe than sorry
932
+ spanState.currentSPAN = undefined;
933
+ }
934
+ // This can only happen if the security class is known
935
+ return getNonceAndDecrypt();
936
+ }
937
+ else if (spanState.type === core_1.SPANState.LocalEI) {
938
+ // We've sent the other our receiver's EI and received its sender's EI,
939
+ // meaning we can now establish an SPAN
940
+ const senderEI = this.getSenderEI();
941
+ if (!senderEI)
942
+ throw failNoSPAN();
943
+ const receiverEI = spanState.receiverEI;
944
+ // How we do this depends on whether we know the security class of the other node
945
+ if (this.host.securityManager2.tempKeys.has(sendingNodeId)) {
946
+ // We're currently bootstrapping the node, it might be using a temporary key
947
+ this.host.securityManager2.initializeTempSPAN(sendingNodeId, senderEI, receiverEI);
948
+ const ret = getNonceAndDecrypt();
949
+ // Decryption with the temporary key worked
950
+ if (ret.authOK)
951
+ return ret;
952
+ // Reset the SPAN state and try with the recently granted security class
953
+ this.host.securityManager2.setSPANState(sendingNodeId, spanState);
954
+ }
955
+ if (securityClass != undefined) {
956
+ this.host.securityManager2.initializeSPAN(sendingNodeId, securityClass, senderEI, receiverEI);
957
+ return getNonceAndDecrypt();
958
+ }
959
+ else {
960
+ // Not knowing it can happen if we just took over an existing network
961
+ // Try multiple security classes
962
+ const possibleSecurityClasses = core_1.securityClassOrder
963
+ .filter((s) => (0, core_1.securityClassIsS2)(s))
964
+ .filter((s) => this.host.hasSecurityClass(sendingNodeId, s) !==
965
+ false);
966
+ for (const secClass of possibleSecurityClasses) {
967
+ // Initialize an SPAN with that security class
968
+ this.host.securityManager2.initializeSPAN(sendingNodeId, secClass, senderEI, receiverEI);
969
+ const ret = getNonceAndDecrypt();
970
+ // It worked, return the result and remember the security class
971
+ if (ret.authOK) {
972
+ this.host.setSecurityClass(sendingNodeId, secClass, true);
973
+ return ret;
974
+ }
975
+ // Reset the SPAN state and try with the next security class
976
+ this.host.securityManager2.setSPANState(sendingNodeId, spanState);
977
+ }
978
+ }
979
+ }
980
+ // Nothing worked, fail the decryption
981
+ return { plaintext: Buffer.from([]), authOK: false };
982
+ }
983
+ decryptMulticast(sendingNodeId, groupId, ciphertext, authData, authTag) {
984
+ const iv = this.host.securityManager2.nextPeerMPAN(sendingNodeId, groupId);
985
+ const { keyCCM: key } = this.host.securityManager2.getKeysForNode(sendingNodeId);
986
+ return {
987
+ key,
988
+ iv,
989
+ ...(0, core_1.decryptAES128CCM)(key, iv, ciphertext, authData, authTag),
990
+ };
991
+ }
796
992
  };
797
993
  Security2CCMessageEncapsulation = __decorate([
798
994
  (0, CommandClassDecorators_1.CCCommand)(_Types_1.Security2Command.MessageEncapsulation),