diff --git a/src/cli/device-protection.js b/src/cli/device-protection.js index 22701252a..31e393a97 100644 --- a/src/cli/device-protection.js +++ b/src/cli/device-protection.js @@ -1,7 +1,7 @@ const unindent = require('../lib/unindent'); module.exports = ({ commandProcessor, root }) => { - const deviceProtection = commandProcessor.createCategory(root, 'protection', 'Commands for managing device protection'); + const deviceProtection = commandProcessor.createCategory(root, 'device-protection', 'Commands for managing device protection'); commandProcessor.createCommand(deviceProtection, 'status', 'Gets the current device protection status', { handler: () => { @@ -14,18 +14,28 @@ module.exports = ({ commandProcessor, root }) => { }); commandProcessor.createCommand(deviceProtection, 'disable', 'Disables device protection (temporary or permanent)', { - params: '[permanent]', + options: { + 'open': { + boolean: true, + description: 'Unlocks a protected device and makes it an Open device' + } + }, handler: (args) => { const DeviceProtectionCommands = require('../cmd/device-protection'); - return new DeviceProtectionCommands().disableProtection(args.params); + return new DeviceProtectionCommands(args).disableProtection(args); }, examples: { - '$0 $command': 'Disables device protection temporarily', - '$0 $command permanent': 'Disables device protection permanently' + '$0 $command': 'Device is temporarily unprotected', + '$0 $command --open': '[TBD] Device becomes an Open device' } }); commandProcessor.createCommand(deviceProtection, 'enable', 'Enables device protection', { + options: { + file: { + description: 'Provide file to use for device protection' + } + }, handler: (args) => { const DeviceProtectionCommands = require('../cmd/device-protection'); return new DeviceProtectionCommands().enableProtection(args); @@ -39,7 +49,7 @@ module.exports = ({ commandProcessor, root }) => { params: '', handler: (args) => { const DeviceProtectionCommands = require('../cmd/device-protection'); - return new DeviceProtectionCommands().protectBinary(args.params.file); + return new DeviceProtectionCommands().protectBinary({ file: args.params.file, verbose: true }); }, examples: { '$0 $command myBootloader.bin': 'Adds device-protection to your bootloader binary' diff --git a/src/cmd/api.js b/src/cmd/api.js index 2815b2c13..20031ab32 100644 --- a/src/cmd/api.js +++ b/src/cmd/api.js @@ -297,6 +297,15 @@ module.exports = class ParticleApi { })); } + getProduct({ product, auth, headers, context }) { + return this._wrap(this.api.getProduct({ + product, + auth, + headers, + context + })); + } + _wrap(promise){ return Promise.resolve(promise) .then(result => result.body || result) diff --git a/src/cmd/device-protection.js b/src/cmd/device-protection.js index b73f8833e..8e3728d79 100644 --- a/src/cmd/device-protection.js +++ b/src/cmd/device-protection.js @@ -11,7 +11,6 @@ const fs = require('fs-extra'); const { downloadDeviceOsVersionBinaries } = require('../lib/device-os-version-util'); const FlashCommand = require('./flash'); const { platformForId } = require('../lib/platform'); -const chalk = require('chalk'); module.exports = class DeviceProtectionCommands extends CLICommandBase { constructor({ ui } = {}) { @@ -36,34 +35,38 @@ module.exports = class DeviceProtectionCommands extends CLICommandBase { try { s = await this.device.getProtectionState(); } catch (error) { - if (error.message === 'Not implemented') { - throw new Error(`Device protection status is not supported on this device${os.EOL}${os.EOL}`); + if (error.message === 'Not supported') { + throw new Error(`Device protection feature is not supported on this device`); } - throw new Error('Failed to get device protection status'); } let res; if (!s.protected && !s.overridden) { - res = 'not protected'; + res = 'Open (not protected)'; } else if (s.protected && !s.overridden) { - res = 'protected'; + res = 'Protected'; } else if (s.overridden) { - res = 'temporarily not protected. A reset is required to re-enable protection.'; + res = `Protected (service mode)${os.EOL}Device is put into service mode for a total of 20 reboots or 24 hours.`; } - this.ui.stdout.write(`Device (${this.deviceId}) is ${chalk.bold(res)}${os.EOL}${os.EOL}`); + this.ui.stdout.write(`Device protection: ${res}${os.EOL}`); return s; }); } - async disableProtection({ permanent }) { - // TODO : Remove logs with sensitive information - - return this._withDevice(async () => { - let s = await this.device.getProtectionState(); + async disableProtection({ open } = {}) { + return this._withDevice(async () => { + let s; + try { + s = await this.device.getProtectionState(); + } catch (error) { + if (error.message === 'Not supported') { + throw new Error(`Device protection feature is not supported on this device${os.EOL}`); + } + } if (!s.protected && !s.overridden) { - this.ui.stdout.write(`Device is not protected${os.EOL}${os.EOL}`); + this.ui.stdout.write(`Device is not protected${os.EOL}`); return; } @@ -102,19 +105,18 @@ module.exports = class DeviceProtectionCommands extends CLICommandBase { await this.device.unprotectDevice({ action: 'confirm', serverSignature, serverPublicKeyFingerprint }); s = await this.device.getProtectionState(); - if (!permanent) { - this.ui.stdout.write(`Device protection ${chalk.bold('temporarily disabled')}${os.EOL}${os.EOL}`); + if (!open) { + this.ui.stdout.write(`Device protection temporarily disabled.${os.EOL}Device is put into service mode for 20 reboots or 24 hours.${os.EOL}`); + return; } - if (permanent) { + if (open) { const localBootloaderPath = await this._downloadBootloader(); - await this._flashBootloader(localBootloaderPath); - - this.ui.stdout.write(os.EOL); + await this._flashBootloader(localBootloaderPath, 'disable'); - this.ui.stdout.write(`Device is permanently unlocked${os.EOL}${os.EOL}`); + this.ui.stdout.write(`Device protection disabled.${os.EOL}Device is open${os.EOL}`); const success = await this._markAsDevelopmentDevice(true); @@ -126,7 +128,6 @@ module.exports = class DeviceProtectionCommands extends CLICommandBase { } async _downloadBootloader() { - let version; const modules = await this.device.getFirmwareModuleInfo(); modules.forEach((m) => { @@ -147,62 +148,91 @@ module.exports = class DeviceProtectionCommands extends CLICommandBase { - async enableProtection() { + async enableProtection({ file } = {}) { // TODO: Option to provide bootloader binary in the path - // TODO: error if device protection is not supported on this firmware version + // TODO: Log better error if device protection is not supported on this firmware version + let protectedBinary = file; return this._withDevice(async () => { - let s = await this.device.getProtectionState(); + let s; + try { + s = await this.device.getProtectionState(); + } catch (error) { + if (error.message === 'Not supported') { + throw new Error(`Device protection feature is not supported on this device`); + } + } + + const attrs = await this.api.getDeviceAttributes(this.deviceId); + let deviceProtectionActiveInProduct = false; + if (attrs.platform_id !== attrs.product_id) { + // it's in a product + const res = await this.api.getProduct({ product: attrs.product_id, auth: settings.access_token }); + deviceProtectionActiveInProduct = res.product.device_protection; + } + if (s.protected && !s.overridden) { - this.ui.stdout.write(`Device is protected${os.EOL}${os.EOL}`); + this.ui.stdout.write(`Device is protected`); return; } if (s.overridden) { // terminate unlock await this.device.unprotectDevice({ action: 'reset' }); - this.ui.stdout.write(`Device is ${chalk.bold('protected')}${os.EOL}`); + this.ui.stdout.write(`Device is protected`); const success = await this._markAsDevelopmentDevice(false); - if (success) { - this.ui.stdout.write(`Device was removed from development mode to keep in protection mode.${os.EOL}`); - } else { - this.ui.stdout.write(`Failed to mark device as development device${os.EOL}`); + if (!success) { + this.ui.stdout.write(`Failed to remove device from development mode. Ensure it is not in development mode for protection to work properly`); } - return; } - if (!s.protected && !s.overridden) { + if (!s.protected && !s.overridden && deviceProtectionActiveInProduct) { // Protect device (permanently) - const localBootloaderPath = await this._downloadBootloader(); - - const resPath = await this.protectBinary(localBootloaderPath); - - await this._flashBootloader(resPath); + if (!protectedBinary) { + const localBootloaderPath = await this._downloadBootloader(); + protectedBinary = await this.protectBinary({ file: localBootloaderPath, verbose: false }); + } - this.ui.stdout.write(`Remove device as development device...${os.EOL}${os.EOL}`); + await this._flashBootloader(protectedBinary, 'enable'); const success = await this._markAsDevelopmentDevice(false); - - if (success) { - this.ui.stdout.write(`Device was removed from development mode to enable protection.${os.EOL}`); - } else { - this.ui.stdout.write(`Failed to remove device from development mode.${os.EOL}`); + + if (!success) { + this.ui.stdout.write(`Failed to remove device from development mode. Ensure it is not in development mode for protection to work properly`); } } + + this.ui.write(`Device is protected`); }); } - async _flashBootloader(path) { + async _flashBootloader(path, action) { + let msg; + switch (action) { + case 'enable': + msg = 'Enabling protection on the device. Please wait...'; + break; + case 'disable': + msg = 'Disabling protection on the device. Please wait...'; + break; + default: + throw new Error('Invalid action'); + } + const flashCmdInstance = new FlashCommand(); - await flashCmdInstance.flashLocal({ files: [path], applicationOnly: true }); + + const flashPromise = flashCmdInstance.flashLocal({ files: [path], applicationOnly: true, verbose: false }); + + await this.ui.showBusySpinnerUntilResolved(msg, flashPromise); } async _markAsDevelopmentDevice(state) { let attrs; try { + // TODO: Refactor attrs = await this.api.getDeviceAttributes(this.deviceId); if (attrs.platform_id !== attrs.product_id) { // it's in a product @@ -225,7 +255,7 @@ module.exports = class DeviceProtectionCommands extends CLICommandBase { if (this.device.isInDfuMode) { this.ui.stdout.write(`Device is in DFU mode. Performing a reset to get the device in normal mode. Please wait...${os.EOL}`); await this.resetDevice(this.device); - this.ui.stdout.write(`Done! Device is now in normal mode.${os.EOL}`); + this.ui.stdout.write(`Done! Device is now in normal mode`); await this.getUsbDevice(this.device); } @@ -252,7 +282,7 @@ module.exports = class DeviceProtectionCommands extends CLICommandBase { await new Promise(resolve => setTimeout(resolve, 3000)); } - async protectBinary(file) { + async protectBinary({ file, verbose=true }) { if (!file) { throw new Error('Please provide a file to add device protection'); } @@ -266,7 +296,9 @@ module.exports = class DeviceProtectionCommands extends CLICommandBase { const protectedBinary = await createProtectedModule(binary); await fs.writeFile(resBinaryPath, protectedBinary); - this.ui.stdout.write(`Protected binary saved at ${resBinaryPath}${os.EOL}${os.EOL}`); + if (verbose) { + this.ui.stdout.write(`Protected binary saved at ${resBinaryPath}`); + } return resBinaryPath; } diff --git a/src/cmd/flash.js b/src/cmd/flash.js index 1a30695cc..fd9ce7832 100644 --- a/src/cmd/flash.js +++ b/src/cmd/flash.js @@ -115,7 +115,7 @@ module.exports = class FlashCommand extends CLICommandBase { return new SerialCommands().flashDevice(binary, { port, yes }); } - async flashLocal({ files, applicationOnly, target }) { + async flashLocal({ files, applicationOnly, target, verbose=true }) { const { files: parsedFiles, deviceIdOrName, knownApp } = await this._analyzeFiles(files); const { api, auth } = this._particleApi(); const device = await usbUtils.getOneUsbDevice({ idOrName: deviceIdOrName, api, auth, ui: this.ui }); @@ -124,7 +124,9 @@ module.exports = class FlashCommand extends CLICommandBase { const platformName = platformForId(platformId).name; const currentDeviceOsVersion = device.firmwareVersion; - this.ui.write(`Flashing ${platformName} ${deviceIdOrName || device.id}`); + if (verbose) { + this.ui.write(`Flashing ${platformName} ${deviceIdOrName || device.id}`); + } validateDFUSupport({ device, ui: this.ui }); @@ -160,7 +162,7 @@ module.exports = class FlashCommand extends CLICommandBase { platformId }); - await flashFiles({ device, flashSteps, ui: this.ui }); + await flashFiles({ device, flashSteps, ui: this.ui, verbose }); } async _analyzeFiles(files) { diff --git a/src/lib/flash-helper.js b/src/lib/flash-helper.js index de20b3274..9729010c9 100644 --- a/src/lib/flash-helper.js +++ b/src/lib/flash-helper.js @@ -16,8 +16,9 @@ const ensureError = utilities.ensureError; // Flashing an NCP firmware can take a few minutes const FLASH_TIMEOUT = 4 * 60000; -async function flashFiles({ device, flashSteps, resetAfterFlash = true, ui }) { - const progress = _createFlashProgress({ flashSteps, ui }); +async function flashFiles({ device, flashSteps, resetAfterFlash = true, ui, verbose=true }) { + let progress = null; + progress = verbose ? _createFlashProgress({ flashSteps, ui, verbose }) : null; let success = false; let lastStepDfu = false; try { @@ -35,7 +36,9 @@ async function flashFiles({ device, flashSteps, resetAfterFlash = true, ui }) { } success = true; } finally { - progress({ event: 'finish', success }); + if (progress) { + progress({ event: 'finish', success }); + } if (device.isOpen) { // only reset the device if the last step was in DFU mode if (resetAfterFlash && lastStepDfu) { @@ -135,6 +138,7 @@ async function _flashDeviceInDfuMode(device, data, { name, altSetting, startAddr } function _createFlashProgress({ flashSteps, ui }) { + console.log('verbose: ' , verbose); const NORMAL_MULTIPLIER = 10; // flashing in normal mode is slower so count each byte more const { isInteractive } = ui; let progressBar; diff --git a/test/e2e/help.e2e.js b/test/e2e/help.e2e.js index 0fa1a0bca..6afc7c9c8 100644 --- a/test/e2e/help.e2e.js +++ b/test/e2e/help.e2e.js @@ -72,7 +72,7 @@ describe('Help & Unknown Command / Argument Handling', () => { 'variable list', 'variable get', 'variable monitor', 'variable', 'webhook create', 'webhook list', 'webhook delete', 'webhook POST', 'webhook GET', 'webhook', 'whoami', 'wifi add', 'wifi join', 'wifi clear', 'wifi list', 'wifi remove', 'wifi current', 'wifi', - 'protection', 'protection status', 'protection disable', 'protection enable' + 'device-protection', 'device-protection status', 'device-protection disable', 'device-protection enable' ]; const mainCmds = dedupe(allCmds.map(c => c.split(' ')[0]));