Skip to content

Commit

Permalink
fix: Ember: set NWK frame counter on backup restore (#1213)
Browse files Browse the repository at this point in the history
  • Loading branch information
Nerivec authored Oct 7, 2024
1 parent 8485184 commit 48a4278
Show file tree
Hide file tree
Showing 3 changed files with 169 additions and 3 deletions.
25 changes: 25 additions & 0 deletions src/adapter/ember/adapter/emberAdapter.ts
Original file line number Diff line number Diff line change
Expand Up @@ -986,6 +986,7 @@ export class EmberAdapter extends Adapter {
true /*from backup*/,
backup!.networkOptions.networkKey,
backup!.networkKeyInfo.sequenceNumber,
backup!.networkKeyInfo.frameCounter,
backup!.networkOptions.panId,
Array.from(backup!.networkOptions.extendedPanId),
backup!.logicalChannel,
Expand All @@ -1001,6 +1002,7 @@ export class EmberAdapter extends Adapter {
false /*from config*/,
configNetworkKey,
0,
0,
this.networkOptions.panID,
this.networkOptions.extendedPanID!,
this.networkOptions.channelList[0],
Expand Down Expand Up @@ -1046,6 +1048,7 @@ export class EmberAdapter extends Adapter {
fromBackup: boolean,
networkKey: Buffer,
networkKeySequenceNumber: number,
networkKeyFrameCounter: number,
panId: PanId,
extendedPanId: ExtendedPanId,
radioChannel: number,
Expand All @@ -1066,6 +1069,18 @@ export class EmberAdapter extends Adapter {

if (fromBackup) {
state.bitmask |= EmberInitialSecurityBitmask.NO_FRAME_COUNTER_RESET;

const status = await this.ezsp.ezspSetNWKFrameCounter(networkKeyFrameCounter);

if (status !== SLStatus.OK) {
throw new Error(`[INIT FORM] Failed to set NWK frame counter with status=${SLStatus[status]}.`);
}

// status = await this.ezsp.ezspSetAPSFrameCounter(tcLinkKeyFrameCounter);

// if (status !== SLStatus.OK) {
// throw new Error(`[INIT FORM] Failed to set TC APS frame counter with status=${SLStatus[status]}.`);
// }
}

let status = await this.ezsp.ezspSetInitialSecurityState(state);
Expand Down Expand Up @@ -1654,6 +1669,12 @@ export class EmberAdapter extends Adapter {
throw new Error(`[BACKUP] Failed to export TC Link Key with status=${SLStatus[tclkStatus]}.`);
}

// const [tcKeyStatus, tcKeyInfo] = await this.ezsp.ezspGetApsKeyInfo(context);

// if (tcKeyStatus !== SLStatus.OK) {
// throw new Error(`[BACKUP] Failed to get TC APS key info with status=${SLStatus[tcKeyStatus]}.`);
// }

context = initSecurityManagerContext(); // make sure it's back to zeroes
context.coreKeyType = SecManKeyType.NETWORK;
context.keyIndex = 0;
Expand All @@ -1676,6 +1697,10 @@ export class EmberAdapter extends Adapter {
sequenceNumber: netKeyInfo.networkKeySequenceNumber,
frameCounter: netKeyInfo.networkKeyFrameCounter,
},
// tcLinkKeyInfo: {
// incomingFrameCounter: tcKeyInfo.bitmask & EmberKeyStructBitmask.HAS_INCOMING_FRAME_COUNTER ? tcKeyInfo.incomingFrameCounter : 0,
// outgoingFrameCounter: tcKeyInfo.bitmask & EmberKeyStructBitmask.HAS_OUTGOING_FRAME_COUNTER ? tcKeyInfo.outgoingFrameCounter : 0,
// },
securityLevel: SECURITY_LEVEL_Z3,
networkUpdateId: netParams.nwkUpdateId,
coordinatorIeeeAddress: Buffer.from(this.networkCache.eui64.substring(2) /*take out 0x*/, 'hex').reverse(),
Expand Down
52 changes: 51 additions & 1 deletion src/adapter/ember/ezsp/ezsp.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1332,6 +1332,56 @@ export class Ezsp extends EventEmitter<EmberEzspEventMap> {
return await this.ezspSetValue(EzspValueId.STACK_TOKEN_WRITING, 1, [0]);
}

/**
* Wrapper for `ezspSetValue`.
*
* Set NWK layer outgoing frame counter (intended for device restoration purposes).
* Caveats:
* - Can only be called before NetworkInit / FormNetwork / JoinNetwork, when sl_zigbee_network_state()==SL_ZIGBEE_NO_NETWORK.
* - This function should be called before ::sl_zigbee_set_initial_security_state, and the SL_ZIGBEE_NO_FRAME_COUNTER_RESET
* bitmask should be added to the initial security bitmask when ::emberSetInitialSecurityState is called.
* - If used in multi-network context, be sure to call ::sl_zigbee_set_current_network() prior to calling this function.
*
* @param desiredValue The desired outgoing NWK frame counter value.
* This should needs to be less than MAX_INT32U_VALUE to ensure that rollover does not occur on the next encrypted transmission.
* @returns
* - SL_STATUS_OK if calling context is valid (sl_zigbee_network_state() == SL_ZIGBEE_NO_NETWORK) and desiredValue < MAX_INT32U_VALUE.
* - SL_STATUS_INVALID_STATE.
*/
public async ezspSetNWKFrameCounter(frameCounter: number): Promise<SLStatus> {
return await this.ezspSetValue(EzspValueId.NWK_FRAME_COUNTER, 4, [
frameCounter & 0xff,
(frameCounter >> 8) & 0xff,
(frameCounter >> 16) & 0xff,
(frameCounter >> 24) & 0xff,
]);
}

/**
* Wrapper for `ezspSetValue`.
*
* Function to set APS layer outgoing frame counter for Trust Center Link Key (intended for device restoration purposes).
* Caveats:
* - Can only be called before NetworkInit / FormNetwork / JoinNetwork, when sl_zigbee_network_state()==SL_ZIGBEE_NO_NETWORK.
* - This function should be called before ::sl_zigbee_set_initial_security_state, and the SL_ZIGBEE_NO_FRAME_COUNTER_RESET
* bitmask should be added to the initial security bitmask when ::emberSetInitialSecurityState is called.
* - If used in multi-network context, be sure to call ::sl_zigbee_set_current_network() prior to calling this function.
*
* @param desiredValue The desired outgoing APS frame counter value.
* This should needs to be less than MAX_INT32U_VALUE to ensure that rollover does not occur on the next encrypted transmission.
* @returns
* - SL_STATUS_OK if calling context is valid (sl_zigbee_network_state() == SL_ZIGBEE_NO_NETWORK) and desiredValue < MAX_INT32U_VALUE.
* - SL_STATUS_INVALID_STATE.
*/
public async ezspSetAPSFrameCounter(frameCounter: number): Promise<SLStatus> {
return await this.ezspSetValue(EzspValueId.APS_FRAME_COUNTER, 4, [
frameCounter & 0xff,
(frameCounter >> 8) & 0xff,
(frameCounter >> 16) & 0xff,
(frameCounter >> 24) & 0xff,
]);
}

//-----------------------------------------------------------------------------//
//---------------------------- START EZSP COMMANDS ----------------------------//
//-----------------------------------------------------------------------------//
Expand Down Expand Up @@ -2768,7 +2818,7 @@ export class Ezsp extends EventEmitter<EmberEzspEventMap> {
* Callback
* This function returns an unused panID and channel pair found via the find
* unused panId scan procedure.
* @param The unused panID which has been found.
* @param panId The unused panID which has been found.
* @param channel uint8_t The channel that the unused panID was found on.
*/
ezspUnusedPanIdFoundHandler(panId: PanId, channel: number): void {
Expand Down
95 changes: 93 additions & 2 deletions test/adapter/ember/emberAdapter.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -255,11 +255,22 @@ const mockEzspGetNetworkKeyInfo = jest.fn().mockResolvedValue([
networkKeyFrameCounter: DEFAULT_BACKUP.network_key.frame_counter,
} as SecManNetworkKeyInfo,
]);
const mockEzspGetApsKeyInfo = jest.fn().mockResolvedValue([
SLStatus.OK,
{
bitmask: EmberKeyStructBitmask.HAS_OUTGOING_FRAME_COUNTER,
outgoingFrameCounter: 456,
incomingFrameCounter: 0,
ttlInSeconds: 0,
} as SecManAPSKeyMetadata,
]);
const mockEzspSetRadioPower = jest.fn().mockResolvedValue(SLStatus.OK);
const mockEzspImportTransientKey = jest.fn().mockResolvedValue(SLStatus.OK);
const mockEzspClearTransientLinkKeys = jest.fn().mockResolvedValue(SLStatus.OK);
const mockEzspSetLogicalAndRadioChannel = jest.fn().mockResolvedValue(SLStatus.OK);
const mockEzspSendRawMessage = jest.fn().mockResolvedValue(SLStatus.OK);
const mockEzspSetNWKFrameCounter = jest.fn().mockResolvedValue(SLStatus.OK);
const mockEzspSetAPSFrameCounter = jest.fn().mockResolvedValue(SLStatus.OK);

jest.mock('../../../src/adapter/ember/uart/ash');

Expand Down Expand Up @@ -313,11 +324,14 @@ jest.mock('../../../src/adapter/ember/ezsp/ezsp', () => ({
ezspSendBroadcast: mockEzspSendBroadcast,
ezspSendUnicast: mockEzspSendUnicast,
ezspGetNetworkKeyInfo: mockEzspGetNetworkKeyInfo,
ezspGetApsKeyInfo: mockEzspGetApsKeyInfo,
ezspSetRadioPower: mockEzspSetRadioPower,
ezspImportTransientKey: mockEzspImportTransientKey,
ezspClearTransientLinkKeys: mockEzspClearTransientLinkKeys,
ezspSetLogicalAndRadioChannel: mockEzspSetLogicalAndRadioChannel,
ezspSendRawMessage: mockEzspSendRawMessage,
ezspSetNWKFrameCounter: mockEzspSetNWKFrameCounter,
ezspSetAPSFrameCounter: mockEzspSetAPSFrameCounter,
})),
}));

Expand Down Expand Up @@ -365,11 +379,14 @@ const ezspMocks = [
mockEzspSendBroadcast,
mockEzspSendUnicast,
mockEzspGetNetworkKeyInfo,
mockEzspGetApsKeyInfo,
mockEzspSetRadioPower,
mockEzspImportTransientKey,
mockEzspClearTransientLinkKeys,
mockEzspSetLogicalAndRadioChannel,
mockEzspSendRawMessage,
mockEzspSetNWKFrameCounter,
mockEzspSetAPSFrameCounter,
];

describe('Ember Adapter Layer', () => {
Expand Down Expand Up @@ -412,6 +429,23 @@ describe('Ember Adapter Layer', () => {
]);
};

const takeRestoredCodePath = () => {
mockEzspGetNetworkParameters.mockResolvedValueOnce([
SLStatus.OK,
EmberNodeType.COORDINATOR,
{
extendedPanId: DEFAULT_NETWORK_OPTIONS.extendedPanID!,
panId: 1234,
radioTxPower: 5,
radioChannel: DEFAULT_NETWORK_OPTIONS.channelList[0],
joinMethod: 0,
nwkManagerId: 0,
nwkUpdateId: 0,
channels: ZSpec.ALL_802_15_4_CHANNELS_MASK,
} as EmberNetworkParameters,
]);
};

const clearMocks = () => {
for (const mock of ezspMocks) {
mock.mockClear();
Expand Down Expand Up @@ -627,17 +661,40 @@ describe('Ember Adapter Layer', () => {

it('Starts with restored when no network in adapter', async () => {
adapter = new EmberAdapter(DEFAULT_NETWORK_OPTIONS, DEFAULT_SERIAL_PORT_OPTIONS, backupPath, DEFAULT_ADAPTER_OPTIONS);
const expectedNetParams: EmberNetworkParameters = {
extendedPanId: DEFAULT_NETWORK_OPTIONS.extendedPanID!,
panId: DEFAULT_NETWORK_OPTIONS.panID,
radioTxPower: 5,
radioChannel: DEFAULT_NETWORK_OPTIONS.channelList[0],
joinMethod: 0,
nwkManagerId: 0,
nwkUpdateId: 0,
channels: ZSpec.ALL_802_15_4_CHANNELS_MASK,
};

mockEzspNetworkInit.mockResolvedValueOnce(SLStatus.NOT_JOINED);

const result = adapter.start();

await jest.advanceTimersByTimeAsync(5000);
expect(mockEzspSetNWKFrameCounter).toHaveBeenCalledWith(DEFAULT_BACKUP.network_key.frame_counter);
// expect(mockEzspSetAPSFrameCounter).toHaveBeenCalledWith(DEFAULT_BACKUP.???.???);
expect(mockEzspFormNetwork).toHaveBeenCalledWith(expectedNetParams);
await expect(result).resolves.toStrictEqual('restored');
});

it('Starts with restored when network param mismatch but backup available', async () => {
adapter = new EmberAdapter(DEFAULT_NETWORK_OPTIONS, DEFAULT_SERIAL_PORT_OPTIONS, backupPath, DEFAULT_ADAPTER_OPTIONS);
const expectedNetParams: EmberNetworkParameters = {
extendedPanId: DEFAULT_NETWORK_OPTIONS.extendedPanID!,
panId: DEFAULT_NETWORK_OPTIONS.panID,
radioTxPower: 5,
radioChannel: DEFAULT_NETWORK_OPTIONS.channelList[0],
joinMethod: 0,
nwkManagerId: 0,
nwkUpdateId: 0,
channels: ZSpec.ALL_802_15_4_CHANNELS_MASK,
};

mockEzspGetNetworkParameters.mockResolvedValueOnce([
SLStatus.OK,
Expand All @@ -657,12 +714,14 @@ describe('Ember Adapter Layer', () => {
const result = adapter.start();

await jest.advanceTimersByTimeAsync(5000);
expect(mockEzspSetNWKFrameCounter).toHaveBeenCalledWith(DEFAULT_BACKUP.network_key.frame_counter);
// expect(mockEzspSetAPSFrameCounter).toHaveBeenCalledWith(DEFAULT_BACKUP.???.???);
expect(mockEzspFormNetwork).toHaveBeenCalledWith(expectedNetParams);
await expect(result).resolves.toStrictEqual('restored');
});

it('Starts with restored when network key mismatch but backup available', async () => {
adapter = new EmberAdapter(DEFAULT_NETWORK_OPTIONS, DEFAULT_SERIAL_PORT_OPTIONS, backupPath, DEFAULT_ADAPTER_OPTIONS);

const expectedNetParams: EmberNetworkParameters = {
extendedPanId: DEFAULT_NETWORK_OPTIONS.extendedPanID!,
panId: DEFAULT_NETWORK_OPTIONS.panID,
Expand All @@ -673,14 +732,19 @@ describe('Ember Adapter Layer', () => {
nwkUpdateId: 0,
channels: ZSpec.ALL_802_15_4_CHANNELS_MASK,
};

mockEzspGetNetworkParameters.mockResolvedValueOnce([SLStatus.OK, EmberNodeType.COORDINATOR, expectedNetParams]);

const contents = Buffer.from(DEFAULT_BACKUP.network_key.key, 'hex').fill(0xff);

mockEzspExportKey.mockResolvedValueOnce([SLStatus.OK, {contents} as SecManKey]);

const result = adapter.start();

await jest.advanceTimersByTimeAsync(5000);
await expect(result).resolves.toStrictEqual('restored');
expect(mockEzspSetNWKFrameCounter).toHaveBeenCalledWith(DEFAULT_BACKUP.network_key.frame_counter);
// expect(mockEzspSetAPSFrameCounter).toHaveBeenCalledWith(DEFAULT_BACKUP.???.???);
expect(mockEzspFormNetwork).toHaveBeenCalledWith(expectedNetParams);
});

Expand Down Expand Up @@ -721,6 +785,8 @@ describe('Ember Adapter Layer', () => {

await jest.advanceTimersByTimeAsync(5000);
await expect(result).resolves.toStrictEqual('reset');
expect(mockEzspSetNWKFrameCounter).toHaveBeenCalledTimes(0);
// expect(mockEzspSetAPSFrameCounter).toHaveBeenCalledTimes(0);
expect(mockEzspFormNetwork).toHaveBeenCalledWith({
panId: 1234,
extendedPanId: DEFAULT_NETWORK_OPTIONS.extendedPanID!,
Expand All @@ -745,6 +811,8 @@ describe('Ember Adapter Layer', () => {

await jest.advanceTimersByTimeAsync(5000);
await expect(result).resolves.toStrictEqual('reset');
expect(mockEzspSetNWKFrameCounter).toHaveBeenCalledTimes(0);
// expect(mockEzspSetAPSFrameCounter).toHaveBeenCalledTimes(0);
expect(mockEzspFormNetwork).toHaveBeenCalledWith({
panId: 1234,
extendedPanId: DEFAULT_NETWORK_OPTIONS.extendedPanID!,
Expand Down Expand Up @@ -921,6 +989,22 @@ describe('Ember Adapter Layer', () => {
},
`[INIT TC] Failed leave network request with status=FAIL.`,
],
[
'if form could not set NWK frame counter',
() => {
takeRestoredCodePath();
mockEzspSetNWKFrameCounter.mockResolvedValueOnce(SLStatus.FAIL);
},
`[INIT FORM] Failed to set NWK frame counter with status=FAIL.`,
],
// [
// 'if form could not set TC APS frame counter',
// () => {
// takeRestoredCodePath();
// mockEzspSetAPSFrameCounter.mockResolvedValueOnce(SLStatus.FAIL);
// },
// `[INIT FORM] Failed to set TC APS frame counter with status=FAIL.`,
// ],
[
'if form could not set initial security state',
() => {
Expand Down Expand Up @@ -2113,12 +2197,19 @@ describe('Ember Adapter Layer', () => {
`[BACKUP] Failed to get network parameters with status=FAIL.`,
],
[
'failed get network keys info',
'failed get network key info',
() => {
mockEzspGetNetworkKeyInfo.mockResolvedValueOnce([SLStatus.FAIL, {}]);
},
`[BACKUP] Failed to get network keys info with status=FAIL.`,
],
// [
// 'failed get TC APS key info',
// () => {
// mockEzspGetNetworkKeyInfo.mockResolvedValueOnce([SLStatus.FAIL, {}]);
// },
// `[BACKUP] Failed to get TC APS key info with status=FAIL.`,
// ],
[
'no network key set',
() => {
Expand Down

0 comments on commit 48a4278

Please sign in to comment.