@zwave-js/cc 14.2.0 → 14.3.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.
- package/build/cjs/cc/Security2CC.d.ts +3 -1
- package/build/cjs/cc/Security2CC.js +218 -102
- package/build/cjs/cc/Security2CC.js.map +2 -2
- package/build/cjs/cc/SecurityCC.d.ts +1 -0
- package/build/cjs/cc/SecurityCC.js +54 -12
- package/build/cjs/cc/SecurityCC.js.map +2 -2
- package/build/esm/cc/Security2CC.d.ts +3 -1
- package/build/esm/cc/Security2CC.d.ts.map +1 -1
- package/build/esm/cc/Security2CC.js +299 -134
- package/build/esm/cc/Security2CC.js.map +1 -1
- package/build/esm/cc/SecurityCC.d.ts +1 -0
- package/build/esm/cc/SecurityCC.d.ts.map +1 -1
- package/build/esm/cc/SecurityCC.js +66 -13
- package/build/esm/cc/SecurityCC.js.map +1 -1
- package/package.json +6 -6
|
@@ -100,7 +100,9 @@ export declare class Security2CCMessageEncapsulation extends Security2CC {
|
|
|
100
100
|
getSenderEI(): Uint8Array | undefined;
|
|
101
101
|
/** Returns the multicast group ID if this command contains an MGRP extension */
|
|
102
102
|
getMulticastGroupId(): number | undefined;
|
|
103
|
-
|
|
103
|
+
/** @deprecated Use {@link maybeAddSPANExtensionAsync} instead */
|
|
104
|
+
private maybeAddSPANExtensionSync;
|
|
105
|
+
private maybeAddSPANExtensionAsync;
|
|
104
106
|
/** @deprecated Use {@link serializeAsync} instead */
|
|
105
107
|
serialize(ctx: CCEncodingContext): Bytes;
|
|
106
108
|
serializeAsync(ctx: CCEncodingContext): Promise<Bytes>;
|
|
@@ -99,6 +99,7 @@ function bitMaskToSecurityClass(buffer, offset) {
|
|
|
99
99
|
(0, import_core.validatePayload)(keys.length === 1);
|
|
100
100
|
return keys[0];
|
|
101
101
|
}
|
|
102
|
+
const SECURITY_S2_AUTH_TAG_LENGTH = 8;
|
|
102
103
|
function getAuthenticationData(sendingNodeId, destination, homeId, commandLength, unencryptedPayload) {
|
|
103
104
|
const nodeIdSize = (0, import_core.isLongRangeNodeId)(sendingNodeId) || (0, import_core.isLongRangeNodeId)(destination) ? 2 : 1;
|
|
104
105
|
const ret = new import_safe2.Bytes(2 * nodeIdSize + 6 + unencryptedPayload.length);
|
|
@@ -145,14 +146,15 @@ function assertSecurityTX(ctx, destination) {
|
|
|
145
146
|
}
|
|
146
147
|
return ret;
|
|
147
148
|
}
|
|
148
|
-
function
|
|
149
|
+
function decryptSinglecastSync(ctx, securityManager, sendingNodeId, curSequenceNumber, prevSequenceNumber, ciphertext, authData, authTag, spanState, extensions) {
|
|
149
150
|
const decryptWithNonce = (nonce) => {
|
|
150
151
|
const { keyCCM: key } = securityManager.getKeysForNode(sendingNodeId);
|
|
151
152
|
const iv = nonce;
|
|
152
153
|
return {
|
|
153
154
|
key,
|
|
154
155
|
iv,
|
|
155
|
-
|
|
156
|
+
// eslint-disable-next-line @typescript-eslint/no-deprecated
|
|
157
|
+
...(0, import_core.decryptAES128CCMSync)(key, iv, ciphertext, authData, authTag)
|
|
156
158
|
};
|
|
157
159
|
};
|
|
158
160
|
const getNonceAndDecrypt = () => {
|
|
@@ -223,17 +225,150 @@ function decryptSinglecast(ctx, securityManager, sendingNodeId, curSequenceNumbe
|
|
|
223
225
|
securityClass: void 0
|
|
224
226
|
};
|
|
225
227
|
}
|
|
226
|
-
function
|
|
228
|
+
async function decryptSinglecastAsync(ctx, securityManager, sendingNodeId, curSequenceNumber, prevSequenceNumber, ciphertext, authData, authTag, spanState, extensions) {
|
|
229
|
+
const decryptWithNonce = async (nonce) => {
|
|
230
|
+
const { keyCCM: key } = securityManager.getKeysForNode(sendingNodeId);
|
|
231
|
+
const iv = nonce;
|
|
232
|
+
return {
|
|
233
|
+
key,
|
|
234
|
+
iv,
|
|
235
|
+
...await (0, import_core.decryptAES128CCMAsync)(ciphertext, key, iv, authData, authTag)
|
|
236
|
+
};
|
|
237
|
+
};
|
|
238
|
+
const getNonceAndDecrypt = async () => {
|
|
239
|
+
const iv = await securityManager.nextNonceAsync(sendingNodeId);
|
|
240
|
+
return decryptWithNonce(iv);
|
|
241
|
+
};
|
|
242
|
+
if (spanState.type === import_core.SPANState.SPAN) {
|
|
243
|
+
if (
|
|
244
|
+
// The previous SPAN is still known, i.e. the node didn't send another command that was successfully decrypted
|
|
245
|
+
!!spanState.currentSPAN && spanState.currentSPAN.expires > (0, import_core.highResTimestamp)() && prevSequenceNumber != void 0 && curSequenceNumber === (prevSequenceNumber + 1 & 255) && !ctx.__internalIsMockNode
|
|
246
|
+
) {
|
|
247
|
+
const nonce = spanState.currentSPAN.nonce;
|
|
248
|
+
spanState.currentSPAN = void 0;
|
|
249
|
+
const result = await decryptWithNonce(nonce);
|
|
250
|
+
if (result.authOK) {
|
|
251
|
+
return {
|
|
252
|
+
...result,
|
|
253
|
+
securityClass: spanState.securityClass
|
|
254
|
+
};
|
|
255
|
+
}
|
|
256
|
+
} else {
|
|
257
|
+
spanState.currentSPAN = void 0;
|
|
258
|
+
}
|
|
259
|
+
return {
|
|
260
|
+
...await getNonceAndDecrypt(),
|
|
261
|
+
securityClass: spanState.securityClass
|
|
262
|
+
};
|
|
263
|
+
} else if (spanState.type === import_core.SPANState.LocalEI) {
|
|
264
|
+
const senderEI = getSenderEI(extensions);
|
|
265
|
+
if (!senderEI)
|
|
266
|
+
failNoSPAN();
|
|
267
|
+
const receiverEI = spanState.receiverEI;
|
|
268
|
+
const isBootstrappingNode = securityManager.tempKeys.has(sendingNodeId);
|
|
269
|
+
if (isBootstrappingNode) {
|
|
270
|
+
await securityManager.initializeTempSPANAsync(sendingNodeId, senderEI, receiverEI);
|
|
271
|
+
const ret = await getNonceAndDecrypt();
|
|
272
|
+
if (ret.authOK) {
|
|
273
|
+
return {
|
|
274
|
+
...ret,
|
|
275
|
+
securityClass: import_core.SecurityClass.Temporary
|
|
276
|
+
};
|
|
277
|
+
}
|
|
278
|
+
securityManager.setSPANState(sendingNodeId, spanState);
|
|
279
|
+
}
|
|
280
|
+
const possibleSecurityClasses = isBootstrappingNode ? [ctx.getHighestSecurityClass(sendingNodeId)] : import_core.securityClassOrder.filter((s) => ctx.hasSecurityClass(sendingNodeId, s) !== false);
|
|
281
|
+
for (const secClass of possibleSecurityClasses) {
|
|
282
|
+
if (!securityManager.hasKeysForSecurityClass(secClass)) {
|
|
283
|
+
continue;
|
|
284
|
+
}
|
|
285
|
+
await securityManager.initializeSPANAsync(sendingNodeId, secClass, senderEI, receiverEI);
|
|
286
|
+
const ret = await getNonceAndDecrypt();
|
|
287
|
+
if (ret.authOK) {
|
|
288
|
+
if (ctx.hasSecurityClass(sendingNodeId, secClass) === void 0) {
|
|
289
|
+
ctx.setSecurityClass(sendingNodeId, secClass, true);
|
|
290
|
+
}
|
|
291
|
+
return {
|
|
292
|
+
...ret,
|
|
293
|
+
securityClass: secClass
|
|
294
|
+
};
|
|
295
|
+
} else {
|
|
296
|
+
securityManager.setSPANState(sendingNodeId, spanState);
|
|
297
|
+
}
|
|
298
|
+
}
|
|
299
|
+
}
|
|
300
|
+
return {
|
|
301
|
+
plaintext: new Uint8Array(),
|
|
302
|
+
authOK: false,
|
|
303
|
+
securityClass: void 0
|
|
304
|
+
};
|
|
305
|
+
}
|
|
306
|
+
function decryptMulticastSync(sendingNodeId, securityManager, groupId, ciphertext, authData, authTag) {
|
|
227
307
|
const iv = securityManager.nextPeerMPAN(sendingNodeId, groupId);
|
|
228
308
|
const { keyCCM: key } = securityManager.getKeysForNode(sendingNodeId);
|
|
229
309
|
return {
|
|
230
310
|
key,
|
|
231
311
|
iv,
|
|
232
|
-
|
|
312
|
+
// eslint-disable-next-line @typescript-eslint/no-deprecated
|
|
313
|
+
...(0, import_core.decryptAES128CCMSync)(key, iv, ciphertext, authData, authTag),
|
|
233
314
|
// The security class is irrelevant when decrypting multicast commands
|
|
234
315
|
securityClass: void 0
|
|
235
316
|
};
|
|
236
317
|
}
|
|
318
|
+
async function decryptMulticastAsync(sendingNodeId, securityManager, groupId, ciphertext, authData, authTag) {
|
|
319
|
+
const iv = await securityManager.nextPeerMPANAsync(sendingNodeId, groupId);
|
|
320
|
+
const { keyCCM: key } = securityManager.getKeysForNode(sendingNodeId);
|
|
321
|
+
return {
|
|
322
|
+
key,
|
|
323
|
+
iv,
|
|
324
|
+
...await (0, import_core.decryptAES128CCMAsync)(ciphertext, key, iv, authData, authTag),
|
|
325
|
+
// The security class is irrelevant when decrypting multicast commands
|
|
326
|
+
securityClass: void 0
|
|
327
|
+
};
|
|
328
|
+
}
|
|
329
|
+
function parseExtensions(buffer, wasEncrypted) {
|
|
330
|
+
const extensions = [];
|
|
331
|
+
let mustDiscardCommand = false;
|
|
332
|
+
let offset = 0;
|
|
333
|
+
parsing: while (true) {
|
|
334
|
+
if (buffer.length < offset + 2) {
|
|
335
|
+
mustDiscardCommand = true;
|
|
336
|
+
break parsing;
|
|
337
|
+
}
|
|
338
|
+
const { actual: actualLength, expected: expectedLength } = import_Extension.Security2Extension.getExtensionLength(buffer.subarray(offset));
|
|
339
|
+
const extensionLength = expectedLength ?? actualLength;
|
|
340
|
+
if (extensionLength < 2) {
|
|
341
|
+
mustDiscardCommand = true;
|
|
342
|
+
break parsing;
|
|
343
|
+
} else if (extensionLength > buffer.length - offset - (wasEncrypted ? 0 : SECURITY_S2_AUTH_TAG_LENGTH)) {
|
|
344
|
+
mustDiscardCommand = true;
|
|
345
|
+
break parsing;
|
|
346
|
+
}
|
|
347
|
+
const extensionData = buffer.subarray(offset, offset + extensionLength);
|
|
348
|
+
offset += extensionLength;
|
|
349
|
+
const ext = import_Extension.Security2Extension.parse(extensionData);
|
|
350
|
+
switch ((0, import_Extension.validateS2Extension)(ext, wasEncrypted)) {
|
|
351
|
+
case import_Extension.ValidateS2ExtensionResult.OK:
|
|
352
|
+
if (expectedLength != void 0 && actualLength !== expectedLength) {
|
|
353
|
+
} else {
|
|
354
|
+
extensions.push(ext);
|
|
355
|
+
}
|
|
356
|
+
break;
|
|
357
|
+
case import_Extension.ValidateS2ExtensionResult.DiscardExtension:
|
|
358
|
+
break;
|
|
359
|
+
case import_Extension.ValidateS2ExtensionResult.DiscardCommand:
|
|
360
|
+
mustDiscardCommand = true;
|
|
361
|
+
break;
|
|
362
|
+
}
|
|
363
|
+
if (!ext.moreToFollow)
|
|
364
|
+
break parsing;
|
|
365
|
+
}
|
|
366
|
+
return {
|
|
367
|
+
extensions,
|
|
368
|
+
mustDiscardCommand,
|
|
369
|
+
bytesRead: offset
|
|
370
|
+
};
|
|
371
|
+
}
|
|
237
372
|
function getDestinationIDTX() {
|
|
238
373
|
if (this.isSinglecast())
|
|
239
374
|
return this.nodeId;
|
|
@@ -292,7 +427,7 @@ let Security2CCAPI = (() => {
|
|
|
292
427
|
if (!securityManager) {
|
|
293
428
|
throw new import_core.ZWaveError(`Nonces can only be sent if secure communication is set up!`, import_core.ZWaveErrorCodes.Driver_NoSecurity);
|
|
294
429
|
}
|
|
295
|
-
const receiverEI = securityManager.
|
|
430
|
+
const receiverEI = await securityManager.generateNonceAsync(this.endpoint.nodeId);
|
|
296
431
|
const cc = new Security2CCNonceReport({
|
|
297
432
|
nodeId: this.endpoint.nodeId,
|
|
298
433
|
endpointIndex: this.endpoint.index,
|
|
@@ -843,43 +978,12 @@ let Security2CCMessageEncapsulation = (() => {
|
|
|
843
978
|
let offset = 2;
|
|
844
979
|
const extensions = [];
|
|
845
980
|
let mustDiscardCommand = false;
|
|
846
|
-
|
|
847
|
-
|
|
848
|
-
|
|
849
|
-
|
|
850
|
-
|
|
851
|
-
|
|
852
|
-
const { actual: actualLength, expected: expectedLength } = import_Extension.Security2Extension.getExtensionLength(buffer.subarray(offset));
|
|
853
|
-
const extensionLength = expectedLength ?? actualLength;
|
|
854
|
-
if (extensionLength < 2) {
|
|
855
|
-
mustDiscardCommand = true;
|
|
856
|
-
return;
|
|
857
|
-
} else if (extensionLength > buffer.length - offset - (wasEncrypted ? 0 : import_core.SECURITY_S2_AUTH_TAG_LENGTH)) {
|
|
858
|
-
mustDiscardCommand = true;
|
|
859
|
-
return;
|
|
860
|
-
}
|
|
861
|
-
const extensionData = buffer.subarray(offset, offset + extensionLength);
|
|
862
|
-
offset += extensionLength;
|
|
863
|
-
const ext = import_Extension.Security2Extension.parse(extensionData);
|
|
864
|
-
switch ((0, import_Extension.validateS2Extension)(ext, wasEncrypted)) {
|
|
865
|
-
case import_Extension.ValidateS2ExtensionResult.OK:
|
|
866
|
-
if (expectedLength != void 0 && actualLength !== expectedLength) {
|
|
867
|
-
} else {
|
|
868
|
-
extensions.push(ext);
|
|
869
|
-
}
|
|
870
|
-
break;
|
|
871
|
-
case import_Extension.ValidateS2ExtensionResult.DiscardExtension:
|
|
872
|
-
break;
|
|
873
|
-
case import_Extension.ValidateS2ExtensionResult.DiscardCommand:
|
|
874
|
-
mustDiscardCommand = true;
|
|
875
|
-
break;
|
|
876
|
-
}
|
|
877
|
-
if (!ext.moreToFollow)
|
|
878
|
-
break;
|
|
879
|
-
}
|
|
880
|
-
};
|
|
881
|
-
if (hasExtensions)
|
|
882
|
-
parseExtensions(raw.payload, false);
|
|
981
|
+
if (hasExtensions) {
|
|
982
|
+
const parseResult = parseExtensions(raw.payload.subarray(offset), false);
|
|
983
|
+
extensions.push(...parseResult.extensions);
|
|
984
|
+
offset += parseResult.bytesRead;
|
|
985
|
+
mustDiscardCommand = parseResult.mustDiscardCommand;
|
|
986
|
+
}
|
|
883
987
|
const mcctx = (() => {
|
|
884
988
|
const multicastGroupId = getMulticastGroupId(extensions);
|
|
885
989
|
if (ctx.frameType === "multicast" || ctx.frameType === "broadcast") {
|
|
@@ -913,8 +1017,8 @@ let Security2CCMessageEncapsulation = (() => {
|
|
|
913
1017
|
}
|
|
914
1018
|
}
|
|
915
1019
|
const unencryptedPayload = raw.payload.subarray(0, offset);
|
|
916
|
-
const ciphertext = raw.payload.subarray(offset, -
|
|
917
|
-
const authTag = raw.payload.subarray(-
|
|
1020
|
+
const ciphertext = raw.payload.subarray(offset, -SECURITY_S2_AUTH_TAG_LENGTH);
|
|
1021
|
+
const authTag = raw.payload.subarray(-SECURITY_S2_AUTH_TAG_LENGTH);
|
|
918
1022
|
const messageLength = 2 + raw.payload.length;
|
|
919
1023
|
const authData = getAuthenticationData(ctx.sourceNodeId, getDestinationIDRX(ctx, extensions), ctx.homeId, messageLength, unencryptedPayload);
|
|
920
1024
|
let decrypt;
|
|
@@ -923,7 +1027,10 @@ let Security2CCMessageEncapsulation = (() => {
|
|
|
923
1027
|
securityManager.storePeerMPAN(ctx.sourceNodeId, mcctx.groupId, { type: import_core.MPANState.OutOfSync });
|
|
924
1028
|
failNoMPAN();
|
|
925
1029
|
}
|
|
926
|
-
decrypt = () =>
|
|
1030
|
+
decrypt = () => (
|
|
1031
|
+
// eslint-disable-next-line @typescript-eslint/no-deprecated
|
|
1032
|
+
decryptMulticastSync(ctx.sourceNodeId, securityManager, mcctx.groupId, ciphertext, authData, authTag)
|
|
1033
|
+
);
|
|
927
1034
|
} else {
|
|
928
1035
|
const spanState = securityManager.getSPANState(ctx.sourceNodeId);
|
|
929
1036
|
if (spanState.type === import_core.SPANState.None) {
|
|
@@ -931,7 +1038,10 @@ let Security2CCMessageEncapsulation = (() => {
|
|
|
931
1038
|
} else if (spanState.type === import_core.SPANState.RemoteEI) {
|
|
932
1039
|
failNoSPAN();
|
|
933
1040
|
}
|
|
934
|
-
decrypt = () =>
|
|
1041
|
+
decrypt = () => (
|
|
1042
|
+
// eslint-disable-next-line @typescript-eslint/no-deprecated
|
|
1043
|
+
decryptSinglecastSync(ctx, securityManager, ctx.sourceNodeId, sequenceNumber, prevSequenceNumber, ciphertext, authData, authTag, spanState, extensions)
|
|
1044
|
+
);
|
|
935
1045
|
}
|
|
936
1046
|
let plaintext;
|
|
937
1047
|
let authOK = false;
|
|
@@ -964,8 +1074,12 @@ let Security2CCMessageEncapsulation = (() => {
|
|
|
964
1074
|
}
|
|
965
1075
|
const securityClass = decryptionSecurityClass;
|
|
966
1076
|
offset = 0;
|
|
967
|
-
if (hasEncryptedExtensions)
|
|
968
|
-
parseExtensions(plaintext, true);
|
|
1077
|
+
if (hasEncryptedExtensions) {
|
|
1078
|
+
const parseResult = parseExtensions(plaintext, true);
|
|
1079
|
+
extensions.push(...parseResult.extensions);
|
|
1080
|
+
offset += parseResult.bytesRead;
|
|
1081
|
+
mustDiscardCommand = parseResult.mustDiscardCommand;
|
|
1082
|
+
}
|
|
969
1083
|
if (mustDiscardCommand) {
|
|
970
1084
|
import_core.validatePayload.fail("Invalid extension");
|
|
971
1085
|
}
|
|
@@ -1008,43 +1122,12 @@ let Security2CCMessageEncapsulation = (() => {
|
|
|
1008
1122
|
let offset = 2;
|
|
1009
1123
|
const extensions = [];
|
|
1010
1124
|
let mustDiscardCommand = false;
|
|
1011
|
-
|
|
1012
|
-
|
|
1013
|
-
|
|
1014
|
-
|
|
1015
|
-
|
|
1016
|
-
|
|
1017
|
-
const { actual: actualLength, expected: expectedLength } = import_Extension.Security2Extension.getExtensionLength(buffer.subarray(offset));
|
|
1018
|
-
const extensionLength = expectedLength ?? actualLength;
|
|
1019
|
-
if (extensionLength < 2) {
|
|
1020
|
-
mustDiscardCommand = true;
|
|
1021
|
-
return;
|
|
1022
|
-
} else if (extensionLength > buffer.length - offset - (wasEncrypted ? 0 : import_core.SECURITY_S2_AUTH_TAG_LENGTH)) {
|
|
1023
|
-
mustDiscardCommand = true;
|
|
1024
|
-
return;
|
|
1025
|
-
}
|
|
1026
|
-
const extensionData = buffer.subarray(offset, offset + extensionLength);
|
|
1027
|
-
offset += extensionLength;
|
|
1028
|
-
const ext = import_Extension.Security2Extension.parse(extensionData);
|
|
1029
|
-
switch ((0, import_Extension.validateS2Extension)(ext, wasEncrypted)) {
|
|
1030
|
-
case import_Extension.ValidateS2ExtensionResult.OK:
|
|
1031
|
-
if (expectedLength != void 0 && actualLength !== expectedLength) {
|
|
1032
|
-
} else {
|
|
1033
|
-
extensions.push(ext);
|
|
1034
|
-
}
|
|
1035
|
-
break;
|
|
1036
|
-
case import_Extension.ValidateS2ExtensionResult.DiscardExtension:
|
|
1037
|
-
break;
|
|
1038
|
-
case import_Extension.ValidateS2ExtensionResult.DiscardCommand:
|
|
1039
|
-
mustDiscardCommand = true;
|
|
1040
|
-
break;
|
|
1041
|
-
}
|
|
1042
|
-
if (!ext.moreToFollow)
|
|
1043
|
-
break;
|
|
1044
|
-
}
|
|
1045
|
-
};
|
|
1046
|
-
if (hasExtensions)
|
|
1047
|
-
parseExtensions(raw.payload, false);
|
|
1125
|
+
if (hasExtensions) {
|
|
1126
|
+
const parseResult = parseExtensions(raw.payload.subarray(offset), false);
|
|
1127
|
+
extensions.push(...parseResult.extensions);
|
|
1128
|
+
offset += parseResult.bytesRead;
|
|
1129
|
+
mustDiscardCommand = parseResult.mustDiscardCommand;
|
|
1130
|
+
}
|
|
1048
1131
|
const mcctx = (() => {
|
|
1049
1132
|
const multicastGroupId = getMulticastGroupId(extensions);
|
|
1050
1133
|
if (ctx.frameType === "multicast" || ctx.frameType === "broadcast") {
|
|
@@ -1061,9 +1144,9 @@ let Security2CCMessageEncapsulation = (() => {
|
|
|
1061
1144
|
})();
|
|
1062
1145
|
if (mustDiscardCommand) {
|
|
1063
1146
|
if (mcctx.isMulticast) {
|
|
1064
|
-
securityManager.
|
|
1147
|
+
await securityManager.nextPeerMPANAsync(ctx.sourceNodeId, mcctx.groupId);
|
|
1065
1148
|
} else {
|
|
1066
|
-
securityManager.
|
|
1149
|
+
await securityManager.nextNonceAsync(ctx.sourceNodeId);
|
|
1067
1150
|
}
|
|
1068
1151
|
import_core.validatePayload.fail("Invalid S2 extension");
|
|
1069
1152
|
}
|
|
@@ -1078,8 +1161,8 @@ let Security2CCMessageEncapsulation = (() => {
|
|
|
1078
1161
|
}
|
|
1079
1162
|
}
|
|
1080
1163
|
const unencryptedPayload = raw.payload.subarray(0, offset);
|
|
1081
|
-
const ciphertext = raw.payload.subarray(offset, -
|
|
1082
|
-
const authTag = raw.payload.subarray(-
|
|
1164
|
+
const ciphertext = raw.payload.subarray(offset, -SECURITY_S2_AUTH_TAG_LENGTH);
|
|
1165
|
+
const authTag = raw.payload.subarray(-SECURITY_S2_AUTH_TAG_LENGTH);
|
|
1083
1166
|
const messageLength = 2 + raw.payload.length;
|
|
1084
1167
|
const authData = getAuthenticationData(ctx.sourceNodeId, getDestinationIDRX(ctx, extensions), ctx.homeId, messageLength, unencryptedPayload);
|
|
1085
1168
|
let decrypt;
|
|
@@ -1088,7 +1171,7 @@ let Security2CCMessageEncapsulation = (() => {
|
|
|
1088
1171
|
securityManager.storePeerMPAN(ctx.sourceNodeId, mcctx.groupId, { type: import_core.MPANState.OutOfSync });
|
|
1089
1172
|
failNoMPAN();
|
|
1090
1173
|
}
|
|
1091
|
-
decrypt = () =>
|
|
1174
|
+
decrypt = () => decryptMulticastAsync(ctx.sourceNodeId, securityManager, mcctx.groupId, ciphertext, authData, authTag);
|
|
1092
1175
|
} else {
|
|
1093
1176
|
const spanState = securityManager.getSPANState(ctx.sourceNodeId);
|
|
1094
1177
|
if (spanState.type === import_core.SPANState.None) {
|
|
@@ -1096,7 +1179,7 @@ let Security2CCMessageEncapsulation = (() => {
|
|
|
1096
1179
|
} else if (spanState.type === import_core.SPANState.RemoteEI) {
|
|
1097
1180
|
failNoSPAN();
|
|
1098
1181
|
}
|
|
1099
|
-
decrypt = () =>
|
|
1182
|
+
decrypt = () => decryptSinglecastAsync(ctx, securityManager, ctx.sourceNodeId, sequenceNumber, prevSequenceNumber, ciphertext, authData, authTag, spanState, extensions);
|
|
1100
1183
|
}
|
|
1101
1184
|
let plaintext;
|
|
1102
1185
|
let authOK = false;
|
|
@@ -1111,7 +1194,7 @@ let Security2CCMessageEncapsulation = (() => {
|
|
|
1111
1194
|
key,
|
|
1112
1195
|
iv,
|
|
1113
1196
|
securityClass: decryptionSecurityClass
|
|
1114
|
-
} = decrypt());
|
|
1197
|
+
} = await decrypt());
|
|
1115
1198
|
if (!!authOK && !!plaintext)
|
|
1116
1199
|
break;
|
|
1117
1200
|
if (!!getSenderEI(extensions))
|
|
@@ -1129,8 +1212,12 @@ let Security2CCMessageEncapsulation = (() => {
|
|
|
1129
1212
|
}
|
|
1130
1213
|
const securityClass = decryptionSecurityClass;
|
|
1131
1214
|
offset = 0;
|
|
1132
|
-
if (hasEncryptedExtensions)
|
|
1133
|
-
parseExtensions(plaintext, true);
|
|
1215
|
+
if (hasEncryptedExtensions) {
|
|
1216
|
+
const parseResult = parseExtensions(plaintext, true);
|
|
1217
|
+
extensions.push(...parseResult.extensions);
|
|
1218
|
+
offset += parseResult.bytesRead;
|
|
1219
|
+
mustDiscardCommand = parseResult.mustDiscardCommand;
|
|
1220
|
+
}
|
|
1134
1221
|
if (mustDiscardCommand) {
|
|
1135
1222
|
import_core.validatePayload.fail("Invalid extension");
|
|
1136
1223
|
}
|
|
@@ -1200,7 +1287,8 @@ let Security2CCMessageEncapsulation = (() => {
|
|
|
1200
1287
|
getMulticastGroupId() {
|
|
1201
1288
|
return getMulticastGroupId(this.extensions);
|
|
1202
1289
|
}
|
|
1203
|
-
|
|
1290
|
+
/** @deprecated Use {@link maybeAddSPANExtensionAsync} instead */
|
|
1291
|
+
maybeAddSPANExtensionSync(ctx, securityManager) {
|
|
1204
1292
|
if (!this.isSinglecast())
|
|
1205
1293
|
return;
|
|
1206
1294
|
const receiverNodeId = this.nodeId;
|
|
@@ -1228,11 +1316,39 @@ let Security2CCMessageEncapsulation = (() => {
|
|
|
1228
1316
|
}
|
|
1229
1317
|
}
|
|
1230
1318
|
}
|
|
1319
|
+
async maybeAddSPANExtensionAsync(ctx, securityManager) {
|
|
1320
|
+
if (!this.isSinglecast())
|
|
1321
|
+
return;
|
|
1322
|
+
const receiverNodeId = this.nodeId;
|
|
1323
|
+
const spanState = securityManager.getSPANState(receiverNodeId);
|
|
1324
|
+
if (spanState.type === import_core.SPANState.None || spanState.type === import_core.SPANState.LocalEI) {
|
|
1325
|
+
throw new import_core.ZWaveError(`Security S2 CC requires the receiver's nonce to be sent!`, import_core.ZWaveErrorCodes.Security2CC_NoSPAN);
|
|
1326
|
+
} else if (spanState.type === import_core.SPANState.RemoteEI) {
|
|
1327
|
+
const senderEI = await securityManager.generateNonceAsync(void 0);
|
|
1328
|
+
const receiverEI = spanState.receiverEI;
|
|
1329
|
+
if (this.securityClass == void 0 && securityManager.tempKeys.has(receiverNodeId)) {
|
|
1330
|
+
await securityManager.initializeTempSPANAsync(receiverNodeId, senderEI, receiverEI);
|
|
1331
|
+
} else {
|
|
1332
|
+
const securityClass = this.securityClass ?? ctx.getHighestSecurityClass(receiverNodeId);
|
|
1333
|
+
if (securityClass == void 0) {
|
|
1334
|
+
throw new import_core.ZWaveError("No security class defined for this command!", import_core.ZWaveErrorCodes.Security2CC_NoSPAN);
|
|
1335
|
+
}
|
|
1336
|
+
await securityManager.initializeSPANAsync(receiverNodeId, securityClass, senderEI, receiverEI);
|
|
1337
|
+
}
|
|
1338
|
+
let spanExtension = this.extensions.find((e) => e instanceof import_Extension.SPANExtension);
|
|
1339
|
+
if (spanExtension) {
|
|
1340
|
+
spanExtension.senderEI = senderEI;
|
|
1341
|
+
} else {
|
|
1342
|
+
spanExtension = new import_Extension.SPANExtension({ senderEI });
|
|
1343
|
+
this.extensions.push(spanExtension);
|
|
1344
|
+
}
|
|
1345
|
+
}
|
|
1346
|
+
}
|
|
1231
1347
|
/** @deprecated Use {@link serializeAsync} instead */
|
|
1232
1348
|
serialize(ctx) {
|
|
1233
1349
|
const securityManager = assertSecurityTX(ctx, this.nodeId);
|
|
1234
1350
|
this.ensureSequenceNumber(securityManager);
|
|
1235
|
-
this.
|
|
1351
|
+
this.maybeAddSPANExtensionSync(ctx, securityManager);
|
|
1236
1352
|
const unencryptedExtensions = this.extensions.filter((e) => !e.isEncrypted());
|
|
1237
1353
|
const encryptedExtensions = this.extensions.filter((e) => e.isEncrypted());
|
|
1238
1354
|
const unencryptedPayload = import_safe2.Bytes.concat([
|
|
@@ -1264,7 +1380,7 @@ let Security2CCMessageEncapsulation = (() => {
|
|
|
1264
1380
|
key = keyAndIV.key;
|
|
1265
1381
|
iv = keyAndIV.iv;
|
|
1266
1382
|
}
|
|
1267
|
-
const { ciphertext: ciphertextPayload, authTag } = (0, import_core.
|
|
1383
|
+
const { ciphertext: ciphertextPayload, authTag } = (0, import_core.encryptAES128CCMSync)(key, iv, plaintextPayload, authData, SECURITY_S2_AUTH_TAG_LENGTH);
|
|
1268
1384
|
this.key = key;
|
|
1269
1385
|
this.iv = iv;
|
|
1270
1386
|
this.authData = authData;
|
|
@@ -1280,7 +1396,7 @@ let Security2CCMessageEncapsulation = (() => {
|
|
|
1280
1396
|
async serializeAsync(ctx) {
|
|
1281
1397
|
const securityManager = assertSecurityTX(ctx, this.nodeId);
|
|
1282
1398
|
this.ensureSequenceNumber(securityManager);
|
|
1283
|
-
this.
|
|
1399
|
+
await this.maybeAddSPANExtensionAsync(ctx, securityManager);
|
|
1284
1400
|
const unencryptedExtensions = this.extensions.filter((e) => !e.isEncrypted());
|
|
1285
1401
|
const encryptedExtensions = this.extensions.filter((e) => e.isEncrypted());
|
|
1286
1402
|
const unencryptedPayload = import_safe2.Bytes.concat([
|
|
@@ -1301,18 +1417,18 @@ let Security2CCMessageEncapsulation = (() => {
|
|
|
1301
1417
|
let key;
|
|
1302
1418
|
let iv;
|
|
1303
1419
|
if (this.isSinglecast()) {
|
|
1304
|
-
iv = securityManager.
|
|
1420
|
+
iv = await securityManager.nextNonceAsync(this.nodeId, true);
|
|
1305
1421
|
const { keyCCM } = (
|
|
1306
1422
|
// Prefer the overridden security class if it was given
|
|
1307
1423
|
this.securityClass != void 0 ? securityManager.getKeysForSecurityClass(this.securityClass) : securityManager.getKeysForNode(this.nodeId)
|
|
1308
1424
|
);
|
|
1309
1425
|
key = keyCCM;
|
|
1310
1426
|
} else {
|
|
1311
|
-
const keyAndIV = securityManager.
|
|
1427
|
+
const keyAndIV = await securityManager.getMulticastKeyAndIVAsync(destinationTag);
|
|
1312
1428
|
key = keyAndIV.key;
|
|
1313
1429
|
iv = keyAndIV.iv;
|
|
1314
1430
|
}
|
|
1315
|
-
const { ciphertext: ciphertextPayload, authTag } = (0, import_core.
|
|
1431
|
+
const { ciphertext: ciphertextPayload, authTag } = await (0, import_core.encryptAES128CCMAsync)(plaintextPayload, key, iv, authData, SECURITY_S2_AUTH_TAG_LENGTH);
|
|
1316
1432
|
this.key = key;
|
|
1317
1433
|
this.iv = iv;
|
|
1318
1434
|
this.authData = authData;
|
|
@@ -1327,7 +1443,7 @@ let Security2CCMessageEncapsulation = (() => {
|
|
|
1327
1443
|
}
|
|
1328
1444
|
computeEncapsulationOverhead() {
|
|
1329
1445
|
const extensionBytes = this.extensions.map((e) => e.computeLength()).reduce((a, b) => a + b, 0);
|
|
1330
|
-
return super.computeEncapsulationOverhead() + 2 +
|
|
1446
|
+
return super.computeEncapsulationOverhead() + 2 + SECURITY_S2_AUTH_TAG_LENGTH + extensionBytes;
|
|
1331
1447
|
}
|
|
1332
1448
|
toLogEntry(ctx) {
|
|
1333
1449
|
const message = {
|