@zwave-js/cc 10.10.0 → 10.11.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.
- package/build/cc/AlarmSensorCC.d.ts +1 -1
- package/build/cc/AlarmSensorCC.d.ts.map +1 -1
- package/build/cc/AlarmSensorCC.js +11 -9
- package/build/cc/AlarmSensorCC.js.map +1 -1
- package/build/cc/AssociationCC.d.ts +4 -4
- package/build/cc/AssociationGroupInfoCC.d.ts +4 -4
- package/build/cc/BarrierOperatorCC.d.ts +1 -1
- package/build/cc/BarrierOperatorCC.d.ts.map +1 -1
- package/build/cc/BarrierOperatorCC.js +4 -0
- package/build/cc/BarrierOperatorCC.js.map +1 -1
- package/build/cc/BasicCC.d.ts +1 -1
- package/build/cc/BinarySensorCC.d.ts +1 -1
- package/build/cc/BinarySensorCC.d.ts.map +1 -1
- package/build/cc/BinarySensorCC.js +4 -0
- package/build/cc/BinarySensorCC.js.map +1 -1
- package/build/cc/CRC16CC.d.ts.map +1 -1
- package/build/cc/CRC16CC.js +1 -0
- package/build/cc/CRC16CC.js.map +1 -1
- package/build/cc/CentralSceneCC.d.ts +3 -3
- package/build/cc/ColorSwitchCC.d.ts +2 -2
- package/build/cc/ColorSwitchCC.d.ts.map +1 -1
- package/build/cc/ColorSwitchCC.js +4 -0
- package/build/cc/ColorSwitchCC.js.map +1 -1
- package/build/cc/ConfigurationCC.d.ts +2 -2
- package/build/cc/DoorLockCC.d.ts +4 -4
- package/build/cc/DoorLockCC.js +1 -1
- package/build/cc/DoorLockCC.js.map +1 -1
- package/build/cc/DoorLockLoggingCC.d.ts +1 -1
- package/build/cc/EntryControlCC.d.ts +3 -3
- package/build/cc/FirmwareUpdateMetaDataCC.d.ts +4 -4
- package/build/cc/HumidityControlModeCC.d.ts +1 -1
- package/build/cc/HumidityControlSetpointCC.d.ts +1 -1
- package/build/cc/IndicatorCC.d.ts +2 -2
- package/build/cc/IndicatorCC.js +1 -1
- package/build/cc/IndicatorCC.js.map +1 -1
- package/build/cc/IrrigationCC.d.ts +4 -4
- package/build/cc/ManufacturerSpecificCC.d.ts +3 -3
- package/build/cc/MeterCC.d.ts +4 -4
- package/build/cc/MultiChannelAssociationCC.d.ts +4 -4
- package/build/cc/MultiChannelCC.d.ts +8 -8
- package/build/cc/MultiChannelCC.d.ts.map +1 -1
- package/build/cc/MultiChannelCC.js +21 -0
- package/build/cc/MultiChannelCC.js.map +1 -1
- package/build/cc/MultiCommandCC.d.ts.map +1 -1
- package/build/cc/MultiCommandCC.js +2 -1
- package/build/cc/MultiCommandCC.js.map +1 -1
- package/build/cc/MultilevelSensorCC.d.ts +2 -2
- package/build/cc/MultilevelSwitchCC.d.ts +3 -3
- package/build/cc/MultilevelSwitchCC.d.ts.map +1 -1
- package/build/cc/MultilevelSwitchCC.js +1 -1
- package/build/cc/MultilevelSwitchCC.js.map +1 -1
- package/build/cc/NodeNamingCC.d.ts +2 -2
- package/build/cc/NotificationCC.d.ts +5 -5
- package/build/cc/PowerlevelCC.d.ts +1 -1
- package/build/cc/PowerlevelCC.d.ts.map +1 -1
- package/build/cc/ProtectionCC.d.ts +4 -4
- package/build/cc/SceneActivationCC.d.ts +1 -1
- package/build/cc/ScheduleEntryLockCC.d.ts +3 -3
- package/build/cc/ScheduleEntryLockCC.d.ts.map +1 -1
- package/build/cc/ScheduleEntryLockCC.js +12 -10
- package/build/cc/ScheduleEntryLockCC.js.map +1 -1
- package/build/cc/Security2CC.d.ts +20 -3
- package/build/cc/Security2CC.d.ts.map +1 -1
- package/build/cc/Security2CC.js +329 -133
- package/build/cc/Security2CC.js.map +1 -1
- package/build/cc/SupervisionCC.d.ts +1 -1
- package/build/cc/SupervisionCC.d.ts.map +1 -1
- package/build/cc/SupervisionCC.js +1 -0
- package/build/cc/SupervisionCC.js.map +1 -1
- package/build/cc/ThermostatFanModeCC.d.ts +1 -1
- package/build/cc/ThermostatModeCC.d.ts +5 -5
- package/build/cc/ThermostatModeCC.d.ts.map +1 -1
- package/build/cc/ThermostatSetpointCC.d.ts +3 -3
- package/build/cc/TransportServiceCC.d.ts +10 -0
- package/build/cc/TransportServiceCC.d.ts.map +1 -1
- package/build/cc/TransportServiceCC.js +13 -4
- package/build/cc/TransportServiceCC.js.map +1 -1
- package/build/cc/UserCodeCC.d.ts +11 -11
- package/build/cc/VersionCC.d.ts +3 -3
- package/build/cc/WakeUpCC.d.ts +2 -2
- package/build/cc/ZWavePlusCC.d.ts +5 -5
- package/build/index.d.ts +1 -0
- package/build/index.d.ts.map +1 -1
- package/build/index.js +3 -1
- package/build/index.js.map +1 -1
- package/build/lib/API.d.ts +63 -61
- package/build/lib/API.d.ts.map +1 -1
- package/build/lib/API.js +23 -4
- package/build/lib/API.js.map +1 -1
- package/build/lib/CommandClass.d.ts +10 -4
- package/build/lib/CommandClass.d.ts.map +1 -1
- package/build/lib/CommandClass.js +31 -8
- package/build/lib/CommandClass.js.map +1 -1
- package/build/lib/Security2/Extension.d.ts.map +1 -1
- package/build/lib/Security2/Extension.js +6 -3
- package/build/lib/Security2/Extension.js.map +1 -1
- package/build/lib/Values.d.ts +2 -2
- package/build/lib/Values.d.ts.map +1 -1
- package/build/lib/Values.js +4 -0
- package/build/lib/Values.js.map +1 -1
- package/package.json +13 -13
package/build/cc/Security2CC.js
CHANGED
|
@@ -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
|
|
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,
|
|
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
|
|
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
|
-
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
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
|
-
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
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.
|
|
559
|
-
//
|
|
560
|
-
//
|
|
561
|
-
|
|
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
|
-
|
|
601
|
-
|
|
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
|
-
|
|
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
|
-
|
|
641
|
-
|
|
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.
|
|
658
|
-
this.
|
|
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
|
-
|
|
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
|
-
|
|
675
|
-
|
|
676
|
-
|
|
677
|
-
return this.
|
|
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
|
-
|
|
685
|
-
|
|
686
|
-
|
|
687
|
-
const receiverNodeId = this.
|
|
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,
|
|
740
|
-
|
|
741
|
-
|
|
742
|
-
|
|
743
|
-
|
|
744
|
-
|
|
745
|
-
|
|
746
|
-
|
|
747
|
-
|
|
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),
|