diff --git a/src/adapter/deconz/adapter/deconzAdapter.ts b/src/adapter/deconz/adapter/deconzAdapter.ts index 8268c416fe..86931eed68 100644 --- a/src/adapter/deconz/adapter/deconzAdapter.ts +++ b/src/adapter/deconz/adapter/deconzAdapter.ts @@ -299,9 +299,8 @@ class DeconzAdapter extends Adapter { } } - // eslint-disable-next-line @typescript-eslint/no-unused-vars public async addInstallCode(ieeeAddress: string, key: Buffer): Promise { - return await Promise.reject(new Error('Add install code is not supported')); + await this.driver.writeLinkKey(ieeeAddress, ZSpec.Utils.aes128MmoHash(key)); } // eslint-disable-next-line @typescript-eslint/no-unused-vars diff --git a/src/adapter/deconz/driver/constants.ts b/src/adapter/deconz/driver/constants.ts index 0d7d2c2b60..3c5aa46ed7 100644 --- a/src/adapter/deconz/driver/constants.ts +++ b/src/adapter/deconz/driver/constants.ts @@ -15,6 +15,7 @@ const PARAM = { CHANNEL_MASK: 0x0a, APS_EXT_PAN_ID: 0x0b, NETWORK_KEY: 0x18, + LINK_KEY: 0x19, CHANNEL: 0x1c, PERMIT_JOIN: 0x21, WATCHDOG_TTL: 0x26, diff --git a/src/adapter/deconz/driver/driver.ts b/src/adapter/deconz/driver/driver.ts index 48013a410c..365678b6b6 100644 --- a/src/adapter/deconz/driver/driver.ts +++ b/src/adapter/deconz/driver/driver.ts @@ -1,5 +1,4 @@ /* istanbul ignore file */ -/* eslint-disable */ import events from 'events'; import net from 'net'; @@ -43,7 +42,7 @@ class Driver extends events.EventEmitter { private parser: Parser; private frameParserEvent = frameParserEvents; private seqNumber: number; - private timeoutResetTimeout: any; + private timeoutResetTimeout?: NodeJS.Timeout; private apsRequestFreeSlots: number; private apsDataConfirm: number; private apsDataIndication: number; @@ -61,7 +60,7 @@ class Driver extends events.EventEmitter { this.path = path; this.initialized = false; this.seqNumber = 0; - this.timeoutResetTimeout = null; + this.timeoutResetTimeout = undefined; this.apsRequestFreeSlots = 1; this.apsDataConfirm = 0; @@ -75,23 +74,22 @@ class Driver extends events.EventEmitter { this.writer = new Writer(); this.parser = new Parser(); - const that = this; setInterval(() => { - that.deviceStateRequest() - .then((result) => {}) - .catch((error) => {}); + this.deviceStateRequest() + .then(() => {}) + .catch(() => {}); }, 10000); setInterval( () => { - that.writeParameterRequest(0x26, 600) // reset watchdog // 10 minutes - .then((result) => {}) - .catch((error) => { + this.writeParameterRequest(0x26, 600) // reset watchdog // 10 minutes + .then(() => {}) + .catch(() => { //try again logger.debug('try again to reset watchdog', NS); - that.writeParameterRequest(0x26, 600) - .then((result) => {}) - .catch((error) => { + this.writeParameterRequest(0x26, 600) + .then(() => {}) + .catch(() => { logger.debug('warning watchdog was not reset', NS); }); }); @@ -100,8 +98,8 @@ class Driver extends events.EventEmitter { ); // 8 minutes this.onParsed = this.onParsed.bind(this); - this.frameParserEvent.on('receivedDataNotification', (data: number) => { - this.catchPromise(this.checkDeviceStatus(data)); + this.frameParserEvent.on('receivedDataNotification', async (data: number) => { + await this.catchPromise(this.checkDeviceStatus(data)); }); this.on('close', () => { @@ -117,12 +115,12 @@ class Driver extends events.EventEmitter { protected intervals: NodeJS.Timeout[] = []; - protected registerInterval(interval: NodeJS.Timeout) { + protected registerInterval(interval: NodeJS.Timeout): void { this.intervals.push(interval); } - protected catchPromise(val: any) { - return Promise.resolve(val).catch((err) => logger.debug(`Promise was caught with reason: ${err}`, NS)); + protected async catchPromise(val: Promise): Promise> { + return await Promise.resolve(val).catch((err) => logger.debug(`Promise was caught with reason: ${err}`, NS)); } public setDelay(delay: number): void { @@ -152,35 +150,34 @@ class Driver extends events.EventEmitter { this.HANDLE_DEVICE_STATUS_DELAY = 60; } - const that = this; this.registerInterval( setInterval(() => { - that.processQueue(); + this.processQueue(); }, this.PROCESS_QUEUES), ); // fire non aps requests this.registerInterval( - setInterval(() => { - this.catchPromise(that.processBusyQueue()); + setInterval(async () => { + await this.catchPromise(this.processBusyQueue()); }, this.PROCESS_QUEUES), ); // check timeouts for non aps requests this.registerInterval( - setInterval(() => { - this.catchPromise(that.processApsQueue()); + setInterval(async () => { + await this.catchPromise(this.processApsQueue()); }, this.PROCESS_QUEUES), ); // fire aps request this.registerInterval( setInterval(() => { - that.processApsBusyQueue(); + this.processApsBusyQueue(); }, this.PROCESS_QUEUES), ); // check timeouts for all open aps requests this.registerInterval( - setInterval(() => { - this.catchPromise(that.processApsConfirmIndQueue()); + setInterval(async () => { + this.processApsConfirmIndQueue(); }, this.PROCESS_QUEUES), ); // fire aps indications and confirms this.registerInterval( - setInterval(() => { - this.catchPromise(that.handleDeviceStatus()); + setInterval(async () => { + await this.catchPromise(this.handleDeviceStatus()); }, this.HANDLE_DEVICE_STATUS_DELAY), ); // query confirm and indication requests } @@ -193,7 +190,7 @@ class Driver extends events.EventEmitter { public async open(baudrate: number): Promise { this.currentBaudRate = baudrate; - return SocketPortUtils.isTcpPath(this.path) ? this.openSocketPort() : this.openSerialPort(baudrate); + return await (SocketPortUtils.isTcpPath(this.path) ? this.openSocketPort() : this.openSerialPort(baudrate)); } public openSerialPort(baudrate: number): Promise { @@ -236,24 +233,23 @@ class Driver extends events.EventEmitter { this.socketPort.pipe(this.parser); this.parser.on('parsed', this.onParsed); - return new Promise((resolve, reject): void => { - this.socketPort!.on('connect', function () { + return await new Promise((resolve, reject): void => { + this.socketPort!.on('connect', () => { logger.debug('Socket connected', NS); }); - const self = this; - this.socketPort!.on('ready', async function () { + this.socketPort!.on('ready', async () => { logger.debug('Socket ready', NS); - self.initialized = true; + this.initialized = true; resolve(); }); this.socketPort!.once('close', this.onPortClose); - this.socketPort!.on('error', function (error) { + this.socketPort!.on('error', (error) => { logger.error(`Socket error ${error}`, NS); reject(new Error(`Error while opening socket`)); - self.initialized = false; + this.initialized = false; }); this.socketPort!.connect(info.port, info.host); @@ -267,7 +263,13 @@ class Driver extends events.EventEmitter { this.serialPort.flush((): void => { this.serialPort!.close((error): void => { this.initialized = false; - error == null ? resolve() : reject(new Error(`Error while closing serialport '${error}'`)); + + if (error == null) { + resolve(); + } else { + reject(new Error(`Error while closing serialport '${error}'`)); + } + this.emit('close'); }); }); @@ -304,6 +306,10 @@ class Driver extends events.EventEmitter { }); } + public async writeLinkKey(ieeeAddress: string, hashedKey: Buffer): Promise { + await this.writeParameterRequest(PARAM.PARAM.Network.LINK_KEY, [...this.macAddrStringToArray(ieeeAddress), ...hashedKey]); + } + public readFirmwareVersionRequest(): Promise { const seqNumber = this.nextSeqNumber(); return new Promise((resolve, reject): void => { @@ -315,7 +321,7 @@ class Driver extends events.EventEmitter { }); } - private sendReadParameterRequest(parameterId: number, seqNumber: number) { + private sendReadParameterRequest(parameterId: number, seqNumber: number): void { /* command id, sequence number, 0, framelength(U16), payloadlength(U16), parameter id */ if (parameterId === PARAM.PARAM.Network.NETWORK_KEY) { this.sendRequest(Buffer.from([PARAM.PARAM.FrameType.ReadParameter, seqNumber, 0x00, 0x09, 0x00, 0x02, 0x00, parameterId, 0x00])); @@ -324,11 +330,11 @@ class Driver extends events.EventEmitter { } } - private sendWriteParameterRequest(parameterId: number, value: parameterT, seqNumber: number) { + private sendWriteParameterRequest(parameterId: number, value: parameterT, seqNumber: number): void { /* command id, sequence number, 0, framelength(U16), payloadlength(U16), parameter id, pameter */ let parameterLength = 0; if (parameterId === PARAM.PARAM.STK.Endpoint) { - let arrayParameterValue = value as number[]; + const arrayParameterValue = value as number[]; parameterLength = arrayParameterValue.length; } else { parameterLength = this.getLengthOfParameter(parameterId); @@ -408,20 +414,21 @@ class Driver extends events.EventEmitter { } } - private sendReadFirmwareVersionRequest(seqNumber: number) { + private sendReadFirmwareVersionRequest(seqNumber: number): void { /* command id, sequence number, 0, framelength(U16) */ this.sendRequest(Buffer.from([PARAM.PARAM.FrameType.ReadFirmwareVersion, seqNumber, 0x00, 0x09, 0x00, 0x00, 0x00, 0x00, 0x00])); } - private sendReadDeviceStateRequest(seqNumber: number) { + private sendReadDeviceStateRequest(seqNumber: number): void { /* command id, sequence number, 0, framelength(U16) */ this.sendRequest(Buffer.from([PARAM.PARAM.FrameType.ReadDeviceState, seqNumber, 0x00, 0x08, 0x00, 0x00, 0x00, 0x00])); } - private sendRequest(buffer: Buffer) { + private sendRequest(buffer: Buffer): void { const frame = Buffer.concat([buffer, this.calcCrc(buffer)]); const slipframe = slip.encode(frame); + // TODO: write not awaited? if (this.serialPort) { this.serialPort.write(slipframe, function (err) { if (err) { @@ -437,7 +444,7 @@ class Driver extends events.EventEmitter { } } - private processQueue() { + private processQueue(): void { if (queue.length === 0) { return; } @@ -482,7 +489,7 @@ class Driver extends events.EventEmitter { } } - private async processBusyQueue() { + private async processBusyQueue(): Promise { let i = busyQueue.length; while (i--) { const req: Request = busyQueue[i]; @@ -496,7 +503,7 @@ class Driver extends events.EventEmitter { // after a timeout the timeoutcounter will be reset after 1 min. If another timeout happen then the timeoutcounter // will not be reset clearTimeout(this.timeoutResetTimeout); - this.timeoutResetTimeout = null; + this.timeoutResetTimeout = undefined; this.resetTimeoutCounterAfter1min(); req.reject(new Error('TIMEOUT')); if (this.timeoutCounter >= 2) { @@ -525,13 +532,13 @@ class Driver extends events.EventEmitter { }); } - private sendChangeNetworkStateRequest(seqNumber: number, networkState: number) { + private sendChangeNetworkStateRequest(seqNumber: number, networkState: number): void { this.sendRequest(Buffer.from([PARAM.PARAM.NetworkState.CHANGE_NETWORK_STATE, seqNumber, 0x00, 0x06, 0x00, networkState])); } - private deviceStateRequest() { + private async deviceStateRequest(): Promise { const seqNumber = this.nextSeqNumber(); - return new Promise((resolve, reject): void => { + return await new Promise((resolve, reject): void => { //logger.debug(`DEVICE_STATE Request - seqNr: ${seqNumber}`, NS); const ts = 0; const commandId = PARAM.PARAM.FrameType.ReadDeviceState; @@ -540,7 +547,7 @@ class Driver extends events.EventEmitter { }); } - private async checkDeviceStatus(currentDeviceStatus: number) { + private async checkDeviceStatus(currentDeviceStatus: number): Promise { const networkState = currentDeviceStatus & 0x03; this.apsDataConfirm = (currentDeviceStatus >> 2) & 0x01; this.apsDataIndication = (currentDeviceStatus >> 3) & 0x01; @@ -562,12 +569,12 @@ class Driver extends events.EventEmitter { ); } - private async handleDeviceStatus() { + private async handleDeviceStatus(): Promise { if (this.apsDataConfirm === 1) { try { logger.debug('query aps data confirm', NS); this.apsDataConfirm = 0; - const x = await this.querySendDataStateRequest(); + await this.querySendDataStateRequest(); } catch (error) { // @ts-expect-error TODO: this doesn't look right? if (error.status === 5) { @@ -579,7 +586,7 @@ class Driver extends events.EventEmitter { try { logger.debug('query aps data indication', NS); this.apsDataIndication = 0; - const x = await this.readReceivedDataRequest(); + await this.readReceivedDataRequest(); } catch (error) { // @ts-expect-error TODO: this doesn't look right? if (error.status === 5) { @@ -610,7 +617,6 @@ class Driver extends events.EventEmitter { return new Promise((resolve, reject): void => { //logger.debug(`push enqueue send data request to apsQueue. seqNr: ${seqNumber}`, NS); const ts = 0; - const requestId = request.requestId; const commandId = PARAM.PARAM.APS.DATA_REQUEST; const req: Request = {commandId, seqNumber, request, resolve, reject, ts}; apsQueue.push(req); @@ -629,7 +635,7 @@ class Driver extends events.EventEmitter { }); } - private async processApsQueue() { + private async processApsQueue(): Promise { if (apsQueue.length === 0) { return; } @@ -667,7 +673,7 @@ class Driver extends events.EventEmitter { } } - private async processApsConfirmIndQueue() { + private processApsConfirmIndQueue(): void { if (apsConfirmIndQueue.length === 0) { return; } @@ -685,7 +691,7 @@ class Driver extends events.EventEmitter { if (this.DELAY === 0) { this.sendReadReceivedDataRequest(req.seqNumber); } else { - await this.sendReadReceivedDataRequest(req.seqNumber); + this.sendReadReceivedDataRequest(req.seqNumber); } break; case PARAM.PARAM.APS.DATA_CONFIRM: @@ -693,7 +699,7 @@ class Driver extends events.EventEmitter { if (this.DELAY === 0) { this.sendQueryDataStateRequest(req.seqNumber); } else { - await this.sendQueryDataStateRequest(req.seqNumber); + this.sendQueryDataStateRequest(req.seqNumber); } break; default: @@ -702,18 +708,18 @@ class Driver extends events.EventEmitter { } } - private sendQueryDataStateRequest(seqNumber: number) { + private sendQueryDataStateRequest(seqNumber: number): void { logger.debug(`DATA_CONFIRM - sending data state request - SeqNr. ${seqNumber}`, NS); this.sendRequest(Buffer.from([PARAM.PARAM.APS.DATA_CONFIRM, seqNumber, 0x00, 0x07, 0x00, 0x00, 0x00])); } - private sendReadReceivedDataRequest(seqNumber: number) { + private sendReadReceivedDataRequest(seqNumber: number): void { logger.debug(`DATA_INDICATION - sending read data request - SeqNr. ${seqNumber}`, NS); // payloadlength = 0, flag = none this.sendRequest(Buffer.from([PARAM.PARAM.APS.DATA_INDICATION, seqNumber, 0x00, 0x08, 0x00, 0x01, 0x00, 0x01])); } - private sendEnqueueSendDataRequest(request: ApsDataRequest, seqNumber: number) { + private sendEnqueueSendDataRequest(request: ApsDataRequest, seqNumber: number): void { const payloadLength = 12 + (request.destAddrMode === PARAM.PARAM.addressMode.GROUP_ADDR ? 2 : request.destAddrMode === PARAM.PARAM.addressMode.NWK_ADDR ? 3 : 9) + @@ -770,7 +776,7 @@ class Driver extends events.EventEmitter { ); } - private processApsBusyQueue() { + private processApsBusyQueue(): void { let i = apsBusyQueue.length; while (i--) { const req = apsBusyQueue[i]; @@ -807,7 +813,7 @@ class Driver extends events.EventEmitter { addr = '0' + addr; } } - let result: number[] = new Array(); + const result = new Array(); let y = 0; for (let i = 0; i < 8; i++) { result[i] = parseInt(addr.substr(y, 2), 16); @@ -871,11 +877,11 @@ class Driver extends events.EventEmitter { return new Promise((resolve) => setTimeout(resolve, ms)); } - private resetTimeoutCounterAfter1min() { - if (this.timeoutResetTimeout === null) { + private resetTimeoutCounterAfter1min(): void { + if (this.timeoutResetTimeout === undefined) { this.timeoutResetTimeout = setTimeout(() => { this.timeoutCounter = 0; - this.timeoutResetTimeout = null; + this.timeoutResetTimeout = undefined; }, 60000); } } diff --git a/src/adapter/deconz/driver/writer.ts b/src/adapter/deconz/driver/writer.ts index 1de397ec9c..a7ba21f1c9 100644 --- a/src/adapter/deconz/driver/writer.ts +++ b/src/adapter/deconz/driver/writer.ts @@ -1,9 +1,7 @@ /* istanbul ignore file */ -/* eslint-disable */ import * as stream from 'stream'; -// @ts-ignore import slip from 'slip'; import {logger} from '../../../utils/logger'; diff --git a/src/adapter/ember/adapter/emberAdapter.ts b/src/adapter/ember/adapter/emberAdapter.ts index 428ffd2119..4d097cf2cb 100644 --- a/src/adapter/ember/adapter/emberAdapter.ts +++ b/src/adapter/ember/adapter/emberAdapter.ts @@ -16,8 +16,6 @@ import * as ZdoTypes from '../../../zspec/zdo/definition/tstypes'; import {DeviceJoinedPayload, DeviceLeavePayload, ZclPayload} from '../../events'; import { EMBER_HIGH_RAM_CONCENTRATOR, - EMBER_INSTALL_CODE_CRC_SIZE, - EMBER_INSTALL_CODE_SIZES, EMBER_LOW_RAM_CONCENTRATOR, EMBER_MIN_BROADCAST_ADDRESS, INTERPAN_APS_FRAME_TYPE, @@ -70,8 +68,8 @@ import { SecManContext, SecManKey, } from '../types'; -import {aesMmoHashInit, initNetworkCache, initSecurityManagerContext} from '../utils/initters'; -import {halCommonCrc16, highByte, highLowToInt, lowByte, lowHighBytes} from '../utils/math'; +import {initNetworkCache, initSecurityManagerContext} from '../utils/initters'; +import {lowHighBytes} from '../utils/math'; import {FIXED_ENDPOINTS} from './endpoints'; import {EmberOneWaitress, OneWaitressEvents} from './oneWaitress'; @@ -1192,19 +1190,14 @@ export class EmberAdapter extends Adapter { // Rather than give the real link key, the backup contains a hashed version of the key. // This is done to prevent a compromise of the backup data from compromising the current link keys. // This is per the Smart Energy spec. - const [hashStatus, hashedKey] = await this.emberAesHashSimple(plaintextKey.contents); - - if (hashStatus === SLStatus.OK) { - keyList.push({ - deviceEui64: context.eui64, - key: {contents: hashedKey}, - outgoingFrameCounter: apsKeyMeta.outgoingFrameCounter, - incomingFrameCounter: apsKeyMeta.incomingFrameCounter, - }); - } else { - // this should never happen? - logger.error(`[BACKUP] Failed to hash link key at index ${i} with status=${SLStatus[hashStatus]}. Omitting from backup.`, NS); - } + const hashedKey = ZSpec.Utils.aes128MmoHash(plaintextKey.contents); + + keyList.push({ + deviceEui64: context.eui64, + key: {contents: hashedKey}, + outgoingFrameCounter: apsKeyMeta.outgoingFrameCounter, + incomingFrameCounter: apsKeyMeta.incomingFrameCounter, + }); } } @@ -1494,26 +1487,6 @@ export class EmberAdapter extends Adapter { return status; } - /** - * This is a convenience method when the hash data is less than 255 - * bytes. It inits, updates, and finalizes the hash in one function call. - * - * @param data const uint8_t* The data to hash. Expected of valid length (as in, not larger alloc) - * - * @returns An ::SLStatus value indicating EMBER_SUCCESS if the hash was - * calculated successfully. EMBER_INVALID_CALL if the block size is not a - * multiple of 16 bytes, and EMBER_INDEX_OUT_OF_RANGE is returned when the - * data exceeds the maximum limits of the hash function. - * @returns result uint8_t* The location where the result of the hash will be written. - */ - private async emberAesHashSimple(data: Buffer): Promise<[SLStatus, result: Buffer]> { - const context = aesMmoHashInit(); - - const [status, reContext] = await this.ezsp.ezspAesMmoHash(context, true, data); - - return [status, reContext?.result]; - } - /** * Set the trust center policy bitmask using decision. * @param decision @@ -1716,43 +1689,10 @@ export class EmberAdapter extends Adapter { // queued public async addInstallCode(ieeeAddress: string, key: Buffer): Promise { - // codes with CRC, check CRC before sending to NCP, otherwise let NCP handle - if (EMBER_INSTALL_CODE_SIZES.indexOf(key.length) !== -1) { - // Reverse the bits in a byte (uint8_t) - const reverse = (b: number): number => { - return (((((b * 0x0802) & 0x22110) | ((b * 0x8020) & 0x88440)) * 0x10101) >> 16) & 0xff; - }; - let crc = 0xffff; // uint16_t - - // Compute the CRC and verify that it matches. - // The bit reversals, byte swap, and ones' complement are due to differences between halCommonCrc16 and the Smart Energy version. - for (let index = 0; index < key.length - EMBER_INSTALL_CODE_CRC_SIZE; index++) { - crc = halCommonCrc16(reverse(key[index]), crc); - } - - crc = ~highLowToInt(reverse(lowByte(crc)), reverse(highByte(crc))) & 0xffff; - - if ( - key[key.length - EMBER_INSTALL_CODE_CRC_SIZE] !== lowByte(crc) || - key[key.length - EMBER_INSTALL_CODE_CRC_SIZE + 1] !== highByte(crc) - ) { - throw new Error(`[ADD INSTALL CODE] Failed for '${ieeeAddress}'; invalid code CRC.`); - } else { - logger.debug(`[ADD INSTALL CODE] CRC validated for '${ieeeAddress}'.`, NS); - } - } - return await this.queue.execute(async () => { - // Compute the key from the install code and CRC. - const [aesStatus, keyContents] = await this.emberAesHashSimple(key); - - if (aesStatus !== SLStatus.OK) { - throw new Error(`[ADD INSTALL CODE] Failed AES hash for '${ieeeAddress}' with status=${SLStatus[aesStatus]}.`); - } - // Add the key to the transient key table. // This will be used while the DUT joins. - const impStatus = await this.ezsp.ezspImportTransientKey(ieeeAddress as EUI64, {contents: keyContents}); + const impStatus = await this.ezsp.ezspImportTransientKey(ieeeAddress as EUI64, {contents: ZSpec.Utils.aes128MmoHash(key)}); if (impStatus == SLStatus.OK) { logger.debug(`[ADD INSTALL CODE] Success for '${ieeeAddress}'.`, NS); diff --git a/src/adapter/ember/consts.ts b/src/adapter/ember/consts.ts index 38db6e595b..37cb3d0c68 100644 --- a/src/adapter/ember/consts.ts +++ b/src/adapter/ember/consts.ts @@ -77,23 +77,6 @@ export const EMBER_HIGH_RAM_CONCENTRATOR = 0xfff9; /** The short address of the trust center. This address never changes dynamically. */ export const EMBER_TRUST_CENTER_NODE_ID = 0x0000; -/** The size of the CRC that is appended to an installation code. */ -export const EMBER_INSTALL_CODE_CRC_SIZE = 2; - -/** The number of sizes of acceptable installation codes used in Certificate Based Key Establishment (CBKE). */ -export const EMBER_NUM_INSTALL_CODE_SIZES = 4; - -/** - * Various sizes of valid installation codes that are stored in the manufacturing tokens. - * Note that each size includes 2 bytes of CRC appended to the end of the installation code. - */ -export const EMBER_INSTALL_CODE_SIZES = [ - 6 + EMBER_INSTALL_CODE_CRC_SIZE, - 8 + EMBER_INSTALL_CODE_CRC_SIZE, - 12 + EMBER_INSTALL_CODE_CRC_SIZE, - 16 + EMBER_INSTALL_CODE_CRC_SIZE, -]; - /** * Default value for context's PSA algorithm permission (CCM* with 4 byte tag). * Only used by NCPs with secure key storage; define is mirrored here to allow diff --git a/src/controller/controller.ts b/src/controller/controller.ts index dc9612a636..1db602faed 100644 --- a/src/controller/controller.ts +++ b/src/controller/controller.ts @@ -271,7 +271,16 @@ class Controller extends events.EventEmitter { // match valid else asserted above key = Buffer.from(key.match(/.{1,2}/g)!.map((d) => parseInt(d, 16))); - await this.adapter.addInstallCode(ieeeAddr, key); + // will throw if code cannot be fixed and is invalid + const [adjustedKey, adjusted] = ZSpec.Utils.checkInstallCode(key, true); + + if (adjusted) { + logger.info(`Install code was adjusted for reason '${adjusted}'.`, NS); + } + + logger.info(`Adding install code for ${ieeeAddr}.`, NS); + + await this.adapter.addInstallCode(ieeeAddr, adjustedKey); } public async permitJoin(time: number, device?: Device): Promise { diff --git a/src/zspec/consts.ts b/src/zspec/consts.ts index 9c47c0c29b..12422817a8 100644 --- a/src/zspec/consts.ts +++ b/src/zspec/consts.ts @@ -72,3 +72,13 @@ export const PAN_ID_SIZE = 2; export const EXTENDED_PAN_ID_SIZE = 8; /** Size of an encryption key in bytes. */ export const DEFAULT_ENCRYPTION_KEY_SIZE = 16; +/** Size of a AES-128-MMO (Matyas-Meyer-Oseas) block in bytes. */ +export const AES_MMO_128_BLOCK_SIZE = 16; +/** + * Valid install code sizes, including `INSTALL_CODE_CRC_SIZE`. + * + * NOTE: 18 is now standard, first for iterations, order after is important (8 before 10)! + */ +export const INSTALL_CODE_SIZES: ReadonlyArray = [18, 8, 10, 14]; +/** Size of the CRC appended to install codes. */ +export const INSTALL_CODE_CRC_SIZE = 2; diff --git a/src/zspec/utils.ts b/src/zspec/utils.ts index 0b2f80405b..8b17f453b8 100644 --- a/src/zspec/utils.ts +++ b/src/zspec/utils.ts @@ -1,6 +1,8 @@ import type {EUI64} from './tstypes'; -import {ALL_802_15_4_CHANNELS} from './consts'; +import {createCipheriv} from 'crypto'; + +import {AES_MMO_128_BLOCK_SIZE, ALL_802_15_4_CHANNELS, INSTALL_CODE_CRC_SIZE, INSTALL_CODE_SIZES} from './consts'; import {BroadcastAddress} from './enums'; /** @@ -49,3 +51,255 @@ export const eui64LEBufferToHex = (eui64LEBuf: Buffer): EUI64 => `0x${Buffer.fro * Represent a big endian buffer in `0x...` form */ export const eui64BEBufferToHex = (eui64BEBuf: Buffer): EUI64 => `0x${eui64BEBuf.toString('hex')}`; + +/** + * Calculate the CRC 8, 16 or 32 for the given data. + * + * @see https://www.crccalc.com/ + * + * @param data + * @param length CRC Length + * @param poly Polynomial + * @param crc Initialization value + * @param xorOut Final XOR value + * @param refIn Reflected In + * @param refOut Reflected Out + * @returns The calculated CRC + * + * NOTE: This is not exported for test coverage reasons (large number of combinations possible, many unused). + * Specific, needed, algorithms should be defined as exported wrappers below, and coverage added for them. + */ +/* istanbul ignore next */ +function calcCRC( + data: number[] | Uint8Array | Buffer, + length: 8 | 16 | 32, + poly: number, + crc: number = 0, + xorOut: number = 0, + refIn: boolean = false, + refOut: boolean = false, +): number { + // https://web.archive.org/web/20150226083354/http://leetcode.com/2011/08/reverse-bits.html + const reflect = (x: number, size: 8 | 16 | 32): number => { + if (size === 8) { + x = ((x & 0x55) << 1) | ((x & 0xaa) >> 1); + x = ((x & 0x33) << 2) | ((x & 0xcc) >> 2); + x = ((x & 0x0f) << 4) | ((x & 0xf0) >> 4); + } else if (size === 16) { + x = ((x & 0x5555) << 1) | ((x & 0xaaaa) >> 1); + x = ((x & 0x3333) << 2) | ((x & 0xcccc) >> 2); + x = ((x & 0x0f0f) << 4) | ((x & 0xf0f0) >> 4); + x = ((x & 0x00ff) << 8) | ((x & 0xff00) >> 8); + } /* if (size === 32) */ else { + x = ((x & 0x55555555) << 1) | ((x & 0xaaaaaaaa) >> 1); + x = ((x & 0x33333333) << 2) | ((x & 0xcccccccc) >> 2); + x = ((x & 0x0f0f0f0f) << 4) | ((x & 0xf0f0f0f0) >> 4); + x = ((x & 0x00ff00ff) << 8) | ((x & 0xff00ff00) >> 8); + x = ((x & 0x0000ffff) << 16) | ((x & 0xffff0000) >> 16); + } + + return x; + }; + + poly = (1 << length) | poly; + + for (let byte of data) { + if (refIn) { + byte = reflect(byte, 8); + } + + crc ^= byte << (length - 8); + + for (let i = 0; i < 8; i++) { + crc <<= 1; + + if (crc & (1 << length)) { + crc ^= poly; + } + } + } + + if (refOut) { + crc = reflect(crc, length); + } + + return crc ^ xorOut; +} + +/** + * CRC-16/X-25 + * aka CRC-16/IBM-SDLC + * aka CRC-16/ISO-HDLC + * aka CRC-16/ISO-IEC-14443-3-B + * aka CRC-B + * aka X-25 + * + * Shortcut for `calcCRC(data, 16, 0x1021, 0xFFFF, 0xFFFF, true, true)` + * + * Used for Install Codes - see Document 13-0402-13 - 10.1 + */ +export function crc16X25(data: number[] | Uint8Array | Buffer): number { + return calcCRC(data, 16, 0x1021, 0xffff, 0xffff, true, true); +} + +/** + * CRC-16/XMODEM + * aka CRC-16/ACORN + * aka CRC-16/LTE + * aka CRC-16/V-41-MSB + * aka XMODEM + * aka ZMODEM + * + * Shortcut for `calcCRC(data, 16, 0x1021)` + * + * Used for XMODEM transfers, often involved in ZigBee environments + */ +export function crc16XMODEM(data: number[] | Uint8Array | Buffer): number { + return calcCRC(data, 16, 0x1021); +} + +/** + * CRC-16/CCITT + * aka CRC-16/KERMIT + * aka CRC-16/BLUETOOTH + * aka CRC-16/CCITT-TRUE + * aka CRC-16/V-41-LSB + * aka CRC-CCITT + * aka KERMIT + * + * Shortcut for `calcCRC(data, 16, 0x1021, 0x0000, 0x0000, true, true)` + */ +export function crc16CCITT(data: number[] | Uint8Array | Buffer): number { + return calcCRC(data, 16, 0x1021, 0x0000, 0x0000, true, true); +} + +/** + * CRC-16/CCITT-FALSE + * aka CRC-16/IBM-3740 + * aka CRC-16/AUTOSAR + * + * Shortcut for `calcCRC(data, 16, 0x1021, 0xffff)` + */ +export function crc16CCITTFALSE(data: number[] | Uint8Array | Buffer): number { + return calcCRC(data, 16, 0x1021, 0xffff); +} + +/** + * AES-128-MMO (Matyas-Meyer-Oseas) hashing (using node 'crypto' built-in with 'aes-128-ecb') + * + * Used for Install Codes - see Document 13-0402-13 - 10.1 + */ +export function aes128MmoHash(data: Buffer): Buffer { + const update = (result: Buffer, data: Buffer, dataSize: number): boolean => { + while (dataSize >= AES_MMO_128_BLOCK_SIZE) { + const cipher = createCipheriv('aes-128-ecb', result, null); + const block = data.subarray(0, AES_MMO_128_BLOCK_SIZE); + const encryptedBlock = Buffer.concat([cipher.update(block), cipher.final()]); + + // XOR encrypted and plaintext + for (let i = 0; i < AES_MMO_128_BLOCK_SIZE; i++) { + result[i] = encryptedBlock[i] ^ block[i]; + } + + data = data.subarray(AES_MMO_128_BLOCK_SIZE); + dataSize -= AES_MMO_128_BLOCK_SIZE; + } + + return true; + }; + + const hashResult = Buffer.alloc(AES_MMO_128_BLOCK_SIZE); + const temp = Buffer.alloc(AES_MMO_128_BLOCK_SIZE); + let remainingLength = data.length; + let position = 0; + + for (position; remainingLength >= AES_MMO_128_BLOCK_SIZE; ) { + update(hashResult, data.subarray(position, position + AES_MMO_128_BLOCK_SIZE), data.length); + + position += AES_MMO_128_BLOCK_SIZE; + remainingLength -= AES_MMO_128_BLOCK_SIZE; + } + + for (let i = 0; i < remainingLength; i++) { + temp[i] = data[position + i]; + } + + // per the spec, concatenate a 1 bit followed by all zero bits + temp[remainingLength] = 0x80; + + // if appending the bit string will push us beyond the 16-byte boundary, hash that block and append another 16-byte block + if (AES_MMO_128_BLOCK_SIZE - remainingLength < 3) { + update(hashResult, temp, AES_MMO_128_BLOCK_SIZE); + temp.fill(0); + } + + temp[AES_MMO_128_BLOCK_SIZE - 2] = (data.length >> 5) & 0xff; + temp[AES_MMO_128_BLOCK_SIZE - 1] = (data.length << 3) & 0xff; + + update(hashResult, temp, AES_MMO_128_BLOCK_SIZE); + + const result = Buffer.alloc(AES_MMO_128_BLOCK_SIZE); + + for (let i = 0; i < AES_MMO_128_BLOCK_SIZE; i++) { + result[i] = hashResult[i]; + } + + return result; +} + +/** + * Check if install code (little-endian) is valid, and if not, and requested, fix it. + * + * WARNING: Due to conflicting sizes between 8-length code with invalid CRC, and 10-length code missing CRC, given 8-length codes are always assumed to be 8-length code with invalid CRC (most probable scenario). + * + * @param code The code to check. Reference is not modified by this procedure but is returned when code was valid, as `outCode`. + * @param adjust If false, throws if the install code is invalid, otherwise try to fix it (CRC) + * @returns + * - The adjusted code, or `code` if not adjusted. + * - If adjust is false, undefined, otherwise, the reason why the code needed adjusting or undefined if not. + * - Throws when adjust=false and invalid, or cannot fix. + */ +export function checkInstallCode(code: Buffer, adjust: boolean = true): [outCode: Buffer, adjusted: string | undefined] { + const crcLowByteIndex = code.length - INSTALL_CODE_CRC_SIZE; + const crcHighByteIndex = code.length - INSTALL_CODE_CRC_SIZE + 1; + + for (const codeSize of INSTALL_CODE_SIZES) { + if (code.length === codeSize) { + // install code has CRC, check if valid, if not, replace it + const crc = crc16X25(code.subarray(0, -2)); + const crcHighByte = (crc >> 8) & 0xff; + const crcLowByte = crc & 0xff; + + if (code[crcLowByteIndex] !== crcLowByte || code[crcHighByteIndex] !== crcHighByte) { + // see WARNING above, 8 is smallest valid length, so always ends up here + if (adjust) { + const outCode = Buffer.from(code); + outCode[crcLowByteIndex] = crcLowByte; + outCode[crcHighByteIndex] = crcHighByte; + + return [outCode, 'invalid CRC']; + } else { + throw new Error(`Install code ${code.toString('hex')} failed CRC validation`); + } + } + + return [code, undefined]; + } else if (code.length === codeSize - INSTALL_CODE_CRC_SIZE) { + if (adjust) { + // install code is missing CRC + const crc = crc16X25(code); + const outCode = Buffer.alloc(code.length + INSTALL_CODE_CRC_SIZE); + + code.copy(outCode, 0); + outCode.writeUInt16LE(crc, code.length); + + return [outCode, 'missing CRC']; + } else { + throw new Error(`Install code ${code.toString('hex')} failed CRC validation`); + } + } + } + + // never returned from within the above loop + throw new Error(`Install code ${code.toString('hex')} has invalid size`); +} diff --git a/test/adapter/ember/emberAdapter.test.ts b/test/adapter/ember/emberAdapter.test.ts index 3b4389a07c..bca411cf2c 100644 --- a/test/adapter/ember/emberAdapter.test.ts +++ b/test/adapter/ember/emberAdapter.test.ts @@ -31,7 +31,6 @@ import {EzspConfigId, EzspDecisionBitmask, EzspEndpointFlag, EzspPolicyId, EzspV import {EmberEzspEventMap, Ezsp} from '../../../src/adapter/ember/ezsp/ezsp'; import {EzspError} from '../../../src/adapter/ember/ezspError'; import { - EmberAesMmoHashContext, EmberApsFrame, EmberMulticastTableEntry, EmberNetworkInitStruct, @@ -231,10 +230,6 @@ const mockEzspGetVersionStruct = jest.fn().mockResolvedValue([ const mockEzspSetConfigurationValue = jest.fn().mockResolvedValue(SLStatus.OK); const mockEzspSetValue = jest.fn().mockResolvedValue(SLStatus.OK); const mockEzspSetPolicy = jest.fn().mockResolvedValue(SLStatus.OK); -const mockEzspAesMmoHash = jest.fn().mockImplementation((context: EmberAesMmoHashContext, finalize: boolean, data: Buffer) => [ - SLStatus.OK, - {result: data, length: data.length} as EmberAesMmoHashContext, // echo data -]); const mockEzspPermitJoining = jest.fn().mockImplementation((duration: number) => { setTimeout(async () => { mockEzspEmitter.emit('stackStatus', duration > 0 ? SLStatus.ZIGBEE_NETWORK_OPENED : SLStatus.ZIGBEE_NETWORK_CLOSED); @@ -319,7 +314,6 @@ jest.mock('../../../src/adapter/ember/ezsp/ezsp', () => ({ ezspSetConfigurationValue: mockEzspSetConfigurationValue, ezspSetValue: mockEzspSetValue, ezspSetPolicy: mockEzspSetPolicy, - ezspAesMmoHash: mockEzspAesMmoHash, ezspPermitJoining: mockEzspPermitJoining, ezspSendBroadcast: mockEzspSendBroadcast, ezspSendUnicast: mockEzspSendUnicast, @@ -374,7 +368,6 @@ const ezspMocks = [ mockEzspSetConfigurationValue, mockEzspSetValue, mockEzspSetPolicy, - mockEzspAesMmoHash, mockEzspPermitJoining, mockEzspSendBroadcast, mockEzspSendUnicast, @@ -1743,6 +1736,7 @@ describe('Ember Adapter Layer', () => { psaKeyAlgPermission: 0, }; const k1 = Buffer.from([1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16]); + const k1Hashed = ZSpec.Utils.aes128MmoHash(k1); const k1Metadata: SecManAPSKeyMetadata = { bitmask: EmberKeyStructBitmask.HAS_INCOMING_FRAME_COUNTER | EmberKeyStructBitmask.HAS_OUTGOING_FRAME_COUNTER, outgoingFrameCounter: 1, @@ -1759,6 +1753,7 @@ describe('Ember Adapter Layer', () => { psaKeyAlgPermission: 0, }; const k2 = Buffer.from([2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17]); + const k2Hashed = ZSpec.Utils.aes128MmoHash(k2); const k2Metadata: SecManAPSKeyMetadata = { bitmask: EmberKeyStructBitmask.HAS_INCOMING_FRAME_COUNTER | EmberKeyStructBitmask.HAS_OUTGOING_FRAME_COUNTER, outgoingFrameCounter: 10, @@ -1775,6 +1770,7 @@ describe('Ember Adapter Layer', () => { psaKeyAlgPermission: 0, }; const k3 = Buffer.from([3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18]); + const k3Hashed = ZSpec.Utils.aes128MmoHash(k3); const k3Metadata: SecManAPSKeyMetadata = { bitmask: EmberKeyStructBitmask.HAS_INCOMING_FRAME_COUNTER | EmberKeyStructBitmask.HAS_OUTGOING_FRAME_COUNTER, outgoingFrameCounter: 100, @@ -1794,19 +1790,19 @@ describe('Ember Adapter Layer', () => { expect(keys).toStrictEqual([ { deviceEui64: k1Context.eui64, - key: {contents: k1}, + key: {contents: k1Hashed}, outgoingFrameCounter: k1Metadata.outgoingFrameCounter, incomingFrameCounter: k1Metadata.incomingFrameCounter, } as LinkKeyBackupData, { deviceEui64: k2Context.eui64, - key: {contents: k2}, + key: {contents: k2Hashed}, outgoingFrameCounter: k2Metadata.outgoingFrameCounter, incomingFrameCounter: k2Metadata.incomingFrameCounter, } as LinkKeyBackupData, { deviceEui64: k3Context.eui64, - key: {contents: k3}, + key: {contents: k3Hashed}, outgoingFrameCounter: k3Metadata.outgoingFrameCounter, incomingFrameCounter: k3Metadata.incomingFrameCounter, } as LinkKeyBackupData, @@ -1826,36 +1822,6 @@ describe('Ember Adapter Layer', () => { await expect(adapter.exportLinkKeys()).rejects.toThrow(`[BACKUP] Failed to retrieve key table size from NCP with status=FAIL.`); }); - it('Fails to export link keys due to failed AES hashing', async () => { - const k1Context: SecManContext = { - coreKeyType: SecManKeyType.APP_LINK, - keyIndex: 0, - derivedType: SecManDerivedKeyType.NONE, - eui64: '0x1122334455667788', - multiNetworkIndex: 0, - flags: SecManFlag.EUI_IS_VALID | SecManFlag.KEY_INDEX_IS_VALID, - psaKeyAlgPermission: 0, - }; - const k1 = Buffer.from([1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16]); - const k1Metadata: SecManAPSKeyMetadata = { - bitmask: EmberKeyStructBitmask.HAS_INCOMING_FRAME_COUNTER | EmberKeyStructBitmask.HAS_OUTGOING_FRAME_COUNTER, - outgoingFrameCounter: 1, - incomingFrameCounter: 2, - ttlInSeconds: 0, - }; - - mockEzspGetConfigurationValue.mockResolvedValueOnce([SLStatus.OK, 1]); - mockEzspExportLinkKeyByIndex.mockResolvedValueOnce([SLStatus.OK, k1Context, {contents: k1} as SecManKey, k1Metadata]); - mockEzspAesMmoHash.mockResolvedValueOnce([SLStatus.FAIL, {result: k1, length: k1.length} as EmberAesMmoHashContext]); - - await adapter.exportLinkKeys(); - - expect(loggerSpies.error).toHaveBeenCalledWith( - `[BACKUP] Failed to hash link key at index 0 with status=FAIL. Omitting from backup.`, - 'zh:ember', - ); - }); - it('Imports link keys', async () => { const k1Context: SecManContext = { coreKeyType: SecManKeyType.APP_LINK, @@ -2289,51 +2255,23 @@ describe('Ember Adapter Layer', () => { expect(mockEzspGetNetworkParameters).toHaveBeenCalledTimes(1); }); - it('Adapter impl: addInstallCode without local CRC validation', async () => { - await expect(adapter.addInstallCode('0x1122334455667788', Buffer.alloc(16))).resolves.toStrictEqual(undefined); - expect(mockEzspAesMmoHash).toHaveBeenCalledTimes(1); - expect(mockEzspImportTransientKey).toHaveBeenCalledTimes(1); - expect(loggerSpies.debug).toHaveBeenCalledWith(`[ADD INSTALL CODE] Success for '0x1122334455667788'.`, 'zh:ember'); - }); - - it('Adapter impl: addInstallCode with local CRC validation', async () => { + it('Adapter impl: addInstallCode', async () => { await expect( adapter.addInstallCode('0x1122334455667788', Buffer.from('DD7ED5CDAA8E2C708B67D2B1573DB6843A5F', 'hex')), ).resolves.toStrictEqual(undefined); - expect(mockEzspAesMmoHash).toHaveBeenCalledTimes(1); expect(mockEzspImportTransientKey).toHaveBeenCalledTimes(1); - expect(loggerSpies.debug).toHaveBeenCalledWith(`[ADD INSTALL CODE] CRC validated for '0x1122334455667788'.`, 'zh:ember'); expect(loggerSpies.debug).toHaveBeenCalledWith(`[ADD INSTALL CODE] Success for '0x1122334455667788'.`, 'zh:ember'); }); - it('Adapter impl: throw when addInstallCode fails AES hashing', async () => { - mockEzspAesMmoHash.mockResolvedValueOnce([SLStatus.FAIL, Buffer.alloc(16)]); - - await expect(adapter.addInstallCode('0x1122334455667788', Buffer.alloc(16))).rejects.toThrow( - `[ADD INSTALL CODE] Failed AES hash for '0x1122334455667788' with status=FAIL.`, - ); - expect(mockEzspAesMmoHash).toHaveBeenCalledTimes(1); - expect(mockEzspImportTransientKey).toHaveBeenCalledTimes(0); - }); - it('Adapter impl: throw when addInstallCode fails import transient key', async () => { mockEzspImportTransientKey.mockResolvedValueOnce(SLStatus.FAIL); await expect(adapter.addInstallCode('0x1122334455667788', Buffer.alloc(16))).rejects.toThrow( `[ADD INSTALL CODE] Failed for '0x1122334455667788' with status=FAIL.`, ); - expect(mockEzspAesMmoHash).toHaveBeenCalledTimes(1); expect(mockEzspImportTransientKey).toHaveBeenCalledTimes(1); }); - it('Adapter impl: throw when addInstallCode fails local CRC validation', async () => { - await expect(adapter.addInstallCode('0x1122334455667788', Buffer.alloc(18))).rejects.toThrow( - `[ADD INSTALL CODE] Failed for '0x1122334455667788'; invalid code CRC.`, - ); - expect(mockEzspAesMmoHash).toHaveBeenCalledTimes(0); - expect(mockEzspImportTransientKey).toHaveBeenCalledTimes(0); - }); - it('Adapter impl: waitFor', async () => { const waiter = adapter.waitFor(1234, 1, Zcl.FrameType.GLOBAL, Zcl.Direction.CLIENT_TO_SERVER, 10, 0, 1, 15000); const spyCancel = jest.spyOn(waiter, 'cancel'); diff --git a/test/controller.test.ts b/test/controller.test.ts index 89f7036418..a014acbda1 100755 --- a/test/controller.test.ts +++ b/test/controller.test.ts @@ -2295,15 +2295,16 @@ describe('Controller', () => { ); }); - it('Add install code 16 byte', async () => { + it('Add install code 16 byte - missing CRC is appended', async () => { await controller.start(); const code = 'RB01SG0D836591B3CC0010000000000000000000000D6F00179F2BC9DLKD0F471C9BBA2C0208608E91EED17E2B1'; await controller.addInstallCode(code); expect(mockAddInstallCode).toHaveBeenCalledTimes(1); expect(mockAddInstallCode).toHaveBeenCalledWith( '0x000D6F00179F2BC9', - Buffer.from([0xd0, 0xf4, 0x71, 0xc9, 0xbb, 0xa2, 0xc0, 0x20, 0x86, 0x08, 0xe9, 0x1e, 0xed, 0x17, 0xe2, 0xb1]), + Buffer.from([0xd0, 0xf4, 0x71, 0xc9, 0xbb, 0xa2, 0xc0, 0x20, 0x86, 0x08, 0xe9, 0x1e, 0xed, 0x17, 0xe2, 0xb1, 0x9a, 0xec]), ); + expect(mockLogger.info).toHaveBeenCalledWith(`Install code was adjusted for reason 'missing CRC'.`, 'zh:controller'); }); it('Add install code Aqara', async () => { @@ -2328,6 +2329,18 @@ describe('Controller', () => { ); }); + it('Add install code invalid', async () => { + await controller.start(); + + const code = '54EF44100006E7DF|3313A005E177A647FC7925620AB207'; + + expect(async () => { + await controller.addInstallCode(code); + }).rejects.toThrow(`Install code 3313a005e177a647fc7925620ab207 has invalid size`); + + expect(mockAddInstallCode).toHaveBeenCalledTimes(0); + }); + it('Controller permit joining all, disabled automatically', async () => { await controller.start(); await controller.permitJoin(254); diff --git a/test/zspec/utils.test.ts b/test/zspec/utils.test.ts index aee6a5f636..b58c754559 100644 --- a/test/zspec/utils.test.ts +++ b/test/zspec/utils.test.ts @@ -32,4 +32,94 @@ describe('ZSpec Utils', () => { expect(ZSpec.Utils.eui64BEBufferToHex(buffer)).toStrictEqual(`0x8877665544332211`); }); + + it('Calculates CRC variants', () => { + // see https://www.crccalc.com/ + const val1 = Buffer.from('83FED3407A939723A5C639FF4C12', 'hex').subarray(0, -2); // crc-appended + + expect(ZSpec.Utils.crc16X25(val1)).toStrictEqual(0x124c); + expect(ZSpec.Utils.crc16XMODEM(val1)).toStrictEqual(0xc4b8); + expect(ZSpec.Utils.crc16CCITT(val1)).toStrictEqual(0x7292); + expect(ZSpec.Utils.crc16CCITTFALSE(val1)).toStrictEqual(0x4041); + + const val2 = Buffer.from('83FED3407A939723A5C639B26916D505C3B5', 'hex').subarray(0, -2); // crc-appended + + expect(ZSpec.Utils.crc16X25(val2)).toStrictEqual(0xb5c3); + expect(ZSpec.Utils.crc16XMODEM(val2)).toStrictEqual(0xff08); + expect(ZSpec.Utils.crc16CCITT(val2)).toStrictEqual(0x1a6a); + expect(ZSpec.Utils.crc16CCITTFALSE(val2)).toStrictEqual(0x9502); + }); + + it('Hashes using AES-128-MMO', () => { + const val1 = Buffer.from('83FED3407A939723A5C639FF4C12', 'hex'); + // Example from Zigbee spec + const val2 = Buffer.from('83FED3407A939723A5C639B26916D505C3B5', 'hex'); + + expect(ZSpec.Utils.aes128MmoHash(val1)).toStrictEqual(Buffer.from('58C1828CF7F1C3FE29E7B1024AD84BFA', 'hex')); + expect(ZSpec.Utils.aes128MmoHash(val2)).toStrictEqual(Buffer.from('66B6900981E1EE3CA4206B6B861C02BB', 'hex')); + }); + + it('Checks install codes of all lengths', () => { + expect(() => ZSpec.Utils.checkInstallCode(Buffer.from('001122', 'hex'))).toThrow(`Install code 001122 has invalid size`); + + const code8Valid = Buffer.from('83FED3407A932B70', 'hex'); + const code8Invalid = Buffer.from('FFFED3407A939723', 'hex'); + const code8InvalidFixed = Buffer.from('FFFED3407A93DE84', 'hex'); + const code8MissingCRC = Buffer.from('83FED3407A93', 'hex'); + + expect(ZSpec.Utils.checkInstallCode(code8Valid)).toStrictEqual([code8Valid, undefined]); + expect(ZSpec.Utils.checkInstallCode(code8Invalid)).toStrictEqual([code8InvalidFixed, 'invalid CRC']); + expect(() => ZSpec.Utils.checkInstallCode(code8Invalid, false)).toThrow(`Install code ${code8Invalid.toString('hex')} failed CRC validation`); + expect(ZSpec.Utils.checkInstallCode(code8MissingCRC)).toStrictEqual([code8Valid, 'missing CRC']); + expect(() => ZSpec.Utils.checkInstallCode(code8MissingCRC, false)).toThrow( + `Install code ${code8MissingCRC.toString('hex')} failed CRC validation`, + ); + + const code10Valid = Buffer.from('83FED3407A93972397FC', 'hex'); + const code10Invalid = Buffer.from('FFFED3407A939723A5C6', 'hex'); + const code10InvalidFixed = Buffer.from('FFFED3407A9397238C4F', 'hex'); + // consired as 8-length with invalid CRC + const code10MissingCRC = Buffer.from('83FED3407A939723', 'hex'); + const code10MissingCRCFixed = Buffer.from('83FED3407A932B70', 'hex'); + + expect(ZSpec.Utils.checkInstallCode(code10Valid)).toStrictEqual([code10Valid, undefined]); + expect(ZSpec.Utils.checkInstallCode(code10Invalid)).toStrictEqual([code10InvalidFixed, 'invalid CRC']); + expect(() => ZSpec.Utils.checkInstallCode(code10Invalid, false)).toThrow( + `Install code ${code10Invalid.toString('hex')} failed CRC validation`, + ); + expect(ZSpec.Utils.checkInstallCode(code10MissingCRC)).toStrictEqual([code10MissingCRCFixed, 'invalid CRC']); + expect(() => ZSpec.Utils.checkInstallCode(code10MissingCRC, false)).toThrow( + `Install code ${code10MissingCRC.toString('hex')} failed CRC validation`, + ); + + const code14Valid = Buffer.from('83FED3407A939723A5C639FF4C12', 'hex'); + const code14Invalid = Buffer.from('FFFED3407A939723A5C639FF4C12', 'hex'); + const code14InvalidFixed = Buffer.from('FFFED3407A939723A5C639FFDE74', 'hex'); + const code14MissingCRC = Buffer.from('83FED3407A939723A5C639FF', 'hex'); + + expect(ZSpec.Utils.checkInstallCode(code14Valid)).toStrictEqual([code14Valid, undefined]); + expect(ZSpec.Utils.checkInstallCode(code14Invalid)).toStrictEqual([code14InvalidFixed, 'invalid CRC']); + expect(() => ZSpec.Utils.checkInstallCode(code14Invalid, false)).toThrow( + `Install code ${code14Invalid.toString('hex')} failed CRC validation`, + ); + expect(ZSpec.Utils.checkInstallCode(code14MissingCRC)).toStrictEqual([code14Valid, 'missing CRC']); + expect(() => ZSpec.Utils.checkInstallCode(code14MissingCRC, false)).toThrow( + `Install code ${code14MissingCRC.toString('hex')} failed CRC validation`, + ); + + const code18Valid = Buffer.from('83FED3407A939723A5C639B26916D505C3B5', 'hex'); + const code18Invalid = Buffer.from('FFFED3407A939723A5C639B26916D505C3B5', 'hex'); + const code18InvalidFixed = Buffer.from('FFFED3407A939723A5C639B26916D505EEB1', 'hex'); + const code18MissingCRC = Buffer.from('83FED3407A939723A5C639B26916D505', 'hex'); + + expect(ZSpec.Utils.checkInstallCode(code18Valid)).toStrictEqual([code18Valid, undefined]); + expect(ZSpec.Utils.checkInstallCode(code18Invalid)).toStrictEqual([code18InvalidFixed, 'invalid CRC']); + expect(() => ZSpec.Utils.checkInstallCode(code18Invalid, false)).toThrow( + `Install code ${code18Invalid.toString('hex')} failed CRC validation`, + ); + expect(ZSpec.Utils.checkInstallCode(code18MissingCRC)).toStrictEqual([code18Valid, 'missing CRC']); + expect(() => ZSpec.Utils.checkInstallCode(code18MissingCRC, false)).toThrow( + `Install code ${code18MissingCRC.toString('hex')} failed CRC validation`, + ); + }); });