Source code
Revision control
Copy as Markdown
Other Tools
/*!
*
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
*
* The following bundle is from an external repository at github.com/mozilla/fxa-pairing-channel,
* it implements a shared library for two javascript environments to create an encrypted and authenticated
* communication channel by sharing a secret key and by relaying messages through a websocket server.
*
* It is used by the Firefox Accounts pairing flow, with one side of the channel being web
* content from https://accounts.firefox.com and the other side of the channel being chrome native code.
*
* This uses the event-target-shim node library published under the MIT license:
*
* Bundle generated from https://github.com/mozilla/fxa-pairing-channel.git. Hash:c8ec3119920b4ffa833b, Chunkhash:378a5f51445e7aa7630e.
*
*/
// This header provides a little bit of plumbing to use `FxAccountsPairingChannel`
// from Firefox browser code, hence the presence of these privileged browser APIs.
// If you're trying to use this from ordinary web content you're in for a bad time.
import { setTimeout } from "resource://gre/modules/Timer.sys.mjs";
// We cannot use WebSocket from chrome code without a window,
const browser = Services.appShell.createWindowlessBrowser(true);
const {WebSocket} = browser.document.ownerGlobal;
export var FxAccountsPairingChannel =
/******/ (function(modules) { // webpackBootstrap
/******/ // The module cache
/******/ var installedModules = {};
/******/
/******/ // The require function
/******/ function __webpack_require__(moduleId) {
/******/
/******/ // Check if module is in cache
/******/ if(installedModules[moduleId]) {
/******/ return installedModules[moduleId].exports;
/******/ }
/******/ // Create a new module (and put it into the cache)
/******/ var module = installedModules[moduleId] = {
/******/ i: moduleId,
/******/ l: false,
/******/ exports: {}
/******/ };
/******/
/******/ // Execute the module function
/******/ modules[moduleId].call(module.exports, module, module.exports, __webpack_require__);
/******/
/******/ // Flag the module as loaded
/******/ module.l = true;
/******/
/******/ // Return the exports of the module
/******/ return module.exports;
/******/ }
/******/
/******/
/******/ // expose the modules object (__webpack_modules__)
/******/ __webpack_require__.m = modules;
/******/
/******/ // expose the module cache
/******/ __webpack_require__.c = installedModules;
/******/
/******/ // define getter function for harmony exports
/******/ __webpack_require__.d = function(exports, name, getter) {
/******/ if(!__webpack_require__.o(exports, name)) {
/******/ Object.defineProperty(exports, name, { enumerable: true, get: getter });
/******/ }
/******/ };
/******/
/******/ // define __esModule on exports
/******/ __webpack_require__.r = function(exports) {
/******/ if(typeof Symbol !== 'undefined' && Symbol.toStringTag) {
/******/ Object.defineProperty(exports, Symbol.toStringTag, { value: 'Module' });
/******/ }
/******/ Object.defineProperty(exports, '__esModule', { value: true });
/******/ };
/******/
/******/ // create a fake namespace object
/******/ // mode & 1: value is a module id, require it
/******/ // mode & 2: merge all properties of value into the ns
/******/ // mode & 4: return value when already ns object
/******/ // mode & 8|1: behave like require
/******/ __webpack_require__.t = function(value, mode) {
/******/ if(mode & 1) value = __webpack_require__(value);
/******/ if(mode & 8) return value;
/******/ if((mode & 4) && typeof value === 'object' && value && value.__esModule) return value;
/******/ var ns = Object.create(null);
/******/ __webpack_require__.r(ns);
/******/ Object.defineProperty(ns, 'default', { enumerable: true, value: value });
/******/ if(mode & 2 && typeof value != 'string') for(var key in value) __webpack_require__.d(ns, key, function(key) { return value[key]; }.bind(null, key));
/******/ return ns;
/******/ };
/******/
/******/ // getDefaultExport function for compatibility with non-harmony modules
/******/ __webpack_require__.n = function(module) {
/******/ var getter = module && module.__esModule ?
/******/ function getDefault() { return module['default']; } :
/******/ function getModuleExports() { return module; };
/******/ __webpack_require__.d(getter, 'a', getter);
/******/ return getter;
/******/ };
/******/
/******/ // Object.prototype.hasOwnProperty.call
/******/ __webpack_require__.o = function(object, property) { return Object.prototype.hasOwnProperty.call(object, property); };
/******/
/******/ // __webpack_public_path__
/******/ __webpack_require__.p = "";
/******/
/******/
/******/ // Load entry module and return exports
/******/ return __webpack_require__(__webpack_require__.s = 0);
/******/ })
/************************************************************************/
/******/ ([
/* 0 */
/***/ (function(module, __webpack_exports__, __webpack_require__) {
"use strict";
// ESM COMPAT FLAG
__webpack_require__.r(__webpack_exports__);
// EXPORTS
__webpack_require__.d(__webpack_exports__, "PairingChannel", function() { return /* binding */ src_PairingChannel; });
__webpack_require__.d(__webpack_exports__, "base64urlToBytes", function() { return /* reexport */ base64urlToBytes; });
__webpack_require__.d(__webpack_exports__, "bytesToBase64url", function() { return /* reexport */ bytesToBase64url; });
__webpack_require__.d(__webpack_exports__, "bytesToHex", function() { return /* reexport */ bytesToHex; });
__webpack_require__.d(__webpack_exports__, "bytesToUtf8", function() { return /* reexport */ bytesToUtf8; });
__webpack_require__.d(__webpack_exports__, "hexToBytes", function() { return /* reexport */ hexToBytes; });
__webpack_require__.d(__webpack_exports__, "TLSCloseNotify", function() { return /* reexport */ TLSCloseNotify; });
__webpack_require__.d(__webpack_exports__, "TLSError", function() { return /* reexport */ TLSError; });
__webpack_require__.d(__webpack_exports__, "utf8ToBytes", function() { return /* reexport */ utf8ToBytes; });
__webpack_require__.d(__webpack_exports__, "_internals", function() { return /* binding */ _internals; });
// CONCATENATED MODULE: ./src/alerts.js
/* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
/* eslint-disable sorting/sort-object-props */
const ALERT_LEVEL = {
WARNING: 1,
FATAL: 2
};
const ALERT_DESCRIPTION = {
CLOSE_NOTIFY: 0,
UNEXPECTED_MESSAGE: 10,
BAD_RECORD_MAC: 20,
RECORD_OVERFLOW: 22,
HANDSHAKE_FAILURE: 40,
ILLEGAL_PARAMETER: 47,
DECODE_ERROR: 50,
DECRYPT_ERROR: 51,
PROTOCOL_VERSION: 70,
INTERNAL_ERROR: 80,
MISSING_EXTENSION: 109,
UNSUPPORTED_EXTENSION: 110,
UNKNOWN_PSK_IDENTITY: 115,
NO_APPLICATION_PROTOCOL: 120,
};
/* eslint-enable sorting/sort-object-props */
function alertTypeToName(type) {
for (const name in ALERT_DESCRIPTION) {
if (ALERT_DESCRIPTION[name] === type) {
return `${name} (${type})`;
}
}
return `UNKNOWN (${type})`;
}
class TLSAlert extends Error {
constructor(description, level) {
super(`TLS Alert: ${alertTypeToName(description)}`);
this.description = description;
this.level = level;
}
static fromBytes(bytes) {
if (bytes.byteLength !== 2) {
throw new TLSError(ALERT_DESCRIPTION.DECODE_ERROR);
}
switch (bytes[1]) {
case ALERT_DESCRIPTION.CLOSE_NOTIFY:
if (bytes[0] !== ALERT_LEVEL.WARNING) {
// Close notifications should be fatal.
throw new TLSError(ALERT_DESCRIPTION.ILLEGAL_PARAMETER);
}
return new TLSCloseNotify();
default:
return new TLSError(bytes[1]);
}
}
toBytes() {
return new Uint8Array([this.level, this.description]);
}
}
class TLSCloseNotify extends TLSAlert {
constructor() {
super(ALERT_DESCRIPTION.CLOSE_NOTIFY, ALERT_LEVEL.WARNING);
}
}
class TLSError extends TLSAlert {
constructor(description = ALERT_DESCRIPTION.INTERNAL_ERROR) {
super(description, ALERT_LEVEL.FATAL);
}
}
// CONCATENATED MODULE: ./src/utils.js
/* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
//
// Various low-level utility functions.
//
// These are mostly conveniences for working with Uint8Arrays as
// the primitive "bytes" type.
//
const UTF8_ENCODER = new TextEncoder();
const UTF8_DECODER = new TextDecoder();
function noop() {}
function assert(cond, msg) {
if (! cond) {
throw new Error('assert failed: ' + msg);
}
}
function assertIsBytes(value, msg = 'value must be a Uint8Array') {
// Using `value instanceof Uint8Array` seems to fail in Firefox chrome code
// for inscrutable reasons, so we do a less direct check.
assert(ArrayBuffer.isView(value), msg);
assert(value.BYTES_PER_ELEMENT === 1, msg);
return value;
}
const EMPTY = new Uint8Array(0);
function zeros(n) {
return new Uint8Array(n);
}
function arrayToBytes(value) {
return new Uint8Array(value);
}
function bytesToHex(bytes) {
return Array.prototype.map.call(bytes, byte => {
let s = byte.toString(16);
if (s.length === 1) {
s = '0' + s;
}
return s;
}).join('');
}
function hexToBytes(hexstr) {
assert(hexstr.length % 2 === 0, 'hexstr.length must be even');
return new Uint8Array(Array.prototype.map.call(hexstr, (c, n) => {
if (n % 2 === 1) {
return hexstr[n - 1] + c;
} else {
return '';
}
}).filter(s => {
return !! s;
}).map(s => {
return parseInt(s, 16);
}));
}
function bytesToUtf8(bytes) {
return UTF8_DECODER.decode(bytes);
}
function utf8ToBytes(str) {
return UTF8_ENCODER.encode(str);
}
function bytesToBase64url(bytes) {
// XXX TODO: try to use something constant-time, in case calling code
// uses it to encode secrets?
const charCodes = String.fromCharCode.apply(String, bytes);
return btoa(charCodes).replace(/\+/g, '-').replace(/\//g, '_');
}
function base64urlToBytes(str) {
// XXX TODO: try to use something constant-time, in case calling code
// uses it to decode secrets?
str = atob(str.replace(/-/g, '+').replace(/_/g, '/'));
const bytes = new Uint8Array(str.length);
for (let i = 0; i < str.length; i++) {
bytes[i] = str.charCodeAt(i);
}
return bytes;
}
function bytesAreEqual(v1, v2) {
assertIsBytes(v1);
assertIsBytes(v2);
if (v1.length !== v2.length) {
return false;
}
for (let i = 0; i < v1.length; i++) {
if (v1[i] !== v2[i]) {
return false;
}
}
return true;
}
// The `BufferReader` and `BufferWriter` classes are helpers for dealing with the
// binary struct format that's used for various TLS message. Think of them as a
// buffer with a pointer to the "current position" and a bunch of helper methods
// to read/write structured data and advance said pointer.
class utils_BufferWithPointer {
constructor(buf) {
this._buffer = buf;
this._dataview = new DataView(buf.buffer, buf.byteOffset, buf.byteLength);
this._pos = 0;
}
length() {
return this._buffer.byteLength;
}
tell() {
return this._pos;
}
seek(pos) {
if (pos < 0) {
throw new TLSError(ALERT_DESCRIPTION.DECODE_ERROR);
}
if (pos > this.length()) {
throw new TLSError(ALERT_DESCRIPTION.DECODE_ERROR);
}
this._pos = pos;
}
incr(offset) {
this.seek(this._pos + offset);
}
}
// The `BufferReader` class helps you read structured data from a byte array.
// It offers methods for reading both primitive values, and the variable-length
//
// Such vectors are represented as a length followed by the concatenated
// bytes of each item, and the size of the length field is determined by
// the maximum allowed number of bytes in the vector. For example
// to read a vector that may contain up to 65535 bytes, use `readVector16`.
//
// To read a variable-length vector of between 1 and 100 uint16 values,
// defined in the RFC like this:
//
// uint16 items<2..200>;
//
// You would do something like this:
//
// const items = []
// buf.readVector8(buf => {
// items.push(buf.readUint16())
// })
//
// The various `read` will throw `DECODE_ERROR` if you attempt to read path
// the end of the buffer, or past the end of a variable-length list.
//
class utils_BufferReader extends utils_BufferWithPointer {
hasMoreBytes() {
return this.tell() < this.length();
}
readBytes(length) {
// This avoids copies by returning a view onto the existing buffer.
const start = this._buffer.byteOffset + this.tell();
this.incr(length);
return new Uint8Array(this._buffer.buffer, start, length);
}
_rangeErrorToAlert(cb) {
try {
return cb(this);
} catch (err) {
if (err instanceof RangeError) {
throw new TLSError(ALERT_DESCRIPTION.DECODE_ERROR);
}
throw err;
}
}
readUint8() {
return this._rangeErrorToAlert(() => {
const n = this._dataview.getUint8(this._pos);
this.incr(1);
return n;
});
}
readUint16() {
return this._rangeErrorToAlert(() => {
const n = this._dataview.getUint16(this._pos);
this.incr(2);
return n;
});
}
readUint24() {
return this._rangeErrorToAlert(() => {
let n = this._dataview.getUint16(this._pos);
n = (n << 8) | this._dataview.getUint8(this._pos + 2);
this.incr(3);
return n;
});
}
readUint32() {
return this._rangeErrorToAlert(() => {
const n = this._dataview.getUint32(this._pos);
this.incr(4);
return n;
});
}
_readVector(length, cb) {
const contentsBuf = new utils_BufferReader(this.readBytes(length));
const expectedEnd = this.tell();
// Keep calling the callback until we've consumed the expected number of bytes.
let n = 0;
while (contentsBuf.hasMoreBytes()) {
const prevPos = contentsBuf.tell();
cb(contentsBuf, n);
// Check that the callback made forward progress, otherwise we'll infinite loop.
if (contentsBuf.tell() <= prevPos) {
throw new TLSError(ALERT_DESCRIPTION.DECODE_ERROR);
}
n += 1;
}
// Check that the callback correctly consumed the vector's entire contents.
if (this.tell() !== expectedEnd) {
throw new TLSError(ALERT_DESCRIPTION.DECODE_ERROR);
}
}
readVector8(cb) {
const length = this.readUint8();
return this._readVector(length, cb);
}
readVector16(cb) {
const length = this.readUint16();
return this._readVector(length, cb);
}
readVector24(cb) {
const length = this.readUint24();
return this._readVector(length, cb);
}
readVectorBytes8() {
return this.readBytes(this.readUint8());
}
readVectorBytes16() {
return this.readBytes(this.readUint16());
}
readVectorBytes24() {
return this.readBytes(this.readUint24());
}
}
class utils_BufferWriter extends utils_BufferWithPointer {
constructor(size = 1024) {
super(new Uint8Array(size));
}
_maybeGrow(n) {
const curSize = this._buffer.byteLength;
const newPos = this._pos + n;
const shortfall = newPos - curSize;
if (shortfall > 0) {
// Classic grow-by-doubling, up to 4kB max increment.
// This formula was not arrived at by any particular science.
const incr = Math.min(curSize, 4 * 1024);
const newbuf = new Uint8Array(curSize + Math.ceil(shortfall / incr) * incr);
newbuf.set(this._buffer, 0);
this._buffer = newbuf;
this._dataview = new DataView(newbuf.buffer, newbuf.byteOffset, newbuf.byteLength);
}
}
slice(start = 0, end = this.tell()) {
if (end < 0) {
end = this.tell() + end;
}
if (start < 0) {
throw new TLSError(ALERT_DESCRIPTION.INTERNAL_ERROR);
}
if (end < 0) {
throw new TLSError(ALERT_DESCRIPTION.INTERNAL_ERROR);
}
if (end > this.length()) {
throw new TLSError(ALERT_DESCRIPTION.INTERNAL_ERROR);
}
return this._buffer.slice(start, end);
}
flush() {
const slice = this.slice();
this.seek(0);
return slice;
}
writeBytes(data) {
this._maybeGrow(data.byteLength);
this._buffer.set(data, this.tell());
this.incr(data.byteLength);
}
writeUint8(n) {
this._maybeGrow(1);
this._dataview.setUint8(this._pos, n);
this.incr(1);
}
writeUint16(n) {
this._maybeGrow(2);
this._dataview.setUint16(this._pos, n);
this.incr(2);
}
writeUint24(n) {
this._maybeGrow(3);
this._dataview.setUint16(this._pos, n >> 8);
this._dataview.setUint8(this._pos + 2, n & 0xFF);
this.incr(3);
}
writeUint32(n) {
this._maybeGrow(4);
this._dataview.setUint32(this._pos, n);
this.incr(4);
}
// These are helpers for writing the variable-length vector structure
//
// Such vectors are represented as a length followed by the concatenated
// bytes of each item, and the size of the length field is determined by
// the maximum allowed size of the vector. For example to write a vector
// that may contain up to 65535 bytes, use `writeVector16`.
//
// To write a variable-length vector of between 1 and 100 uint16 values,
// defined in the RFC like this:
//
// uint16 items<2..200>;
//
// You would do something like this:
//
// buf.writeVector8(buf => {
// for (let item of items) {
// buf.writeUint16(item)
// }
// })
//
// The helper will automatically take care of writing the appropriate
// length field once the callback completes.
_writeVector(maxLength, writeLength, cb) {
// Initially, write the length field as zero.
const lengthPos = this.tell();
writeLength(0);
// Call the callback to write the vector items.
const bodyPos = this.tell();
cb(this);
const length = this.tell() - bodyPos;
if (length >= maxLength) {
throw new TLSError(ALERT_DESCRIPTION.INTERNAL_ERROR);
}
// Backfill the actual length field.
this.seek(lengthPos);
writeLength(length);
this.incr(length);
return length;
}
writeVector8(cb) {
return this._writeVector(Math.pow(2, 8), len => this.writeUint8(len), cb);
}
writeVector16(cb) {
return this._writeVector(Math.pow(2, 16), len => this.writeUint16(len), cb);
}
writeVector24(cb) {
return this._writeVector(Math.pow(2, 24), len => this.writeUint24(len), cb);
}
writeVectorBytes8(bytes) {
return this.writeVector8(buf => {
buf.writeBytes(bytes);
});
}
writeVectorBytes16(bytes) {
return this.writeVector16(buf => {
buf.writeBytes(bytes);
});
}
writeVectorBytes24(bytes) {
return this.writeVector24(buf => {
buf.writeBytes(bytes);
});
}
}
// CONCATENATED MODULE: ./src/crypto.js
/* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
//
// Low-level crypto primitives.
//
// This file implements the AEAD encrypt/decrypt and hashing routines
// for the TLS_AES_128_GCM_SHA256 ciphersuite. They are (thankfully)
// fairly light-weight wrappers around what's available via the WebCrypto
// API.
//
const AEAD_SIZE_INFLATION = 16;
const KEY_LENGTH = 16;
const IV_LENGTH = 12;
const HASH_LENGTH = 32;
async function prepareKey(key, mode) {
return crypto.subtle.importKey('raw', key, { name: 'AES-GCM' }, false, [mode]);
}
async function encrypt(key, iv, plaintext, additionalData) {
const ciphertext = await crypto.subtle.encrypt({
additionalData,
iv,
name: 'AES-GCM',
tagLength: AEAD_SIZE_INFLATION * 8
}, key, plaintext);
return new Uint8Array(ciphertext);
}
async function decrypt(key, iv, ciphertext, additionalData) {
try {
const plaintext = await crypto.subtle.decrypt({
additionalData,
iv,
name: 'AES-GCM',
tagLength: AEAD_SIZE_INFLATION * 8
}, key, ciphertext);
return new Uint8Array(plaintext);
} catch (err) {
// Yes, we really do throw 'decrypt_error' when failing to verify a HMAC,
// and a 'bad_record_mac' error when failing to decrypt.
throw new TLSError(ALERT_DESCRIPTION.BAD_RECORD_MAC);
}
}
async function hash(message) {
return new Uint8Array(await crypto.subtle.digest({ name: 'SHA-256' }, message));
}
async function hmac(keyBytes, message) {
const key = await crypto.subtle.importKey('raw', keyBytes, {
hash: { name: 'SHA-256' },
name: 'HMAC',
}, false, ['sign']);
const sig = await crypto.subtle.sign({ name: 'HMAC' }, key, message);
return new Uint8Array(sig);
}
async function verifyHmac(keyBytes, signature, message) {
const key = await crypto.subtle.importKey('raw', keyBytes, {
hash: { name: 'SHA-256' },
name: 'HMAC',
}, false, ['verify']);
if (! (await crypto.subtle.verify({ name: 'HMAC' }, key, signature, message))) {
// Yes, we really do throw 'decrypt_error' when failing to verify a HMAC,
// and a 'bad_record_mac' error when failing to decrypt.
throw new TLSError(ALERT_DESCRIPTION.DECRYPT_ERROR);
}
}
async function hkdfExtract(salt, ikm) {
return await hmac(salt, ikm);
}
async function hkdfExpand(prk, info, length) {
const N = Math.ceil(length / HASH_LENGTH);
if (N <= 0) {
throw new TLSError(ALERT_DESCRIPTION.INTERNAL_ERROR);
}
if (N >= 255) {
throw new TLSError(ALERT_DESCRIPTION.INTERNAL_ERROR);
}
const input = new utils_BufferWriter();
const output = new utils_BufferWriter();
let T = new Uint8Array(0);
for (let i = 1; i <= N; i++) {
input.writeBytes(T);
input.writeBytes(info);
input.writeUint8(i);
T = await hmac(prk, input.flush());
output.writeBytes(T);
}
return output.slice(0, length);
}
async function hkdfExpandLabel(secret, label, context, length) {
// struct {
// uint16 length = Length;
// opaque label < 7..255 > = "tls13 " + Label;
// opaque context < 0..255 > = Context;
// } HkdfLabel;
const hkdfLabel = new utils_BufferWriter();
hkdfLabel.writeUint16(length);
hkdfLabel.writeVectorBytes8(utf8ToBytes('tls13 ' + label));
hkdfLabel.writeVectorBytes8(context);
return hkdfExpand(secret, hkdfLabel.flush(), length);
}
async function getRandomBytes(size) {
const bytes = new Uint8Array(size);
crypto.getRandomValues(bytes);
return bytes;
}
// CONCATENATED MODULE: ./src/extensions.js
/* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
//
// Extension parsing.
//
// This file contains some helpers for reading/writing the various kinds
// of Extension that might appear in a HandshakeMessage.
//
// "Extensions" are how TLS signals the presence of particular bits of optional
// functionality in the protocol. Lots of parts of TLS1.3 that don't seem like
// they're optional are implemented in terms of an extension, IIUC because that's
// what was needed for a clean deployment in amongst earlier versions of the protocol.
//
/* eslint-disable sorting/sort-object-props */
const EXTENSION_TYPE = {
PRE_SHARED_KEY: 41,
SUPPORTED_VERSIONS: 43,
PSK_KEY_EXCHANGE_MODES: 45,
};
/* eslint-enable sorting/sort-object-props */
// Base class for generic reading/writing of extensions,
// which are all uniformly formatted as:
//
// struct {
// ExtensionType extension_type;
// opaque extension_data<0..2^16-1>;
// } Extension;
//
// Extensions always appear inside of a handshake message,
// and their internal structure may differ based on the
// type of that message.
class extensions_Extension {
get TYPE_TAG() {
throw new Error('not implemented');
}
static read(messageType, buf) {
const type = buf.readUint16();
let ext = {
TYPE_TAG: type,
};
buf.readVector16(buf => {
switch (type) {
case EXTENSION_TYPE.PRE_SHARED_KEY:
ext = extensions_PreSharedKeyExtension._read(messageType, buf);
break;
case EXTENSION_TYPE.SUPPORTED_VERSIONS:
ext = extensions_SupportedVersionsExtension._read(messageType, buf);
break;
case EXTENSION_TYPE.PSK_KEY_EXCHANGE_MODES:
ext = extensions_PskKeyExchangeModesExtension._read(messageType, buf);
break;
default:
// Skip over unrecognised extensions.
buf.incr(buf.length());
}
if (buf.hasMoreBytes()) {
throw new TLSError(ALERT_DESCRIPTION.DECODE_ERROR);
}
});
return ext;
}
write(messageType, buf) {
buf.writeUint16(this.TYPE_TAG);
buf.writeVector16(buf => {
this._write(messageType, buf);
});
}
static _read(messageType, buf) {
throw new Error('not implemented');
}
static _write(messageType, buf) {
throw new Error('not implemented');
}
}
// The PreSharedKey extension:
//
// struct {
// opaque identity<1..2^16-1>;
// uint32 obfuscated_ticket_age;
// } PskIdentity;
// opaque PskBinderEntry<32..255>;
// struct {
// PskIdentity identities<7..2^16-1>;
// PskBinderEntry binders<33..2^16-1>;
// } OfferedPsks;
// struct {
// select(Handshake.msg_type) {
// case client_hello: OfferedPsks;
// case server_hello: uint16 selected_identity;
// };
// } PreSharedKeyExtension;
class extensions_PreSharedKeyExtension extends extensions_Extension {
constructor(identities, binders, selectedIdentity) {
super();
this.identities = identities;
this.binders = binders;
this.selectedIdentity = selectedIdentity;
}
get TYPE_TAG() {
return EXTENSION_TYPE.PRE_SHARED_KEY;
}
static _read(messageType, buf) {
let identities = null, binders = null, selectedIdentity = null;
switch (messageType) {
case HANDSHAKE_TYPE.CLIENT_HELLO:
identities = []; binders = [];
buf.readVector16(buf => {
const identity = buf.readVectorBytes16();
buf.readBytes(4); // Skip over the ticket age.
identities.push(identity);
});
buf.readVector16(buf => {
const binder = buf.readVectorBytes8();
if (binder.byteLength < HASH_LENGTH) {
throw new TLSError(ALERT_DESCRIPTION.ILLEGAL_PARAMETER);
}
binders.push(binder);
});
if (identities.length !== binders.length) {
throw new TLSError(ALERT_DESCRIPTION.ILLEGAL_PARAMETER);
}
break;
case HANDSHAKE_TYPE.SERVER_HELLO:
selectedIdentity = buf.readUint16();
break;
default:
throw new TLSError(ALERT_DESCRIPTION.ILLEGAL_PARAMETER);
}
return new this(identities, binders, selectedIdentity);
}
_write(messageType, buf) {
switch (messageType) {
case HANDSHAKE_TYPE.CLIENT_HELLO:
buf.writeVector16(buf => {
this.identities.forEach(pskId => {
buf.writeVectorBytes16(pskId);
buf.writeUint32(0); // Zero for "tag age" field.
});
});
buf.writeVector16(buf => {
this.binders.forEach(pskBinder => {
buf.writeVectorBytes8(pskBinder);
});
});
break;
case HANDSHAKE_TYPE.SERVER_HELLO:
buf.writeUint16(this.selectedIdentity);
break;
default:
throw new TLSError(ALERT_DESCRIPTION.INTERNAL_ERROR);
}
}
}
// The SupportedVersions extension:
//
// struct {
// select(Handshake.msg_type) {
// case client_hello:
// ProtocolVersion versions < 2..254 >;
// case server_hello:
// ProtocolVersion selected_version;
// };
// } SupportedVersions;
class extensions_SupportedVersionsExtension extends extensions_Extension {
constructor(versions, selectedVersion) {
super();
this.versions = versions;
this.selectedVersion = selectedVersion;
}
get TYPE_TAG() {
return EXTENSION_TYPE.SUPPORTED_VERSIONS;
}
static _read(messageType, buf) {
let versions = null, selectedVersion = null;
switch (messageType) {
case HANDSHAKE_TYPE.CLIENT_HELLO:
versions = [];
buf.readVector8(buf => {
versions.push(buf.readUint16());
});
break;
case HANDSHAKE_TYPE.SERVER_HELLO:
selectedVersion = buf.readUint16();
break;
default:
throw new TLSError(ALERT_DESCRIPTION.ILLEGAL_PARAMETER);
}
return new this(versions, selectedVersion);
}
_write(messageType, buf) {
switch (messageType) {
case HANDSHAKE_TYPE.CLIENT_HELLO:
buf.writeVector8(buf => {
this.versions.forEach(version => {
buf.writeUint16(version);
});
});
break;
case HANDSHAKE_TYPE.SERVER_HELLO:
buf.writeUint16(this.selectedVersion);
break;
default:
throw new TLSError(ALERT_DESCRIPTION.INTERNAL_ERROR);
}
}
}
class extensions_PskKeyExchangeModesExtension extends extensions_Extension {
constructor(modes) {
super();
this.modes = modes;
}
get TYPE_TAG() {
return EXTENSION_TYPE.PSK_KEY_EXCHANGE_MODES;
}
static _read(messageType, buf) {
const modes = [];
switch (messageType) {
case HANDSHAKE_TYPE.CLIENT_HELLO:
buf.readVector8(buf => {
modes.push(buf.readUint8());
});
break;
default:
throw new TLSError(ALERT_DESCRIPTION.ILLEGAL_PARAMETER);
}
return new this(modes);
}
_write(messageType, buf) {
switch (messageType) {
case HANDSHAKE_TYPE.CLIENT_HELLO:
buf.writeVector8(buf => {
this.modes.forEach(mode => {
buf.writeUint8(mode);
});
});
break;
default:
throw new TLSError(ALERT_DESCRIPTION.INTERNAL_ERROR);
}
}
}
// CONCATENATED MODULE: ./src/constants.js
/* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
const VERSION_TLS_1_0 = 0x0301;
const VERSION_TLS_1_2 = 0x0303;
const VERSION_TLS_1_3 = 0x0304;
const TLS_AES_128_GCM_SHA256 = 0x1301;
const PSK_MODE_KE = 0;
// CONCATENATED MODULE: ./src/messages.js
/* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
//
// Message parsing.
//
// Herein we have code for reading and writing the various Handshake
// messages involved in the TLS protocol.
//
/* eslint-disable sorting/sort-object-props */
const HANDSHAKE_TYPE = {
CLIENT_HELLO: 1,
SERVER_HELLO: 2,
NEW_SESSION_TICKET: 4,
ENCRYPTED_EXTENSIONS: 8,
FINISHED: 20,
};
/* eslint-enable sorting/sort-object-props */
// Base class for generic reading/writing of handshake messages,
// which are all uniformly formatted as:
//
// struct {
// HandshakeType msg_type; /* handshake type */
// uint24 length; /* bytes in message */
// select(Handshake.msg_type) {
// ... type specific cases here ...
// };
// } Handshake;
class messages_HandshakeMessage {
get TYPE_TAG() {
throw new Error('not implemented');
}
static fromBytes(bytes) {
// Each handshake message has a type and length prefix, per
const buf = new utils_BufferReader(bytes);
const msg = this.read(buf);
if (buf.hasMoreBytes()) {
throw new TLSError(ALERT_DESCRIPTION.DECODE_ERROR);
}
return msg;
}
toBytes() {
const buf = new utils_BufferWriter();
this.write(buf);
return buf.flush();
}
static read(buf) {
const type = buf.readUint8();
let msg = null;
buf.readVector24(buf => {
switch (type) {
case HANDSHAKE_TYPE.CLIENT_HELLO:
msg = messages_ClientHello._read(buf);
break;
case HANDSHAKE_TYPE.SERVER_HELLO:
msg = messages_ServerHello._read(buf);
break;
case HANDSHAKE_TYPE.NEW_SESSION_TICKET:
msg = messages_NewSessionTicket._read(buf);
break;
case HANDSHAKE_TYPE.ENCRYPTED_EXTENSIONS:
msg = EncryptedExtensions._read(buf);
break;
case HANDSHAKE_TYPE.FINISHED:
msg = messages_Finished._read(buf);
break;
}
if (buf.hasMoreBytes()) {
throw new TLSError(ALERT_DESCRIPTION.DECODE_ERROR);
}
});
if (msg === null) {
throw new TLSError(ALERT_DESCRIPTION.UNEXPECTED_MESSAGE);
}
return msg;
}
write(buf) {
buf.writeUint8(this.TYPE_TAG);
buf.writeVector24(buf => {
this._write(buf);
});
}
static _read(buf) {
throw new Error('not implemented');
}
_write(buf) {
throw new Error('not implemented');
}
// Some little helpers for reading a list of extensions,
// which is uniformly represented as:
//
// Extension extensions<8..2^16-1>;
//
// Recognized extensions are returned as a Map from extension type
// to extension data object, with a special `lastSeenExtension`
// property to make it easy to check which one came last.
static _readExtensions(messageType, buf) {
const extensions = new Map();
buf.readVector16(buf => {
const ext = extensions_Extension.read(messageType, buf);
if (extensions.has(ext.TYPE_TAG)) {
throw new TLSError(ALERT_DESCRIPTION.DECODE_ERROR);
}
extensions.set(ext.TYPE_TAG, ext);
extensions.lastSeenExtension = ext.TYPE_TAG;
});
return extensions;
}
_writeExtensions(buf, extensions) {
buf.writeVector16(buf => {
extensions.forEach(ext => {
ext.write(this.TYPE_TAG, buf);
});
});
}
}
// The ClientHello message:
//
// struct {
// ProtocolVersion legacy_version = 0x0303;
// Random random;
// opaque legacy_session_id<0..32>;
// CipherSuite cipher_suites<2..2^16-2>;
// opaque legacy_compression_methods<1..2^8-1>;
// Extension extensions<8..2^16-1>;
// } ClientHello;
class messages_ClientHello extends messages_HandshakeMessage {
constructor(random, sessionId, extensions) {
super();
this.random = random;
this.sessionId = sessionId;
this.extensions = extensions;
}
get TYPE_TAG() {
return HANDSHAKE_TYPE.CLIENT_HELLO;
}
static _read(buf) {
// The legacy_version field may indicate an earlier version of TLS
// for backwards compatibility, but must not predate TLS 1.0!
if (buf.readUint16() < VERSION_TLS_1_0) {
throw new TLSError(ALERT_DESCRIPTION.PROTOCOL_VERSION);
}
// The random bytes provided by the peer.
const random = buf.readBytes(32);
// Read legacy_session_id, so the server can echo it.
const sessionId = buf.readVectorBytes8();
// We only support a single ciphersuite, but the peer may offer several.
// Scan the list to confirm that the one we want is present.
let found = false;
buf.readVector16(buf => {
const cipherSuite = buf.readUint16();
if (cipherSuite === TLS_AES_128_GCM_SHA256) {
found = true;
}
});
if (! found) {
throw new TLSError(ALERT_DESCRIPTION.HANDSHAKE_FAILURE);
}
// legacy_compression_methods must be a single zero byte for TLS1.3 ClientHellos.
// It can be non-zero in previous versions of TLS, but we're not going to
// make a successful handshake with such versions, so better to just bail out now.
const legacyCompressionMethods = buf.readVectorBytes8();
if (legacyCompressionMethods.byteLength !== 1) {
throw new TLSError(ALERT_DESCRIPTION.ILLEGAL_PARAMETER);
}
if (legacyCompressionMethods[0] !== 0x00) {
throw new TLSError(ALERT_DESCRIPTION.ILLEGAL_PARAMETER);
}
// Read and check the extensions.
const extensions = this._readExtensions(HANDSHAKE_TYPE.CLIENT_HELLO, buf);
if (! extensions.has(EXTENSION_TYPE.SUPPORTED_VERSIONS)) {
throw new TLSError(ALERT_DESCRIPTION.MISSING_EXTENSION);
}
if (extensions.get(EXTENSION_TYPE.SUPPORTED_VERSIONS).versions.indexOf(VERSION_TLS_1_3) === -1) {
throw new TLSError(ALERT_DESCRIPTION.PROTOCOL_VERSION);
}
// Was the PreSharedKey extension the last one?
if (extensions.has(EXTENSION_TYPE.PRE_SHARED_KEY)) {
if (extensions.lastSeenExtension !== EXTENSION_TYPE.PRE_SHARED_KEY) {
throw new TLSError(ALERT_DESCRIPTION.ILLEGAL_PARAMETER);
}
}
return new this(random, sessionId, extensions);
}
_write(buf) {
buf.writeUint16(VERSION_TLS_1_2);
buf.writeBytes(this.random);
buf.writeVectorBytes8(this.sessionId);
// Our single supported ciphersuite
buf.writeVector16(buf => {
buf.writeUint16(TLS_AES_128_GCM_SHA256);
});
// A single zero byte for legacy_compression_methods
buf.writeVectorBytes8(new Uint8Array(1));
this._writeExtensions(buf, this.extensions);
}
}
// The ServerHello message:
//
// struct {
// ProtocolVersion legacy_version = 0x0303; /* TLS v1.2 */
// Random random;
// opaque legacy_session_id_echo<0..32>;
// CipherSuite cipher_suite;
// uint8 legacy_compression_method = 0;
// Extension extensions < 6..2 ^ 16 - 1 >;
// } ServerHello;
class messages_ServerHello extends messages_HandshakeMessage {
constructor(random, sessionId, extensions) {
super();
this.random = random;
this.sessionId = sessionId;
this.extensions = extensions;
}
get TYPE_TAG() {
return HANDSHAKE_TYPE.SERVER_HELLO;
}
static _read(buf) {
// Fixed value for legacy_version.
if (buf.readUint16() !== VERSION_TLS_1_2) {
throw new TLSError(ALERT_DESCRIPTION.ILLEGAL_PARAMETER);
}
// Random bytes from the server.
const random = buf.readBytes(32);
// It should have echoed our vector for legacy_session_id.
const sessionId = buf.readVectorBytes8();
// It should have selected our single offered ciphersuite.
if (buf.readUint16() !== TLS_AES_128_GCM_SHA256) {
throw new TLSError(ALERT_DESCRIPTION.ILLEGAL_PARAMETER);
}
// legacy_compression_method must be zero.
if (buf.readUint8() !== 0) {
throw new TLSError(ALERT_DESCRIPTION.ILLEGAL_PARAMETER);
}
const extensions = this._readExtensions(HANDSHAKE_TYPE.SERVER_HELLO, buf);
if (! extensions.has(EXTENSION_TYPE.SUPPORTED_VERSIONS)) {
throw new TLSError(ALERT_DESCRIPTION.MISSING_EXTENSION);
}
if (extensions.get(EXTENSION_TYPE.SUPPORTED_VERSIONS).selectedVersion !== VERSION_TLS_1_3) {
throw new TLSError(ALERT_DESCRIPTION.ILLEGAL_PARAMETER);
}
return new this(random, sessionId, extensions);
}
_write(buf) {
buf.writeUint16(VERSION_TLS_1_2);
buf.writeBytes(this.random);
buf.writeVectorBytes8(this.sessionId);
// Our single supported ciphersuite
buf.writeUint16(TLS_AES_128_GCM_SHA256);
// A single zero byte for legacy_compression_method
buf.writeUint8(0);
this._writeExtensions(buf, this.extensions);
}
}
// The EncryptedExtensions message:
//
// struct {
// Extension extensions < 0..2 ^ 16 - 1 >;
// } EncryptedExtensions;
//
// We don't actually send any EncryptedExtensions,
// but still have to send an empty message.
class EncryptedExtensions extends messages_HandshakeMessage {
constructor(extensions) {
super();
this.extensions = extensions;
}
get TYPE_TAG() {
return HANDSHAKE_TYPE.ENCRYPTED_EXTENSIONS;
}
static _read(buf) {
const extensions = this._readExtensions(HANDSHAKE_TYPE.ENCRYPTED_EXTENSIONS, buf);
return new this(extensions);
}
_write(buf) {
this._writeExtensions(buf, this.extensions);
}
}
// The Finished message:
//
// struct {
// opaque verify_data[Hash.length];
// } Finished;
class messages_Finished extends messages_HandshakeMessage {
constructor(verifyData) {
super();
this.verifyData = verifyData;
}
get TYPE_TAG() {
return HANDSHAKE_TYPE.FINISHED;
}
static _read(buf) {
const verifyData = buf.readBytes(HASH_LENGTH);
return new this(verifyData);
}
_write(buf) {
buf.writeBytes(this.verifyData);
}
}
// The NewSessionTicket message:
//
// struct {
// uint32 ticket_lifetime;
// uint32 ticket_age_add;
// opaque ticket_nonce < 0..255 >;
// opaque ticket < 1..2 ^ 16 - 1 >;
// Extension extensions < 0..2 ^ 16 - 2 >;
// } NewSessionTicket;
//
// We don't actually make use of these, but we need to be able
// to accept them and do basic validation.
class messages_NewSessionTicket extends messages_HandshakeMessage {
constructor(ticketLifetime, ticketAgeAdd, ticketNonce, ticket, extensions) {
super();
this.ticketLifetime = ticketLifetime;
this.ticketAgeAdd = ticketAgeAdd;
this.ticketNonce = ticketNonce;
this.ticket = ticket;
this.extensions = extensions;
}
get TYPE_TAG() {
return HANDSHAKE_TYPE.NEW_SESSION_TICKET;
}
static _read(buf) {
const ticketLifetime = buf.readUint32();
const ticketAgeAdd = buf.readUint32();
const ticketNonce = buf.readVectorBytes8();
const ticket = buf.readVectorBytes16();
if (ticket.byteLength < 1) {
throw new TLSError(ALERT_DESCRIPTION.DECODE_ERROR);
}
const extensions = this._readExtensions(HANDSHAKE_TYPE.NEW_SESSION_TICKET, buf);
return new this(ticketLifetime, ticketAgeAdd, ticketNonce, ticket, extensions);
}
_write(buf) {
buf.writeUint32(this.ticketLifetime);
buf.writeUint32(this.ticketAgeAdd);
buf.writeVectorBytes8(this.ticketNonce);
buf.writeVectorBytes16(this.ticket);
this._writeExtensions(buf, this.extensions);
}
}
// CONCATENATED MODULE: ./src/states.js
/* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
//
// State-machine for TLS Handshake Management.
//
// Internally, we manage the TLS connection by explicitly modelling the
// client and server state-machines from RFC8446. You can think of
// these `State` objects as little plugins for the `Connection` class
// that provide different behaviours of `send` and `receive` depending
// on the state of the connection.
//
class states_State {
constructor(conn) {
this.conn = conn;
}
async initialize() {
// By default, nothing to do when entering the state.
}
async sendApplicationData(bytes) {
// By default, assume we're not ready to send yet and the caller
// should be blocking on the connection promise before reaching here.
throw new TLSError(ALERT_DESCRIPTION.INTERNAL_ERROR);
}
async recvApplicationData(bytes) {
throw new TLSError(ALERT_DESCRIPTION.UNEXPECTED_MESSAGE);
}
async recvHandshakeMessage(msg) {
throw new TLSError(ALERT_DESCRIPTION.UNEXPECTED_MESSAGE);
}
async recvAlertMessage(alert) {
switch (alert.description) {
case ALERT_DESCRIPTION.CLOSE_NOTIFY:
this.conn._closeForRecv(alert);
throw alert;
default:
return await this.handleErrorAndRethrow(alert);
}
}
async recvChangeCipherSpec(bytes) {
throw new TLSError(ALERT_DESCRIPTION.UNEXPECTED_MESSAGE);
}
async handleErrorAndRethrow(err) {
let alert = err;
if (! (alert instanceof TLSAlert)) {
alert = new TLSError(ALERT_DESCRIPTION.INTERNAL_ERROR);
}
// Try to send error alert to the peer, but we may not
// be able to if the outgoing connection was already closed.
try {
await this.conn._sendAlertMessage(alert);
} catch (_) { }
await this.conn._transition(ERROR, err);
throw err;
}
async close() {
const alert = new TLSCloseNotify();
await this.conn._sendAlertMessage(alert);
this.conn._closeForSend(alert);
}
}
// A special "guard" state to prevent us from using
// an improperly-initialized Connection.
class UNINITIALIZED extends states_State {
async initialize() {
throw new Error('uninitialized state');
}
async sendApplicationData(bytes) {
throw new Error('uninitialized state');
}
async recvApplicationData(bytes) {
throw new Error('uninitialized state');
}
async recvHandshakeMessage(msg) {
throw new Error('uninitialized state');
}
async recvChangeCipherSpec(bytes) {
throw new Error('uninitialized state');
}
async handleErrorAndRethrow(err) {
throw err;
}
async close() {
throw new Error('uninitialized state');
}
}
// A special "error" state for when something goes wrong.
// This state never transitions to another state, effectively
// terminating the connection.
class ERROR extends states_State {
async initialize(err) {
this.error = err;
this.conn._setConnectionFailure(err);
// Unceremoniously shut down the record layer on error.
this.conn._recordlayer.setSendError(err);
this.conn._recordlayer.setRecvError(err);
}
async sendApplicationData(bytes) {
throw this.error;
}
async recvApplicationData(bytes) {
throw this.error;
}
async recvHandshakeMessage(msg) {
throw this.error;
}
async recvAlertMessage(err) {
throw this.error;
}
async recvChangeCipherSpec(bytes) {
throw this.error;
}
async handleErrorAndRethrow(err) {
throw err;
}
async close() {
throw this.error;
}