UNPKG

particle-cli

Version:

Simple Node commandline application for working with your Particle devices and using the Particle Cloud

561 lines (469 loc) 18.8 kB
const fs = require('fs'); const url = require('url'); const path = require('path'); const settings = require('../../settings'); const usbUtils = require('./usb-util'); const VError = require('verror'); const temp = require('temp').track(); const utilities = require('../lib/utilities'); const ApiClient = require('../lib/api-client'); const { keysDctOffsets } = require('../lib/keys-specs'); const { platforms } = require('@particle/device-constants'); const ensureError = require('../lib/utilities').ensureError; const { errors: { usageError } } = require('../app/command-processor'); const UI = require('../lib/ui'); const ParticleApi = require('./api'); const { DeviceProtectionError } = require('particle-usb'); /** * Commands for managing encryption keys. * @constructor */ module.exports = class KeysCommand { constructor(){ this.platform = null; this.auth = settings.access_token; this.api = new ParticleApi(settings.apiUrl, { accessToken: this.auth }).api; this.ui = new UI({ stdin: process.stdin, stdout: process.stdout, stderr: process.stderr, quiet: false }); } async makeKeyOpenSSL(filename, alg) { try { const { filenameNoExt, deferredChildProcess } = utilities; filename = filenameNoExt(filename); if (alg === 'rsa'){ await deferredChildProcess(`openssl genrsa -out "${filename}.pem" 1024`); } else if (alg === 'ec'){ await deferredChildProcess(`openssl ecparam -name prime256v1 -genkey -out "${filename}.pem"`); } await deferredChildProcess(`openssl ${alg} -in "${filename}.pem" -pubout -out "${filename}.pub.pem"`); await deferredChildProcess(`openssl ${alg} -in "${filename}.pem" -outform DER -out "${filename}.der"`); return `${filename}.der`; } catch (err) { throw new VError(ensureError(err), 'Failed to generate key using OpenSSL'); } } async makeNewKey({ params: { filename } }) { await this._makeNewKey({ filename }); } async _makeNewKey({ filename, deviceID }) { try { await usbUtils.executeWithUsbDevice({ args: { idOrName: deviceID, api: this.api, auth: this.auth, ui: this.ui }, func: (dev) => this._makeNewKeyImpl(dev, filename), enterDfuMode: true }); } catch (err) { throw new VError(ensureError(err), 'Error creating keys'); } } async _makeNewKeyImpl(device, filename) { this.platform = device._info.type; const protocol = this._getDeviceProtocol(); const alg = this._getPrivateKeyAlgorithm({ protocol }); filename = await this.makeKeyOpenSSL(filename || device.id, alg); console.log(`New key ${path.basename(filename)} created for device ${device.id}`); } async writeKeyToDevice({ params: { filename } }) { await this._writeKeyToDevice({ filename }); } async _writeKeyToDevice({ filename, leave = false, deviceID, allowProtectedDevices = false }) { try { await usbUtils.executeWithUsbDevice({ args: { idOrName: deviceID, api: this.api, auth: this.auth, ui: this.ui }, func: (dev) => this._writeKeyToDeviceImpl(dev, filename, leave), enterDfuMode: true, allowProtectedDevices }); } catch (err) { throw new VError(ensureError(err), 'Error writing key to device.'); } } async _writeKeyToDeviceImpl(device, filename, leave) { console.log('Writing key to device'); this.platform = device._info.type; filename = utilities.filenameNoExt(filename || device.id) + '.der'; console.log(`Writing key ${filename} to device ${device.id}`); if (!fs.existsSync(filename)){ throw new VError("I couldn't find the file: " + filename); } const protocol = this._getDeviceProtocol(); const alg = this._getPrivateKeyAlgorithm({ protocol }); let prefilename = path.join(path.dirname(filename), 'backup_' + alg + '_' + path.basename(filename)); await this._saveKeyFromDevice({ filename: prefilename, force: true, device }); let segment = this._getPrivateKeySegment({ protocol }); const buffer = fs.readFileSync(filename, null); // 'null' to get the raw data await this._dfuWrite(device, buffer, { altSetting: segment.alt, startAddr: segment.address, leave: leave, noErase: true }); console.log(`Key ${filename} written to device`); } async saveKeyFromDevice({ force, params: { filename } }){ await usbUtils.executeWithUsbDevice({ args: { api: this.api, auth: this.auth, ui: this.ui }, func: (device) => this._saveKeyFromDevice({ force, filename, device }), enterDfuMode: true }); } async _saveKeyFromDevice({ force, filename, device }) { this.platform = device._info.type; filename = utilities.filenameNoExt(filename || device.id) + '.der'; await this._saveKeyFromDeviceImpl({ filename, force, device }); } async _saveKeyFromDeviceImpl({ filename, force, device }) { const { tryDelete, filenameNoExt, deferredChildProcess } = utilities; if (!force && fs.existsSync(filename)) { throw new VError(`The file ${filename} already exists, please specify a different file, or use the --force flag.`); } else if (fs.existsSync(filename)) { tryDelete(filename); } try { const protocol = this._getDeviceProtocol(); let segment = this._getPrivateKeySegment({ protocol }); const buf = await this._dfuRead(device, { altSetting: segment.alt, startAddr: segment.address, size: segment.size }); fs.writeFileSync(filename, buf, 'binary'); let pubPemFilename = filenameNoExt(filename) + '.pub.pem'; if (force) { tryDelete(pubPemFilename); } let alg = this._getPrivateKeyAlgorithm({ protocol }); await deferredChildProcess(`openssl ${alg} -in "${filename}" -inform DER -pubout -out ${pubPemFilename}`) .catch((err) => { throw new VError(err, 'Unable to generate a public key from the key downloaded from the device. This usually means you had a corrupt key on the device.'); }); console.log(`Saved existing key to ${filename}`); } catch (err) { return new VError(ensureError(err), 'Error saving key from device'); } } async sendPublicKeyToServer({ product_id: productId, params: { deviceID, filename } }){ await this._sendPublicKeyToServer({ deviceID, filename, productId }); } async _sendPublicKeyToServer({ deviceID, filename, productId }) { const { filenameNoExt, deferredChildProcess, readFile } = utilities; if (!deviceID) { // default to the connected device if Device ID is not passed // This is only to get the Device ID let device = await usbUtils.getOneUsbDevice({ ui: this.ui }); deviceID = device.id; await device.close(); } deviceID = deviceID.toLowerCase(); filename = filename || deviceID; if (!fs.existsSync(filename)){ filename = filenameNoExt(filename) + '.pub.pem'; if (!fs.existsSync(filename)){ throw new VError("Couldn't find " + filename); } } let api = new ApiClient(); api.ensureToken(); let pubKey = temp.path({ suffix: '.pub.pem' }); let inform = path.extname(filename).toLowerCase() === '.der' ? 'DER' : 'PEM'; const cleanup = () => fs.unlinkSync(pubKey); try { let algorithm = 'rsa'; // try both private and public versions and both algorithms await deferredChildProcess(`openssl ${algorithm} -inform ${inform} -in "${filename}" -pubout -outform PEM -out "${pubKey}"`) .catch(() => { return deferredChildProcess(`openssl ${algorithm} -pubin -inform ${inform} -in "${filename}" -pubout -outform PEM -out "${pubKey}"`); }) .catch(() => { // try other algorithm next algorithm = algorithm === 'rsa' ? 'ec' : 'rsa'; return deferredChildProcess(`openssl ${algorithm} -inform ${inform} -in "${filename}" -pubout -outform PEM -out "${pubKey}"`); }) .catch(() => { return deferredChildProcess(`openssl ${algorithm} -pubin -inform ${inform} -in "${filename}" -pubout -outform PEM -out "${pubKey}"`); }); const keyBuf = await readFile(pubKey); let apiAlg = algorithm === 'rsa' ? 'rsa' : 'ecc'; await api.sendPublicKey(deviceID, keyBuf, apiAlg, productId); } catch (err) { cleanup(); throw new VError(ensureError(err), 'Error sending public key to server'); } } async keyDoctor({ params: { deviceID } }) { if (deviceID) { deviceID = deviceID.toLowerCase(); // make lowercase so that it's case-insensitive 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'); console.log('***************************************************************'); } } try { await usbUtils.executeWithUsbDevice({ args: { idOrName: deviceID, api: this.api, auth: this.auth, ui: this.ui }, func: (dev) => this._keyDoctor(dev), enterDfuMode: true }); } catch (err) { throw new VError(ensureError(err), 'Make sure your device is connected to your computer, and that your computer is online'); } } async _keyDoctor(device) { const deviceID = device.id; this.platform = device._info.type; await device.close(); const protocol = this._getDeviceProtocol(); const algorithm = this._getPrivateKeyAlgorithm({ protocol }); const filename = `${deviceID}_${algorithm}_new`; await this._makeNewKey({ filename, deviceID }); await this._writeKeyToDevice({ filename, leave: true, deviceID }); await this._sendPublicKeyToServer({ deviceID, filename, algorithm }); console.log('Okay! New keys in place, your device should restart.'); } _createAddressBuffer(ipOrDomain){ const isIpAddress = /^[0-9.]*$/.test(ipOrDomain); // create a version of this key that points to a particular server or domain const addressBuf = Buffer.alloc(ipOrDomain.length + 2); addressBuf[0] = (isIpAddress) ? 0 : 1; addressBuf[1] = (isIpAddress) ? 4 : ipOrDomain.length; if (isIpAddress){ const parts = ipOrDomain.split('.').map((obj) => { return parseInt(obj); }); addressBuf[2] = parts[0]; addressBuf[3] = parts[1]; addressBuf[4] = parts[2]; addressBuf[5] = parts[3]; return addressBuf.slice(0, 6); } else { addressBuf.write(ipOrDomain, 2); } return addressBuf; } async writeServerPublicKey({ 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.'); } try { let noDevice = false; if (deviceType) { if (!platforms[deviceType]) { throw new VError(`Unknown device type ${deviceType}. Use one of ${Object.keys(platforms).join(', ')}`); } this.platform = deviceType; noDevice = true; } else { await usbUtils.executeWithUsbDevice({ args: { api: this.api, auth: this.auth, ui: this.ui }, func: (dev) => this._writeServerPublicKey(dev, noDevice, host, port, filename, outputFilename), enterDfuMode: true, allowProtectedDevices: false }); } } catch (err) { throw new VError(ensureError(err), 'Make sure your device is connected to your computer'); } } async _writeServerPublicKey(device, noDevice, host, port, filename, outputFilename) { this.platform = device._info.type; const protocol = this._getDeviceProtocol(); const derFile = await this._getDERPublicKey(filename, { protocol }); const bufferFile = await this._formatPublicKey(derFile, host, port, { protocol, outputFilename }); const segment = this._getServerKeySegment({ protocol }); if (!noDevice) { const buffer = fs.readFileSync(bufferFile); await this._dfuWrite(device, buffer, { altSetting: segment.alt, startAddr: segment.address, leave: false, noErase: true }); } if (!noDevice){ console.log('Okay! New keys in place, your device will not restart.'); } else { console.log('Okay! Formatted server key file generated for this type of device.'); } } async readServerAddress() { try { await usbUtils.executeWithUsbDevice({ args: { api: this.api, auth: this.auth, ui: this.ui }, func: (dev) => this._readServerAddress(dev), enterDfuMode: true }); } catch (err) { throw new VError(ensureError(err), 'Make sure your device is connected to your computer'); } } async _readServerAddress(device) { this.platform = device._info.type; const protocol = this._getDeviceProtocol(); const segment = this._getServerKeySegment({ protocol }); const keyBuf = await this._dfuRead(device, { altSetting: segment.alt, startAddr: segment.address, size: segment.size }); let offset = segment.addressOffset || 384; let portOffset = segment.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'; 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)); } _getServerKeySegment({ protocol }){ let segmentName = `${protocol}ServerKey`; return this._getDctKeySegments()[segmentName]; } _getServerKeyAlgorithm({ protocol }){ let segment = this._getServerKeySegment({ protocol }); return segment.alg; } _getPrivateKeySegment({ protocol }){ let segmentName = `${protocol}PrivateKey`; return this._getDctKeySegments()[segmentName]; } _getPrivateKeyAlgorithm({ protocol }){ let segment = this._getPrivateKeySegment({ protocol }); return segment.alg; } async _getDERPublicKey(filename, { protocol }) { const { getFilenameExt, filenameNoExt, deferredChildProcess } = utilities; let alg = this._getServerKeyAlgorithm({ protocol }); if (!alg){ throw new VError('Unable to get the algorithm for that protocol'); } if (!filename){ filename = this.serverKeyFilename({ alg }); } if (getFilenameExt(filename).toLowerCase() !== '.der'){ let derFile = filenameNoExt(filename) + '.der'; if (!fs.existsSync(derFile)){ console.log('Creating DER format file'); try { derFile = await deferredChildProcess(`openssl ${alg} -in "${filename}" -pubin -pubout -outform DER -out "${derFile}"`); return derFile; } catch (err) { throw new VError(ensureError(err), 'Error creating a DER formatted version of that key. Make sure you specified the public key'); } } else { return derFile; } } return filename; } serverKeyFilename({ alg }){ return path.join(__dirname, `../../assets/keys/${alg}.pub.der`); } // eslint-disable-next-line max-statements _formatPublicKey(filename, ipOrDomain, port, { protocol, outputFilename }){ let segment = this._getServerKeySegment({ protocol }); if (!segment){ throw new VError('No segment found for that protocol'); } let buf, fileBuf; 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. 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 = Buffer.alloc(segment.size); //copy in the key fileBuf = fs.readFileSync(filename); fileBuf.copy(buf, 0, 0, fileBuf.length); //fill the rest with "FF" buf.fill(255, fileBuf.length); let offset = segment.addressOffset || 384; addressBuf.copy(buf, offset, 0, addressBuf.length); if (port && segment.portOffset){ buf.writeUInt16BE(port, segment.portOffset); } //console.log("address chunk is now: " + addressBuf.toString('hex')); //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){ let fileWithSize = `${utilities.filenameNoExt(filename)}-padded.der`; if (outputFilename){ fileWithSize = outputFilename; } if (!fs.existsSync(fileWithSize)){ buf = Buffer.alloc(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; } /** * Resets the device and waits for it to restart. * * @async * @param {Object} device - The device to reset. * @returns {Promise<void>} */ async _putDeviceInSafeMode(device) { await device.enterSafeMode(); await device.close(); device = await usbUtils.reopenInNormalMode( { id: device.id }); } _getDctKeySegments() { if (this.platform === 'core') { return keysDctOffsets['generation1']; } else { return keysDctOffsets['laterGenerations']; } } _getDeviceProtocol() { return platforms[this.platform].features.includes('tcp') ? 'tcp' : 'udp'; } async _dfuWrite(device, buffer, { altSetting, startAddr, leave, noErase }) { try { await device.writeOverDfu(buffer, { altSetting, startAddr, leave, noErase }); } catch (err) { if (err instanceof DeviceProtectionError) { throw new Error('Operation could not be completed due to Device Protection.'); } throw new VError(ensureError(err), 'Writing over DFU failed'); } } async _dfuRead(device, { altSetting, startAddr, size }) { let buf; try { buf = await device.readOverDfu({ altSetting, startAddr, size }); } catch (err) { if (err instanceof DeviceProtectionError) { throw new Error('Operation could not be completed due to Device Protection.'); } // FIXME: First time read may fail so we retry try { buf = await device.readOverDfu({ altSetting, startAddr, size }); } catch (err) { throw new VError(ensureError(err), 'Reading over DFU failed'); } } return buf; } };