Skip to content

Commit

Permalink
feat: EZSP: Network restore from backup (#950)
Browse files Browse the repository at this point in the history
* restore again

* restore

* adaptation of sec key types to the EZSP 13 protocol

* lint fix
  • Loading branch information
kirovilya committed Mar 5, 2024
1 parent 52a0ff2 commit 1260ff1
Show file tree
Hide file tree
Showing 5 changed files with 147 additions and 31 deletions.
31 changes: 30 additions & 1 deletion src/adapter/ezsp/adapter/backup.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,8 @@ import {
EmberKeyData
} from '../driver/types';
import {channelsMask2list} from '../driver/utils';

import {fs} from "mz";
import {BackupUtils} from "../../../utils";

export class EZSPAdapterBackup {
private driver: Driver;
Expand Down Expand Up @@ -75,4 +76,32 @@ export class EZSPAdapterBackup {
devices: []
};
}

/**
* Loads currently stored backup and returns it in internal backup model.
*/
public async getStoredBackup(): Promise<Models.Backup> {
try {
await fs.access(this.defaultPath);
} catch (error) {
return null;
}
let data;
try {
data = JSON.parse((await fs.readFile(this.defaultPath)).toString());
} catch (error) {
throw new Error('Coordinator backup is corrupted');
}
if (data.metadata?.format === "zigpy/open-coordinator-backup" && data.metadata?.version) {
if (data.metadata?.version !== 1) {
throw new Error(`Unsupported open coordinator backup version (version=${data.metadata?.version})`);
}
if (!data.metadata.internal?.ezspVersion) {
throw new Error(`This open coordinator backup format not for EZSP adapter`);
}
return BackupUtils.fromUnifiedBackup(data);
} else {
throw new Error("Unknown backup format");
}
}
}
16 changes: 8 additions & 8 deletions src/adapter/ezsp/adapter/ezspAdapter.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,8 +16,8 @@ import {Queue, Waitress, Wait, RealpathSync} from '../../../utils';
import * as Models from "../../../models";
import SerialPortUtils from '../../serialPortUtils';
import SocketPortUtils from '../../socketPortUtils';
import {EZSPAdapterBackup} from './backup';
import {EZSPZDOResponseFrameData} from '../driver/ezsp';
import {LoggerStub} from "../../../controller/logger-stub";


