Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

fix: Better support for install codes (including deconz) #1243

Merged
merged 5 commits into from
Nov 24, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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