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