Skip to content

Commit

Permalink
fix: Better support for install codes (including deconz) (#1243)
Browse files Browse the repository at this point in the history
  • Loading branch information
Nerivec authored Nov 24, 2024
1 parent bd3f58e commit b98b141
Show file tree
Hide file tree
Showing 12 changed files with 473 additions and 232 deletions.
3 changes: 1 addition & 2 deletions src/adapter/deconz/adapter/deconzAdapter.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<void> {
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
Expand Down
1 change: 1 addition & 0 deletions src/adapter/deconz/driver/constants.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down
140 changes: 73 additions & 67 deletions src/adapter/deconz/driver/driver.ts

Large diffs are not rendered by default.

2 changes: 0 additions & 2 deletions src/adapter/deconz/driver/writer.ts
Original file line number Diff line number Diff line change
@@ -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';
Expand Down
82 changes: 11 additions & 71 deletions src/adapter/ember/adapter/emberAdapter.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -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';

Expand Down Expand Up @@ -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,
});
}
}

Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -1716,43 +1689,10 @@ export class EmberAdapter extends Adapter {

// queued
public async addInstallCode(ieeeAddress: string, key: Buffer): Promise<void> {
// 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<void>(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);
Expand Down
17 changes: 0 additions & 17 deletions src/adapter/ember/consts.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
11 changes: 10 additions & 1 deletion src/controller/controller.ts
Original file line number Diff line number Diff line change
Expand Up @@ -271,7 +271,16 @@ class Controller extends events.EventEmitter<ControllerEventMap> {
// 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<void> {
Expand Down
10 changes: 10 additions & 0 deletions src/zspec/consts.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<number> = [18, 8, 10, 14];
/** Size of the CRC appended to install codes. */
export const INSTALL_CODE_CRC_SIZE = 2;
Loading

0 comments on commit b98b141

Please sign in to comment.