/*! * * 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. const {setTimeout} = ChromeUtils.importESModule("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; const EXPORTED_SYMBOLS = ["FxAccountsPairingChannel"]; 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, ) // await rl.send(RECORD_TYPE.HANDSHAKE, ) // 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 * @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} * @private */ const privateData = new WeakMap(); /** * Cache for wrapper classes. * @type {WeakMap} * @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>} * @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} 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 */ 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 */ 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"];