diff --git a/src/cmd/binary.js b/src/cmd/binary.js index df5bb5d50..9930503c3 100644 --- a/src/cmd/binary.js +++ b/src/cmd/binary.js @@ -29,7 +29,7 @@ const fs = require('fs-extra'); const path = require('path'); const VError = require('verror'); const chalk = require('chalk'); -const { HalModuleParser: Parser, unpackApplicationAndAssetBundle, isAssetValid, createProtectedModule } = require('binary-version-reader'); +const { HalModuleParser: Parser, unpackApplicationAndAssetBundle, isAssetValid, createProtectedModule, ModuleInfo } = require('binary-version-reader'); const utilities = require('../lib/utilities'); const ensureError = utilities.ensureError; @@ -37,10 +37,12 @@ const INVALID_SUFFIX_SIZE = 65535; const DEFAULT_PRODUCT_ID = 65535; const DEFAULT_PRODUCT_VERSION = 65535; +const PROTECTED_MINIMUM_BOOTLOADER_VERSION = 3000; + class BinaryCommand { async inspectBinary(file) { await this._checkFile(file); - const extractedFiles = await this._extractFiles(file); + const extractedFiles = await this._extractApplicationFiles(file); const parsedAppInfo = await this._parseApplicationBinary(extractedFiles.application); const assets = extractedFiles.assets; await this._verifyBundle(parsedAppInfo, assets); @@ -48,6 +50,9 @@ class BinaryCommand { async createProtectedBinary({ saveTo, file, verbose }) { await this._checkFile(file); + const extractedFile = await this._extractFile(file); + const binaryModule = await this._parseBinary(extractedFile); + this._validateProtectedBinary(binaryModule); let resBinaryName; if (saveTo) { @@ -69,6 +74,12 @@ class BinaryCommand { return resBinaryPath; } + _validateProtectedBinary(module) { + const { moduleFunction, moduleVersion, moduleIndex } = module.prefixInfo; + if (moduleFunction !== ModuleInfo.FunctionType.BOOTLOADER || moduleIndex !== 0 || moduleVersion < PROTECTED_MINIMUM_BOOTLOADER_VERSION) { + throw new Error('Device protection feature is not supported for this binary.'); + } + } async _checkFile(file) { try { @@ -79,7 +90,7 @@ class BinaryCommand { return true; } - async _extractFiles(file) { + async _extractApplicationFiles(file) { if (utilities.getFilenameExt(file) === '.zip') { return unpackApplicationAndAssetBundle(file); } else if (utilities.getFilenameExt(file) === '.bin') { @@ -90,6 +101,15 @@ class BinaryCommand { } } + async _extractFile(file) { + if (utilities.getFilenameExt(file) === '.bin') { + const data = await fs.readFile(file); + return { name: path.basename(file), data }; + } else { + throw new VError(`File must be a .bin: ${file}`); + } + } + async _parseApplicationBinary(applicationBinary) { const parser = new Parser(); let fileInfo; @@ -110,6 +130,17 @@ class BinaryCommand { return fileInfo; } + async _parseBinary(binary) { + const parser = new Parser(); + let fileInfo; + try { + fileInfo = await parser.parseBuffer({ filename: binary.name, fileBuffer: binary.data }); + return fileInfo; + } catch (err) { + throw new VError(ensureError(err), `Could not parse ${binary.name}`); + } + } + async _verifyBundle(appInfo, assets) { const appAssets = appInfo.assets; if (appAssets && assets.length > 0) { diff --git a/src/cmd/binary.test.js b/src/cmd/binary.test.js index 974dfe3f7..0ad8edf0d 100644 --- a/src/cmd/binary.test.js +++ b/src/cmd/binary.test.js @@ -56,12 +56,12 @@ describe('Binary Inspect', () => { }); }); - describe('_extractFiles', () => { + describe('_extractApplicationFiles', () => { it('errors if file is not .zip or .bin', async () => { let error; try { - await binaryCommand._extractFiles('not-a-zip-or-bin-file'); + await binaryCommand._extractApplicationFiles('not-a-zip-or-bin-file'); } catch (_error) { error = _error; } @@ -73,7 +73,7 @@ describe('Binary Inspect', () => { it('extracts a .zip file', async () => { const zipPath = path.join(PATH_FIXTURES_THIRDPARTY_OTA_DIR, 'bundle.zip'); - const binaryInfo = await binaryCommand._extractFiles(zipPath); + const binaryInfo = await binaryCommand._extractApplicationFiles(zipPath); expect(binaryInfo).to.have.property('application').with.property('name', 'app.bin'); expect(binaryInfo).to.have.property('assets').with.lengthOf(3); @@ -87,7 +87,7 @@ describe('Binary Inspect', () => { it('extracts a .bin file', async () => { const binPath = path.join(PATH_FIXTURES_BINARIES_DIR, 'argon_stroby.bin'); - const binaryInfo = await binaryCommand._extractFiles(binPath); + const binaryInfo = await binaryCommand._extractApplicationFiles(binPath); expect(binaryInfo).to.have.property('application').with.property('name', 'argon_stroby.bin'); expect(binaryInfo).to.have.property('assets').with.lengthOf(0); @@ -96,7 +96,7 @@ describe('Binary Inspect', () => { it('handles if zip file does not have a binary or assets', async () => { const zipPath = path.join(PATH_FIXTURES_THIRDPARTY_OTA_DIR, 'invalid-bundle.zip'); - const binaryInfo = await binaryCommand._extractFiles(zipPath); + const binaryInfo = await binaryCommand._extractApplicationFiles(zipPath); expect(binaryInfo).to.have.property('application').with.property('name', 'app.txt'); expect(binaryInfo).to.have.property('assets').with.lengthOf(0); @@ -133,10 +133,39 @@ describe('Binary Inspect', () => { }); }); + describe('_parseBinary', () => { + it('parses a .bin file', async () => { + const name = 'argon_stroby.bin'; + const data = await fs.readFile(path.join(PATH_FIXTURES_BINARIES_DIR, name)); + const applicationBinary = { name, data }; + + const res = await binaryCommand._parseBinary(applicationBinary); + + expect(path.basename(res.filename)).to.equal('argon_stroby.bin'); + expect(res.crc.ok).to.equal(true); + expect(res).to.have.property('prefixInfo'); + expect(res).to.have.property('suffixInfo'); + }); + + it('errors if the binary is not valid', async () => { + const binary = { name: 'junk', data: Buffer.from('junk') }; + + let error; + try { + await binaryCommand._parseBinary(binary); + } catch (_error) { + error = _error; + } + + expect(error).to.be.an.instanceof(Error); + expect(error.message).to.match(/Could not parse junk/); + }); + }); + describe('_verifyBundle', () => { it('verifies bundle with asset info', async () => { const zipPath = path.join(PATH_FIXTURES_THIRDPARTY_OTA_DIR, 'bundle.zip'); - const res = await binaryCommand._extractFiles(zipPath); + const res = await binaryCommand._extractApplicationFiles(zipPath); const parsedBinaryInfo = await binaryCommand._parseApplicationBinary(res.application); const verify = await binaryCommand._verifyBundle(parsedBinaryInfo, res.assets); @@ -144,5 +173,83 @@ describe('Binary Inspect', () => { expect(verify).to.equal(true); }); }); + + describe('_validateProtectedBinary', () => { + it('validates a protected binary', async () => { + const module = { + prefixInfo: { + moduleIndex: 0, + moduleFunction: 2, + moduleVersion: 3000 + } + }; + + let error; + try { + binaryCommand._validateProtectedBinary(module); + } catch (e) { + error = e; + } + + expect(error).to.equal(undefined); + }); + + it('errors if binary is of the wrong module index', async () => { + const module = { + prefixInfo: { + moduleIndex: 1, + moduleFunction: 2, + moduleVersion: 3000 + } + }; + + let error; + try { + binaryCommand._validateProtectedBinary(module); + } catch (e) { + error = e; + } + + expect(error.message).to.equal('Device protection feature is not supported for this binary.'); + }); + + it('errors if binary is not a bootloader', async () => { + const module = { + prefixInfo: { + moduleIndex: 1, + moduleFunction: 0, + moduleVersion: 3000 + } + }; + + let error; + try { + binaryCommand._validateProtectedBinary(module); + } catch (e) { + error = e; + } + + expect(error.message).to.equal('Device protection feature is not supported for this binary.'); + }); + + it('errors if binary is of an older bootloader version', async () => { + const module = { + prefixInfo: { + moduleIndex: 1, + moduleFunction: 0, + moduleVersion: 2000 + } + }; + + let error; + try { + binaryCommand._validateProtectedBinary(module); + } catch (e) { + error = e; + } + + expect(error.message).to.equal('Device protection feature is not supported for this binary.'); + }); + }); });