UNPKG

@twilio/plugin-microvisor

Version:

Interact with your Twilio Microvisor devices

173 lines (172 loc) 7.01 kB
"use strict"; Object.defineProperty(exports, "__esModule", { value: true }); exports.AppBundle = exports.AppLayer = exports.MemoryRegion = void 0; const tslib_1 = require("tslib"); const node_crypto_1 = tslib_1.__importDefault(require("node:crypto")); const adm_zip_1 = tslib_1.__importDefault(require("adm-zip")); const long_1 = tslib_1.__importDefault(require("long")); const proto_1 = require("../../../proto_gen/proto"); const hpke_1 = require("../debug/proxy/hpke"); var MemoryRegion; (function (MemoryRegion) { MemoryRegion[MemoryRegion["INTERNAL_FLASH"] = 0] = "INTERNAL_FLASH"; MemoryRegion[MemoryRegion["SPI_FLASH"] = 1] = "SPI_FLASH"; })(MemoryRegion = exports.MemoryRegion || (exports.MemoryRegion = {})); function padStart(b, totalRequired) { const padding = Buffer.alloc(totalRequired - b.length); // pad start with zeros to make up total length return Buffer.concat([padding, b]); } /** * Return the public key after validating that it is created using the NIST P-256 Elliptic Curve * @returns public key in X9.62 uncompressed point format as expected by Microvisor devices */ function publicKeyToBuffer(pemKey) { var _a; const debugKey = node_crypto_1.default.createPublicKey(pemKey); const isP256Curve = ((_a = debugKey.asymmetricKeyDetails) === null || _a === void 0 ? void 0 : _a.namedCurve) === 'prime256v1'; // aka secp256r1 if (!isP256Curve) { throw new Error('Debugging public key should use NIST P-256 curve.'); } const jwk = debugKey.export({ format: 'jwk' }); // export the key object to Json Web Key format /** * Because this is an EC key, the JWK's x and y coordinates are encoded in (a special) base64-unpadded format. * See https://www.rfc-editor.org/rfc/rfc7515#appendix-C. Node can parse them into byte arrays just fine. */ const x = Buffer.from(jwk.x, 'base64url'); const y = Buffer.from(jwk.y, 'base64url'); /** * According to https://security.stackexchange.com/questions/185520/what-is-the-format-of-an-x9-62-key, * "The uncompressed point format [..] is one octet 04 followed by the X and Y coordinates as an unsigned BigEndian * integer of size determined by the field of the underlying curve". In this case the field size is 256 bits. * So, each coordinate should be encoded with 256 bits i.e. 32 bytes. If it's not, pad start with zeroes. */ const prefix = Buffer.from([0x04]); return Buffer.concat([prefix, padStart(x, 32), padStart(y, 32)]); } class AppLayer { constructor(romData, startAddr, size, region, encryptionKey, writeable) { this.startAddr = startAddr; this.size = size; this.region = region; this.encryptionKey = (encryptionKey) ? publicKeyToBuffer(encryptionKey) : undefined; this.writeable = writeable; if (typeof this.encryptionKey !== 'undefined') { const sealResults = (0, hpke_1.BaseSeal)(this.encryptionKey, romData); this.maybeEncryptedRomData = Buffer.concat([Buffer.from([0x01]), sealResults.encap, sealResults.ciphertext]); } else { this.maybeEncryptedRomData = romData; } this.romDataHash = this.hash(this.maybeEncryptedRomData); this.regionHash = this.hashOfFlash(romData); } rom() { return this.maybeEncryptedRomData; } romHash() { return this.romDataHash; } manifestLayer() { let region = { start: this.startAddr, size: this.size }; let layer = { fetchHash: this.romDataHash }; if (!this.writeable) { layer.measurementHash = this.regionHash; } switch (this.region) { case MemoryRegion.INTERNAL_FLASH: layer.target = { internalFlash: region }; break; case MemoryRegion.SPI_FLASH: layer.target = { internalFlash: region }; break; } if (typeof this.encryptionKey !== 'undefined') { layer.encryptedWithKeyId = this.keyId(Buffer.from(this.encryptionKey)); } return layer; } keyId(publicKey) { const hash = this.hash(Buffer.concat([Buffer.from([0, 1]), publicKey])); // ID is the first 8 bytes of hash collected into 64-bit integer // in big-endian order let hi = hash[0] << 24 | hash[1] << 16 | hash[2] << 8 | hash[3]; let lo = hash[4] << 24 | hash[5] << 16 | hash[6] << 8 | hash[7]; let res = new long_1.default(lo, hi, true); return res; } hash(contents) { return node_crypto_1.default.createHash('sha256').update(contents).digest(); } hashOfFlash(contents) { const contentSize = contents.byteLength; if (contentSize > this.size) { throw new Error('Contents of the ROM file are too big for flash area.'); } const padding = new Uint8Array(this.size - contentSize).fill(-1); const image = Buffer.concat([contents, padding]); return this.hash(image); } } exports.AppLayer = AppLayer; class AppBundle { constructor(layers, debugKey, minimumKernelVersion, connectionGraceTime, minimumCheckInTime) { this.layers = layers; this.debugKey = debugKey; this.minimumKernelVersion = minimumKernelVersion; this.connectionGraceTime = connectionGraceTime; this.minimumCheckInTime = minimumCheckInTime; } debugAuthPubKey() { if (typeof this.debugKey === 'undefined') { return null; } return publicKeyToBuffer(this.debugKey); } getManifest() { let manifestObj = { layers: this.layers.map((l) => l.manifestLayer()), debug: { debugAuthPubkey: this.debugAuthPubKey() }, update: { minimumKernelVersion: this.minimumKernelVersion }, connectivity: { connectionGraceTimeSec: this.connectionGraceTime, minimumCheckInTimeSec: this.minimumCheckInTime } }; const body = proto_1.bundle.AppManifest.AppBody.create(manifestObj); const bodyBytes = proto_1.bundle.AppManifest.AppBody.encode(body).finish(); const signedManifest = proto_1.bundle.OpaqueManifest.create({ body: bodyBytes, signerId: 0, signature: new Uint8Array() }); return Buffer.from(proto_1.bundle.OpaqueManifest.encode(signedManifest).finish()); } getZip() { let zip = new adm_zip_1.default(); zip.addFile('manifest', Buffer.from(this.getManifest())); for (const layer of this.layers) { zip.addFile(layer.romHash().toString('hex'), layer.rom()); } return zip; } } exports.AppBundle = AppBundle;