From e75010aaff8f8e3eed4fdff378e6a8626a6c3f30 Mon Sep 17 00:00:00 2001 From: keeramis Date: Mon, 17 Jun 2024 14:01:58 -0700 Subject: [PATCH] Improved formatting --- src/cli/device-protection.js | 22 +++-- src/cmd/api.js | 9 ++ src/cmd/device-protection.js | 164 ++++++++++++++++++++--------------- src/cmd/flash.js | 8 +- src/lib/flash-helper.js | 10 ++- test/e2e/help.e2e.js | 2 +- 6 files changed, 134 insertions(+), 81 deletions(-) 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..a5db10307 100644 --- a/src/cmd/device-protection.js +++ b/src/cmd/device-protection.js @@ -32,38 +32,29 @@ module.exports = class DeviceProtectionCommands extends CLICommandBase { // then the device is not pretected. For now though, let's assume the device is in normal mode and not in dfu mode. return this._withDevice(async () => { - let s; - 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}`); - } - throw new Error('Failed to get device protection status'); - } + const s = await this._getDeviceProtection(); let res; if (!s.protected && !s.overridden) { - res = 'not protected'; + res = `Open (not protected)${os.EOL}Run ${chalk.yellow('particle device-protection enable')} to protect the device`; } else if (s.protected && !s.overridden) { - res = 'protected'; + res = `Protected${os.EOL}Run ${chalk.yellow('particle device-protection disable')} unlock the device`; } else if (s.overridden) { - res = 'temporarily not protected. A reset is required to re-enable protection.'; + res = `Protected (service mode)${os.EOL}Run ${chalk.yellow('particle device-protection enable')} to enable protection`; } - 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 + async disableProtection({ open } = {}) { + return this._withDevice(async () => { + let s = await this._getDeviceProtection(); - return this._withDevice(async () => { - let s = await this.device.getProtectionState(); 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; } @@ -76,8 +67,8 @@ module.exports = class DeviceProtectionCommands extends CLICommandBase { // console.log(`CLI -> Device:\n\tserver_nonce=${serverNonce.toString('base64')}`); r = await this.device.unprotectDevice({ action: 'prepare', serverNonce }); if (!r.protected) { - console.log('Device is not protected'); - return; + this.ui.stdout.write(`Device is not protected${os.EOL}`); + return; } const { deviceNonce, deviceSignature, devicePublicKeyFingerprint } = r; // console.log(`Device -> CLI:\n\tdevice_signature=${deviceSignature.toString('base64')}`); @@ -85,13 +76,13 @@ module.exports = class DeviceProtectionCommands extends CLICommandBase { // Verify the device signature and get a server signature // console.log(`CLI -> Server:\n\tdevice_signature=${deviceSignature.toString('base64')}`); r = await this.api.unprotectDevice({ - deviceId: this.deviceId, - action: 'confirm', - serverNonce: serverNonce.toString('base64'), - deviceNonce: deviceNonce.toString('base64'), - deviceSignature: deviceSignature.toString('base64'), - devicePublicKeyFingerprint: devicePublicKeyFingerprint.toString('base64'), - auth: settings.access_token + deviceId: this.deviceId, + action: 'confirm', + serverNonce: serverNonce.toString('base64'), + deviceNonce: deviceNonce.toString('base64'), + deviceSignature: deviceSignature.toString('base64'), + devicePublicKeyFingerprint: devicePublicKeyFingerprint.toString('base64'), + auth: settings.access_token }); const serverSignature = Buffer.from(r.server_signature, 'base64'); @@ -101,32 +92,30 @@ module.exports = class DeviceProtectionCommands extends CLICommandBase { // Unprotect the device 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}`); + s = await this._getDeviceProtection(); + 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) { - - const localBootloaderPath = await this._downloadBootloader(); - - await this._flashBootloader(localBootloaderPath); + const localBootloaderPath = await this._downloadBootloader(); - 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. Device is now open${os.EOL}`); + this.ui.stdout.write(`Putting device in developement mode to prevent cloud from enabling protection...${os.EOL}`); - const success = await this._markAsDevelopmentDevice(true); + const success = await this._markAsDevelopmentDevice(true); - if (!success) { - this.ui.stdout.write(`Failed to mark device as development device. Protection will be automatically enabled after a power cycle${os.EOL}`); - } + if (!success) { + this.ui.stdout.write(`Failed to mark device as development device. Protection will be automatically enabled after a power cycle${os.EOL}`); + } else { + this.ui.stdout.write(`Device is now in development mode${os.EOL}`); } }); } async _downloadBootloader() { - let version; const modules = await this.device.getFirmwareModuleInfo(); modules.forEach((m) => { @@ -147,62 +136,99 @@ 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(); + const s = await this._getDeviceProtection(); + + 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${os.EOL}`); 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${os.EOL}`); + this.ui.stdout.write(`Removing device from developement mode...${os.EOL}`); + 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${os.EOL}`); } - 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); + if (!protectedBinary) { + const localBootloaderPath = await this._downloadBootloader(); + protectedBinary = await this.protectBinary({ file: localBootloaderPath, verbose: false }); + } - await this._flashBootloader(resPath); + await this._flashBootloader(protectedBinary, 'enable'); - this.ui.stdout.write(`Remove device as development device...${os.EOL}${os.EOL}`); + this.ui.write(`Device is protected${os.EOL}`); + this.ui.stdout.write(`Removing device from developement mode...${os.EOL}`); 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${os.EOL}`); } } }); } - async _flashBootloader(path) { + async _getDeviceProtection() { + try { + const s = await this.device.getProtectionState(); + return s; + } catch (error) { + if (error.message === 'Not supported') { + throw new Error(`Device protection feature is not supported on this device${os.EOL}`); + } + } + } + + 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 +251,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${os.EOL}`); await this.getUsbDevice(this.device); } @@ -252,7 +278,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 +292,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]));