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