const autoDetectDefinitions = [
Expand All @@ -38,14 +38,14 @@ class EZSPAdapter extends Adapter {
private driver: Driver;
private waitress: Waitress<Events.ZclDataPayload, WaitressMatcher>;
private interpanLock: boolean;
private backupMan: EZSPAdapterBackup;
private queue: Queue;
private closing: boolean;


public constructor(networkOptions: NetworkOptions,
serialPortOptions: SerialPortOptions, backupPath: string, adapterOptions: AdapterOptions) {
super(networkOptions, serialPortOptions, backupPath, adapterOptions);
serialPortOptions: SerialPortOptions, backupPath: string, adapterOptions: AdapterOptions,
logger?: LoggerStub) {
super(networkOptions, serialPortOptions, backupPath, adapterOptions, logger);

this.waitress = new Waitress<Events.ZclDataPayload, WaitressMatcher>(
this.waitressValidator, this.waitressTimeoutFormatter
Expand All @@ -57,12 +57,12 @@ class EZSPAdapter extends Adapter {
debug(`Adapter concurrent: ${concurrent}`);
this.queue = new Queue(concurrent);

this.driver = new Driver(this.serialPortOptions, this.networkOptions, this.greenPowerGroup);
this.driver = new Driver(this.serialPortOptions, this.networkOptions, this.greenPowerGroup,
backupPath, this.logger);
this.driver.on('close', this.onDriverClose.bind(this));
this.driver.on('deviceJoined', this.handleDeviceJoin.bind(this));
this.driver.on('deviceLeft', this.handleDeviceLeft.bind(this));
this.driver.on('incomingMessage', this.processMessage.bind(this));
this.backupMan = new EZSPAdapterBackup(this.driver, backupPath);
}

private async processMessage(frame: EmberIncomingMessage): Promise<void> {
Expand Down Expand Up @@ -630,12 +630,12 @@ class EZSPAdapter extends Adapter {
}

public async supportsBackup(): Promise<boolean> {
return (this.driver?.ezsp?.ezspV < 13);
return true;
}

public async backup(): Promise<Models.Backup> {
if (this.driver.ezsp.isInitialized()) {
return this.backupMan.createBackup();
return this.driver.backupMan.createBackup();
}
}

Expand Down
125 changes: 106 additions & 19 deletions src/adapter/ezsp/driver/driver.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ import {Ezsp, EZSPFrameData, EZSPZDOResponseFrameData} from './ezsp';
import {EmberStatus, EmberNodeType, uint16_t, uint8_t, uint32_t, EmberZDOCmd, EmberApsOption, EmberKeyData,
EmberJoinDecision} from './types';
import {EventEmitter} from "events";
import {EmberApsFrame, EmberNetworkParameters, EmberInitialSecurityState,
import {EmberApsFrame, EmberNetworkParameters, EmberInitialSecurityState, EmberKeyStruct,
EmberRawFrame, EmberIeeeRawFrame, EmberAesMmoHashContext, EmberSecurityManagerContext} from './types/struct';
import {ember_security} from './utils';
import {
Expand All @@ -20,12 +20,15 @@ import {
EmberDerivedKeyType,
EmberStackError,
SLStatus,
EmberInitialSecurityBitmask,
} from './types/named';
import {Multicast} from './multicast';
import {Waitress, Wait} from '../../../utils';
import Debug from "debug";
import equals from 'fast-deep-equal/es6';
import {ParamsDesc} from './commands';
import {EZSPAdapterBackup} from '../adapter/backup';
import {LoggerStub} from "../../../controller/logger-stub";

const debug = {
error: Debug('zigbee-herdsman:adapter:ezsp:erro'),
Expand Down Expand Up @@ -93,15 +96,20 @@ export class Driver extends EventEmitter {
private waitress: Waitress<EmberFrame, EmberWaitressMatcher>;
private transactionID = 1;
private serialOpt: TsType.SerialPortOptions;
public backupMan: EZSPAdapterBackup;
private logger: LoggerStub;

constructor(serialOpt: TsType.SerialPortOptions, nwkOpt: TsType.NetworkOptions, greenPowerGroup: number) {
constructor(serialOpt: TsType.SerialPortOptions, nwkOpt: TsType.NetworkOptions, greenPowerGroup: number,
backupPath: string, logger: LoggerStub) {
super();

this.nwkOpt = nwkOpt;
this.serialOpt = serialOpt;
this.greenPowerGroup = greenPowerGroup;
this.waitress = new Waitress<EmberFrame, EmberWaitressMatcher>(
this.waitressValidator, this.waitressTimeoutFormatter);
this.logger = logger;
this.backupMan = new EZSPAdapterBackup(this, backupPath);
}

/**
Expand Down Expand Up @@ -214,6 +222,9 @@ export class Driver extends EventEmitter {
};

if (await this.needsToBeInitialised(this.nwkOpt)) {
// need to check the backup
const restore = await this.needsToBeRestore(this.nwkOpt);

const res = await this.ezsp.execCommand('networkState');

debug.log(`Network state ${res.status}`);
Expand All @@ -226,10 +237,19 @@ export class Driver extends EventEmitter {
console.assert(st == EmberStatus.NETWORK_DOWN, `leaveNetwork returned unexpected status: ${st}`);
}

await this.formNetwork();

result = 'reset';
if (restore) {
// restore
debug.log("Restore network from backup");
await this.formNetwork(true);
result = 'restored';
} else {
// reset
debug.log("Form network");
await this.formNetwork(false);
result = 'reset';
}
}

const state = (await this.ezsp.execCommand('networkState')).status;
debug.log(`Network state ${state}`);

Expand Down Expand Up @@ -276,26 +296,39 @@ export class Driver extends EventEmitter {
return !valid;
}

private async formNetwork(): Promise<void> {
let status;
status = (await this.ezsp.execCommand('clearKeyTable')).status;
console.assert(status == EmberStatus.SUCCESS,
`Command clearKeyTable returned unexpected state: ${status}`);
private async formNetwork(restore: boolean): Promise<void> {
let backup;
await this.ezsp.execCommand('clearTransientLinkKeys');

const panID = this.nwkOpt.panID;
const extendedPanID = this.nwkOpt.extendedPanID;
const initial_security_state: EmberInitialSecurityState = ember_security(this.nwkOpt);
status = await this.ezsp.setInitialSecurityState(initial_security_state);
let initial_security_state: EmberInitialSecurityState;
if (restore) {
backup = await this.backupMan.getStoredBackup();
initial_security_state = ember_security(backup.networkOptions.networkKey);
initial_security_state.bitmask |= EmberInitialSecurityBitmask.NO_FRAME_COUNTER_RESET;
initial_security_state.networkKeySequenceNumber = backup.networkKeyInfo.sequenceNumber;
initial_security_state.preconfiguredKey.contents = backup.ezsp.hashed_tclk;
} else {
await this.ezsp.execCommand('clearKeyTable');
initial_security_state = ember_security(Buffer.from(this.nwkOpt.networkKey));
}
await this.ezsp.setInitialSecurityState(initial_security_state);

const parameters: EmberNetworkParameters = new EmberNetworkParameters();
parameters.panId = panID;
parameters.extendedPanId = extendedPanID;
parameters.radioTxPower = 5;
parameters.radioChannel = this.nwkOpt.channelList[0];
parameters.joinMethod = EmberJoinMethod.USE_MAC_ASSOCIATION;
parameters.nwkManagerId = 0;
parameters.nwkUpdateId = 0;
parameters.channels = 0x07FFF800; // all channels
if (restore) {
parameters.panId = backup.networkOptions.panId;
parameters.extendedPanId = backup.networkOptions.extendedPanId;
parameters.radioChannel = backup.logicalChannel;
parameters.nwkUpdateId = backup.networkUpdateId;
} else {
parameters.radioChannel = this.nwkOpt.channelList[0];
parameters.panId = this.nwkOpt.panID;
parameters.extendedPanId = Buffer.from(this.nwkOpt.extendedPanID);
}

await this.ezsp.formNetwork(parameters);
await this.ezsp.setValue(EzspValueId.VALUE_STACK_TOKEN_WRITING, 1);
Expand Down Expand Up @@ -851,8 +884,13 @@ export class Driver extends EventEmitter {
if (this.ezsp.ezspV < 13) {
return this.ezsp.execCommand('getKey', {keyType});
} else {
const smc = new EmberSecurityManagerContext();
smc.type = keyType;
// Mapping EmberKeyType to SecManKeyType (ezsp13)
const SecManKeyType = {
[EmberKeyType.TRUST_CENTER_LINK_KEY]: 2,
[EmberKeyType.CURRENT_NETWORK_KEY]: 1,
};
const smc = new EmberSecurityManagerContext();
smc.type = SecManKeyType[keyType as number];
smc.index = 0;
smc.derivedType = EmberDerivedKeyType.NONE;
smc.eui64 = new EmberEUI64('0x0000000000000000');
Expand Down Expand Up @@ -880,4 +918,53 @@ export class Driver extends EventEmitter {
return keyInfo;
}
}

private async needsToBeRestore(options: TsType.NetworkOptions): Promise<boolean> {
// if no backup and the settings have been changed, then need to start a new network
const backup = await this.backupMan.getStoredBackup();
if (!backup) return false;

let valid = true;
//valid = valid && (await this.ezsp.networkInit());
const netParams = await this.ezsp.execCommand('getNetworkParameters');
const networkParams = netParams.parameters;
debug.log("Current Node type: %s, Network parameters: %s", netParams.nodeType, networkParams);
debug.log("Backuped network parameters: %s", backup.networkOptions);
const networkKey = await this.getKey(EmberKeyType.CURRENT_NETWORK_KEY);
let netKey: Buffer = null;
if (this.ezsp.ezspV < 13) {
netKey = Buffer.from((networkKey.keyStruct as EmberKeyStruct).key.contents);
} else {
netKey = Buffer.from((networkKey.keyData as EmberKeyData).contents);
}

// if the settings in the backup match the chip, then need to warn to delete the backup file first
valid = valid && (networkParams.panId == backup.networkOptions.panId);
valid = valid && (networkParams.radioChannel == backup.logicalChannel);
valid = valid && (Buffer.from(networkParams.extendedPanId).equals(backup.networkOptions.extendedPanId));
valid = valid && (Buffer.from(netKey).equals(backup.networkOptions.networkKey));
if (valid) {
this.logger.error(`Configuration is not consistent with adapter backup!`);
this.logger.error(`- PAN ID: configured=${options.panID}, adapter=${networkParams.panId}, backup=${backup.networkOptions.panId}`);
this.logger.error(`- Extended PAN ID: configured=${Buffer.from(options.extendedPanID).toString("hex")}, `+
`adapter=${Buffer.from(networkParams.extendedPanId).toString("hex")}, `+
`backup=${Buffer.from(networkParams.extendedPanId).toString("hex")}`);
this.logger.error(`- Channel: configured=${options.channelList}, adapter=${networkParams.radioChannel}, `+
`backup=${backup.logicalChannel}`);
this.logger.error(`- Network key: configured=${Buffer.from(options.networkKey).toString("hex")}, `+
`adapter=${Buffer.from(netKey).toString("hex")}, `+
`backup=${backup.networkOptions.networkKey.toString("hex")}`);
this.logger.error(`Please update configuration to prevent further issues.`);
this.logger.error(`If you wish to re-commission your network, please remove coordinator backup.`);
this.logger.error(`Re-commissioning your network will require re-pairing of all devices!`);
throw new Error("startup failed - configuration-adapter mismatch - see logs above for more information");
}
valid = true;
// if the settings in the backup match the config, then the old network is in the chip and needs to be restored
valid = valid && (options.panID == backup.networkOptions.panId);
valid = valid && (options.channelList.includes(backup.logicalChannel));
valid = valid && (Buffer.from(options.extendedPanID).equals(backup.networkOptions.extendedPanId));
valid = valid && (Buffer.from(options.networkKey).equals(backup.networkOptions.networkKey));
return valid;
}
}
2 changes: 1 addition & 1 deletion src/adapter/ezsp/driver/types/struct.ts
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,7 @@ export class EzspStruct {
}

export class EmberNetworkParameters extends EzspStruct {
public extendedPanId: number[];
public extendedPanId: Buffer;
public panId: number;
public radioTxPower: number;
public radioChannel: number;
Expand Down
4 changes: 2 additions & 2 deletions src/adapter/ezsp/driver/utils/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -50,7 +50,7 @@ export class Deferred<T> {
}

/* eslint-disable-next-line @typescript-eslint/no-explicit-any*/
function ember_security(config: Record<string, any>): EmberInitialSecurityState {
function ember_security(networkKey: Buffer): EmberInitialSecurityState {
const isc: EmberInitialSecurityState = new EmberInitialSecurityState();
isc.bitmask = (EmberInitialSecurityBitmask.HAVE_PRECONFIGURED_KEY |
EmberInitialSecurityBitmask.TRUST_CENTER_GLOBAL_LINK_KEY |
Expand All @@ -61,7 +61,7 @@ function ember_security(config: Record<string, any>): EmberInitialSecurityState
isc.preconfiguredKey = new EmberKeyData();
isc.preconfiguredKey.contents = randomBytes(16);
isc.networkKey = new EmberKeyData();
isc.networkKey.contents = config.networkKey;
isc.networkKey.contents = networkKey;
isc.networkKeySequenceNumber = 0;
isc.preconfiguredTrustCenterEui64 = new EmberEUI64([0, 0, 0, 0, 0, 0, 0, 0]);
return isc;
Expand Down

0 comments on commit 1260ff1

Please sign in to comment.