import { CommandClasses , decryptAES128CCM , encodeBitMask , encryptAES128CCM , getCCName , isTransmissionError , isZWaveError , Maybe , MessageOrCCLogEntry , MessageRecord , parseBitMask , parseCCList , S2SecurityClass , SecurityClass , securityClassIsS2 , securityClassOrder , SecurityManager2 , SECURITY_S2_AUTH_TAG_LENGTH , SPANState , validatePayload , ZWaveError , ZWaveErrorCodes , } from "@zwave-js/core" ; import { buffer2hex , getEnumMemberName , pick } from "@zwave-js/shared" ; import type { ZWaveController } from "../controller/Controller" ; import { SendDataBridgeRequest } from "../controller/SendDataBridgeMessages" ; import { SendDataRequest } from "../controller/SendDataMessages" ; import { TransmitOptions } from "../controller/SendDataShared" ; import type { Driver } from "../driver/Driver" ; import { FunctionType , MessagePriority } from "../message/Constants" ; import { CCAPI } from "./API" ; import { API , CCCommand , CCCommandOptions , CommandClass , commandClass , CommandClassDeserializationOptions , CommandClassOptions , expectedCCResponse , gotDeserializationOptions , implementedVersion , } from "./CommandClass" ; import { MultiChannelCC } from "./MultiChannelCC" ; import { MGRPExtension , Security2Extension , SPANExtension , } from "./Security2/Extension" ; import { ECDHProfiles , KEXFailType , KEXSchemes } from "./Security2/shared" ; export enum Security2Command { NonceGet = 0x01 , NonceReport = 0x02 , MessageEncapsulation = 0x03 , KEXGet = 0x04 , KEXReport = 0x05 , KEXSet = 0x06 , KEXFail = 0x07 , PublicKeyReport = 0x08 , NetworkKeyGet = 0x09 , NetworkKeyReport = 0x0a , NetworkKeyVerify = 0x0b , TransferEnd = 0x0c , CommandsSupportedGet = 0x0d , CommandsSupportedReport = 0x0e } function securityClassToBitMask ( key ) { return encodeBitMask ( [ key ] , SecurityClass . S0_Legacy , SecurityClass . S2_Unauthenticated ) ; } function bitMaskToSecurityClass ( buffer , offset ) { const keys = parseBitMask ( buffer . slice ( offset , offset + 1 ) , SecurityClass . S2_Unauthenticated ) ; validatePayload ( keys . length === 1 ) ; return keys [ 0 ] ; } function getAuthenticationData ( sendingNodeId , destination , homeId , commandLength , unencryptedPayload ) { const ret = Buffer . allocUnsafe ( 8 + unencryptedPayload . length ) ; ret [ 0 ] = sendingNodeId ; ret [ 1 ] = destination ; ret . writeUInt32BE ( homeId , 2 ) ; ret . writeUInt16BE ( commandLength , 6 ) ; unencryptedPayload . copy ( ret , 8 , 0 ) ; return ret ; } /** Validates that a sequence number is not a duplicate and updates the SPAN table if it is accepted */ function validateSequenceNumber ( this , sequenceNumber ) { const peerNodeID = this . nodeId as number ; validatePayload . withReason ( "Duplicate command" ) ( ! this . driver . securityManager2 ! . isDuplicateSinglecast ( peerNodeID , sequenceNumber ) ) ; this . driver . securityManager2 ! . storeSequenceNumber ( peerNodeID , sequenceNumber ) ; } function assertSecurity ( this , options ) { const verb = gotDeserializationOptions ( options ) ? "decoded" : "sent" ; if ( ! this . driver . controller . ownNodeId ) { throw new ZWaveError ( `Secure commands (S2) can only be ${ verb } when the controller's node id is known!` , ZWaveErrorCodes . Driver_NotReady ) ; } else if ( ! this . driver . securityManager2 ) { throw new ZWaveError ( `Secure commands (S2) can only be ${ verb } when the network keys for the driver are set!` , ZWaveErrorCodes . Driver_NoSecurity ) ; } } const DECRYPT_ATTEMPTS = 5 ; @ API ( CommandClasses [ "Security 2" ] ) export class Security2CCAPI extends CCAPI { public supportsCommand ( _cmd ) { return true ; } /**
     * Sends a nonce to the node, either in response to a NonceGet request or a message that failed to decrypt. The message is sent without any retransmission etc.
     * The return value indicates whether a nonce was successfully sent
     */ public async sendNonce ( ) { this . assertSupportsCommand ( Security2Command , Security2Command . NonceReport ) ; this . assertPhysicalEndpoint ( this . endpoint ) ; if ( ! this . driver . securityManager2 ) { throw new ZWaveError ( `Nonces can only be sent if secure communication is set up!` , ZWaveErrorCodes . Driver_NoSecurity ) ; } const receiverEI = this . driver . securityManager2 . generateNonce ( this . endpoint . nodeId ) ; const cc = new Security2CCNonceReport ( this . driver , { nodeId : this . endpoint . nodeId , endpoint : this . endpoint . index , SOS : true , MOS : false , receiverEI } ) ; const SendDataConstructor = this . driver . controller . isFunctionSupported ( FunctionType . SendDataBridge ) ? SendDataBridgeRequest : SendDataRequest ; const msg = new SendDataConstructor ( this . driver , { command : cc , transmitOptions : TransmitOptions . ACK | TransmitOptions . AutoRoute , maxSendAttempts : 1 } ) ; try { await this . driver . sendMessage ( msg , { ... this . commandOptions , priority : MessagePriority . Nonce , changeNodeStatusOnMissingACK : false } ) ; } catch ( e ) { if ( isTransmissionError ( e ) ) { this . driver . securityManager2 . deleteNonce ( this . endpoint . nodeId ) ; return false ; } else { throw e ; } } return true ; } param securityClass public async getSupportedCommands ( securityClass ) { this . assertSupportsCommand ( Security2Command , Security2Command . CommandsSupportedGet ) ; let cc = new Security2CCCommandsSupportedGet ( this . driver , { nodeId : this . endpoint . nodeId , endpoint : this . endpoint . index } ) ; if ( MultiChannelCC . requiresEncapsulation ( cc ) ) { cc = MultiChannelCC . encapsulate ( this . driver , cc ) ; } cc = Security2CC . encapsulate ( this . driver , cc , securityClass ) ; const response = await this . driver . sendCommand < Security2CCCommandsSupportedReport > ( cc , { ... this . commandOptions , autoEncapsulate : false } ) ; return response ?. supportedCCs ; } public async getKeyExchangeParameters ( ) { this . assertSupportsCommand ( Security2Command , Security2Command . KEXGet ) ; const cc = new Security2CCKEXGet ( this . driver , { nodeId : this . endpoint . nodeId , endpoint : this . endpoint . index } ) ; const response = await this . driver . sendCommand < Security2CCKEXReport > ( cc , this . commandOptions ) ; if ( response ) { return pick ( response , [ "requestCSA" , "echo" , "supportedKEXSchemes" , "supportedECDHProfiles" , "requestedKeys" , ] ) ; } } /** Grants the joining node the given keys */ public async grantKeys ( params ) { this . assertSupportsCommand ( Security2Command , Security2Command . KEXSet ) ; const cc = new Security2CCKEXSet ( this . driver , { nodeId : this . endpoint . nodeId , endpoint : this . endpoint . index , ... params , echo : false } ) ; await this . driver . sendCommand ( cc , this . commandOptions ) ; } /** Confirms the keys that were granted to a node */ public async confirmGrantedKeys ( params ) { this . assertSupportsCommand ( Security2Command , Security2Command . KEXReport ) ; const cc = new Security2CCKEXReport ( this . driver , { nodeId : this . endpoint . nodeId , endpoint : this . endpoint . index , ... params , echo : true } ) ; await this . driver . sendCommand ( cc , this . commandOptions ) ; } /** Notifies the other node that the ongoing key exchange was aborted */ public async abortKeyExchange ( failType ) { this . assertSupportsCommand ( Security2Command , Security2Command . KEXFail ) ; const cc = new Security2CCKEXFail ( this . driver , { nodeId : this . endpoint . nodeId , endpoint : this . endpoint . index , failType } ) ; await this . driver . sendCommand ( cc , this . commandOptions ) ; } public async sendPublicKey ( publicKey ) { this . assertSupportsCommand ( Security2Command , Security2Command . PublicKeyReport ) ; const cc = new Security2CCPublicKeyReport ( this . driver , { nodeId : this . endpoint . nodeId , endpoint : this . endpoint . index , includingNode : true , publicKey } ) ; await this . driver . sendCommand ( cc , this . commandOptions ) ; } public async sendNetworkKey ( securityClass , networkKey ) { this . assertSupportsCommand ( Security2Command , Security2Command . NetworkKeyReport ) ; const cc = new Security2CCNetworkKeyReport ( this . driver , { nodeId : this . endpoint . nodeId , endpoint : this . endpoint . index , grantedKey : securityClass , networkKey } ) ; await this . driver . sendCommand ( cc , this . commandOptions ) ; } public async confirmKeyVerification ( ) { this . assertSupportsCommand ( Security2Command , Security2Command . TransferEnd ) ; const cc = new Security2CCTransferEnd ( this . driver , { nodeId : this . endpoint . nodeId , endpoint : this . endpoint . index , keyVerified : true , keyRequestComplete : false } ) ; await this . driver . sendCommand ( cc , { ... this . commandOptions , transmitOptions : TransmitOptions . DEFAULT & ~ TransmitOptions . ACK } ) ; } } @ commandClass ( CommandClasses [ "Security 2" ] ) @ implementedVersion ( 1 ) export class Security2CC extends CommandClass { declare ccCommand : Security2Command ; public async interview ( ) { const node = this . getNode ( ) ! ; const endpoint = this . getEndpoint ( ) ! ; const api = endpoint . commandClasses [ "Security 2" ] . withOptions ( { priority : MessagePriority . NodeQuery } ) ; const secClass = node . getHighestSecurityClass ( ) ; let hasReceivedSecureCommands = false ; let possibleSecurityClasses ; if ( securityClassIsS2 ( secClass ) ) { possibleSecurityClasses = [ secClass ] ; } else if ( endpoint . index === 0 ) { possibleSecurityClasses = [ SecurityClass . S2_Unauthenticated , SecurityClass . S2_Authenticated , SecurityClass . S2_AccessControl , ] ; } else { this . driver . controllerLog . logNode ( node . id , { endpoint : endpoint . index , message : `Cannot query securely supported commands for endpoint because the node's security class isn't known...` , level : "error" } ) ; return ; } for ( const secClass of possibleSecurityClasses ) { if ( node . hasSecurityClass ( secClass ) === false ) continue ; if ( ! this . driver . securityManager2 ?. hasKeysForSecurityClass ( secClass ) ) { this . driver . controllerLog . logNode ( node . id , { endpoint : endpoint . index , message : `Cannot query securely supported commands (${ getEnumMemberName ( SecurityClass , secClass ) }) - network key is not configured...` , level : "warn" } ) ; continue ; } this . driver . controllerLog . logNode ( node . id , { endpoint : endpoint . index , message : `Querying securely supported commands (${ getEnumMemberName ( SecurityClass , secClass ) })...` , direction : "outbound" } ) ; let supportedCCs ; try { supportedCCs = await api . getSupportedCommands ( secClass ) ; } catch ( e ) { if ( isZWaveError ( e ) && e . code === ZWaveErrorCodes . Security2CC_CannotDecode ) { supportedCCs = undefined ; } else { throw e ; } } if ( supportedCCs == undefined ) { if ( endpoint . index === 0 ) { node . securityClasses . set ( secClass , false ) ; this . driver . controllerLog . logNode ( node . id , { message : `The node was NOT granted the security class ${ getEnumMemberName ( SecurityClass , secClass ) }` , direction : "inbound" } ) ; } continue ; } if ( endpoint . index === 0 ) { node . securityClasses . set ( secClass , true ) ; this . driver . controllerLog . logNode ( node . id , { message : `The node was granted the security class ${ getEnumMemberName ( SecurityClass , secClass ) }` , direction : "inbound" } ) ; } if ( ! hasReceivedSecureCommands && supportedCCs . length > 0 ) { hasReceivedSecureCommands = true ; const logLines = [ `received secure commands (${ getEnumMemberName ( SecurityClass , secClass ) })` , "supported CCs:" , ] ; for ( const cc of supportedCCs ) { logLines . push ( `· ${ getCCName ( cc ) }` ) ; } this . driver . controllerLog . logNode ( node . id , { endpoint : endpoint . index , message : logLines . join ( "\n" ) , direction : "inbound" } ) ; for ( const cc of supportedCCs ) { endpoint . addCC ( cc , { isSupported : true , secure : true } ) ; } } } this . interviewComplete = true ; } /** Tests if a command should be sent secure and thus requires encapsulation */ public static requiresEncapsulation ( cc ) { if ( ! cc . secure ) return false ; if ( ! ( cc instanceof Security2CC ) ) return true ; switch ( cc . ccCommand ) { case Security2Command . CommandsSupportedGet : case Security2Command . CommandsSupportedReport : case Security2Command . NetworkKeyGet : case Security2Command . NetworkKeyReport : case Security2Command . NetworkKeyVerify : case Security2Command . TransferEnd : return true ; case Security2Command . KEXSet : case Security2Command . KEXReport : return ( cc as Security2CCKEXSet | Security2CCKEXReport ) . echo ; case Security2Command . KEXFail : { switch ( ( cc as Security2CCKEXFail ) . failType ) { case KEXFailType . Decrypt : case KEXFailType . WrongSecurityLevel : case KEXFailType . KeyNotGranted : case KEXFailType . NoVerify : return true ; default : return false ; } } } return false ; } /** Encapsulates a command that should be sent encrypted */ public static encapsulate ( driver , cc , securityClass ? ) { return new Security2CCMessageEncapsulation ( driver , { nodeId : cc . nodeId , encapsulated : cc , securityClass } ) ; } } interface Security2CCMessageEncapsulationOptions extends CCCommandOptions { /** Can be used to override the default security class for the command */ securityClass ? : SecurityClass ; extensions ? : Security2Extension [ ] ; encapsulated : CommandClass ; } function getCCResponseForMessageEncapsulation ( sent ) { if ( sent . encapsulated ?. expectsCCResponse ( ) ) { return [ Security2CCMessageEncapsulation as any , Security2CCNonceReport as any , ] ; } } function testCCResponseForMessageEncapsulation ( sent , received ) { if ( received instanceof Security2CCMessageEncapsulation ) { return "checkEncapsulated" ; } else { return received . SOS && ! ! received . receiverEI ; } } @ CCCommand ( Security2Command . MessageEncapsulation ) @ expectedCCResponse ( getCCResponseForMessageEncapsulation , testCCResponseForMessageEncapsulation ) export class Security2CCMessageEncapsulation extends Security2CC { declare driver : Driver & { securityManager2 : SecurityManager2 ; controller : ZWaveController & { ownNodeId : number ; } ; } ; public constructor ( driver , options ) { super ( driver , options ) ; assertSecurity . call ( this , options ) ; if ( gotDeserializationOptions ( options ) ) { validatePayload ( this . payload . length >= 2 ) ; this . _sequenceNumber = this . payload [ 0 ] ; validateSequenceNumber . call ( this , this . _sequenceNumber ) ; const peerNodeID = this . nodeId as number ; const node = this . getNode ( ) ! ; validatePayload . withReason ( "The node is not included" ) ( ! ! node ) ; const securityClass = node . getHighestSecurityClass ( ) ; validatePayload . withReason ( "No security class granted" ) ( securityClass !== SecurityClass . None ) ; const hasExtensions = ! ! ( this . payload [ 1 ] & 0b1 ) ; const hasEncryptedExtensions = ! ! ( this . payload [ 1 ] & 0b10 ) ; let offset = 2 ; this . extensions = [ ] ; const parseExtensions = ( buffer ) => { while ( true ) { validatePayload ( buffer . length >= offset + 1 ) ; const extensionLength = Security2Extension . getExtensionLength ( buffer . slice ( offset ) ) ; const ext = Security2Extension . from ( buffer . slice ( offset , offset + extensionLength ) ) ; this . extensions . push ( ext ) ; offset += extensionLength ; if ( ! ext . moreToFollow ) break ; } } ; if ( hasExtensions ) parseExtensions ( this . payload ) ; const unencryptedPayload = this . payload . slice ( 0 , offset ) ; const ciphertext = this . payload . slice ( offset , - SECURITY_S2_AUTH_TAG_LENGTH ) ; const authTag = this . payload . slice ( - SECURITY_S2_AUTH_TAG_LENGTH ) ; const messageLength = super . computeEncapsulationOverhead ( ) + this . payload . length ; const authData = getAuthenticationData ( this . nodeId as number , this . getDestinationIDRX ( ) , this . driver . controller . homeId ! , messageLength , unencryptedPayload ) ; const spanState = this . driver . securityManager2 . getSPANState ( peerNodeID ) ; const failNoSPAN = ( ) => { return validatePayload . fail ( ZWaveErrorCodes . Security2CC_NoSPAN ) ; } ; if ( spanState . type === SPANState . None ) { return failNoSPAN ( ) ; } else if ( spanState . type === SPANState . RemoteEI ) { return failNoSPAN ( ) ; } const decrypt = ( ) : { plaintext : Buffer ; authOK : boolean ; decryptionKey ? : Buffer ; } => { const getNonceAndDecrypt = ( ) => { const iv = this . driver . securityManager2 . nextNonce ( node . id ) ; const { keyCCM : key } = this . driver . securityManager2 . getKeysForNode ( node ) ; return { decryptionKey : key , ... decryptAES128CCM ( key , iv , ciphertext , authData , authTag ) } ; } ; if ( spanState . type === SPANState . SPAN ) { return getNonceAndDecrypt ( ) ; } else if ( spanState . type === SPANState . LocalEI ) { const senderEI = this . getSenderEI ( ) ; if ( ! senderEI ) return failNoSPAN ( ) ; const receiverEI = spanState . receiverEI ; if ( this . driver . securityManager2 . tempKeys . has ( peerNodeID ) ) { this . driver . securityManager2 . initializeTempSPAN ( node , senderEI , receiverEI ) ; const ret = getNonceAndDecrypt ( ) ; if ( ret . authOK ) return ret ; this . driver . securityManager2 . setSPANState ( node . id , spanState ) ; } if ( securityClass != undefined ) { this . driver . securityManager2 . initializeSPAN ( node , securityClass , senderEI , receiverEI ) ; return getNonceAndDecrypt ( ) ; } else { const possibleSecurityClasses = securityClassOrder . filter ( ( s ) => securityClassIsS2 ( s ) ) . filter ( ( s ) => node . hasSecurityClass ( s ) !== false ) ; for ( const secClass of possibleSecurityClasses ) { this . driver . securityManager2 . initializeSPAN ( node , secClass , senderEI , receiverEI ) ; const ret = getNonceAndDecrypt ( ) ; if ( ret . authOK ) { node . securityClasses . set ( secClass , true ) ; return ret ; } this . driver . securityManager2 . setSPANState ( node . id , spanState ) ; } } } return { plaintext : Buffer . from ( [ ] ) , authOK : false } ; } ; let plaintext ; let authOK = false ; let decryptionKey ; for ( let i = 0 ; i < DECRYPT_ATTEMPTS ; i ++ ) { ( { plaintext , authOK , decryptionKey } = decrypt ( ) ) ; if ( ! ! authOK && ! ! plaintext ) break ; } if ( ! authOK || ! plaintext ) { return validatePayload . fail ( ZWaveErrorCodes . Security2CC_CannotDecode ) ; } offset = 0 ; if ( hasEncryptedExtensions ) parseExtensions ( plaintext ) ; const decryptedCCBytes = plaintext . slice ( offset ) ; if ( decryptedCCBytes . length > 0 ) { validatePayload ( decryptedCCBytes . length >= 2 ) ; this . encapsulated = CommandClass . from ( this . driver , { data : decryptedCCBytes , fromEncapsulation : true , encapCC : this } ) ; } this . decryptionKey = decryptionKey ; } else { this . _securityClass = options . securityClass ; this . encapsulated = options . encapsulated ; options . encapsulated . encapsulatingCC = this as any ; this . extensions = options . extensions ?? [ ] ; if ( typeof this . nodeId !== "number" && ! this . extensions . some ( ( e ) => e instanceof MGRPExtension ) ) { throw new ZWaveError ( "Multicast Security S2 encapsulation requires the MGRP extension" , ZWaveErrorCodes . Security2CC_MissingExtension ) ; } } } private _securityClass ? : SecurityClass ; public readonly decryptionKey ? : Buffer ; private _sequenceNumber : number | undefined ; /**
     * Return the sequence number of this command.
     *
     * **WARNING:** If the sequence number hasn't been set before, this will create a new one.
     * When sending messages, this should only happen immediately before serializing.
     */ public get sequenceNumber ( ) : number { if ( this . _sequenceNumber == undefined ) { this . _sequenceNumber = this . driver . securityManager2 . nextSequenceNumber ( this . nodeId as number ) ; } return this . _sequenceNumber ; } public encapsulated ? : CommandClass ; public extensions : Security2Extension [ ] ; public unsetSequenceNumber ( ) { this . _sequenceNumber = undefined ; } private getDestinationIDTX ( ) { const mgrpExtension = this . extensions . find ( ( e ) : e is MGRPExtension => e instanceof MGRPExtension ) ; if ( mgrpExtension ) return mgrpExtension . groupId ; else if ( typeof this . nodeId === "number" ) return this . nodeId ; throw new ZWaveError ( "Multicast Security S2 encapsulation requires the MGRP extension" , ZWaveErrorCodes . Security2CC_MissingExtension ) ; } private getDestinationIDRX ( ) { const mgrpExtension = this . extensions . find ( ( e ) : e is MGRPExtension => e instanceof MGRPExtension ) ; if ( mgrpExtension ) return mgrpExtension . groupId ; return this . driver . controller . ownNodeId ; } /** Returns the Sender's Entropy Input if this command contains an SPAN extension */ private getSenderEI ( ) { const spanExtension = this . extensions . find ( ( e ) : e is SPANExtension => e instanceof SPANExtension ) ; return spanExtension ?. senderEI ; } public serialize ( ) { const node = this . getNode ( ) ! ; const spanState = this . driver . securityManager2 . getSPANState ( node . id ) ; if ( spanState . type === SPANState . None || spanState . type === SPANState . LocalEI ) { throw new ZWaveError ( `Security S2 CC requires the receiver's nonce to be sent!` , ZWaveErrorCodes . Security2CC_NoSPAN ) ; } else if ( spanState . type === SPANState . RemoteEI ) { const senderEI = this . driver . securityManager2 . generateNonce ( undefined ) ; const receiverEI = spanState . receiverEI ; if ( this . driver . securityManager2 . tempKeys . has ( node . id ) ) { this . driver . securityManager2 . initializeTempSPAN ( node , senderEI , receiverEI ) ; } else { const securityClass = this . _securityClass ?? node . getHighestSecurityClass ( ) ; if ( securityClass == undefined ) { throw new ZWaveError ( "No security class defined for this command!" , ZWaveErrorCodes . Security2CC_NoSPAN ) ; } this . driver . securityManager2 . initializeSPAN ( node , securityClass , senderEI , receiverEI ) ; } let spanExtension = this . extensions . find ( ( e ) : e is SPANExtension => e instanceof SPANExtension ) ; if ( spanExtension ) { spanExtension . senderEI = senderEI ; } else { spanExtension = new SPANExtension ( { senderEI } ) ; this . extensions . push ( spanExtension ) ; } } const unencryptedExtensions = this . extensions . filter ( ( e ) => ! e . isEncrypted ( ) ) ; const encryptedExtensions = this . extensions . filter ( ( e ) => e . isEncrypted ( ) ) ; const unencryptedPayload = Buffer . concat ( [ Buffer . from ( [ this . sequenceNumber , ( encryptedExtensions . length > 0 ? 0b10 : 0 ) | ( unencryptedExtensions . length > 0 ? 1 : 0 ) , ] ) , ... unencryptedExtensions . map ( ( e , index ) => e . serialize ( index < unencryptedExtensions . length - 1 ) ) , ] ) ; const serializedCC = this . encapsulated ?. serialize ( ) ?? Buffer . from ( [ ] ) ; const plaintextPayload = Buffer . concat ( [ ... encryptedExtensions . map ( ( e , index ) => e . serialize ( index < encryptedExtensions . length - 1 ) ) , serializedCC , ] ) ; const messageLength = this . computeEncapsulationOverhead ( ) + serializedCC . length ; const authData = getAuthenticationData ( this . driver . controller . ownNodeId , this . getDestinationIDTX ( ) , this . driver . controller . homeId ! , messageLength , unencryptedPayload ) ; const iv = this . driver . securityManager2 . nextNonce ( node . id ) ; const { keyCCM : key } = this . _securityClass != undefined ? this . driver . securityManager2 . getKeysForSecurityClass ( this . _securityClass ) : this . driver . securityManager2 . getKeysForNode ( node ) ; const { ciphertext : ciphertextPayload , authTag } = encryptAES128CCM ( key , iv , plaintextPayload , authData , SECURITY_S2_AUTH_TAG_LENGTH ) ; this . payload = Buffer . concat ( [ unencryptedPayload , ciphertextPayload , authTag , ] ) ; return super . serialize ( ) ; } protected computeEncapsulationOverhead ( ) { const extensionBytes = this . extensions . map ( ( e ) => e . computeLength ( ) ) . reduce ( ( a , b ) => a + b , 0 ) ; return ( super . computeEncapsulationOverhead ( ) + 2 + SECURITY_S2_AUTH_TAG_LENGTH + extensionBytes ) ; } public toLogEntry ( ) { const message = { "sequence number" : this . sequenceNumber } ; if ( this . extensions . length > 0 ) { message . extensions = this . extensions . map ( ( e ) => e . toLogEntry ( ) ) . join ( "" ) ; } return { ... super . toLogEntry ( ) , message } ; } } export type Security2CCNonceReportOptions = { MOS : boolean ; SOS : true ; receiverEI : Buffer ; } | { MOS : true ; SOS : false ; receiverEI ? : undefined ; } ; @ CCCommand ( Security2Command . NonceReport ) export class Security2CCNonceReport extends Security2CC { declare driver : Driver & { securityManager2 : SecurityManager2 ; controller : ZWaveController & { ownNodeId : number ; } ; } ; public constructor ( driver , options ) { super ( driver , options ) ; assertSecurity . call ( this , options ) ; if ( gotDeserializationOptions ( options ) ) { validatePayload ( this . payload . length >= 2 ) ; this . _sequenceNumber = this . payload [ 0 ] ; validateSequenceNumber . call ( this , this . _sequenceNumber ) ; this . MOS = ! ! ( this . payload [ 1 ] & 0b10 ) ; this . SOS = ! ! ( this . payload [ 1 ] & 0b1 ) ; validatePayload ( this . MOS || this . SOS ) ; if ( this . SOS ) { validatePayload ( this . payload . length >= 18 ) ; this . receiverEI = this . payload . slice ( 2 , 18 ) ; this . driver . securityManager2 . storeRemoteEI ( this . nodeId as number , this . receiverEI ) ; } } else { this . SOS = options . SOS ; this . MOS = options . MOS ; if ( options . SOS ) this . receiverEI = options . receiverEI ; } } private _sequenceNumber : number | undefined ; /**
     * Return the sequence number of this command.
     *
     * **WARNING:** If the sequence number hasn't been set before, this will create a new one.
     * When sending messages, this should only happen immediately before serializing.
     */ public get sequenceNumber ( ) : number { if ( this . _sequenceNumber == undefined ) { this . _sequenceNumber = this . driver . securityManager2 . nextSequenceNumber ( this . nodeId as number ) ; } return this . _sequenceNumber ; } public readonly SOS : boolean ; public readonly MOS : boolean ; public readonly receiverEI ? : Buffer ; public serialize ( ) { this . payload = Buffer . from ( [ this . sequenceNumber , ( this . MOS ? 0b10 : 0 ) + ( this . SOS ? 0b1 : 0 ) , ] ) ; if ( this . SOS ) { this . payload = Buffer . concat ( [ this . payload , this . receiverEI ! ] ) ; } return super . serialize ( ) ; } public toLogEntry ( ) { const message = { "sequence number" : this . sequenceNumber , SOS : this . SOS , MOS : this . MOS } ; if ( this . receiverEI ) { message [ "receiver entropy" ] = buffer2hex ( this . receiverEI ) ; } return { ... super . toLogEntry ( ) , message } ; } } @ CCCommand ( Security2Command . NonceGet ) @ expectedCCResponse ( Security2CCNonceReport ) export class Security2CCNonceGet extends Security2CC { declare driver : Driver & { securityManager2 : SecurityManager2 ; controller : ZWaveController & { ownNodeId : number ; } ; } ; public constructor ( driver , options ) { super ( driver , options ) ; assertSecurity . call ( this , options ) ; if ( gotDeserializationOptions ( options ) ) { validatePayload ( this . payload . length >= 1 ) ; this . _sequenceNumber = this . payload [ 0 ] ; validateSequenceNumber . call ( this , this . _sequenceNumber ) ; } else { } } private _sequenceNumber : number | undefined ; /**
     * Return the sequence number of this command.
     *
     * **WARNING:** If the sequence number hasn't been set before, this will create a new one.
     * When sending messages, this should only happen immediately before serializing.
     */ public get sequenceNumber ( ) : number { if ( this . _sequenceNumber == undefined ) { this . _sequenceNumber = this . driver . securityManager2 . nextSequenceNumber ( this . nodeId as number ) ; } return this . _sequenceNumber ; } public serialize ( ) { this . payload = Buffer . from ( [ this . sequenceNumber ] ) ; return super . serialize ( ) ; } public toLogEntry ( ) { return { ... super . toLogEntry ( ) , message : { "sequence number" : this . sequenceNumber } } ; } } interface Security2CCKEXReportOptions { requestCSA : boolean ; echo : boolean ; supportedKEXSchemes : KEXSchemes [ ] ; supportedECDHProfiles : ECDHProfiles [ ] ; requestedKeys : SecurityClass [ ] ; } @ CCCommand ( Security2Command . KEXReport ) export class Security2CCKEXReport extends Security2CC { public constructor ( driver , options ) { super ( driver , options ) ; if ( gotDeserializationOptions ( options ) ) { validatePayload ( this . payload . length >= 4 ) ; this . requestCSA = ! ! ( this . payload [ 0 ] & 0b10 ) ; this . echo = ! ! ( this . payload [ 0 ] & 0b1 ) ; this . supportedKEXSchemes = parseBitMask ( this . payload . slice ( 1 , 2 ) , 0 ) . filter ( ( s ) => s !== 0 ) ; this . supportedECDHProfiles = parseBitMask ( this . payload . slice ( 2 , 3 ) , ECDHProfiles . Curve25519 ) ; this . requestedKeys = parseBitMask ( this . payload . slice ( 3 , 4 ) , SecurityClass . S2_Unauthenticated ) ; } else { this . requestCSA = options . requestCSA ; this . echo = options . echo ; this . supportedKEXSchemes = options . supportedKEXSchemes ; this . supportedECDHProfiles = options . supportedECDHProfiles ; this . requestedKeys = options . requestedKeys ; } } public readonly requestCSA : boolean ; public readonly echo : boolean ; public readonly supportedKEXSchemes : readonly KEXSchemes [ ] ; public readonly supportedECDHProfiles : readonly ECDHProfiles [ ] ; public readonly requestedKeys : readonly SecurityClass [ ] ; public serialize ( ) { this . payload = Buffer . concat ( [ Buffer . from ( [ ( this . requestCSA ? 0b10 : 0 ) + ( this . echo ? 0b1 : 0 ) ] ) , encodeBitMask ( this . supportedKEXSchemes , 7 , 0 ) , encodeBitMask ( this . supportedECDHProfiles , 7 , ECDHProfiles . Curve25519 ) , encodeBitMask ( this . requestedKeys , SecurityClass . S0_Legacy , SecurityClass . S2_Unauthenticated ) , ] ) ; return super . serialize ( ) ; } public toLogEntry ( ) { return { ... super . toLogEntry ( ) , message : { echo : this . echo , "supported schemes" : this . supportedKEXSchemes . map ( ( s ) => `\n· ${ getEnumMemberName ( KEXSchemes , s ) }` ) . join ( "" ) , "supported ECDH profiles" : this . supportedECDHProfiles . map ( ( s ) => `\n· ${ getEnumMemberName ( ECDHProfiles , s ) }` ) . join ( "" ) , "CSA requested" : this . requestCSA , "requested security classes" : this . requestedKeys . map ( ( s ) => `\n· ${ getEnumMemberName ( SecurityClass , s ) }` ) . join ( "" ) } } ; } } @ CCCommand ( Security2Command . KEXGet ) @ expectedCCResponse ( Security2CCKEXReport ) export class Security2CCKEXGet extends Security2CC { } interface Security2CCKEXSetOptions { permitCSA : boolean ; echo : boolean ; selectedKEXScheme : KEXSchemes ; selectedECDHProfile : ECDHProfiles ; grantedKeys : SecurityClass [ ] ; } @ CCCommand ( Security2Command . KEXSet ) export class Security2CCKEXSet extends Security2CC { public constructor ( driver , options ) { super ( driver , options ) ; if ( gotDeserializationOptions ( options ) ) { validatePayload ( this . payload . length >= 4 ) ; this . permitCSA = ! ! ( this . payload [ 0 ] & 0b10 ) ; this . echo = ! ! ( this . payload [ 0 ] & 0b1 ) ; const selectedKEXSchemes = parseBitMask ( this . payload . slice ( 1 , 2 ) , 0 ) . filter ( ( s ) => s !== 0 ) ; validatePayload ( selectedKEXSchemes . length === 1 ) ; this . selectedKEXScheme = selectedKEXSchemes [ 0 ] ; const selectedECDHProfiles = parseBitMask ( this . payload . slice ( 2 , 3 ) , ECDHProfiles . Curve25519 ) ; validatePayload ( selectedECDHProfiles . length === 1 ) ; this . selectedECDHProfile = selectedECDHProfiles [ 0 ] ; this . grantedKeys = parseBitMask ( this . payload . slice ( 3 , 4 ) , SecurityClass . S2_Unauthenticated ) ; } else { this . permitCSA = options . permitCSA ; this . echo = options . echo ; this . selectedKEXScheme = options . selectedKEXScheme ; this . selectedECDHProfile = options . selectedECDHProfile ; this . grantedKeys = options . grantedKeys ; } } public permitCSA : boolean ; public echo : boolean ; public selectedKEXScheme : KEXSchemes ; public selectedECDHProfile : ECDHProfiles ; public grantedKeys : SecurityClass [ ] ; public serialize ( ) { this . payload = Buffer . concat ( [ Buffer . from ( [ ( this . permitCSA ? 0b10 : 0 ) + ( this . echo ? 0b1 : 0 ) ] ) , encodeBitMask ( [ this . selectedKEXScheme ] , 7 , 0 ) , encodeBitMask ( [ this . selectedECDHProfile ] , 7 , ECDHProfiles . Curve25519 ) , encodeBitMask ( this . grantedKeys , SecurityClass . S0_Legacy , SecurityClass . S2_Unauthenticated ) , ] ) ; return super . serialize ( ) ; } public toLogEntry ( ) { return { ... super . toLogEntry ( ) , message : { echo : this . echo , "selected scheme" : getEnumMemberName ( KEXSchemes , this . selectedKEXScheme ) , "selected ECDH profile" : getEnumMemberName ( ECDHProfiles , this . selectedECDHProfile ) , "CSA permitted" : this . permitCSA , "granted security classes" : this . grantedKeys . map ( ( s ) => `\n· ${ getEnumMemberName ( SecurityClass , s ) }` ) . join ( "" ) } } ; } } interface Security2CCKEXFailOptions extends CCCommandOptions { failType : KEXFailType ; } @ CCCommand ( Security2Command . KEXFail ) export class Security2CCKEXFail extends Security2CC { public constructor ( driver , options ) { super ( driver , options ) ; if ( gotDeserializationOptions ( options ) ) { validatePayload ( this . payload . length >= 1 ) ; this . failType = this . payload [ 0 ] ; } else { this . failType = options . failType ; } } public failType : KEXFailType ; public serialize ( ) { this . payload = Buffer . from ( [ this . failType ] ) ; return super . serialize ( ) ; } public toLogEntry ( ) { return { ... super . toLogEntry ( ) , message : { reason : getEnumMemberName ( KEXFailType , this . failType ) } } ; } } interface Security2CCPublicKeyReportOptions extends CCCommandOptions { includingNode : boolean ; publicKey : Buffer ; } @ CCCommand ( Security2Command . PublicKeyReport ) export class Security2CCPublicKeyReport extends Security2CC { public constructor ( driver , options ) { super ( driver , options ) ; if ( gotDeserializationOptions ( options ) ) { validatePayload ( this . payload . length >= 17 ) ; this . includingNode = ! ! ( this . payload [ 0 ] & 0b1 ) ; this . publicKey = this . payload . slice ( 1 ) ; } else { this . includingNode = options . includingNode ; this . publicKey = options . publicKey ; } } public includingNode : boolean ; public publicKey : Buffer ; public serialize ( ) { this . payload = Buffer . concat ( [ Buffer . from ( [ this . includingNode ? 1 : 0 ] ) , this . publicKey , ] ) ; return super . serialize ( ) ; } public toLogEntry ( ) { return { ... super . toLogEntry ( ) , message : { "is including node" : this . includingNode , "public key" : buffer2hex ( this . publicKey ) } } ; } } interface Security2CCNetworkKeyReportOptions extends CCCommandOptions { grantedKey : SecurityClass ; networkKey : Buffer ; } @ CCCommand ( Security2Command . NetworkKeyReport ) export class Security2CCNetworkKeyReport extends Security2CC { public constructor ( driver , options ) { super ( driver , options ) ; if ( gotDeserializationOptions ( options ) ) { throw new ZWaveError ( `${ this . constructor . name }: deserialization not implemented` , ZWaveErrorCodes . Deserialization_NotImplemented ) ; } else { this . grantedKey = options . grantedKey ; this . networkKey = options . networkKey ; } } public grantedKey : SecurityClass ; public networkKey : Buffer ; public serialize ( ) { this . payload = Buffer . concat ( [ securityClassToBitMask ( this . grantedKey ) , this . networkKey , ] ) ; return super . serialize ( ) ; } public toLogEntry ( ) { return { ... super . toLogEntry ( ) , message : { "security class" : getEnumMemberName ( SecurityClass , this . grantedKey ) , "network key" : buffer2hex ( this . networkKey ) } } ; } } interface Security2CCNetworkKeyGetOptions extends CCCommandOptions { requestedKey : SecurityClass ; } @ CCCommand ( Security2Command . NetworkKeyGet ) @ expectedCCResponse ( Security2CCNetworkKeyReport ) export class Security2CCNetworkKeyGet extends Security2CC { public constructor ( driver , options ) { super ( driver , options ) ; if ( gotDeserializationOptions ( options ) ) { validatePayload ( this . payload . length >= 1 ) ; this . requestedKey = bitMaskToSecurityClass ( this . payload , 0 ) ; } else { this . requestedKey = options . requestedKey ; } } public requestedKey : SecurityClass ; public serialize ( ) { this . payload = securityClassToBitMask ( this . requestedKey ) ; return super . serialize ( ) ; } public toLogEntry ( ) { return { ... super . toLogEntry ( ) , message : { "security class" : getEnumMemberName ( SecurityClass , this . requestedKey ) } } ; } } @ CCCommand ( Security2Command . NetworkKeyVerify ) export class Security2CCNetworkKeyVerify extends Security2CC { } interface Security2CCTransferEndOptions extends CCCommandOptions { keyVerified : boolean ; keyRequestComplete : boolean ; } @ CCCommand ( Security2Command . TransferEnd ) export class Security2CCTransferEnd extends Security2CC { public constructor ( driver , options ) { super ( driver , options ) ; if ( gotDeserializationOptions ( options ) ) { validatePayload ( this . payload . length >= 1 ) ; this . keyVerified = ! ! ( this . payload [ 0 ] & 0b10 ) ; this . keyRequestComplete = ! ! ( this . payload [ 0 ] & 0b1 ) ; } else { this . keyVerified = options . keyVerified ; this . keyRequestComplete = options . keyRequestComplete ; } } public keyVerified : boolean ; public keyRequestComplete : boolean ; public serialize ( ) { this . payload = Buffer . from ( [ ( this . keyVerified ? 0b10 : 0 ) + ( this . keyRequestComplete ? 0b1 : 0 ) , ] ) ; return super . serialize ( ) ; } public toLogEntry ( ) { return { ... super . toLogEntry ( ) , message : { "key verified" : this . keyVerified , "request complete" : this . keyRequestComplete } } ; } } @ CCCommand ( Security2Command . CommandsSupportedReport ) export class Security2CCCommandsSupportedReport extends Security2CC { public constructor ( driver , options ) { super ( driver , options ) ; const CCs = parseCCList ( this . payload ) ; this . supportedCCs = CCs . supportedCCs ; } public readonly supportedCCs : CommandClasses [ ] ; public toLogEntry ( ) { return { ... super . toLogEntry ( ) , message : { supportedCCs : this . supportedCCs . map ( ( cc ) => getCCName ( cc ) ) . map ( ( cc ) => `\n· ${ cc }` ) . join ( "" ) } } ; } } @ CCCommand ( Security2Command . CommandsSupportedGet ) @ expectedCCResponse ( Security2CCCommandsSupportedReport ) export class Security2CCCommandsSupportedGet extends Security2CC { }