import { CommandClasses , computeMAC , decryptAES128OFB , encryptAES128OFB , generateAuthKey , generateEncryptionKey , getCCName , isTransmissionError , Maybe , MessageOrCCLogEntry , MessageRecord , parseCCList , SecurityClass , SecurityManager , validatePayload , ZWaveError , ZWaveErrorCodes , } from "@zwave-js/core" ; import { buffer2hex , num2hex , pick } from "@zwave-js/shared" ; import { randomBytes } from "crypto" ; 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 { PhysicalCCAPI } from "./API" ; import { API , CCCommand , CCCommandOptions , CommandClass , commandClass , CommandClassDeserializationOptions , expectedCCResponse , gotDeserializationOptions , implementedVersion , } from "./CommandClass" ; import { Security2CC } from "./Security2CC" ; export enum SecurityCommand { CommandsSupportedGet = 0x02 , CommandsSupportedReport = 0x03 , SchemeGet = 0x04 , SchemeReport = 0x05 , SchemeInherit = 0x08 , NetworkKeySet = 0x06 , NetworkKeyVerify = 0x07 , NonceGet = 0x40 , NonceReport = 0x80 , CommandEncapsulation = 0x81 , CommandEncapsulationNonceGet = 0xc1 } function getAuthenticationData ( senderNonce , receiverNonce , ccCommand , sendingNodeId , receivingNodeId , encryptedPayload ) { return Buffer . concat ( [ senderNonce , receiverNonce , Buffer . from ( [ ccCommand , sendingNodeId , receivingNodeId , encryptedPayload . length , ] ) , encryptedPayload , ] ) ; } function throwNoNonce ( reason ? ) { let message = `Security CC requires a nonce to be sent!` ; if ( reason ) message += ` Reason: ${ reason }` ; throw new ZWaveError ( message , ZWaveErrorCodes . SecurityCC_NoNonce ) ; } const HALF_NONCE_SIZE = 8 ; @ API ( CommandClasses . Security ) export class SecurityCCAPI extends PhysicalCCAPI { public supportsCommand ( _cmd ) { return true ; } public async sendEncapsulated ( encapsulated , requestNextNonce = false ) { if ( requestNextNonce ) { this . assertSupportsCommand ( SecurityCommand , SecurityCommand . CommandEncapsulation ) ; } else { this . assertSupportsCommand ( SecurityCommand , SecurityCommand . CommandEncapsulationNonceGet ) ; } const cc = new ( requestNextNonce ? SecurityCCCommandEncapsulationNonceGet : SecurityCCCommandEncapsulation ) ( this . driver , { nodeId : this . endpoint . nodeId , encapsulated } ) ; await this . driver . sendCommand ( cc , this . commandOptions ) ; } /**
     * Requests a new nonce for Security CC encapsulation which is not directly linked to a specific command.
     */ public async getNonce ( ) { this . assertSupportsCommand ( SecurityCommand , SecurityCommand . NonceGet ) ; const cc = new SecurityCCNonceGet ( this . driver , { nodeId : this . endpoint . nodeId , endpoint : this . endpoint . index } ) ; const response = await this . driver . sendCommand < SecurityCCNonceReport > ( cc , { ... this . commandOptions , priority : MessagePriority . Nonce , maxSendAttempts : 1 } ) ; if ( ! response ) return ; const nonce = response . nonce ; const secMan = this . driver . securityManager ! ; secMan . setNonce ( { issuer : this . endpoint . nodeId , nonceId : secMan . getNonceId ( nonce ) } , { nonce , receiver : this . driver . controller . ownNodeId ! } , { free : true } ) ; return nonce ; } /**
     * Responds to a NonceGet request. The message is sent without any retransmission etc.
     * The return value indicates whether a nonce was successfully sent
     */ public async sendNonce ( ) { this . assertSupportsCommand ( SecurityCommand , SecurityCommand . NonceReport ) ; if ( ! this . driver . securityManager ) { throw new ZWaveError ( `Nonces can only be sent if secure communication is set up!` , ZWaveErrorCodes . Driver_NoSecurity ) ; } const nonce = this . driver . securityManager . generateNonce ( this . endpoint . nodeId , HALF_NONCE_SIZE ) ; const nonceId = this . driver . securityManager . getNonceId ( nonce ) ; const cc = new SecurityCCNonceReport ( this . driver , { nodeId : this . endpoint . nodeId , endpoint : this . endpoint . index , nonce } ) ; 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 . securityManager . deleteNonce ( nonceId ) ; return false ; } else { throw e ; } } return true ; } public async getSecurityScheme ( ) { this . assertSupportsCommand ( SecurityCommand , SecurityCommand . SchemeGet ) ; const cc = new SecurityCCSchemeGet ( this . driver , { nodeId : this . endpoint . nodeId , endpoint : this . endpoint . index } ) ; await this . driver . sendCommand ( cc , this . commandOptions ) ; return [ 0 ] ; } public async inheritSecurityScheme ( ) { this . assertSupportsCommand ( SecurityCommand , SecurityCommand . SchemeInherit ) ; const cc = new SecurityCCSchemeInherit ( this . driver , { nodeId : this . endpoint . nodeId , endpoint : this . endpoint . index } ) ; await this . driver . sendCommand ( cc , this . commandOptions ) ; } public async setNetworkKey ( networkKey ) { this . assertSupportsCommand ( SecurityCommand , SecurityCommand . NetworkKeySet ) ; const keySet = new SecurityCCNetworkKeySet ( this . driver , { nodeId : this . endpoint . nodeId , endpoint : this . endpoint . index , networkKey } ) ; const cc = new SecurityCCCommandEncapsulation ( this . driver , { nodeId : this . endpoint . nodeId , endpoint : this . endpoint . index , encapsulated : keySet , alternativeNetworkKey : Buffer . alloc ( 16 , 0 ) } ) ; await this . driver . sendCommand ( cc , this . commandOptions ) ; } public async getSupportedCommands ( ) { this . assertSupportsCommand ( SecurityCommand , SecurityCommand . CommandsSupportedGet ) ; const cc = new SecurityCCCommandsSupportedGet ( this . driver , { nodeId : this . endpoint . nodeId , endpoint : this . endpoint . index } ) ; const response = await this . driver . sendCommand < SecurityCCCommandsSupportedReport > ( cc , this . commandOptions ) ; if ( response ) { return pick ( response , [ "supportedCCs" , "controlledCCs" ] ) ; } } } @ commandClass ( CommandClasses . Security ) @ implementedVersion ( 1 ) export class SecurityCC extends CommandClass { declare ccCommand : SecurityCommand ; declare nodeId : number ; declare driver : Driver & { securityManager : SecurityManager ; controller : ZWaveController & { ownNodeId : number ; } ; } ; public async interview ( ) { const node = this . getNode ( ) ! ; const endpoint = this . getEndpoint ( ) ! ; const api = endpoint . commandClasses . Security . withOptions ( { priority : MessagePriority . NodeQuery } ) ; this . driver . controllerLog . logNode ( node . id , { message : "Querying securely supported commands (S0)..." , direction : "outbound" } ) ; const resp = await api . getSupportedCommands ( ) ; if ( ! resp ) { if ( node . securityClasses . get ( SecurityClass . S0_Legacy ) === true ) { this . driver . controllerLog . logNode ( node . id , { endpoint : this . endpointIndex , message : "Querying securely supported commands (S0) timed out" , level : "warn" } ) ; } else { this . driver . controllerLog . logNode ( node . id , `The node was not granted the S0 security class. Continuing interview non-securely.` ) ; node . securityClasses . set ( SecurityClass . S0_Legacy , false ) ; } return ; } const logLines = [ "received secure commands (S0)" , "supported CCs:" , ] ; for ( const cc of resp . supportedCCs ) { logLines . push ( `· ${ getCCName ( cc ) }` ) ; } logLines . push ( "controlled CCs:" ) ; for ( const cc of resp . controlledCCs ) { logLines . push ( `· ${ getCCName ( cc ) }` ) ; } this . driver . controllerLog . logNode ( node . id , { message : logLines . join ( "\n" ) , direction : "inbound" } ) ; for ( const cc of resp . supportedCCs ) { endpoint . addCC ( cc , { isSupported : true , secure : true } ) ; } for ( const cc of resp . controlledCCs ) { endpoint . addCC ( cc , { isControlled : true , secure : true } ) ; } if ( node . hasSecurityClass ( SecurityClass . S0_Legacy ) !== true ) { node . securityClasses . set ( SecurityClass . S0_Legacy , true ) ; this . driver . controllerLog . logNode ( node . id , `The node was granted the S0 security class` ) ; } this . interviewComplete = true ; } /** Tests if a command should be sent secure and thus requires encapsulation */ public static requiresEncapsulation ( cc ) { return ( cc . secure && ! ( cc instanceof Security2CC ) && ! ( cc instanceof SecurityCCCommandEncapsulation ) && ! ( cc instanceof SecurityCCNonceGet ) && ! ( cc instanceof SecurityCCNonceReport ) && ! ( cc instanceof SecurityCCSchemeGet ) && ! ( cc instanceof SecurityCCSchemeReport ) ) ; } /** Encapsulates a command that should be sent encrypted */ public static encapsulate ( driver , cc ) { return new SecurityCCCommandEncapsulation ( driver , { nodeId : cc . nodeId , encapsulated : cc } ) ; } } interface SecurityCCNonceReportOptions extends CCCommandOptions { nonce : Buffer ; } @ CCCommand ( SecurityCommand . NonceReport ) export class SecurityCCNonceReport extends SecurityCC { constructor ( driver , options ) { super ( driver , options ) ; if ( gotDeserializationOptions ( options ) ) { validatePayload . withReason ( "Invalid nonce length" ) ( this . payload . length === HALF_NONCE_SIZE ) ; this . nonce = this . payload ; } else { if ( options . nonce . length !== HALF_NONCE_SIZE ) { throw new ZWaveError ( `Nonce must have length ${ HALF_NONCE_SIZE }!` , ZWaveErrorCodes . Argument_Invalid ) ; } this . nonce = options . nonce ; } } public nonce : Buffer ; public serialize ( ) { this . payload = this . nonce ; return super . serialize ( ) ; } public toLogEntry ( ) { return { ... super . toLogEntry ( ) , message : { nonce : buffer2hex ( this . nonce ) } } ; } } @ CCCommand ( SecurityCommand . NonceGet ) @ expectedCCResponse ( SecurityCCNonceReport ) export class SecurityCCNonceGet extends SecurityCC { } interface SecurityCCCommandEncapsulationOptions extends CCCommandOptions { encapsulated : CommandClass ; alternativeNetworkKey ? : Buffer ; } function getCCResponseForCommandEncapsulation ( sent ) { if ( sent . encapsulated . expectsCCResponse ( ) ) { return SecurityCCCommandEncapsulation ; } } @ CCCommand ( SecurityCommand . CommandEncapsulation ) @ expectedCCResponse ( getCCResponseForCommandEncapsulation , ( ) => "checkEncapsulated" ) export class SecurityCCCommandEncapsulation extends SecurityCC { public constructor ( driver , options ) { super ( driver , options ) ; const verb = gotDeserializationOptions ( options ) ? "decoded" : "sent" ; if ( ! ( this . driver . controller . ownNodeId as unknown ) ) { throw new ZWaveError ( `Secure commands (S0) can only be ${ verb } when the controller's node id is known!` , ZWaveErrorCodes . Driver_NotReady ) ; } else if ( ! ( this . driver . securityManager as unknown ) ) { throw new ZWaveError ( `Secure commands (S0) can only be ${ verb } when the network key for the driver is set` , ZWaveErrorCodes . Driver_NoSecurity ) ; } if ( gotDeserializationOptions ( options ) ) { validatePayload ( this . payload . length >= HALF_NONCE_SIZE + 1 + 1 + 1 + 8 ) ; const iv = this . payload . slice ( 0 , HALF_NONCE_SIZE ) ; const encryptedPayload = this . payload . slice ( HALF_NONCE_SIZE , - 9 ) ; const nonceId = this . payload [ this . payload . length - 9 ] ; const authCode = this . payload . slice ( - 8 ) ; const nonce = this . driver . securityManager . getNonce ( nonceId ) ; validatePayload . withReason ( `Nonce ${ num2hex ( nonceId ) } expired, cannot decode security encapsulated command.` ) ( ! ! nonce ) ; this . driver . securityManager . deleteNonce ( nonceId ) ; this . authKey = this . driver . securityManager . authKey ; this . encryptionKey = this . driver . securityManager . encryptionKey ; const authData = getAuthenticationData ( iv , nonce ! , this . ccCommand , this . nodeId , this . driver . controller . ownNodeId , encryptedPayload ) ; const expectedAuthCode = computeMAC ( authData , this . authKey ) ; validatePayload . withReason ( "Invalid auth code, won't accept security encapsulated command." ) ( authCode . equals ( expectedAuthCode ) ) ; const frameControlAndDecryptedCC = decryptAES128OFB ( encryptedPayload , this . encryptionKey , Buffer . concat ( [ iv , nonce ! ] ) ) ; const frameControl = frameControlAndDecryptedCC [ 0 ] ; this . sequenceCounter = frameControl & 0b1111 ; this . sequenced = ! ! ( frameControl & 16 ) ; this . secondFrame = ! ! ( frameControl & 32 ) ; this . decryptedCCBytes = frameControlAndDecryptedCC . slice ( 1 ) ; } else { this . encapsulated = options . encapsulated ; options . encapsulated . encapsulatingCC = this as any ; if ( options . alternativeNetworkKey ) { this . authKey = generateAuthKey ( options . alternativeNetworkKey ) ; this . encryptionKey = generateEncryptionKey ( options . alternativeNetworkKey ) ; } else { this . authKey = this . driver . securityManager . authKey ; this . encryptionKey = this . driver . securityManager . encryptionKey ; } } } private sequenced : boolean | undefined ; private secondFrame : boolean | undefined ; private sequenceCounter : number | undefined ; private decryptedCCBytes : Buffer | undefined ; public encapsulated ! : CommandClass ; private authKey : Buffer ; private encryptionKey : Buffer ; public get nonceId ( ) : number | undefined { if ( ! this . nonce ) return undefined ; return this . driver . securityManager . getNonceId ( this . nonce ) ; } public nonce : Buffer | undefined ; public getPartialCCSessionId ( ) { if ( this . sequenced ) { return { ccCommand : undefined , sequence : this . sequenceCounter } ; } else { return { ccCommand : undefined } ; } } public expectMoreMessages ( ) { return ! ! this . sequenced && ! this . secondFrame ; } public mergePartialCCs ( partials ) { this . decryptedCCBytes = Buffer . concat ( [ ... partials , this ] . map ( ( cc ) => cc . decryptedCCBytes ! ) ) ; validatePayload ( this . decryptedCCBytes . length >= 2 ) ; this . encapsulated = CommandClass . from ( this . driver , { data : this . decryptedCCBytes , fromEncapsulation : true , encapCC : this } ) ; } public serialize ( ) { if ( ! this . nonce ) throwNoNonce ( ) ; if ( this . nonce . length !== HALF_NONCE_SIZE ) throwNoNonce ( "Invalid nonce size" ) ; const serializedCC = this . encapsulated . serialize ( ) ; const plaintext = Buffer . concat ( [ Buffer . from ( [ 0 ] ) , serializedCC , ] ) ; const senderNonce = randomBytes ( HALF_NONCE_SIZE ) ; const iv = Buffer . concat ( [ senderNonce , this . nonce ] ) ; const ciphertext = encryptAES128OFB ( plaintext , this . encryptionKey , iv ) ; const authData = getAuthenticationData ( senderNonce , this . nonce , this . ccCommand , this . driver . controller . ownNodeId , this . nodeId , ciphertext ) ; const authCode = computeMAC ( authData , this . authKey ) ; this . payload = Buffer . concat ( [ senderNonce , ciphertext , Buffer . from ( [ this . nonceId ! ] ) , authCode , ] ) ; return super . serialize ( ) ; } protected computeEncapsulationOverhead ( ) { return super . computeEncapsulationOverhead ( ) + 18 ; } public toLogEntry ( ) { const message = { } ; if ( this . nonceId != undefined ) { message [ "nonce id" ] = this . nonceId ; } if ( this . sequenced != undefined ) { message . sequenced = this . sequenced ; if ( this . sequenced ) { if ( this . secondFrame != undefined ) { message [ "second frame" ] = this . secondFrame ; } if ( this . sequenceCounter != undefined ) { message [ "sequence counter" ] = this . sequenceCounter ; } } } return { ... super . toLogEntry ( ) , message } ; } } @ CCCommand ( SecurityCommand . CommandEncapsulationNonceGet ) export class SecurityCCCommandEncapsulationNonceGet extends SecurityCCCommandEncapsulation { } @ CCCommand ( SecurityCommand . SchemeReport ) export class SecurityCCSchemeReport extends SecurityCC { public constructor ( driver , options ) { super ( driver , options ) ; validatePayload ( this . payload . length >= 1 , ( this . payload [ 0 ] & 0b1 ) === 0 ) ; } } @ CCCommand ( SecurityCommand . SchemeGet ) @ expectedCCResponse ( SecurityCCSchemeReport ) export class SecurityCCSchemeGet extends SecurityCC { public constructor ( driver , options ) { super ( driver , options ) ; } public serialize ( ) { this . payload = Buffer . from ( [ 0 ] ) ; return super . serialize ( ) ; } public toLogEntry ( ) { return { ... super . toLogEntry ( ) , message : undefined } ; } } @ CCCommand ( SecurityCommand . SchemeInherit ) @ expectedCCResponse ( SecurityCCSchemeReport ) export class SecurityCCSchemeInherit extends SecurityCC { public constructor ( driver , options ) { super ( driver , options ) ; } public serialize ( ) { this . payload = Buffer . from ( [ 0 ] ) ; return super . serialize ( ) ; } public toLogEntry ( ) { return { ... super . toLogEntry ( ) , message : undefined } ; } } @ CCCommand ( SecurityCommand . NetworkKeyVerify ) export class SecurityCCNetworkKeyVerify extends SecurityCC { } interface SecurityCCNetworkKeySetOptions extends CCCommandOptions { networkKey : Buffer ; } @ CCCommand ( SecurityCommand . NetworkKeySet ) @ expectedCCResponse ( SecurityCCNetworkKeyVerify ) export class SecurityCCNetworkKeySet extends SecurityCC { public constructor ( driver , options ) { super ( driver , options ) ; if ( gotDeserializationOptions ( options ) ) { throw new ZWaveError ( `${ this . constructor . name }: deserialization not implemented` , ZWaveErrorCodes . Deserialization_NotImplemented ) ; } else { if ( options . networkKey . length !== 16 ) { throw new ZWaveError ( `The network key must have length 16!` , ZWaveErrorCodes . Argument_Invalid ) ; } this . networkKey = options . networkKey ; } } public networkKey : Buffer ; public serialize ( ) { this . payload = this . networkKey ; return super . serialize ( ) ; } public toLogEntry ( ) { return { ... super . toLogEntry ( ) , message : { "network key" : buffer2hex ( this . networkKey ) } } ; } } @ CCCommand ( SecurityCommand . CommandsSupportedReport ) export class SecurityCCCommandsSupportedReport extends SecurityCC { public constructor ( driver , options ) { super ( driver , options ) ; validatePayload ( this . payload . length >= 1 ) ; this . reportsToFollow = this . payload [ 0 ] ; const list = parseCCList ( this . payload . slice ( 1 ) ) ; this . _supportedCCs = list . supportedCCs ; this . _controlledCCs = list . controlledCCs ; } public readonly reportsToFollow : number ; public getPartialCCSessionId ( ) { return { } ; } public expectMoreMessages ( ) { return this . reportsToFollow > 0 ; } private _supportedCCs : CommandClasses [ ] ; public get supportedCCs ( ) : CommandClasses [ ] { return this . _supportedCCs ; } private _controlledCCs : CommandClasses [ ] ; public get controlledCCs ( ) : CommandClasses [ ] { return this . _controlledCCs ; } public mergePartialCCs ( partials ) { this . _supportedCCs = [ ... partials , this ] . map ( ( report ) => report . _supportedCCs ) . reduce ( ( prev , cur ) => prev . concat ( ... cur ) , [ ] ) ; this . _controlledCCs = [ ... partials , this ] . map ( ( report ) => report . _controlledCCs ) . reduce ( ( prev , cur ) => prev . concat ( ... cur ) , [ ] ) ; } public toLogEntry ( ) { return { ... super . toLogEntry ( ) , message : { reportsToFollow : this . reportsToFollow , supportedCCs : this . _supportedCCs . map ( ( cc ) => getCCName ( cc ) ) . map ( ( cc ) => `\n· ${ cc }` ) . join ( "" ) , controlledCCs : this . _controlledCCs . map ( ( cc ) => getCCName ( cc ) ) . map ( ( cc ) => `\n· ${ cc }` ) . join ( "" ) } } ; } } @ CCCommand ( SecurityCommand . CommandsSupportedGet ) @ expectedCCResponse ( SecurityCCCommandsSupportedReport ) export class SecurityCCCommandsSupportedGet extends SecurityCC { }