From 04ff0b3e382ba3ad21fb1f51cdc13c2723216959 Mon Sep 17 00:00:00 2001 From: James Hagerman Date: Thu, 27 Feb 2020 17:24:19 -0800 Subject: [PATCH 1/7] Adding deviceType parameter to the server keys command and logic to skip DFU commands when needed. Adding optional argument to pass a filename for the output key file. --- src/cli/keys.js | 7 +++++-- src/cmd/keys.js | 43 +++++++++++++++++++++++++++++++++---------- 2 files changed, 38 insertions(+), 12 deletions(-) diff --git a/src/cli/keys.js b/src/cli/keys.js index a196ef0fb..2185f055b 100644 --- a/src/cli/keys.js +++ b/src/cli/keys.js @@ -64,7 +64,7 @@ module.exports = ({ commandProcessor, root }) => { commandProcessor.createCommand(keys, 'server', 'Switch server public keys.', { epilogue: 'Defaults to the Particle public cloud or you can provide another key in DER format and the server hostname or IP and port', - params: '[filename]', + params: '[filename] [outputFilename]', options: Object.assign({}, protocolOption, { 'host': { description: 'Hostname or IP address of the server to add to the key' @@ -72,11 +72,14 @@ module.exports = ({ commandProcessor, root }) => { 'port': { number: true, description: 'Port number of the server to add to the key' + }, + 'deviceType': { + description: 'Generate key file for the provided device type' } }), handler: (args) => { const KeysCommand = require('../cmd/keys'); - return new KeysCommand().writeServerPublicKey(args.params.filename, args); + return new KeysCommand().writeServerPublicKey({ ...args.params, ...args }); } }); diff --git a/src/cmd/keys.js b/src/cmd/keys.js index b9a040b0e..491d06dd1 100644 --- a/src/cmd/keys.js +++ b/src/cmd/keys.js @@ -329,28 +329,45 @@ module.exports = class KeysCommand { return addressBuf; } - writeServerPublicKey(filename, { host, port, protocol } = {}) { + writeServerPublicKey({ filename, outputFilename, host, port, protocol, deviceType } = {}) { if (filename && !fs.existsSync(filename)) { // TODO UsageError throw new VError('Please specify a server key in DER format.'); } + let skipDFU = false; + if (deviceType) { + skipDFU = true; + this.dfu.dfuId = Object.keys(deviceSpecs).filter(key => deviceSpecs[key].productName.toLowerCase() === deviceType.toLowerCase())[0]; + } + return Promise.resolve().then(() => { - return this.dfu.isDfuUtilInstalled(); - }).then(() => { - return this.dfu.findCompatibleDFU(); - }).then(() => { - return this.validateDeviceProtocol({ protocol }); + if (!skipDFU) { + return this.dfu.isDfuUtilInstalled() + .then(() => { + return this.dfu.findCompatibleDFU(); + }).then(() => { + return this.validateDeviceProtocol({ protocol }); + }) + } else { + return protocol; + } }).then(_protocol => { protocol = _protocol; return this._getDERPublicKey(filename, { protocol }); }).then(derFile => { - return this._formatPublicKey(derFile, host, port, { protocol }); + return this._formatPublicKey(derFile, host, port, { protocol, outputFilename }); }).then(bufferFile => { let segment = this._getServerKeySegmentName({ protocol }); - return this.dfu.write(bufferFile, segment, false); + if (!skipDFU) { + return this.dfu.write(bufferFile, segment, false); + } }).then(() => { - console.log('Okay! New keys in place, your device will not restart.'); + if (!skipDFU) { + console.log('Okay! New keys in place, your device will not restart.'); + } else { + console.log('Okay! Formated server key file generated for this type of device.'); + } }).catch(err => { throw new VError(ensureError(err), 'Make sure your device is in DFU mode (blinking yellow), and is connected to your computer.'); }); @@ -559,7 +576,7 @@ module.exports = class KeysCommand { return path.join(__dirname, `../../assets/keys/${basename}.pub.der`); } - _formatPublicKey(filename, ipOrDomain, port, { protocol }) { + _formatPublicKey(filename, ipOrDomain, port, { protocol, outputFilename }) { let segment = this._getServerKeySegment({ protocol }); if (!segment) { throw new VError('No device specs'); @@ -569,6 +586,9 @@ module.exports = class KeysCommand { if (ipOrDomain) { let alg = segment.alg || 'rsa'; let fileWithAddress = `${utilities.filenameNoExt(filename)}-${utilities.replaceAll(ipOrDomain, '.', '_')}-${alg}.der`; + if (outputFilename) { + fileWithAddress = outputFilename; + } let addressBuf = this._createAddressBuffer(ipOrDomain); // To generate a file like this, just add a type-length-value (TLV) encoded IP or domain beginning 384 bytes into the file—on external flash the address begins at 0x1180. @@ -604,6 +624,9 @@ module.exports = class KeysCommand { let stats = fs.statSync(filename); if (stats.size < segment.size) { let fileWithSize = `${utilities.filenameNoExt(filename)}-padded.der`; + if (outputFilename) { + fileWithSize = outputFilename; + } if (!fs.existsSync(fileWithSize)) { buf = new Buffer(segment.size); From 389864fd9b04737039c2ac24b3599d32b1b6d62a Mon Sep 17 00:00:00 2001 From: James Hagerman Date: Mon, 2 Mar 2020 13:53:36 -0800 Subject: [PATCH 2/7] Updating formatting to pass linting. --- src/cli/keys.js | 2 +- src/cmd/keys.js | 74 ++++++++++++++++++++++++++++--------------------- 2 files changed, 43 insertions(+), 33 deletions(-) diff --git a/src/cli/keys.js b/src/cli/keys.js index 2185f055b..aa1bbd86c 100644 --- a/src/cli/keys.js +++ b/src/cli/keys.js @@ -79,7 +79,7 @@ module.exports = ({ commandProcessor, root }) => { }), handler: (args) => { const KeysCommand = require('../cmd/keys'); - return new KeysCommand().writeServerPublicKey({ ...args.params, ...args }); + return new KeysCommand().writeServerPublicKey({ ...args }); } }); diff --git a/src/cmd/keys.js b/src/cmd/keys.js index 491d06dd1..86c23caed 100644 --- a/src/cmd/keys.js +++ b/src/cmd/keys.js @@ -329,7 +329,7 @@ module.exports = class KeysCommand { return addressBuf; } - writeServerPublicKey({ filename, outputFilename, host, port, protocol, deviceType } = {}) { + writeServerPublicKey({ protocol, host, port, deviceType, params: { filename, outputFilename } } = {}) { if (filename && !fs.existsSync(filename)) { // TODO UsageError throw new VError('Please specify a server key in DER format.'); @@ -338,39 +338,49 @@ module.exports = class KeysCommand { let skipDFU = false; if (deviceType) { skipDFU = true; - this.dfu.dfuId = Object.keys(deviceSpecs).filter(key => deviceSpecs[key].productName.toLowerCase() === deviceType.toLowerCase())[0]; + + // Lookup the DFU ID string that matches the provided deviceType: + this.dfu.dfuId = Object.keys(deviceSpecs) + .filter(key => deviceSpecs[key].productName.toLowerCase() === deviceType.toLowerCase())[0]; } - return Promise.resolve().then(() => { - if (!skipDFU) { - return this.dfu.isDfuUtilInstalled() - .then(() => { - return this.dfu.findCompatibleDFU(); - }).then(() => { - return this.validateDeviceProtocol({ protocol }); - }) - } else { - return protocol; - } - }).then(_protocol => { - protocol = _protocol; - return this._getDERPublicKey(filename, { protocol }); - }).then(derFile => { - return this._formatPublicKey(derFile, host, port, { protocol, outputFilename }); - }).then(bufferFile => { - let segment = this._getServerKeySegmentName({ protocol }); - if (!skipDFU) { - return this.dfu.write(bufferFile, segment, false); - } - }).then(() => { - if (!skipDFU) { - console.log('Okay! New keys in place, your device will not restart.'); - } else { - console.log('Okay! Formated server key file generated for this type of device.'); - } - }).catch(err => { - throw new VError(ensureError(err), 'Make sure your device is in DFU mode (blinking yellow), and is connected to your computer.'); - }); + return Promise.resolve() + .then(() => { + if (!skipDFU) { + return this.dfu.isDfuUtilInstalled() + .then(() => { + return this.dfu.findCompatibleDFU(); + }) + .then(() => { + return this.validateDeviceProtocol({ protocol }); + }); + } else { + return protocol; + } + }) + .then(_protocol => { + protocol = _protocol; + return this._getDERPublicKey(filename, { protocol }); + }) + .then(derFile => { + return this._formatPublicKey(derFile, host, port, { protocol, outputFilename }); + }) + .then(bufferFile => { + let segment = this._getServerKeySegmentName({ protocol }); + if (!skipDFU) { + return this.dfu.write(bufferFile, segment, false); + } + }) + .then(() => { + if (!skipDFU) { + console.log('Okay! New keys in place, your device will not restart.'); + } else { + console.log('Okay! Formated server key file generated for this type of device.'); + } + }) + .catch(err => { + throw new VError(ensureError(err), 'Make sure your device is in DFU mode (blinking yellow), and is connected to your computer.'); + }); } readServerAddress({ protocol }) { From 4c53ef4b710f23211983ebc4b4da571ee37a925e Mon Sep 17 00:00:00 2001 From: busticated Date: Wed, 22 Apr 2020 21:08:17 -0700 Subject: [PATCH 3/7] add basic tests for `keys` commands --- src/cli/keys.test.js | 360 +++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 360 insertions(+) create mode 100644 src/cli/keys.test.js diff --git a/src/cli/keys.test.js b/src/cli/keys.test.js new file mode 100644 index 000000000..6bb16739f --- /dev/null +++ b/src/cli/keys.test.js @@ -0,0 +1,360 @@ +const os = require('os'); +const { expect } = require('../../test/setup'); +const commandProcessor = require('../app/command-processor'); +const keys = require('./keys'); + + +describe('Keys Command-Line Interface', () => { + let root; + + beforeEach(() => { + root = commandProcessor.createAppCategory(); + keys({ root, commandProcessor }); + }); + + describe('Top-Level `keys` Namespace', () => { + it('Handles `keys` command', () => { + const argv = commandProcessor.parse(root, ['keys']); + expect(argv.clierror).to.equal(undefined); + expect(argv.params).to.equal(undefined); + }); + + it('Includes help', () => { + const termWidth = null; // don't right-align option type labels so testing is easier + commandProcessor.parse(root, ['keys', '--help'], termWidth); + commandProcessor.showHelp((helpText) => { + expect(helpText).to.equal([ + 'Manage your device\'s key pair and server public key', + 'Usage: particle keys ', + 'Help: particle help keys ', + '', + 'Commands:', + ' new Generate a new set of keys for your device', + ' load Load a key saved in a file onto your device', + ' save Save a key from your device to a file', + ' send Tell a server which key you\'d like to use by sending your public key in PEM format', + ' doctor Creates and assigns a new key to your device, and uploads it to the cloud', + ' server Switch server public keys.', + ' address Read server configured in device server public key', + ' protocol Retrieve or change transport protocol the device uses to communicate with the cloud', + '' + ].join(os.EOL)); + }); + }); + }); + + describe('`keys new` Namespace', () => { + it('Handles `new` command', () => { + const argv = commandProcessor.parse(root, ['keys', 'new']); + expect(argv.clierror).to.equal(undefined); + expect(argv.params).to.eql({ filename: undefined }); + }); + + it('Parses optional arguments', () => { + const argv = commandProcessor.parse(root, ['keys', 'new', '/path/to/my-key.pem']); + expect(argv.clierror).to.equal(undefined); + expect(argv.params).to.eql({ filename: '/path/to/my-key.pem' }); + expect(argv.protocol).to.equal(undefined); + }); + + it('Parses options', () => { + const argv = commandProcessor.parse(root, ['keys', 'new', '--protocol', 'udp']); + expect(argv.clierror).to.equal(undefined); + expect(argv.params).to.eql({ filename: undefined }); + expect(argv.protocol).to.equal('udp'); + }); + + it('Includes help', () => { + const termWidth = null; // don't right-align option type labels so testing is easier + commandProcessor.parse(root, ['keys', 'new', '--help'], termWidth); + commandProcessor.showHelp((helpText) => { + expect(helpText).to.equal([ + 'Generate a new set of keys for your device', + 'Usage: particle keys new [options] [filename]', + '', + 'Options:', + ' --protocol Communication protocol for the device using the key. tcp or udp [string]', + '' + ].join(os.EOL)); + }); + }); + }); + + describe('`keys claim` Namespace', () => { + it('Handles `claim` command', () => { + const argv = commandProcessor.parse(root, ['keys', 'load', '/path/to/my-key.pem']); + expect(argv.clierror).to.equal(undefined); + expect(argv.params).to.eql({ filename: '/path/to/my-key.pem' }); + }); + + it('Errors when required `deviceID` argument is missing', () => { + const argv = commandProcessor.parse(root, ['keys', 'load']); + expect(argv.clierror).to.be.an.instanceof(Error); + expect(argv.clierror).to.have.property('message', 'Parameter \'filename\' is required.'); + expect(argv.clierror).to.have.property('data', 'filename'); + expect(argv.clierror).to.have.property('isUsageError', true); + expect(argv.params).to.eql({}); + }); + + it('Includes help', () => { + const termWidth = null; // don't right-align option type labels so testing is easier + commandProcessor.parse(root, ['keys', 'load', '--help'], termWidth); + commandProcessor.showHelp((helpText) => { + expect(helpText).to.equal([ + 'Load a key saved in a file onto your device', + 'Usage: particle keys load [options] ', + '' + ].join(os.EOL)); + }); + }); + }); + + describe('`keys save` Namespace', () => { + it('Handles `save` command', () => { + const argv = commandProcessor.parse(root, ['keys', 'save', '/path/to/my-key.pem']); + expect(argv.clierror).to.equal(undefined); + expect(argv.params).to.eql({ filename: '/path/to/my-key.pem' }); + expect(argv.force).to.equal(false); + }); + + it('Errors when required `device` argument is missing', () => { + const argv = commandProcessor.parse(root, ['keys', 'save']); + expect(argv.clierror).to.be.an.instanceof(Error); + expect(argv.clierror).to.have.property('message', 'Parameter \'filename\' is required.'); + expect(argv.clierror).to.have.property('data', 'filename'); + expect(argv.clierror).to.have.property('isUsageError', true); + expect(argv.params).to.eql({}); + expect(argv.force).to.equal(false); + }); + + it('Parses options', () => { + const argv = commandProcessor.parse(root, ['keys', 'save', '/path/to/my-key.pem', '--force']); + expect(argv.clierror).to.equal(undefined); + expect(argv.params).to.eql({ filename: '/path/to/my-key.pem' }); + expect(argv.force).to.equal(true); + }); + + it('Includes help', () => { + const termWidth = null; // don't right-align option type labels so testing is easier + commandProcessor.parse(root, ['keys', 'save', '--help'], termWidth); + commandProcessor.showHelp((helpText) => { + expect(helpText).to.equal([ + 'Save a key from your device to a file', + 'Usage: particle keys save [options] ', + '', + 'Options:', + ' --force Force overwriting of if it exists [boolean] [default: false]', + '' + ].join(os.EOL)); + }); + }); + }); + + describe('`keys send` Namespace', () => { + it('Handles `send` command', () => { + const argv = commandProcessor.parse(root, ['keys', 'send', '1234', '/path/to/my-key.pem']); + expect(argv.clierror).to.equal(undefined); + expect(argv.params).to.eql({ deviceID: '1234', filename: '/path/to/my-key.pem' }); + expect(argv.product_id).to.equal(undefined); + }); + + it('Errors when required `deviceID` argument is missing', () => { + const argv = commandProcessor.parse(root, ['keys', 'send']); + expect(argv.clierror).to.be.an.instanceof(Error); + expect(argv.clierror).to.have.property('message', 'Parameter \'deviceID\' is required.'); + expect(argv.clierror).to.have.property('data', 'deviceID'); + expect(argv.clierror).to.have.property('isUsageError', true); + expect(argv.params).to.eql({}); + expect(argv.product_id).to.equal(undefined); + }); + + it('Errors when required `filename` argument is missing', () => { + const argv = commandProcessor.parse(root, ['keys', 'send', '1234']); + expect(argv.clierror).to.be.an.instanceof(Error); + expect(argv.clierror).to.have.property('message', 'Parameter \'filename\' is required.'); + expect(argv.clierror).to.have.property('data', 'filename'); + expect(argv.clierror).to.have.property('isUsageError', true); + expect(argv.params).to.eql({ deviceID: '1234' }); + expect(argv.product_id).to.equal(undefined); + }); + + it('Parses options', () => { + const argv = commandProcessor.parse(root, ['keys', 'send', '1234', '/path/to/my-key.pem', '--product_id', '4321']); + expect(argv.clierror).to.equal(undefined); + expect(argv.params).to.eql({ deviceID: '1234', filename: '/path/to/my-key.pem' }); + expect(argv.product_id).to.equal(4321); // TODO (mirande): should be a string + }); + + it('Includes help', () => { + const termWidth = null; // don't right-align option type labels so testing is easier + commandProcessor.parse(root, ['keys', 'send', '--help'], termWidth); + commandProcessor.showHelp((helpText) => { + expect(helpText).to.equal([ + 'Tell a server which key you\'d like to use by sending your public key in PEM format', + 'Usage: particle keys send [options] ', + '', + 'Options:', + ' --product_id The product ID to use when provisioning a new device [number]', + '' + ].join(os.EOL)); + }); + }); + }); + + describe('`keys doctor` Namespace', () => { + it('Handles `doctor` command', () => { + const argv = commandProcessor.parse(root, ['keys', 'doctor', '1234']); + expect(argv.clierror).to.equal(undefined); + expect(argv.params).to.eql({ deviceID: '1234' }); + expect(argv.protocol).to.equal(undefined); + }); + + it('Errors when required `device` argument is missing', () => { + const argv = commandProcessor.parse(root, ['keys', 'doctor']); + expect(argv.clierror).to.be.an.instanceof(Error); + expect(argv.clierror).to.have.property('message', 'Parameter \'deviceID\' is required.'); + expect(argv.clierror).to.have.property('data', 'deviceID'); + expect(argv.clierror).to.have.property('isUsageError', true); + expect(argv.params).to.eql({}); + expect(argv.protocol).to.equal(undefined); + }); + + it('Parses options', () => { + const argv = commandProcessor.parse(root, ['keys', 'doctor', '1234', '--protocol', 'udp']); + expect(argv.clierror).to.equal(undefined); + expect(argv.params).to.eql({ deviceID: '1234' }); + expect(argv.protocol).to.equal('udp'); + }); + + it('Includes help', () => { + const termWidth = null; // don't right-align option type labels so testing is easier + commandProcessor.parse(root, ['keys', 'doctor', '--help'], termWidth); + commandProcessor.showHelp((helpText) => { + expect(helpText).to.equal([ + 'Creates and assigns a new key to your device, and uploads it to the cloud', + 'Usage: particle keys doctor [options] ', + '', + 'Options:', + ' --protocol Communication protocol for the device using the key. tcp or udp [string]', + '', + ].join(os.EOL)); + }); + }); + }); + + describe('`keys server` Namespace', () => { + it('Handles `server` command', () => { + const argv = commandProcessor.parse(root, ['keys', 'server']); + expect(argv.clierror).to.equal(undefined); + expect(argv.params).to.eql({ filename: undefined, outputFilename: undefined }); + expect(argv.protocol).to.equal(undefined); + expect(argv.host).to.equal(undefined); + expect(argv.port).to.equal(undefined); + expect(argv.deviceType).to.equal(undefined); + }); + + it('Parses optional params', () => { + const argv = commandProcessor.parse(root, ['keys', 'server', '/path/to/my-key.pem', '/path/to/output.pem']); + expect(argv.clierror).to.equal(undefined); + expect(argv.params).to.eql({ filename: '/path/to/my-key.pem', outputFilename: '/path/to/output.pem' }); + expect(argv.protocol).to.equal(undefined); + expect(argv.host).to.equal(undefined); + expect(argv.port).to.equal(undefined); + expect(argv.deviceType).to.equal(undefined); + }); + + it('Parses options', () => { + const flags = ['--protocol', 'udp', '--host', 'example.com', '--port', '5050', '--deviceType', 'argon']; + const argv = commandProcessor.parse(root, ['keys', 'server', ...flags]); + expect(argv.clierror).to.equal(undefined); + expect(argv.params).to.eql({ filename: undefined, outputFilename: undefined }); + expect(argv.protocol).to.equal('udp'); + expect(argv.host).to.equal('example.com'); + expect(argv.port).to.equal(5050); + expect(argv.deviceType).to.equal('argon'); + }); + + it('Includes help', () => { + const termWidth = null; // don't right-align option type labels so testing is easier + commandProcessor.parse(root, ['keys', 'server', '--help'], termWidth); + commandProcessor.showHelp((helpText) => { + expect(helpText).to.equal([ + 'Switch server public keys.', + 'Usage: particle keys server [options] [filename] [outputFilename]', + '', + 'Options:', + ' --protocol Communication protocol for the device using the key. tcp or udp [string]', + ' --host Hostname or IP address of the server to add to the key [string]', + ' --port Port number of the server to add to the key [number]', + ' --deviceType Generate key file for the provided device type [string]', + '', + 'Defaults to the Particle public cloud or you can provide another key in DER format and the server hostname or IP and port', + '' + ].join(os.EOL)); + }); + }); + }); + + describe('`keys address` Namespace', () => { + it('Handles `address` command', () => { + const argv = commandProcessor.parse(root, ['keys', 'address']); + expect(argv.clierror).to.equal(undefined); + expect(argv.params).to.eql({}); + expect(argv.protocol).to.equal(undefined); + }); + + it('Parses options', () => { + const argv = commandProcessor.parse(root, ['keys', 'address', '--protocol', 'udp']); + expect(argv.clierror).to.equal(undefined); + expect(argv.params).to.eql({}); + expect(argv.protocol).to.equal('udp'); + }); + + it('Includes help', () => { + const termWidth = null; // don't right-align option type labels so testing is easier + commandProcessor.parse(root, ['keys', 'address', '--help'], termWidth); + commandProcessor.showHelp((helpText) => { + expect(helpText).to.equal([ + 'Read server configured in device server public key', + 'Usage: particle keys address [options]', + '', + 'Options:', + ' --protocol Communication protocol for the device using the key. tcp or udp [string]', + '', + ].join(os.EOL)); + }); + }); + }); + + describe('`keys protocol` Namespace', () => { + it('Handles `protocol` command', () => { + const argv = commandProcessor.parse(root, ['keys', 'protocol']); + expect(argv.clierror).to.equal(undefined); + expect(argv.params).to.eql({}); + expect(argv.protocol).to.equal(undefined); + }); + + it('Parses options', () => { + const argv = commandProcessor.parse(root, ['keys', 'protocol', '--protocol', 'udp']); + expect(argv.clierror).to.equal(undefined); + expect(argv.params).to.eql({}); + expect(argv.protocol).to.equal('udp'); + }); + + it('Includes help', () => { + const termWidth = null; // don't right-align option type labels so testing is easier + commandProcessor.parse(root, ['keys', 'protocol', '--help'], termWidth); + commandProcessor.showHelp((helpText) => { + expect(helpText).to.equal([ + 'Retrieve or change transport protocol the device uses to communicate with the cloud', + 'Usage: particle keys protocol [options]', + '', + 'Options:', + ' --protocol Communication protocol for the device using the key. tcp or udp [string]', + '', + ].join(os.EOL)); + }); + }); + }); +}); + From 7ee590f055b5a3eae90416fbdb1622ad8e12556a Mon Sep 17 00:00:00 2001 From: busticated Date: Wed, 22 Apr 2020 21:09:32 -0700 Subject: [PATCH 4/7] command processor exposes usage error --- src/app/command-processor.js | 21 ++++++++++----------- 1 file changed, 10 insertions(+), 11 deletions(-) diff --git a/src/app/command-processor.js b/src/app/command-processor.js index fb77847f2..1be6c030f 100644 --- a/src/app/command-processor.js +++ b/src/app/command-processor.js @@ -704,16 +704,6 @@ function unknownParametersError(params){ ); } -const errors = { - unknownCommandError, - unknownArgumentError, - requiredParameterError, - variadicParameterRequiredError, - variadicParameterPositionError, - requiredParameterPositionError, - unknownParametersError -}; - function showHelp(cb){ Yargs.showHelp(cb); } @@ -725,7 +715,16 @@ module.exports = { createAppCategory, createErrorHandler, showHelp, - errors, + errors: { + usageError, + unknownCommandError, + unknownArgumentError, + requiredParameterError, + variadicParameterRequiredError, + variadicParameterPositionError, + requiredParameterPositionError, + unknownParametersError + }, test: { consoleErrorLogger } From 5a8babac3dd096b3d83fbb3b26e82a97cbe4687a Mon Sep 17 00:00:00 2001 From: busticated Date: Wed, 22 Apr 2020 21:19:49 -0700 Subject: [PATCH 5/7] clean up code style, add e2e tests for keys doctor, address, and protocol subcommands --- src/cli/keys.js | 12 +- src/cmd/keys.js | 525 ++++++++++++++++++++++--------------------- src/cmd/keys.test.js | 16 +- test/e2e/keys.e2e.js | 95 +++++++- 4 files changed, 375 insertions(+), 273 deletions(-) diff --git a/src/cli/keys.js b/src/cli/keys.js index aa1bbd86c..c018f01ff 100644 --- a/src/cli/keys.js +++ b/src/cli/keys.js @@ -12,7 +12,7 @@ module.exports = ({ commandProcessor, root }) => { options: protocolOption, handler: (args) => { const KeysCommand = require('../cmd/keys'); - return new KeysCommand().makeNewKey(args.params.filename, args); + return new KeysCommand().makeNewKey(args); } }); @@ -20,7 +20,7 @@ module.exports = ({ commandProcessor, root }) => { params: '', handler: (args) => { const KeysCommand = require('../cmd/keys'); - return new KeysCommand().writeKeyToDevice(args.params.filename); + return new KeysCommand().writeKeyToDevice(args); } }); @@ -35,7 +35,7 @@ module.exports = ({ commandProcessor, root }) => { }, handler: (args) => { const KeysCommand = require('../cmd/keys'); - return new KeysCommand().saveKeyFromDevice(args.params.filename, args); + return new KeysCommand().saveKeyFromDevice(args); } }); @@ -49,7 +49,7 @@ module.exports = ({ commandProcessor, root }) => { }, handler: (args) => { const KeysCommand = require('../cmd/keys'); - return new KeysCommand().sendPublicKeyToServer(args.params.deviceID, args.params.filename, args); + return new KeysCommand().sendPublicKeyToServer(args); } }); @@ -58,7 +58,7 @@ module.exports = ({ commandProcessor, root }) => { options: protocolOption, handler: (args) => { const KeysCommand = require('../cmd/keys'); - return new KeysCommand().keyDoctor(args.params.deviceID, args); + return new KeysCommand().keyDoctor(args); } }); @@ -79,7 +79,7 @@ module.exports = ({ commandProcessor, root }) => { }), handler: (args) => { const KeysCommand = require('../cmd/keys'); - return new KeysCommand().writeServerPublicKey({ ...args }); + return new KeysCommand().writeServerPublicKey(args); } }); diff --git a/src/cmd/keys.js b/src/cmd/keys.js index 86c23caed..c719713dc 100644 --- a/src/cmd/keys.js +++ b/src/cmd/keys.js @@ -19,55 +19,57 @@ const dfu = require('../lib/dfu'); * @constructor */ module.exports = class KeysCommand { - constructor() { + constructor(){ this.dfu = dfu; } - transportProtocol({ protocol }) { - return protocol ? this.changeTransportProtocol(protocol) : this.showTransportProtocol(); + transportProtocol({ protocol }){ + return protocol + ? this.changeTransportProtocol(protocol) + : this.showTransportProtocol(); } - showTransportProtocol() { - return Promise.resolve().then(() => { - return this.dfu.isDfuUtilInstalled(); - }).then(() => { + showTransportProtocol(){ + return Promise.resolve() + .then(() => this.dfu.isDfuUtilInstalled()) //make sure our device is online and in dfu mode - return this.dfu.findCompatibleDFU(); - }).then(() => { - return this.validateDeviceProtocol(); - }).then(protocol => { - console.log(`Device protocol is set to ${protocol}`); - }).catch(err => { - throw new VError(ensureError(err), 'Could not fetch device transport protocol'); - }); + .then(() => this.dfu.findCompatibleDFU()) + .then(() => this.validateDeviceProtocol()) + .then(protocol => { + console.log(`Device protocol is set to ${protocol}`); + }) + .catch(err => { + throw new VError(ensureError(err), 'Could not fetch device transport protocol'); + }); } - changeTransportProtocol(protocol) { - if (protocol !== 'udp' && protocol !== 'tcp') { + changeTransportProtocol(protocol){ + if (protocol !== 'udp' && protocol !== 'tcp'){ return new VError('Invalid protocol'); } - return Promise.resolve().then(() => { - return this.dfu.isDfuUtilInstalled(); - }).then(() => { + return Promise.resolve() + .then(() => this.dfu.isDfuUtilInstalled()) //make sure our device is online and in dfu mode - return this.dfu.findCompatibleDFU(); - }).then(() => { - let specs = deviceSpecs[this.dfu.dfuId]; - if (!specs.transport) { - throw new VError('Protocol cannot be changed for this device'); - } + .then(() => this.dfu.findCompatibleDFU()) + .then(() => { + let specs = deviceSpecs[this.dfu.dfuId]; + if (!specs.transport){ + throw new VError('Protocol cannot be changed for this device'); + } - let flagValue = specs.defaultProtocol === protocol ? new Buffer([255]) : new Buffer([0]); - return this.dfu.writeBuffer(flagValue, 'transport', false); - }).then(() => { - console.log(`Protocol changed to ${protocol}`); - }).catch(err => { - throw new VError(ensureError(err), 'Could not change device transport protocol'); - }); + let flagValue = specs.defaultProtocol === protocol ? new Buffer([255]) : new Buffer([0]); + return this.dfu.writeBuffer(flagValue, 'transport', false); + }) + .then(() => { + console.log(`Protocol changed to ${protocol}`); + }) + .catch(err => { + throw new VError(ensureError(err), 'Could not change device transport protocol'); + }); } - makeKeyOpenSSL(filename, alg, { protocol }) { + makeKeyOpenSSL(filename, alg, { protocol }){ const { filenameNoExt, deferredChildProcess } = utilities; filename = filenameNoExt(filename); @@ -75,9 +77,9 @@ module.exports = class KeysCommand { return Promise.resolve() .then(() => { - if (alg === 'rsa') { + if (alg === 'rsa'){ return deferredChildProcess(`openssl genrsa -out "${filename}.pem" 1024`); - } else if (alg === 'ec') { + } else if (alg === 'ec'){ return deferredChildProcess(`openssl ecparam -name prime256v1 -genkey -out "${filename}.pem"`); } }) @@ -89,89 +91,93 @@ module.exports = class KeysCommand { }); } - keyAlgorithmForProtocol(protocol) { + keyAlgorithmForProtocol(protocol){ return protocol === 'udp' ? 'ec' : 'rsa'; } - makeNewKey(filename, { protocol }) { + makeNewKey({ protocol, params: { filename } }){ return this._makeNewKey({ filename: filename || 'device', protocol }); } - _makeNewKey({ filename, protocol }) { + _makeNewKey({ filename, protocol }){ let alg; let showHelp = !protocol; - return Promise.resolve().then(() => { - return Promise.resolve().then(() => { - return this.dfu.isDfuUtilInstalled(); - }).then(() => { - return this.dfu.findCompatibleDFU(showHelp); - }).catch((err) => { - if (protocol) { - alg = this.keyAlgorithmForProtocol(protocol); - return; - } - throw err; + return Promise.resolve() + .then(() => { + return Promise.resolve() + .then(() => this.dfu.isDfuUtilInstalled()) + .then(() => this.dfu.findCompatibleDFU(showHelp)) + .catch((err) => { + if (protocol){ + alg = this.keyAlgorithmForProtocol(protocol); + return; + } + throw err; + }); + }) + .then(() => this.makeKeyOpenSSL(filename, alg, { protocol })) + .then(() => { + console.log('New Key Created!'); + }) + .catch(err => { + throw new VError(ensureError(err), 'Error creating keys'); }); - }).then(() => { - return this.makeKeyOpenSSL(filename, alg, { protocol }); - }).then(() => { - console.log('New Key Created!'); - }).catch(err => { - throw new VError(ensureError(err), 'Error creating keys'); - }); } - writeKeyToDevice(filename) { + writeKeyToDevice({ params: { filename } }){ return this._writeKeyToDevice({ filename }); } - _writeKeyToDevice({ filename, leave = false }) { + _writeKeyToDevice({ filename, leave = false }){ let protocol; + filename = utilities.filenameNoExt(filename) + '.der'; - if (!fs.existsSync(filename)) { + + if (!fs.existsSync(filename)){ throw new VError("I couldn't find the file: " + filename); } //TODO: give the user a warning before doing this, since it'll bump their device offline. - return Promise.resolve().then(() => { - return this.dfu.isDfuUtilInstalled(); - }).then(() => { + return Promise.resolve() + .then(() => this.dfu.isDfuUtilInstalled()) //make sure our device is online and in DFU mode - return this.dfu.findCompatibleDFU(); - }).then(() => { - return this.validateDeviceProtocol(); - }).then(_protocol => { - protocol = _protocol; - //backup their existing key so they don't lock themselves out. - let alg = this._getPrivateKeyAlgorithm({ protocol }); - let prefilename = path.join( - path.dirname(filename), - 'backup_' + alg + '_' + path.basename(filename) - ); - return this._saveKeyFromDevice({ filename: prefilename, force: true }); - }).then(() => { - let segment = this._getPrivateKeySegmentName({ protocol }); - return this.dfu.write(filename, segment, leave); - }).then(() => { - console.log('Saved!'); - }).catch(err => { - throw new VError(ensureError(err), 'Error writing key to device.'); - }); + .then(() => this.dfu.findCompatibleDFU()) + .then(() => this.validateDeviceProtocol()) + .then(_protocol => { + protocol = _protocol; + //backup their existing key so they don't lock themselves out. + let alg = this._getPrivateKeyAlgorithm({ protocol }); + let prefilename = path.join( + path.dirname(filename), + 'backup_' + alg + '_' + path.basename(filename) + ); + return this._saveKeyFromDevice({ filename: prefilename, force: true }); + }) + .then(() => { + let segment = this._getPrivateKeySegmentName({ protocol }); + return this.dfu.write(filename, segment, leave); + }) + .then(() => { + console.log('Saved!'); + }) + .catch(err => { + throw new VError(ensureError(err), 'Error writing key to device.'); + }); } - saveKeyFromDevice(filename, { force }) { + saveKeyFromDevice({ force, params: { filename } }){ filename = utilities.filenameNoExt(filename) + '.der'; return this._saveKeyFromDevice({ filename, force }); } - _saveKeyFromDevice({ filename, force }) { + _saveKeyFromDevice({ filename, force }){ const { tryDelete, filenameNoExt, deferredChildProcess } = utilities; let protocol; - if (!force && fs.existsSync(filename)) { + if (!force && fs.existsSync(filename)){ throw new VError('This file already exists, please specify a different file, or use the --force flag.'); - } else if (fs.existsSync(filename)) { + } else if (fs.existsSync(filename)){ tryDelete(filename); } @@ -179,15 +185,9 @@ module.exports = class KeysCommand { //pull the key down and save it there return Promise.resolve() - .then(() => { - return this.dfu.isDfuUtilInstalled(); - }) - .then(() => { - return this.dfu.findCompatibleDFU(); - }) - .then(() => { - return this.validateDeviceProtocol(); - }) + .then(() => this.dfu.isDfuUtilInstalled()) + .then(() => this.dfu.findCompatibleDFU()) + .then(() => this.validateDeviceProtocol()) .then(_protocol => { protocol = _protocol; let segment = this._getPrivateKeySegmentName({ protocol }); @@ -196,7 +196,7 @@ module.exports = class KeysCommand { .then(() => { let pubPemFilename = filenameNoExt(filename) + '.pub.pem'; - if (force) { + if (force){ tryDelete(pubPemFilename); } @@ -217,21 +217,21 @@ module.exports = class KeysCommand { }); } - sendPublicKeyToServer(deviceId, filename, { product_id: productId }) { - return this._sendPublicKeyToServer({ deviceId, filename, productId, algorithm: 'rsa' }); + sendPublicKeyToServer({ product_id: productId, params: { deviceID, filename } }){ + return this._sendPublicKeyToServer({ deviceID, filename, productId, algorithm: 'rsa' }); } - _sendPublicKeyToServer({ deviceId, filename, productId, algorithm }) { + _sendPublicKeyToServer({ deviceID, filename, productId, algorithm }){ const { filenameNoExt, deferredChildProcess, readFile } = utilities; - if (!fs.existsSync(filename)) { + if (!fs.existsSync(filename)){ filename = filenameNoExt(filename) + '.pub.pem'; - if (!fs.existsSync(filename)) { + if (!fs.existsSync(filename)){ throw new VError("Couldn't find " + filename); } } - deviceId = deviceId.toLowerCase(); + deviceID = deviceID.toLowerCase(); let api = new ApiClient(); api.ensureToken(); @@ -261,7 +261,7 @@ module.exports = class KeysCommand { }) .then(keyBuf => { let apiAlg = algorithm === 'rsa' ? 'rsa' : 'ecc'; - return api.sendPublicKey(deviceId, keyBuf, apiAlg, productId); + return api.sendPublicKey(deviceID, keyBuf, apiAlg, productId); }) .catch(err => { throw new VError(ensureError(err), 'Error sending public key to server'); @@ -272,10 +272,10 @@ module.exports = class KeysCommand { }); } - keyDoctor(deviceId, { protocol } = {}) { - deviceId = deviceId.toLowerCase(); // make lowercase so that it's case insensitive + keyDoctor({ protocol, params: { deviceID } }){ + deviceID = deviceID.toLowerCase(); // make lowercase so that it's case insensitive - if (deviceId.length < 24) { + if (deviceID.length < 24){ console.log('***************************************************************'); console.log(' Warning! - device id was shorter than 24 characters - did you use something other than an id?'); console.log(' use particle identify to find your device id'); @@ -283,29 +283,27 @@ module.exports = class KeysCommand { } let algorithm, filename; - return Promise.resolve().then(() => { - return this.dfu.isDfuUtilInstalled(); - }).then(() => { - return this.dfu.findCompatibleDFU(); - }).then(() => { - return this.validateDeviceProtocol({ protocol }); - }).then(_protocol => { - protocol = _protocol; - algorithm = this._getPrivateKeyAlgorithm({ protocol }); - filename = deviceId + '_' + algorithm + '_new'; - return this._makeNewKey({ filename }); - }).then(() => { - return this._writeKeyToDevice({ filename, leave: true }); - }).then(() => { - return this._sendPublicKeyToServer({ deviceId, filename, algorithm }); - }).then(() => { - console.log('Okay! New keys in place, your device should restart.'); - }).catch(err => { - throw new VError(ensureError(err), 'Make sure your device is in DFU mode (blinking yellow), and that your computer is online.'); - }); + return Promise.resolve() + .then(() => this.dfu.isDfuUtilInstalled()) + .then(() => this.dfu.findCompatibleDFU()) + .then(() => this.validateDeviceProtocol({ protocol })) + .then(_protocol => { + protocol = _protocol; + algorithm = this._getPrivateKeyAlgorithm({ protocol }); + filename = `${deviceID}_${algorithm}_new`; + return this._makeNewKey({ filename }); + }) + .then(() => this._writeKeyToDevice({ filename, leave: true })) + .then(() => this._sendPublicKeyToServer({ deviceID, filename, algorithm })) + .then(() => { + console.log('Okay! New keys in place, your device should restart.'); + }) + .catch(err => { + throw new VError(ensureError(err), 'Make sure your device is in DFU mode (blinking yellow), and that your computer is online.'); + }); } - _createAddressBuffer(ipOrDomain) { + _createAddressBuffer(ipOrDomain){ const isIpAddress = /^[0-9.]*$/.test(ipOrDomain); // create a version of this key that points to a particular server or domain @@ -313,7 +311,7 @@ module.exports = class KeysCommand { addressBuf[0] = (isIpAddress) ? 0 : 1; addressBuf[1] = (isIpAddress) ? 4 : ipOrDomain.length; - if (isIpAddress) { + if (isIpAddress){ const parts = ipOrDomain.split('.').map((obj) => { return parseInt(obj); }); @@ -329,14 +327,14 @@ module.exports = class KeysCommand { return addressBuf; } - writeServerPublicKey({ protocol, host, port, deviceType, params: { filename, outputFilename } } = {}) { - if (filename && !fs.existsSync(filename)) { + writeServerPublicKey({ protocol, host, port, deviceType, params: { filename, outputFilename } }){ + if (filename && !fs.existsSync(filename)){ // TODO UsageError throw new VError('Please specify a server key in DER format.'); } let skipDFU = false; - if (deviceType) { + if (deviceType){ skipDFU = true; // Lookup the DFU ID string that matches the provided deviceType: @@ -346,17 +344,12 @@ module.exports = class KeysCommand { return Promise.resolve() .then(() => { - if (!skipDFU) { - return this.dfu.isDfuUtilInstalled() - .then(() => { - return this.dfu.findCompatibleDFU(); - }) - .then(() => { - return this.validateDeviceProtocol({ protocol }); - }); - } else { + if (skipDFU){ return protocol; } + return this.dfu.isDfuUtilInstalled() + .then(() => this.dfu.findCompatibleDFU()) + .then(() => this.validateDeviceProtocol({ protocol })); }) .then(_protocol => { protocol = _protocol; @@ -367,12 +360,12 @@ module.exports = class KeysCommand { }) .then(bufferFile => { let segment = this._getServerKeySegmentName({ protocol }); - if (!skipDFU) { + if (!skipDFU){ return this.dfu.write(bufferFile, segment, false); } }) .then(() => { - if (!skipDFU) { + if (!skipDFU){ console.log('Okay! New keys in place, your device will not restart.'); } else { console.log('Okay! Formated server key file generated for this type of device.'); @@ -383,57 +376,59 @@ module.exports = class KeysCommand { }); } - readServerAddress({ protocol }) { + readServerAddress({ protocol }){ let keyBuf, serverKeySeg; - return Promise.resolve().then(() => { - return this.dfu.isDfuUtilInstalled(); - }).then(() => { - return this.dfu.findCompatibleDFU(); - }).then(() => { - return this.validateDeviceProtocol({ protocol }); - }).then(_protocol => { - protocol = _protocol; - serverKeySeg = this._getServerKeySegment({ protocol }); - }).then(() => { - let segment = this._getServerKeySegmentName({ protocol }); - return this.dfu.readBuffer(segment, false) - .then((buf) => { - keyBuf = buf; - }); - }).then(() => { - let offset = serverKeySeg.addressOffset || 384; - let portOffset = serverKeySeg.portOffset || 450; - let type = keyBuf[offset]; - let len = keyBuf[offset+1]; - let data = keyBuf.slice(offset + 2, offset + 2 + len); - - let port = keyBuf[portOffset] << 8 | keyBuf[portOffset+1]; - if (port === 0xFFFF) { - port = protocol === 'tcp' ? 5683 : 5684; - } + return Promise.resolve() + .then(() => this.dfu.isDfuUtilInstalled()) + .then(() => this.dfu.findCompatibleDFU()) + .then(() => this.validateDeviceProtocol({ protocol })) + .then(_protocol => { + protocol = _protocol; + serverKeySeg = this._getServerKeySegment({ protocol }); + }) + .then(() => { + let segment = this._getServerKeySegmentName({ protocol }); + return this.dfu.readBuffer(segment, false) + .then((buf) => { + keyBuf = buf; + }); + }) + .then(() => { + let offset = serverKeySeg.addressOffset || 384; + let portOffset = serverKeySeg.portOffset || 450; + let type = keyBuf[offset]; + let len = keyBuf[offset+1]; + let data = keyBuf.slice(offset + 2, offset + 2 + len); + let port = keyBuf[portOffset] << 8 | keyBuf[portOffset+1]; + + if (port === 0xFFFF){ + port = protocol === 'tcp' ? 5683 : 5684; + } + + let host = protocol === 'tcp' ? 'device.spark.io' : 'udp.particle.io'; - let host = protocol === 'tcp' ? 'device.spark.io' : 'udp.particle.io'; - if (len > 0) { - if (type === 0) { - host = Array.prototype.slice.call(data).join('.'); - } else if (type === 1) { - host = data.toString('utf8'); + if (len > 0){ + if (type === 0){ + host = Array.prototype.slice.call(data).join('.'); + } else if (type === 1){ + host = data.toString('utf8'); + } } - } - let result = { - hostname: host, - port: port, - protocol: protocol, - slashes: true - }; - console.log(); - console.log(url.format(result)); - return result; - }).catch(err => { - throw new VError(ensureError(err), 'Make sure your device is in DFU mode (blinking yellow), and is connected to your computer.'); - }); + let result = { + hostname: host, + port: port, + protocol: protocol, + slashes: true + }; + console.log(); + console.log(url.format(result)); + return result; + }) + .catch(err => { + throw new VError(ensureError(err), 'Make sure your device is in DFU mode (blinking yellow), and is connected to your computer.'); + }); } /** @@ -443,32 +438,33 @@ module.exports = class KeysCommand { * @param specs The this.dfu device sepcs. * @returns {Promise.} The */ - validateDeviceProtocol({ specs, protocol } = {}) { + validateDeviceProtocol({ specs, protocol } = {}){ specs = specs || deviceSpecs[this.dfu.dfuId]; - let protocolPromise = protocol ? Promise.resolve(protocol) : this.fetchDeviceProtocol(specs); - return protocolPromise.then(detectedProtocol => { - let supported = [specs.defaultProtocol]; - if (specs.alternativeProtocol) { - supported.push(specs.alternativeProtocol); - } - if (supported.indexOf(detectedProtocol)<0) { - throw new VError(`The device does not support the protocol ${detectedProtocol}. It has support for ${supported.join(', ')}`); - } - return detectedProtocol; - }); + return protocol ? Promise.resolve(protocol) : this.fetchDeviceProtocol(specs) + .then(detectedProtocol => { + let supported = [specs.defaultProtocol]; + if (specs.alternativeProtocol){ + supported.push(specs.alternativeProtocol); + } + if (supported.indexOf(detectedProtocol)<0){ + throw new VError(`The device does not support the protocol ${detectedProtocol}. It has support for ${supported.join(', ')}`); + } + return detectedProtocol; + }); } - _getServerKeySegmentName({ protocol }) { - if (!this.dfu.dfuId) { + _getServerKeySegmentName({ protocol }){ + if (!this.dfu.dfuId){ return; } let specs = deviceSpecs[this.dfu.dfuId]; - if (!specs) { + + if (!specs){ return; } - protocol = protocol || specs.defaultProtocol || 'tcp'; - return protocol + 'ServerKey'; + + return `${protocol || specs.defaultProtocol || 'tcp'}ServerKey`; } /** @@ -480,97 +476,109 @@ module.exports = class KeysCommand { * @param specs The this.dfu specs for the device * @returns {Promise.} The protocol configured on the device. */ - fetchDeviceProtocol(specs) { - if (specs.transport && specs.alternativeProtocol) { - return this.dfu.readBuffer('transport', false).then(buf => { - return buf[0]===0xFF ? specs.defaultProtocol : specs.alternativeProtocol; - }); + fetchDeviceProtocol(specs){ + if (specs.transport && specs.alternativeProtocol){ + return this.dfu.readBuffer('transport', false) + .then(buf => { + return buf[0] === 0xFF + ? specs.defaultProtocol + : specs.alternativeProtocol; + }); } return Promise.resolve(specs.defaultProtocol); } - _getServerKeySegment({ protocol }) { - if (!this.dfu.dfuId) { + _getServerKeySegment({ protocol }){ + if (!this.dfu.dfuId){ return; } + let specs = deviceSpecs[this.dfu.dfuId]; let segmentName = this._getServerKeySegmentName({ protocol }); - if (!specs || !segmentName) { + + if (!specs || !segmentName){ return; } + return specs[segmentName]; } - _getServerKeyAlgorithm({ protocol }) { + _getServerKeyAlgorithm({ protocol }){ let segment = this._getServerKeySegment({ protocol }); - if (!segment) { + + if (!segment){ return; } + return segment.alg || 'rsa'; } - _getServerKeyVariant({ protocol }) { + _getServerKeyVariant({ protocol }){ let segment = this._getServerKeySegment({ protocol }); - if (!segment) { + + if (!segment){ return; } + return segment.variant; } - _getPrivateKeySegmentName({ protocol }) { - if (!this.dfu.dfuId) { + _getPrivateKeySegmentName({ protocol }){ + if (!this.dfu.dfuId){ return; } let specs = deviceSpecs[this.dfu.dfuId]; - if (!specs) { + + if (!specs){ return; } - protocol = protocol || specs.defaultProtocol || 'tcp'; - return protocol + 'PrivateKey'; + + return `${protocol || specs.defaultProtocol || 'tcp'}PrivateKey`; } - _getPrivateKeySegment({ protocol }) { - if (!this.dfu.dfuId) { + _getPrivateKeySegment({ protocol }){ + if (!this.dfu.dfuId){ return; } + let specs = deviceSpecs[this.dfu.dfuId]; let segmentName = this._getPrivateKeySegmentName({ protocol }); - if (!specs || !segmentName) { + + if (!specs || !segmentName){ return; } + return specs[segmentName]; } - _getPrivateKeyAlgorithm({ protocol }) { + _getPrivateKeyAlgorithm({ protocol }){ let segment = this._getPrivateKeySegment({ protocol }); return (segment && segment.alg) || 'rsa'; } - _getDERPublicKey(filename, { protocol }) { + _getDERPublicKey(filename, { protocol }){ const { getFilenameExt, filenameNoExt, deferredChildProcess } = utilities; let alg = this._getServerKeyAlgorithm({ protocol }); - if (!alg) { + if (!alg){ throw new VError('No device specs'); } let variant = this._getServerKeyVariant({ protocol }); - if (!filename) { + if (!filename){ filename = this.serverKeyFilename({ alg, variant }); } - if (getFilenameExt(filename).toLowerCase() !== '.der') { + if (getFilenameExt(filename).toLowerCase() !== '.der'){ let derFile = filenameNoExt(filename) + '.der'; - if (!fs.existsSync(derFile)) { + if (!fs.existsSync(derFile)){ console.log('Creating DER format file'); let derFilePromise = deferredChildProcess(`openssl ${alg} -in "${filename}" -pubin -pubout -outform DER -out "${derFile}"`); return derFilePromise - .then(() => { - return derFile; - }) + .then(() => derFile) .catch(err => { throw new VError(ensureError(err), 'Error creating a DER formatted version of that key. Make sure you specified the public key'); }); @@ -581,31 +589,41 @@ module.exports = class KeysCommand { return Promise.resolve(filename); } - serverKeyFilename({ alg, variant }) { + serverKeyFilename({ alg, variant }){ const basename = variant ? `${alg}-${variant}` : alg; return path.join(__dirname, `../../assets/keys/${basename}.pub.der`); } - _formatPublicKey(filename, ipOrDomain, port, { protocol, outputFilename }) { + // eslint-disable-next-line max-statements + _formatPublicKey(filename, ipOrDomain, port, { protocol, outputFilename }){ let segment = this._getServerKeySegment({ protocol }); - if (!segment) { + + if (!segment){ throw new VError('No device specs'); } let buf, fileBuf; - if (ipOrDomain) { + + if (ipOrDomain){ let alg = segment.alg || 'rsa'; let fileWithAddress = `${utilities.filenameNoExt(filename)}-${utilities.replaceAll(ipOrDomain, '.', '_')}-${alg}.der`; - if (outputFilename) { + + if (outputFilename){ fileWithAddress = outputFilename; } + let addressBuf = this._createAddressBuffer(ipOrDomain); - // To generate a file like this, just add a type-length-value (TLV) encoded IP or domain beginning 384 bytes into the file—on external flash the address begins at 0x1180. - // Everything between the end of the key and the beginning of the address should be 0xFF. - // The first byte representing "type" is 0x00 for 4-byte IP address or 0x01 for domain name—anything else is considered invalid and uses the fallback domain. - // The second byte is 0x04 for an IP address or the length of the string for a domain name. - // The remaining bytes are the IP or domain name. If the length of the domain name is odd, add a zero byte to get the file length to be even as usual. + // To generate a file like this, just add a type-length-value (TLV) + // encoded IP or domain beginning 384 bytes into the file—on external + // flash the address begins at 0x1180. Everything between the end of + // the key and the beginning of the address should be 0xFF. The first + // byte representing "type" is 0x00 for 4-byte IP address or 0x01 for + // domain name—anything else is considered invalid and uses the + // fallback domain. The second byte is 0x04 for an IP address or the + // length of the string for a domain name. The remaining bytes are + // the IP or domain name. If the length of the domain name is odd, + // add a zero byte to get the file length to be even as usual. buf = new Buffer(segment.size); @@ -619,7 +637,7 @@ module.exports = class KeysCommand { let offset = segment.addressOffset || 384; addressBuf.copy(buf, offset, 0, addressBuf.length); - if (port && segment.portOffset) { + if (port && segment.portOffset){ buf.writeUInt16BE(port, segment.portOffset); } @@ -627,28 +645,29 @@ module.exports = class KeysCommand { //console.log("Key chunk is now: " + buf.toString('hex')); fs.writeFileSync(fileWithAddress, buf); - return fileWithAddress; } let stats = fs.statSync(filename); - if (stats.size < segment.size) { + + if (stats.size < segment.size){ let fileWithSize = `${utilities.filenameNoExt(filename)}-padded.der`; - if (outputFilename) { + + if (outputFilename){ fileWithSize = outputFilename; } - if (!fs.existsSync(fileWithSize)) { - buf = new Buffer(segment.size); + if (!fs.existsSync(fileWithSize)){ + buf = new Buffer(segment.size); fileBuf = fs.readFileSync(filename); fileBuf.copy(buf, 0, 0, fileBuf.length); - buf.fill(255, fileBuf.length); - fs.writeFileSync(fileWithSize, buf); } + return fileWithSize; } + return filename; } }; diff --git a/src/cmd/keys.test.js b/src/cmd/keys.test.js index 329ac32bc..ade69a944 100644 --- a/src/cmd/keys.test.js +++ b/src/cmd/keys.test.js @@ -53,7 +53,7 @@ describe('Key Command', () => { it('Can create device key', () => { setupCommand(); - return key.makeNewKey('', {}).then(() => { + return key.makeNewKey({ params: {} }).then(() => { expect(utilities.deferredChildProcess).to.have.property('callCount', 3); }); }); @@ -78,9 +78,9 @@ describe('Key Command', () => { key._makeNewKey = sinon.stub(); key._writeKeyToDevice = sinon.stub(); key._sendPublicKeyToServer = sinon.stub(); - return key.keyDoctor('ABcd', {}).then(() => { + return key.keyDoctor({ params: { deviceID: 'ABcd' } }).then(() => { expect(key._sendPublicKeyToServer).to.be.calledWith({ - deviceId: 'abcd', filename: 'abcd_rsa_new', algorithm: 'rsa' + deviceID: 'abcd', filename: 'abcd_rsa_new', algorithm: 'rsa' }); }); }); @@ -150,7 +150,7 @@ describe('Key Command', () => { dfu.write = sinon.stub(); key.validateDeviceProtocol = sinon.stub().returns('tcp'); filename = key.serverKeyFilename({ alg: 'rsa' }); - return key.writeKeyToDevice(filename) + return key.writeKeyToDevice({ params: { filename } }) .then(() => { expect(key.validateDeviceProtocol).to.have.been.called; expect(dfu.write).to.have.been.calledWith(filename, 'tcpPrivateKey', false); @@ -166,7 +166,7 @@ describe('Key Command', () => { it('reads device protocol when the device supports multiple protocols and no protocol is given, alternate protocol', () => { transport.push(0x00); - return key.saveKeyFromDevice(filename, {}) + return key.saveKeyFromDevice({ params: { filename } }) .then(() => { expect(dfu.readBuffer).to.have.been.calledWith('transport', false); expect(dfu.read).to.have.been.calledWith(keyFilename, 'tcpPrivateKey', false); @@ -176,7 +176,7 @@ describe('Key Command', () => { it('reads device protocol when the device supports multiple protocols and no protocol is given, default protocol', () => { transport.push(0xFF); - return key.saveKeyFromDevice(filename, {}) + return key.saveKeyFromDevice({ params: { filename } }) .then(() => { expect(dfu.readBuffer).to.have.been.calledWith('transport', false); expect(dfu.read).to.have.been.calledWith(keyFilename, 'udpPrivateKey', false); @@ -186,7 +186,7 @@ describe('Key Command', () => { it('raises an error when the protocol is not recognized', () => { key.validateDeviceProtocol = sinon.stub().returns('zip'); - return key.saveKeyFromDevice(filename, {}) + return key.saveKeyFromDevice({ params: { filename } }) .catch((err) => { expect(err).to.equal('Error saving key from device... The device does not support the protocol zip. It has support for udp, tcp'); }); @@ -196,7 +196,7 @@ describe('Key Command', () => { key.validateDeviceProtocol = sinon.stub().returns('tcp'); key.fetchDeviceProtocol = sinon.stub(); - return key.saveKeyFromDevice(filename, {}) + return key.saveKeyFromDevice({ params: { filename } }) .then(() => { expect(key.fetchDeviceProtocol).to.not.have.been.called; expect(dfu.read).to.have.been.calledWith(keyFilename, 'tcpPrivateKey', false); diff --git a/test/e2e/keys.e2e.js b/test/e2e/keys.e2e.js index 7fccd580e..04adfbe01 100644 --- a/test/e2e/keys.e2e.js +++ b/test/e2e/keys.e2e.js @@ -6,12 +6,16 @@ const openSSL = require('../lib/open-ssl'); const cli = require('../lib/cli'); const fs = require('../lib/fs'); const { + DEVICE_ID, DEVICE_NAME, - PATH_TMP_DIR + PATH_TMP_DIR, + DEVICE_PLATFORM_NAME } = require('../lib/env'); describe('Keys Commands [@device]', () => { + const extendedTimeout = 5 * 60 * 1000; + const help = [ 'Manage your device\'s key pair and server public key', 'Usage: particle keys ', @@ -45,6 +49,11 @@ describe('Keys Commands [@device]', () => { await Promise.all([openSSL.ensureExists(), dfuUtil.ensureExists()]); }); + after(async () => { + await cli.logout(); + await cli.setDefaultProfile(); + }); + it('Shows `help` content', async () => { const { stdout, stderr, exitCode } = await cli.run(['help', 'keys']); @@ -181,15 +190,44 @@ describe('Keys Commands [@device]', () => { }); }); - describe('Server Subcommand', () => { + describe('Doctor Subcommand', () => { before(async () => { await cli.setTestProfileAndLogin(); await cli.flashStrobyFirmwareOTAForTest(); }); - after(async () => { - await cli.logout(); - await cli.setDefaultProfile(); + it('Fixes devices keys', async () => { + await cli.enterDFUMode(); + const { stdout, stderr, exitCode } = await cli.run(['keys', 'doctor', DEVICE_ID]); + const log = [ + 'New Key Created!', + 'Saved!', + 'Saved!', + `attempting to add a new public key for device ${DEVICE_ID}`, + 'submitting public key succeeded!', + 'Okay! New keys in place, your device should restart.', + ]; + + expect(stdout.split('\n')).to.include.members(log); + expect(stderr).to.equal(''); + expect(exitCode).to.equal(0); + + await cli.waitForVariable('name', 'stroby'); + }).timeout(extendedTimeout); + + it('Fails to fix device keys when device is not in DFU mode', async () => { + const { stdout, stderr, exitCode } = await cli.run(['keys', 'doctor', DEVICE_ID]); + + expect(stdout.split('\n')).to.include.members(dfuInstructions); + expect(stderr).to.equal(''); + expect(exitCode).to.equal(1); + }); + }); + + describe('Server Subcommand', () => { + before(async () => { + await cli.setTestProfileAndLogin(); + await cli.flashStrobyFirmwareOTAForTest(); }); it('Switches server public keys', async () => { @@ -202,7 +240,7 @@ describe('Keys Commands [@device]', () => { await cli.resetDevice(); await cli.waitForVariable('name', 'stroby'); - }).timeout(5 * 60 * 1000); + }).timeout(extendedTimeout); it('Fails to switch server public keys when device is not in DFU mode', async () => { const { stdout, stderr, exitCode } = await cli.run(['keys', 'server']); @@ -212,5 +250,50 @@ describe('Keys Commands [@device]', () => { expect(exitCode).to.equal(1); }); }); + + describe('Address Subcommand', () => { + it('Reads server address from device\'s server public key', async () => { + await cli.enterDFUMode(); + const { stdout, stderr, exitCode } = await cli.run(['keys', 'address']); + const addressPtn = /(udp|tcp):\/\/(\$id\.udp\.particle\.io|\$id\.udp-mesh\.particle\.io|device\.spark\.io):?(\d{1,5})?/; + + expect(stdout.trim()).to.match(addressPtn); + expect(stderr).to.equal(''); + expect(exitCode).to.equal(0); + + await cli.resetDevice(); + }).timeout(extendedTimeout); + + it('Fails to save device keys when device is not in DFU mode', async () => { + const { stdout, stderr, exitCode } = await cli.run(['keys', 'address']); + + expect(stdout.split('\n')).to.include.members(dfuInstructions); + expect(stderr).to.equal(''); + expect(exitCode).to.equal(1); + }); + }); + + describe('Protocol Subcommand', () => { + it('Reads server address from device\'s server public key', async () => { + await cli.enterDFUMode(); + const { stdout, stderr, exitCode } = await cli.run(['keys', 'protocol']); + const protocolPtn = /(udp|tcp)$/; + + expect(stdout).to.include('Device protocol is set to '); + expect(stdout.trim()).to.match(protocolPtn); + expect(stderr).to.equal(''); + expect(exitCode).to.equal(0); + + await cli.resetDevice(); + }).timeout(extendedTimeout); + + it('Fails to save device keys when device is not in DFU mode', async () => { + const { stdout, stderr, exitCode } = await cli.run(['keys', 'protocol']); + + expect(stdout.split('\n')).to.include.members(dfuInstructions); + expect(stderr).to.equal(''); + expect(exitCode).to.equal(1); + }); + }); }); From 42091eee5bcb6779c163a30434ec9d9d83b14b0a Mon Sep 17 00:00:00 2001 From: busticated Date: Wed, 22 Apr 2020 21:20:23 -0700 Subject: [PATCH 6/7] add e2e tests for keys server subcommand w/ --deviceType flag --- src/cmd/keys.js | 7 +++++++ test/e2e/keys.e2e.js | 23 +++++++++++++++++++++++ 2 files changed, 30 insertions(+) diff --git a/src/cmd/keys.js b/src/cmd/keys.js index c719713dc..b31e44a56 100644 --- a/src/cmd/keys.js +++ b/src/cmd/keys.js @@ -7,6 +7,7 @@ const utilities = require('../lib/utilities'); const ApiClient = require('../lib/api-client'); const deviceSpecs = require('../lib/deviceSpecs'); const ensureError = require('../lib/utilities').ensureError; +const { errors: { usageError } } = require('../app/command-processor'); const dfu = require('../lib/dfu'); /** @@ -328,6 +329,12 @@ module.exports = class KeysCommand { } writeServerPublicKey({ protocol, host, port, deviceType, params: { filename, outputFilename } }){ + if (deviceType && !filename){ + throw usageError( + '`filename` parameter is required when `--deviceType` is set' + ); + } + if (filename && !fs.existsSync(filename)){ // TODO UsageError throw new VError('Please specify a server key in DER format.'); diff --git a/test/e2e/keys.e2e.js b/test/e2e/keys.e2e.js index 04adfbe01..2ac0b7a50 100644 --- a/test/e2e/keys.e2e.js +++ b/test/e2e/keys.e2e.js @@ -249,6 +249,29 @@ describe('Keys Commands [@device]', () => { expect(stderr).to.equal(''); expect(exitCode).to.equal(1); }); + + it('Saves server public keys locally when `--deviceType` flag is set', async () => { + const filename = path.join(PATH_TMP_DIR, `${DEVICE_NAME}-test.der`); + await cli.run(['keys', 'new', filename, '--protocol', 'udp'], { reject: true }); + const args = ['keys', 'server', filename, '--deviceType', DEVICE_PLATFORM_NAME]; + const { stdout, stderr, exitCode } = await cli.debug(args); + + expect(stdout).to.equal('Okay! Formated server key file generated for this type of device.'); + // TODO (mirande): fix `(node:3228) [DEP0005] DeprecationWarning: + // Buffer() is deprecated due to security and usability issues. + // Please use the Buffer.alloc(), Buffer.allocUnsafe(), or Buffer.from() + // methods instead. + expect(stderr).to.exist; + expect(exitCode).to.equal(0); + }).timeout(extendedTimeout); + + it('Fails when `--deviceType` is set but `filename` param is omitted', async () => { + const { stdout, stderr, exitCode } = await cli.run(['keys', 'server', '--deviceType', 'Electron']); + + expect(stdout).to.equal('`filename` parameter is required when `--deviceType` is set'); + expect(stderr).to.include('Usage: particle keys server [options] [filename] [outputFilename]'); + expect(exitCode).to.equal(1); + }); }); describe('Address Subcommand', () => { From 0214d89fbbbee40d21e2ebf1f62b7795e2a8e06f Mon Sep 17 00:00:00 2001 From: busticated Date: Thu, 23 Apr 2020 09:27:31 -0700 Subject: [PATCH 7/7] tune e2e tests for variable command to improve reliability --- test/e2e/get.e2e.js | 6 +++++- test/e2e/variable.e2e.js | 6 +++++- 2 files changed, 10 insertions(+), 2 deletions(-) diff --git a/test/e2e/get.e2e.js b/test/e2e/get.e2e.js index 507654629..eae36b993 100644 --- a/test/e2e/get.e2e.js +++ b/test/e2e/get.e2e.js @@ -1,5 +1,7 @@ +const os = require('os'); const { expect } = require('../setup'); const { delay } = require('../lib/mocha-utils'); +const stripANSI = require('../lib/ansi-strip'); const cli = require('../lib/cli'); const { DEVICE_ID, @@ -66,8 +68,10 @@ describe('Get Commands [@device]', () => { subprocess.stdin.end('\n'); const { stdout, stderr, exitCode } = await subprocess; + const log = stripANSI(stdout); - expect(stdout).to.include('Which variable did you want? (Use arrow keys)'); + expect(log).to.include('Which variable did you want?'); + expect(log).to.include(`name (string)${os.EOL}stroby`); expect(stderr).to.equal(''); expect(exitCode).to.equal(0); }); diff --git a/test/e2e/variable.e2e.js b/test/e2e/variable.e2e.js index 84fd3b1a0..4f7f8b9f7 100644 --- a/test/e2e/variable.e2e.js +++ b/test/e2e/variable.e2e.js @@ -1,5 +1,7 @@ +const os = require('os'); const { expect } = require('../setup'); const { delay } = require('../lib/mocha-utils'); +const stripANSI = require('../lib/ansi-strip'); const cli = require('../lib/cli'); const { DEVICE_ID, @@ -86,8 +88,10 @@ describe('Variable Commands [@device]', () => { subprocess.stdin.end('\n'); const { stdout, stderr, exitCode } = await subprocess; + const log = stripANSI(stdout); - expect(stdout).to.include('Which variable did you want? (Use arrow keys)'); + expect(log).to.include('Which variable did you want?'); + expect(log).to.include(`name (string)${os.EOL}stroby`); expect(stderr).to.equal(''); expect(exitCode).to.equal(0); });