UNPKG

@twilio/plugin-microvisor

Version:

Interact with your Twilio Microvisor devices

301 lines (300 loc) 12.1 kB
"use strict"; Object.defineProperty(exports, "__esModule", { value: true }); exports.BaseSeal = exports.Session = exports.EcdhPrivateKey = exports.UnsuitablePrivateKeyError = void 0; const tslib_1 = require("tslib"); const crypto = tslib_1.__importStar(require("node:crypto")); /** * Used for hpke 'info' parameter, so we could break compatibility * for security reasons if ever needed, or (more importantly) make * it safe if the user uses the same key for multiple things. */ const VERSION_INFO = Buffer.from('01', 'hex'); /** HPKE constants. Much of this is specific to our chosen parameters: * * kem: DHKEM(P-256, HKDF-SHA256) * kdf: HKDF-SHA256 * aead: AES-128-GCM */ const AESGCM128_NK = 16; const AESGCM128_NN = 12; const AESGCM128_NT = 16; const KEM_SUITE_ID = Buffer.concat([ Buffer.from('KEM'), Buffer.from('0010', 'hex') ]); const HPKE_SUITE_ID = Buffer.concat([ Buffer.from('HPKE'), Buffer.from('001000010001', 'hex') ]); const KEM_CURVE = 'prime256v1'; const KEM_NSECRET = 32; const HKDFSHA256_NH = 32; const MODE_BASE = Buffer.from('00', 'hex'); const MODE_AUTH = Buffer.from('02', 'hex'); class UnsuitablePrivateKeyError extends Error { constructor(message) { super(message); this.name = 'UnsuitablePrivateKeyError'; } } exports.UnsuitablePrivateKeyError = UnsuitablePrivateKeyError; /** Type-checked wrapper on createPrivateKey. */ class EcdhPrivateKey { constructor(pemEncoded) { this.key = crypto.createPrivateKey(pemEncoded); if (this.key === undefined || this.key.asymmetricKeyType !== 'ec') { throw new UnsuitablePrivateKeyError('incorrect key type'); } if (this.key.asymmetricKeyDetails === undefined || this.key.asymmetricKeyDetails.namedCurve !== KEM_CURVE) { throw new UnsuitablePrivateKeyError('incorrect curve'); } } exchange(publicKey) { // this is annoying because node's crypto.createECDH doesn't support node's // crypto.KeyObject produced by crypto.createPrivateKey. on top of that, // the semantics of setPrivateKey aren't documented -- a Buffer containing // the scalar seems to work. const ecdh = crypto.createECDH(KEM_CURVE); const rawKey = this.key.export({ format: 'jwk' }); ecdh.setPrivateKey(Buffer.from(rawKey.d, 'base64')); return { secret: ecdh.computeSecret(publicKey), publicKey: ecdh.getPublicKey() }; } } exports.EcdhPrivateKey = EcdhPrivateKey; function hkdfExtract(salt, ikm) { // unfortunately crypto.hkdfSync is one-piece (fused Extract+Expand) // so doesn't work for us here. const hmac = crypto.createHmac('sha256', salt); hmac.update(ikm); return hmac.digest(); } function labeledExtract(suiteId, salt, label, ikm) { /* def LabeledExtract(salt, label, ikm): * labeled_ikm = concat("HPKE-v1", suite_id, label, ikm) */ const labeledIkm = Buffer.concat([Buffer.from('HPKE-v1'), suiteId, label, ikm]); /* return Extract(salt, labeled_ikm) */ return hkdfExtract(salt, labeledIkm); } function hkdfExpand(prk, info, L) { let data = Buffer.alloc(0); let tnMinus1 = Buffer.alloc(0); let n = 1; const nBuffer = Buffer.alloc(1); while (data.byteLength < L) { const hmac = crypto.createHmac('sha256', prk); hmac.update(tnMinus1); hmac.update(info); nBuffer[0] = n; hmac.update(nBuffer); tnMinus1 = hmac.digest(); data = Buffer.concat([data, tnMinus1]); n += 1; } // console.log('_hkdf_expand %j', data.subarray(0, L).toString('hex')); return data.subarray(0, L); } // eslint-disable-next-line max-params function labeledExpand(suiteId, prk, label, info, L) { /* def LabeledExpand(prk, label, info, L): * labeled_info = concat(I2OSP(L, 2), "HPKE-v1", suite_id, * label, info) */ const lengthBuf = Buffer.alloc(2); lengthBuf.writeUint16BE(L, 0); const labeledInfo = Buffer.concat([lengthBuf, Buffer.from('HPKE-v1'), suiteId, Buffer.from(label), info]); // console.log('labeled_info is %j', labeledInfo.toString('hex')); /* return Expand(prk, labeled_info, L) */ return hkdfExpand(prk, labeledInfo, L); } function extractAndExpand(suiteId, dh, kemContext) { /* def ExtractAndExpand(dh, kem_context): * eae_prk = LabeledExtract("", "eae_prk", dh) */ const eaePrk = labeledExtract(KEM_SUITE_ID, Buffer.from(''), Buffer.from('eae_prk'), dh); // console.log('_extract_and_expand suite_id %j', KEM_SUITE_ID.toString('hex')); // console.log('_extract_and_expand eae_prk %j', eaePrk.toString('hex')); /* shared_secret = LabeledExpand(eae_prk, "shared_secret", * kem_context, Nsecret) * return shared_secret */ return labeledExpand(KEM_SUITE_ID, eaePrk, 'shared_secret', kemContext, KEM_NSECRET); } function authEncap(pkR, skS) { /* def AuthEncap(pkR, skS): * skE, pkE = GenerateKeyPair() */ const ecdhEphem = crypto.createECDH(KEM_CURVE); const pkE = ecdhEphem.generateKeys(); /* dh = concat(DH(skE, pkR), DH(skS, pkR)) */ const dhLeft = ecdhEphem.computeSecret(pkR); const dhRight = skS.exchange(pkR); // console.log('dhLeft %j dhRight %j', dhLeft.toString('hex'), dhRight.secret.toString('hex')); /* enc = SerializePublicKey(pkE) */ const encap = pkE; /* pkRm = SerializePublicKey(pkR) * pkSm = SerializePublicKey(pk(skS)) * kem_context = concat(enc, pkRm, pkSm) */ const kemContext = Buffer.concat([encap, pkR, dhRight.publicKey]); // console.log('kemContext %j', kemContext.toString('hex')); /* shared_secret = ExtractAndExpand(dh, kem_context) */ const sharedSecret = extractAndExpand(KEM_SUITE_ID, Buffer.concat([dhLeft, dhRight.secret]), kemContext); // console.log('sharedSecret %j', sharedSecret.toString('hex')); /* return shared_secret, enc */ return { sharedSecret: sharedSecret, encap: encap }; } function encap(pkR) { /* def Encap(pkR): * skE, pkE = GenerateKeyPair() */ const ecdhEphem = crypto.createECDH(KEM_CURVE); const pkE = ecdhEphem.generateKeys(); /* dh = DH(skE, pkR) */ const dh = ecdhEphem.computeSecret(pkR); /* enc = SerializePublicKey(pkE) */ const encap = pkE; /* pkRm = SerializePublicKey(pkR) * kem_context = concat(enc, pkRm) */ const kemContext = Buffer.concat([encap, pkR]); /* shared_secret = ExtractAndExpand(dh, kem_context) */ const sharedSecret = extractAndExpand(KEM_SUITE_ID, dh, kemContext); /* return shared_secret, enc */ return { sharedSecret: sharedSecret, encap: encap }; } /** Add one to `seq`, as if it were a big endian integer in * the range 0..2**96-1. Throws on wrap. * * @param {Uint8Array} seq - a 12-byte array, which is mutated. */ function nextSeq(seq) { let carry = 1; let i = AESGCM128_NN - 1; while (carry) { seq[i] += carry; if (seq[i] === 0) { i -= 1; carry = 1; if (i === -1) { // that means we have a carry at the top byte (index 0), // which is not allowed. happens after 2**96-1 messages in // a single debugging session -- never going to happen. throw new RangeError('nonce sequence wrapped'); } } else { return; } } } function xor(base, seq) { const result = new Uint8Array(seq.length); for (const [index, seqByte] of seq.entries()) { result[index] = seqByte ^ base[index]; } return Buffer.from(result); } class AesGcm128Encrypter { constructor(key, nonce) { this.key = key; this.nonce = nonce; this.seq = new Uint8Array(AESGCM128_NN); } encrypt(plaintext) { const cipher = crypto.createCipheriv('aes-128-gcm', this.key, xor(this.nonce, this.seq)); nextSeq(this.seq); const output = cipher.update(plaintext); return Buffer.concat([output, cipher.final(), cipher.getAuthTag()]); } } class AesGcm128Decrypter { constructor(key, nonce) { this.key = key; this.nonce = nonce; this.seq = new Uint8Array(AESGCM128_NN); } decrypt(ciphertext) { if (ciphertext.length < AESGCM128_NT) { throw new Error('encrypted message too short'); } const decipher = crypto.createDecipheriv('aes-128-gcm', this.key, xor(this.nonce, this.seq)); nextSeq(this.seq); const split = ciphertext.length - AESGCM128_NT; decipher.setAuthTag(ciphertext.subarray(split)); const output = decipher.update(ciphertext.subarray(0, split)); return Buffer.concat([output, decipher.final()]); } } function keyScheduleS(mode, sharedSecret, info) { // no PSK support /* def KeySchedule<ROLE>(mode, shared_secret, info, psk, psk_id): * VerifyPSKInputs(mode, psk, psk_id) * * psk_id_hash = LabeledExtract("", "psk_id_hash", psk_id) */ const pskIdHash = labeledExtract(HPKE_SUITE_ID, Buffer.from(''), Buffer.from('psk_id_hash'), Buffer.from('')); /* info_hash = LabeledExtract("", "info_hash", info) */ const infoHash = labeledExtract(HPKE_SUITE_ID, Buffer.from(''), Buffer.from('info_hash'), info); /* key_schedule_context = concat(mode, psk_id_hash, info_hash) */ const keyScheduleContext = Buffer.concat([mode, pskIdHash, infoHash]); /* secret = LabeledExtract(shared_secret, "secret", psk) */ const secret = labeledExtract(HPKE_SUITE_ID, sharedSecret, Buffer.from('secret'), Buffer.from('')); /* key = LabeledExpand(secret, "key", key_schedule_context, Nk) * base_nonce = LabeledExpand(secret, "base_nonce", * key_schedule_context, Nn) */ const key = labeledExpand(HPKE_SUITE_ID, secret, 'key', keyScheduleContext, AESGCM128_NK); const nonce = labeledExpand(HPKE_SUITE_ID, secret, 'base_nonce', keyScheduleContext, AESGCM128_NN); /* exporter_secret = LabeledExpand(secret, "exp", * key_schedule_context, Nh) */ const exporter = labeledExpand(HPKE_SUITE_ID, secret, 'exp', keyScheduleContext, HKDFSHA256_NH); /* Our custom things from here-on: */ const recvKey = labeledExpand(HPKE_SUITE_ID, exporter, 'sec', Buffer.from('response key'), AESGCM128_NK); const recvNonce = labeledExpand(HPKE_SUITE_ID, exporter, 'sec', Buffer.from('response nonce'), AESGCM128_NN); return { sender: new AesGcm128Encrypter(key, nonce), receiver: new AesGcm128Decrypter(recvKey, recvNonce) }; } class HpkeContext { constructor(encap, sender, receiver) { this.encap = encap; this.sender = sender; this.receiver = receiver; } static setupAuthS(pkR, info, skS) { const kx = authEncap(pkR, skS); const sched = keyScheduleS(MODE_AUTH, kx.sharedSecret, info); return new HpkeContext(kx.encap, sched.sender, sched.receiver); } static setupBaseS(pkR, info) { const kx = encap(pkR); const sched = keyScheduleS(MODE_BASE, kx.sharedSecret, info); return new HpkeContext(kx.encap, sched.sender, sched.receiver); } } class Session { constructor(peerPublicKey, ourPrivateKey) { this.context = HpkeContext.setupAuthS(peerPublicKey, VERSION_INFO, ourPrivateKey); } encap() { return this.context.encap; } encryptMessage(message) { return this.context.sender.encrypt(message); } decryptMessage(ciphertext) { return this.context.receiver.decrypt(ciphertext); } } exports.Session = Session; function BaseSeal(publicKey, message) { let context = HpkeContext.setupBaseS(publicKey, VERSION_INFO); return { encap: context.encap, ciphertext: context.sender.encrypt(message) }; } exports.BaseSeal = BaseSeal;