summaryrefslogtreecommitdiffstats
path: root/services/fxaccounts/FxAccountsPairingChannel.sys.mjs
diff options
context:
space:
mode:
authorDaniel Baumann <daniel.baumann@progress-linux.org>2024-04-19 01:47:29 +0000
committerDaniel Baumann <daniel.baumann@progress-linux.org>2024-04-19 01:47:29 +0000
commit0ebf5bdf043a27fd3dfb7f92e0cb63d88954c44d (patch)
treea31f07c9bcca9d56ce61e9a1ffd30ef350d513aa /services/fxaccounts/FxAccountsPairingChannel.sys.mjs
parentInitial commit. (diff)
downloadfirefox-esr-0ebf5bdf043a27fd3dfb7f92e0cb63d88954c44d.tar.xz
firefox-esr-0ebf5bdf043a27fd3dfb7f92e0cb63d88954c44d.zip
Adding upstream version 115.8.0esr.upstream/115.8.0esr
Signed-off-by: Daniel Baumann <daniel.baumann@progress-linux.org>
Diffstat (limited to 'services/fxaccounts/FxAccountsPairingChannel.sys.mjs')
-rw-r--r--services/fxaccounts/FxAccountsPairingChannel.sys.mjs3693
1 files changed, 3693 insertions, 0 deletions
diff --git a/services/fxaccounts/FxAccountsPairingChannel.sys.mjs b/services/fxaccounts/FxAccountsPairingChannel.sys.mjs
new file mode 100644
index 0000000000..cb6d3fdb91
--- /dev/null
+++ b/services/fxaccounts/FxAccountsPairingChannel.sys.mjs
@@ -0,0 +1,3693 @@
+/*!
+ *
+ * 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
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/.
+ *
+ * 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:
+ * https://github.com/mysticatea/event-target-shim/blob/master/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,
+// see https://bugzilla.mozilla.org/show_bug.cgi?id=784686
+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
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+/* 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
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+
+
+//
+// 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
+// vector structures defined in https://tools.ietf.org/html/rfc8446#section-3.4.
+//
+// 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
+ // defined in https://tools.ietf.org/html/rfc8446#section-3.4.
+ //
+ // 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
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+//
+// 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) {
+ // Ref https://tools.ietf.org/html/rfc5869#section-2.2
+ return await hmac(salt, ikm);
+}
+
+async function hkdfExpand(prk, info, length) {
+ // Ref https://tools.ietf.org/html/rfc5869#section-2.3
+ 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
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+//
+// 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
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+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
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+//
+// 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
+ // https://tools.ietf.org/html/rfc8446#appendix-B.3
+ 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
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+
+
+
+
+
+
+
+//
+// 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;
+ }
+}
+
+// The "connected" state, for when the handshake is complete
+// and we're ready to send application-level data.
+// The logic for this is largely symmetric between client and server.
+
+class states_CONNECTED extends states_State {
+ async initialize() {
+ this.conn._setConnectionSuccess();
+ }
+ async sendApplicationData(bytes) {
+ await this.conn._sendApplicationData(bytes);
+ }
+ async recvApplicationData(bytes) {
+ return bytes;
+ }
+ async recvChangeCipherSpec(bytes) {
+ throw new TLSError(ALERT_DESCRIPTION.UNEXPECTED_MESSAGE);
+ }
+}
+
+// A base class for states that occur in the middle of the handshake
+// (that is, between ClientHello and Finished). These states may receive
+// CHANGE_CIPHER_SPEC records for b/w compat reasons, which must contain
+// exactly a single 0x01 byte and must otherwise be ignored.
+
+class states_MidHandshakeState extends states_State {
+ async recvChangeCipherSpec(bytes) {
+ if (this.conn._hasSeenChangeCipherSpec) {
+ throw new TLSError(ALERT_DESCRIPTION.UNEXPECTED_MESSAGE);
+ }
+ if (bytes.byteLength !== 1 || bytes[0] !== 1) {
+ throw new TLSError(ALERT_DESCRIPTION.UNEXPECTED_MESSAGE);
+ }
+ this.conn._hasSeenChangeCipherSpec = true;
+ }
+}
+
+// These states implement (part of) the client state-machine from
+// https://tools.ietf.org/html/rfc8446#appendix-A.1
+//
+// Since we're only implementing a small subset of TLS1.3,
+// we only need a small subset of the handshake. It basically goes:
+//
+// * send ClientHello
+// * receive ServerHello
+// * receive EncryptedExtensions
+// * receive server Finished
+// * send client Finished
+//
+// We include some unused states for completeness, so that it's easier
+// to check the implementation against the diagrams in the RFC.
+
+class states_CLIENT_START extends states_State {
+ async initialize() {
+ const keyschedule = this.conn._keyschedule;
+ await keyschedule.addPSK(this.conn.psk);
+ // Construct a ClientHello message with our single PSK.
+ // We can't know the PSK binder value yet, so we initially write zeros.
+ const clientHello = new messages_ClientHello(
+ // Client random salt.
+ await getRandomBytes(32),
+ // Random legacy_session_id; we *could* send an empty string here,
+ // but sending a random one makes it easier to be compatible with
+ // the data emitted by tlslite-ng for test-case generation.
+ await getRandomBytes(32),
+ [
+ new extensions_SupportedVersionsExtension([VERSION_TLS_1_3]),
+ new extensions_PskKeyExchangeModesExtension([PSK_MODE_KE]),
+ new extensions_PreSharedKeyExtension([this.conn.pskId], [zeros(HASH_LENGTH)]),
+ ],
+ );
+ const buf = new utils_BufferWriter();
+ clientHello.write(buf);
+ // Now that we know what the ClientHello looks like,
+ // go back and calculate the appropriate PSK binder value.
+ // We only support a single PSK, so the length of the binders field is the
+ // length of the hash plus one for rendering it as a variable-length byte array,
+ // plus two for rendering the variable-length list of PSK binders.
+ const PSK_BINDERS_SIZE = HASH_LENGTH + 1 + 2;
+ const truncatedTranscript = buf.slice(0, buf.tell() - PSK_BINDERS_SIZE);
+ const pskBinder = await keyschedule.calculateFinishedMAC(keyschedule.extBinderKey, truncatedTranscript);
+ buf.incr(-HASH_LENGTH);
+ buf.writeBytes(pskBinder);
+ await this.conn._sendHandshakeMessageBytes(buf.flush());
+ await this.conn._transition(states_CLIENT_WAIT_SH, clientHello.sessionId);
+ }
+}
+
+class states_CLIENT_WAIT_SH extends states_State {
+ async initialize(sessionId) {
+ this._sessionId = sessionId;
+ }
+ async recvHandshakeMessage(msg) {
+ if (! (msg instanceof messages_ServerHello)) {
+ throw new TLSError(ALERT_DESCRIPTION.UNEXPECTED_MESSAGE);
+ }
+ if (! bytesAreEqual(msg.sessionId, this._sessionId)) {
+ throw new TLSError(ALERT_DESCRIPTION.ILLEGAL_PARAMETER);
+ }
+ const pskExt = msg.extensions.get(EXTENSION_TYPE.PRE_SHARED_KEY);
+ if (! pskExt) {
+ throw new TLSError(ALERT_DESCRIPTION.MISSING_EXTENSION);
+ }
+ // We expect only the SUPPORTED_VERSIONS and PRE_SHARED_KEY extensions.
+ if (msg.extensions.size !== 2) {
+ throw new TLSError(ALERT_DESCRIPTION.UNSUPPORTED_EXTENSION);
+ }
+ if (pskExt.selectedIdentity !== 0) {
+ throw new TLSError(ALERT_DESCRIPTION.ILLEGAL_PARAMETER);
+ }
+ await this.conn._keyschedule.addECDHE(null);
+ await this.conn._setSendKey(this.conn._keyschedule.clientHandshakeTrafficSecret);
+ await this.conn._setRecvKey(this.conn._keyschedule.serverHandshakeTrafficSecret);
+ await this.conn._transition(states_CLIENT_WAIT_EE);
+ }
+}
+
+class states_CLIENT_WAIT_EE extends states_MidHandshakeState {
+ async recvHandshakeMessage(msg) {
+ // We don't make use of any encrypted extensions, but we still
+ // have to wait for the server to send the (empty) list of them.
+ if (! (msg instanceof EncryptedExtensions)) {
+ throw new TLSError(ALERT_DESCRIPTION.UNEXPECTED_MESSAGE);
+ }
+ // We do not support any EncryptedExtensions.
+ if (msg.extensions.size !== 0) {
+ throw new TLSError(ALERT_DESCRIPTION.UNSUPPORTED_EXTENSION);
+ }
+ const keyschedule = this.conn._keyschedule;
+ const serverFinishedTranscript = keyschedule.getTranscript();
+ await this.conn._transition(states_CLIENT_WAIT_FINISHED, serverFinishedTranscript);
+ }
+}
+
+class states_CLIENT_WAIT_FINISHED extends states_State {
+ async initialize(serverFinishedTranscript) {
+ this._serverFinishedTranscript = serverFinishedTranscript;
+ }
+ async recvHandshakeMessage(msg) {
+ if (! (msg instanceof messages_Finished)) {
+ throw new TLSError(ALERT_DESCRIPTION.UNEXPECTED_MESSAGE);
+ }
+ // Verify server Finished MAC.
+ const keyschedule = this.conn._keyschedule;
+ await keyschedule.verifyFinishedMAC(keyschedule.serverHandshakeTrafficSecret, msg.verifyData, this._serverFinishedTranscript);
+ // Send our own Finished message in return.
+ // This must be encrypted with the handshake traffic key,
+ // but must not appear in the transcript used to calculate the application keys.
+ const clientFinishedMAC = await keyschedule.calculateFinishedMAC(keyschedule.clientHandshakeTrafficSecret);
+ await keyschedule.finalize();
+ await this.conn._sendHandshakeMessage(new messages_Finished(clientFinishedMAC));
+ await this.conn._setSendKey(keyschedule.clientApplicationTrafficSecret);
+ await this.conn._setRecvKey(keyschedule.serverApplicationTrafficSecret);
+ await this.conn._transition(states_CLIENT_CONNECTED);
+ }
+}
+
+class states_CLIENT_CONNECTED extends states_CONNECTED {
+ async recvHandshakeMessage(msg) {
+ // A connected client must be prepared to accept NewSessionTicket
+ // messages. We never use them, but other server implementations
+ // might send them.
+ if (! (msg instanceof messages_NewSessionTicket)) {
+ throw new TLSError(ALERT_DESCRIPTION.UNEXPECTED_MESSAGE);
+ }
+ }
+}
+
+// These states implement (part of) the server state-machine from
+// https://tools.ietf.org/html/rfc8446#appendix-A.2
+//
+// Since we're only implementing a small subset of TLS1.3,
+// we only need a small subset of the handshake. It basically goes:
+//
+// * receive ClientHello
+// * send ServerHello
+// * send empty EncryptedExtensions
+// * send server Finished
+// * receive client Finished
+//
+// We include some unused states for completeness, so that it's easier
+// to check the implementation against the diagrams in the RFC.
+
+class states_SERVER_START extends states_State {
+ async recvHandshakeMessage(msg) {
+ if (! (msg instanceof messages_ClientHello)) {
+ throw new TLSError(ALERT_DESCRIPTION.UNEXPECTED_MESSAGE);
+ }
+ // In the spec, this is where we select connection parameters, and maybe
+ // tell the client to try again if we can't find a compatible set.
+ // Since we only support a fixed cipherset, the only thing to "negotiate"
+ // is whether they provided an acceptable PSK.
+ const pskExt = msg.extensions.get(EXTENSION_TYPE.PRE_SHARED_KEY);
+ const pskModesExt = msg.extensions.get(EXTENSION_TYPE.PSK_KEY_EXCHANGE_MODES);
+ if (! pskExt || ! pskModesExt) {
+ throw new TLSError(ALERT_DESCRIPTION.MISSING_EXTENSION);
+ }
+ if (pskModesExt.modes.indexOf(PSK_MODE_KE) === -1) {
+ throw new TLSError(ALERT_DESCRIPTION.HANDSHAKE_FAILURE);
+ }
+ const pskIndex = pskExt.identities.findIndex(pskId => bytesAreEqual(pskId, this.conn.pskId));
+ if (pskIndex === -1) {
+ throw new TLSError(ALERT_DESCRIPTION.UNKNOWN_PSK_IDENTITY);
+ }
+ await this.conn._keyschedule.addPSK(this.conn.psk);
+ // Validate the PSK binder.
+ const keyschedule = this.conn._keyschedule;
+ const transcript = keyschedule.getTranscript();
+ // Calculate size occupied by the PSK binders.
+ let pskBindersSize = 2; // Vector16 representation overhead.
+ for (const binder of pskExt.binders) {
+ pskBindersSize += binder.byteLength + 1; // Vector8 representation overhead.
+ }
+ await keyschedule.verifyFinishedMAC(keyschedule.extBinderKey, pskExt.binders[pskIndex], transcript.slice(0, -pskBindersSize));
+ await this.conn._transition(states_SERVER_NEGOTIATED, msg.sessionId, pskIndex);
+ }
+}
+
+class states_SERVER_NEGOTIATED extends states_MidHandshakeState {
+ async initialize(sessionId, pskIndex) {
+ await this.conn._sendHandshakeMessage(new messages_ServerHello(
+ // Server random
+ await getRandomBytes(32),
+ sessionId,
+ [
+ new extensions_SupportedVersionsExtension(null, VERSION_TLS_1_3),
+ new extensions_PreSharedKeyExtension(null, null, pskIndex),
+ ]
+ ));
+ // If the client sent a non-empty sessionId, the server *must* send a change-cipher-spec for b/w compat.
+ if (sessionId.byteLength > 0) {
+ await this.conn._sendChangeCipherSpec();
+ }
+ // We can now transition to the encrypted part of the handshake.
+ const keyschedule = this.conn._keyschedule;
+ await keyschedule.addECDHE(null);
+ await this.conn._setSendKey(keyschedule.serverHandshakeTrafficSecret);
+ await this.conn._setRecvKey(keyschedule.clientHandshakeTrafficSecret);
+ // Send an empty EncryptedExtensions message.
+ await this.conn._sendHandshakeMessage(new EncryptedExtensions([]));
+ // Send the Finished message.
+ const serverFinishedMAC = await keyschedule.calculateFinishedMAC(keyschedule.serverHandshakeTrafficSecret);
+ await this.conn._sendHandshakeMessage(new messages_Finished(serverFinishedMAC));
+ // We can now *send* using the application traffic key,
+ // but have to wait to receive the client Finished before receiving under that key.
+ // We need to remember the handshake state from before the client Finished
+ // in order to successfully verify the client Finished.
+ const clientFinishedTranscript = await keyschedule.getTranscript();
+ const clientHandshakeTrafficSecret = keyschedule.clientHandshakeTrafficSecret;
+ await keyschedule.finalize();
+ await this.conn._setSendKey(keyschedule.serverApplicationTrafficSecret);
+ await this.conn._transition(states_SERVER_WAIT_FINISHED, clientHandshakeTrafficSecret, clientFinishedTranscript);
+ }
+}
+
+class states_SERVER_WAIT_FINISHED extends states_MidHandshakeState {
+ async initialize(clientHandshakeTrafficSecret, clientFinishedTranscript) {
+ this._clientHandshakeTrafficSecret = clientHandshakeTrafficSecret;
+ this._clientFinishedTranscript = clientFinishedTranscript;
+ }
+ async recvHandshakeMessage(msg) {
+ if (! (msg instanceof messages_Finished)) {
+ throw new TLSError(ALERT_DESCRIPTION.UNEXPECTED_MESSAGE);
+ }
+ const keyschedule = this.conn._keyschedule;
+ await keyschedule.verifyFinishedMAC(this._clientHandshakeTrafficSecret, msg.verifyData, this._clientFinishedTranscript);
+ this._clientHandshakeTrafficSecret = this._clientFinishedTranscript = null;
+ await this.conn._setRecvKey(keyschedule.clientApplicationTrafficSecret);
+ await this.conn._transition(states_CONNECTED);
+ }
+}
+
+// CONCATENATED MODULE: ./src/keyschedule.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
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+// TLS1.3 Key Schedule.
+//
+// In this file we implement the "key schedule" from
+// https://tools.ietf.org/html/rfc8446#section-7.1, which
+// defines how to calculate various keys as the handshake
+// state progresses.
+
+
+
+
+
+
+
+// The `KeySchedule` class progresses through three stages corresponding
+// to the three phases of the TLS1.3 key schedule:
+//
+// UNINITIALIZED
+// |
+// | addPSK()
+// v
+// EARLY_SECRET
+// |
+// | addECDHE()
+// v
+// HANDSHAKE_SECRET
+// |
+// | finalize()
+// v
+// MASTER_SECRET
+//
+// It will error out if the calling code attempts to add key material
+// in the wrong order.
+
+const STAGE_UNINITIALIZED = 0;
+const STAGE_EARLY_SECRET = 1;
+const STAGE_HANDSHAKE_SECRET = 2;
+const STAGE_MASTER_SECRET = 3;
+
+class keyschedule_KeySchedule {
+ constructor() {
+ this.stage = STAGE_UNINITIALIZED;
+ // WebCrypto doesn't support a rolling hash construct, so we have to
+ // keep the entire message transcript in memory.
+ this.transcript = new utils_BufferWriter();
+ // This tracks the main secret from with other keys are derived at each stage.
+ this.secret = null;
+ // And these are all the various keys we'll derive as the handshake progresses.
+ this.extBinderKey = null;
+ this.clientHandshakeTrafficSecret = null;
+ this.serverHandshakeTrafficSecret = null;
+ this.clientApplicationTrafficSecret = null;
+ this.serverApplicationTrafficSecret = null;
+ }
+
+ async addPSK(psk) {
+ // Use the selected PSK (if any) to calculate the "early secret".
+ if (psk === null) {
+ psk = zeros(HASH_LENGTH);
+ }
+ if (this.stage !== STAGE_UNINITIALIZED) {
+ throw new TLSError(ALERT_DESCRIPTION.INTERNAL_ERROR);
+ }
+ this.stage = STAGE_EARLY_SECRET;
+ this.secret = await hkdfExtract(zeros(HASH_LENGTH), psk);
+ this.extBinderKey = await this.deriveSecret('ext binder', EMPTY);
+ this.secret = await this.deriveSecret('derived', EMPTY);
+ }
+
+ async addECDHE(ecdhe) {
+ // Mix in the ECDHE output (if any) to calculate the "handshake secret".
+ if (ecdhe === null) {
+ ecdhe = zeros(HASH_LENGTH);
+ }
+ if (this.stage !== STAGE_EARLY_SECRET) {
+ throw new TLSError(ALERT_DESCRIPTION.INTERNAL_ERROR);
+ }
+ this.stage = STAGE_HANDSHAKE_SECRET;
+ this.extBinderKey = null;
+ this.secret = await hkdfExtract(this.secret, ecdhe);
+ this.clientHandshakeTrafficSecret = await this.deriveSecret('c hs traffic');
+ this.serverHandshakeTrafficSecret = await this.deriveSecret('s hs traffic');
+ this.secret = await this.deriveSecret('derived', EMPTY);
+ }
+
+ async finalize() {
+ if (this.stage !== STAGE_HANDSHAKE_SECRET) {
+ throw new TLSError(ALERT_DESCRIPTION.INTERNAL_ERROR);
+ }
+ this.stage = STAGE_MASTER_SECRET;
+ this.clientHandshakeTrafficSecret = null;
+ this.serverHandshakeTrafficSecret = null;
+ this.secret = await hkdfExtract(this.secret, zeros(HASH_LENGTH));
+ this.clientApplicationTrafficSecret = await this.deriveSecret('c ap traffic');
+ this.serverApplicationTrafficSecret = await this.deriveSecret('s ap traffic');
+ this.secret = null;
+ }
+
+ addToTranscript(bytes) {
+ this.transcript.writeBytes(bytes);
+ }
+
+ getTranscript() {
+ return this.transcript.slice();
+ }
+
+ async deriveSecret(label, transcript = undefined) {
+ transcript = transcript || this.getTranscript();
+ return await hkdfExpandLabel(this.secret, label, await hash(transcript), HASH_LENGTH);
+ }
+
+ async calculateFinishedMAC(baseKey, transcript = undefined) {
+ transcript = transcript || this.getTranscript();
+ const finishedKey = await hkdfExpandLabel(baseKey, 'finished', EMPTY, HASH_LENGTH);
+ return await hmac(finishedKey, await hash(transcript));
+ }
+
+ async verifyFinishedMAC(baseKey, mac, transcript = undefined) {
+ transcript = transcript || this.getTranscript();
+ const finishedKey = await hkdfExpandLabel(baseKey, 'finished', EMPTY, HASH_LENGTH);
+ await verifyHmac(finishedKey, mac, await hash(transcript));
+ }
+}
+
+// CONCATENATED MODULE: ./src/recordlayer.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
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+//
+// This file implements the "record layer" for TLS1.3, as defined in
+// https://tools.ietf.org/html/rfc8446#section-5.
+//
+// The record layer is responsible for encrypting/decrypting bytes to be
+// sent over the wire, including stateful management of sequence numbers
+// for the incoming and outgoing stream.
+//
+// The main interface is the RecordLayer class, which takes a callback function
+// sending data and can be used like so:
+//
+// rl = new RecordLayer(async function send_encrypted_data(data) {
+// // application-specific sending logic here.
+// });
+//
+// // Records are sent and received in plaintext by default,
+// // until you specify the key to use.
+// await rl.setSendKey(key)
+//
+// // Send some data by specifying the record type and the bytes.
+// // Where allowed by the record type, it will be buffered until
+// // explicitly flushed, and then sent by calling the callback.
+// await rl.send(RECORD_TYPE.HANDSHAKE, <bytes for a handshake message>)
+// await rl.send(RECORD_TYPE.HANDSHAKE, <bytes for another handshake message>)
+// await rl.flush()
+//
+// // Separate keys are used for sending and receiving.
+// rl.setRecvKey(key);
+//
+// // When data is received, push it into the RecordLayer
+// // and pass a callback that will be called with a [type, bytes]
+// // pair for each message parsed from the data.
+// rl.recv(dataReceivedFromPeer, async (type, bytes) => {
+// switch (type) {
+// case RECORD_TYPE.APPLICATION_DATA:
+// // do something with application data
+// case RECORD_TYPE.HANDSHAKE:
+// // do something with a handshake message
+// default:
+// // etc...
+// }
+// });
+//
+
+
+
+
+
+
+
+/* eslint-disable sorting/sort-object-props */
+const RECORD_TYPE = {
+ CHANGE_CIPHER_SPEC: 20,
+ ALERT: 21,
+ HANDSHAKE: 22,
+ APPLICATION_DATA: 23,
+};
+/* eslint-enable sorting/sort-object-props */
+
+// Encrypting at most 2^24 records will force us to stay
+// below data limits on AES-GCM encryption key use, and also
+// means we can accurately represent the sequence number as
+// a javascript double.
+const MAX_SEQUENCE_NUMBER = Math.pow(2, 24);
+const MAX_RECORD_SIZE = Math.pow(2, 14);
+const MAX_ENCRYPTED_RECORD_SIZE = MAX_RECORD_SIZE + 256;
+const RECORD_HEADER_SIZE = 5;
+
+// These are some helper classes to manage the encryption/decryption state
+// for a particular key.
+
+class recordlayer_CipherState {
+ constructor(key, iv) {
+ this.key = key;
+ this.iv = iv;
+ this.seqnum = 0;
+ }
+
+ static async create(baseKey, mode) {
+ // Derive key and iv per https://tools.ietf.org/html/rfc8446#section-7.3
+ const key = await prepareKey(await hkdfExpandLabel(baseKey, 'key', EMPTY, KEY_LENGTH), mode);
+ const iv = await hkdfExpandLabel(baseKey, 'iv', EMPTY, IV_LENGTH);
+ return new this(key, iv);
+ }
+
+ nonce() {
+ // Ref https://tools.ietf.org/html/rfc8446#section-5.3:
+ // * left-pad the sequence number with zeros to IV_LENGTH
+ // * xor with the provided iv
+ // Our sequence numbers are always less than 2^24, so fit in a Uint32
+ // in the last 4 bytes of the nonce.
+ const nonce = this.iv.slice();
+ const dv = new DataView(nonce.buffer, nonce.byteLength - 4, 4);
+ dv.setUint32(0, dv.getUint32(0) ^ this.seqnum);
+ this.seqnum += 1;
+ if (this.seqnum > MAX_SEQUENCE_NUMBER) {
+ throw new TLSError(ALERT_DESCRIPTION.INTERNAL_ERROR);
+ }
+ return nonce;
+ }
+}
+
+class recordlayer_EncryptionState extends recordlayer_CipherState {
+ static async create(key) {
+ return super.create(key, 'encrypt');
+ }
+
+ async encrypt(plaintext, additionalData) {
+ return await encrypt(this.key, this.nonce(), plaintext, additionalData);
+ }
+}
+
+class recordlayer_DecryptionState extends recordlayer_CipherState {
+ static async create(key) {
+ return super.create(key, 'decrypt');
+ }
+
+ async decrypt(ciphertext, additionalData) {
+ return await decrypt(this.key, this.nonce(), ciphertext, additionalData);
+ }
+}
+
+// The main RecordLayer class.
+
+class recordlayer_RecordLayer {
+ constructor(sendCallback) {
+ this.sendCallback = sendCallback;
+ this._sendEncryptState = null;
+ this._sendError = null;
+ this._recvDecryptState = null;
+ this._recvError = null;
+ this._pendingRecordType = 0;
+ this._pendingRecordBuf = null;
+ }
+
+ async setSendKey(key) {
+ await this.flush();
+ this._sendEncryptState = await recordlayer_EncryptionState.create(key);
+ }
+
+ async setRecvKey(key) {
+ this._recvDecryptState = await recordlayer_DecryptionState.create(key);
+ }
+
+ async setSendError(err) {
+ this._sendError = err;
+ }
+
+ async setRecvError(err) {
+ this._recvError = err;
+ }
+
+ async send(type, data) {
+ if (this._sendError !== null) {
+ throw this._sendError;
+ }
+ // Forbid sending data that doesn't fit into a single record.
+ // We do not support fragmentation over multiple records.
+ if (data.byteLength > MAX_RECORD_SIZE) {
+ throw new TLSError(ALERT_DESCRIPTION.INTERNAL_ERROR);
+ }
+ // Flush if we're switching to a different record type.
+ if (this._pendingRecordType && this._pendingRecordType !== type) {
+ await this.flush();
+ }
+ // Flush if we would overflow the max size of a record.
+ if (this._pendingRecordBuf !== null) {
+ if (this._pendingRecordBuf.tell() + data.byteLength > MAX_RECORD_SIZE) {
+ await this.flush();
+ }
+ }
+ // Start a new pending record if necessary.
+ // We reserve space at the start of the buffer for the record header,
+ // which is conveniently always a fixed size.
+ if (this._pendingRecordBuf === null) {
+ this._pendingRecordType = type;
+ this._pendingRecordBuf = new utils_BufferWriter();
+ this._pendingRecordBuf.incr(RECORD_HEADER_SIZE);
+ }
+ this._pendingRecordBuf.writeBytes(data);
+ }
+
+ async flush() {
+ // If there's nothing to flush, bail out early.
+ // Don't throw `_sendError` if we're not sending anything, because `flush()`
+ // can be called when we're trying to transition into an error state.
+ const buf = this._pendingRecordBuf;
+ let type = this._pendingRecordType;
+ if (! type) {
+ if (buf !== null) {
+ throw new TLSError(ALERT_DESCRIPTION.INTERNAL_ERROR);
+ }
+ return;
+ }
+ if (this._sendError !== null) {
+ throw this._sendError;
+ }
+ // If we're encrypting, turn the existing buffer contents into a `TLSInnerPlaintext` by
+ // appending the type. We don't do any zero-padding, although the spec allows it.
+ let inflation = 0, innerPlaintext = null;
+ if (this._sendEncryptState !== null) {
+ buf.writeUint8(type);
+ innerPlaintext = buf.slice(RECORD_HEADER_SIZE);
+ inflation = AEAD_SIZE_INFLATION;
+ type = RECORD_TYPE.APPLICATION_DATA;
+ }
+ // Write the common header for either `TLSPlaintext` or `TLSCiphertext` record.
+ const length = buf.tell() - RECORD_HEADER_SIZE + inflation;
+ buf.seek(0);
+ buf.writeUint8(type);
+ buf.writeUint16(VERSION_TLS_1_2);
+ buf.writeUint16(length);
+ // Followed by different payload depending on encryption status.
+ if (this._sendEncryptState !== null) {
+ const additionalData = buf.slice(0, RECORD_HEADER_SIZE);
+ const ciphertext = await this._sendEncryptState.encrypt(innerPlaintext, additionalData);
+ buf.writeBytes(ciphertext);
+ } else {
+ buf.incr(length);
+ }
+ this._pendingRecordBuf = null;
+ this._pendingRecordType = 0;
+ await this.sendCallback(buf.flush());
+ }
+
+ async recv(data) {
+ if (this._recvError !== null) {
+ throw this._recvError;
+ }
+ // For simplicity, we assume that the given data contains exactly one record.
+ // Peers using this library will send one record at a time over the websocket
+ // connection, and we can assume that the server-side websocket bridge will split
+ // up any traffic into individual records if we ever start interoperating with
+ // peers using a different TLS implementation.
+ // Similarly, we assume that handshake messages will not be fragmented across
+ // multiple records. This should be trivially true for the PSK-only mode used
+ // by this library, but we may want to relax it in future for interoperability
+ // with e.g. large ClientHello messages that contain lots of different options.
+ const buf = new utils_BufferReader(data);
+ // The data to read is either a TLSPlaintext or TLSCiphertext struct,
+ // depending on whether record protection has been enabled yet:
+ //
+ // struct {
+ // ContentType type;
+ // ProtocolVersion legacy_record_version;
+ // uint16 length;
+ // opaque fragment[TLSPlaintext.length];
+ // } TLSPlaintext;
+ //
+ // struct {
+ // ContentType opaque_type = application_data; /* 23 */
+ // ProtocolVersion legacy_record_version = 0x0303; /* TLS v1.2 */
+ // uint16 length;
+ // opaque encrypted_record[TLSCiphertext.length];
+ // } TLSCiphertext;
+ //
+ let type = buf.readUint8();
+ // The spec says legacy_record_version "MUST be ignored for all purposes",
+ // but we know TLS1.3 implementations will only ever emit two possible values,
+ // so it seems useful to bail out early if we receive anything else.
+ const version = buf.readUint16();
+ if (version !== VERSION_TLS_1_2) {
+ // TLS1.0 is only acceptable on initial plaintext records.
+ if (this._recvDecryptState !== null || version !== VERSION_TLS_1_0) {
+ throw new TLSError(ALERT_DESCRIPTION.DECODE_ERROR);
+ }
+ }
+ const length = buf.readUint16();
+ let plaintext;
+ if (this._recvDecryptState === null || type === RECORD_TYPE.CHANGE_CIPHER_SPEC) {
+ [type, plaintext] = await this._readPlaintextRecord(type, length, buf);
+ } else {
+ [type, plaintext] = await this._readEncryptedRecord(type, length, buf);
+ }
+ // Sanity-check that we received exactly one record.
+ if (buf.hasMoreBytes()) {
+ throw new TLSError(ALERT_DESCRIPTION.DECODE_ERROR);
+ }
+ return [type, plaintext];
+ }
+
+ // Helper to read an unencrypted `TLSPlaintext` struct
+
+ async _readPlaintextRecord(type, length, buf) {
+ if (length > MAX_RECORD_SIZE) {
+ throw new TLSError(ALERT_DESCRIPTION.RECORD_OVERFLOW);
+ }
+ return [type, buf.readBytes(length)];
+ }
+
+ // Helper to read an encrypted `TLSCiphertext` struct,
+ // decrypting it into plaintext.
+
+ async _readEncryptedRecord(type, length, buf) {
+ if (length > MAX_ENCRYPTED_RECORD_SIZE) {
+ throw new TLSError(ALERT_DESCRIPTION.RECORD_OVERFLOW);
+ }
+ // The outer type for encrypted records is always APPLICATION_DATA.
+ if (type !== RECORD_TYPE.APPLICATION_DATA) {
+ throw new TLSError(ALERT_DESCRIPTION.DECODE_ERROR);
+ }
+ // Decrypt and decode the contained `TLSInnerPlaintext` struct:
+ //
+ // struct {
+ // opaque content[TLSPlaintext.length];
+ // ContentType type;
+ // uint8 zeros[length_of_padding];
+ // } TLSInnerPlaintext;
+ //
+ // The additional data for the decryption is the `TLSCiphertext` record
+ // header, which is a fixed size and immediately prior to current buffer position.
+ buf.incr(-RECORD_HEADER_SIZE);
+ const additionalData = buf.readBytes(RECORD_HEADER_SIZE);
+ const ciphertext = buf.readBytes(length);
+ const paddedPlaintext = await this._recvDecryptState.decrypt(ciphertext, additionalData);
+ // We have to scan backwards over the zero padding at the end of the struct
+ // in order to find the non-zero `type` byte.
+ let i;
+ for (i = paddedPlaintext.byteLength - 1; i >= 0; i--) {
+ if (paddedPlaintext[i] !== 0) {
+ break;
+ }
+ }
+ if (i < 0) {
+ throw new TLSError(ALERT_DESCRIPTION.UNEXPECTED_MESSAGE);
+ }
+ type = paddedPlaintext[i];
+ // `change_cipher_spec` records must always be plaintext.
+ if (type === RECORD_TYPE.CHANGE_CIPHER_SPEC) {
+ throw new TLSError(ALERT_DESCRIPTION.DECODE_ERROR);
+ }
+ return [type, paddedPlaintext.slice(0, i)];
+ }
+}
+
+// CONCATENATED MODULE: ./src/tlsconnection.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
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+// The top-level APIs offered by this module are `ClientConnection` and
+// `ServerConnection` classes, which provide authenticated and encrypted
+// communication via the "externally-provisioned PSK" mode of TLS1.3.
+// They each take a callback to be used for sending data to the remote peer,
+// and operate like this:
+//
+// conn = await ClientConnection.create(psk, pskId, async function send_data_to_server(data) {
+// // application-specific sending logic here.
+// })
+//
+// // Send data to the server by calling `send`,
+// // which will use the callback provided in the constructor.
+// // A single `send()` by the application may result in multiple
+// // invokations of the callback.
+//
+// await conn.send('application-level data')
+//
+// // When data is received from the server, push it into
+// // the connection and let it return any decrypted app-level data.
+// // There might not be any app-level data if it was a protocol control
+// // message, and the receipt of the data might trigger additional calls
+// // to the send callback for protocol control purposes.
+//
+// serverSocket.on('data', async encrypted_data => {
+// const plaintext = await conn.recv(data)
+// if (plaintext !== null) {
+// do_something_with_app_level_data(plaintext)
+// }
+// })
+//
+// // It's good practice to explicitly close the connection
+// // when finished. This will send a "closed" notification
+// // to the server.
+//
+// await conn.close()
+//
+// // When the peer sends a "closed" notification it will show up
+// // as a `TLSCloseNotify` exception from recv:
+//
+// try {
+// data = await conn.recv(data);
+// } catch (err) {
+// if (! (err instanceof TLSCloseNotify) { throw err }
+// do_something_to_cleanly_close_data_connection();
+// }
+//
+// The `ServerConnection` API operates similarly; the distinction is mainly
+// in which side is expected to send vs receieve during the protocol handshake.
+
+
+
+
+
+
+
+
+
+
+class tlsconnection_Connection {
+ constructor(psk, pskId, sendCallback) {
+ this.psk = assertIsBytes(psk);
+ this.pskId = assertIsBytes(pskId);
+ this.connected = new Promise((resolve, reject) => {
+ this._onConnectionSuccess = resolve;
+ this._onConnectionFailure = reject;
+ });
+ this._state = new UNINITIALIZED(this);
+ this._handshakeRecvBuffer = null;
+ this._hasSeenChangeCipherSpec = false;
+ this._recordlayer = new recordlayer_RecordLayer(sendCallback);
+ this._keyschedule = new keyschedule_KeySchedule();
+ this._lastPromise = Promise.resolve();
+ }
+
+ // Subclasses will override this with some async initialization logic.
+ static async create(psk, pskId, sendCallback) {
+ return new this(psk, pskId, sendCallback);
+ }
+
+ // These are the three public API methods that consumers can use
+ // to send and receive data encrypted with TLS1.3.
+
+ async send(data) {
+ assertIsBytes(data);
+ await this.connected;
+ await this._synchronized(async () => {
+ await this._state.sendApplicationData(data);
+ });
+ }
+
+ async recv(data) {
+ assertIsBytes(data);
+ return await this._synchronized(async () => {
+ // Decrypt the data using the record layer.
+ // We expect to receive precisely one record at a time.
+ const [type, bytes] = await this._recordlayer.recv(data);
+ // Dispatch based on the type of the record.
+ switch (type) {
+ case RECORD_TYPE.CHANGE_CIPHER_SPEC:
+ await this._state.recvChangeCipherSpec(bytes);
+ return null;
+ case RECORD_TYPE.ALERT:
+ await this._state.recvAlertMessage(TLSAlert.fromBytes(bytes));
+ return null;
+ case RECORD_TYPE.APPLICATION_DATA:
+ return await this._state.recvApplicationData(bytes);
+ case RECORD_TYPE.HANDSHAKE:
+ // Multiple handshake messages may be coalesced into a single record.
+ // Store the in-progress record buffer on `this` so that we can guard
+ // against handshake messages that span a change in keys.
+ this._handshakeRecvBuffer = new utils_BufferReader(bytes);
+ if (! this._handshakeRecvBuffer.hasMoreBytes()) {
+ throw new TLSError(ALERT_DESCRIPTION.UNEXPECTED_MESSAGE);
+ }
+ do {
+ // Each handshake messages has a type and length prefix, per
+ // https://tools.ietf.org/html/rfc8446#appendix-B.3
+ this._handshakeRecvBuffer.incr(1);
+ const mlength = this._handshakeRecvBuffer.readUint24();
+ this._handshakeRecvBuffer.incr(-4);
+ const messageBytes = this._handshakeRecvBuffer.readBytes(mlength + 4);
+ this._keyschedule.addToTranscript(messageBytes);
+ await this._state.recvHandshakeMessage(messages_HandshakeMessage.fromBytes(messageBytes));
+ } while (this._handshakeRecvBuffer.hasMoreBytes());
+ this._handshakeRecvBuffer = null;
+ return null;
+ default:
+ throw new TLSError(ALERT_DESCRIPTION.UNEXPECTED_MESSAGE);
+ }
+ });
+ }
+
+ async close() {
+ await this._synchronized(async () => {
+ await this._state.close();
+ });
+ }
+
+ // Ensure that async functions execute one at a time,
+ // by waiting for the previous call to `_synchronized()` to complete
+ // before starting a new one. This helps ensure that we complete
+ // one state-machine transition before starting to do the next.
+ // It's also a convenient place to catch and alert on errors.
+
+ _synchronized(cb) {
+ const nextPromise = this._lastPromise.then(() => {
+ return cb();
+ }).catch(async err => {
+ if (err instanceof TLSCloseNotify) {
+ throw err;
+ }
+ await this._state.handleErrorAndRethrow(err);
+ });
+ // We don't want to hold on to the return value or error,
+ // just synchronize on the fact that it completed.
+ this._lastPromise = nextPromise.then(noop, noop);
+ return nextPromise;
+ }
+
+ // This drives internal transition of the state-machine,
+ // ensuring that the new state is properly initialized.
+
+ async _transition(State, ...args) {
+ this._state = new State(this);
+ await this._state.initialize(...args);
+ await this._recordlayer.flush();
+ }
+
+ // These are helpers to allow the State to manipulate the recordlayer
+ // and send out various types of data.
+
+ async _sendApplicationData(bytes) {
+ await this._recordlayer.send(RECORD_TYPE.APPLICATION_DATA, bytes);
+ await this._recordlayer.flush();
+ }
+
+ async _sendHandshakeMessage(msg) {
+ await this._sendHandshakeMessageBytes(msg.toBytes());
+ }
+
+ async _sendHandshakeMessageBytes(bytes) {
+ this._keyschedule.addToTranscript(bytes);
+ await this._recordlayer.send(RECORD_TYPE.HANDSHAKE, bytes);
+ // Don't flush after each handshake message, since we can probably
+ // coalesce multiple messages into a single record.
+ }
+
+ async _sendAlertMessage(err) {
+ await this._recordlayer.send(RECORD_TYPE.ALERT, err.toBytes());
+ await this._recordlayer.flush();
+ }
+
+ async _sendChangeCipherSpec() {
+ await this._recordlayer.send(RECORD_TYPE.CHANGE_CIPHER_SPEC, new Uint8Array([0x01]));
+ await this._recordlayer.flush();
+ }
+
+ async _setSendKey(key) {
+ return await this._recordlayer.setSendKey(key);
+ }
+
+ async _setRecvKey(key) {
+ // Handshake messages that change keys must be on a record boundary.
+ if (this._handshakeRecvBuffer && this._handshakeRecvBuffer.hasMoreBytes()) {
+ throw new TLSError(ALERT_DESCRIPTION.UNEXPECTED_MESSAGE);
+ }
+ return await this._recordlayer.setRecvKey(key);
+ }
+
+ _setConnectionSuccess() {
+ if (this._onConnectionSuccess !== null) {
+ this._onConnectionSuccess();
+ this._onConnectionSuccess = null;
+ this._onConnectionFailure = null;
+ }
+ }
+
+ _setConnectionFailure(err) {
+ if (this._onConnectionFailure !== null) {
+ this._onConnectionFailure(err);
+ this._onConnectionSuccess = null;
+ this._onConnectionFailure = null;
+ }
+ }
+
+ _closeForSend(alert) {
+ this._recordlayer.setSendError(alert);
+ }
+
+ _closeForRecv(alert) {
+ this._recordlayer.setRecvError(alert);
+ }
+}
+
+class tlsconnection_ClientConnection extends tlsconnection_Connection {
+ static async create(psk, pskId, sendCallback) {
+ const instance = await super.create(psk, pskId, sendCallback);
+ await instance._transition(states_CLIENT_START);
+ return instance;
+ }
+}
+
+class tlsconnection_ServerConnection extends tlsconnection_Connection {
+ static async create(psk, pskId, sendCallback) {
+ const instance = await super.create(psk, pskId, sendCallback);
+ await instance._transition(states_SERVER_START);
+ return instance;
+ }
+}
+
+// CONCATENATED MODULE: ./node_modules/event-target-shim/dist/event-target-shim.mjs
+/**
+ * @author Toru Nagashima <https://github.com/mysticatea>
+ * @copyright 2015 Toru Nagashima. All rights reserved.
+ * See LICENSE file in root directory for full license.
+ */
+/**
+ * @typedef {object} PrivateData
+ * @property {EventTarget} eventTarget The event target.
+ * @property {{type:string}} event The original event object.
+ * @property {number} eventPhase The current event phase.
+ * @property {EventTarget|null} currentTarget The current event target.
+ * @property {boolean} canceled The flag to prevent default.
+ * @property {boolean} stopped The flag to stop propagation.
+ * @property {boolean} immediateStopped The flag to stop propagation immediately.
+ * @property {Function|null} passiveListener The listener if the current listener is passive. Otherwise this is null.
+ * @property {number} timeStamp The unix time.
+ * @private
+ */
+
+/**
+ * Private data for event wrappers.
+ * @type {WeakMap<Event, PrivateData>}
+ * @private
+ */
+const privateData = new WeakMap();
+
+/**
+ * Cache for wrapper classes.
+ * @type {WeakMap<Object, Function>}
+ * @private
+ */
+const wrappers = new WeakMap();
+
+/**
+ * Get private data.
+ * @param {Event} event The event object to get private data.
+ * @returns {PrivateData} The private data of the event.
+ * @private
+ */
+function pd(event) {
+ const retv = privateData.get(event);
+ console.assert(
+ retv != null,
+ "'this' is expected an Event object, but got",
+ event
+ );
+ return retv
+}
+
+/**
+ * https://dom.spec.whatwg.org/#set-the-canceled-flag
+ * @param data {PrivateData} private data.
+ */
+function setCancelFlag(data) {
+ if (data.passiveListener != null) {
+ if (
+ typeof console !== "undefined" &&
+ typeof console.error === "function"
+ ) {
+ console.error(
+ "Unable to preventDefault inside passive event listener invocation.",
+ data.passiveListener
+ );
+ }
+ return
+ }
+ if (!data.event.cancelable) {
+ return
+ }
+
+ data.canceled = true;
+ if (typeof data.event.preventDefault === "function") {
+ data.event.preventDefault();
+ }
+}
+
+/**
+ * @see https://dom.spec.whatwg.org/#interface-event
+ * @private
+ */
+/**
+ * The event wrapper.
+ * @constructor
+ * @param {EventTarget} eventTarget The event target of this dispatching.
+ * @param {Event|{type:string}} event The original event to wrap.
+ */
+function Event(eventTarget, event) {
+ privateData.set(this, {
+ eventTarget,
+ event,
+ eventPhase: 2,
+ currentTarget: eventTarget,
+ canceled: false,
+ stopped: false,
+ immediateStopped: false,
+ passiveListener: null,
+ timeStamp: event.timeStamp || Date.now(),
+ });
+
+ // https://heycam.github.io/webidl/#Unforgeable
+ Object.defineProperty(this, "isTrusted", { value: false, enumerable: true });
+
+ // Define accessors
+ const keys = Object.keys(event);
+ for (let i = 0; i < keys.length; ++i) {
+ const key = keys[i];
+ if (!(key in this)) {
+ Object.defineProperty(this, key, defineRedirectDescriptor(key));
+ }
+ }
+}
+
+// Should be enumerable, but class methods are not enumerable.
+Event.prototype = {
+ /**
+ * The type of this event.
+ * @type {string}
+ */
+ get type() {
+ return pd(this).event.type
+ },
+
+ /**
+ * The target of this event.
+ * @type {EventTarget}
+ */
+ get target() {
+ return pd(this).eventTarget
+ },
+
+ /**
+ * The target of this event.
+ * @type {EventTarget}
+ */
+ get currentTarget() {
+ return pd(this).currentTarget
+ },
+
+ /**
+ * @returns {EventTarget[]} The composed path of this event.
+ */
+ composedPath() {
+ const currentTarget = pd(this).currentTarget;
+ if (currentTarget == null) {
+ return []
+ }
+ return [currentTarget]
+ },
+
+ /**
+ * Constant of NONE.
+ * @type {number}
+ */
+ get NONE() {
+ return 0
+ },
+
+ /**
+ * Constant of CAPTURING_PHASE.
+ * @type {number}
+ */
+ get CAPTURING_PHASE() {
+ return 1
+ },
+
+ /**
+ * Constant of AT_TARGET.
+ * @type {number}
+ */
+ get AT_TARGET() {
+ return 2
+ },
+
+ /**
+ * Constant of BUBBLING_PHASE.
+ * @type {number}
+ */
+ get BUBBLING_PHASE() {
+ return 3
+ },
+
+ /**
+ * The target of this event.
+ * @type {number}
+ */
+ get eventPhase() {
+ return pd(this).eventPhase
+ },
+
+ /**
+ * Stop event bubbling.
+ * @returns {void}
+ */
+ stopPropagation() {
+ const data = pd(this);
+
+ data.stopped = true;
+ if (typeof data.event.stopPropagation === "function") {
+ data.event.stopPropagation();
+ }
+ },
+
+ /**
+ * Stop event bubbling.
+ * @returns {void}
+ */
+ stopImmediatePropagation() {
+ const data = pd(this);
+
+ data.stopped = true;
+ data.immediateStopped = true;
+ if (typeof data.event.stopImmediatePropagation === "function") {
+ data.event.stopImmediatePropagation();
+ }
+ },
+
+ /**
+ * The flag to be bubbling.
+ * @type {boolean}
+ */
+ get bubbles() {
+ return Boolean(pd(this).event.bubbles)
+ },
+
+ /**
+ * The flag to be cancelable.
+ * @type {boolean}
+ */
+ get cancelable() {
+ return Boolean(pd(this).event.cancelable)
+ },
+
+ /**
+ * Cancel this event.
+ * @returns {void}
+ */
+ preventDefault() {
+ setCancelFlag(pd(this));
+ },
+
+ /**
+ * The flag to indicate cancellation state.
+ * @type {boolean}
+ */
+ get defaultPrevented() {
+ return pd(this).canceled
+ },
+
+ /**
+ * The flag to be composed.
+ * @type {boolean}
+ */
+ get composed() {
+ return Boolean(pd(this).event.composed)
+ },
+
+ /**
+ * The unix time of this event.
+ * @type {number}
+ */
+ get timeStamp() {
+ return pd(this).timeStamp
+ },
+
+ /**
+ * The target of this event.
+ * @type {EventTarget}
+ * @deprecated
+ */
+ get srcElement() {
+ return pd(this).eventTarget
+ },
+
+ /**
+ * The flag to stop event bubbling.
+ * @type {boolean}
+ * @deprecated
+ */
+ get cancelBubble() {
+ return pd(this).stopped
+ },
+ set cancelBubble(value) {
+ if (!value) {
+ return
+ }
+ const data = pd(this);
+
+ data.stopped = true;
+ if (typeof data.event.cancelBubble === "boolean") {
+ data.event.cancelBubble = true;
+ }
+ },
+
+ /**
+ * The flag to indicate cancellation state.
+ * @type {boolean}
+ * @deprecated
+ */
+ get returnValue() {
+ return !pd(this).canceled
+ },
+ set returnValue(value) {
+ if (!value) {
+ setCancelFlag(pd(this));
+ }
+ },
+
+ /**
+ * Initialize this event object. But do nothing under event dispatching.
+ * @param {string} type The event type.
+ * @param {boolean} [bubbles=false] The flag to be possible to bubble up.
+ * @param {boolean} [cancelable=false] The flag to be possible to cancel.
+ * @deprecated
+ */
+ initEvent() {
+ // Do nothing.
+ },
+};
+
+// `constructor` is not enumerable.
+Object.defineProperty(Event.prototype, "constructor", {
+ value: Event,
+ configurable: true,
+ writable: true,
+});
+
+// Ensure `event instanceof window.Event` is `true`.
+if (typeof window !== "undefined" && typeof window.Event !== "undefined") {
+ Object.setPrototypeOf(Event.prototype, window.Event.prototype);
+
+ // Make association for wrappers.
+ wrappers.set(window.Event.prototype, Event);
+}
+
+/**
+ * Get the property descriptor to redirect a given property.
+ * @param {string} key Property name to define property descriptor.
+ * @returns {PropertyDescriptor} The property descriptor to redirect the property.
+ * @private
+ */
+function defineRedirectDescriptor(key) {
+ return {
+ get() {
+ return pd(this).event[key]
+ },
+ set(value) {
+ pd(this).event[key] = value;
+ },
+ configurable: true,
+ enumerable: true,
+ }
+}
+
+/**
+ * Get the property descriptor to call a given method property.
+ * @param {string} key Property name to define property descriptor.
+ * @returns {PropertyDescriptor} The property descriptor to call the method property.
+ * @private
+ */
+function defineCallDescriptor(key) {
+ return {
+ value() {
+ const event = pd(this).event;
+ return event[key].apply(event, arguments)
+ },
+ configurable: true,
+ enumerable: true,
+ }
+}
+
+/**
+ * Define new wrapper class.
+ * @param {Function} BaseEvent The base wrapper class.
+ * @param {Object} proto The prototype of the original event.
+ * @returns {Function} The defined wrapper class.
+ * @private
+ */
+function defineWrapper(BaseEvent, proto) {
+ const keys = Object.keys(proto);
+ if (keys.length === 0) {
+ return BaseEvent
+ }
+
+ /** CustomEvent */
+ function CustomEvent(eventTarget, event) {
+ BaseEvent.call(this, eventTarget, event);
+ }
+
+ CustomEvent.prototype = Object.create(BaseEvent.prototype, {
+ constructor: { value: CustomEvent, configurable: true, writable: true },
+ });
+
+ // Define accessors.
+ for (let i = 0; i < keys.length; ++i) {
+ const key = keys[i];
+ if (!(key in BaseEvent.prototype)) {
+ const descriptor = Object.getOwnPropertyDescriptor(proto, key);
+ const isFunc = typeof descriptor.value === "function";
+ Object.defineProperty(
+ CustomEvent.prototype,
+ key,
+ isFunc
+ ? defineCallDescriptor(key)
+ : defineRedirectDescriptor(key)
+ );
+ }
+ }
+
+ return CustomEvent
+}
+
+/**
+ * Get the wrapper class of a given prototype.
+ * @param {Object} proto The prototype of the original event to get its wrapper.
+ * @returns {Function} The wrapper class.
+ * @private
+ */
+function getWrapper(proto) {
+ if (proto == null || proto === Object.prototype) {
+ return Event
+ }
+
+ let wrapper = wrappers.get(proto);
+ if (wrapper == null) {
+ wrapper = defineWrapper(getWrapper(Object.getPrototypeOf(proto)), proto);
+ wrappers.set(proto, wrapper);
+ }
+ return wrapper
+}
+
+/**
+ * Wrap a given event to management a dispatching.
+ * @param {EventTarget} eventTarget The event target of this dispatching.
+ * @param {Object} event The event to wrap.
+ * @returns {Event} The wrapper instance.
+ * @private
+ */
+function wrapEvent(eventTarget, event) {
+ const Wrapper = getWrapper(Object.getPrototypeOf(event));
+ return new Wrapper(eventTarget, event)
+}
+
+/**
+ * Get the immediateStopped flag of a given event.
+ * @param {Event} event The event to get.
+ * @returns {boolean} The flag to stop propagation immediately.
+ * @private
+ */
+function isStopped(event) {
+ return pd(event).immediateStopped
+}
+
+/**
+ * Set the current event phase of a given event.
+ * @param {Event} event The event to set current target.
+ * @param {number} eventPhase New event phase.
+ * @returns {void}
+ * @private
+ */
+function setEventPhase(event, eventPhase) {
+ pd(event).eventPhase = eventPhase;
+}
+
+/**
+ * Set the current target of a given event.
+ * @param {Event} event The event to set current target.
+ * @param {EventTarget|null} currentTarget New current target.
+ * @returns {void}
+ * @private
+ */
+function setCurrentTarget(event, currentTarget) {
+ pd(event).currentTarget = currentTarget;
+}
+
+/**
+ * Set a passive listener of a given event.
+ * @param {Event} event The event to set current target.
+ * @param {Function|null} passiveListener New passive listener.
+ * @returns {void}
+ * @private
+ */
+function setPassiveListener(event, passiveListener) {
+ pd(event).passiveListener = passiveListener;
+}
+
+/**
+ * @typedef {object} ListenerNode
+ * @property {Function} listener
+ * @property {1|2|3} listenerType
+ * @property {boolean} passive
+ * @property {boolean} once
+ * @property {ListenerNode|null} next
+ * @private
+ */
+
+/**
+ * @type {WeakMap<object, Map<string, ListenerNode>>}
+ * @private
+ */
+const listenersMap = new WeakMap();
+
+// Listener types
+const CAPTURE = 1;
+const BUBBLE = 2;
+const ATTRIBUTE = 3;
+
+/**
+ * Check whether a given value is an object or not.
+ * @param {any} x The value to check.
+ * @returns {boolean} `true` if the value is an object.
+ */
+function isObject(x) {
+ return x !== null && typeof x === "object" //eslint-disable-line no-restricted-syntax
+}
+
+/**
+ * Get listeners.
+ * @param {EventTarget} eventTarget The event target to get.
+ * @returns {Map<string, ListenerNode>} The listeners.
+ * @private
+ */
+function getListeners(eventTarget) {
+ const listeners = listenersMap.get(eventTarget);
+ if (listeners == null) {
+ throw new TypeError(
+ "'this' is expected an EventTarget object, but got another value."
+ )
+ }
+ return listeners
+}
+
+/**
+ * Get the property descriptor for the event attribute of a given event.
+ * @param {string} eventName The event name to get property descriptor.
+ * @returns {PropertyDescriptor} The property descriptor.
+ * @private
+ */
+function defineEventAttributeDescriptor(eventName) {
+ return {
+ get() {
+ const listeners = getListeners(this);
+ let node = listeners.get(eventName);
+ while (node != null) {
+ if (node.listenerType === ATTRIBUTE) {
+ return node.listener
+ }
+ node = node.next;
+ }
+ return null
+ },
+
+ set(listener) {
+ if (typeof listener !== "function" && !isObject(listener)) {
+ listener = null; // eslint-disable-line no-param-reassign
+ }
+ const listeners = getListeners(this);
+
+ // Traverse to the tail while removing old value.
+ let prev = null;
+ let node = listeners.get(eventName);
+ while (node != null) {
+ if (node.listenerType === ATTRIBUTE) {
+ // Remove old value.
+ if (prev !== null) {
+ prev.next = node.next;
+ } else if (node.next !== null) {
+ listeners.set(eventName, node.next);
+ } else {
+ listeners.delete(eventName);
+ }
+ } else {
+ prev = node;
+ }
+
+ node = node.next;
+ }
+
+ // Add new value.
+ if (listener !== null) {
+ const newNode = {
+ listener,
+ listenerType: ATTRIBUTE,
+ passive: false,
+ once: false,
+ next: null,
+ };
+ if (prev === null) {
+ listeners.set(eventName, newNode);
+ } else {
+ prev.next = newNode;
+ }
+ }
+ },
+ configurable: true,
+ enumerable: true,
+ }
+}
+
+/**
+ * Define an event attribute (e.g. `eventTarget.onclick`).
+ * @param {Object} eventTargetPrototype The event target prototype to define an event attrbite.
+ * @param {string} eventName The event name to define.
+ * @returns {void}
+ */
+function defineEventAttribute(eventTargetPrototype, eventName) {
+ Object.defineProperty(
+ eventTargetPrototype,
+ `on${eventName}`,
+ defineEventAttributeDescriptor(eventName)
+ );
+}
+
+/**
+ * Define a custom EventTarget with event attributes.
+ * @param {string[]} eventNames Event names for event attributes.
+ * @returns {EventTarget} The custom EventTarget.
+ * @private
+ */
+function defineCustomEventTarget(eventNames) {
+ /** CustomEventTarget */
+ function CustomEventTarget() {
+ EventTarget.call(this);
+ }
+
+ CustomEventTarget.prototype = Object.create(EventTarget.prototype, {
+ constructor: {
+ value: CustomEventTarget,
+ configurable: true,
+ writable: true,
+ },
+ });
+
+ for (let i = 0; i < eventNames.length; ++i) {
+ defineEventAttribute(CustomEventTarget.prototype, eventNames[i]);
+ }
+
+ return CustomEventTarget
+}
+
+/**
+ * EventTarget.
+ *
+ * - This is constructor if no arguments.
+ * - This is a function which returns a CustomEventTarget constructor if there are arguments.
+ *
+ * For example:
+ *
+ * class A extends EventTarget {}
+ * class B extends EventTarget("message") {}
+ * class C extends EventTarget("message", "error") {}
+ * class D extends EventTarget(["message", "error"]) {}
+ */
+function EventTarget() {
+ /*eslint-disable consistent-return */
+ if (this instanceof EventTarget) {
+ listenersMap.set(this, new Map());
+ return
+ }
+ if (arguments.length === 1 && Array.isArray(arguments[0])) {
+ return defineCustomEventTarget(arguments[0])
+ }
+ if (arguments.length > 0) {
+ const types = new Array(arguments.length);
+ for (let i = 0; i < arguments.length; ++i) {
+ types[i] = arguments[i];
+ }
+ return defineCustomEventTarget(types)
+ }
+ throw new TypeError("Cannot call a class as a function")
+ /*eslint-enable consistent-return */
+}
+
+// Should be enumerable, but class methods are not enumerable.
+EventTarget.prototype = {
+ /**
+ * Add a given listener to this event target.
+ * @param {string} eventName The event name to add.
+ * @param {Function} listener The listener to add.
+ * @param {boolean|{capture?:boolean,passive?:boolean,once?:boolean}} [options] The options for this listener.
+ * @returns {void}
+ */
+ addEventListener(eventName, listener, options) {
+ if (listener == null) {
+ return
+ }
+ if (typeof listener !== "function" && !isObject(listener)) {
+ throw new TypeError("'listener' should be a function or an object.")
+ }
+
+ const listeners = getListeners(this);
+ const optionsIsObj = isObject(options);
+ const capture = optionsIsObj
+ ? Boolean(options.capture)
+ : Boolean(options);
+ const listenerType = capture ? CAPTURE : BUBBLE;
+ const newNode = {
+ listener,
+ listenerType,
+ passive: optionsIsObj && Boolean(options.passive),
+ once: optionsIsObj && Boolean(options.once),
+ next: null,
+ };
+
+ // Set it as the first node if the first node is null.
+ let node = listeners.get(eventName);
+ if (node === undefined) {
+ listeners.set(eventName, newNode);
+ return
+ }
+
+ // Traverse to the tail while checking duplication..
+ let prev = null;
+ while (node != null) {
+ if (
+ node.listener === listener &&
+ node.listenerType === listenerType
+ ) {
+ // Should ignore duplication.
+ return
+ }
+ prev = node;
+ node = node.next;
+ }
+
+ // Add it.
+ prev.next = newNode;
+ },
+
+ /**
+ * Remove a given listener from this event target.
+ * @param {string} eventName The event name to remove.
+ * @param {Function} listener The listener to remove.
+ * @param {boolean|{capture?:boolean,passive?:boolean,once?:boolean}} [options] The options for this listener.
+ * @returns {void}
+ */
+ removeEventListener(eventName, listener, options) {
+ if (listener == null) {
+ return
+ }
+
+ const listeners = getListeners(this);
+ const capture = isObject(options)
+ ? Boolean(options.capture)
+ : Boolean(options);
+ const listenerType = capture ? CAPTURE : BUBBLE;
+
+ let prev = null;
+ let node = listeners.get(eventName);
+ while (node != null) {
+ if (
+ node.listener === listener &&
+ node.listenerType === listenerType
+ ) {
+ if (prev !== null) {
+ prev.next = node.next;
+ } else if (node.next !== null) {
+ listeners.set(eventName, node.next);
+ } else {
+ listeners.delete(eventName);
+ }
+ return
+ }
+
+ prev = node;
+ node = node.next;
+ }
+ },
+
+ /**
+ * Dispatch a given event.
+ * @param {Event|{type:string}} event The event to dispatch.
+ * @returns {boolean} `false` if canceled.
+ */
+ dispatchEvent(event) {
+ if (event == null || typeof event.type !== "string") {
+ throw new TypeError('"event.type" should be a string.')
+ }
+
+ // If listeners aren't registered, terminate.
+ const listeners = getListeners(this);
+ const eventName = event.type;
+ let node = listeners.get(eventName);
+ if (node == null) {
+ return true
+ }
+
+ // Since we cannot rewrite several properties, so wrap object.
+ const wrappedEvent = wrapEvent(this, event);
+
+ // This doesn't process capturing phase and bubbling phase.
+ // This isn't participating in a tree.
+ let prev = null;
+ while (node != null) {
+ // Remove this listener if it's once
+ if (node.once) {
+ if (prev !== null) {
+ prev.next = node.next;
+ } else if (node.next !== null) {
+ listeners.set(eventName, node.next);
+ } else {
+ listeners.delete(eventName);
+ }
+ } else {
+ prev = node;
+ }
+
+ // Call this listener
+ setPassiveListener(
+ wrappedEvent,
+ node.passive ? node.listener : null
+ );
+ if (typeof node.listener === "function") {
+ try {
+ node.listener.call(this, wrappedEvent);
+ } catch (err) {
+ if (
+ typeof console !== "undefined" &&
+ typeof console.error === "function"
+ ) {
+ console.error(err);
+ }
+ }
+ } else if (
+ node.listenerType !== ATTRIBUTE &&
+ typeof node.listener.handleEvent === "function"
+ ) {
+ node.listener.handleEvent(wrappedEvent);
+ }
+
+ // Break if `event.stopImmediatePropagation` was called.
+ if (isStopped(wrappedEvent)) {
+ break
+ }
+
+ node = node.next;
+ }
+ setPassiveListener(wrappedEvent, null);
+ setEventPhase(wrappedEvent, 0);
+ setCurrentTarget(wrappedEvent, null);
+
+ return !wrappedEvent.defaultPrevented
+ },
+};
+
+// `constructor` is not enumerable.
+Object.defineProperty(EventTarget.prototype, "constructor", {
+ value: EventTarget,
+ configurable: true,
+ writable: true,
+});
+
+// Ensure `eventTarget instanceof window.EventTarget` is `true`.
+if (
+ typeof window !== "undefined" &&
+ typeof window.EventTarget !== "undefined"
+) {
+ Object.setPrototypeOf(EventTarget.prototype, window.EventTarget.prototype);
+}
+
+/* harmony default export */ var event_target_shim = (EventTarget);
+
+
+// CONCATENATED MODULE: ./src/index.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
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+// A wrapper that combines a WebSocket to the channelserver
+// with some client-side encryption for securing the channel.
+//
+// This code is responsible for the event handling and the consumer API.
+// All the details of encrypting the messages are delegated to`./tlsconnection.js`.
+
+
+
+
+
+
+
+const CLOSE_FLUSH_BUFFER_INTERVAL_MS = 200;
+const CLOSE_FLUSH_BUFFER_MAX_TRIES = 5;
+
+class src_PairingChannel extends EventTarget {
+ constructor(channelId, channelKey, socket, connection) {
+ super();
+ this._channelId = channelId;
+ this._channelKey = channelKey;
+ this._socket = socket;
+ this._connection = connection;
+ this._selfClosed = false;
+ this._peerClosed = false;
+ this._setupListeners();
+ }
+
+ /**
+ * Create a new pairing channel.
+ *
+ * This will open a channel on the channelserver, and generate a random client-side
+ * encryption key. When the promise resolves, `this.channelId` and `this.channelKey`
+ * can be transferred to another client to allow it to securely connect to the channel.
+ *
+ * @returns Promise<PairingChannel>
+ */
+ static create(channelServerURI) {
+ const wsURI = new URL('/v1/ws/', channelServerURI).href;
+ const channelKey = crypto.getRandomValues(new Uint8Array(32));
+ // The one who creates the channel plays the role of 'server' in the underlying TLS exchange.
+ return this._makePairingChannel(wsURI, tlsconnection_ServerConnection, channelKey);
+ }
+
+ /**
+ * Connect to an existing pairing channel.
+ *
+ * This will connect to a channel on the channelserver previously established by
+ * another client calling `create`. The `channelId` and `channelKey` must have been
+ * obtained via some out-of-band mechanism (such as by scanning from a QR code).
+ *
+ * @returns Promise<PairingChannel>
+ */
+ static connect(channelServerURI, channelId, channelKey) {
+ const wsURI = new URL(`/v1/ws/${channelId}`, channelServerURI).href;
+ // The one who connects to an existing channel plays the role of 'client'
+ // in the underlying TLS exchange.
+ return this._makePairingChannel(wsURI, tlsconnection_ClientConnection, channelKey);
+ }
+
+ static _makePairingChannel(wsUri, ConnectionClass, psk) {
+ const socket = new WebSocket(wsUri);
+ return new Promise((resolve, reject) => {
+ // eslint-disable-next-line prefer-const
+ let stopListening;
+ const onConnectionError = async () => {
+ stopListening();
+ reject(new Error('Error while creating the pairing channel'));
+ };
+ const onFirstMessage = async event => {
+ stopListening();
+ try {
+ // The channelserver echos back the channel id, and we use it as an
+ // additional input to the TLS handshake via the "psk id" field.
+ const {channelid: channelId} = JSON.parse(event.data);
+ const pskId = utf8ToBytes(channelId);
+ const connection = await ConnectionClass.create(psk, pskId, data => {
+ // Send data by forwarding it via the channelserver websocket.
+ // The TLS connection gives us `data` as raw bytes, but channelserver
+ // expects b64urlsafe strings, because it wraps them in a JSON object envelope.
+ socket.send(bytesToBase64url(data));
+ });
+ const instance = new this(channelId, psk, socket, connection);
+ resolve(instance);
+ } catch (err) {
+ reject(err);
+ }
+ };
+ stopListening = () => {
+ socket.removeEventListener('close', onConnectionError);
+ socket.removeEventListener('error', onConnectionError);
+ socket.removeEventListener('message', onFirstMessage);
+ };
+ socket.addEventListener('close', onConnectionError);
+ socket.addEventListener('error', onConnectionError);
+ socket.addEventListener('message', onFirstMessage);
+ });
+ }
+
+ _setupListeners() {
+ this._socket.addEventListener('message', async event => {
+ try {
+ // When we receive data from the channelserver, pump it through the TLS connection
+ // to decrypt it, then echo it back out to consumers as an event.
+ const channelServerEnvelope = JSON.parse(event.data);
+ const payload = await this._connection.recv(base64urlToBytes(channelServerEnvelope.message));
+ if (payload !== null) {
+ const data = JSON.parse(bytesToUtf8(payload));
+ this.dispatchEvent(new CustomEvent('message', {
+ detail: {
+ data,
+ sender: channelServerEnvelope.sender,
+ },
+ }));
+ }
+ } catch (error) {
+ let event;
+ // The underlying TLS connection will signal a clean shutdown of the channel
+ // by throwing a special error, because it doesn't really have a better
+ // signally mechanism available.
+ if (error instanceof TLSCloseNotify) {
+ this._peerClosed = true;
+ if (this._selfClosed) {
+ this._shutdown();
+ }
+ event = new CustomEvent('close');
+ } else {
+ event = new CustomEvent('error', {
+ detail: {
+ error,
+ }
+ });
+ }
+ this.dispatchEvent(event);
+ }
+ });
+ // Relay the WebSocket events.
+ this._socket.addEventListener('error', () => {
+ this._shutdown();
+ // The dispatched event that we receive has no useful information.
+ this.dispatchEvent(new CustomEvent('error', {
+ detail: {
+ error: new Error('WebSocket error.'),
+ },
+ }));
+ });
+ // In TLS, the peer has to explicitly send a close notification,
+ // which we dispatch above. Unexpected socket close is an error.
+ this._socket.addEventListener('close', () => {
+ this._shutdown();
+ if (! this._peerClosed) {
+ this.dispatchEvent(new CustomEvent('error', {
+ detail: {
+ error: new Error('WebSocket unexpectedly closed'),
+ }
+ }));
+ }
+ });
+ }
+
+ /**
+ * @param {Object} data
+ */
+ async send(data) {
+ const payload = utf8ToBytes(JSON.stringify(data));
+ await this._connection.send(payload);
+ }
+
+ async close() {
+ this._selfClosed = true;
+ await this._connection.close();
+ try {
+ // Ensure all queued bytes have been sent before closing the connection.
+ let tries = 0;
+ while (this._socket.bufferedAmount > 0) {
+ if (++tries > CLOSE_FLUSH_BUFFER_MAX_TRIES) {
+ throw new Error('Could not flush the outgoing buffer in time.');
+ }
+ await new Promise(res => setTimeout(res, CLOSE_FLUSH_BUFFER_INTERVAL_MS));
+ }
+ } finally {
+ // If the peer hasn't closed, we might still receive some data.
+ if (this._peerClosed) {
+ this._shutdown();
+ }
+ }
+ }
+
+ _shutdown() {
+ if (this._socket) {
+ this._socket.close();
+ this._socket = null;
+ this._connection = null;
+ }
+ }
+
+ get closed() {
+ return (! this._socket) || (this._socket.readyState === 3);
+ }
+
+ get channelId() {
+ return this._channelId;
+ }
+
+ get channelKey() {
+ return this._channelKey;
+ }
+}
+
+// Re-export helpful utilities for calling code to use.
+
+
+// For running tests using the built bundle,
+// expose a bunch of implementation details.
+
+
+
+
+
+
+
+const _internals = {
+ arrayToBytes: arrayToBytes,
+ BufferReader: utils_BufferReader,
+ BufferWriter: utils_BufferWriter,
+ bytesAreEqual: bytesAreEqual,
+ bytesToHex: bytesToHex,
+ bytesToUtf8: bytesToUtf8,
+ ClientConnection: tlsconnection_ClientConnection,
+ Connection: tlsconnection_Connection,
+ DecryptionState: recordlayer_DecryptionState,
+ EncryptedExtensions: EncryptedExtensions,
+ EncryptionState: recordlayer_EncryptionState,
+ Finished: messages_Finished,
+ HASH_LENGTH: HASH_LENGTH,
+ hexToBytes: hexToBytes,
+ hkdfExpand: hkdfExpand,
+ KeySchedule: keyschedule_KeySchedule,
+ NewSessionTicket: messages_NewSessionTicket,
+ RecordLayer: recordlayer_RecordLayer,
+ ServerConnection: tlsconnection_ServerConnection,
+ utf8ToBytes: utf8ToBytes,
+ zeros: zeros,
+};
+
+
+/***/ })
+/******/ ])["PairingChannel"];