diff --git a/lib/util/settings.js b/lib/util/settings.js index 4bcd8cba..c678f7a3 100644 --- a/lib/util/settings.js +++ b/lib/util/settings.js @@ -178,7 +178,7 @@ const schema = { baudrate: {type: 'number'}, rtscts: {type: 'boolean'}, soft_reset_timeout: {type: 'number', minimum: 0}, - network_key: {type: 'array', items: {type: 'number'}}, + network_key: {type: ['array', 'string'], items: {type: 'number'}}, last_seen: {type: 'string', enum: ['disable', 'ISO_8601', 'ISO_8601_local', 'epoch']}, elapsed: {type: 'boolean'}, availability_timeout: {type: 'number', minimum: 0}, @@ -282,13 +282,16 @@ function write() { // Read settings to check if we have to split devices/groups into separate file. const actual = yaml.read(file); - if (actual.mqtt && actual.mqtt.password && actual.mqtt.user) { - toWrite.mqtt.user = actual.mqtt.user; - toWrite.mqtt.password = actual.mqtt.password; - } - if (actual.advanced && actual.advanced.network_key) { - toWrite.advanced.network_key = actual.advanced.network_key; + // In case the setting is defined in a separte file (e.g. !secret network_key) update it there. + for (const path of [['mqtt', 'user'], ['mqtt', 'password'], ['advanced', 'network_key']]) { + if (actual[path[0]] && actual[path[0]][path[1]]) { + const match = /!(.*) (.*)/g.exec(actual[path[0]][path[1]]); + if (match) { + yaml.updateIfChanged(data.joinPath(`${match[1]}.yaml`), match[2], toWrite[path[0]][path[1]]); + toWrite[path[0]][path[1]] = actual[path[0]][path[1]]; + } + } } if (typeof actual.devices === 'string') { @@ -310,6 +313,11 @@ function write() { function validate() { const validate = ajv.compile(schema); const valid = validate(_settings); + if (_settings.advanced && _settings.advanced.network_key && typeof _settings.advanced.network_key === 'string' && + _settings.advanced.network_key !== 'GENERATE') { + throw new Error(`advanced.network_key: should be array or 'GENERATE' (is '${_settings.advanced.network_key}')`); + } + const postfixes = utils.getEndpointNames(); // Verify that all friendly names are unique diff --git a/lib/util/yaml.js b/lib/util/yaml.js index a3a64c1c..cf460f07 100644 --- a/lib/util/yaml.js +++ b/lib/util/yaml.js @@ -33,4 +33,12 @@ function writeIfChanged(file, content) { } } -module.exports = {read, readIfExists, writeIfChanged}; +function updateIfChanged(file, key, value) { + const content = read(file); + if (content[key] !== value) { + content[key] = value; + writeIfChanged(file, content); + } +} + +module.exports = {read, readIfExists, writeIfChanged, updateIfChanged}; diff --git a/lib/zigbee.js b/lib/zigbee.js index c2f8e92f..ef6ddd42 100644 --- a/lib/zigbee.js +++ b/lib/zigbee.js @@ -11,27 +11,6 @@ const zigbeeHerdsmanConverters = require('zigbee-herdsman-converters'); const endpointNames = utils.getEndpointNames(); const keyEndpointByNumber = new RegExp(`.*/([0-9]*)$`); -const herdsmanSettings = { - network: { - panID: settings.get().advanced.pan_id, - extendedPanID: settings.get().advanced.ext_pan_id, - channelList: [settings.get().advanced.channel], - networkKey: settings.get().advanced.network_key, - }, - databasePath: data.joinPath('database.db'), - databaseBackupPath: data.joinPath('database.db.backup'), - backupPath: data.joinPath('coordinator_backup.json'), - serialPort: { - baudRate: settings.get().advanced.baudrate, - rtscts: settings.get().advanced.rtscts, - path: settings.get().serial.port, - adapter: settings.get().serial.adapter, - }, - adapter: { - concurrent: settings.get().advanced.adapter_concurrent, - }, -}; - class Zigbee extends events.EventEmitter { constructor() { super(); @@ -40,10 +19,37 @@ class Zigbee extends events.EventEmitter { async start() { logger.info(`Starting zigbee-herdsman...`); + const herdsmanSettings = { + network: { + panID: settings.get().advanced.pan_id, + extendedPanID: settings.get().advanced.ext_pan_id, + channelList: [settings.get().advanced.channel], + networkKey: settings.get().advanced.network_key, + }, + databasePath: data.joinPath('database.db'), + databaseBackupPath: data.joinPath('database.db.backup'), + backupPath: data.joinPath('coordinator_backup.json'), + serialPort: { + baudRate: settings.get().advanced.baudrate, + rtscts: settings.get().advanced.rtscts, + path: settings.get().serial.port, + adapter: settings.get().serial.adapter, + }, + adapter: { + concurrent: settings.get().advanced.adapter_concurrent, + }, + }; + const herdsmanSettingsLog = objectAssignDeep.noMutate(herdsmanSettings); herdsmanSettingsLog.network.networkKey = 'HIDDEN'; logger.debug(`Using zigbee-herdsman with settings: '${JSON.stringify(herdsmanSettingsLog)}'`); + if (herdsmanSettings.network.networkKey === 'GENERATE') { + const newKey = Array.from({length: 16}, () => Math.floor(Math.random() * 255)); + settings.set(['advanced', 'network_key'], newKey); + herdsmanSettings.network.networkKey = newKey; + } + try { herdsmanSettings.acceptJoiningDeviceHandler = this.acceptJoiningDeviceHandler; this.herdsman = new ZigbeeHerdsman.Controller(herdsmanSettings); diff --git a/test/controller.test.js b/test/controller.test.js index 22d58ada..f30b223c 100644 --- a/test/controller.test.js +++ b/test/controller.test.js @@ -85,6 +85,14 @@ describe('Controller', () => { expect(MQTT.connect).toHaveBeenCalledWith("mqtt://localhost", expected); }); + it('Should generate network_key when set to GENERATE', async () => { + settings.set(['advanced', 'network_key'], 'GENERATE'); + await controller.start(); + await flushPromises(); + expect(zigbeeHerdsman.constructor.mock.calls[0][0].network.networkKey.length).toStrictEqual(16); + expect(data.read().advanced.network_key.length).toStrictEqual(16); + }); + it('Start controller should publish cached states', async () => { data.writeDefaultState(); await controller.start(); diff --git a/test/settings.test.js b/test/settings.test.js index 91e1e6ed..f4bac8d5 100644 --- a/test/settings.test.js +++ b/test/settings.test.js @@ -125,6 +125,12 @@ describe('Settings', () => { settings._write(); expect(read(configurationFile)).toStrictEqual(contentConfiguration); + expect(read(secretFile)).toStrictEqual(contentSecret); + + settings.set(['mqtt', 'user'], 'test123'); + settings.set(['advanced', 'network_key'], [1,2,3, 4]); + expect(read(configurationFile)).toStrictEqual(contentConfiguration); + expect(read(secretFile)).toStrictEqual({...contentSecret, username: 'test123', network_key: [1,2,3,4]}); }); it('Should read devices form a separate file', () => { @@ -489,6 +495,19 @@ describe('Settings', () => { }).toThrow(new Error("Device '0x123' already exists")); }); + it('Should not allow any string values for network_key', () => { + write(configurationFile, { + advanced: {network_key: 'NOT_GENERATE'}, + }); + + settings._reRead(); + + expect(() => { + settings.validate(); + }).toThrowError(`advanced.network_key: should be array or 'GENERATE' (is 'NOT_GENERATE')`); + }); + + it('Should not allow retention configuration without MQTT v5', () => { write(configurationFile, { devices: {'0x0017880104e45519': {friendly_name: 'tain', retention: 900}}, diff --git a/test/stub/data.js b/test/stub/data.js index 0b296879..6d0e43d5 100644 --- a/test/stub/data.js +++ b/test/stub/data.js @@ -214,6 +214,7 @@ writeDefaultState(); module.exports = { mockDir, + read: () => yaml.read(path.join(mockDir, 'configuration.yaml')), writeDefaultConfiguration, writeDefaultState, removeState,