diff --git a/packages/core/src/security/Manager2.test.ts b/packages/core/src/security/Manager2.test.ts index 8ab40cf1fd12..985661f09edf 100644 --- a/packages/core/src/security/Manager2.test.ts +++ b/packages/core/src/security/Manager2.test.ts @@ -191,6 +191,21 @@ test("createMulticastGroup() -> should return a different group ID for a differe t.not(group1, group2); }); +test("createMulticastGroup() -> should return a different group ID for a different node set for LR nodes", (t) => { + const man = new SecurityManager2(); + dummyInit(man); + const group1 = man.createMulticastGroup( + [260, 261, 262], + SecurityClass.S2_Authenticated, + ); + const group2 = man.createMulticastGroup( + [259, 260, 261], + SecurityClass.S2_Authenticated, + ); + + t.not(group1, group2); +}); + // test("createMulticastGroup() -> should return the same group ID for a previously used node set", (t) => { @@ -208,6 +223,21 @@ test("createMulticastGroup() -> should return the same group ID for a previously t.is(group1, group2); }); +test("createMulticastGroup() -> should return the same group ID for a previously used LR node set", (t) => { + const man = new SecurityManager2(); + dummyInit(man); + const group1 = man.createMulticastGroup( + [260, 261, 262], + SecurityClass.S2_Authenticated, + ); + const group2 = man.createMulticastGroup( + [260, 261, 262], + SecurityClass.S2_Authenticated, + ); + + t.is(group1, group2); +}); + test("getMulticastKeyAndIV() -> should throw if the MPAN state for the given multicast group has not been initialized", (t) => { const man = new SecurityManager2(); assertZWaveError(t, () => man.getMulticastKeyAndIV(1), { diff --git a/packages/core/src/security/Manager2.ts b/packages/core/src/security/Manager2.ts index 019f9fef43f5..afb9e9ae4a09 100644 --- a/packages/core/src/security/Manager2.ts +++ b/packages/core/src/security/Manager2.ts @@ -2,8 +2,9 @@ import { createWrappingCounter, getEnumMemberName } from "@zwave-js/shared"; import * as crypto from "node:crypto"; +import { deflateSync } from "node:zlib"; import { ZWaveError, ZWaveErrorCodes } from "../error/ZWaveError"; -import { encodeNodeBitMask } from "../index_safe"; +import { MAX_NODES_LR, encodeBitMask } from "../index_safe"; import { highResTimestamp } from "../util/date"; import { type S2SecurityClass, SecurityClass } from "./SecurityClass"; import { increment } from "./bufferUtils"; @@ -155,7 +156,7 @@ export class SecurityManager2 { s2SecurityClass: S2SecurityClass, ): number { // Check if we already have a group for these nodes - const newHash = encodeNodeBitMask(nodeIDs).toString("hex"); + const newHash = hashNodeIds(nodeIDs); if (this.multicastGroupLookup.has(newHash)) { return this.multicastGroupLookup.get(newHash)!; } @@ -167,7 +168,7 @@ export class SecurityManager2 { if (this.multicastGroups.has(groupId)) { const oldGroup = this.multicastGroups.get(groupId)!; this.multicastGroups.delete(groupId); - const oldHash = encodeNodeBitMask(oldGroup.nodeIDs).toString("hex"); + const oldHash = hashNodeIds(oldGroup.nodeIDs); this.multicastGroupLookup.delete(oldHash); } @@ -555,3 +556,12 @@ export class SecurityManager2 { this.peerMPANs.get(peerNodeId)!.set(groupId, mpanState); } } + +/** Creates a unique string that can be used to look up existing node ID arrays */ +function hashNodeIds(nodeIds: readonly number[]): string { + const raw = encodeBitMask(nodeIds, MAX_NODES_LR); + // Compress the bitmask to avoid 1000 character strings as keys. + // This compresses considerably well, usually in the 12-20 byte range + const compressed = deflateSync(raw); + return compressed.toString("hex"); +}