UNPKG

inventoresed

Version:

Z-Wave driver written entirely in JavaScript/TypeScript

389 lines (355 loc) 11.5 kB
/** Management class and utils for Security S2 */ import { getEnumMemberName } from "@zwave-js/shared"; import * as crypto from "crypto"; import { ZWaveError, ZWaveErrorCodes } from "../error/ZWaveError"; import { highResTimestamp } from "../util/date"; import { increment } from "./bufferUtils"; import { computeNoncePRK, deriveMEI, deriveNetworkKeys, encryptAES128ECB, } from "./crypto"; import { CtrDRBG } from "./ctr_drbg"; import { SecurityClass } from "./SecurityClass"; interface NetworkKeys { pnk: Buffer; keyCCM: Buffer; keyMPAN: Buffer; personalizationString: Buffer; } interface TempNetworkKeys { keyCCM: Buffer; personalizationString: Buffer; } export enum SPANState { /** No entry exists */ None = 0, /* The other node's receiver's entropy input is known but, but we didn't send it our sender's EI yet */ RemoteEI, /* We've sent the other node our receiver's entropy input, but we didn't receive its sender's EI yet */ LocalEI, /* An SPAN with the other node has been established */ SPAN, } export type SPANTableEntry = | { // We know the other node's receiver's entropy input, but we didn't send it our sender's EI yet type: SPANState.RemoteEI; receiverEI: Buffer; } | { // We've sent the other node our receiver's entropy input, but we didn't receive its sender's EI yet type: SPANState.LocalEI; receiverEI: Buffer; } | { // We've established an SPAN with the other node type: SPANState.SPAN; securityClass: SecurityClass; rng: CtrDRBG; /** The most recent generated SPAN */ currentSPAN?: { nonce: Buffer; expires: number; }; }; // How many sequence numbers are remembered for each node when checking for duplicates const SINGLECAST_MAX_SEQ_NUMS = 10; // How long a singlecast nonce used for encryption will be kept around to attempt decryption of in-flight messages 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, Buffer.alloc(32, 0), ); } /** PRNG used to initialize the others */ private rng: CtrDRBG; /** A map of SPAN states for each node */ private spanTable = new Map<number, SPANTableEntry>(); /** A map of temporary keys for each node that are used for the key exchange */ public readonly tempKeys = new Map<number, TempNetworkKeys>(); /** A map of sequence numbers that were last used in communication with a node */ private ownSequenceNumbers = new Map<number, number>(); private peerSequenceNumbers = new Map<number, number[]>(); /** A map of MPAN states for each multicast group */ private mpanStates = new Map<number, Buffer>(); /** A map of permanent network keys per security class */ private networkKeys = new Map<SecurityClass, NetworkKeys>(); /** Which multicast group has been assigned which security class */ private groupClasses = new Map<number, SecurityClass>(); /** Sets the PNK for a given security class and derives the encryption keys from it */ public setKey(securityClass: SecurityClass, key: Buffer): void { 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, ...deriveNetworkKeys(key), }); } public assignSecurityClassMulticast( group: number, securityClass: SecurityClass, ): void { this.groupClasses.set(group, securityClass); } public hasKeysForSecurityClass(securityClass: SecurityClass): boolean { return this.networkKeys.has(securityClass); } public getKeysForSecurityClass(securityClass: SecurityClass): NetworkKeys { const keys = this.networkKeys.get(securityClass); if (!keys) { throw new ZWaveError( `The network key for the security class ${getEnumMemberName( SecurityClass, securityClass, )} has not been set up yet!`, ZWaveErrorCodes.Security2CC_NotInitialized, ); } return { ...keys }; } public getKeysForNode(peerNodeID: number): NetworkKeys | TempNetworkKeys { const spanState = this.getSPANState(peerNodeID); // The keys we return must match the actual SPAN state (if we have one) // Meaning if an SPAN for the temporary inclusion key is established, // we need to return that temporary key if ( spanState.type === SPANState.SPAN && spanState.securityClass === SecurityClass.Temporary ) { if (this.tempKeys.has(peerNodeID)) return this.tempKeys.get(peerNodeID)!; throw new ZWaveError( `Temporary encryption key for node ${peerNodeID} is not known!`, ZWaveErrorCodes.Security2CC_NotInitialized, ); } else if (spanState.type !== SPANState.SPAN) { throw new ZWaveError( `Security class for node ${peerNodeID} is not yet known!`, ZWaveErrorCodes.Security2CC_NotInitialized, ); } return this.getKeysForSecurityClass(spanState.securityClass); } public getSPANState( peerNodeID: number, ): SPANTableEntry | { type: SPANState.None } { return this.spanTable.get(peerNodeID) ?? { type: SPANState.None }; } /** Tests whether the most recent secure command for a node has used the given security class */ public hasUsedSecurityClass( peerNodeID: number, securityClass: SecurityClass, ): boolean { const spanState = this.spanTable.get(peerNodeID); if (!spanState) return false; if (spanState.type !== SPANState.SPAN) return false; return spanState.securityClass === securityClass; } /** * 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 generateNonce(receiver: number | undefined): Buffer { const receiverEI = this.rng.generate(16); if (receiver != undefined) { this.spanTable.set(receiver, { type: SPANState.LocalEI, receiverEI, }); } return receiverEI; } /** * Stores the given SPAN state in the table. This should NEVER be called by user code. */ public setSPANState( peerNodeID: number, state: SPANTableEntry | { type: SPANState.None }, ): void { if (state.type === SPANState.None) { this.spanTable.delete(peerNodeID); } else { this.spanTable.set(peerNodeID, state); } } /** Invalidates the SPAN state for the given receiver */ public deleteNonce(receiver: number): void { this.spanTable.delete(receiver); this.peerSequenceNumbers.delete(receiver); // Keep our own sequence number } /** Initializes the singlecast PAN generator for a given node based on the given entropy inputs */ public initializeSPAN( peerNodeId: number, securityClass: SecurityClass, senderEI: Buffer, receiverEI: Buffer, ): void { 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 = computeNoncePRK(senderEI, receiverEI); const MEI = deriveMEI(noncePRK); this.spanTable.set(peerNodeId, { securityClass, type: SPANState.SPAN, rng: new CtrDRBG( 128, false, MEI, undefined, keys.personalizationString, ), }); } /** Initializes the singlecast PAN generator for a given node based on the given entropy inputs */ public initializeTempSPAN( peerNodeId: number, senderEI: Buffer, receiverEI: Buffer, ): void { 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 = computeNoncePRK(senderEI, receiverEI); const MEI = deriveMEI(noncePRK); this.spanTable.set(peerNodeId, { securityClass: SecurityClass.Temporary, type: SPANState.SPAN, rng: new CtrDRBG( 128, false, MEI, undefined, keys.personalizationString, ), }); } /** Tests if the given combination of peer node ID and sequence number is a duplicate */ public isDuplicateSinglecast( peerNodeId: number, sequenceNumber: number, ): boolean { return ( this.peerSequenceNumbers .get(peerNodeId) ?.includes(sequenceNumber) ?? false ); } /** Stores the latest sequence number for the given peer node ID and returns the previous one */ public storeSequenceNumber( peerNodeId: number, sequenceNumber: number, ): number | undefined { if (this.peerSequenceNumbers.has(peerNodeId)) { // Store the last SINGLECAST_MAX_SEQ_NUMS sequence numbers const arr = this.peerSequenceNumbers.get(peerNodeId)!; const prev = arr[arr.length - 1]; arr.push(sequenceNumber); if (arr.length > SINGLECAST_MAX_SEQ_NUMS) arr.shift(); return prev; } else { this.peerSequenceNumbers.set(peerNodeId, [sequenceNumber]); } } public storeRemoteEI(peerNodeId: number, remoteEI: Buffer): void { if (remoteEI.length !== 16) { throw new ZWaveError( `The entropy input must consist of 16 bytes`, ZWaveErrorCodes.Argument_Invalid, ); } this.spanTable.set(peerNodeId, { type: SPANState.RemoteEI, receiverEI: remoteEI, }); } /** * 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 nextNonce(peerNodeId: number, store?: boolean): Buffer { 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 = spanState.rng.generate(16).slice(0, 13); spanState.currentSPAN = store ? { nonce, expires: highResTimestamp() + SINGLECAST_NONCE_EXPIRY_NS, } : undefined; return nonce; } /** Returns the next sequence number to use for outgoing messages to the given node */ public nextSequenceNumber(peerNodeId: number): number { let seq = this.ownSequenceNumbers.get(peerNodeId); if (seq == undefined) { seq = crypto.randomInt(256); } else { seq = (seq + 1) & 0xff; } this.ownSequenceNumbers.set(peerNodeId, seq); return seq; } public initializeMPAN(group: number): void { this.mpanStates.set(group, this.rng.generate(16)); } public nextMPAN(group: number): Buffer { if (!this.mpanStates.has(group)) { throw new ZWaveError( `The MPAN state has not been initialized for multicast group ${group}`, ZWaveErrorCodes.Security2CC_NotInitialized, ); } else if (!this.groupClasses.has(group)) { throw new ZWaveError( `Multicast group ${group} has not been assigned to a security class yet!`, ZWaveErrorCodes.Security2CC_NotInitialized, ); } const keys = this.networkKeys.get(this.groupClasses.get(group)!); if (!keys) { throw new ZWaveError( `The network keys for the security class of multicast group ${group} have not been set up yet!`, ZWaveErrorCodes.Security2CC_NotInitialized, ); } // Compute the next MPAN const stateN = this.mpanStates.get(group)!; const ret = encryptAES128ECB(stateN, keys.keyMPAN); // Increment the inner state increment(stateN); return ret; } }