diff --git a/package.json b/package.json index 67592e7c299d..67b5eeb2c384 100644 --- a/package.json +++ b/package.json @@ -32,7 +32,7 @@ "@actions/core": "^1.11.1", "@actions/exec": "^1.1.1", "@actions/github": "^6.0.0", - "@alcalzone/esm2cjs": "^1.3.0", + "@alcalzone/esm2cjs": "^1.4.0", "@alcalzone/jsonl-db": "^3.1.1", "@alcalzone/monopack": "^1.3.0", "@alcalzone/release-script": "~3.8.0", diff --git a/packages/cc/package.json b/packages/cc/package.json index fad30dde8dfe..4de7358c8d39 100644 --- a/packages/cc/package.json +++ b/packages/cc/package.json @@ -76,7 +76,7 @@ "reflect-metadata": "^0.2.2" }, "devDependencies": { - "@alcalzone/esm2cjs": "^1.3.0", + "@alcalzone/esm2cjs": "^1.4.0", "@microsoft/api-extractor": "^7.47.9", "@types/node": "^18.19.63", "@zwave-js/maintenance": "workspace:*", diff --git a/packages/cc/src/cc/Security2CC.ts b/packages/cc/src/cc/Security2CC.ts index 22173cc6f030..9c2cbe6e6581 100644 --- a/packages/cc/src/cc/Security2CC.ts +++ b/packages/cc/src/cc/Security2CC.ts @@ -6,7 +6,6 @@ import { type MessageRecord, type MulticastDestination, type S2SecurityClass, - SECURITY_S2_AUTH_TAG_LENGTH, SPANState, type SPANTableEntry, SecurityClass, @@ -15,9 +14,11 @@ import { type WithAddress, ZWaveError, ZWaveErrorCodes, - decryptAES128CCM, + decryptAES128CCMAsync, + decryptAES128CCMSync, encodeBitMask, - encryptAES128CCM, + encryptAES128CCMAsync, + encryptAES128CCMSync, getCCName, highResTimestamp, isLongRangeNodeId, @@ -100,6 +101,8 @@ function bitMaskToSecurityClass( return keys[0]; } +const SECURITY_S2_AUTH_TAG_LENGTH = 8; + function getAuthenticationData( sendingNodeId: number, destination: number, @@ -220,7 +223,8 @@ function assertSecurityTX( return ret; } -function decryptSinglecast( +/** @deprecated Use {@link decryptSinglecastAsync} instead */ +function decryptSinglecastSync( ctx: CCParsingContext, securityManager: SecurityManager2, sendingNodeId: number, @@ -243,10 +247,12 @@ function decryptSinglecast( return { key, iv, - ...decryptAES128CCM(key, iv, ciphertext, authData, authTag), + // eslint-disable-next-line @typescript-eslint/no-deprecated + ...decryptAES128CCMSync(key, iv, ciphertext, authData, authTag), }; }; const getNonceAndDecrypt = () => { + // eslint-disable-next-line @typescript-eslint/no-deprecated const iv = securityManager.nextNonce(sendingNodeId); return decryptWithNonce(iv); }; @@ -302,6 +308,7 @@ function decryptSinglecast( ); if (isBootstrappingNode) { // We're currently bootstrapping the node, it might be using a temporary key + // eslint-disable-next-line @typescript-eslint/no-deprecated securityManager.initializeTempSPAN( sendingNodeId, senderEI, @@ -353,6 +360,7 @@ function decryptSinglecast( } // Initialize an SPAN with that security class + // eslint-disable-next-line @typescript-eslint/no-deprecated securityManager.initializeSPAN( sendingNodeId, secClass, @@ -392,7 +400,186 @@ function decryptSinglecast( }; } -function decryptMulticast( +async function decryptSinglecastAsync( + ctx: CCParsingContext, + securityManager: SecurityManager2, + sendingNodeId: number, + curSequenceNumber: number, + prevSequenceNumber: number, + ciphertext: Uint8Array, + authData: Uint8Array, + authTag: Uint8Array, + spanState: SPANTableEntry & { + type: SPANState.SPAN | SPANState.LocalEI; + }, + extensions: Security2Extension[], +): Promise { + const decryptWithNonce = async (nonce: Uint8Array) => { + const { keyCCM: key } = securityManager.getKeysForNode( + sendingNodeId, + ); + + const iv = nonce; + return { + key, + iv, + ...(await decryptAES128CCMAsync( + ciphertext, + key, + iv, + authData, + authTag, + )), + }; + }; + const getNonceAndDecrypt = async () => { + const iv = await securityManager.nextNonceAsync(sendingNodeId); + return decryptWithNonce(iv); + }; + + if (spanState.type === SPANState.SPAN) { + // There SHOULD be a shared SPAN between both parties. But experience has shown that both could have + // sent a command at roughly the same time, using the same SPAN for encryption. + // To avoid a nasty desync and both nodes trying to resync at the same time, causing message loss, + // we accept commands encrypted with the previous SPAN under very specific circumstances: + if ( + // The previous SPAN is still known, i.e. the node didn't send another command that was successfully decrypted + !!spanState.currentSPAN + // it is still valid + && spanState.currentSPAN.expires > highResTimestamp() + // The received command is exactly the next, expected one + && prevSequenceNumber != undefined + && curSequenceNumber === ((prevSequenceNumber + 1) & 0xff) + // And in case of a mock-based test, do this only on the controller + && !ctx.__internalIsMockNode + ) { + const nonce = spanState.currentSPAN.nonce; + spanState.currentSPAN = undefined; + + // If we could decrypt this way, we're done... + const result = await decryptWithNonce(nonce); + if (result.authOK) { + return { + ...result, + securityClass: spanState.securityClass, + }; + } + // ...otherwise, we need to try the normal way + } else { + // forgetting the current SPAN shouldn't be necessary but better be safe than sorry + spanState.currentSPAN = undefined; + } + + // This can only happen if the security class is known + return { + ...(await getNonceAndDecrypt()), + securityClass: spanState.securityClass, + }; + } else if (spanState.type === SPANState.LocalEI) { + // We've sent the other our receiver's EI and received its sender's EI, + // meaning we can now establish an SPAN + const senderEI = getSenderEI(extensions); + if (!senderEI) failNoSPAN(); + const receiverEI = spanState.receiverEI; + + // How we do this depends on whether we know the security class of the other node + const isBootstrappingNode = securityManager.tempKeys.has( + sendingNodeId, + ); + if (isBootstrappingNode) { + // We're currently bootstrapping the node, it might be using a temporary key + await securityManager.initializeTempSPANAsync( + sendingNodeId, + senderEI, + receiverEI, + ); + + const ret = await getNonceAndDecrypt(); + // Decryption with the temporary key worked + if (ret.authOK) { + return { + ...ret, + securityClass: SecurityClass.Temporary, + }; + } + + // Reset the SPAN state and try with the recently granted security class + securityManager.setSPANState( + sendingNodeId, + spanState, + ); + } + + // When ending up here, one of two situations has occured: + // a) We've taken over an existing network and do not know the node's security class + // b) We know the security class, but we're about to establish a new SPAN. This may happen at a lower + // security class than the one the node normally uses, e.g. when we're being queried for securely + // supported CCs. + // In both cases, we should simply try decoding with multiple security classes, starting from the highest one. + // If this fails, we restore the previous (partial) SPAN state. + + // Try all security classes where we do not definitely know that it was not granted + // While bootstrapping a node, we consider the key that is being exchanged (including S0) to be the highest. No need to look at others + const possibleSecurityClasses = isBootstrappingNode + ? [ctx.getHighestSecurityClass(sendingNodeId)!] + : securityClassOrder.filter( + (s) => + ctx.hasSecurityClass(sendingNodeId, s) + !== false, + ); + + for (const secClass of possibleSecurityClasses) { + // Skip security classes we don't have keys for + if ( + !securityManager.hasKeysForSecurityClass( + secClass, + ) + ) { + continue; + } + + // Initialize an SPAN with that security class + await securityManager.initializeSPANAsync( + sendingNodeId, + secClass, + senderEI, + receiverEI, + ); + const ret = await getNonceAndDecrypt(); + + // It worked, return the result + if (ret.authOK) { + // Also if we weren't sure before, we now know that the security class is granted + if ( + ctx.hasSecurityClass(sendingNodeId, secClass) + === undefined + ) { + ctx.setSecurityClass(sendingNodeId, secClass, true); + } + return { + ...ret, + securityClass: secClass, + }; + } else { + // Reset the SPAN state and try with the next security class + securityManager.setSPANState( + sendingNodeId, + spanState, + ); + } + } + } + + // Nothing worked, fail the decryption + return { + plaintext: new Uint8Array(), + authOK: false, + securityClass: undefined, + }; +} + +/** @deprecated Use {@link decryptMulticastAsync} instead */ +function decryptMulticastSync( sendingNodeId: number, securityManager: SecurityManager2, groupId: number, @@ -400,6 +587,7 @@ function decryptMulticast( authData: Uint8Array, authTag: Uint8Array, ): DecryptionResult { + // eslint-disable-next-line @typescript-eslint/no-deprecated const iv = securityManager.nextPeerMPAN( sendingNodeId, groupId, @@ -410,12 +598,124 @@ function decryptMulticast( return { key, iv, - ...decryptAES128CCM(key, iv, ciphertext, authData, authTag), + // eslint-disable-next-line @typescript-eslint/no-deprecated + ...decryptAES128CCMSync(key, iv, ciphertext, authData, authTag), + // The security class is irrelevant when decrypting multicast commands + securityClass: undefined, + }; +} + +async function decryptMulticastAsync( + sendingNodeId: number, + securityManager: SecurityManager2, + groupId: number, + ciphertext: Uint8Array, + authData: Uint8Array, + authTag: Uint8Array, +): Promise { + const iv = await securityManager.nextPeerMPANAsync( + sendingNodeId, + groupId, + ); + const { keyCCM: key } = securityManager.getKeysForNode( + sendingNodeId, + ); + return { + key, + iv, + ...(await decryptAES128CCMAsync( + ciphertext, + key, + iv, + authData, + authTag, + )), // The security class is irrelevant when decrypting multicast commands securityClass: undefined, }; } +function parseExtensions(buffer: Uint8Array, wasEncrypted: boolean): { + extensions: Security2Extension[]; + mustDiscardCommand: boolean; + bytesRead: number; +} { + const extensions: Security2Extension[] = []; + let mustDiscardCommand = false; + let offset = 0; + + parsing: while (true) { + if (buffer.length < offset + 2) { + // An S2 extension was expected, but the buffer is too short + mustDiscardCommand = true; + break parsing; + } + + // The length field could be too large, which would cause part of the actual ciphertext + // to be ignored. Try to avoid this for known extensions by checking the actual and expected length. + const { actual: actualLength, expected: expectedLength } = + Security2Extension + .getExtensionLength( + buffer.subarray(offset), + ); + + // Parse the extension using the expected length if possible + const extensionLength = expectedLength ?? actualLength; + if (extensionLength < 2) { + // An S2 extension was expected, but the length is too short + mustDiscardCommand = true; + break parsing; + } else if ( + extensionLength + > buffer.length + - offset + - (wasEncrypted + ? 0 + : SECURITY_S2_AUTH_TAG_LENGTH) + ) { + // The supposed length is longer than the space the extensions may occupy + mustDiscardCommand = true; + break parsing; + } + + const extensionData = buffer.subarray( + offset, + offset + extensionLength, + ); + offset += extensionLength; + + const ext = Security2Extension.parse(extensionData); + + switch (validateS2Extension(ext, wasEncrypted)) { + case ValidateS2ExtensionResult.OK: + if ( + expectedLength != undefined + && actualLength !== expectedLength + ) { + // The extension length field does not match, ignore the extension + } else { + extensions.push(ext); + } + break; + case ValidateS2ExtensionResult.DiscardExtension: + // Do nothing + break; + case ValidateS2ExtensionResult.DiscardCommand: + mustDiscardCommand = true; + break; + } + + // Check if that was the last extension + if (!ext.moreToFollow) break parsing; + } + + return { + extensions, + mustDiscardCommand, + bytesRead: offset, + }; +} + function getDestinationIDTX( this: Security2CC & { extensions: Security2Extension[] }, ): number { @@ -502,7 +802,7 @@ export class Security2CCAPI extends CCAPI { ); } - const receiverEI = securityManager.generateNonce( + const receiverEI = await securityManager.generateNonceAsync( this.endpoint.nodeId, ); @@ -1384,73 +1684,15 @@ export class Security2CCMessageEncapsulation extends Security2CC { const extensions: Security2Extension[] = []; let mustDiscardCommand = false; - const parseExtensions = (buffer: Uint8Array, wasEncrypted: boolean) => { - while (true) { - if (buffer.length < offset + 2) { - // An S2 extension was expected, but the buffer is too short - mustDiscardCommand = true; - return; - } - - // The length field could be too large, which would cause part of the actual ciphertext - // to be ignored. Try to avoid this for known extensions by checking the actual and expected length. - const { actual: actualLength, expected: expectedLength } = - Security2Extension - .getExtensionLength( - buffer.subarray(offset), - ); - - // Parse the extension using the expected length if possible - const extensionLength = expectedLength ?? actualLength; - if (extensionLength < 2) { - // An S2 extension was expected, but the length is too short - mustDiscardCommand = true; - return; - } else if ( - extensionLength - > buffer.length - - offset - - (wasEncrypted - ? 0 - : SECURITY_S2_AUTH_TAG_LENGTH) - ) { - // The supposed length is longer than the space the extensions may occupy - mustDiscardCommand = true; - return; - } - - const extensionData = buffer.subarray( - offset, - offset + extensionLength, - ); - offset += extensionLength; - - const ext = Security2Extension.parse(extensionData); - - switch (validateS2Extension(ext, wasEncrypted)) { - case ValidateS2ExtensionResult.OK: - if ( - expectedLength != undefined - && actualLength !== expectedLength - ) { - // The extension length field does not match, ignore the extension - } else { - extensions.push(ext); - } - break; - case ValidateS2ExtensionResult.DiscardExtension: - // Do nothing - break; - case ValidateS2ExtensionResult.DiscardCommand: - mustDiscardCommand = true; - break; - } - - // Check if that was the last extension - if (!ext.moreToFollow) break; - } - }; - if (hasExtensions) parseExtensions(raw.payload, false); + if (hasExtensions) { + const parseResult = parseExtensions( + raw.payload.subarray(offset), + false, + ); + extensions.push(...parseResult.extensions); + offset += parseResult.bytesRead; + mustDiscardCommand = parseResult.mustDiscardCommand; + } const mcctx = ((): MulticastContext => { const multicastGroupId = getMulticastGroupId(extensions); @@ -1475,11 +1717,13 @@ export class Security2CCMessageEncapsulation extends Security2CC { // we still need to increment the SPAN or MPAN state if (mustDiscardCommand) { if (mcctx.isMulticast) { + // eslint-disable-next-line @typescript-eslint/no-deprecated securityManager.nextPeerMPAN( ctx.sourceNodeId, mcctx.groupId, ); } else { + // eslint-disable-next-line @typescript-eslint/no-deprecated securityManager.nextNonce(ctx.sourceNodeId); } validatePayload.fail( @@ -1545,7 +1789,8 @@ export class Security2CCMessageEncapsulation extends Security2CC { } decrypt = () => - decryptMulticast( + // eslint-disable-next-line @typescript-eslint/no-deprecated + decryptMulticastSync( ctx.sourceNodeId, securityManager, mcctx.groupId, @@ -1569,7 +1814,8 @@ export class Security2CCMessageEncapsulation extends Security2CC { } decrypt = () => - decryptSinglecast( + // eslint-disable-next-line @typescript-eslint/no-deprecated + decryptSinglecastSync( ctx, securityManager, ctx.sourceNodeId, @@ -1646,7 +1892,12 @@ export class Security2CCMessageEncapsulation extends Security2CC { decryptionSecurityClass; offset = 0; - if (hasEncryptedExtensions) parseExtensions(plaintext, true); + if (hasEncryptedExtensions) { + const parseResult = parseExtensions(plaintext, true); + extensions.push(...parseResult.extensions); + offset += parseResult.bytesRead; + mustDiscardCommand = parseResult.mustDiscardCommand; + } // Before we can continue, check if the command must be discarded if (mustDiscardCommand) { @@ -1723,73 +1974,15 @@ export class Security2CCMessageEncapsulation extends Security2CC { const extensions: Security2Extension[] = []; let mustDiscardCommand = false; - const parseExtensions = (buffer: Uint8Array, wasEncrypted: boolean) => { - while (true) { - if (buffer.length < offset + 2) { - // An S2 extension was expected, but the buffer is too short - mustDiscardCommand = true; - return; - } - - // The length field could be too large, which would cause part of the actual ciphertext - // to be ignored. Try to avoid this for known extensions by checking the actual and expected length. - const { actual: actualLength, expected: expectedLength } = - Security2Extension - .getExtensionLength( - buffer.subarray(offset), - ); - - // Parse the extension using the expected length if possible - const extensionLength = expectedLength ?? actualLength; - if (extensionLength < 2) { - // An S2 extension was expected, but the length is too short - mustDiscardCommand = true; - return; - } else if ( - extensionLength - > buffer.length - - offset - - (wasEncrypted - ? 0 - : SECURITY_S2_AUTH_TAG_LENGTH) - ) { - // The supposed length is longer than the space the extensions may occupy - mustDiscardCommand = true; - return; - } - - const extensionData = buffer.subarray( - offset, - offset + extensionLength, - ); - offset += extensionLength; - - const ext = Security2Extension.parse(extensionData); - - switch (validateS2Extension(ext, wasEncrypted)) { - case ValidateS2ExtensionResult.OK: - if ( - expectedLength != undefined - && actualLength !== expectedLength - ) { - // The extension length field does not match, ignore the extension - } else { - extensions.push(ext); - } - break; - case ValidateS2ExtensionResult.DiscardExtension: - // Do nothing - break; - case ValidateS2ExtensionResult.DiscardCommand: - mustDiscardCommand = true; - break; - } - - // Check if that was the last extension - if (!ext.moreToFollow) break; - } - }; - if (hasExtensions) parseExtensions(raw.payload, false); + if (hasExtensions) { + const parseResult = parseExtensions( + raw.payload.subarray(offset), + false, + ); + extensions.push(...parseResult.extensions); + offset += parseResult.bytesRead; + mustDiscardCommand = parseResult.mustDiscardCommand; + } const mcctx = ((): MulticastContext => { const multicastGroupId = getMulticastGroupId(extensions); @@ -1814,12 +2007,12 @@ export class Security2CCMessageEncapsulation extends Security2CC { // we still need to increment the SPAN or MPAN state if (mustDiscardCommand) { if (mcctx.isMulticast) { - securityManager.nextPeerMPAN( + await securityManager.nextPeerMPANAsync( ctx.sourceNodeId, mcctx.groupId, ); } else { - securityManager.nextNonce(ctx.sourceNodeId); + await securityManager.nextNonceAsync(ctx.sourceNodeId); } validatePayload.fail( "Invalid S2 extension", @@ -1870,7 +2063,7 @@ export class Security2CCMessageEncapsulation extends Security2CC { unencryptedPayload, ); - let decrypt: () => DecryptionResult; + let decrypt: () => Promise; if (mcctx.isMulticast) { // For incoming multicast commands, make sure we have an MPAN if (mpanState?.type !== MPANState.MPAN) { @@ -1884,7 +2077,7 @@ export class Security2CCMessageEncapsulation extends Security2CC { } decrypt = () => - decryptMulticast( + decryptMulticastAsync( ctx.sourceNodeId, securityManager, mcctx.groupId, @@ -1908,7 +2101,7 @@ export class Security2CCMessageEncapsulation extends Security2CC { } decrypt = () => - decryptSinglecast( + decryptSinglecastAsync( ctx, securityManager, ctx.sourceNodeId, @@ -1948,7 +2141,7 @@ export class Security2CCMessageEncapsulation extends Security2CC { key, iv, securityClass: decryptionSecurityClass, - } = decrypt()); + } = await decrypt()); if (!!authOK && !!plaintext) break; // No need to try further SPANs if we just got the sender's EI if (!!getSenderEI(extensions)) break; @@ -1985,7 +2178,12 @@ export class Security2CCMessageEncapsulation extends Security2CC { decryptionSecurityClass; offset = 0; - if (hasEncryptedExtensions) parseExtensions(plaintext, true); + if (hasEncryptedExtensions) { + const parseResult = parseExtensions(plaintext, true); + extensions.push(...parseResult.extensions); + offset += parseResult.bytesRead; + mustDiscardCommand = parseResult.mustDiscardCommand; + } // Before we can continue, check if the command must be discarded if (mustDiscardCommand) { @@ -2092,7 +2290,8 @@ export class Security2CCMessageEncapsulation extends Security2CC { return getMulticastGroupId(this.extensions); } - private maybeAddSPANExtension( + /** @deprecated Use {@link maybeAddSPANExtensionAsync} instead */ + private maybeAddSPANExtensionSync( ctx: CCEncodingContext, securityManager: SecurityManager2, ): void { @@ -2114,6 +2313,7 @@ export class Security2CCMessageEncapsulation extends Security2CC { } else if (spanState.type === SPANState.RemoteEI) { // We have the receiver's EI, generate our input and send it over // With both, we can create an SPAN + // eslint-disable-next-line @typescript-eslint/no-deprecated const senderEI = securityManager.generateNonce( undefined, ); @@ -2125,6 +2325,7 @@ export class Security2CCMessageEncapsulation extends Security2CC { this.securityClass == undefined && securityManager.tempKeys.has(receiverNodeId) ) { + // eslint-disable-next-line @typescript-eslint/no-deprecated securityManager.initializeTempSPAN( receiverNodeId, senderEI, @@ -2140,6 +2341,7 @@ export class Security2CCMessageEncapsulation extends Security2CC { ZWaveErrorCodes.Security2CC_NoSPAN, ); } + // eslint-disable-next-line @typescript-eslint/no-deprecated securityManager.initializeSPAN( receiverNodeId, securityClass, @@ -2161,13 +2363,83 @@ export class Security2CCMessageEncapsulation extends Security2CC { } } + private async maybeAddSPANExtensionAsync( + ctx: CCEncodingContext, + securityManager: SecurityManager2, + ): Promise { + if (!this.isSinglecast()) return; + + const receiverNodeId: number = this.nodeId; + const spanState = securityManager.getSPANState( + receiverNodeId, + ); + if ( + spanState.type === SPANState.None + || spanState.type === SPANState.LocalEI + ) { + // Can't do anything here if we don't have the receiver's EI + throw new ZWaveError( + `Security S2 CC requires the receiver's nonce to be sent!`, + ZWaveErrorCodes.Security2CC_NoSPAN, + ); + } else if (spanState.type === SPANState.RemoteEI) { + // We have the receiver's EI, generate our input and send it over + // With both, we can create an SPAN + const senderEI = await securityManager.generateNonceAsync( + undefined, + ); + const receiverEI = spanState.receiverEI; + + // While bootstrapping a node, prefer the temporary key, unless the + // specific command specifies a security class + if ( + this.securityClass == undefined + && securityManager.tempKeys.has(receiverNodeId) + ) { + await securityManager.initializeTempSPANAsync( + receiverNodeId, + senderEI, + receiverEI, + ); + } else { + const securityClass = this.securityClass + ?? ctx.getHighestSecurityClass(receiverNodeId); + + if (securityClass == undefined) { + throw new ZWaveError( + "No security class defined for this command!", + ZWaveErrorCodes.Security2CC_NoSPAN, + ); + } + await securityManager.initializeSPANAsync( + receiverNodeId, + securityClass, + senderEI, + receiverEI, + ); + } + + // Add or update the SPAN extension + let spanExtension = this.extensions.find( + (e) => e instanceof SPANExtension, + ); + if (spanExtension) { + spanExtension.senderEI = senderEI; + } else { + spanExtension = new SPANExtension({ senderEI }); + this.extensions.push(spanExtension); + } + } + } + /** @deprecated Use {@link serializeAsync} instead */ public serialize(ctx: CCEncodingContext): Bytes { const securityManager = assertSecurityTX(ctx, this.nodeId); this.ensureSequenceNumber(securityManager); // Include Sender EI in the command if we only have the receiver's EI - this.maybeAddSPANExtension(ctx, securityManager); + // eslint-disable-next-line @typescript-eslint/no-deprecated + this.maybeAddSPANExtensionSync(ctx, securityManager); const unencryptedExtensions = this.extensions.filter( (e) => !e.isEncrypted(), @@ -2217,6 +2489,7 @@ export class Security2CCMessageEncapsulation extends Security2CC { // Singlecast: // Generate a nonce for encryption, and remember it to attempt decryption // of potential in-flight messages from the target node. + // eslint-disable-next-line @typescript-eslint/no-deprecated iv = securityManager.nextNonce(this.nodeId, true); const { keyCCM } = // Prefer the overridden security class if it was given @@ -2228,6 +2501,7 @@ export class Security2CCMessageEncapsulation extends Security2CC { key = keyCCM; } else { // Multicast: + // eslint-disable-next-line @typescript-eslint/no-deprecated const keyAndIV = securityManager.getMulticastKeyAndIV( destinationTag, ); @@ -2235,7 +2509,8 @@ export class Security2CCMessageEncapsulation extends Security2CC { iv = keyAndIV.iv; } - const { ciphertext: ciphertextPayload, authTag } = encryptAES128CCM( + // eslint-disable-next-line @typescript-eslint/no-deprecated + const { ciphertext: ciphertextPayload, authTag } = encryptAES128CCMSync( key, iv, plaintextPayload, @@ -2264,7 +2539,7 @@ export class Security2CCMessageEncapsulation extends Security2CC { this.ensureSequenceNumber(securityManager); // Include Sender EI in the command if we only have the receiver's EI - this.maybeAddSPANExtension(ctx, securityManager); + await this.maybeAddSPANExtensionAsync(ctx, securityManager); const unencryptedExtensions = this.extensions.filter( (e) => !e.isEncrypted(), @@ -2313,7 +2588,7 @@ export class Security2CCMessageEncapsulation extends Security2CC { // Singlecast: // Generate a nonce for encryption, and remember it to attempt decryption // of potential in-flight messages from the target node. - iv = securityManager.nextNonce(this.nodeId, true); + iv = await securityManager.nextNonceAsync(this.nodeId, true); const { keyCCM } = // Prefer the overridden security class if it was given this.securityClass != undefined @@ -2324,20 +2599,21 @@ export class Security2CCMessageEncapsulation extends Security2CC { key = keyCCM; } else { // Multicast: - const keyAndIV = securityManager.getMulticastKeyAndIV( + const keyAndIV = await securityManager.getMulticastKeyAndIVAsync( destinationTag, ); key = keyAndIV.key; iv = keyAndIV.iv; } - const { ciphertext: ciphertextPayload, authTag } = encryptAES128CCM( - key, - iv, - plaintextPayload, - authData, - SECURITY_S2_AUTH_TAG_LENGTH, - ); + const { ciphertext: ciphertextPayload, authTag } = + await encryptAES128CCMAsync( + plaintextPayload, + key, + iv, + authData, + SECURITY_S2_AUTH_TAG_LENGTH, + ); // Remember key and IV for debugging purposes this.key = key; diff --git a/packages/cc/src/cc/SecurityCC.ts b/packages/cc/src/cc/SecurityCC.ts index 9844c401a159..7c71b6d4147e 100644 --- a/packages/cc/src/cc/SecurityCC.ts +++ b/packages/cc/src/cc/SecurityCC.ts @@ -10,12 +10,17 @@ import { type WithAddress, ZWaveError, ZWaveErrorCodes, - computeMAC, - decryptAES128OFB, + computeMACAsync, + computeMACSync, + decryptAES128OFBAsync, + decryptAES128OFBSync, encodeCCList, - encryptAES128OFB, - generateAuthKey, - generateEncryptionKey, + encryptAES128OFBAsync, + encryptAES128OFBSync, + generateAuthKeyAsync, + generateAuthKeySync, + generateEncryptionKeyAsync, + generateEncryptionKeySync, getCCName, isTransmissionError, parseCCList, @@ -686,8 +691,10 @@ export class SecurityCCCommandEncapsulation extends SecurityCC { ctx.ownNodeId, encryptedPayload, ); - const expectedAuthCode = computeMAC( + // eslint-disable-next-line @typescript-eslint/no-deprecated + const expectedAuthCode = computeMACSync( authData, + // eslint-disable-next-line @typescript-eslint/no-deprecated ctx.securityManager.authKey, ); // Only accept messages with a correct auth code @@ -696,8 +703,10 @@ export class SecurityCCCommandEncapsulation extends SecurityCC { )(authCode.equals(expectedAuthCode)); // Decrypt the encapsulated CC - const frameControlAndDecryptedCC = decryptAES128OFB( + // eslint-disable-next-line @typescript-eslint/no-deprecated + const frameControlAndDecryptedCC = decryptAES128OFBSync( encryptedPayload, + // eslint-disable-next-line @typescript-eslint/no-deprecated ctx.securityManager.encryptionKey, Bytes.concat([iv, nonce]), ); @@ -724,6 +733,83 @@ export class SecurityCCCommandEncapsulation extends SecurityCC { return ret; } + public static async fromAsync( + raw: CCRaw, + ctx: CCParsingContext, + ): Promise { + assertSecurityRX(ctx); + + // HALF_NONCE_SIZE bytes iv, 1 byte frame control, at least 1 CC byte, 1 byte nonce id, 8 bytes auth code + validatePayload( + raw.payload.length >= HALF_NONCE_SIZE + 1 + 1 + 1 + 8, + ); + const iv = raw.payload.subarray(0, HALF_NONCE_SIZE); + const encryptedPayload = raw.payload.subarray(HALF_NONCE_SIZE, -9); + const nonceId = raw.payload.at(-9)!; + const authCode = raw.payload.subarray(-8); + + // Retrieve the used nonce from the nonce store + const nonce = ctx.securityManager.getNonce(nonceId); + // Only accept the message if the nonce hasn't expired + if (!nonce) { + validatePayload.fail( + `Nonce ${ + num2hex( + nonceId, + ) + } expired, cannot decode security encapsulated command.`, + ); + } + // and mark the nonce as used + ctx.securityManager.deleteNonce(nonceId); + + // Validate the encrypted data + const authData = getAuthenticationData( + iv, + nonce, + SecurityCommand.CommandEncapsulation, + ctx.sourceNodeId, + ctx.ownNodeId, + encryptedPayload, + ); + const expectedAuthCode = await computeMACAsync( + authData, + await ctx.securityManager.getAuthKey(), + ); + // Only accept messages with a correct auth code + validatePayload.withReason( + "Invalid auth code, won't accept security encapsulated command.", + )(authCode.equals(expectedAuthCode)); + + // Decrypt the encapsulated CC + const frameControlAndDecryptedCC = await decryptAES128OFBAsync( + encryptedPayload, + await ctx.securityManager.getEncryptionKey(), + Bytes.concat([iv, nonce]), + ); + const frameControl = frameControlAndDecryptedCC[0]; + const sequenceCounter = frameControl & 0b1111; + const sequenced = !!(frameControl & 0b1_0000); + const secondFrame = !!(frameControl & 0b10_0000); + const decryptedCCBytes: Uint8Array | undefined = + frameControlAndDecryptedCC + .subarray(1); + + const ret = new SecurityCCCommandEncapsulation({ + nodeId: ctx.sourceNodeId, + sequenceCounter, + sequenced, + secondFrame, + decryptedCCBytes, + }); + + ret.authData = authData; + ret.authCode = authCode; + ret.iv = iv; + + return ret; + } + private sequenced: boolean | undefined; private secondFrame: boolean | undefined; private sequenceCounter: number | undefined; @@ -809,12 +895,16 @@ export class SecurityCCCommandEncapsulation extends SecurityCC { let authKey: Uint8Array; let encryptionKey: Uint8Array; if (this.alternativeNetworkKey) { - authKey = generateAuthKey(this.alternativeNetworkKey); - encryptionKey = generateEncryptionKey( + // eslint-disable-next-line @typescript-eslint/no-deprecated + authKey = generateAuthKeySync(this.alternativeNetworkKey); + // eslint-disable-next-line @typescript-eslint/no-deprecated + encryptionKey = generateEncryptionKeySync( this.alternativeNetworkKey, ); } else { + // eslint-disable-next-line @typescript-eslint/no-deprecated authKey = ctx.securityManager.authKey; + // eslint-disable-next-line @typescript-eslint/no-deprecated encryptionKey = ctx.securityManager.encryptionKey; } @@ -827,7 +917,8 @@ export class SecurityCCCommandEncapsulation extends SecurityCC { // Encrypt the payload const senderNonce = randomBytes(HALF_NONCE_SIZE); const iv = Bytes.concat([senderNonce, this.nonce]); - const ciphertext = encryptAES128OFB(plaintext, encryptionKey, iv); + // eslint-disable-next-line @typescript-eslint/no-deprecated + const ciphertext = encryptAES128OFBSync(plaintext, encryptionKey, iv); // And generate the auth code const authData = getAuthenticationData( senderNonce, @@ -837,7 +928,8 @@ export class SecurityCCCommandEncapsulation extends SecurityCC { this.nodeId, ciphertext, ); - const authCode = computeMAC(authData, authKey); + // eslint-disable-next-line @typescript-eslint/no-deprecated + const authCode = computeMACSync(authData, authKey); // Remember for debugging purposes this.iv = iv; @@ -865,13 +957,13 @@ export class SecurityCCCommandEncapsulation extends SecurityCC { let authKey: Uint8Array; let encryptionKey: Uint8Array; if (this.alternativeNetworkKey) { - authKey = generateAuthKey(this.alternativeNetworkKey); - encryptionKey = generateEncryptionKey( + authKey = await generateAuthKeyAsync(this.alternativeNetworkKey); + encryptionKey = await generateEncryptionKeyAsync( this.alternativeNetworkKey, ); } else { - authKey = ctx.securityManager.authKey; - encryptionKey = ctx.securityManager.encryptionKey; + authKey = await ctx.securityManager.getAuthKey(); + encryptionKey = await ctx.securityManager.getEncryptionKey(); } const serializedCC = await this.encapsulated.serializeAsync(ctx); @@ -882,7 +974,11 @@ export class SecurityCCCommandEncapsulation extends SecurityCC { // Encrypt the payload const senderNonce = randomBytes(HALF_NONCE_SIZE); const iv = Bytes.concat([senderNonce, this.nonce]); - const ciphertext = encryptAES128OFB(plaintext, encryptionKey, iv); + const ciphertext = await encryptAES128OFBAsync( + plaintext, + encryptionKey, + iv, + ); // And generate the auth code const authData = getAuthenticationData( senderNonce, @@ -892,7 +988,7 @@ export class SecurityCCCommandEncapsulation extends SecurityCC { this.nodeId, ciphertext, ); - const authCode = computeMAC(authData, authKey); + const authCode = await computeMACAsync(authData, authKey); // Remember for debugging purposes this.iv = iv; diff --git a/packages/config/package.json b/packages/config/package.json index 5db8f1ffc56b..74de446b2227 100644 --- a/packages/config/package.json +++ b/packages/config/package.json @@ -68,7 +68,7 @@ "winston": "^3.15.0" }, "devDependencies": { - "@alcalzone/esm2cjs": "^1.3.0", + "@alcalzone/esm2cjs": "^1.4.0", "@microsoft/api-extractor": "^7.47.9", "@types/js-levenshtein": "^1.1.3", "@types/json-logic-js": "^2.0.7", diff --git a/packages/core/package.json b/packages/core/package.json index 7ea121a4ca0a..ebda8b249bf0 100644 --- a/packages/core/package.json +++ b/packages/core/package.json @@ -60,6 +60,14 @@ "sideEffects": [ "./reflection/decorators.js" ], + "imports": { + "#crypto_primitives": { + "@@dev": "./src/crypto/primitives/primitives.node.ts", + "require": "./build/cjs/crypto/primitives/primitives.node.js", + "node": "./build/esm/crypto/primitives/primitives.node.js", + "default": "./build/esm/crypto/primitives/primitives.browser.js" + } + }, "files": [ "build/**/*.{js,cjs,mjs,d.ts,d.cts,d.mts,map}", "build/**/package.json" @@ -110,7 +118,7 @@ "winston-transport": "^4.8.0" }, "devDependencies": { - "@alcalzone/esm2cjs": "^1.3.0", + "@alcalzone/esm2cjs": "^1.4.0", "@microsoft/api-extractor": "^7.47.9", "@types/node": "^18.19.63", "@types/semver": "^7.5.8", diff --git a/packages/core/src/crypto/index.browser.ts b/packages/core/src/crypto/index.browser.ts new file mode 100644 index 000000000000..42b186a98c17 --- /dev/null +++ b/packages/core/src/crypto/index.browser.ts @@ -0,0 +1,15 @@ +export { + computeCMAC as computeCMACAsync, + computeMAC as computeMACAsync, + computeNoncePRK as computeNoncePRKAsync, + computePRK as computePRKAsync, + decryptAES128CCM as decryptAES128CCMAsync, + decryptAES128OFB as decryptAES128OFBAsync, + deriveMEI as deriveMEIAsync, + deriveNetworkKeys as deriveNetworkKeysAsync, + deriveTempKeys as deriveTempKeysAsync, + encryptAES128CCM as encryptAES128CCMAsync, + encryptAES128ECB as encryptAES128ECBAsync, + encryptAES128OFB as encryptAES128OFBAsync, + randomBytes as randomBytesAsync, +} from "./operations.async.js"; diff --git a/packages/core/src/crypto/index.node.ts b/packages/core/src/crypto/index.node.ts new file mode 100644 index 000000000000..228db673ab60 --- /dev/null +++ b/packages/core/src/crypto/index.node.ts @@ -0,0 +1,36 @@ +export { + type KeyPair, + extractRawECDHPrivateKey as extractRawECDHPrivateKeySync, + extractRawECDHPublicKey as extractRawECDHPublicKeySync, + generateECDHKeyPair as generateECDHKeyPairSync, + importRawECDHPrivateKey as importRawECDHPrivateKeySync, + importRawECDHPublicKey as importRawECDHPublicKeySync, + keyPairFromRawECDHPrivateKey as keyPairFromRawECDHPrivateKeySync, +} from "./keys.sync.js"; +export { + computeCMAC as computeCMACAsync, + computeMAC as computeMACAsync, + computeNoncePRK as computeNoncePRKAsync, + computePRK as computePRKAsync, + decryptAES128CCM as decryptAES128CCMAsync, + decryptAES128OFB as decryptAES128OFBAsync, + deriveMEI as deriveMEIAsync, + deriveNetworkKeys as deriveNetworkKeysAsync, + deriveTempKeys as deriveTempKeysAsync, + encryptAES128CCM as encryptAES128CCMAsync, + encryptAES128ECB as encryptAES128ECBAsync, + encryptAES128OFB as encryptAES128OFBAsync, + randomBytes as randomBytesAsync, +} from "./operations.async.js"; +export { + computeCMAC as computeCMACSync, + computeMAC as computeMACSync, + computeNoncePRK as computeNoncePRKSync, + computePRK as computePRKSync, + decryptAES128CCM as decryptAES128CCMSync, + decryptAES128OFB as decryptAES128OFBSync, + encryptAES128CCM as encryptAES128CCMSync, + encryptAES128ECB as encryptAES128ECBSync, + encryptAES128OFB as encryptAES128OFBSync, + randomBytes as randomBytesSync, +} from "./operations.sync.js"; diff --git a/packages/core/src/crypto/keys.sync.ts b/packages/core/src/crypto/keys.sync.ts new file mode 100644 index 000000000000..64305f0fab5c --- /dev/null +++ b/packages/core/src/crypto/keys.sync.ts @@ -0,0 +1,85 @@ +import * as crypto from "node:crypto"; +import { + decodeX25519KeyDER, + encodeX25519KeyDERPKCS8, + encodeX25519KeyDERSPKI, +} from "./shared.js"; + +export interface KeyPair { + publicKey: crypto.KeyObject; + privateKey: crypto.KeyObject; +} + +/** Generates an x25519 / ECDH key pair */ +export function generateECDHKeyPair(): KeyPair { + return crypto.generateKeyPairSync("x25519"); +} + +export function keyPairFromRawECDHPrivateKey(privateKey: Uint8Array): KeyPair { + const privateKeyObject = importRawECDHPrivateKey(privateKey); + const publicKeyObject = crypto.createPublicKey(privateKeyObject); + return { + privateKey: privateKeyObject, + publicKey: publicKeyObject, + }; +} + +/** Takes an ECDH public KeyObject and returns the raw key as a buffer */ +export function extractRawECDHPublicKey( + publicKey: crypto.KeyObject, +): Uint8Array { + return decodeX25519KeyDER( + publicKey.export({ + type: "spki", + format: "der", + }), + ); +} + +/** Converts a raw public key to an ECDH KeyObject */ +export function importRawECDHPublicKey( + publicKey: Uint8Array, +): crypto.KeyObject { + return crypto.createPublicKey({ + // eslint-disable-next-line no-restricted-globals -- crypto API requires Buffer instances + key: Buffer.from(encodeX25519KeyDERSPKI(publicKey).buffer), + format: "der", + type: "spki", + }); +} + +/** Takes an ECDH private KeyObject and returns the raw key as a buffer */ +export function extractRawECDHPrivateKey( + privateKey: crypto.KeyObject, +): Uint8Array { + return decodeX25519KeyDER( + privateKey.export({ + type: "pkcs8", + format: "der", + }), + ); +} + +/** Converts a raw private key to an ECDH KeyObject */ +export function importRawECDHPrivateKey( + privateKey: Uint8Array, +): crypto.KeyObject { + return crypto.createPrivateKey({ + // eslint-disable-next-line no-restricted-globals -- crypto API requires Buffer instances + key: Buffer.from(encodeX25519KeyDERPKCS8(privateKey).buffer), + format: "der", + type: "pkcs8", + }); +} + +// Decoding with asn1js for reference: +// const asn1 = require("asn1js"); +// const public = asn1.fromBER(keypair.publicKey.buffer); +// const private = asn1.fromBER(keypair.privateKey.buffer); +// const privateKeyBER = private.result.valueBlock.value[2].valueBlock.valueHex; +// const privateKey = Buffer.from( +// asn1.fromBER(privateKeyBER).result.valueBlock.valueHex, +// ); +// const publicKey = Buffer.from( +// public.result.valueBlock.value[1].valueBlock.valueHex, +// ); diff --git a/packages/core/src/crypto/operations.async.test.ts b/packages/core/src/crypto/operations.async.test.ts new file mode 100644 index 000000000000..af3037ed1381 --- /dev/null +++ b/packages/core/src/crypto/operations.async.test.ts @@ -0,0 +1,76 @@ +import { Bytes } from "@zwave-js/shared/safe"; +import { type ExpectStatic, test } from "vitest"; +import { computeCMAC, computeMAC } from "./operations.async.js"; + +function assertBufferEquals( + expect: ExpectStatic, + actual: Uint8Array, + expected: Uint8Array, +) { + expect(Uint8Array.from(actual)).toStrictEqual(Uint8Array.from(expected)); +} + +test(`computeMAC() -> should work correctly`, async (t) => { + // Test vector taken from https://nvlpubs.nist.gov/nistpubs/Legacy/SP/nistspecialpublication800-38a.pdf + const key = Bytes.from("2b7e151628aed2a6abf7158809cf4f3c", "hex"); + // The Z-Wave specs use 16 zeros, but we only found test vectors for this + const iv = Bytes.from("000102030405060708090a0b0c0d0e0f", "hex"); + const plaintext = Bytes.from("6bc1bee22e409f96e93d7e117393172a", "hex"); + const expected = Bytes.from("7649abac8119b246", "hex"); + const actual = await computeMAC(plaintext, key, iv); + assertBufferEquals(t.expect, actual, expected); +}); + +test(`computeMAC() -> should work correctly (part 2)`, async (t) => { + // Taken from real Z-Wave communication - if anything must be changed, this is the test case to keep! + const key = Bytes.from("c5fe1ca17d36c992731a0c0c468c1ef9", "hex"); + const plaintext = Bytes.from( + "ddd360c382a437514392826cbba0b3128114010cf3fb762d6e82126681c18597", + "hex", + ); + const expected = Bytes.from("2bc20a8aa9bbb371", "hex"); + const actual = await computeMAC(plaintext, key); + assertBufferEquals(t.expect, actual, expected); +}); + +test(`computeCMAC() -> should work correctly (part 1)`, async (t) => { + // Test vector taken from https://csrc.nist.gov/CSRC/media/Projects/Cryptographic-Standards-and-Guidelines/documents/examples/AES_CMAC.pdf + const key = Bytes.from("2B7E151628AED2A6ABF7158809CF4F3C", "hex"); + const plaintext = new Bytes(); + const expected = Bytes.from("BB1D6929E95937287FA37D129B756746", "hex"); + const actual = await computeCMAC(plaintext, key); + assertBufferEquals(t.expect, actual, expected); +}); + +test(`computeCMAC() -> should work correctly (part 2)`, async (t) => { + // Test vector taken from https://csrc.nist.gov/CSRC/media/Projects/Cryptographic-Standards-and-Guidelines/documents/examples/AES_CMAC.pdf + const key = Bytes.from("2B7E151628AED2A6ABF7158809CF4F3C", "hex"); + const plaintext = Bytes.from("6BC1BEE22E409F96E93D7E117393172A", "hex"); + const expected = Bytes.from("070A16B46B4D4144F79BDD9DD04A287C", "hex"); + const actual = await computeCMAC(plaintext, key); + assertBufferEquals(t.expect, actual, expected); +}); + +test(`computeCMAC() -> should work correctly (part 3)`, async (t) => { + // Test vector taken from https://csrc.nist.gov/CSRC/media/Projects/Cryptographic-Standards-and-Guidelines/documents/examples/AES_CMAC.pdf + const key = Bytes.from("2B7E151628AED2A6ABF7158809CF4F3C", "hex"); + const plaintext = Bytes.from( + "6BC1BEE22E409F96E93D7E117393172AAE2D8A57", + "hex", + ); + const expected = Bytes.from("7D85449EA6EA19C823A7BF78837DFADE", "hex"); + const actual = await computeCMAC(plaintext, key); + assertBufferEquals(t.expect, actual, expected); +}); + +test(`computeCMAC() -> should work correctly (part 4)`, async (t) => { + // Test vector taken from https://csrc.nist.gov/CSRC/media/Projects/Cryptographic-Standards-and-Guidelines/documents/examples/AES_CMAC.pdf + const key = Bytes.from("2B7E151628AED2A6ABF7158809CF4F3C", "hex"); + const plaintext = Bytes.from( + "6BC1BEE22E409F96E93D7E117393172AAE2D8A571E03AC9C9EB76FAC45AF8E5130C81C46A35CE411E5FBC1191A0A52EFF69F2445DF4F9B17AD2B417BE66C3710", + "hex", + ); + const expected = Bytes.from("51F0BEBF7E3B9D92FC49741779363CFE", "hex"); + const actual = await computeCMAC(plaintext, key); + assertBufferEquals(t.expect, actual, expected); +}); diff --git a/packages/core/src/crypto/operations.async.ts b/packages/core/src/crypto/operations.async.ts new file mode 100644 index 000000000000..ed3c7a78c2d4 --- /dev/null +++ b/packages/core/src/crypto/operations.async.ts @@ -0,0 +1,176 @@ +import { Bytes } from "@zwave-js/shared/safe"; +import { BLOCK_SIZE, leftShift1, xor, zeroPad } from "./shared.js"; + +// Import the correct primitives based on the environment +import primitives from "#crypto_primitives"; +const { + decryptAES128OFB, + encryptAES128CBC, + encryptAES128ECB, + encryptAES128OFB, + encryptAES128CCM, + decryptAES128CCM, + randomBytes, +} = primitives; + +export { + decryptAES128CCM, + decryptAES128OFB, + encryptAES128CBC, + encryptAES128CCM, + encryptAES128ECB, + encryptAES128OFB, + randomBytes, +}; + +const Z128 = new Uint8Array(16).fill(0); +const R128 = Bytes.from("00000000000000000000000000000087", "hex"); +const constantPRK = new Uint8Array(16).fill(0x33); +const constantTE = new Uint8Array(15).fill(0x88); +const constantNK = new Uint8Array(15).fill(0x55); +const constantNonce = new Uint8Array(16).fill(0x26); +const constantEI = new Uint8Array(15).fill(0x88); + +/** Computes a message authentication code for Security S0 (as described in SDS10865) */ +export async function computeMAC( + authData: Uint8Array, + key: Uint8Array, + iv: Uint8Array = new Uint8Array(BLOCK_SIZE).fill(0), +): Promise { + const ciphertext = await encryptAES128CBC(authData, key, iv); + // The MAC is the first 8 bytes of the last 16 byte block + return ciphertext.subarray(ciphertext.length - BLOCK_SIZE).subarray(0, 8); +} + +async function generateAES128CMACSubkeys( + key: Uint8Array, +): Promise<[k1: Uint8Array, k2: Uint8Array]> { + // NIST SP 800-38B, chapter 6.1 + const L = await encryptAES128ECB(Z128, key); + const k1 = !(L[0] & 0x80) ? leftShift1(L) : xor(leftShift1(L), R128); + const k2 = !(k1[0] & 0x80) ? leftShift1(k1) : xor(leftShift1(k1), R128); + return [k1, k2]; +} + +/** Computes a message authentication code for Security S2 (as described in SDS13783) */ +export async function computeCMAC( + message: Uint8Array, + key: Uint8Array, +): Promise { + const blockSize = 16; + const numBlocks = Math.ceil(message.length / blockSize); + let lastBlock = message.subarray((numBlocks - 1) * blockSize); + const lastBlockIsComplete = message.length > 0 + && message.length % blockSize === 0; + if (!lastBlockIsComplete) { + lastBlock = zeroPad( + Bytes.concat([lastBlock, Bytes.from([0x80])]), + blockSize, + ).output; + } + + // Compute all steps but the last one + let ret = Z128; + for (let i = 0; i < numBlocks - 1; i++) { + ret = xor(ret, message.subarray(i * blockSize, (i + 1) * blockSize)); + ret = await encryptAES128ECB(ret, key); + } + // Compute the last step + const [k1, k2] = await generateAES128CMACSubkeys(key); + ret = xor(ret, xor(lastBlockIsComplete ? k1 : k2, lastBlock)); + ret = await encryptAES128ECB(ret, key); + + return ret.subarray(0, blockSize); +} + +/** Computes the Pseudo Random Key (PRK) used to derive auth, encryption and nonce keys */ +export function computePRK( + ecdhSharedSecret: Uint8Array, + pubKeyA: Uint8Array, + pubKeyB: Uint8Array, +): Promise { + const message = Bytes.concat([ecdhSharedSecret, pubKeyA, pubKeyB]); + return computeCMAC(message, constantPRK); +} + +/** Derives the temporary auth, encryption and nonce keys from the PRK */ +export async function deriveTempKeys( + PRK: Uint8Array, +): Promise<{ tempKeyCCM: Uint8Array; tempPersonalizationString: Uint8Array }> { + const T1 = await computeCMAC( + Bytes.concat([constantTE, [0x01]]), + PRK, + ); + const T2 = await computeCMAC( + Bytes.concat([T1, constantTE, [0x02]]), + PRK, + ); + const T3 = await computeCMAC( + Bytes.concat([T2, constantTE, [0x03]]), + PRK, + ); + return { + tempKeyCCM: T1, + tempPersonalizationString: Bytes.concat([T2, T3]), + }; +} + +/** Derives the CCM, MPAN keys and the personalization string from the permanent network key (PNK) */ +export async function deriveNetworkKeys( + PNK: Uint8Array, +): Promise< + { + keyCCM: Uint8Array; + keyMPAN: Uint8Array; + personalizationString: Uint8Array; + } +> { + const T1 = await computeCMAC( + Bytes.concat([constantNK, [0x01]]), + PNK, + ); + const T2 = await computeCMAC( + Bytes.concat([T1, constantNK, [0x02]]), + PNK, + ); + const T3 = await computeCMAC( + Bytes.concat([T2, constantNK, [0x03]]), + PNK, + ); + const T4 = await computeCMAC( + Bytes.concat([T3, constantNK, [0x04]]), + PNK, + ); + return { + keyCCM: T1, + keyMPAN: T4, + personalizationString: Bytes.concat([T2, T3]), + }; +} + +/** Computes the Pseudo Random Key (PRK) used to derive the mixed entropy input (MEI) for nonce generation */ +export function computeNoncePRK( + senderEI: Uint8Array, + receiverEI: Uint8Array, +): Promise { + const message = Bytes.concat([senderEI, receiverEI]); + return computeCMAC(message, constantNonce); +} + +/** Derives the MEI from the nonce PRK */ +export async function deriveMEI(noncePRK: Uint8Array): Promise { + const T1 = await computeCMAC( + Bytes.concat([ + constantEI, + [0x00], + constantEI, + [0x01], + ]), + noncePRK, + ); + const T2 = await computeCMAC( + Bytes.concat([T1, constantEI, [0x02]]), + noncePRK, + ); + return Bytes.concat([T1, T2]); +} diff --git a/packages/core/src/crypto/operations.sync.test.ts b/packages/core/src/crypto/operations.sync.test.ts new file mode 100644 index 000000000000..e0dee2a579c4 --- /dev/null +++ b/packages/core/src/crypto/operations.sync.test.ts @@ -0,0 +1,77 @@ +/* eslint-disable @typescript-eslint/no-deprecated */ +import { Bytes } from "@zwave-js/shared/safe"; +import { type ExpectStatic, test } from "vitest"; +import { computeCMAC, computeMAC } from "./operations.sync.js"; + +function assertBufferEquals( + expect: ExpectStatic, + actual: Uint8Array, + expected: Uint8Array, +) { + expect(Uint8Array.from(actual)).toStrictEqual(Uint8Array.from(expected)); +} + +test(`computeMAC() -> should work correctly`, async (t) => { + // Test vector taken from https://nvlpubs.nist.gov/nistpubs/Legacy/SP/nistspecialpublication800-38a.pdf + const key = Bytes.from("2b7e151628aed2a6abf7158809cf4f3c", "hex"); + // The Z-Wave specs use 16 zeros, but we only found test vectors for this + const iv = Bytes.from("000102030405060708090a0b0c0d0e0f", "hex"); + const plaintext = Bytes.from("6bc1bee22e409f96e93d7e117393172a", "hex"); + const expected = Bytes.from("7649abac8119b246", "hex"); + const actual = computeMAC(plaintext, key, iv); + assertBufferEquals(t.expect, actual, expected); +}); + +test(`computeMAC() -> should work correctly (part 2)`, async (t) => { + // Taken from real Z-Wave communication - if anything must be changed, this is the test case to keep! + const key = Bytes.from("c5fe1ca17d36c992731a0c0c468c1ef9", "hex"); + const plaintext = Bytes.from( + "ddd360c382a437514392826cbba0b3128114010cf3fb762d6e82126681c18597", + "hex", + ); + const expected = Bytes.from("2bc20a8aa9bbb371", "hex"); + const actual = computeMAC(plaintext, key); + assertBufferEquals(t.expect, actual, expected); +}); + +test(`computeCMAC() -> should work correctly (part 1)`, async (t) => { + // Test vector taken from https://csrc.nist.gov/CSRC/media/Projects/Cryptographic-Standards-and-Guidelines/documents/examples/AES_CMAC.pdf + const key = Bytes.from("2B7E151628AED2A6ABF7158809CF4F3C", "hex"); + const plaintext = new Bytes(); + const expected = Bytes.from("BB1D6929E95937287FA37D129B756746", "hex"); + const actual = computeCMAC(plaintext, key); + assertBufferEquals(t.expect, actual, expected); +}); + +test(`computeCMAC() -> should work correctly (part 2)`, async (t) => { + // Test vector taken from https://csrc.nist.gov/CSRC/media/Projects/Cryptographic-Standards-and-Guidelines/documents/examples/AES_CMAC.pdf + const key = Bytes.from("2B7E151628AED2A6ABF7158809CF4F3C", "hex"); + const plaintext = Bytes.from("6BC1BEE22E409F96E93D7E117393172A", "hex"); + const expected = Bytes.from("070A16B46B4D4144F79BDD9DD04A287C", "hex"); + const actual = computeCMAC(plaintext, key); + assertBufferEquals(t.expect, actual, expected); +}); + +test(`computeCMAC() -> should work correctly (part 3)`, async (t) => { + // Test vector taken from https://csrc.nist.gov/CSRC/media/Projects/Cryptographic-Standards-and-Guidelines/documents/examples/AES_CMAC.pdf + const key = Bytes.from("2B7E151628AED2A6ABF7158809CF4F3C", "hex"); + const plaintext = Bytes.from( + "6BC1BEE22E409F96E93D7E117393172AAE2D8A57", + "hex", + ); + const expected = Bytes.from("7D85449EA6EA19C823A7BF78837DFADE", "hex"); + const actual = computeCMAC(plaintext, key); + assertBufferEquals(t.expect, actual, expected); +}); + +test(`computeCMAC() -> should work correctly (part 4)`, async (t) => { + // Test vector taken from https://csrc.nist.gov/CSRC/media/Projects/Cryptographic-Standards-and-Guidelines/documents/examples/AES_CMAC.pdf + const key = Bytes.from("2B7E151628AED2A6ABF7158809CF4F3C", "hex"); + const plaintext = Bytes.from( + "6BC1BEE22E409F96E93D7E117393172AAE2D8A571E03AC9C9EB76FAC45AF8E5130C81C46A35CE411E5FBC1191A0A52EFF69F2445DF4F9B17AD2B417BE66C3710", + "hex", + ); + const expected = Bytes.from("51F0BEBF7E3B9D92FC49741779363CFE", "hex"); + const actual = computeCMAC(plaintext, key); + assertBufferEquals(t.expect, actual, expected); +}); diff --git a/packages/core/src/security/crypto.ts b/packages/core/src/crypto/operations.sync.ts similarity index 60% rename from packages/core/src/security/crypto.ts rename to packages/core/src/crypto/operations.sync.ts index beb3fb7e11f1..4a21ebbe7778 100644 --- a/packages/core/src/security/crypto.ts +++ b/packages/core/src/crypto/operations.sync.ts @@ -1,6 +1,19 @@ +/* eslint-disable @typescript-eslint/no-deprecated */ import { Bytes } from "@zwave-js/shared/safe"; -import * as crypto from "node:crypto"; -import { leftShift1, xor, zeroPad } from "./bufferUtils.js"; +import crypto from "node:crypto"; +import { leftShift1, xor, zeroPad } from "./shared.js"; + +const Z128 = new Uint8Array(16).fill(0); +const R128 = Bytes.from("00000000000000000000000000000087", "hex"); +const constantPRK = new Uint8Array(16).fill(0x33); +const constantTE = new Uint8Array(15).fill(0x88); +const constantNK = new Uint8Array(15).fill(0x55); +const constantNonce = new Uint8Array(16).fill(0x26); +const constantEI = new Uint8Array(15).fill(0x88); + +export function randomBytes(length: number): Uint8Array { + return crypto.randomBytes(length); +} function encrypt( algorithm: string, @@ -44,7 +57,10 @@ function decrypt( } } -/** Encrypts a payload using AES-128-ECB (as described in SDS10865) */ +/** + * Encrypts a payload using AES-128-ECB (as described in SDS10865) + * @deprecated Use the async version of this function instead + */ export function encryptAES128ECB( plaintext: Uint8Array, key: Uint8Array, @@ -52,7 +68,10 @@ export function encryptAES128ECB( return encrypt("aes-128-ecb", 16, false, plaintext, key, new Uint8Array()); } -/** Encrypts a payload using AES-OFB (as described in SDS10865) */ +/** + * Encrypts a payload using AES-OFB (as described in SDS10865) + * @deprecated Use the async version of this function instead + */ export const encryptAES128OFB = encrypt.bind( undefined, "aes-128-ofb", @@ -60,7 +79,10 @@ export const encryptAES128OFB = encrypt.bind( true, ); -/** Decrypts a payload using AES-OFB (as described in SDS10865) */ +/** + * Decrypts a payload using AES-OFB (as described in SDS10865) + * @deprecated Use the async version of this function instead + */ export const decryptAES128OFB = decrypt.bind( undefined, "aes-128-ofb", @@ -68,7 +90,10 @@ export const decryptAES128OFB = decrypt.bind( true, ); -/** Computes a message authentication code for Security S0 (as described in SDS10865) */ +/** + * Computes a message authentication code for Security S0 (as described in SDS10865) + * @deprecated Use the async version of this function instead + */ export function computeMAC( authData: Uint8Array, key: Uint8Array, @@ -79,109 +104,6 @@ export function computeMAC( return ciphertext.subarray(-16, -8); } -/** Decodes a DER-encoded x25519 key (PKCS#8 or SPKI) */ -export function decodeX25519KeyDER(key: Uint8Array): Uint8Array { - // We could parse this with asn1js but that doesn't seem necessary for now - return key.subarray(-32); -} - -/** Encodes an x25519 key from a raw buffer with DER/PKCS#8 */ -export function encodeX25519KeyDERPKCS8(key: Uint8Array): Uint8Array { - // We could encode this with asn1js but that doesn't seem necessary for now - return Bytes.concat([ - Bytes.from("302e020100300506032b656e04220420", "hex"), - key, - ]); -} - -/** Encodes an x25519 key from a raw buffer with DER/SPKI */ -export function encodeX25519KeyDERSPKI(key: Uint8Array): Uint8Array { - // We could encode this with asn1js but that doesn't seem necessary for now - return Bytes.concat([Bytes.from("302a300506032b656e032100", "hex"), key]); -} - -export interface KeyPair { - publicKey: crypto.KeyObject; - privateKey: crypto.KeyObject; -} - -/** Generates an x25519 / ECDH key pair */ -export function generateECDHKeyPair(): KeyPair { - return crypto.generateKeyPairSync("x25519"); -} - -export function keyPairFromRawECDHPrivateKey(privateKey: Uint8Array): KeyPair { - const privateKeyObject = importRawECDHPrivateKey(privateKey); - const publicKeyObject = crypto.createPublicKey(privateKeyObject); - return { - privateKey: privateKeyObject, - publicKey: publicKeyObject, - }; -} - -/** Takes an ECDH public KeyObject and returns the raw key as a buffer */ -export function extractRawECDHPublicKey( - publicKey: crypto.KeyObject, -): Uint8Array { - return decodeX25519KeyDER( - publicKey.export({ - type: "spki", - format: "der", - }), - ); -} - -/** Converts a raw public key to an ECDH KeyObject */ -export function importRawECDHPublicKey( - publicKey: Uint8Array, -): crypto.KeyObject { - return crypto.createPublicKey({ - // eslint-disable-next-line no-restricted-globals -- crypto API requires Buffer instances - key: Buffer.from(encodeX25519KeyDERSPKI(publicKey).buffer), - format: "der", - type: "spki", - }); -} - -/** Takes an ECDH private KeyObject and returns the raw key as a buffer */ -export function extractRawECDHPrivateKey( - privateKey: crypto.KeyObject, -): Uint8Array { - return decodeX25519KeyDER( - privateKey.export({ - type: "pkcs8", - format: "der", - }), - ); -} - -/** Converts a raw private key to an ECDH KeyObject */ -export function importRawECDHPrivateKey( - privateKey: Uint8Array, -): crypto.KeyObject { - return crypto.createPrivateKey({ - // eslint-disable-next-line no-restricted-globals -- crypto API requires Buffer instances - key: Buffer.from(encodeX25519KeyDERPKCS8(privateKey).buffer), - format: "der", - type: "pkcs8", - }); -} - -// Decoding with asn1js for reference: -// const asn1 = require("asn1js"); -// const public = asn1.fromBER(keypair.publicKey.buffer); -// const private = asn1.fromBER(keypair.privateKey.buffer); -// const privateKeyBER = private.result.valueBlock.value[2].valueBlock.valueHex; -// const privateKey = Buffer.from( -// asn1.fromBER(privateKeyBER).result.valueBlock.valueHex, -// ); -// const publicKey = Buffer.from( -// public.result.valueBlock.value[1].valueBlock.valueHex, -// ); - -const Z128 = new Uint8Array(16).fill(0); -const R128 = Bytes.from("00000000000000000000000000000087", "hex"); - function generateAES128CMACSubkeys( key: Uint8Array, ): [k1: Uint8Array, k2: Uint8Array] { @@ -192,7 +114,10 @@ function generateAES128CMACSubkeys( return [k1, k2]; } -/** Computes a message authentication code for Security S2 (as described in SDS13783) */ +/** + * Computes a message authentication code for Security S2 (as described in SDS13783) + * @deprecated Use the async version of this function instead + */ export function computeCMAC(message: Uint8Array, key: Uint8Array): Uint8Array { const blockSize = 16; const numBlocks = Math.ceil(message.length / blockSize); @@ -220,9 +145,10 @@ export function computeCMAC(message: Uint8Array, key: Uint8Array): Uint8Array { return ret.subarray(0, blockSize); } -const constantPRK = new Uint8Array(16).fill(0x33); - -/** Computes the Pseudo Random Key (PRK) used to derive auth, encryption and nonce keys */ +/** + * Computes the Pseudo Random Key (PRK) used to derive auth, encryption and nonce keys + * @deprecated Use the async version of this function instead + */ export function computePRK( ecdhSharedSecret: Uint8Array, pubKeyA: Uint8Array, @@ -232,9 +158,10 @@ export function computePRK( return computeCMAC(message, constantPRK); } -const constantTE = new Uint8Array(15).fill(0x88); - -/** Derives the temporary auth, encryption and nonce keys from the PRK */ +/** + * Derives the temporary auth, encryption and nonce keys from the PRK + * @deprecated Use the async version of this function instead + */ export function deriveTempKeys(PRK: Uint8Array): { tempKeyCCM: Uint8Array; tempPersonalizationString: Uint8Array; @@ -257,9 +184,10 @@ export function deriveTempKeys(PRK: Uint8Array): { }; } -const constantNK = new Uint8Array(15).fill(0x55); - -/** Derives the CCM, MPAN keys and the personalization string from the permanent network key (PNK) */ +/** + * Derives the CCM, MPAN keys and the personalization string from the permanent network key (PNK) + * @deprecated Use the async version of this function instead + */ export function deriveNetworkKeys(PNK: Uint8Array): { keyCCM: Uint8Array; keyMPAN: Uint8Array; @@ -288,9 +216,10 @@ export function deriveNetworkKeys(PNK: Uint8Array): { }; } -const constantNonce = new Uint8Array(16).fill(0x26); - -/** Computes the Pseudo Random Key (PRK) used to derive the mixed entropy input (MEI) for nonce generation */ +/** + * Computes the Pseudo Random Key (PRK) used to derive the mixed entropy input (MEI) for nonce generation + * @deprecated Use the async version of this function instead + */ export function computeNoncePRK( senderEI: Uint8Array, receiverEI: Uint8Array, @@ -299,9 +228,10 @@ export function computeNoncePRK( return computeCMAC(message, constantNonce); } -const constantEI = new Uint8Array(15).fill(0x88); - -/** Derives the MEI from the nonce PRK */ +/** + * Derives the MEI from the nonce PRK + * @deprecated Use the async version of this function instead + */ export function deriveMEI(noncePRK: Uint8Array): Uint8Array { const T1 = computeCMAC( Bytes.concat([ @@ -319,8 +249,10 @@ export function deriveMEI(noncePRK: Uint8Array): Uint8Array { return Bytes.concat([T1, T2]); } -export const SECURITY_S2_AUTH_TAG_LENGTH = 8; - +/** + * Encrypts a payload using AES-CCM + * @deprecated Use the async version of this function instead + */ export function encryptAES128CCM( key: Uint8Array, iv: Uint8Array, @@ -341,6 +273,10 @@ export function encryptAES128CCM( return { ciphertext, authTag }; } +/** + * Decrypts a payload using AES-CCM + * @deprecated Use the async version of this function instead + */ export function decryptAES128CCM( key: Uint8Array, iv: Uint8Array, diff --git a/packages/core/src/crypto/primitives/primitives.browser.ts b/packages/core/src/crypto/primitives/primitives.browser.ts new file mode 100644 index 000000000000..f70b3db0d573 --- /dev/null +++ b/packages/core/src/crypto/primitives/primitives.browser.ts @@ -0,0 +1,370 @@ +import { Bytes } from "@zwave-js/shared/safe"; +import { BLOCK_SIZE, xor, zeroPad } from "../shared.js"; +import { type CryptoPrimitives } from "./primitives.js"; + +const webcrypto = typeof process !== "undefined" + && (globalThis as any).crypto === undefined + && typeof require === "function" + // Node.js <= 18 + // eslint-disable-next-line @typescript-eslint/no-require-imports + ? require("node:crypto").webcrypto + // @ts-expect-error Node.js 18 is missing the types for this + : globalThis.crypto as typeof import("node:crypto").webcrypto; + +const { subtle } = webcrypto; + +function randomBytes(length: number): Uint8Array { + const buffer = new Uint8Array(length); + return webcrypto.getRandomValues(buffer); +} + +/** Encrypts a payload using AES-128-ECB */ +async function encryptAES128ECB( + plaintext: Uint8Array, + key: Uint8Array, +): Promise { + // ECB for a single block is identical to the CBC mode, with an IV of all zeros + // FIXME: Assert that both the plaintext and the key are 16 bytes long + return encryptAES128CBC(plaintext, key, new Uint8Array(BLOCK_SIZE).fill(0)); +} + +/** Encrypts a payload using AES-128-CBC */ +async function encryptAES128CBC( + plaintext: Uint8Array, + key: Uint8Array, + iv: Uint8Array, +): Promise { + const cryptoKey = await subtle.importKey( + "raw", + key, + { name: "AES-CBC" }, + true, + ["encrypt"], + ); + const ciphertext = await subtle.encrypt( + { + name: "AES-CBC", + iv, + }, + cryptoKey, + plaintext, + ); + + // The WebCrypto API adds 16 bytes PKCS#7 padding, but we're only interested + // in the blocks which correspond to the plaintext + const paddedLength = Math.ceil(plaintext.length / BLOCK_SIZE) * BLOCK_SIZE; + return new Uint8Array(ciphertext, 0, paddedLength); +} + +/** Encrypts a payload using AES-128-OFB */ +async function encryptAES128OFB( + plaintext: Uint8Array, + key: Uint8Array, + iv: Uint8Array, +): Promise { + // The Web Crypto API does not support OFB mode, but it supports CTR mode. + // We can use that to fake OFB mode, by using the IV as the value for the counter + // when encrypting block 1, and ciphertext N-1 XOR plaintext N-1 as the counter + // when encrypting block N. + + const cryptoKey = await subtle.importKey( + "raw", + key, + { name: "AES-CTR" }, + true, + [ + "encrypt", + "decrypt", + ], + ); + + const ret = new Uint8Array(plaintext.length); + let counter = zeroPad(iv, BLOCK_SIZE).output; + + for (let offset = 0; offset < plaintext.length - 1; offset += BLOCK_SIZE) { + const input = plaintext.slice(offset, offset + BLOCK_SIZE); + const ciphertextBuffer = await subtle.encrypt( + { + name: "AES-CTR", + counter, + length: BLOCK_SIZE * 8, + }, + cryptoKey, + input, + ); + const ciphertext = new Uint8Array(ciphertextBuffer); + ret.set(ciphertext, offset); + + // Determine the next counter value + counter = zeroPad( + xor(ciphertext, input), + BLOCK_SIZE, + ).output; + } + + return ret; +} + +/** Decrypts a payload using AES-128-OFB */ +async function decryptAES128OFB( + ciphertext: Uint8Array, + key: Uint8Array, + iv: Uint8Array, +): Promise { + // The Web Crypto API does not support OFB mode, but it supports CTR mode. + // We can use that to fake OFB mode, by using the IV as the value for the counter + // when encrypting block 1, and ciphertext N-1 XOR plaintext N-1 as the counter + // when encrypting block N. + + const cryptoKey = await subtle.importKey( + "raw", + key, + { name: "AES-CTR" }, + true, + [ + "encrypt", + "decrypt", + ], + ); + + const ret = new Uint8Array(ciphertext.length); + let counter = zeroPad(iv, BLOCK_SIZE).output; + + for (let offset = 0; offset < ciphertext.length - 1; offset += BLOCK_SIZE) { + const input = ciphertext.slice(offset, offset + BLOCK_SIZE); + const plaintextBuffer = await subtle.decrypt( + { + name: "AES-CTR", + counter, + length: BLOCK_SIZE * 8, + }, + cryptoKey, + input, + ); + const plaintext = new Uint8Array(plaintextBuffer); + ret.set(plaintext, offset); + + // Determine the next counter value + counter = zeroPad( + xor(plaintext, input), + BLOCK_SIZE, + ).output; + } + + return ret; +} + +async function encryptAES128CCM( + plaintext: Uint8Array, + key: Uint8Array, + iv: Uint8Array, + additionalData: Uint8Array, + authTagLength: number, +): Promise<{ ciphertext: Uint8Array; authTag: Uint8Array }> { + // FIXME: Validate iv and authTagLength + + const M = (authTagLength - 2) >> 1; + const L = 15 - iv.length; + const hasAData = additionalData.length > 0; + + const plaintextBlocks = getCCMPlaintextBlocks(plaintext); + + // First step: Authentication + const B = getCCMAuthenticationBlocks( + hasAData, + M, + L, + iv, + plaintext, + additionalData, + plaintextBlocks, + ); + const X = await computeCBCMac(B, key); + + // Second step: Encryption + const A0 = new Uint8Array(BLOCK_SIZE); + A0[0] = (L - 1) & 0b111; + A0.set(iv, 1); + // remaining bytes are initially 0 + + const cryptoKey = await subtle.importKey( + "raw", + key, + { name: "AES-CTR" }, + true, + ["encrypt"], + ); + + const encryptionInput = Bytes.concat([X, plaintextBlocks]); + const encryptionOutput = await subtle.encrypt( + { + name: "AES-CTR", + counter: A0, + length: BLOCK_SIZE * 8, + }, + cryptoKey, + encryptionInput, + ); + const authTagAndCiphertext = new Uint8Array(encryptionOutput); + + const authTag = authTagAndCiphertext.slice(0, authTagLength); + const ciphertext = authTagAndCiphertext.slice(BLOCK_SIZE).slice( + 0, + plaintext.length, + ); + + return { ciphertext, authTag }; +} + +async function computeCBCMac(B: Bytes, key: Uint8Array) { + // There is an opportunity here to optimize memory usage by keeping only the last block + // during the encryption process + const macOutput = await encryptAES128CBC( + B, + key, + new Uint8Array(BLOCK_SIZE).fill(0), + ); + const X = macOutput.subarray(-BLOCK_SIZE); + return X; +} + +function getCCMPlaintextBlocks(plaintext: Uint8Array) { + const plaintextBlocks = new Bytes( + // plaintext | ...padding + Math.ceil(plaintext.length / BLOCK_SIZE) * BLOCK_SIZE, + ); + plaintextBlocks.set(plaintext, 0); + return plaintextBlocks; +} + +function getCCMAuthenticationBlocks( + hasAData: boolean, + M: number, + L: number, + iv: Uint8Array, + plaintext: Uint8Array, + additionalData: Uint8Array, + plaintextBlocks: Bytes, +) { + const B0 = new Bytes(BLOCK_SIZE); + B0[0] = (hasAData ? 64 : 0) + | ((M & 0b111) << 3) + | ((L - 1) & 0b111); + B0.set(iv, 1); + B0.writeUIntBE(plaintext.length, 16 - L, L); + + let aDataLength: Bytes; + if (additionalData.length === 0) { + aDataLength = new Bytes(0); + } else if (additionalData.length < 0xff00) { + aDataLength = new Bytes(2); + aDataLength.writeUInt16BE(additionalData.length, 0); + } else if (additionalData.length <= 4294967295) { + aDataLength = new Bytes(6); + aDataLength.writeUInt16BE(0xfffe, 0); + aDataLength.writeUInt32BE(additionalData.length, 2); + } else { + // Technically goes up to 2^64-1, but JS can only handle up to 2^53-1 + aDataLength = new Bytes(10); + aDataLength.writeUInt16BE(0xffff, 0); + aDataLength.writeBigUInt64BE(BigInt(additionalData.length), 2); + } + + const aDataBlocks = new Bytes( + // B0 | aDataLength | additionalData | ...padding + Math.ceil( + (BLOCK_SIZE + aDataLength.length + additionalData.length) + / BLOCK_SIZE, + ) * BLOCK_SIZE, + ); + aDataBlocks.set(B0, 0); + aDataBlocks.set(aDataLength, BLOCK_SIZE); + aDataBlocks.set(additionalData, BLOCK_SIZE + aDataLength.length); + + const B = Bytes.concat([aDataBlocks, plaintextBlocks]); + return B; +} + +async function decryptAES128CCM( + ciphertext: Uint8Array, + key: Uint8Array, + iv: Uint8Array, + additionalData: Uint8Array, + authTag: Uint8Array, +): Promise<{ plaintext: Uint8Array; authOK: boolean }> { + const M = (authTag.length - 2) >> 1; + const L = 15 - iv.length; + const hasAData = additionalData.length > 0; + + // First step: Decryption + const A0 = new Uint8Array(BLOCK_SIZE); + A0[0] = (L - 1) & 0b111; + A0.set(iv, 1); + // remaining bytes are initially 0 + + const cryptoKey = await subtle.importKey( + "raw", + key, + { name: "AES-CTR" }, + true, + ["decrypt"], + ); + + // Input to the decryption function is the padded auth tag + // and the ciphertext: authTag | 0... | ciphertext + const paddedAuthTag = new Bytes(BLOCK_SIZE); + paddedAuthTag.set(authTag, 0); + const decryptionInput = Bytes.concat([paddedAuthTag, ciphertext]); + const decryptionOutput = await subtle.decrypt( + { + name: "AES-CTR", + counter: A0, + length: BLOCK_SIZE * 8, + }, + cryptoKey, + decryptionInput, + ); + const plaintextAndT = new Uint8Array(decryptionOutput); + const T = plaintextAndT.slice(0, authTag.length); + const plaintext = plaintextAndT.slice(BLOCK_SIZE); + + const plaintextBlocks = getCCMPlaintextBlocks(plaintext); + const B = getCCMAuthenticationBlocks( + hasAData, + M, + L, + iv, + plaintext, + additionalData, + plaintextBlocks, + ); + const X = await computeCBCMac(B, key); + + const expectedAuthTag = X.subarray(0, authTag.length); + + // Compare the expected and actual auth tags in constant time + const emptyPlaintext = new Uint8Array(); + let result = 0; + + if (T.length !== expectedAuthTag.length) { + return { plaintext: emptyPlaintext, authOK: false }; + } + for (let i = 0; i < T.length; i++) { + result |= T[i] ^ expectedAuthTag[i]; + } + if (result === 0) { + return { plaintext, authOK: true }; + } else { + return { plaintext: emptyPlaintext, authOK: false }; + } +} + +export default { + randomBytes, + encryptAES128ECB, + encryptAES128CBC, + encryptAES128OFB, + decryptAES128OFB, + encryptAES128CCM, + decryptAES128CCM, +} satisfies CryptoPrimitives; diff --git a/packages/core/src/crypto/primitives/primitives.node.ts b/packages/core/src/crypto/primitives/primitives.node.ts new file mode 100644 index 000000000000..2a82e95ddbe7 --- /dev/null +++ b/packages/core/src/crypto/primitives/primitives.node.ts @@ -0,0 +1,182 @@ +import { Bytes } from "@zwave-js/shared/safe"; +import crypto from "node:crypto"; +import { BLOCK_SIZE, zeroPad } from "../shared.js"; +import { type CryptoPrimitives } from "./primitives.js"; + +// For Node.js, we use the built-in crypto module since it has better support +// for some algorithms Z-Wave needs than the Web Crypto API, so we can implement +// those without additional operations. + +function randomBytes(length: number): Uint8Array { + return crypto.randomBytes(length); +} + +function encrypt( + algorithm: string, + blockSize: number, + trimToInputLength: boolean, + input: Uint8Array, + key: Uint8Array, + iv: Uint8Array, +): Uint8Array { + const cipher = crypto.createCipheriv(algorithm, key, iv); + cipher.setAutoPadding(false); + + const { output: plaintext, paddingLength } = zeroPad(input, blockSize); + const ret = Bytes.concat([cipher.update(plaintext), cipher.final()]); + + if (trimToInputLength && paddingLength > 0) { + return ret.subarray(0, -paddingLength); + } else { + return ret; + } +} + +function decrypt( + algorithm: string, + blockSize: number, + trimToInputLength: boolean, + input: Uint8Array, + key: Uint8Array, + iv: Uint8Array, +): Uint8Array { + const cipher = crypto.createDecipheriv(algorithm, key, iv); + cipher.setAutoPadding(false); + + const { output: ciphertext, paddingLength } = zeroPad(input, blockSize); + const ret = Bytes.concat([cipher.update(ciphertext), cipher.final()]); + + if (trimToInputLength && paddingLength > 0) { + return ret.subarray(0, -paddingLength); + } else { + return ret; + } +} + +/** Encrypts a payload using AES-128-ECB (as described in SDS10865) */ +function encryptAES128ECB( + plaintext: Uint8Array, + key: Uint8Array, +): Promise { + return Promise.resolve( + encrypt( + "aes-128-ecb", + BLOCK_SIZE, + false, + plaintext, + key, + new Uint8Array(), + ), + ); +} + +/** Encrypts a payload using AES-OFB (as described in SDS10865) */ +function encryptAES128OFB( + plaintext: Uint8Array, + key: Uint8Array, + iv: Uint8Array, +): Promise { + return Promise.resolve( + encrypt( + "aes-128-ofb", + BLOCK_SIZE, + true, + plaintext, + key, + iv, + ), + ); +} + +/** Decrypts a payload using AES-OFB */ +function decryptAES128OFB( + ciphertext: Uint8Array, + key: Uint8Array, + iv: Uint8Array, +): Promise { + return Promise.resolve( + decrypt( + "aes-128-ofb", + BLOCK_SIZE, + true, + ciphertext, + key, + iv, + ), + ); +} + +function encryptAES128CBC( + plaintext: Uint8Array, + key: Uint8Array, + iv: Uint8Array, +): Promise { + return Promise.resolve( + encrypt( + "aes-128-cbc", + BLOCK_SIZE, + false, + plaintext, + key, + iv, + ), + ); +} + +function encryptAES128CCM( + plaintext: Uint8Array, + key: Uint8Array, + iv: Uint8Array, + additionalData: Uint8Array, + authTagLength: number, +): Promise<{ ciphertext: Uint8Array; authTag: Uint8Array }> { + // prepare encryption + const algorithm = `aes-128-ccm`; + const cipher = crypto.createCipheriv(algorithm, key, iv, { authTagLength }); + cipher.setAAD(additionalData, { plaintextLength: plaintext.length }); + + // do encryption + const ciphertext = cipher.update(plaintext); + cipher.final(); + const authTag = cipher.getAuthTag(); + + return Promise.resolve({ ciphertext, authTag }); +} + +function decryptAES128CCM( + ciphertext: Uint8Array, + key: Uint8Array, + iv: Uint8Array, + additionalData: Uint8Array, + authTag: Uint8Array, +): Promise<{ plaintext: Uint8Array; authOK: boolean }> { + // prepare decryption + const algorithm = `aes-128-ccm`; + const decipher = crypto.createDecipheriv(algorithm, key, iv, { + authTagLength: authTag.length, + }); + decipher.setAuthTag(authTag); + decipher.setAAD(additionalData, { plaintextLength: ciphertext.length }); + + // do decryption + const plaintext = decipher.update(ciphertext); + // verify decryption + let authOK = false; + try { + decipher.final(); + authOK = true; + } catch { + /* nothing to do */ + } + return Promise.resolve({ plaintext, authOK }); +} + +export default { + randomBytes, + encryptAES128ECB, + encryptAES128CBC, + encryptAES128OFB, + decryptAES128OFB, + encryptAES128CCM, + decryptAES128CCM, +} satisfies CryptoPrimitives; diff --git a/packages/core/src/crypto/primitives/primitives.test.ts b/packages/core/src/crypto/primitives/primitives.test.ts new file mode 100644 index 000000000000..a8ec4f839166 --- /dev/null +++ b/packages/core/src/crypto/primitives/primitives.test.ts @@ -0,0 +1,364 @@ +import { Bytes } from "@zwave-js/shared/safe"; +import { type ExpectStatic, test } from "vitest"; +import { type CryptoPrimitives } from "./primitives.js"; + +function assertBufferEquals( + expect: ExpectStatic, + actual: Uint8Array, + expected: Uint8Array, +) { + expect(Uint8Array.from(actual)).toStrictEqual(Uint8Array.from(expected)); +} + +for ( + const primitives of [ + "./primitives.browser.js", + "./primitives.node.js", + ] as const +) { + const { + decryptAES128OFB, + encryptAES128ECB, + encryptAES128OFB, + encryptAES128CCM, + decryptAES128CCM, + randomBytes, + }: CryptoPrimitives = (await import(primitives)).default; + + test(`${primitives} -> encryptAES128ECB() -> should work correctly`, async (t) => { + // // Test vector taken from https://nvlpubs.nist.gov/nistpubs/Legacy/SP/nistspecialpublication800-38a.pdf + const key = Bytes.from("2b7e151628aed2a6abf7158809cf4f3c", "hex"); + const plaintext = Bytes.from("6bc1bee22e409f96e93d7e117393172a", "hex"); + const expected = Bytes.from("3ad77bb40d7a3660a89ecaf32466ef97", "hex"); + const actual = await encryptAES128ECB(plaintext, key); + assertBufferEquals(t.expect, actual, expected); + }); + + test(`${primitives} -> encryptAES128OFB() -> should work correctly, part 1`, async (t) => { + // Test vector taken from https://nvlpubs.nist.gov/nistpubs/Legacy/SP/nistspecialpublication800-38a.pdf + const key = Bytes.from("2b7e151628aed2a6abf7158809cf4f3c", "hex"); + const iv = Bytes.from("000102030405060708090a0b0c0d0e0f", "hex"); + const plaintext = Bytes.from("6bc1bee22e409f96e93d7e117393172a", "hex"); + const expected = Bytes.from("3b3fd92eb72dad20333449f8e83cfb4a", "hex"); + const actual = await encryptAES128OFB(plaintext, key, iv); + assertBufferEquals(t.expect, actual, expected); + }); + + test(`${primitives} -> encryptAES128OFB() -> should work correctly, part 2`, async (t) => { + // Test vector taken from https://nvlpubs.nist.gov/nistpubs/Legacy/SP/nistspecialpublication800-38a.pdf + const key = Bytes.from("2b7e151628aed2a6abf7158809cf4f3c", "hex"); + const iv = Bytes.from("000102030405060708090a0b0c0d0e0f", "hex"); + const plaintext = Bytes.from( + "6bc1bee22e409f96e93d7e117393172aae2d8a571e03ac9c9eb76fac45af8e5130c81c46a35ce411e5fbc1191a0a52eff69f2445df4f9b17ad2b417be66c3710", + "hex", + ); + const expected = Bytes.from( + "3b3fd92eb72dad20333449f8e83cfb4a7789508d16918f03f53c52dac54ed8259740051e9c5fecf64344f7a82260edcc304c6528f659c77866a510d9c1d6ae5e", + "hex", + ); + const actual = await encryptAES128OFB(plaintext, key, iv); + assertBufferEquals(t.expect, actual, expected); + }); + + test(`${primitives} -> decryptAES128OFB() -> should work correctly, part 1`, async (t) => { + // Test vector taken from https://nvlpubs.nist.gov/nistpubs/Legacy/SP/nistspecialpublication800-38a.pdf + const key = Bytes.from("2b7e151628aed2a6abf7158809cf4f3c", "hex"); + const iv = Bytes.from("000102030405060708090a0b0c0d0e0f", "hex"); + const ciphertext = Bytes.from( + "3b3fd92eb72dad20333449f8e83cfb4a", + "hex", + ); + const expected = Bytes.from("6bc1bee22e409f96e93d7e117393172a", "hex"); + const actual = await decryptAES128OFB(ciphertext, key, iv); + assertBufferEquals(t.expect, actual, expected); + }); + + test(`${primitives} -> decryptAES128OFB() -> should work correctly, part 2`, async (t) => { + // Test vector taken from https://nvlpubs.nist.gov/nistpubs/Legacy/SP/nistspecialpublication800-38a.pdf + const key = Bytes.from("2b7e151628aed2a6abf7158809cf4f3c", "hex"); + const iv = Bytes.from("000102030405060708090a0b0c0d0e0f", "hex"); + const ciphertext = Bytes.from( + "3b3fd92eb72dad20333449f8e83cfb4a7789508d16918f03f53c52dac54ed8259740051e9c5fecf64344f7a82260edcc304c6528f659c77866a510d9c1d6ae5e", + "hex", + ); + const expected = Bytes.from( + "6bc1bee22e409f96e93d7e117393172aae2d8a571e03ac9c9eb76fac45af8e5130c81c46a35ce411e5fbc1191a0a52eff69f2445df4f9b17ad2b417be66c3710", + "hex", + ); + const actual = await decryptAES128OFB(ciphertext, key, iv); + assertBufferEquals(t.expect, actual, expected); + }); + + test(`${primitives} -> decryptAES128OFB() -> should correctly decrypt a real packet`, async (t) => { + // Taken from an OZW log: + // Raw: 0x9881 78193fd7b91995ba 47645ec33fcdb3994b104ebd712e8b7fbd9120d049 28 4e39c14a0dc9aee5 + // Decrypted Packet: 0x009803008685598e60725a845b7170807aef2526ef + // Nonce: 0x2866211bff3783d6 + // Network Key: 0x0102030405060708090a0b0c0d0e0f10 + + const key = await encryptAES128ECB( + new Uint8Array(16).fill(0xaa), + Bytes.from("0102030405060708090a0b0c0d0e0f10", "hex"), + ); + const iv = Bytes.from("78193fd7b91995ba2866211bff3783d6", "hex"); + const ciphertext = Bytes.from( + "47645ec33fcdb3994b104ebd712e8b7fbd9120d049", + "hex", + ); + const plaintext = await decryptAES128OFB(ciphertext, key, iv); + const expected = Bytes.from( + "009803008685598e60725a845b7170807aef2526ef", + "hex", + ); + assertBufferEquals(t.expect, plaintext, expected); + }); + + test(`${primitives} -> encryptAES128OFB() / decryptAES128OFB() -> should be able to en- and decrypt the same data`, async (t) => { + const plaintextIn = + "Lorem ipsum dolor sit amet, consetetur sadipscing elitr, sed diam nonumy eirmod tempor invidunt ut labore et dolore magna aliquyam"; + const key = randomBytes(16); + const iv = randomBytes(16); + const ciphertext = await encryptAES128OFB( + Bytes.from(plaintextIn), + key, + iv, + ); + const plaintextBuffer = await decryptAES128OFB(ciphertext, key, iv); + const plaintextOut = Bytes.view(plaintextBuffer).toString(); + t.expect(plaintextOut).toBe(plaintextIn); + }); + + test(`${primitives} -> encryptAES128CCM() -> should work correctly (part 1)`, async (t) => { + const key = Bytes.from("404142434445464748494a4b4c4d4e4f", "hex"); + const iv = Bytes.from("10111213141516", "hex"); + const additionalData = Bytes.from("0001020304050607", "hex"); + const plaintext = Bytes.from("20212223", "hex"); + const expectedCiphertext = Bytes.from("7162015b", "hex"); + const expectedAuthTag = Bytes.from("4dac255d", "hex"); + const actual = await encryptAES128CCM( + plaintext, + key, + iv, + additionalData, + expectedAuthTag.length, + ); + assertBufferEquals(t.expect, actual.ciphertext, expectedCiphertext); + assertBufferEquals(t.expect, actual.authTag, expectedAuthTag); + }); + + test(`${primitives} -> encryptAES128CCM() -> should work correctly (part 2)`, async (t) => { + const key = Bytes.from("404142434445464748494a4b4c4d4e4f", "hex"); + const iv = Bytes.from("1011121314151617", "hex"); + const additionalData = Bytes.from( + "000102030405060708090a0b0c0d0e0f", + "hex", + ); + const plaintext = Bytes.from("202122232425262728292a2b2c2d2e2f", "hex"); + const expectedCiphertext = Bytes.from( + "d2a1f0e051ea5f62081a7792073d593d", + "hex", + ); + const expectedAuthTag = Bytes.from("1fc64fbfaccd", "hex"); + const actual = await encryptAES128CCM( + plaintext, + key, + iv, + additionalData, + expectedAuthTag.length, + ); + assertBufferEquals(t.expect, actual.ciphertext, expectedCiphertext); + assertBufferEquals(t.expect, actual.authTag, expectedAuthTag); + }); + + test(`${primitives} -> encryptAES128CCM() -> should work correctly (part 3)`, async (t) => { + const key = Bytes.from("404142434445464748494a4b4c4d4e4f", "hex"); + const iv = Bytes.from("101112131415161718191a1b", "hex"); + const additionalData = Bytes.from( + "000102030405060708090a0b0c0d0e0f10111213", + "hex", + ); + const plaintext = Bytes.from( + "202122232425262728292a2b2c2d2e2f3031323334353637", + "hex", + ); + const expectedCiphertext = Bytes.from( + "e3b201a9f5b71a7a9b1ceaeccd97e70b6176aad9a4428aa5", + "hex", + ); + const expectedAuthTag = Bytes.from("484392fbc1b09951", "hex"); + const actual = await encryptAES128CCM( + plaintext, + key, + iv, + additionalData, + expectedAuthTag.length, + ); + assertBufferEquals(t.expect, actual.ciphertext, expectedCiphertext); + assertBufferEquals(t.expect, actual.authTag, expectedAuthTag); + }); + + test(`${primitives} -> decryptAES128CCM() -> should work correctly (part 1)`, async (t) => { + const key = Bytes.from("404142434445464748494a4b4c4d4e4f", "hex"); + const iv = Bytes.from("10111213141516", "hex"); + const additionalData = Bytes.from("0001020304050607", "hex"); + + const ciphertext = Bytes.from("7162015b", "hex"); + const authTag = Bytes.from("4dac255d", "hex"); + + const expectedPlaintext = Bytes.from("20212223", "hex"); + const expectedAuthOK = true; + const actual = await decryptAES128CCM( + ciphertext, + key, + iv, + additionalData, + authTag, + ); + + assertBufferEquals(t.expect, actual.plaintext, expectedPlaintext); + t.expect(actual.authOK).toBe(expectedAuthOK); + }); + + test(`${primitives} -> decryptAES128CCM() -> should work correctly (part 2)`, async (t) => { + const key = Bytes.from("404142434445464748494a4b4c4d4e4f", "hex"); + const iv = Bytes.from("1011121314151617", "hex"); + const additionalData = Bytes.from( + "000102030405060708090a0b0c0d0e0f", + "hex", + ); + + const ciphertext = Bytes.from( + "d2a1f0e051ea5f62081a7792073d593d", + "hex", + ); + const authTag = Bytes.from("1fc64fbfaccd", "hex"); + + const expectedPlaintext = Bytes.from( + "202122232425262728292a2b2c2d2e2f", + "hex", + ); + const expectedAuthOK = true; + const actual = await decryptAES128CCM( + ciphertext, + key, + iv, + additionalData, + authTag, + ); + + assertBufferEquals(t.expect, actual.plaintext, expectedPlaintext); + t.expect(actual.authOK).toBe(expectedAuthOK); + }); + + test(`${primitives} -> decryptAES128CCM() -> should work correctly (part 3)`, async (t) => { + const key = Bytes.from("404142434445464748494a4b4c4d4e4f", "hex"); + const iv = Bytes.from("101112131415161718191a1b", "hex"); + const additionalData = Bytes.from( + "000102030405060708090a0b0c0d0e0f10111213", + "hex", + ); + + const ciphertext = Bytes.from( + "e3b201a9f5b71a7a9b1ceaeccd97e70b6176aad9a4428aa5", + "hex", + ); + const authTag = Bytes.from("484392fbc1b09951", "hex"); + + const expectedPlaintext = Bytes.from( + "202122232425262728292a2b2c2d2e2f3031323334353637", + "hex", + ); + const expectedAuthOK = true; + const actual = await decryptAES128CCM( + ciphertext, + key, + iv, + additionalData, + authTag, + ); + + assertBufferEquals(t.expect, actual.plaintext, expectedPlaintext); + t.expect(actual.authOK).toBe(expectedAuthOK); + }); + + test(`${primitives} -> decryptAES128CCM() -> should work correctly (part 4)`, async (t) => { + // Like part 3, but the additional data was changed + const key = Bytes.from("404142434445464748494a4b4c4d4e4f", "hex"); + const iv = Bytes.from("101112131415161718191a1b", "hex"); + const additionalData = Bytes.from( + "000102030405060708090a0b0c0d0e0f101112", + "hex", + ); + + const ciphertext = Bytes.from( + "e3b201a9f5b71a7a9b1ceaeccd97e70b6176aad9a4428aa5", + "hex", + ); + const authTag = Bytes.from("484392fbc1b09951", "hex"); + + const expectedPlaintext = new Uint8Array(); + const expectedAuthOK = false; + const actual = await decryptAES128CCM( + ciphertext, + key, + iv, + additionalData, + authTag, + ); + + assertBufferEquals(t.expect, actual.plaintext, expectedPlaintext); + t.expect(actual.authOK).toBe(expectedAuthOK); + }); + + test(`${primitives} -> decryptAES128CCM() -> should work correctly (part 5)`, async (t) => { + // Like part 2, but the ciphertext was changed + const key = Bytes.from("404142434445464748494a4b4c4d4e4f", "hex"); + const iv = Bytes.from("1011121314151617", "hex"); + const additionalData = Bytes.from( + "000102030405060708090a0b0c0d0e0f", + "hex", + ); + + const ciphertext = Bytes.from( + "d2a1f0e051ea5f62081a7792073d593e", + "hex", + ); + const authTag = Bytes.from("1fc64fbfaccd", "hex"); + + const expectedPlaintext = new Uint8Array(); + const expectedAuthOK = false; + const actual = await decryptAES128CCM( + ciphertext, + key, + iv, + additionalData, + authTag, + ); + + assertBufferEquals(t.expect, actual.plaintext, expectedPlaintext); + t.expect(actual.authOK).toBe(expectedAuthOK); + }); + + test(`${primitives} -> decryptAES128CCM() -> should work correctly (part 6)`, async (t) => { + // Like part 1, but the authTag was changed + const key = Bytes.from("404142434445464748494a4b4c4d4e4f", "hex"); + const iv = Bytes.from("10111213141516", "hex"); + const additionalData = Bytes.from("0001020304050607", "hex"); + + const ciphertext = Bytes.from("7162015b", "hex"); + const authTag = Bytes.from("4dac25555566", "hex"); + + const expectedPlaintext = new Uint8Array(); + const expectedAuthOK = false; + const actual = await decryptAES128CCM( + ciphertext, + key, + iv, + additionalData, + authTag, + ); + + assertBufferEquals(t.expect, actual.plaintext, expectedPlaintext); + t.expect(actual.authOK).toBe(expectedAuthOK); + }); +} diff --git a/packages/core/src/crypto/primitives/primitives.ts b/packages/core/src/crypto/primitives/primitives.ts new file mode 100644 index 000000000000..7588794db4a0 --- /dev/null +++ b/packages/core/src/crypto/primitives/primitives.ts @@ -0,0 +1,42 @@ +export interface CryptoPrimitives { + randomBytes(length: number): Uint8Array; + /** Encrypts a payload using AES-128-ECB */ + encryptAES128ECB( + plaintext: Uint8Array, + key: Uint8Array, + ): Promise; + /** Encrypts a payload using AES-128-CBC */ + encryptAES128CBC( + plaintext: Uint8Array, + key: Uint8Array, + iv: Uint8Array, + ): Promise; + /** Encrypts a payload using AES-128-OFB */ + encryptAES128OFB( + plaintext: Uint8Array, + key: Uint8Array, + iv: Uint8Array, + ): Promise; + /** Decrypts a payload using AES-128-OFB */ + decryptAES128OFB( + ciphertext: Uint8Array, + key: Uint8Array, + iv: Uint8Array, + ): Promise; + /** Encrypts and authenticates a payload using AES-128-CCM */ + encryptAES128CCM( + plaintext: Uint8Array, + key: Uint8Array, + iv: Uint8Array, + additionalData: Uint8Array, + authTagLength: number, + ): Promise<{ ciphertext: Uint8Array; authTag: Uint8Array }>; + /** Decrypts and verifies a payload using AES-128-CCM */ + decryptAES128CCM( + ciphertext: Uint8Array, + key: Uint8Array, + iv: Uint8Array, + additionalData: Uint8Array, + authTag: Uint8Array, + ): Promise<{ plaintext: Uint8Array; authOK: boolean }>; +} diff --git a/packages/core/src/security/bufferUtils.ts b/packages/core/src/crypto/shared.ts similarity index 58% rename from packages/core/src/security/bufferUtils.ts rename to packages/core/src/crypto/shared.ts index 86e8c5eefb66..daf070536257 100644 --- a/packages/core/src/security/bufferUtils.ts +++ b/packages/core/src/crypto/shared.ts @@ -1,3 +1,7 @@ +import { Bytes } from "@zwave-js/shared/safe"; + +export const BLOCK_SIZE = 16; + export function zeroPad( input: Uint8Array, blockSize: number, @@ -40,3 +44,24 @@ export function increment(buffer: Uint8Array): void { if (buffer[i] !== 0x00) break; } } + +/** Decodes a DER-encoded x25519 key (PKCS#8 or SPKI) */ +export function decodeX25519KeyDER(key: Uint8Array): Uint8Array { + // We could parse this with asn1js but that doesn't seem necessary for now + return key.subarray(-32); +} + +/** Encodes an x25519 key from a raw buffer with DER/PKCS#8 */ +export function encodeX25519KeyDERPKCS8(key: Uint8Array): Uint8Array { + // We could encode this with asn1js but that doesn't seem necessary for now + return Bytes.concat([ + Bytes.from("302e020100300506032b656e04220420", "hex"), + key, + ]); +} + +/** Encodes an x25519 key from a raw buffer with DER/SPKI */ +export function encodeX25519KeyDERSPKI(key: Uint8Array): Uint8Array { + // We could encode this with asn1js but that doesn't seem necessary for now + return Bytes.concat([Bytes.from("302a300506032b656e032100", "hex"), key]); +} diff --git a/packages/core/src/index.ts b/packages/core/src/index.ts index 404b5a3babfd..f1dabe039aee 100644 --- a/packages/core/src/index.ts +++ b/packages/core/src/index.ts @@ -1,4 +1,5 @@ /* eslint-disable @typescript-eslint/consistent-type-exports */ +export * from "./crypto/index.node.js"; export * from "./definitions/index.js"; export * from "./dsk/index.js"; export * from "./error/ZWaveError.js"; @@ -10,8 +11,7 @@ export * from "./reflection/decorators.js"; export * from "./registries/index.js"; export * from "./security/Manager.js"; export * from "./security/Manager2.js"; -export * from "./security/crypto.js"; -export * from "./security/ctr_drbg.js"; +export * from "./security/ctr_drbg.wrapper.js"; export * from "./test/assertZWaveError.js"; export * from "./traits/CommandClasses.js"; export * from "./traits/Endpoints.js"; diff --git a/packages/core/src/index_browser.ts b/packages/core/src/index_browser.ts index 3976051f2c17..b06a0d448383 100644 --- a/packages/core/src/index_browser.ts +++ b/packages/core/src/index_browser.ts @@ -1,6 +1,8 @@ /* @forbiddenImports external */ // FIXME: Find a way to make sure that the forbiddenImports lint uses the "browser" condition +// eslint-disable-next-line @zwave-js/no-forbidden-imports -- FIXME: The lint fails here +export * from "./crypto/index.browser.js"; export * from "./definitions/index.js"; export * from "./dsk/index.js"; export * from "./error/ZWaveError.js"; diff --git a/packages/core/src/security/Manager.test.ts b/packages/core/src/security/Manager.test.ts index 4b7a38027fae..28629dbbca88 100644 --- a/packages/core/src/security/Manager.test.ts +++ b/packages/core/src/security/Manager.test.ts @@ -33,13 +33,15 @@ vi.mock("node:crypto", async () => { }; }); -test("constructor() -> should set the network key, auth key and encryption key", (t) => { +test("constructor() -> should set the network key, auth key and encryption key", async (t) => { const man = new SecurityManager(options); t.expect(man.networkKey).toStrictEqual(networkKey); - t.expect(isUint8Array(man.authKey)).toBe(true); - t.expect(man.authKey.length).toBe(16); - t.expect(isUint8Array(man.encryptionKey)).toBe(true); - t.expect(man.encryptionKey.length).toBe(16); + const authKey = await man.getAuthKey(); + const encryptionKey = await man.getEncryptionKey(); + t.expect(isUint8Array(authKey)).toBe(true); + t.expect(authKey).toHaveLength(16); + t.expect(isUint8Array(encryptionKey)).toBe(true); + t.expect(encryptionKey).toHaveLength(16); }); test("constructor() -> should throw if the network key doesn't have length 16", (t) => { diff --git a/packages/core/src/security/Manager.ts b/packages/core/src/security/Manager.ts index 1bf94da01411..41576f074ffd 100644 --- a/packages/core/src/security/Manager.ts +++ b/packages/core/src/security/Manager.ts @@ -1,18 +1,35 @@ /** Management class and utils for Security S0 */ import { randomBytes } from "node:crypto"; +import { encryptAES128ECB as encryptAES128ECBAsync } from "../crypto/operations.async.js"; +import { encryptAES128ECB as encryptAES128ECBSync } from "../crypto/operations.sync.js"; import { ZWaveError, ZWaveErrorCodes } from "../error/ZWaveError.js"; -import { encryptAES128ECB } from "./crypto.js"; const authKeyBase = new Uint8Array(16).fill(0x55); const encryptionKeyBase = new Uint8Array(16).fill(0xaa); -export function generateAuthKey(networkKey: Uint8Array): Uint8Array { - return encryptAES128ECB(authKeyBase, networkKey); +/** @deprecated Use {@link generateAuthKeyAsync} instead */ +export function generateAuthKeySync(networkKey: Uint8Array): Uint8Array { + // eslint-disable-next-line @typescript-eslint/no-deprecated + return encryptAES128ECBSync(authKeyBase, networkKey); } -export function generateEncryptionKey(networkKey: Uint8Array): Uint8Array { - return encryptAES128ECB(encryptionKeyBase, networkKey); +export function generateAuthKeyAsync( + networkKey: Uint8Array, +): Promise { + return encryptAES128ECBAsync(authKeyBase, networkKey); +} + +/** @deprecated Use {@link generateEncryptionKeyAsync} instead */ +export function generateEncryptionKeySync(networkKey: Uint8Array): Uint8Array { + // eslint-disable-next-line @typescript-eslint/no-deprecated + return encryptAES128ECBSync(encryptionKeyBase, networkKey); +} + +export function generateEncryptionKeyAsync( + networkKey: Uint8Array, +): Promise { + return encryptAES128ECBAsync(encryptionKeyBase, networkKey); } interface NonceKey { @@ -59,17 +76,43 @@ export class SecurityManager { ); } this._networkKey = v; - this._authKey = generateAuthKey(this._networkKey); - this._encryptionKey = generateEncryptionKey(this._networkKey); + this._authKey = undefined; + this._encryptionKey = undefined; } - private _authKey!: Uint8Array; + private _authKey: Uint8Array | undefined; + /** @deprecated Use {@link getAuthKey} instead */ public get authKey(): Uint8Array { + if (!this._authKey) { + // eslint-disable-next-line @typescript-eslint/no-deprecated + this._authKey = generateAuthKeySync(this.networkKey); + } return this._authKey; } - private _encryptionKey!: Uint8Array; + public async getAuthKey(): Promise { + if (!this._authKey) { + this._authKey = await generateAuthKeyAsync(this.networkKey); + } + return this._authKey; + } + + private _encryptionKey: Uint8Array | undefined; + /** @deprecated Use {@link getEncryptionKey} instead */ public get encryptionKey(): Uint8Array { + if (!this._encryptionKey) { + // eslint-disable-next-line @typescript-eslint/no-deprecated + this._encryptionKey = generateEncryptionKeySync(this.networkKey); + } + return this._encryptionKey; + } + + public async getEncryptionKey(): Promise { + if (!this._encryptionKey) { + this._encryptionKey = await generateEncryptionKeyAsync( + this.networkKey, + ); + } return this._encryptionKey; } diff --git a/packages/core/src/security/Manager2.test.ts b/packages/core/src/security/Manager2.test.ts index 4257ad7a17a7..53e5096b8b3c 100644 --- a/packages/core/src/security/Manager2.test.ts +++ b/packages/core/src/security/Manager2.test.ts @@ -6,22 +6,31 @@ import { ZWaveErrorCodes } from "../error/ZWaveError.js"; import { assertZWaveError } from "../test/assertZWaveError.js"; import { SecurityManager2 } from "./Manager2.js"; -function dummyInit( +async function dummyInit( man: SecurityManager2, options: { keys?: boolean; nodeId?: number; secClass?: SecurityClass; } = {}, -): void { +): Promise { if (options.keys !== false) { - man.setKey(SecurityClass.S0_Legacy, crypto.randomBytes(16)); - man.setKey(SecurityClass.S2_AccessControl, crypto.randomBytes(16)); - man.setKey(SecurityClass.S2_Authenticated, crypto.randomBytes(16)); - man.setKey(SecurityClass.S2_Unauthenticated, crypto.randomBytes(16)); + await man.setKeyAsync(SecurityClass.S0_Legacy, crypto.randomBytes(16)); + await man.setKeyAsync( + SecurityClass.S2_AccessControl, + crypto.randomBytes(16), + ); + await man.setKeyAsync( + SecurityClass.S2_Authenticated, + crypto.randomBytes(16), + ); + await man.setKeyAsync( + SecurityClass.S2_Unauthenticated, + crypto.randomBytes(16), + ); } if (options.nodeId) { - man.initializeSPAN( + await man.initializeSPANAsync( options.nodeId, options.secClass ?? SecurityClass.S2_Authenticated, crypto.randomBytes(16), @@ -30,45 +39,45 @@ function dummyInit( } } -test("nextNonce() -> should throw if the PRNG for the given receiver node has not been initialized", (t) => { - const man = new SecurityManager2(); - assertZWaveError(t.expect, () => man.nextNonce(2), { +test("nextNonce() -> should throw if the PRNG for the given receiver node has not been initialized", async (t) => { + const man = await SecurityManager2.create(); + assertZWaveError(t.expect, () => man.nextNonceAsync(2), { errorCode: ZWaveErrorCodes.Security2CC_NotInitialized, messageMatches: "initialized", }); }); -test("nextNonce() -> should generate a 13-byte nonce otherwise", (t) => { - const man = new SecurityManager2(); - dummyInit(man, { +test("nextNonce() -> should generate a 13-byte nonce otherwise", async (t) => { + const man = await SecurityManager2.create(); + await dummyInit(man, { nodeId: 2, secClass: SecurityClass.S2_AccessControl, }); - const ret = man.nextNonce(2); + const ret = await man.nextNonceAsync(2); t.expect(isUint8Array(ret)).toBe(true); t.expect(ret.length).toBe(13); }); -test("nextNonce() -> two nonces should be different", (t) => { - const man = new SecurityManager2(); - dummyInit(man, { +test("nextNonce() -> two nonces should be different", async (t) => { + const man = await SecurityManager2.create(); + await dummyInit(man, { nodeId: 2, secClass: SecurityClass.S2_AccessControl, }); - const nonce1 = man.nextNonce(2); - const nonce2 = man.nextNonce(2); + const nonce1 = await man.nextNonceAsync(2); + const nonce2 = await man.nextNonceAsync(2); t.expect(nonce1).not.toStrictEqual(nonce2); }); -test("initializeSPAN() -> should throw if either entropy input does not have length 16", (t) => { - const man = new SecurityManager2(); +test("initializeSPAN() -> should throw if either entropy input does not have length 16", async (t) => { + const man = await SecurityManager2.create(); const nodeId = 2; assertZWaveError( t.expect, - () => - man.initializeSPAN( + async () => + await man.initializeSPANAsync( nodeId, SecurityClass.S2_Authenticated, new Uint8Array(15), @@ -82,8 +91,8 @@ test("initializeSPAN() -> should throw if either entropy input does not have len assertZWaveError( t.expect, - () => - man.initializeSPAN( + async () => + await man.initializeSPANAsync( nodeId, SecurityClass.S2_Authenticated, new Uint8Array(16), @@ -96,13 +105,13 @@ test("initializeSPAN() -> should throw if either entropy input does not have len ); }); -test("initializeSPAN() -> should throw if the node has not been assigned a security class", (t) => { - const man = new SecurityManager2(); +test("initializeSPAN() -> should throw if the node has not been assigned a security class", async (t) => { + const man = await SecurityManager2.create(); const nodeId = 2; assertZWaveError( t.expect, - () => - man.initializeSPAN( + async () => + await man.initializeSPANAsync( nodeId, SecurityClass.S2_Authenticated, new Uint8Array(16), @@ -115,13 +124,13 @@ test("initializeSPAN() -> should throw if the node has not been assigned a secur ); }); -test("initializeSPAN() -> should throw if the keys for the node's security class have not been set up", (t) => { - const man = new SecurityManager2(); +test("initializeSPAN() -> should throw if the keys for the node's security class have not been set up", async (t) => { + const man = await SecurityManager2.create(); const nodeId = 2; assertZWaveError( t.expect, - () => - man.initializeSPAN( + async () => + await man.initializeSPANAsync( nodeId, SecurityClass.S2_Authenticated, new Uint8Array(16), @@ -134,28 +143,30 @@ test("initializeSPAN() -> should throw if the keys for the node's security class ); }); -test("initializeSPAN() -> should not throw otherwise", (t) => { - const man = new SecurityManager2(); +test("initializeSPAN() -> should not throw otherwise", async (t) => { + const man = await SecurityManager2.create(); const nodeId = 2; - dummyInit(man, { + await dummyInit(man, { nodeId, secClass: SecurityClass.S2_Authenticated, }); - t.expect(() => - man.initializeSPAN( - nodeId, - SecurityClass.S2_Authenticated, - new Uint8Array(16), - new Uint8Array(16), - ) - ).not.toThrow(); + await man.initializeSPANAsync( + nodeId, + SecurityClass.S2_Authenticated, + new Uint8Array(16), + new Uint8Array(16), + ); }); -test("setKeys() -> throws if the network key does not have length 16", (t) => { - const man = new SecurityManager2(); +test("setKeys() -> throws if the network key does not have length 16", async (t) => { + const man = await SecurityManager2.create(); assertZWaveError( t.expect, - () => man.setKey(SecurityClass.S2_Authenticated, new Uint8Array(15)), + async () => + await man.setKeyAsync( + SecurityClass.S2_Authenticated, + new Uint8Array(15), + ), { errorCode: ZWaveErrorCodes.Argument_Invalid, messageMatches: "16 bytes", @@ -163,11 +174,11 @@ test("setKeys() -> throws if the network key does not have length 16", (t) => { ); }); -test("setKeys() -> throws if the security class is not valid", (t) => { - const man = new SecurityManager2(); +test("setKeys() -> throws if the security class is not valid", async (t) => { + const man = await SecurityManager2.create(); assertZWaveError( t.expect, - () => man.setKey(-1 as any, new Uint8Array(16)), + async () => await man.setKeyAsync(-1 as any, new Uint8Array(16)), { errorCode: ZWaveErrorCodes.Argument_Invalid, messageMatches: "security class", @@ -175,9 +186,9 @@ test("setKeys() -> throws if the security class is not valid", (t) => { ); }); -test("createMulticastGroup() -> should return a different group ID for a different node set", (t) => { - const man = new SecurityManager2(); - dummyInit(man); +test("createMulticastGroup() -> should return a different group ID for a different node set", async (t) => { + const man = await SecurityManager2.create(); + await dummyInit(man); const group1 = man.createMulticastGroup( [2, 3, 4], SecurityClass.S2_Authenticated, @@ -190,9 +201,9 @@ test("createMulticastGroup() -> should return a different group ID for a differe t.expect(group1).not.toBe(group2); }); -test("createMulticastGroup() -> should return a different group ID for a different node set for LR nodes", (t) => { - const man = new SecurityManager2(); - dummyInit(man); +test("createMulticastGroup() -> should return a different group ID for a different node set for LR nodes", async (t) => { + const man = await SecurityManager2.create(); + await dummyInit(man); const group1 = man.createMulticastGroup( [260, 261, 262], SecurityClass.S2_Authenticated, @@ -207,9 +218,9 @@ test("createMulticastGroup() -> should return a different group ID for a differe // -test("createMulticastGroup() -> should return the same group ID for a previously used node set", (t) => { - const man = new SecurityManager2(); - dummyInit(man); +test("createMulticastGroup() -> should return the same group ID for a previously used node set", async (t) => { + const man = await SecurityManager2.create(); + await dummyInit(man); const group1 = man.createMulticastGroup( [2, 3, 4], SecurityClass.S2_Authenticated, @@ -222,9 +233,9 @@ test("createMulticastGroup() -> should return the same group ID for a previously t.expect(group1).toBe(group2); }); -test("createMulticastGroup() -> should return the same group ID for a previously used LR node set", (t) => { - const man = new SecurityManager2(); - dummyInit(man); +test("createMulticastGroup() -> should return the same group ID for a previously used LR node set", async (t) => { + const man = await SecurityManager2.create(); + await dummyInit(man); const group1 = man.createMulticastGroup( [260, 261, 262], SecurityClass.S2_Authenticated, @@ -237,65 +248,65 @@ test("createMulticastGroup() -> should return the same group ID for a previously t.expect(group1).toBe(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.expect, () => man.getMulticastKeyAndIV(1), { +test("getMulticastKeyAndIV() -> should throw if the MPAN state for the given multicast group has not been initialized", async (t) => { + const man = await SecurityManager2.create(); + assertZWaveError(t.expect, () => man.getMulticastKeyAndIVAsync(1), { errorCode: ZWaveErrorCodes.Security2CC_NotInitialized, messageMatches: "does not exist", }); }); -test("getMulticastKeyAndIV() -> should throw if the multicast group has not been created", (t) => { - const man = new SecurityManager2(); - assertZWaveError(t.expect, () => man.getMulticastKeyAndIV(1), { +test("getMulticastKeyAndIV() -> should throw if the multicast group has not been created", async (t) => { + const man = await SecurityManager2.create(); + assertZWaveError(t.expect, () => man.getMulticastKeyAndIVAsync(1), { errorCode: ZWaveErrorCodes.Security2CC_NotInitialized, messageMatches: "does not exist", }); }); -test("getMulticastKeyAndIV() -> should throw if the keys for the group's security class have not been set up", (t) => { - const man = new SecurityManager2(); +test("getMulticastKeyAndIV() -> should throw if the keys for the group's security class have not been set up", async (t) => { + const man = await SecurityManager2.create(); const groupId = man.createMulticastGroup( [2, 3, 4], SecurityClass.S2_Authenticated, ); - assertZWaveError(t.expect, () => man.getMulticastKeyAndIV(groupId), { + assertZWaveError(t.expect, () => man.getMulticastKeyAndIVAsync(groupId), { errorCode: ZWaveErrorCodes.Security2CC_NotInitialized, messageMatches: "network key", }); }); -test("getMulticastKeyAndIV() -> should generate a 13-byte IV otherwise", (t) => { - const man = new SecurityManager2(); - dummyInit(man); +test("getMulticastKeyAndIV() -> should generate a 13-byte IV otherwise", async (t) => { + const man = await SecurityManager2.create(); + await dummyInit(man); const groupId = man.createMulticastGroup( [2, 3, 4], SecurityClass.S2_Authenticated, ); - const ret = man.getMulticastKeyAndIV(groupId).iv; + const { iv: ret } = await man.getMulticastKeyAndIVAsync(groupId); t.expect(isUint8Array(ret)).toBe(true); t.expect(ret.length).toBe(13); }); -test("getMulticastKeyAndIV() -> two nonces for the same group should be different", (t) => { - const man = new SecurityManager2(); - dummyInit(man); +test("getMulticastKeyAndIV() -> two nonces for the same group should be different", async (t) => { + const man = await SecurityManager2.create(); + await dummyInit(man); const groupId = man.createMulticastGroup( [2, 3, 4], SecurityClass.S2_Authenticated, ); - const nonce1 = man.getMulticastKeyAndIV(groupId).iv; - const nonce2 = man.getMulticastKeyAndIV(groupId).iv; + const { iv: nonce1 } = await man.getMulticastKeyAndIVAsync(groupId); + const { iv: nonce2 } = await man.getMulticastKeyAndIVAsync(groupId); t.expect(nonce1).not.toStrictEqual(nonce2); }); -test("getMulticastKeyAndIV() -> two nonces for different groups should be different", (t) => { - const man = new SecurityManager2(); - dummyInit(man); +test("getMulticastKeyAndIV() -> two nonces for different groups should be different", async (t) => { + const man = await SecurityManager2.create(); + await dummyInit(man); const group1 = man.createMulticastGroup( [2, 3, 4], SecurityClass.S2_Authenticated, @@ -305,8 +316,8 @@ test("getMulticastKeyAndIV() -> two nonces for different groups should be differ SecurityClass.S2_Authenticated, ); - const nonce1 = man.getMulticastKeyAndIV(group1).iv; - const nonce2 = man.getMulticastKeyAndIV(group2).iv; + const { iv: nonce1 } = await man.getMulticastKeyAndIVAsync(group1); + const { iv: nonce2 } = await man.getMulticastKeyAndIVAsync(group2); t.expect(nonce1).not.toStrictEqual(nonce2); }); diff --git a/packages/core/src/security/Manager2.ts b/packages/core/src/security/Manager2.ts index 5225bcaa631b..6832f77e7dc9 100644 --- a/packages/core/src/security/Manager2.ts +++ b/packages/core/src/security/Manager2.ts @@ -3,22 +3,31 @@ import { createWrappingCounter, getEnumMemberName } from "@zwave-js/shared"; import * as crypto from "node:crypto"; import { deflateSync } from "node:zlib"; +import { + encryptAES128ECBSync, + randomBytesAsync, +} from "../crypto/index.node.js"; +import { + computeNoncePRK as computeNoncePRKAsync, + deriveMEI as deriveMEIAsync, + deriveNetworkKeys as deriveNetworkKeysAsync, +} from "../crypto/operations.async.js"; +import { + computeNoncePRK as computeNoncePRKSync, + deriveMEI as deriveMEISync, + deriveNetworkKeys as deriveNetworkKeysSync, +} from "../crypto/operations.sync.js"; +import { increment } from "../crypto/shared.js"; import { type S2SecurityClass, SecurityClass, } from "../definitions/SecurityClass.js"; import { MAX_NODES_LR } from "../definitions/consts.js"; import { ZWaveError, ZWaveErrorCodes } from "../error/ZWaveError.js"; +import { encryptAES128ECBAsync } from "../index_browser.js"; import { highResTimestamp } from "../util/date.js"; import { encodeBitMask } from "../values/Primitive.js"; -import { increment } from "./bufferUtils.js"; -import { - computeNoncePRK, - deriveMEI, - deriveNetworkKeys, - encryptAES128ECB, -} from "./crypto.js"; -import { CtrDRBG } from "./ctr_drbg.js"; +import { CtrDRBG } from "./ctr_drbg.wrapper.js"; interface NetworkKeys { pnk: Uint8Array; @@ -96,18 +105,18 @@ const SINGLECAST_MAX_SEQ_NUMS = 1; // more than 1 will confuse the certification const SINGLECAST_NONCE_EXPIRY_NS = 500 * 1000 * 1000; // 500 ms in nanoseconds export class SecurityManager2 { - public constructor() { - this.rng = new CtrDRBG( - 128, - false, - crypto.randomBytes(32), - undefined, - new Uint8Array(32).fill(0), - ); + private constructor() { + // Consumers must use the async factory method + } + + public static async create(): Promise { + const ret = new SecurityManager2(); + await ret.rng.initAsync(randomBytesAsync(32)); + return ret; } /** PRNG used to initialize the others */ - private rng: CtrDRBG; + private rng: CtrDRBG = new CtrDRBG(); /** A map of SPAN states for each node */ private spanTable = new Map(); @@ -129,7 +138,10 @@ export class SecurityManager2 { private getNextMulticastGroupId = createWrappingCounter(255); - /** Sets the PNK for a given security class and derives the encryption keys from it */ + /** + * Sets the PNK for a given security class and derives the encryption keys from it + * @deprecated Use {@link setKeyAsync} instead + */ public setKey(securityClass: SecurityClass, key: Uint8Array): void { if (key.length !== 16) { throw new ZWaveError( @@ -147,7 +159,33 @@ export class SecurityManager2 { } this.networkKeys.set(securityClass, { pnk: key, - ...deriveNetworkKeys(key), + // eslint-disable-next-line @typescript-eslint/no-deprecated + ...deriveNetworkKeysSync(key), + }); + } + + /** Sets the PNK for a given security class and derives the encryption keys from it */ + public async setKeyAsync( + securityClass: SecurityClass, + key: Uint8Array, + ): Promise { + if (key.length !== 16) { + throw new ZWaveError( + `The network key must consist of 16 bytes!`, + ZWaveErrorCodes.Argument_Invalid, + ); + } else if ( + !(securityClass in SecurityClass) + || securityClass <= SecurityClass.None + ) { + throw new ZWaveError( + `Invalid security class!`, + ZWaveErrorCodes.Argument_Invalid, + ); + } + this.networkKeys.set(securityClass, { + pnk: key, + ...(await deriveNetworkKeysAsync(key)), }); } @@ -260,9 +298,27 @@ export class SecurityManager2 { /** * Prepares the generation of a new SPAN by creating a random sequence number and (local) entropy input * @param receiver The node this nonce is for. If none is given, the nonce is not stored. + * @deprecated Use {@link generateNonceAsync} instead */ public generateNonce(receiver: number | undefined): Uint8Array { - const receiverEI = this.rng.generate(16); + const receiverEI = this.rng.generateSync(16); + if (receiver != undefined) { + this.spanTable.set(receiver, { + type: SPANState.LocalEI, + receiverEI, + }); + } + return receiverEI; + } + + /** + * Prepares the generation of a new SPAN by creating a random sequence number and (local) entropy input + * @param receiver The node this nonce is for. If none is given, the nonce is not stored. + */ + public async generateNonceAsync( + receiver: number | undefined, + ): Promise { + const receiverEI = await this.rng.generateAsync(16); if (receiver != undefined) { this.spanTable.set(receiver, { type: SPANState.LocalEI, @@ -293,7 +349,10 @@ export class SecurityManager2 { // Keep our own sequence number } - /** Initializes the singlecast PAN generator for a given node based on the given entropy inputs */ + /** + * Initializes the singlecast PAN generator for a given node based on the given entropy inputs + * @deprecated Use {@link initializeSPANAsync} instead + */ public initializeSPAN( peerNodeId: number, securityClass: SecurityClass, @@ -308,23 +367,53 @@ export class SecurityManager2 { } const keys = this.getKeysForSecurityClass(securityClass); - const noncePRK = computeNoncePRK(senderEI, receiverEI); - const MEI = deriveMEI(noncePRK); + // eslint-disable-next-line @typescript-eslint/no-deprecated + const noncePRK = computeNoncePRKSync(senderEI, receiverEI); + // eslint-disable-next-line @typescript-eslint/no-deprecated + const MEI = deriveMEISync(noncePRK); + + const rng = new CtrDRBG(); + rng.initSync(MEI, keys.personalizationString); this.spanTable.set(peerNodeId, { securityClass, type: SPANState.SPAN, - rng: new CtrDRBG( - 128, - false, - MEI, - undefined, - keys.personalizationString, - ), + rng, }); } /** Initializes the singlecast PAN generator for a given node based on the given entropy inputs */ + public async initializeSPANAsync( + peerNodeId: number, + securityClass: SecurityClass, + senderEI: Uint8Array, + receiverEI: Uint8Array, + ): Promise { + if (senderEI.length !== 16 || receiverEI.length !== 16) { + throw new ZWaveError( + `The entropy input must consist of 16 bytes`, + ZWaveErrorCodes.Argument_Invalid, + ); + } + + const keys = this.getKeysForSecurityClass(securityClass); + const noncePRK = await computeNoncePRKAsync(senderEI, receiverEI); + const MEI = await deriveMEIAsync(noncePRK); + + const rng = new CtrDRBG(); + await rng.initAsync(MEI, keys.personalizationString); + + this.spanTable.set(peerNodeId, { + securityClass, + type: SPANState.SPAN, + rng, + }); + } + + /** + * Initializes the singlecast PAN generator for a given node based on the given entropy inputs + * @deprecated Use {@link initializeTempSPANAsync} instead + */ public initializeTempSPAN( peerNodeId: number, senderEI: Uint8Array, @@ -338,19 +427,45 @@ export class SecurityManager2 { } const keys = this.tempKeys.get(peerNodeId)!; - const noncePRK = computeNoncePRK(senderEI, receiverEI); - const MEI = deriveMEI(noncePRK); + // eslint-disable-next-line @typescript-eslint/no-deprecated + const noncePRK = computeNoncePRKSync(senderEI, receiverEI); + // eslint-disable-next-line @typescript-eslint/no-deprecated + const MEI = deriveMEISync(noncePRK); + + const rng = new CtrDRBG(); + rng.initSync(MEI, keys.personalizationString); this.spanTable.set(peerNodeId, { securityClass: SecurityClass.Temporary, type: SPANState.SPAN, - rng: new CtrDRBG( - 128, - false, - MEI, - undefined, - keys.personalizationString, - ), + rng, + }); + } + + /** Initializes the singlecast PAN generator for a given node based on the given entropy inputs */ + public async initializeTempSPANAsync( + peerNodeId: number, + senderEI: Uint8Array, + receiverEI: Uint8Array, + ): Promise { + if (senderEI.length !== 16 || receiverEI.length !== 16) { + throw new ZWaveError( + `The entropy input must consist of 16 bytes`, + ZWaveErrorCodes.Argument_Invalid, + ); + } + + const keys = this.tempKeys.get(peerNodeId)!; + const noncePRK = await computeNoncePRKAsync(senderEI, receiverEI); + const MEI = await deriveMEIAsync(noncePRK); + + const rng = new CtrDRBG(); + await rng.initAsync(MEI, keys.personalizationString); + + this.spanTable.set(peerNodeId, { + securityClass: SecurityClass.Temporary, + type: SPANState.SPAN, + rng, }); } @@ -399,6 +514,7 @@ export class SecurityManager2 { /** * Generates the next nonce for the given peer and returns it. * @param store - Whether the nonce should be stored/remembered as the current SPAN. + * @deprecated Use {@link nextNonceAsync} instead */ public nextNonce(peerNodeId: number, store?: boolean): Uint8Array { const spanState = this.spanTable.get(peerNodeId); @@ -408,7 +524,32 @@ export class SecurityManager2 { ZWaveErrorCodes.Security2CC_NotInitialized, ); } - const nonce = spanState.rng.generate(16).subarray(0, 13); + const nonce = spanState.rng.generateSync(16).subarray(0, 13); + spanState.currentSPAN = store + ? { + nonce, + expires: highResTimestamp() + SINGLECAST_NONCE_EXPIRY_NS, + } + : undefined; + return nonce; + } + + /** + * Generates the next nonce for the given peer and returns it. + * @param store - Whether the nonce should be stored/remembered as the current SPAN. + */ + public async nextNonceAsync( + peerNodeId: number, + store?: boolean, + ): Promise { + const spanState = this.spanTable.get(peerNodeId); + if (spanState?.type !== SPANState.SPAN) { + throw new ZWaveError( + `The Singlecast PAN has not been initialized for Node ${peerNodeId}`, + ZWaveErrorCodes.Security2CC_NotInitialized, + ); + } + const nonce = (await spanState.rng.generateAsync(16)).subarray(0, 13); spanState.currentSPAN = store ? { nonce, @@ -450,6 +591,7 @@ export class SecurityManager2 { return this.mpanStates.get(groupId); } + /** @deprecated Use {@link getMulticastKeyAndIVAsync} instead */ public getMulticastKeyAndIV(groupId: number): { key: Uint8Array; iv: Uint8Array; @@ -467,13 +609,47 @@ export class SecurityManager2 { // We may have to initialize the inner MPAN state if (!this.mpanStates.has(groupId)) { - this.mpanStates.set(groupId, this.rng.generate(16)); + this.mpanStates.set(groupId, this.rng.generateSync(16)); + } + + // Compute the next MPAN + const stateN = this.mpanStates.get(groupId)!; + // The specs don't mention this step for multicast, but the IV for AES-CCM is limited to 13 bytes + // eslint-disable-next-line @typescript-eslint/no-deprecated + const ret = encryptAES128ECBSync(stateN, keys.keyMPAN).subarray(0, 13); + // Increment the inner state + increment(stateN); + + return { + key: keys.keyCCM, + iv: ret, + }; + } + + public async getMulticastKeyAndIVAsync( + groupId: number, + ): Promise<{ key: Uint8Array; iv: Uint8Array }> { + const group = this.getMulticastGroup(groupId); + + if (!group) { + throw new ZWaveError( + `Multicast group ${groupId} does not exist!`, + ZWaveErrorCodes.Security2CC_NotInitialized, + ); + } + + const keys = this.getKeysForSecurityClass(group.securityClass); + + // We may have to initialize the inner MPAN state + if (!this.mpanStates.has(groupId)) { + this.mpanStates.set(groupId, await this.rng.generateAsync(16)); } // Compute the next MPAN const stateN = this.mpanStates.get(groupId)!; // The specs don't mention this step for multicast, but the IV for AES-CCM is limited to 13 bytes - const ret = encryptAES128ECB(stateN, keys.keyMPAN).subarray(0, 13); + const ret = (await encryptAES128ECBAsync(stateN, keys.keyMPAN)) + .subarray(0, 13); // Increment the inner state increment(stateN); @@ -491,6 +667,7 @@ export class SecurityManager2 { /** * Generates the next nonce for the given peer and returns it. + * @deprecated Use {@link nextPeerMPANAsync} instead */ public nextPeerMPAN(peerNodeId: number, groupId: number): Uint8Array { const mpanState = this.getPeerMPAN(peerNodeId, groupId); @@ -511,7 +688,40 @@ export class SecurityManager2 { // Compute the next MPAN const stateN = mpanState.currentMPAN; // The specs don't mention this step for multicast, but the IV for AES-CCM is limited to 13 bytes - const ret = encryptAES128ECB(stateN, keys.keyMPAN).subarray(0, 13); + // eslint-disable-next-line @typescript-eslint/no-deprecated + const ret = encryptAES128ECBSync(stateN, keys.keyMPAN).subarray(0, 13); + // Increment the inner state + increment(stateN); + return ret; + } + + /** + * Generates the next nonce for the given peer and returns it. + */ + public async nextPeerMPANAsync( + peerNodeId: number, + groupId: number, + ): Promise { + const mpanState = this.getPeerMPAN(peerNodeId, groupId); + if (mpanState.type !== MPANState.MPAN) { + throw new ZWaveError( + `No peer multicast PAN exists for Node ${peerNodeId}, group ${groupId}`, + ZWaveErrorCodes.Security2CC_NotInitialized, + ); + } + const keys = this.getKeysForNode(peerNodeId); + if (!keys || !("keyMPAN" in keys)) { + throw new ZWaveError( + `The network keys for the security class of Node ${peerNodeId} have not been set up yet!`, + ZWaveErrorCodes.Security2CC_NotInitialized, + ); + } + + // Compute the next MPAN + const stateN = mpanState.currentMPAN; + // The specs don't mention this step for multicast, but the IV for AES-CCM is limited to 13 bytes + const ret = (await encryptAES128ECBAsync(stateN, keys.keyMPAN)) + .subarray(0, 13); // Increment the inner state increment(stateN); return ret; diff --git a/packages/core/src/security/crypto.test.ts b/packages/core/src/security/crypto.test.ts deleted file mode 100644 index 3566c64f9bda..000000000000 --- a/packages/core/src/security/crypto.test.ts +++ /dev/null @@ -1,126 +0,0 @@ -import { Bytes } from "@zwave-js/shared/safe"; -import * as crypto from "node:crypto"; -import { test } from "vitest"; -import { - computeCMAC, - computeMAC, - decryptAES128OFB, - encryptAES128ECB, - encryptAES128OFB, -} from "./crypto.js"; - -test("encryptAES128OFB() / decryptAES128OFB() -> should be able to en- and decrypt the same data", (t) => { - const plaintextIn = - "Lorem ipsum dolor sit amet, consetetur sadipscing elitr, sed diam nonumy eirmod tempor invidunt ut labore et dolore magna aliquyam"; - const key = crypto.randomBytes(16); - const iv = crypto.randomBytes(16); - const ciphertext = encryptAES128OFB(Bytes.from(plaintextIn), key, iv); - const plaintextOut = decryptAES128OFB(ciphertext, key, iv).toString(); - t.expect(plaintextOut).toBe(plaintextIn); -}); - -test("encryptAES128ECB() -> should work correctly", (t) => { - // // Test vector taken from https://nvlpubs.nist.gov/nistpubs/Legacy/SP/nistspecialpublication800-38a.pdf - const key = Bytes.from("2b7e151628aed2a6abf7158809cf4f3c", "hex"); - const plaintext = Bytes.from("6bc1bee22e409f96e93d7e117393172a", "hex"); - const expected = Bytes.from("3ad77bb40d7a3660a89ecaf32466ef97", "hex"); - t.expect(encryptAES128ECB(plaintext, key)).toStrictEqual(expected); -}); - -test("encryptAES128OFB() -> should work correctly", (t) => { - // Test vector taken from https://nvlpubs.nist.gov/nistpubs/Legacy/SP/nistspecialpublication800-38a.pdf - const key = Bytes.from("2b7e151628aed2a6abf7158809cf4f3c", "hex"); - const iv = Bytes.from("000102030405060708090a0b0c0d0e0f", "hex"); - const plaintext = Bytes.from("6bc1bee22e409f96e93d7e117393172a", "hex"); - const expected = Bytes.from("3b3fd92eb72dad20333449f8e83cfb4a", "hex"); - t.expect(encryptAES128OFB(plaintext, key, iv)).toStrictEqual(expected); -}); - -test("encryptAES128OFB() -> should correctly decrypt a real packet", (t) => { - // Taken from an OZW log: - // Raw: 0x9881 78193fd7b91995ba 47645ec33fcdb3994b104ebd712e8b7fbd9120d049 28 4e39c14a0dc9aee5 - // Decrypted Packet: 0x009803008685598e60725a845b7170807aef2526ef - // Nonce: 0x2866211bff3783d6 - // Network Key: 0x0102030405060708090a0b0c0d0e0f10 - - const key = encryptAES128ECB( - new Uint8Array(16).fill(0xaa), - Bytes.from("0102030405060708090a0b0c0d0e0f10", "hex"), - ); - const iv = Bytes.from("78193fd7b91995ba2866211bff3783d6", "hex"); - const ciphertext = Bytes.from( - "47645ec33fcdb3994b104ebd712e8b7fbd9120d049", - "hex", - ); - const plaintext = decryptAES128OFB(ciphertext, key, iv); - const expected = Bytes.from( - "009803008685598e60725a845b7170807aef2526ef", - "hex", - ); - t.expect(plaintext).toStrictEqual(expected); -}); - -test("computeMAC() -> should work correctly", (t) => { - // Test vector taken from https://nvlpubs.nist.gov/nistpubs/Legacy/SP/nistspecialpublication800-38a.pdf - const key = Bytes.from("2b7e151628aed2a6abf7158809cf4f3c", "hex"); - // The Z-Wave specs use 16 zeros, but we only found test vectors for this - const iv = Bytes.from("000102030405060708090a0b0c0d0e0f", "hex"); - const plaintext = Bytes.from("6bc1bee22e409f96e93d7e117393172a", "hex"); - const expected = Bytes.from("7649abac8119b246", "hex"); - - t.expect(computeMAC(plaintext, key, iv)).toStrictEqual(expected); -}); - -test("computeMAC() -> should work correctly (part 2)", (t) => { - // Taken from real Z-Wave communication - if anything must be changed, this is the test case to keep! - const key = Bytes.from("c5fe1ca17d36c992731a0c0c468c1ef9", "hex"); - const plaintext = Bytes.from( - "ddd360c382a437514392826cbba0b3128114010cf3fb762d6e82126681c18597", - "hex", - ); - const expected = Bytes.from("2bc20a8aa9bbb371", "hex"); - - t.expect(computeMAC(plaintext, key)).toStrictEqual(expected); -}); - -test("computeCMAC() -> should work correctly (part 1)", (t) => { - // Test vector taken from https://csrc.nist.gov/CSRC/media/Projects/Cryptographic-Standards-and-Guidelines/documents/examples/AES_CMAC.pdf - const key = Bytes.from("2B7E151628AED2A6ABF7158809CF4F3C", "hex"); - const plaintext = new Bytes(); - const expected = Bytes.from("BB1D6929E95937287FA37D129B756746", "hex"); - - t.expect(computeCMAC(plaintext, key)).toStrictEqual(expected); -}); - -test("computeCMAC() -> should work correctly (part 2)", (t) => { - // Test vector taken from https://csrc.nist.gov/CSRC/media/Projects/Cryptographic-Standards-and-Guidelines/documents/examples/AES_CMAC.pdf - const key = Bytes.from("2B7E151628AED2A6ABF7158809CF4F3C", "hex"); - const plaintext = Bytes.from("6BC1BEE22E409F96E93D7E117393172A", "hex"); - const expected = Bytes.from("070A16B46B4D4144F79BDD9DD04A287C", "hex"); - - t.expect(computeCMAC(plaintext, key)).toStrictEqual(expected); -}); - -test("computeCMAC() -> should work correctly (part 3)", (t) => { - // Test vector taken from https://csrc.nist.gov/CSRC/media/Projects/Cryptographic-Standards-and-Guidelines/documents/examples/AES_CMAC.pdf - const key = Bytes.from("2B7E151628AED2A6ABF7158809CF4F3C", "hex"); - const plaintext = Bytes.from( - "6BC1BEE22E409F96E93D7E117393172AAE2D8A57", - "hex", - ); - const expected = Bytes.from("7D85449EA6EA19C823A7BF78837DFADE", "hex"); - - t.expect(computeCMAC(plaintext, key)).toStrictEqual(expected); -}); - -test("computeCMAC() -> should work correctly (part 4)", (t) => { - // Test vector taken from https://csrc.nist.gov/CSRC/media/Projects/Cryptographic-Standards-and-Guidelines/documents/examples/AES_CMAC.pdf - const key = Bytes.from("2B7E151628AED2A6ABF7158809CF4F3C", "hex"); - const plaintext = Bytes.from( - "6BC1BEE22E409F96E93D7E117393172AAE2D8A571E03AC9C9EB76FAC45AF8E5130C81C46A35CE411E5FBC1191A0A52EFF69F2445DF4F9B17AD2B417BE66C3710", - "hex", - ); - const expected = Bytes.from("51F0BEBF7E3B9D92FC49741779363CFE", "hex"); - - t.expect(computeCMAC(plaintext, key)).toStrictEqual(expected); -}); diff --git a/packages/core/src/security/ctr_drbg.test.ts b/packages/core/src/security/ctr_drbg.async.test.ts similarity index 50% rename from packages/core/src/security/ctr_drbg.test.ts rename to packages/core/src/security/ctr_drbg.async.test.ts index add161f2d942..77e38ed36b08 100644 --- a/packages/core/src/security/ctr_drbg.test.ts +++ b/packages/core/src/security/ctr_drbg.async.test.ts @@ -3,13 +3,13 @@ import * as fs from "node:fs"; import path from "node:path"; import { fileURLToPath } from "node:url"; import { test } from "vitest"; -import { CtrDRBG } from "./ctr_drbg.js"; +import { CtrDrbgAsync } from "./ctr_drbg.async.js"; const __dirname = path.dirname(fileURLToPath(import.meta.url)); function getVectors(alg: string) { const text = fs.readFileSync( - path.join(__dirname, "ctr_drbg.test.vectors.txt"), + path.join(__dirname, "ctr_drbg.test.vectors.limited.txt"), "utf8", ); const vectors = []; @@ -42,44 +42,36 @@ function getVectors(alg: string) { return vectors; } -for (const df of [false, true]) { - for (const id of ["AES-128"]) { - const name = id + (df ? " use df" : " no df"); - const vectors = getVectors(name); - const bits = parseInt(id.slice(-3)) as 128; - - for (const [i, vector] of vectors.entries()) { - test( - `CtrDRBG -> should pass ${name} NIST vector #${ - i + 1 - } (ctr,df=${df})`, - (t) => { - const drbg = new CtrDRBG(bits, df); - - drbg.init( - vector.EntropyInput, - vector.Nonce, - vector.PersonalizationString, - ); - - drbg.reseed( - vector.EntropyInputReseed, - vector.AdditionalInputReseed, - ); - - drbg.generate( - vector.ReturnedBits.byteLength, - vector.AdditionalInput[0], - ); - - const result = drbg.generate( - vector.ReturnedBits.byteLength, - vector.AdditionalInput[1], - ); - - t.expect(result).toStrictEqual(vector.ReturnedBits); - }, - ); - } +for (const id of ["AES-128"]) { + const name = id + " no df"; + const vectors = getVectors(name); + + for (const [i, vector] of vectors.entries()) { + test( + `CtrDrbgAsync -> should pass ${name} NIST vector #${ + i + 1 + } (ctr,df=false)`, + async (t) => { + const drbg = new CtrDrbgAsync(); + + await drbg.init( + vector.EntropyInput, + ); + + await drbg["reseed"]( + vector.EntropyInputReseed, + ); + + await drbg.generate( + vector.ReturnedBits.byteLength, + ); + + const result = await drbg.generate( + vector.ReturnedBits.byteLength, + ); + + t.expect(result).toStrictEqual(vector.ReturnedBits); + }, + ); } } diff --git a/packages/core/src/security/ctr_drbg.async.ts b/packages/core/src/security/ctr_drbg.async.ts new file mode 100644 index 000000000000..a6084cb4f77a --- /dev/null +++ b/packages/core/src/security/ctr_drbg.async.ts @@ -0,0 +1,95 @@ +// A pseudo-random number generator using AES-ECB as described in NIST SP 800-90A +// This does not implement the full standard, but only the necessary subset needed for Z-Wave Security S2 + +// The used crypto primitives are async, so the methods in this implementation are async as well + +import { encryptAES128ECB as encryptAES128ECBAsync } from "../crypto/operations.async.js"; +import { increment, xor } from "../crypto/shared.js"; + +// Warning: This code expects ctr_len to equal BLOCK_LEN. +// See specification on how to handle other cases + +const KEY_LEN = 16; +const BLOCK_LEN = 16; +const SEED_LEN = KEY_LEN + BLOCK_LEN; + +export class CtrDrbgAsync { + private key = new Uint8Array(KEY_LEN); + private v = new Uint8Array(BLOCK_LEN); + // Reseed counter is not used + + public saveState(): { key: Uint8Array; v: Uint8Array } { + return { key: Uint8Array.from(this.key), v: Uint8Array.from(this.v) }; + } + + public restoreState(state: { key: Uint8Array; v: Uint8Array }): void { + this.key = state.key; + this.v = state.v; + } + + public async init( + entropy: Uint8Array, + personalizationString?: Uint8Array, + ): Promise { + if (entropy.length !== SEED_LEN) { + throw new Error(`entropy must be ${SEED_LEN} bytes long`); + } + + if (personalizationString) { + if (personalizationString.length > SEED_LEN) { + throw new Error("Personalization string is too long."); + } + for (let i = 0; i < personalizationString.length; i++) { + entropy[i] ^= personalizationString[i]; + } + } + + await this.update(entropy); + } + + public async update(providedData: Uint8Array | undefined): Promise { + if (providedData && providedData.length !== SEED_LEN) { + throw new Error(`providedData must be ${SEED_LEN} bytes long`); + } + + let temp = new Uint8Array(SEED_LEN); + let tempOffset = 0; + while (tempOffset < SEED_LEN) { + increment(this.v); + const encrypted = await encryptAES128ECBAsync(this.v, this.key); + // We know that we're only dealing with full blocks here, otherwise + // the following line may throw when trying to set a too long last block + temp.set(encrypted, tempOffset); + tempOffset += BLOCK_LEN; + } + + if (providedData) { + temp = xor(temp, providedData); + } + + this.key = temp.subarray(0, KEY_LEN); + this.v = temp.subarray(KEY_LEN); + } + + public async generate(len: number): Promise { + // Additional input is not used + const temp = new Uint8Array(Math.ceil(len / BLOCK_LEN) * BLOCK_LEN); + let tempOffset = 0; + while (tempOffset < len) { + increment(this.v); + const encrypted = await encryptAES128ECBAsync(this.v, this.key); + // The size of temp is a multiple of the block size, so this is safe to do: + temp.set(encrypted, tempOffset); + tempOffset += BLOCK_LEN; + } + + await this.update(undefined); + + return temp.subarray(0, len); + } + + protected async reseed(entropy: Uint8Array): Promise { + // Reseeding isn't necessary for this implementation, but all test vectors use it + await this.update(entropy); + } +} diff --git a/packages/core/src/security/ctr_drbg.sync.test.ts b/packages/core/src/security/ctr_drbg.sync.test.ts new file mode 100644 index 000000000000..f894c4ebdb05 --- /dev/null +++ b/packages/core/src/security/ctr_drbg.sync.test.ts @@ -0,0 +1,77 @@ +import { hexToUint8Array } from "@zwave-js/shared/safe"; +import * as fs from "node:fs"; +import path from "node:path"; +import { fileURLToPath } from "node:url"; +import { test } from "vitest"; +import { CtrDrbgSync } from "./ctr_drbg.sync.js"; + +const __dirname = path.dirname(fileURLToPath(import.meta.url)); + +function getVectors(alg: string) { + const text = fs.readFileSync( + path.join(__dirname, "ctr_drbg.test.vectors.limited.txt"), + "utf8", + ); + const vectors = []; + + let from = -1; + + while (true) { + from = text.indexOf(`[${alg}]`, from + 1); + + if (from === -1) break; + + for (let i = 0; i < 15; i++) { + const vector: Record = {}; + const start = text.indexOf(`COUNT = ${i}`, from); + const end = text.indexOf("\n\n", start); + const items = text.slice(start, end).split("\n"); + + for (let j = 1; j < items.length; j++) { + const key = items[j].split(" = ")[0]; + const value = hexToUint8Array(items[j].split(" = ")[1]); + + if (vector[key]) vector[key] = [vector[key], value]; + else vector[key] = value; + } + + vectors.push(vector); + } + } + + return vectors; +} + +for (const id of ["AES-128"]) { + const name = id + " no df"; + const vectors = getVectors(name); + + for (const [i, vector] of vectors.entries()) { + test( + `CtrDrbgAsync -> should pass ${name} NIST vector #${ + i + 1 + } (ctr,df=false)`, + async (t) => { + const drbg = new CtrDrbgSync(); + + drbg.init( + vector.EntropyInput, + ); + + drbg["reseed"]( + vector.EntropyInputReseed, + ); + + drbg.generate( + vector.ReturnedBits.byteLength, + ); + + const result = drbg.generate( + vector.ReturnedBits.byteLength, + ); + + t.expect(result).toStrictEqual(vector.ReturnedBits); + }, + ); + } +} diff --git a/packages/core/src/security/ctr_drbg.sync.ts b/packages/core/src/security/ctr_drbg.sync.ts new file mode 100644 index 000000000000..f485adbc6740 --- /dev/null +++ b/packages/core/src/security/ctr_drbg.sync.ts @@ -0,0 +1,94 @@ +// A pseudo-random number generator using AES-ECB as described in NIST SP 800-90A +// This does not implement the full standard, but only the necessary subset needed for Z-Wave Security S2 + +// The used crypto primitives are sync, so the methods in this implementation are sync as well + +import { encryptAES128ECB as encryptAES128ECBSync } from "../crypto/operations.sync.js"; +import { increment, xor } from "../crypto/shared.js"; + +// Warning: This code expects ctr_len to equal BLOCK_LEN. +// See specification on how to handle other cases + +const KEY_LEN = 16; +const BLOCK_LEN = 16; +const SEED_LEN = KEY_LEN + BLOCK_LEN; + +export class CtrDrbgSync { + private key = new Uint8Array(KEY_LEN); + private v = new Uint8Array(BLOCK_LEN); + // Reseed counter is not used + + public saveState(): { key: Uint8Array; v: Uint8Array } { + return { key: Uint8Array.from(this.key), v: Uint8Array.from(this.v) }; + } + + public restoreState(state: { key: Uint8Array; v: Uint8Array }): void { + this.key = state.key; + this.v = state.v; + } + + public init(entropy: Uint8Array, personalizationString?: Uint8Array): void { + if (entropy.length !== SEED_LEN) { + throw new Error(`entropy must be ${SEED_LEN} bytes long`); + } + + if (personalizationString) { + if (personalizationString.length > SEED_LEN) { + throw new Error("Personalization string is too long."); + } + for (let i = 0; i < personalizationString.length; i++) { + entropy[i] ^= personalizationString[i]; + } + } + + this.update(entropy); + } + + public update(providedData: Uint8Array | undefined): void { + if (providedData && providedData.length !== SEED_LEN) { + throw new Error(`providedData must be ${SEED_LEN} bytes long`); + } + + let temp = new Uint8Array(SEED_LEN); + let tempOffset = 0; + while (tempOffset < SEED_LEN) { + increment(this.v); + // eslint-disable-next-line @typescript-eslint/no-deprecated + const encrypted = encryptAES128ECBSync(this.v, this.key); + // We know that we're only dealing with full blocks here, otherwise + // the following line may throw when trying to set a too long last block + temp.set(encrypted, tempOffset); + tempOffset += BLOCK_LEN; + } + + if (providedData) { + temp = xor(temp, providedData); + } + + this.key = temp.subarray(0, KEY_LEN); + this.v = temp.subarray(KEY_LEN); + } + + public generate(len: number): Uint8Array { + // Additional input is not used + const temp = new Uint8Array(Math.ceil(len / BLOCK_LEN) * BLOCK_LEN); + let tempOffset = 0; + while (tempOffset < len) { + increment(this.v); + // eslint-disable-next-line @typescript-eslint/no-deprecated + const encrypted = encryptAES128ECBSync(this.v, this.key); + // The size of temp is a multiple of the block size, so this is safe to do: + temp.set(encrypted, tempOffset); + tempOffset += BLOCK_LEN; + } + + this.update(undefined); + + return temp.subarray(0, len); + } + + protected reseed(entropy: Uint8Array): void { + // Reseeding isn't necessary for this implementation, but all test vectors use it + this.update(entropy); + } +} diff --git a/packages/core/src/security/ctr_drbg.test.vectors.limited.txt b/packages/core/src/security/ctr_drbg.test.vectors.limited.txt new file mode 100644 index 000000000000..38e4991265c1 --- /dev/null +++ b/packages/core/src/security/ctr_drbg.test.vectors.limited.txt @@ -0,0 +1,635 @@ +/// NOTE: These test vectors are limited to the ones relevant for the tests of the specific implementation in this library +/// See the other file for the complete list of test vectors. + +[AES-128 no df] +[PredictionResistance = False] +[EntropyInputLen = 256] +[NonceLen = 0] +[PersonalizationStringLen = 0] +[AdditionalInputLen = 0] +[ReturnedBitsLen = 512] + +COUNT = 0 +EntropyInput = ed1e7f21ef66ea5d8e2a85b9337245445b71d6393a4eecb0e63c193d0f72f9a9 +Nonce = +PersonalizationString = +EntropyInputReseed = 303fb519f0a4e17d6df0b6426aa0ecb2a36079bd48be47ad2a8dbfe48da3efad +AdditionalInputReseed = +AdditionalInput = +AdditionalInput = +ReturnedBits = f80111d08e874672f32f42997133a5210f7a9375e22cea70587f9cfafebe0f6a6aa2eb68e7dd9164536d53fa020fcab20f54caddfab7d6d91e5ffec1dfd8deaa + +COUNT = 1 +EntropyInput = eab5a9f23ceac9e4195e185c8cea549d6d97d03276225a7452763c396a7f70bf +Nonce = +PersonalizationString = +EntropyInputReseed = 4258765c65a03af92fc5816f966f1a6644a6134633aad2d5d19bd192e4c1196a +AdditionalInputReseed = +AdditionalInput = +AdditionalInput = +ReturnedBits = 2915c9fabfbf7c62d68d83b4e65a239885e809ceac97eb8ef4b64df59881c277d3a15e0e15b01d167c49038fad2f54785ea714366d17bb2f8239fd217d7e1cba + +COUNT = 2 +EntropyInput = 4465bf169297819160b8ef406ce768f70d094588322e8a214a8d67d55704931c +Nonce = +PersonalizationString = +EntropyInputReseed = a461f049fca9349c29f4aa4909a4d15d11e4ce72747ad5b0a7b1ca6d83f88ff1 +AdditionalInputReseed = +AdditionalInput = +AdditionalInput = +ReturnedBits = 1ed1079763fbe2dcfc65532d2f1db0e1ccd271a9c73b3479f16b0d3d993bc0516f4caf6f0185ecba912ebb8e42437e2016a6121459e64e82b414ba7f994a53bd + +COUNT = 3 +EntropyInput = 67566494c6a170e65d5277b827264da11de1bdef345e593f7a420580be8e3f7b +Nonce = +PersonalizationString = +EntropyInputReseed = 737652b3a4cea2e68f28fed839994170b701aaa0fdc015a945e8ee00577a7f6e +AdditionalInputReseed = +AdditionalInput = +AdditionalInput = +ReturnedBits = e0ee86950de55281d861dc656f80bc4bbeaf8b5303e07df353f67aa63183333a437aabc400643e648f21e63809d688632e4fc8a25aa740637d812abe9eb17b5a + +COUNT = 4 +EntropyInput = 9ba928f88bc924a1e19ea804d7096dd6c55d9497d889fb87eafb179380f7d7a5 +Nonce = +PersonalizationString = +EntropyInputReseed = 76337f55d07c33c21129aa694912703e4fef8e5401185c7e7d47784e963c87a4 +AdditionalInputReseed = +AdditionalInput = +AdditionalInput = +ReturnedBits = 510b18ec20120da8798ca944dfc97c63ae62266d122c70ce5cf472d5ba717dfc80a1cce0c29a8cf3d221583c7223b331727b41a0cd56d4ca425e7678441784fc + +COUNT = 5 +EntropyInput = eb20968b85cdabe87c6400d8b01d93c0240ace20a40bbb4996a0de6ed3c49326 +Nonce = +PersonalizationString = +EntropyInputReseed = c46e67b8027db6b5bac40906ad0be62759524a2f3d90a5025b188e7a850c73be +AdditionalInputReseed = +AdditionalInput = +AdditionalInput = +ReturnedBits = bd158d21c0172d5058f74d69865c98b61025683807df930bf5fc3c500c8c10c71d8804fa67db413a4a5c53d57a52aaac469698b4a42fda0eedf7b45d36078639 + +COUNT = 6 +EntropyInput = 7b3292fed22226315b52c12e0a493eb4eda9a79498cc71985a3bd07d29e5ae04 +Nonce = +PersonalizationString = +EntropyInputReseed = 21317bba5c805b6e05a1137c90b6559bf1027c2a80b95d176e31a87f6ddd48b9 +AdditionalInputReseed = +AdditionalInput = +AdditionalInput = +ReturnedBits = eb68b9985db9fc8654e7219c8599f42ec0164b42b5e95a087c4ee8bd8898fa69548b8c5da1af2a785f5a0149dd30c88922123d449e324c399df4b524a33e5a9d + +COUNT = 7 +EntropyInput = 477baac730e534f2e2525e83719802764b954acf9732e8724d856dcd124aeac7 +Nonce = +PersonalizationString = +EntropyInputReseed = 4461fa9e6fb6d4829c8b16cbccb14dedee9f0d6f5883748d7a90f14fef54d8cc +AdditionalInputReseed = +AdditionalInput = +AdditionalInput = +ReturnedBits = 61e5d9056d27691f4258e8844a516e979aeb49c5d9482682f914cb9b310172ed1ae1b01b241b317a59adcc9444cdd8204e49b8d917892d23725866cd31eff534 + +COUNT = 8 +EntropyInput = 94c77ec6e22b85eeb1d2877b69eeb564258c214e9ea57cef69a829bbd8b7ca09 +Nonce = +PersonalizationString = +EntropyInputReseed = c22677c570fc95918809429c240802f6b5896c48a130cb19bf1c1ad4387622df +AdditionalInputReseed = +AdditionalInput = +AdditionalInput = +ReturnedBits = 525d69e6839a242a901179bc2e239ec37226319a3464b2aa421c9b5ff4c9d6717e5b4ad42c913c532905698dee3b8209f2e227ae4f748deb3ce8d21746b585bd + +COUNT = 9 +EntropyInput = 0e5585e10cedd896792e2b918b2cb0a37844b64c862c283d76c97055c88d702b +Nonce = +PersonalizationString = +EntropyInputReseed = 87445a1ade002d1f0f49d64bda4c8ca427225ff56f371a20bd8a5a3bd35fc568 +AdditionalInputReseed = +AdditionalInput = +AdditionalInput = +ReturnedBits = 83067b4a57a5f6ba418a98996eb102329d6bdc4e1dfb125468f1f8ab36d00732597de568c17cae3412c9ebfae08377ca19406b1abc5e10be5dbeacf3839bcf43 + +COUNT = 10 +EntropyInput = 01d9f6246936ee6682e5cb840a394628c79d0d74c898c73cac2515ed9e05303b +Nonce = +PersonalizationString = +EntropyInputReseed = e2ae7e8d2e3a182936891c066751d40dd6c92ebe146dd13d4e076591d7d63f8d +AdditionalInputReseed = +AdditionalInput = +AdditionalInput = +ReturnedBits = 1ce04a78ac2d53db46a1bb9240d47f37134ca7a2826c09ceb48d533d645bb087bfb77b18f9aafd1cf1727ad48aede207f490bf53e1e19f9f06615dd937073c11 + +COUNT = 11 +EntropyInput = c91189669aab973c92c9a71fd68db253d2adee1cbf25bd6a4a1fa669f7d06e35 +Nonce = +PersonalizationString = +EntropyInputReseed = b76f3931100b658fc064a1cd21cb751d57708f71e903bf7908a80616fa7e5bcf +AdditionalInputReseed = +AdditionalInput = +AdditionalInput = +ReturnedBits = 86a59707f43f09df04d060e9ad080f2d9584dc33c8f2de9733751de4ae17da5ac93ad9f7e304390137325216f37c77a712b6756e6ffa382b63495eeb80332456 + +COUNT = 12 +EntropyInput = b0c35bba01043398443d68dfe2c8898933ce58b98a598064b76d095c30074bf6 +Nonce = +PersonalizationString = +EntropyInputReseed = 02fdeb64d0973996a8a8a0629026f56cbbb91fca34b8f50ec059e746d4b20b1a +AdditionalInputReseed = +AdditionalInput = +AdditionalInput = +ReturnedBits = a3dcfd3547814b5439dd5cc6178c6632cccd81fcc34b8f9c9ceb52c23efdd18bb4873b97ade53c54824c8768df0e9987ecfa9635e1ba3944d8694f7ca8c51fac + +COUNT = 13 +EntropyInput = 569f3b21f1b80c6c517030f51cb81866cda24ac168a99b4e5e9634d0b16ac0a1 +Nonce = +PersonalizationString = +EntropyInputReseed = 0d6625c6e5102216d4e0e5e6171d8ee260cacde6bdb5b082cb9bcfe96b67986e +AdditionalInputReseed = +AdditionalInput = +AdditionalInput = +ReturnedBits = 006be6cbd866e275d97cc499813f462587f938054d733ff209d3035fde3e2d6915cf6ca3342d9064df7ac8075b3f54f87b35cd9b4ebc56835a9ea2557d8e154b + +COUNT = 14 +EntropyInput = 8ecdbf1cba26eae45f70ccfec0e42d6139be57f131ff60898a3b63968acf28ac +Nonce = +PersonalizationString = +EntropyInputReseed = 8d860dcf67fbee47f33ed5273ff81956335d9152085f184f8427ad4234f95661 +AdditionalInputReseed = +AdditionalInput = +AdditionalInput = +ReturnedBits = 8049f3fe2e62883f71cc43873b9775bf60a97c070370f9757c51488b050c00959d085ddd8f8e3702aa4cd6ff19b6c62685afb7792eb003c07bbcc9f4a026d138 + +[AES-128 no df] +[PredictionResistance = False] +[EntropyInputLen = 256] +[NonceLen = 0] +[PersonalizationStringLen = 0] +[AdditionalInputLen = 0] +[ReturnedBitsLen = 512] + +COUNT = 0 +EntropyInput = b2b3298306471bfcae61438a3a79e2355efa0b6ede4cbcd3e66de5140b3ae680 +Nonce = +PersonalizationString = +EntropyInputReseed = 26f0d1e44be575ee6f3eda89c1e7e4fbd1428f8852604871c7a4f4c707a39328 +AdditionalInputReseed = +AdditionalInput = +AdditionalInput = +ReturnedBits = b71b6d7322453a612c34a91c1e5c3f8c30486a692b1ad13a4c08caccd123a639fd2e0a7c389cfa1a97cb78b438dff57b0b5ec4d569a8b2810a15f85c8c9226bf + +COUNT = 1 +EntropyInput = 66f41dc791e155127b7fc680842021296ffd9f5e11d1094eb446af24375f7f79 +Nonce = +PersonalizationString = +EntropyInputReseed = 044eb12db05c2f4574b9b1628c6589177072e8607afed27e4a8faa1adbdd96f0 +AdditionalInputReseed = +AdditionalInput = +AdditionalInput = +ReturnedBits = f272682d392fe707ff8faae471b7fab6851460edeb207d9e7db96d2c27b66c5c45f98a445defd0895e8c3f47fe85f8de3c62d4028f4fcc891e288fcd780f212d + +COUNT = 2 +EntropyInput = c6506dd8b9b74ca16bd4119b690fc30da5fced3b8d315e957b12f1cd9bf95f94 +Nonce = +PersonalizationString = +EntropyInputReseed = 0c80ff54c49ab89782716b1d515da24c3031f3ef7179e599edc8d865a03c799f +AdditionalInputReseed = +AdditionalInput = +AdditionalInput = +ReturnedBits = e1696cdb476b24b513c1e87e68c74b1f3842f4294abf2a1a810073a6a5c8c43993a56507442db36a38228b9c25252993dd8865096fedb5858924f079d56c30d1 + +COUNT = 3 +EntropyInput = 6b2e86d5fc5469da4ed332f4d123a611f60d96491c0a45088104768712314377 +Nonce = +PersonalizationString = +EntropyInputReseed = 762c53608f075ba1926337a61c35dbfe024427d290260785b43b39f32d282fc1 +AdditionalInputReseed = +AdditionalInput = +AdditionalInput = +ReturnedBits = 7a6e193ce1eeb6a543d2ed0c4dbbe42e9b1aee2f3c990916e213f0761399464301c22e87907d3c1b20b1987260157dbc66fb860d6896add9abd5ed256c763563 + +COUNT = 4 +EntropyInput = 4fbead74782fdcd2b3318c71e78aceb23a4d0399fc5c0342545b9980dcd85d53 +Nonce = +PersonalizationString = +EntropyInputReseed = 53a455121596b49dfa3a97932c715f926de40e6fc930a8dd5736159c7493189c +AdditionalInputReseed = +AdditionalInput = +AdditionalInput = +ReturnedBits = c912bfc93b9478bf98f54d5854bb7d9f4eb5d464891cd0c5e84cbc4b44695f7420bc21c6fa7fb57d9cbdc2e58a35d001c5162983ae7f9035bd81dcd1eec09997 + +COUNT = 5 +EntropyInput = 711bf34f33844b1101690aab9a91cf42da10dc6fa281bf0650bc2b21272c02f5 +Nonce = +PersonalizationString = +EntropyInputReseed = 4c5a8aa82421cd101e534813764b6c514de6301826dde3b92124b335ebbf6f92 +AdditionalInputReseed = +AdditionalInput = +AdditionalInput = +ReturnedBits = 6035a0ff6b26d2b9386e893a704f208b67d5b3550ded606c6d5fdb3f6177f3a5d70bf0844cef252b3b38ecc683a8670a9235143137d3e44514598c4486eb7345 + +COUNT = 6 +EntropyInput = 8c006c19f2da7ae88218fcd50d5c6d5102992b4762b1475e122a30ed13292d02 +Nonce = +PersonalizationString = +EntropyInputReseed = 5ce98d2a582b95bda230b7482ff800a82891d6b1df75fececae5d7067ddf5b46 +AdditionalInputReseed = +AdditionalInput = +AdditionalInput = +ReturnedBits = 2a19d7b35833eabfbc43cbc3dde14392d82f3283efbbd33f134b32040402c71326cd31d37e25722ce73bf3640e5e2b00d7dd278f28a0f4e43f8935377ca1a60b + +COUNT = 7 +EntropyInput = 9deefa779337ef1ff7f47c4819d175024df349f8a5cfe857c9b5e822d8dafc56 +Nonce = +PersonalizationString = +EntropyInputReseed = 558d79c83d81d3fd07d6eb739ad30e298345bc4b906d2f6f87ceeb793aaae8d6 +AdditionalInputReseed = +AdditionalInput = +AdditionalInput = +ReturnedBits = 13da72e2a48e5893ae78644057f7d344cf5b56cfc9c49b7e0979c575350718588e73bd130ede3b845131452b820e41e8c99bb7e582e6e8a2e452c09004ade40d + +COUNT = 8 +EntropyInput = b8d82f15c3e0df02d2dcd5a1995cbf478845426ff9e4fd7cbac494a2b750a6db +Nonce = +PersonalizationString = +EntropyInputReseed = a34cd689589e4f6f97356f95fcc8ddfd48401043a6f0a0bb4c8359ca97e3e4ca +AdditionalInputReseed = +AdditionalInput = +AdditionalInput = +ReturnedBits = 29c90817b65f08f92bd568cf40553d998d0f4548276dba089a029625619fc2af85fb64d92a7c0c3337e58d05c34bfae1b999d62e500ce75cb33dec5dcb4d96c7 + +COUNT = 9 +EntropyInput = 10d5d638f70d4b37c91be44c182129264957610184cd9de238745b10579a93de +Nonce = +PersonalizationString = +EntropyInputReseed = 3697340c478373a3b229157e99dd9546fb0fd0370b3739382d384990c2b85b5e +AdditionalInputReseed = +AdditionalInput = +AdditionalInput = +ReturnedBits = a31bba6f7b203e056fff4510250616c8d67e0eb9ac2d11d7f4888846c197971bdb8edb2aaeda12023c4a0d199a892914ef22af691389fe56e9acf31fb58b63e6 + +COUNT = 10 +EntropyInput = e1f67702ec10c75ca3e9d30816c479bf4d04dbd1f664738b21d529e5460e92dd +Nonce = +PersonalizationString = +EntropyInputReseed = 00c8ac1971096ae2a8d288a962e5ab331ebd4ede7dd0723b0a92f9869ab7ea31 +AdditionalInputReseed = +AdditionalInput = +AdditionalInput = +ReturnedBits = a1580a82cdc08611f86b0f1be48810e32f084828e6156cde1a2b204b5d30636f1f06324e215d1c0de88d6034a8e7369ea845f8d4afcbe93bb2470df12a993fe3 + +COUNT = 11 +EntropyInput = c96da7204df69347b91dec743dcbc86ca6690eba8193d1434b1b87c4af03cc1d +Nonce = +PersonalizationString = +EntropyInputReseed = 429674cb12e355884c33f29b46e257f0fb0c38fac9039c0ffc2a77b29acdf1c6 +AdditionalInputReseed = +AdditionalInput = +AdditionalInput = +ReturnedBits = 286f173772d9423e8a49a2a677366ffe2125e116646e799d1c377b330f5a17b82adb652ba9f14a570d3cd3b5e2fbb8df03119dac219d872b11e750fdb326a62d + +COUNT = 12 +EntropyInput = 5d31f81a37c9b5d84b876d8c3825b00edc9f4a51fe82141ddc58b65d5f9a37cd +Nonce = +PersonalizationString = +EntropyInputReseed = 41da0230bb94f2601447e490b0220e7a1f4b2c420ec6de0d675f6343f34f1b6d +AdditionalInputReseed = +AdditionalInput = +AdditionalInput = +ReturnedBits = b618afef1132935ae9ed83e13cc707833219ffbd885ed7aa279b6df4ef62864b3fa3cc7ecbe7d7e1f3b0d5354706973a8594e4124357caf31ffc1d04d49df69b + +COUNT = 13 +EntropyInput = c50ceace05b9aac3407c91fc401e1778d2d7aa44b7a42af6774fd80a139b4e3c +Nonce = +PersonalizationString = +EntropyInputReseed = 15fe0dba967be9c76687c82d74b0a018cd96a81cfbd02e600f99f1d3e965fae3 +AdditionalInputReseed = +AdditionalInput = +AdditionalInput = +ReturnedBits = c0d80e37d20228b9e07ba8da2178bf18ea8d497cdae27ab37d17ebf9baee9a4b88953301c3642de5965a6ca7f90e9f48f8e62e338c77eb859c696088679fb0a4 + +COUNT = 14 +EntropyInput = ac5668ac054f732d2bcd88561642c5a7ca98c68e341cf0cf18873fea93ef33fe +Nonce = +PersonalizationString = +EntropyInputReseed = 4a4d088beb9843e4622cdb0c5a6851587f2b472dc5d734211409bacec7b2ac06 +AdditionalInputReseed = +AdditionalInput = +AdditionalInput = +ReturnedBits = b2013a363f3ee01ab8573f3e3eed32285108c3ed3bf231c066176ed901e4d6ffaaf0cfd12d63d7c19f6c460baf434a1d6a552c62274bcb7469f7009c0beab972 + +[AES-128 no df] +[PredictionResistance = False] +[EntropyInputLen = 256] +[NonceLen = 0] +[PersonalizationStringLen = 0] +[AdditionalInputLen = 0] +[ReturnedBitsLen = 512] + +COUNT = 0 +EntropyInput = 858204f27e93e872a1ce6208ef6de39868ddad55eac899ca0581b9bb697d4417 +Nonce = +PersonalizationString = +EntropyInputReseed = 1e822008aecf6e67cc47f2b98b04687b121ea4036b6cabb0c36efbe15146380b +AdditionalInputReseed = +AdditionalInput = +AdditionalInput = +ReturnedBits = 49ce84963df864aae36bec3f3ed223f95df4956c7c01b620d5f210e638659d5e1886700c04e79e579ca7c9b05454b525c2ea7348032b3ab8d91b4c47002204af + +COUNT = 1 +EntropyInput = 04ca8b70f0d95b2a9f31daa666ac57d2bf7c02fb65122ab8190c567855debab6 +Nonce = +PersonalizationString = +EntropyInputReseed = 0b4093ae0f4c7fec06ecb05da516e22371dc8abd557217903bc5e319c50a0b51 +AdditionalInputReseed = +AdditionalInput = +AdditionalInput = +ReturnedBits = d7713270e7bc2221de929d9622eefe35a4285c27616e2d0ddcf65ec4e79ac518097eacf2a5ca3d207c2017ccbc3a9343587e34ca12a97afaea678bba86b65ca8 + +COUNT = 2 +EntropyInput = 8226acffba923fa4632d9d52bd8263ffe608a72b65559f66e28323bce09ca28d +Nonce = +PersonalizationString = +EntropyInputReseed = f93eee497ae3f066cce46684eb30beff68e8f5738cefdbd519f9a35b2ab2a6ac +AdditionalInputReseed = +AdditionalInput = +AdditionalInput = +ReturnedBits = 14c38f2c21740df8c2f2870cd1fc6c536c1900b3dae634c28ba955b135ebf1ac8e6302dc80f89af982653d2914dff10ec72c09f0d80c918fb5ff96bae888eed5 + +COUNT = 3 +EntropyInput = 1c1db7b9c12c06582fca5304c5da9b14589f52f784e63d2fd6590e042329b950 +Nonce = +PersonalizationString = +EntropyInputReseed = d4bf05755419efc283c0ec0177d15cc44a6384da6ff067f2b9ce5a19f61f082f +AdditionalInputReseed = +AdditionalInput = +AdditionalInput = +ReturnedBits = 91cbd4ea474ed13d2db18be2cb0866160fed17f21b9828333717ccd8cea5129a407aa23d9e9c1b72eadf18ab3cb9d5c2fa0a031cbe3dbc0bb67b2dcb014b98c1 + +COUNT = 4 +EntropyInput = b75e038271398c6e0aee806599c8c088ae926ee714b0e6efa250cf1ce4f9f714 +Nonce = +PersonalizationString = +EntropyInputReseed = b988b167f975975b65b2e0ea2e69c50fb34e69a05bc78cc26e4095f1cf67eb21 +AdditionalInputReseed = +AdditionalInput = +AdditionalInput = +ReturnedBits = 5e1fdbc3633ebe8799f6ca4db5301aff0660ead7825cbc48194aa38770c898f7415a8a6b9307f09ccbf5fce386eab33a973cc46e59bc49ac90d5bc4a47fd3d39 + +COUNT = 5 +EntropyInput = c061cf7b1804d7e79fb38532c9d0d6edebc5c4b1a47d99bfad506ab1c4e020ca +Nonce = +PersonalizationString = +EntropyInputReseed = a2e860023ba15c23215dfb317a99b23868c551fc5ecd1f9571ab9f779200de0c +AdditionalInputReseed = +AdditionalInput = +AdditionalInput = +ReturnedBits = ca4ca61ac87f7aaee3edf69ed95afe4386c7eb0ed20cf0d6fecd89453270c1b1e7ac4facc2f73eb694e350fa12ae82b15c70e2789508d77464ff4fb91bf60fa2 + +COUNT = 6 +EntropyInput = fccc1936326f01a31abaeb6182c99460dd0c923d8fbe55bdc958b128753a3c29 +Nonce = +PersonalizationString = +EntropyInputReseed = dc814a96dbcdc333a94d665483b11fd07a7b5feaaa49a23ee77cca7407548254 +AdditionalInputReseed = +AdditionalInput = +AdditionalInput = +ReturnedBits = e6ba274168825d39e9be80cc6901ae41d8114502c0a10fc495626aa64974c0065eb0e753f669d174a0813aaddec0c7209d19794c9aa14c410abc13f3dd8eba3f + +COUNT = 7 +EntropyInput = a033079ade6fdeb42f91207ca48a4686d962abd65f590d0d8ec58d3ad351fe49 +Nonce = +PersonalizationString = +EntropyInputReseed = 1df48c8facaaaedd6cb895c5aac7bc61b085cb29e3daf5716b55b8958278ceb0 +AdditionalInputReseed = +AdditionalInput = +AdditionalInput = +ReturnedBits = b75a092d2a9154c33042cbd8bb960ed742dc7a002c16e5e747dd58d310577dd0780d4e39fdb897ef37570250714e7468e687d572919ca94a273603464d3e91ad + +COUNT = 8 +EntropyInput = aa83d51123e9dfbdd08f8a0e1b46bff9657d07e22f4bc82c1eba649c78d21a2b +Nonce = +PersonalizationString = +EntropyInputReseed = 80fdcb159f1eb641d72da1d92740ee37a3b1f5ea7ec3695095a5679a8d673449 +AdditionalInputReseed = +AdditionalInput = +AdditionalInput = +ReturnedBits = 6c6523398f06297c9e834d102260c18014f2493b068f6583a8436fc7964e99bbc9315b7dc8015dc4e0548942b024e7c1ab5c2d3750967d7eb6a17837ae948289 + +COUNT = 9 +EntropyInput = 9b237c7a8cae3912a8686e84088ac3b65890d70ed5d20f55e2168c12966bdcf4 +Nonce = +PersonalizationString = +EntropyInputReseed = 5230038a3dd71a1587aa0eb559646601a796dc6947eee286da40bb6df1faecb2 +AdditionalInputReseed = +AdditionalInput = +AdditionalInput = +ReturnedBits = bd22a65b536ca99183a71452ddf6927e6bd6876125076db76134f4b52c359f4644faed46454bc2f72ebf698c86603bcc3b575cfd939cd0861073b32a0c73ecf4 + +COUNT = 10 +EntropyInput = 4bf2b62635adb5d6a0133aed5f19f3368b94f9cb1adbf2ab0b144e618139e831 +Nonce = +PersonalizationString = +EntropyInputReseed = 7d52bac3d0ff5f2bf78f630907222b6ce51ff431c99509beef160d327e3bc8e9 +AdditionalInputReseed = +AdditionalInput = +AdditionalInput = +ReturnedBits = 11abad1fb1325ee33231f390e95065022dea2a57e00b105078df01de3d59adb828f54ce7e9d909b9757146de17a743cc2e57ed14b8541a1a0d8f577f83d75c1a + +COUNT = 11 +EntropyInput = fdce5aeb11f925075b89f95efeeb5c0ade47c5d35f476f78f51450aa1c5d6b40 +Nonce = +PersonalizationString = +EntropyInputReseed = 997384943e5593d616803726b7e80fa8dd785ab2f330b9ae26f65d70a780dbf3 +AdditionalInputReseed = +AdditionalInput = +AdditionalInput = +ReturnedBits = b3f2ecd171b6afbe1bd22cf881ba0a38d7f94b688b4237d3b6e73764854112e9c7c24d8c7e8efbfd9d903b9bceb8e675436680b482ca48ae594985ae16266740 + +COUNT = 12 +EntropyInput = 2ad99598325003579bfd0cf057ca8d287194ffa0d4ade2f9ec5b7a671499a765 +Nonce = +PersonalizationString = +EntropyInputReseed = 9eb02dc8bf74303c542c6da4bff72cb8a28593875a7e9ba0da73bc7b47399576 +AdditionalInputReseed = +AdditionalInput = +AdditionalInput = +ReturnedBits = 65bd86707d0276cd6f079555b8192710ab28b9eb0dcf3c1b01c2f46830477609b41e70ab364b48a89f622416c3c114ea990de74b24c67e2618d1c49f1d169eb3 + +COUNT = 13 +EntropyInput = 4972641ea24232631a5d80832e98ebb1aa1856ef0899920a5b50aa06d205184a +Nonce = +PersonalizationString = +EntropyInputReseed = f5e91c154d6d0cce6b15cca44ba64b670134f6dc869d01057e20c0e027520b4c +AdditionalInputReseed = +AdditionalInput = +AdditionalInput = +ReturnedBits = 3f8a145d9ad6ce2ab88cd863da34a3e5a3d767039769677f5c532dbeb416d39dec4c291ce50ce5377580fe97c193144d79b959f0dc342493daa819eb6012fa9f + +COUNT = 14 +EntropyInput = d083e222d8159740044707c76bd9ec4436202ac778f63646e5b1e88f21ddc13f +Nonce = +PersonalizationString = +EntropyInputReseed = b7b714550798c888a5026b0b7801c0923ae60a2858cabb6d6972d66115f40eda +AdditionalInputReseed = +AdditionalInput = +AdditionalInput = +ReturnedBits = 227a88583bb137f082967af04c27cb464a6332720b759b435d4a7e26349f56f4bb447695c06295e838a6c86fc3867006217c94bb5cc99b3c44bfe541fc77503c + +[AES-128 no df] +[PredictionResistance = False] +[EntropyInputLen = 256] +[NonceLen = 0] +[PersonalizationStringLen = 0] +[AdditionalInputLen = 0] +[ReturnedBitsLen = 512] + +COUNT = 0 +EntropyInput = 6eb2e533b902545c31cdd09a062cf59346594f34b563b090dfbd632aeadbd191 +Nonce = +PersonalizationString = +EntropyInputReseed = 2cd1cec6242ac25e7518b98da8a22bd13acfaf5fa45fee6021ba283926de1922 +AdditionalInputReseed = +AdditionalInput = +AdditionalInput = +ReturnedBits = ba5cab62fddf8c6ed16733c5bf0e39336c189d80a9669043ac892cea63d7e70d390e91741840bf7ee14c078cbbe97d114992ae781d9966a5f6c704e5c01811b5 + +COUNT = 1 +EntropyInput = be72de2b1cdbf0c8fbcd4cfe0b0f8dc5d93d035d9faf884bfba2ef19723f196e +Nonce = +PersonalizationString = +EntropyInputReseed = 89c4f4357f799b658e9d1ca53402bc60264b4cedeb553a0d8afc9046391a043d +AdditionalInputReseed = +AdditionalInput = +AdditionalInput = +ReturnedBits = 0de19571b5f54c8b134933d76b2fa21826394c3bfc06dbdb08c078a158fbc38aac3fcaf6913d1c588a6e369b12b1cd946dce24d96c4ca1746ed1e90ab2620df5 + +COUNT = 2 +EntropyInput = 5b3ea39aaff5c04009eea65cad7c3a02d4f8807255a4f48e60e362e0e4177585 +Nonce = +PersonalizationString = +EntropyInputReseed = f1906910179d2d64ac131075ee60aa690ca4345c187505547db4f43958f6e1c6 +AdditionalInputReseed = +AdditionalInput = +AdditionalInput = +ReturnedBits = 3c5ab25397e02a8c57d24997e51081aed09eb8fc68612cd19c9d560f3597c9db6aaf5f6222778d8184c83ff166f0591ac2a8b4bbe9edfc4fd4ddb3aadcabd324 + +COUNT = 3 +EntropyInput = b1d87c20b18ef4d79bc034e52e69e07f0d1c224d49cfe77c5df5ff7f2605eef5 +Nonce = +PersonalizationString = +EntropyInputReseed = 0bad85806063a7c6f50c7ec498cada912c6167bd6ee2cd3be8d2de94c17dcfa8 +AdditionalInputReseed = +AdditionalInput = +AdditionalInput = +ReturnedBits = 4953c4706fb27457bef8bb4fc4929fe227eb7beb90e66edab9e26ab7671f28a4f5b65d1edf3fc267b4f3e51f4ca7f07f476289ca0ae3a86919705d2dad0e48b7 + +COUNT = 4 +EntropyInput = 082a0f42b54b524eaf14a952197594a1947cffc289716776d52d02d00c7630e0 +Nonce = +PersonalizationString = +EntropyInputReseed = 5e798f69fcd017ceea3c1434ea597dac6847a63a8093309dcbe54fc2a0272b7d +AdditionalInputReseed = +AdditionalInput = +AdditionalInput = +ReturnedBits = 0946b91dc3f19cf08a2af86ba65ab9d162de8f645c2f531dd16a22f615f0562f79a128432e23953dc3f615612218edbee5c9d062ae009d4a2d2416718d593b94 + +COUNT = 5 +EntropyInput = 5f41de01517a0d31a7d53535b858257c9d80e8123cca98ad199ae53e776db9e3 +Nonce = +PersonalizationString = +EntropyInputReseed = b857f6328145fedfd71d2dc3d4c99068bd4648d33103415b9fe114826af66a84 +AdditionalInputReseed = +AdditionalInput = +AdditionalInput = +ReturnedBits = e24446ec149f6f16e4069b27f45b0db293f5ff4b6a0cb465fbef9709a6d9af20a3be609cbf1c647bcda77fdfe30379583f67ad73b9d62a4f5187f21dbb89bb36 + +COUNT = 6 +EntropyInput = d4fe09372d8bbaf39f8388f193310ee63c4ada308a58783f4bfa580806f01bc6 +Nonce = +PersonalizationString = +EntropyInputReseed = 1e46de02dfd2a9f11d2d683f4ab79c2588627872ab14fbcc0bb619d31687d572 +AdditionalInputReseed = +AdditionalInput = +AdditionalInput = +ReturnedBits = 20d87a94ba7af20e41e3848ec31fcbd0b8e51aae70a25eb2d53203a77aacd53240b911585f115d8b8a07e38c4e101cd2ac46d7c9f32505b8564ebf7acd97a799 + +COUNT = 7 +EntropyInput = 5488cd2fb7a002ea1ed7eb6f585c23ddd73cd56c14d39bd780b3069984da0020 +Nonce = +PersonalizationString = +EntropyInputReseed = 24fdcceb766fcbf23c8954625f759d343dd31703a0039e95968b482a33583efa +AdditionalInputReseed = +AdditionalInput = +AdditionalInput = +ReturnedBits = b6d58552bde595fe3eb0ae54531e984510e545bfc45769930a1fef2f96c5d47ea1ff8d3ea417917b8c9a00aa32e4891f3bfe5d6121bb404828866accd5ac769f + +COUNT = 8 +EntropyInput = 0f1e7470944e377d00e87f0920fe60ed4734f6fa4034d1d1a9ad0e67499e3b56 +Nonce = +PersonalizationString = +EntropyInputReseed = b0bbd0b3206825e484118b9ac4a9c7b9bf6983983cb4180d5ce278475e2e85e1 +AdditionalInputReseed = +AdditionalInput = +AdditionalInput = +ReturnedBits = 7e659290503df5ce1762ba721e9f50a0d98710998d75e1fde92829106f1007458574fb1cc73f69427d106d318f1c1690cf748eabad72f870c6056613990f1db9 + +COUNT = 9 +EntropyInput = 30d6adf9b8451390ca566ff199c01db214b9f9e2b147bf5806b7055457930c78 +Nonce = +PersonalizationString = +EntropyInputReseed = 8260eb48acc59ec946905c56eaab34cafa412dadbca02b30c293bff0e6437569 +AdditionalInputReseed = +AdditionalInput = +AdditionalInput = +ReturnedBits = 29aef17c9992e7314aca59626d250820def81be802421ac7ba373905e9f549dfacdede4ef751e447149750148370b8a547c7041495d945985726f897e9ddd3e1 + +COUNT = 10 +EntropyInput = 7172ba994b2eb0c9316e874d2a5c975cfd7e43775912b29436fa7510585a7dc5 +Nonce = +PersonalizationString = +EntropyInputReseed = 9ac9e5144cfd52d3df334736099d2cd9693c97edc7c41c1e173009148549ce8e +AdditionalInputReseed = +AdditionalInput = +AdditionalInput = +ReturnedBits = 6df1de2ddd713f9bc01688366a44d121686812041f0032bc11a8fbbf7f5a225dcc06b5fd12c1911edcecb8065e6295a0b8aa838f3e82f4dce8882ebaeb80276e + +COUNT = 11 +EntropyInput = 33c49e742bb3d9033612f3235e8402904da8bec443393124eabf0ffd92c7fd3a +Nonce = +PersonalizationString = +EntropyInputReseed = 705bb429e7cabdee771655b3a85d4ee3271c9f0571ead34f5d7d1df3918d3caf +AdditionalInputReseed = +AdditionalInput = +AdditionalInput = +ReturnedBits = 19c54ecb76924ff930ada1aa6f35b42c225a0e7264534177e0791cffda4267763429788028eacf9b987766f53a066fa6918958e92e66bf0fac606376c857a078 + +COUNT = 12 +EntropyInput = 0bde8403f4effc3f588d4eaeb9412309ed703d889d118123d5ad55d4b6ad0482 +Nonce = +PersonalizationString = +EntropyInputReseed = 9fd60f0eafedfc8573db6d114f8733c369aa7a449d58642846ed8c049ece928d +AdditionalInputReseed = +AdditionalInput = +AdditionalInput = +ReturnedBits = 86653c0ac3261874a399e4965a12bb98626764fcfa546899cb22e47a4d5ce2121929772ad72e8b6988be1077de4510ff0d4f204a92fa1ead4c1b82a224fb74bd + +COUNT = 13 +EntropyInput = 64591d27dbfb756fa882399d93e24604bbbaa5320238c36d8b66554cb342fc23 +Nonce = +PersonalizationString = +EntropyInputReseed = e28b909ecbbf0c5faf80bd2caea14f55dca08f97206c0e0a44c54725544dd4ac +AdditionalInputReseed = +AdditionalInput = +AdditionalInput = +ReturnedBits = 0f5b8f9e65af247ec6e64a99e7c0a19d9a757b88895db9cf015e6177ec2464dec78e42f19a0fe52f90795b3fdd331223348d4328c1840507881db7a19f168adf + +COUNT = 14 +EntropyInput = e3d8fcb8c049e442d2bd07104c46f0602a1f60f87bdc02dbecdcfcf4006b5b0a +Nonce = +PersonalizationString = +EntropyInputReseed = e25327867ff27456eff9f4ae4375c7a85788b400dcae03ae8c892472c8a05221 +AdditionalInputReseed = +AdditionalInput = +AdditionalInput = +ReturnedBits = 754063c679269931fdab8f90deaa967969f20b1805d93fe5b1928512cd2fe98984974b0bb1d7494d81f53e073f1a3a9378ea27307a154dc8a1fb1d3e17998f85 + diff --git a/packages/core/src/security/ctr_drbg.ts b/packages/core/src/security/ctr_drbg.ts deleted file mode 100644 index 4db39f8f71bb..000000000000 --- a/packages/core/src/security/ctr_drbg.ts +++ /dev/null @@ -1,251 +0,0 @@ -/*! - * ctr-drbg.js - ctr-drbg implementation for bcrypto - * Copyright (c) 2019, Christopher Jeffrey (MIT License). - * https://github.com/bcoin-org/bcrypto - * - * Parts of this software are based on google/boringssl: - * https://github.com/google/boringssl - * - * Resources: - * https://csrc.nist.gov/publications/detail/sp/800-90a/archive/2012-01-23 - * https://github.com/google/boringssl/blob/master/crypto/fipsmodule/rand/ctrdrbg.c - * https://github.com/google/boringssl/blob/master/crypto/fipsmodule/rand/internal.h - * https://github.com/openssl/openssl/blob/master/crypto/rand/drbg_lib.c - * https://github.com/cryptocoinjs/drbg.js/blob/master/ctr.js - * https://github.com/netroby/jdk9-dev/blob/master/jdk/src/java.base/share/classes/sun/security/provider/CtrDrbg.java - */ - -import { Bytes } from "@zwave-js/shared/safe"; -import { increment } from "./bufferUtils.js"; -import { encryptAES128ECB } from "./crypto.js"; - -const MAX_GENERATE_LENGTH = 65536; -// const RESEED_INTERVAL = 0x1000000000000; - -export class CtrDRBG { - constructor( - bits: 128, - derivation: boolean, - entropy?: Uint8Array, - nonce?: Uint8Array, - pers?: Uint8Array, - ) { - this.ctr = new Uint8Array(16).fill(0); - this.keySize = bits >>> 3; - this.blkSize = 16; - this.entSize = this.keySize + this.blkSize; - this.slab = new Uint8Array(this.entSize); - this.K = this.slab.subarray(0, this.keySize); - this.V = this.slab.subarray(this.keySize); - this.derivation = derivation; - // this.rounds = 0; - this.initialized = false; - - if (entropy) this.init(entropy, nonce, pers); - } - - /** The internal counter */ - private ctr: Uint8Array; - private readonly keySize: number; - private readonly blkSize: number; - private readonly entSize: number; - private slab: Uint8Array; - private K: Uint8Array; - private V: Uint8Array; - private readonly derivation: boolean; - // private rounds: number; - private initialized: boolean; - - init( - entropy: Uint8Array, - nonce: Uint8Array = new Uint8Array(), - pers: Uint8Array = new Uint8Array(), - ): this { - let seed: Uint8Array; - - if (this.derivation) { - seed = this.derive(entropy, nonce, pers); - } else { - if (entropy.length + nonce.length > this.entSize) { - throw new Error("Entropy is too long."); - } - - if (pers.length > this.entSize) { - throw new Error("Personalization string is too long."); - } - - seed = new Uint8Array(this.entSize).fill(0); - seed.set(entropy, 0); - seed.set(nonce, entropy.length); - - for (let i = 0; i < pers.length; i++) seed[i] ^= pers[i]; - } - - this.slab.fill(0); - this.ctr.set(this.V, 0); - this.update(seed); - this.initialized = true; - // this.rounds = 1; - - return this; - } - - reseed(entropy: Uint8Array, add: Uint8Array = new Uint8Array()): this { - // if (this.rounds === 0) - if (!this.initialized) throw new Error("DRBG not initialized."); - - let seed: Uint8Array; - - if (this.derivation) { - seed = this.derive(entropy, add); - } else { - if (add.length > this.entSize) { - throw new Error("Additional data is too long."); - } - - seed = new Uint8Array(this.entSize).fill(0x00); - seed.set(entropy, 0); - for (let i = 0; i < add.length; i++) seed[i] ^= add[i]; - } - - this.update(seed); - // this.rounds = 1; - - return this; - } - - private next(): Uint8Array { - increment(this.ctr); - return encryptAES128ECB(this.ctr, this.K); - } - - generate(len: number, add?: Uint8Array): Uint8Array { - // if (this.rounds === 0) - if (!this.initialized) throw new Error("DRBG not initialized."); - - // if (this.rounds > RESEED_INTERVAL) - // throw new Error("Reseed is required."); - - if (len > MAX_GENERATE_LENGTH) { - throw new Error("Requested length is too long."); - } - - if (add && add.length > 0) { - if (this.derivation) add = this.derive(add); - - this.update(add); - } - - const blocks = Math.ceil(len / this.blkSize); - const out = new Uint8Array(blocks * this.blkSize); - - for (let i = 0; i < blocks; i++) { - const ciphertext = this.next(); - out.set(ciphertext, i * this.blkSize); - } - - this.update(add); - // this.rounds += 1; - this.initialized = true; - - return out.subarray(0, len); - } - - /* - * Helpers - */ - - update(seed: Uint8Array = new Uint8Array()): this { - if (seed.length > this.entSize) throw new Error("Seed is too long."); - - const newSlab = new Uint8Array(this.slab.length).fill(0); - // this.slab.fill(0); - - for (let i = 0; i < this.entSize; i += this.blkSize) { - newSlab.set(this.next(), i); - // ciphertext.copy(this.slab, i); - } - - // for (let i = 0; i < seed.length; i++) this.slab[i] ^= seed[i]; - for (let i = 0; i < seed.length; i++) newSlab[i] ^= seed[i]; - - this.slab.set(newSlab, 0); - this.ctr.set(this.V, 0); - - return this; - } - - serialize(...input: Uint8Array[]): Uint8Array { - const N = this.entSize; - - let L = 0; - - for (const item of input) L += item.length; - - let size = this.blkSize + 4 + 4 + L + 1; - - if (size % this.blkSize) size += this.blkSize - (size % this.blkSize); - - // S = IV || (L || N || input || 0x80 || 0x00...) - const S = new Uint8Array(size).fill(0x00); - const view = Bytes.view(S); - - let pos = this.blkSize; - view.writeUInt32BE(L, pos); - pos += 4; - view.writeUInt32BE(N, pos); - pos += 4; - - for (const item of input) { - S.set(item, pos); - pos += item.length; - } - - S[pos++] = 0x80; - - return S; - } - - derive(...input: Uint8Array[]): Uint8Array { - const S = this.serialize(...input); - const view = Bytes.view(S); - const N = S.length / this.blkSize; - const K = new Uint8Array(this.keySize); - const blocks = Math.ceil(this.entSize / this.blkSize); - const slab = new Uint8Array(blocks * this.blkSize); - const out = new Uint8Array(blocks * this.blkSize); - const chain = new Uint8Array(this.blkSize); - - for (let i = 0; i < K.length; i++) K[i] = i; - - for (let i = 0; i < blocks; i++) { - chain.fill(0); - - view.writeUInt32BE(i, 0); - - // chain = BCC(K, IV || S) - for (let j = 0; j < N; j++) { - for (let k = 0; k < chain.length; k++) { - chain[k] ^= S[j * this.blkSize + k]; - } - - // encrypt in-place - chain.set(encryptAES128ECB(chain, K), 0); - // ctx.encrypt(chain, 0, chain, 0); - } - slab.set(chain, i * this.blkSize); - } - - const k = slab.subarray(0, this.keySize); - const x = slab.subarray(this.keySize, this.entSize); - - for (let i = 0; i < blocks; i++) { - // encrypt in-place - x.set(encryptAES128ECB(x, k), 0); - // ctx.encrypt(x, 0, x, 0); - out.set(x, i * this.blkSize); - } - - return out.subarray(0, this.entSize); - } -} diff --git a/packages/core/src/security/ctr_drbg.wrapper.ts b/packages/core/src/security/ctr_drbg.wrapper.ts new file mode 100644 index 000000000000..0dcff7d40090 --- /dev/null +++ b/packages/core/src/security/ctr_drbg.wrapper.ts @@ -0,0 +1,49 @@ +// Wrapper class for both sync and async CtrDrbg implementations. + +import { CtrDrbgAsync } from "./ctr_drbg.async.js"; +import { CtrDrbgSync } from "./ctr_drbg.sync.js"; + +export class CtrDRBG { + private drbgSync = new CtrDrbgSync(); + private drbgAsync = new CtrDrbgAsync(); + + public initSync( + entropy: Uint8Array, + personalizationString?: Uint8Array, + ): void { + this.drbgSync.init(entropy, personalizationString); + this.drbgAsync.restoreState(this.drbgSync.saveState()); + } + + public async initAsync( + entropy: Uint8Array, + personalizationString?: Uint8Array, + ): Promise { + await this.drbgAsync.init(entropy, personalizationString); + this.drbgSync.restoreState(this.drbgAsync.saveState()); + } + + public updateSync(providedData: Uint8Array | undefined): void { + this.drbgSync.update(providedData); + this.drbgAsync.restoreState(this.drbgSync.saveState()); + } + + public async updateAsync( + providedData: Uint8Array | undefined, + ): Promise { + await this.drbgAsync.update(providedData); + this.drbgSync.restoreState(this.drbgAsync.saveState()); + } + + public generateSync(len: number): Uint8Array { + const ret = this.drbgSync.generate(len); + this.drbgAsync.restoreState(this.drbgSync.saveState()); + return ret; + } + + public async generateAsync(len: number): Promise { + const ret = await this.drbgAsync.generate(len); + this.drbgSync.restoreState(this.drbgAsync.saveState()); + return ret; + } +} diff --git a/packages/eslint-plugin/package.json b/packages/eslint-plugin/package.json index 19dc7a4ca160..6d9d661f476a 100644 --- a/packages/eslint-plugin/package.json +++ b/packages/eslint-plugin/package.json @@ -38,7 +38,7 @@ "lint:ts:fix": "yarn run lint:ts --fix" }, "devDependencies": { - "@alcalzone/esm2cjs": "^1.3.0", + "@alcalzone/esm2cjs": "^1.4.0", "@types/eslint": "^9.6.1", "@typescript-eslint/utils": "^8.8.1", "@zwave-js/core": "workspace:*", diff --git a/packages/host/package.json b/packages/host/package.json index 594082fd6155..8c0b5061e3ff 100644 --- a/packages/host/package.json +++ b/packages/host/package.json @@ -58,7 +58,7 @@ "alcalzone-shared": "^5.0.0" }, "devDependencies": { - "@alcalzone/esm2cjs": "^1.3.0", + "@alcalzone/esm2cjs": "^1.4.0", "@microsoft/api-extractor": "^7.47.9", "@types/node": "^18.19.63", "del-cli": "^6.0.0", diff --git a/packages/maintenance/package.json b/packages/maintenance/package.json index d5b21043ddc4..a736741c1ff2 100644 --- a/packages/maintenance/package.json +++ b/packages/maintenance/package.json @@ -38,7 +38,7 @@ "lint:ts:fix": "yarn run lint:ts --fix" }, "devDependencies": { - "@alcalzone/esm2cjs": "^1.3.0", + "@alcalzone/esm2cjs": "^1.4.0", "@dprint/formatter": "^0.4.1", "@dprint/json": "^0.19.4", "@dprint/markdown": "^0.17.8", diff --git a/packages/nvmedit/package.json b/packages/nvmedit/package.json index 78d3e80cdea5..fc125c259f7a 100644 --- a/packages/nvmedit/package.json +++ b/packages/nvmedit/package.json @@ -65,7 +65,7 @@ "test:dirty": "tsx ../maintenance/src/resolveDirtyTests.ts --run" }, "devDependencies": { - "@alcalzone/esm2cjs": "^1.3.0", + "@alcalzone/esm2cjs": "^1.4.0", "@microsoft/api-extractor": "^7.47.9", "@types/node": "^18.19.63", "@types/semver": "^7.5.8", diff --git a/packages/serial/package.json b/packages/serial/package.json index 214c70e283ed..1ae1d4976802 100644 --- a/packages/serial/package.json +++ b/packages/serial/package.json @@ -74,7 +74,7 @@ "winston": "^3.15.0" }, "devDependencies": { - "@alcalzone/esm2cjs": "^1.3.0", + "@alcalzone/esm2cjs": "^1.4.0", "@microsoft/api-extractor": "^7.47.9", "@serialport/binding-mock": "^10.2.2", "@serialport/bindings-interface": "patch:@serialport/bindings-interface@npm%3A1.2.2#~/.yarn/patches/@serialport-bindings-interface-npm-1.2.2-e597dbc676.patch", diff --git a/packages/shared/package.json b/packages/shared/package.json index 6417021d70f6..6fca15064ce1 100644 --- a/packages/shared/package.json +++ b/packages/shared/package.json @@ -59,7 +59,7 @@ "test:dirty": "tsx ../maintenance/src/resolveDirtyTests.ts --run" }, "devDependencies": { - "@alcalzone/esm2cjs": "^1.3.0", + "@alcalzone/esm2cjs": "^1.4.0", "@microsoft/api-extractor": "^7.47.9", "@types/node": "^18.19.63", "@types/sinon": "^17.0.3", diff --git a/packages/shared/src/Bytes.ts b/packages/shared/src/Bytes.ts index 716c06343ac4..52bf0a0537ef 100644 --- a/packages/shared/src/Bytes.ts +++ b/packages/shared/src/Bytes.ts @@ -389,7 +389,7 @@ export class Bytes extends Uint8Array { } } /** - * Writes `byteLength` bytes of `value` to `buf` at the specified `offset`as big-endian. Supports up to 48 bits of accuracy. Behavior is undefined + * Writes `byteLength` bytes of `value` to `buf` at the specified `offset`as big-endian. Supports up to 64 bits of accuracy. Behavior is undefined * when `value` is anything other than an unsigned integer. * * ```js @@ -405,7 +405,7 @@ export class Bytes extends Uint8Array { * @since v0.5.5 * @param value Number to be written to `buf`. * @param offset Number of bytes to skip before starting to write. Must satisfy `0 <= offset <= buf.length - byteLength`. - * @param byteLength Number of bytes to write. Must satisfy `0 < byteLength <= 6`. + * @param byteLength Number of bytes to write. Must satisfy `0 < byteLength <= 8`. * @return `offset` plus the number of bytes written. */ writeUIntBE(value: number, offset: number, byteLength: number): number { @@ -439,6 +439,20 @@ export class Bytes extends Uint8Array { ret = this.writeUInt16BE(low, ret); return ret; } + case 7: { + const big = BigInt(value); + const high = Number(big >> 24n); + const mid = Number((big >> 8n) & 0xffffn); + const low = Number(big & 0xffn); + let ret = this.writeUInt32BE(high, offset); + ret = this.writeUInt16BE(mid, ret); + ret = this.writeUInt8(low, ret); + return ret; + } + case 8: { + const ret = this.writeBigUInt64BE(BigInt(value), offset); + return ret; + } default: throw new RangeError( `The value of "byteLength" is out of range. It must be >= 1 and <= 6. Received ${byteLength}`, diff --git a/packages/testing/package.json b/packages/testing/package.json index f53e3f75e04a..ee88cf15ed07 100644 --- a/packages/testing/package.json +++ b/packages/testing/package.json @@ -55,7 +55,7 @@ "ansi-colors": "^4.1.3" }, "devDependencies": { - "@alcalzone/esm2cjs": "^1.3.0", + "@alcalzone/esm2cjs": "^1.4.0", "@microsoft/api-extractor": "^7.47.9", "@types/node": "^18.19.63", "@types/triple-beam": "^1.3.5", diff --git a/packages/zwave-js/package.json b/packages/zwave-js/package.json index e5c2b252b7e4..d1e0d28869f8 100644 --- a/packages/zwave-js/package.json +++ b/packages/zwave-js/package.json @@ -122,7 +122,7 @@ "xstate": "4.38.3" }, "devDependencies": { - "@alcalzone/esm2cjs": "^1.3.0", + "@alcalzone/esm2cjs": "^1.4.0", "@microsoft/api-extractor": "^7.47.9", "@types/node": "^18.19.63", "@types/proper-lockfile": "^4.1.4", diff --git a/packages/zwave-js/src/lib/controller/Controller.ts b/packages/zwave-js/src/lib/controller/Controller.ts index 8864da102129..0a5d14562121 100644 --- a/packages/zwave-js/src/lib/controller/Controller.ts +++ b/packages/zwave-js/src/lib/controller/Controller.ts @@ -82,15 +82,15 @@ import { ZWaveLibraryTypes, authHomeIdFromDSK, averageRSSI, - computePRK, - deriveTempKeys, + computePRKAsync, + deriveTempKeysAsync, dskFromString, dskToString, - extractRawECDHPublicKey, - generateECDHKeyPair, + extractRawECDHPublicKeySync, + generateECDHKeyPairSync, getChipTypeAndVersion, getHighestSecurityClass, - importRawECDHPublicKey, + importRawECDHPublicKeySync, indexDBsByNode, isEmptyRoute, isLongRangeNodeId, @@ -565,7 +565,7 @@ export class ZWaveController public get dsk(): Uint8Array { if (this._dsk == undefined) { const keyPair = this.driver.getLearnModeAuthenticatedKeyPair(); - const publicKey = extractRawECDHPublicKey(keyPair.publicKey); + const publicKey = extractRawECDHPublicKeySync(keyPair.publicKey); this._dsk = publicKey.subarray(0, 16); } return this._dsk; @@ -3572,8 +3572,8 @@ export class ZWaveController // Generate ECDH key pair. We need to immediately send the other node our public key, // so it won't abort bootstrapping - const keyPair = generateECDHKeyPair(); - const publicKey = extractRawECDHPublicKey(keyPair.publicKey); + const keyPair = generateECDHKeyPairSync(); + const publicKey = extractRawECDHPublicKeySync(keyPair.publicKey); await api.sendPublicKey(publicKey); // After this, the node will start sending us a KEX SET every 10 seconds. // We won't be able to decode it until the DSK was verified @@ -3628,13 +3628,13 @@ export class ZWaveController // After the user has verified the DSK, we can derive the shared secret // Z-Wave works with the "raw" keys, so this is a tad complicated const sharedSecret = crypto.diffieHellman({ - publicKey: importRawECDHPublicKey(nodePublicKey), + publicKey: importRawECDHPublicKeySync(nodePublicKey), privateKey: keyPair.privateKey, }); // Derive temporary key from ECDH key pair - this will allow us to receive the node's KEX SET commands - const tempKeys = deriveTempKeys( - computePRK(sharedSecret, publicKey, nodePublicKey), + const tempKeys = await deriveTempKeysAsync( + await computePRKAsync(sharedSecret, publicKey, nodePublicKey), ); securityManager.deleteNonce(node.id); securityManager.tempKeys.set(node.id, { @@ -8954,9 +8954,9 @@ export class ZWaveController * Is called when a RemoveNode request is received from the controller. * Handles and controls the exclusion process. */ - private handleLearnModeCallback( + private async handleLearnModeCallback( msg: SetLearnModeCallback, - ): boolean { + ): Promise { // not sure what to do with this message, we're not in learn mode if (this._currentLearnMode == undefined) return false; @@ -9003,8 +9003,10 @@ export class ZWaveController if (wasJoining) { this._currentLearnMode = undefined; this.driver["_securityManager"] = undefined; - this.driver["_securityManager2"] = new SecurityManager2(); - this.driver["_securityManagerLR"] = new SecurityManager2(); + this.driver["_securityManager2"] = await SecurityManager2 + .create(); + this.driver["_securityManagerLR"] = await SecurityManager2 + .create(); this._nodes.clear(); process.nextTick(() => this.afterJoiningNetwork().catch(noop)); @@ -9343,8 +9345,8 @@ export class ZWaveController // otherwise generate a new one const keyPair = requiresAuthentication ? this.driver.getLearnModeAuthenticatedKeyPair() - : generateECDHKeyPair(); - const publicKey = extractRawECDHPublicKey(keyPair.publicKey); + : generateECDHKeyPairSync(); + const publicKey = extractRawECDHPublicKeySync(keyPair.publicKey); const transmittedPublicKey = Bytes.from(publicKey); if (requiresAuthentication) { // Authentication requires obfuscating the public key @@ -9377,13 +9379,17 @@ export class ZWaveController const includingNodePubKey = pubKeyReport.publicKey; const sharedSecret = crypto.diffieHellman({ - publicKey: importRawECDHPublicKey(includingNodePubKey), + publicKey: importRawECDHPublicKeySync(includingNodePubKey), privateKey: keyPair.privateKey, }); // Derive temporary key from ECDH key pair - this will allow us to receive the node's KEX SET commands - const tempKeys = deriveTempKeys( - computePRK(sharedSecret, includingNodePubKey, publicKey), + const tempKeys = await deriveTempKeysAsync( + await computePRKAsync( + sharedSecret, + includingNodePubKey, + publicKey, + ), ); securityManager.deleteNonce(bootstrappingNode.id); securityManager.tempKeys.set(bootstrappingNode.id, { @@ -9551,7 +9557,10 @@ export class ZWaveController // Store the network key receivedKeys.set(securityClass, keyReport.networkKey); - securityManager.setKey(securityClass, keyReport.networkKey); + await securityManager.setKeyAsync( + securityClass, + keyReport.networkKey, + ); if (securityClass === SecurityClass.S0_Legacy) { // TODO: This is awkward to have here this.driver["_securityManager"] = new SecurityManager({ diff --git a/packages/zwave-js/src/lib/driver/Driver.ts b/packages/zwave-js/src/lib/driver/Driver.ts index b203c9e59964..e06bada6361e 100644 --- a/packages/zwave-js/src/lib/driver/Driver.ts +++ b/packages/zwave-js/src/lib/driver/Driver.ts @@ -87,8 +87,8 @@ import { ZWaveErrorCodes, ZWaveLogContainer, deserializeCacheValue, - extractRawECDHPrivateKey, - generateECDHKeyPair, + extractRawECDHPrivateKeySync, + generateECDHKeyPairSync, getCCName, highResTimestamp, isEncapsulationCC, @@ -97,7 +97,7 @@ import { isMissingControllerCallback, isMissingControllerResponse, isZWaveError, - keyPairFromRawECDHPrivateKey, + keyPairFromRawECDHPrivateKeySync, messageRecordToLines, securityClassIsS2, securityClassOrder, @@ -1005,13 +1005,13 @@ export class Driver extends TypedEventEmitter ); if (privateKey) { this._learnModeAuthenticatedKeyPair = - keyPairFromRawECDHPrivateKey(privateKey); + keyPairFromRawECDHPrivateKeySync(privateKey); } else { // Not found in cache, create a new one and cache it - this._learnModeAuthenticatedKeyPair = generateECDHKeyPair(); + this._learnModeAuthenticatedKeyPair = generateECDHKeyPairSync(); this.cacheSet( cacheKeys.controller.privateKey, - extractRawECDHPrivateKey( + extractRawECDHPrivateKeySync( this._learnModeAuthenticatedKeyPair.privateKey, ), ); @@ -1780,7 +1780,7 @@ export class Driver extends TypedEventEmitter this.driverLog.print( "At least one network key for S2 configured, enabling S2 security manager...", ); - this._securityManager2 = new SecurityManager2(); + this._securityManager2 = await SecurityManager2.create(); // Set up all keys for ( const secClass of [ @@ -1792,7 +1792,7 @@ export class Driver extends TypedEventEmitter ) { const key = this._options.securityKeys[secClass]; if (key) { - this._securityManager2.setKey( + await this._securityManager2.setKeyAsync( SecurityClass[secClass], key, ); @@ -1812,15 +1812,15 @@ export class Driver extends TypedEventEmitter this.driverLog.print( "At least one network key for Z-Wave Long Range configured, enabling security manager...", ); - this._securityManagerLR = new SecurityManager2(); + this._securityManagerLR = await SecurityManager2.create(); if (this._options.securityKeysLongRange?.S2_AccessControl) { - this._securityManagerLR.setKey( + await this._securityManagerLR.setKeyAsync( SecurityClass.S2_AccessControl, this._options.securityKeysLongRange.S2_AccessControl, ); } if (this._options.securityKeysLongRange?.S2_Authenticated) { - this._securityManagerLR.setKey( + await this._securityManagerLR.setKeyAsync( SecurityClass.S2_Authenticated, this._options.securityKeysLongRange.S2_Authenticated, ); @@ -1853,9 +1853,9 @@ export class Driver extends TypedEventEmitter this.driverLog.print( "At least one network key for Z-Wave Long Range found in cache, enabling security manager...", ); - this._securityManagerLR = new SecurityManager2(); + this._securityManagerLR = await SecurityManager2.create(); for (const [sc, key] of securityKeysLongRange) { - this._securityManagerLR.setKey(sc, key); + await this._securityManagerLR.setKeyAsync(sc, key); } } else if ( this._options.securityKeysLongRange?.S2_AccessControl @@ -1864,16 +1864,16 @@ export class Driver extends TypedEventEmitter this.driverLog.print( "Fallback to configured network keys for Z-Wave Long Range, enabling security manager...", ); - this._securityManagerLR = new SecurityManager2(); + this._securityManagerLR = await SecurityManager2.create(); if (this._options.securityKeysLongRange?.S2_AccessControl) { - this._securityManagerLR.setKey( + await this._securityManagerLR.setKeyAsync( SecurityClass.S2_AccessControl, this._options.securityKeysLongRange .S2_AccessControl, ); } if (this._options.securityKeysLongRange?.S2_Authenticated) { - this._securityManagerLR.setKey( + await this._securityManagerLR.setKeyAsync( SecurityClass.S2_Authenticated, this._options.securityKeysLongRange .S2_Authenticated, @@ -1927,9 +1927,9 @@ export class Driver extends TypedEventEmitter this.driverLog.print( "At least one network key for S2 found in cache, enabling S2 security manager...", ); - this._securityManager2 = new SecurityManager2(); + this._securityManager2 = await SecurityManager2.create(); for (const [sc, key] of securityKeys) { - this._securityManager2.setKey(sc, key); + await this._securityManager2.setKeyAsync(sc, key); } } else if ( this._options.securityKeys @@ -1943,7 +1943,7 @@ export class Driver extends TypedEventEmitter this.driverLog.print( "Fallback to configured network keys for S2, enabling S2 security manager...", ); - this._securityManager2 = new SecurityManager2(); + this._securityManager2 = await SecurityManager2.create(); // Set up all keys for ( const secClass of [ @@ -1955,7 +1955,7 @@ export class Driver extends TypedEventEmitter ) { const key = this._options.securityKeys[secClass]; if (key) { - this._securityManager2.setKey( + await this._securityManager2.setKeyAsync( SecurityClass[secClass], key, ); diff --git a/packages/zwave-js/src/lib/test/cc/WakeUpCC.test.ts b/packages/zwave-js/src/lib/test/cc/WakeUpCC.test.ts index 40b6a4e908bc..4bce91b7e137 100644 --- a/packages/zwave-js/src/lib/test/cc/WakeUpCC.test.ts +++ b/packages/zwave-js/src/lib/test/cc/WakeUpCC.test.ts @@ -3,7 +3,10 @@ import { SecurityCC, WakeUpCCNoMoreInformation, } from "@zwave-js/cc"; -import { generateAuthKey, generateEncryptionKey } from "@zwave-js/core"; +import { + generateAuthKeyAsync, + generateEncryptionKeyAsync, +} from "@zwave-js/core"; import { Bytes } from "@zwave-js/shared/safe"; import { randomBytes } from "node:crypto"; import { test } from "vitest"; @@ -34,8 +37,8 @@ test("SecurityCC/WakeUpCCNoMoreInformation should expect NO response", (t) => { const securityManager = { getNonce: () => nonce, - authKey: generateAuthKey(networkKey), - encryptionKey: generateEncryptionKey(networkKey), + getAuthKey: generateAuthKeyAsync(networkKey), + getEncryptionKey: generateEncryptionKeyAsync(networkKey), }; const ccRequest = SecurityCC.encapsulate( diff --git a/packages/zwave-js/src/lib/test/compliance/decodeLowerS2Keys.test.ts b/packages/zwave-js/src/lib/test/compliance/decodeLowerS2Keys.test.ts index f293733d3039..9a066bdb1f77 100644 --- a/packages/zwave-js/src/lib/test/compliance/decodeLowerS2Keys.test.ts +++ b/packages/zwave-js/src/lib/test/compliance/decodeLowerS2Keys.test.ts @@ -33,17 +33,17 @@ integrationTest( customSetup: async (driver, controller, mockNode) => { // Create a security manager for the node - const sm2Node = new SecurityManager2(); + const sm2Node = await SecurityManager2.create(); // Copy keys from the driver - sm2Node.setKey( + await sm2Node.setKeyAsync( SecurityClass.S2_AccessControl, driver.options.securityKeys!.S2_AccessControl!, ); - sm2Node.setKey( + await sm2Node.setKeyAsync( SecurityClass.S2_Authenticated, driver.options.securityKeys!.S2_Authenticated!, ); - sm2Node.setKey( + await sm2Node.setKeyAsync( SecurityClass.S2_Unauthenticated, driver.options.securityKeys!.S2_Unauthenticated!, ); @@ -53,17 +53,17 @@ integrationTest( SecurityClass.S2_Unauthenticated; // Create a security manager for the controller - const smCtrlr = new SecurityManager2(); + const smCtrlr = await SecurityManager2.create(); // Copy keys from the driver - smCtrlr.setKey( + await smCtrlr.setKeyAsync( SecurityClass.S2_AccessControl, driver.options.securityKeys!.S2_AccessControl!, ); - smCtrlr.setKey( + await smCtrlr.setKeyAsync( SecurityClass.S2_Authenticated, driver.options.securityKeys!.S2_Authenticated!, ); - smCtrlr.setKey( + await smCtrlr.setKeyAsync( SecurityClass.S2_Unauthenticated, driver.options.securityKeys!.S2_Unauthenticated!, ); @@ -73,9 +73,9 @@ integrationTest( // Respond to S2 Nonce Get const respondToS2NonceGet: MockNodeBehavior = { - handleCC(controller, self, receivedCC) { + async handleCC(controller, self, receivedCC) { if (receivedCC instanceof Security2CCNonceGet) { - const nonce = sm2Node.generateNonce( + const nonce = await sm2Node.generateNonceAsync( controller.ownNodeId, ); const cc = new Security2CCNonceReport({ diff --git a/packages/zwave-js/src/lib/test/compliance/discardInsecureCommands.test.ts b/packages/zwave-js/src/lib/test/compliance/discardInsecureCommands.test.ts index 5937dbb3ed06..a88ff9b66f11 100644 --- a/packages/zwave-js/src/lib/test/compliance/discardInsecureCommands.test.ts +++ b/packages/zwave-js/src/lib/test/compliance/discardInsecureCommands.test.ts @@ -30,17 +30,17 @@ integrationTest( customSetup: async (driver, controller, mockNode) => { // Create a security manager for the node - const smNode = new SecurityManager2(); + const smNode = await SecurityManager2.create(); // Copy keys from the driver - smNode.setKey( + await smNode.setKeyAsync( SecurityClass.S2_AccessControl, driver.options.securityKeys!.S2_AccessControl!, ); - smNode.setKey( + await smNode.setKeyAsync( SecurityClass.S2_Authenticated, driver.options.securityKeys!.S2_Authenticated!, ); - smNode.setKey( + await smNode.setKeyAsync( SecurityClass.S2_Unauthenticated, driver.options.securityKeys!.S2_Unauthenticated!, ); @@ -49,17 +49,17 @@ integrationTest( SecurityClass.S2_Unauthenticated; // Create a security manager for the controller - const smCtrlr = new SecurityManager2(); + const smCtrlr = await SecurityManager2.create(); // Copy keys from the driver - smCtrlr.setKey( + await smCtrlr.setKeyAsync( SecurityClass.S2_AccessControl, driver.options.securityKeys!.S2_AccessControl!, ); - smCtrlr.setKey( + await smCtrlr.setKeyAsync( SecurityClass.S2_Authenticated, driver.options.securityKeys!.S2_Authenticated!, ); - smCtrlr.setKey( + await smCtrlr.setKeyAsync( SecurityClass.S2_Unauthenticated, driver.options.securityKeys!.S2_Unauthenticated!, ); @@ -70,9 +70,9 @@ integrationTest( // Respond to Nonce Get const respondToNonceGet: MockNodeBehavior = { - handleCC(controller, self, receivedCC) { + async handleCC(controller, self, receivedCC) { if (receivedCC instanceof Security2CCNonceGet) { - const nonce = smNode.generateNonce( + const nonce = await smNode.generateNonceAsync( controller.ownNodeId, ); const cc = new Security2CCNonceReport({ @@ -89,7 +89,7 @@ integrationTest( // Handle decode errors const handleInvalidCC: MockNodeBehavior = { - handleCC(controller, self, receivedCC) { + async handleCC(controller, self, receivedCC) { if (receivedCC instanceof InvalidCC) { if ( receivedCC.reason @@ -97,7 +97,7 @@ integrationTest( || receivedCC.reason === ZWaveErrorCodes.Security2CC_NoSPAN ) { - const nonce = smNode.generateNonce( + const nonce = await smNode.generateNonceAsync( controller.ownNodeId, ); const cc = new Security2CCNonceReport({ diff --git a/packages/zwave-js/src/lib/test/compliance/secureNodeSecureEndpoint.test.ts b/packages/zwave-js/src/lib/test/compliance/secureNodeSecureEndpoint.test.ts index bf067d49fb08..b41b7ed8f338 100644 --- a/packages/zwave-js/src/lib/test/compliance/secureNodeSecureEndpoint.test.ts +++ b/packages/zwave-js/src/lib/test/compliance/secureNodeSecureEndpoint.test.ts @@ -81,17 +81,17 @@ integrationTest( customSetup: async (driver, controller, mockNode) => { // Create a security manager for the node - const smNode = new SecurityManager2(); + const smNode = await SecurityManager2.create(); // Copy keys from the driver - // smNode.setKey( + // await smNode.setKeyAsync( // SecurityClass.S2_AccessControl, // driver.options.securityKeys!.S2_AccessControl!, // ); - // smNode.setKey( + // await smNode.setKeyAsync( // SecurityClass.S2_Authenticated, // driver.options.securityKeys!.S2_Authenticated!, // ); - smNode.setKey( + await smNode.setKeyAsync( SecurityClass.S2_Unauthenticated, driver.options.securityKeys!.S2_Unauthenticated!, ); @@ -100,17 +100,17 @@ integrationTest( SecurityClass.S2_Unauthenticated; // Create a security manager for the controller - const smCtrlr = new SecurityManager2(); + const smCtrlr = await SecurityManager2.create(); // Copy keys from the driver - smCtrlr.setKey( + await smCtrlr.setKeyAsync( SecurityClass.S2_AccessControl, driver.options.securityKeys!.S2_AccessControl!, ); - smCtrlr.setKey( + await smCtrlr.setKeyAsync( SecurityClass.S2_Authenticated, driver.options.securityKeys!.S2_Authenticated!, ); - smCtrlr.setKey( + await smCtrlr.setKeyAsync( SecurityClass.S2_Unauthenticated, driver.options.securityKeys!.S2_Unauthenticated!, ); @@ -120,9 +120,9 @@ integrationTest( // Respond to Nonce Get const respondToNonceGet: MockNodeBehavior = { - handleCC(controller, self, receivedCC) { + async handleCC(controller, self, receivedCC) { if (receivedCC instanceof Security2CCNonceGet) { - const nonce = smNode.generateNonce( + const nonce = await smNode.generateNonceAsync( controller.ownNodeId, ); const cc = new Security2CCNonceReport({ @@ -139,7 +139,7 @@ integrationTest( // Handle decode errors const handleInvalidCC: MockNodeBehavior = { - handleCC(controller, self, receivedCC) { + async handleCC(controller, self, receivedCC) { if (receivedCC instanceof InvalidCC) { if ( receivedCC.reason @@ -147,7 +147,7 @@ integrationTest( || receivedCC.reason === ZWaveErrorCodes.Security2CC_NoSPAN ) { - const nonce = smNode.generateNonce( + const nonce = await smNode.generateNonceAsync( controller.ownNodeId, ); const cc = new Security2CCNonceReport({ diff --git a/packages/zwave-js/src/lib/test/driver/highestSecurityClass.test.ts b/packages/zwave-js/src/lib/test/driver/highestSecurityClass.test.ts index 924981260f3e..000f5aa786e5 100644 --- a/packages/zwave-js/src/lib/test/driver/highestSecurityClass.test.ts +++ b/packages/zwave-js/src/lib/test/driver/highestSecurityClass.test.ts @@ -79,17 +79,17 @@ integrationTest( driver.options.timeouts.report = 200; // Create a security manager for the node - const smNode = new SecurityManager2(); + const smNode = await SecurityManager2.create(); // Copy keys from the driver - smNode.setKey( + await smNode.setKeyAsync( SecurityClass.S2_AccessControl, driver.options.securityKeys!.S2_AccessControl!, ); - smNode.setKey( + await smNode.setKeyAsync( SecurityClass.S2_Authenticated, driver.options.securityKeys!.S2_Authenticated!, ); - smNode.setKey( + await smNode.setKeyAsync( SecurityClass.S2_Unauthenticated, driver.options.securityKeys!.S2_Unauthenticated!, ); @@ -98,24 +98,23 @@ integrationTest( SecurityClass.None; // Create a security manager for the controller - const smCtrlr = new SecurityManager2(); + const smCtrlr = await SecurityManager2.create(); // Copy keys from the driver - smCtrlr.setKey( + await smCtrlr.setKeyAsync( SecurityClass.S2_AccessControl, driver.options.securityKeys!.S2_AccessControl!, ); - smCtrlr.setKey( + await smCtrlr.setKeyAsync( SecurityClass.S2_Authenticated, driver.options.securityKeys!.S2_Authenticated!, ); - smCtrlr.setKey( + await smCtrlr.setKeyAsync( SecurityClass.S2_Unauthenticated, driver.options.securityKeys!.S2_Unauthenticated!, ); controller.securityManagers.securityManager2 = smCtrlr; - controller.encodingContext.getHighestSecurityClass = - controller.parsingContext.getHighestSecurityClass = - () => NOT_KNOWN; + controller.encodingContext.getHighestSecurityClass = () => + NOT_KNOWN; }, testBody: async (t, driver, node, mockController, mockNode) => { diff --git a/packages/zwave-js/src/lib/test/driver/ignoreCCVersion0ForKnownSupportedCCs.test.ts b/packages/zwave-js/src/lib/test/driver/ignoreCCVersion0ForKnownSupportedCCs.test.ts index 09e789647177..1de6135d29f3 100644 --- a/packages/zwave-js/src/lib/test/driver/ignoreCCVersion0ForKnownSupportedCCs.test.ts +++ b/packages/zwave-js/src/lib/test/driver/ignoreCCVersion0ForKnownSupportedCCs.test.ts @@ -48,9 +48,9 @@ integrationTest( customSetup: async (driver, controller, mockNode) => { // Create a security manager for the node - const smNode = new SecurityManager2(); + const smNode = await SecurityManager2.create(); // Copy keys from the driver - smNode.setKey( + await smNode.setKeyAsync( SecurityClass.S2_Unauthenticated, driver.options.securityKeys!.S2_Unauthenticated!, ); @@ -59,9 +59,9 @@ integrationTest( SecurityClass.S2_Unauthenticated; // Create a security manager for the controller - const smCtrlr = new SecurityManager2(); + const smCtrlr = await SecurityManager2.create(); // Copy keys from the driver - smCtrlr.setKey( + await smCtrlr.setKeyAsync( SecurityClass.S2_Unauthenticated, driver.options.securityKeys!.S2_Unauthenticated!, ); @@ -71,9 +71,9 @@ integrationTest( // Respond to Nonce Get const respondToNonceGet: MockNodeBehavior = { - handleCC(controller, self, receivedCC) { + async handleCC(controller, self, receivedCC) { if (receivedCC instanceof Security2CCNonceGet) { - const nonce = smNode.generateNonce( + const nonce = await smNode.generateNonceAsync( controller.ownNodeId, ); const cc = new Security2CCNonceReport({ @@ -90,7 +90,7 @@ integrationTest( // Handle decode errors const handleInvalidCC: MockNodeBehavior = { - handleCC(controller, self, receivedCC) { + async handleCC(controller, self, receivedCC) { if (receivedCC instanceof InvalidCC) { if ( receivedCC.reason @@ -98,7 +98,7 @@ integrationTest( || receivedCC.reason === ZWaveErrorCodes.Security2CC_NoSPAN ) { - const nonce = smNode.generateNonce( + const nonce = await smNode.generateNonceAsync( controller.ownNodeId, ); const cc = new Security2CCNonceReport({ diff --git a/packages/zwave-js/src/lib/test/driver/s0AndS2Encapsulation.test.ts b/packages/zwave-js/src/lib/test/driver/s0AndS2Encapsulation.test.ts index fa25b4aec2c9..76ae1776b5ea 100644 --- a/packages/zwave-js/src/lib/test/driver/s0AndS2Encapsulation.test.ts +++ b/packages/zwave-js/src/lib/test/driver/s0AndS2Encapsulation.test.ts @@ -35,17 +35,17 @@ integrationTest("S0 commands are S0-encapsulated, even when S2 is supported", { customSetup: async (driver, controller, mockNode) => { // Create a security manager for the node - const sm2Node = new SecurityManager2(); + const sm2Node = await SecurityManager2.create(); // Copy keys from the driver - sm2Node.setKey( + await sm2Node.setKeyAsync( SecurityClass.S2_AccessControl, driver.options.securityKeys!.S2_AccessControl!, ); - sm2Node.setKey( + await sm2Node.setKeyAsync( SecurityClass.S2_Authenticated, driver.options.securityKeys!.S2_Authenticated!, ); - sm2Node.setKey( + await sm2Node.setKeyAsync( SecurityClass.S2_Unauthenticated, driver.options.securityKeys!.S2_Unauthenticated!, ); @@ -61,17 +61,17 @@ integrationTest("S0 commands are S0-encapsulated, even when S2 is supported", { mockNode.securityManagers.securityManager = sm0Node; // Create a security manager for the controller - const smCtrlr = new SecurityManager2(); + const smCtrlr = await SecurityManager2.create(); // Copy keys from the driver - smCtrlr.setKey( + await smCtrlr.setKeyAsync( SecurityClass.S2_AccessControl, driver.options.securityKeys!.S2_AccessControl!, ); - smCtrlr.setKey( + await smCtrlr.setKeyAsync( SecurityClass.S2_Authenticated, driver.options.securityKeys!.S2_Authenticated!, ); - smCtrlr.setKey( + await smCtrlr.setKeyAsync( SecurityClass.S2_Unauthenticated, driver.options.securityKeys!.S2_Unauthenticated!, ); @@ -125,9 +125,9 @@ integrationTest("S0 commands are S0-encapsulated, even when S2 is supported", { // Respond to S2 Nonce Get const respondToS2NonceGet: MockNodeBehavior = { - handleCC(controller, self, receivedCC) { + async handleCC(controller, self, receivedCC) { if (receivedCC instanceof Security2CCNonceGet) { - const nonce = sm2Node.generateNonce( + const nonce = await sm2Node.generateNonceAsync( controller.ownNodeId, ); const cc = new Security2CCNonceReport({ @@ -144,7 +144,7 @@ integrationTest("S0 commands are S0-encapsulated, even when S2 is supported", { // Handle decode errors const handleInvalidCC: MockNodeBehavior = { - handleCC(controller, self, receivedCC) { + async handleCC(controller, self, receivedCC) { if (receivedCC instanceof InvalidCC) { if ( receivedCC.reason @@ -152,7 +152,7 @@ integrationTest("S0 commands are S0-encapsulated, even when S2 is supported", { || receivedCC.reason === ZWaveErrorCodes.Security2CC_NoSPAN ) { - const nonce = sm2Node.generateNonce( + const nonce = await sm2Node.generateNonceAsync( controller.ownNodeId, ); const cc = new Security2CCNonceReport({ diff --git a/packages/zwave-js/src/lib/test/driver/s2Collisions.test.ts b/packages/zwave-js/src/lib/test/driver/s2Collisions.test.ts index d1bd15a6e44a..fdc5f48603b9 100644 --- a/packages/zwave-js/src/lib/test/driver/s2Collisions.test.ts +++ b/packages/zwave-js/src/lib/test/driver/s2Collisions.test.ts @@ -42,17 +42,17 @@ integrationTest( customSetup: async (driver, controller, mockNode) => { // Create a security manager for the node - const smNode = new SecurityManager2(); + const smNode = await SecurityManager2.create(); // Copy keys from the driver - smNode.setKey( + await smNode.setKeyAsync( SecurityClass.S2_AccessControl, driver.options.securityKeys!.S2_AccessControl!, ); - smNode.setKey( + await smNode.setKeyAsync( SecurityClass.S2_Authenticated, driver.options.securityKeys!.S2_Authenticated!, ); - smNode.setKey( + await smNode.setKeyAsync( SecurityClass.S2_Unauthenticated, driver.options.securityKeys!.S2_Unauthenticated!, ); @@ -61,17 +61,17 @@ integrationTest( SecurityClass.S2_Unauthenticated; // Create a security manager for the controller - const smCtrlr = new SecurityManager2(); + const smCtrlr = await SecurityManager2.create(); // Copy keys from the driver - smCtrlr.setKey( + await smCtrlr.setKeyAsync( SecurityClass.S2_AccessControl, driver.options.securityKeys!.S2_AccessControl!, ); - smCtrlr.setKey( + await smCtrlr.setKeyAsync( SecurityClass.S2_Authenticated, driver.options.securityKeys!.S2_Authenticated!, ); - smCtrlr.setKey( + await smCtrlr.setKeyAsync( SecurityClass.S2_Unauthenticated, driver.options.securityKeys!.S2_Unauthenticated!, ); @@ -81,9 +81,9 @@ integrationTest( // Respond to Nonce Get const respondToNonceGet: MockNodeBehavior = { - handleCC(controller, self, receivedCC) { + async handleCC(controller, self, receivedCC) { if (receivedCC instanceof Security2CCNonceGet) { - const nonce = smNode.generateNonce( + const nonce = await smNode.generateNonceAsync( controller.ownNodeId, ); const cc = new Security2CCNonceReport({ @@ -100,7 +100,7 @@ integrationTest( // Handle decode errors const handleInvalidCC: MockNodeBehavior = { - handleCC(controller, self, receivedCC) { + async handleCC(controller, self, receivedCC) { if (receivedCC instanceof InvalidCC) { if ( receivedCC.reason @@ -108,7 +108,7 @@ integrationTest( || receivedCC.reason === ZWaveErrorCodes.Security2CC_NoSPAN ) { - const nonce = smNode.generateNonce( + const nonce = await smNode.generateNonceAsync( controller.ownNodeId, ); const cc = new Security2CCNonceReport({ @@ -209,17 +209,17 @@ integrationTest( customSetup: async (driver, controller, mockNode) => { // Create a security manager for the node - const smNode = new SecurityManager2(); + const smNode = await SecurityManager2.create(); // Copy keys from the driver - smNode.setKey( + await smNode.setKeyAsync( SecurityClass.S2_AccessControl, driver.options.securityKeys!.S2_AccessControl!, ); - smNode.setKey( + await smNode.setKeyAsync( SecurityClass.S2_Authenticated, driver.options.securityKeys!.S2_Authenticated!, ); - smNode.setKey( + await smNode.setKeyAsync( SecurityClass.S2_Unauthenticated, driver.options.securityKeys!.S2_Unauthenticated!, ); @@ -228,17 +228,17 @@ integrationTest( SecurityClass.S2_Unauthenticated; // Create a security manager for the controller - const smCtrlr = new SecurityManager2(); + const smCtrlr = await SecurityManager2.create(); // Copy keys from the driver - smCtrlr.setKey( + await smCtrlr.setKeyAsync( SecurityClass.S2_AccessControl, driver.options.securityKeys!.S2_AccessControl!, ); - smCtrlr.setKey( + await smCtrlr.setKeyAsync( SecurityClass.S2_Authenticated, driver.options.securityKeys!.S2_Authenticated!, ); - smCtrlr.setKey( + await smCtrlr.setKeyAsync( SecurityClass.S2_Unauthenticated, driver.options.securityKeys!.S2_Unauthenticated!, ); @@ -248,9 +248,9 @@ integrationTest( // Respond to Nonce Get const respondToNonceGet: MockNodeBehavior = { - handleCC(controller, self, receivedCC) { + async handleCC(controller, self, receivedCC) { if (receivedCC instanceof Security2CCNonceGet) { - const nonce = smNode.generateNonce( + const nonce = await smNode.generateNonceAsync( controller.ownNodeId, ); const cc = new Security2CCNonceReport({ @@ -267,7 +267,7 @@ integrationTest( // Handle decode errors const handleInvalidCC: MockNodeBehavior = { - handleCC(controller, self, receivedCC) { + async handleCC(controller, self, receivedCC) { if (receivedCC instanceof InvalidCC) { if ( receivedCC.reason @@ -275,7 +275,7 @@ integrationTest( || receivedCC.reason === ZWaveErrorCodes.Security2CC_NoSPAN ) { - const nonce = smNode.generateNonce( + const nonce = await smNode.generateNonceAsync( controller.ownNodeId, ); const cc = new Security2CCNonceReport({ @@ -344,17 +344,17 @@ integrationTest( customSetup: async (driver, controller, mockNode) => { // Create a security manager for the node - const smNode = new SecurityManager2(); + const smNode = await SecurityManager2.create(); // Copy keys from the driver - smNode.setKey( + await smNode.setKeyAsync( SecurityClass.S2_AccessControl, driver.options.securityKeys!.S2_AccessControl!, ); - smNode.setKey( + await smNode.setKeyAsync( SecurityClass.S2_Authenticated, driver.options.securityKeys!.S2_Authenticated!, ); - smNode.setKey( + await smNode.setKeyAsync( SecurityClass.S2_Unauthenticated, driver.options.securityKeys!.S2_Unauthenticated!, ); @@ -363,17 +363,17 @@ integrationTest( SecurityClass.S2_Unauthenticated; // Create a security manager for the controller - const smCtrlr = new SecurityManager2(); + const smCtrlr = await SecurityManager2.create(); // Copy keys from the driver - smCtrlr.setKey( + await smCtrlr.setKeyAsync( SecurityClass.S2_AccessControl, driver.options.securityKeys!.S2_AccessControl!, ); - smCtrlr.setKey( + await smCtrlr.setKeyAsync( SecurityClass.S2_Authenticated, driver.options.securityKeys!.S2_Authenticated!, ); - smCtrlr.setKey( + await smCtrlr.setKeyAsync( SecurityClass.S2_Unauthenticated, driver.options.securityKeys!.S2_Unauthenticated!, ); @@ -382,9 +382,9 @@ integrationTest( // Respond to Nonce Get const respondToNonceGet: MockNodeBehavior = { - handleCC(controller, self, receivedCC) { + async handleCC(controller, self, receivedCC) { if (receivedCC instanceof Security2CCNonceGet) { - const nonce = smNode.generateNonce( + const nonce = await smNode.generateNonceAsync( controller.ownNodeId, ); const cc = new Security2CCNonceReport({ @@ -401,7 +401,7 @@ integrationTest( // Handle decode errors const handleInvalidCC: MockNodeBehavior = { - handleCC(controller, self, receivedCC) { + async handleCC(controller, self, receivedCC) { if (receivedCC instanceof InvalidCC) { if ( receivedCC.reason @@ -409,7 +409,7 @@ integrationTest( || receivedCC.reason === ZWaveErrorCodes.Security2CC_NoSPAN ) { - const nonce = smNode.generateNonce( + const nonce = await smNode.generateNonceAsync( controller.ownNodeId, ); const cc = new Security2CCNonceReport({ diff --git a/packages/zwave-js/src/lib/zniffer/Zniffer.ts b/packages/zwave-js/src/lib/zniffer/Zniffer.ts index ba33b99e99e4..4044b013aaa7 100644 --- a/packages/zwave-js/src/lib/zniffer/Zniffer.ts +++ b/packages/zwave-js/src/lib/zniffer/Zniffer.ts @@ -565,7 +565,7 @@ supported frequencies: ${ securityManager: destSecurityManager, securityManager2: destSecurityManager2, securityManagerLR: destSecurityManagerLR, - } = this.getSecurityManagers(mpdu.destinationNodeId)); + } = await this.getSecurityManagers(mpdu.destinationNodeId)); } // TODO: Support parsing multicast S2 frames @@ -606,7 +606,7 @@ supported frequencies: ${ // Update the security managers when nonces are exchanged, so we can // decrypt the communication if (cc?.ccId === CommandClasses["Security 2"]) { - const securityManagers = this.getSecurityManagers( + const securityManagers = await this.getSecurityManagers( mpdu.sourceNodeId, ); const isLR = isLongRangeNodeId(mpdu.sourceNodeId) @@ -656,9 +656,11 @@ supported frequencies: ${ && cc instanceof SecurityCCNonceReport ) { const senderSecurityManager = - this.getSecurityManagers(mpdu.sourceNodeId).securityManager; + (await this.getSecurityManagers(mpdu.sourceNodeId)) + .securityManager; const destSecurityManager = - this.getSecurityManagers(destNodeId).securityManager; + (await this.getSecurityManagers(destNodeId)) + .securityManager; if (senderSecurityManager && destSecurityManager) { // Both nodes have a shared nonce now @@ -828,7 +830,7 @@ supported frequencies: ${ ); } - private getSecurityManagers( + private async getSecurityManagers( sourceNodeId: number, ) { if (this.securityManagers.has(sourceNodeId)) { @@ -870,7 +872,7 @@ supported frequencies: ${ // this.znifferLog.print( // "At least one network key for S2 configured, enabling S2 security manager...", // ); - securityManager2 = new SecurityManager2(); + securityManager2 = await SecurityManager2.create(); // Small hack: Zniffer does not care about S2 duplicates securityManager2.isDuplicateSinglecast = () => false; @@ -885,7 +887,10 @@ supported frequencies: ${ ) { const key = this._options.securityKeys[secClass]; if (key) { - securityManager2.setKey(SecurityClass[secClass], key); + await securityManager2.setKeyAsync( + SecurityClass[secClass], + key, + ); } } // } else { @@ -903,19 +908,19 @@ supported frequencies: ${ // this.znifferLog.print( // "At least one network key for Z-Wave Long Range configured, enabling security manager...", // ); - securityManagerLR = new SecurityManager2(); + securityManagerLR = await SecurityManager2.create(); // Small hack: Zniffer does not care about S2 duplicates securityManagerLR.isDuplicateSinglecast = () => false; // Set up all keys if (this._options.securityKeysLongRange?.S2_AccessControl) { - securityManagerLR.setKey( + await securityManagerLR.setKeyAsync( SecurityClass.S2_AccessControl, this._options.securityKeysLongRange.S2_AccessControl, ); } if (this._options.securityKeysLongRange?.S2_Authenticated) { - securityManagerLR.setKey( + await securityManagerLR.setKeyAsync( SecurityClass.S2_Authenticated, this._options.securityKeysLongRange.S2_Authenticated, ); diff --git a/yarn.lock b/yarn.lock index a89e48cfc663..476ba031e44f 100644 --- a/yarn.lock +++ b/yarn.lock @@ -69,9 +69,9 @@ __metadata: languageName: node linkType: hard -"@alcalzone/esm2cjs@npm:^1.3.0": - version: 1.3.0 - resolution: "@alcalzone/esm2cjs@npm:1.3.0" +"@alcalzone/esm2cjs@npm:^1.4.0": + version: 1.4.0 + resolution: "@alcalzone/esm2cjs@npm:1.4.0" dependencies: esbuild: "npm:^0.24.0" fs-extra: "npm:^10.1.0" @@ -79,7 +79,7 @@ __metadata: yargs: "npm:^17.5.1" bin: esm2cjs: bin/esm2cjs.cjs - checksum: 10/71d83742d6ce4c32ce15ed0f11a64118ffd88a07fbb54849208de834ed32b79bd05adec46db773938cbca665863064eadde160dff99dd13c58c865a5333bfb4b + checksum: 10/9ab7c4e383e8762319de1f79bd2dd8134f47b5227a935387764a7b7a159de8bab36806274ca875368e821f95285fb61fa4bc2b09df6af3302e5ec524f7e06463 languageName: node linkType: hard @@ -2558,7 +2558,7 @@ __metadata: version: 0.0.0-use.local resolution: "@zwave-js/cc@workspace:packages/cc" dependencies: - "@alcalzone/esm2cjs": "npm:^1.3.0" + "@alcalzone/esm2cjs": "npm:^1.4.0" "@microsoft/api-extractor": "npm:^7.47.9" "@types/node": "npm:^18.19.63" "@zwave-js/core": "workspace:*" @@ -2581,7 +2581,7 @@ __metadata: version: 0.0.0-use.local resolution: "@zwave-js/config@workspace:packages/config" dependencies: - "@alcalzone/esm2cjs": "npm:^1.3.0" + "@alcalzone/esm2cjs": "npm:^1.4.0" "@microsoft/api-extractor": "npm:^7.47.9" "@types/js-levenshtein": "npm:^1.1.3" "@types/json-logic-js": "npm:^2.0.7" @@ -2621,7 +2621,7 @@ __metadata: version: 0.0.0-use.local resolution: "@zwave-js/core@workspace:packages/core" dependencies: - "@alcalzone/esm2cjs": "npm:^1.3.0" + "@alcalzone/esm2cjs": "npm:^1.4.0" "@alcalzone/jsonl-db": "npm:^3.1.1" "@microsoft/api-extractor": "npm:^7.47.9" "@types/node": "npm:^18.19.63" @@ -2653,7 +2653,7 @@ __metadata: version: 0.0.0-use.local resolution: "@zwave-js/eslint-plugin@workspace:packages/eslint-plugin" dependencies: - "@alcalzone/esm2cjs": "npm:^1.3.0" + "@alcalzone/esm2cjs": "npm:^1.4.0" "@types/eslint": "npm:^9.6.1" "@typescript-eslint/utils": "npm:^8.8.1" "@zwave-js/core": "workspace:*" @@ -2698,7 +2698,7 @@ __metadata: version: 0.0.0-use.local resolution: "@zwave-js/host@workspace:packages/host" dependencies: - "@alcalzone/esm2cjs": "npm:^1.3.0" + "@alcalzone/esm2cjs": "npm:^1.4.0" "@microsoft/api-extractor": "npm:^7.47.9" "@types/node": "npm:^18.19.63" "@zwave-js/config": "workspace:*" @@ -2714,7 +2714,7 @@ __metadata: version: 0.0.0-use.local resolution: "@zwave-js/maintenance@workspace:packages/maintenance" dependencies: - "@alcalzone/esm2cjs": "npm:^1.3.0" + "@alcalzone/esm2cjs": "npm:^1.4.0" "@dprint/formatter": "npm:^0.4.1" "@dprint/json": "npm:^0.19.4" "@dprint/markdown": "npm:^0.17.8" @@ -2746,7 +2746,7 @@ __metadata: version: 0.0.0-use.local resolution: "@zwave-js/nvmedit@workspace:packages/nvmedit" dependencies: - "@alcalzone/esm2cjs": "npm:^1.3.0" + "@alcalzone/esm2cjs": "npm:^1.4.0" "@microsoft/api-extractor": "npm:^7.47.9" "@types/node": "npm:^18.19.63" "@types/semver": "npm:^7.5.8" @@ -2773,7 +2773,7 @@ __metadata: "@actions/core": "npm:^1.11.1" "@actions/exec": "npm:^1.1.1" "@actions/github": "npm:^6.0.0" - "@alcalzone/esm2cjs": "npm:^1.3.0" + "@alcalzone/esm2cjs": "npm:^1.4.0" "@alcalzone/jsonl-db": "npm:^3.1.1" "@alcalzone/monopack": "npm:^1.3.0" "@alcalzone/release-script": "npm:~3.8.0" @@ -2837,7 +2837,7 @@ __metadata: version: 0.0.0-use.local resolution: "@zwave-js/serial@workspace:packages/serial" dependencies: - "@alcalzone/esm2cjs": "npm:^1.3.0" + "@alcalzone/esm2cjs": "npm:^1.4.0" "@microsoft/api-extractor": "npm:^7.47.9" "@serialport/binding-mock": "npm:^10.2.2" "@serialport/bindings-interface": "patch:@serialport/bindings-interface@npm%3A1.2.2#~/.yarn/patches/@serialport-bindings-interface-npm-1.2.2-e597dbc676.patch" @@ -2864,7 +2864,7 @@ __metadata: version: 0.0.0-use.local resolution: "@zwave-js/shared@workspace:packages/shared" dependencies: - "@alcalzone/esm2cjs": "npm:^1.3.0" + "@alcalzone/esm2cjs": "npm:^1.4.0" "@microsoft/api-extractor": "npm:^7.47.9" "@types/node": "npm:^18.19.63" "@types/sinon": "npm:^17.0.3" @@ -2881,7 +2881,7 @@ __metadata: version: 0.0.0-use.local resolution: "@zwave-js/testing@workspace:packages/testing" dependencies: - "@alcalzone/esm2cjs": "npm:^1.3.0" + "@alcalzone/esm2cjs": "npm:^1.4.0" "@microsoft/api-extractor": "npm:^7.47.9" "@types/node": "npm:^18.19.63" "@types/triple-beam": "npm:^1.3.5" @@ -10067,7 +10067,7 @@ __metadata: version: 0.0.0-use.local resolution: "zwave-js@workspace:packages/zwave-js" dependencies: - "@alcalzone/esm2cjs": "npm:^1.3.0" + "@alcalzone/esm2cjs": "npm:^1.4.0" "@alcalzone/jsonl-db": "npm:^3.1.1" "@homebridge/ciao": "npm:^1.3.1" "@microsoft/api-extractor": "npm:^7.47.9"