@twilio/plugin-microvisor
Version:
Interact with your Twilio Microvisor devices
173 lines (172 loc) • 7.01 kB
JavaScript
"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;