tor-browser

The Tor Browser
git clone https://git.dasho.dev/tor-browser.git
Log | Files | Refs | README | LICENSE

utils.sys.mjs (16411B)


      1 /* This Source Code Form is subject to the terms of the Mozilla Public
      2 * License, v. 2.0. If a copy of the MPL was not distributed with this file,
      3 * You can obtain one at http://mozilla.org/MPL/2.0/. */
      4 
      5 import { Observers } from "resource://services-common/observers.sys.mjs";
      6 
      7 import { CommonUtils } from "resource://services-common/utils.sys.mjs";
      8 
      9 const lazy = {};
     10 
     11 ChromeUtils.defineLazyGetter(lazy, "textEncoder", function () {
     12  return new TextEncoder();
     13 });
     14 
     15 /**
     16 * A number of `Legacy` suffixed functions are exposed by CryptoUtils.
     17 * They work with octet strings, which were used before Javascript
     18 * got ArrayBuffer and friends.
     19 */
     20 export var CryptoUtils = {
     21  xor(a, b) {
     22    let bytes = [];
     23 
     24    if (a.length != b.length) {
     25      throw new Error(
     26        "can't xor unequal length strings: " + a.length + " vs " + b.length
     27      );
     28    }
     29 
     30    for (let i = 0; i < a.length; i++) {
     31      bytes[i] = a.charCodeAt(i) ^ b.charCodeAt(i);
     32    }
     33 
     34    return String.fromCharCode.apply(String, bytes);
     35  },
     36 
     37  /**
     38   * Generate a string of random bytes.
     39   *
     40   * @returns {string} Octet string
     41   */
     42  generateRandomBytesLegacy(length) {
     43    let bytes = CryptoUtils.generateRandomBytes(length);
     44    return CommonUtils.arrayBufferToByteString(bytes);
     45  },
     46 
     47  generateRandomBytes(length) {
     48    return crypto.getRandomValues(new Uint8Array(length));
     49  },
     50 
     51  /**
     52   * UTF8-encode a message and hash it with the given hasher. Returns a
     53   * string containing bytes.
     54   */
     55  digestUTF8(message, hasher) {
     56    let data = lazy.textEncoder.encode(message);
     57    hasher.update(data, data.length);
     58    let result = hasher.finish(false);
     59    return result;
     60  },
     61 
     62  /**
     63   * Treat the given message as a bytes string (if necessary) and hash it with
     64   * the given hasher. Returns a string containing bytes.
     65   */
     66  digestBytes(bytes, hasher) {
     67    if (typeof bytes == "string" || bytes instanceof String) {
     68      bytes = CommonUtils.byteStringToArrayBuffer(bytes);
     69    }
     70    return CryptoUtils.digestBytesArray(bytes, hasher);
     71  },
     72 
     73  digestBytesArray(bytes, hasher) {
     74    hasher.update(bytes, bytes.length);
     75    let result = hasher.finish(false);
     76    return result;
     77  },
     78 
     79  /**
     80   * Encode the message into UTF-8 and feed the resulting bytes into the
     81   * given hasher. Does not return a hash. This can be called multiple times
     82   * with a single hasher, but eventually you must extract the result
     83   * yourself.
     84   */
     85  updateUTF8(message, hasher) {
     86    let bytes = lazy.textEncoder.encode(message);
     87    hasher.update(bytes, bytes.length);
     88  },
     89 
     90  sha256(message) {
     91    let hasher = Cc["@mozilla.org/security/hash;1"].createInstance(
     92      Ci.nsICryptoHash
     93    );
     94    hasher.init(hasher.SHA256);
     95    return CommonUtils.bytesAsHex(CryptoUtils.digestUTF8(message, hasher));
     96  },
     97 
     98  sha256Base64(message) {
     99    let data = lazy.textEncoder.encode(message);
    100    let hasher = Cc["@mozilla.org/security/hash;1"].createInstance(
    101      Ci.nsICryptoHash
    102    );
    103    hasher.init(hasher.SHA256);
    104    hasher.update(data, data.length);
    105    return hasher.finish(true);
    106  },
    107 
    108  /**
    109   * @param {string} alg Hash algorithm (common values are SHA-1 or SHA-256)
    110   * @param {string} key Key as an octet string.
    111   * @param {string} data Data as an octet string.
    112   */
    113  async hmacLegacy(alg, key, data) {
    114    if (!key || !key.length) {
    115      key = "\0";
    116    }
    117    data = CommonUtils.byteStringToArrayBuffer(data);
    118    key = CommonUtils.byteStringToArrayBuffer(key);
    119    const result = await CryptoUtils.hmac(alg, key, data);
    120    return CommonUtils.arrayBufferToByteString(result);
    121  },
    122 
    123  /**
    124   * @param {string} ikm IKM as an octet string.
    125   * @param {string} salt Salt as an Hex string.
    126   * @param {string} info Info as a regular string.
    127   * @param {number} len Desired output length in bytes.
    128   */
    129  async hkdfLegacy(ikm, xts, info, len) {
    130    ikm = CommonUtils.byteStringToArrayBuffer(ikm);
    131    xts = CommonUtils.byteStringToArrayBuffer(xts);
    132    info = lazy.textEncoder.encode(info);
    133    const okm = await CryptoUtils.hkdf(ikm, xts, info, len);
    134    return CommonUtils.arrayBufferToByteString(okm);
    135  },
    136 
    137  /**
    138   * @param {string} alg Hash algorithm (common values are SHA-1 or SHA-256)
    139   * @param {BufferSource} key
    140   * @param {BufferSource} data
    141   * @returns {Promise<Uint8Array>}
    142   */
    143  async hmac(alg, key, data) {
    144    const hmacKey = await crypto.subtle.importKey(
    145      "raw",
    146      key,
    147      { name: "HMAC", hash: alg },
    148      false,
    149      ["sign"]
    150    );
    151    const result = await crypto.subtle.sign("HMAC", hmacKey, data);
    152    return new Uint8Array(result);
    153  },
    154 
    155  /**
    156   * @param {ArrayBuffer} ikm
    157   * @param {ArrayBuffer} salt
    158   * @param {ArrayBuffer} info
    159   * @param {number} len Desired output length in bytes.
    160   * @returns {Uint8Array}
    161   */
    162  async hkdf(ikm, salt, info, len) {
    163    const key = await crypto.subtle.importKey(
    164      "raw",
    165      ikm,
    166      { name: "HKDF" },
    167      false,
    168      ["deriveBits"]
    169    );
    170    const okm = await crypto.subtle.deriveBits(
    171      {
    172        name: "HKDF",
    173        hash: "SHA-256",
    174        salt,
    175        info,
    176      },
    177      key,
    178      len * 8
    179    );
    180    return new Uint8Array(okm);
    181  },
    182 
    183  /**
    184   * PBKDF2 password stretching with SHA-256 hmac.
    185   *
    186   * @param {string} passphrase Passphrase as an octet string.
    187   * @param {string} salt Salt as an octet string.
    188   * @param {string} iterations Number of iterations, a positive integer.
    189   * @param {string} len Desired output length in bytes.
    190   */
    191  async pbkdf2Generate(passphrase, salt, iterations, len) {
    192    passphrase = CommonUtils.byteStringToArrayBuffer(passphrase);
    193    salt = CommonUtils.byteStringToArrayBuffer(salt);
    194    const key = await crypto.subtle.importKey(
    195      "raw",
    196      passphrase,
    197      { name: "PBKDF2" },
    198      false,
    199      ["deriveBits"]
    200    );
    201    const output = await crypto.subtle.deriveBits(
    202      {
    203        name: "PBKDF2",
    204        hash: "SHA-256",
    205        salt,
    206        iterations,
    207      },
    208      key,
    209      len * 8
    210    );
    211    return CommonUtils.arrayBufferToByteString(new Uint8Array(output));
    212  },
    213 
    214  /**
    215   * Compute the HTTP MAC SHA-1 for an HTTP request.
    216   *
    217   * @param  identifier
    218   *         (string) MAC Key Identifier.
    219   * @param  key
    220   *         (string) MAC Key.
    221   * @param  method
    222   *         (string) HTTP request method.
    223   * @param  URI
    224   *         (nsIURI) HTTP request URI.
    225   * @param  extra
    226   *         (object) Optional extra parameters. Valid keys are:
    227   *           nonce_bytes - How many bytes the nonce should be. This defaults
    228   *             to 8. Note that this many bytes are Base64 encoded, so the
    229   *             string length of the nonce will be longer than this value.
    230   *           ts - Timestamp to use. Should only be defined for testing.
    231   *           nonce - String nonce. Should only be defined for testing as this
    232   *             function will generate a cryptographically secure random one
    233   *             if not defined.
    234   *           ext - Extra string to be included in MAC. Per the HTTP MAC spec,
    235   *             the format is undefined and thus application specific.
    236   * @returns
    237   *         (object) Contains results of operation and input arguments (for
    238   *           symmetry). The object has the following keys:
    239   *
    240   *           identifier - (string) MAC Key Identifier (from arguments).
    241   *           key - (string) MAC Key (from arguments).
    242   *           method - (string) HTTP request method (from arguments).
    243   *           hostname - (string) HTTP hostname used (derived from arguments).
    244   *           port - (string) HTTP port number used (derived from arguments).
    245   *           mac - (string) Raw HMAC digest bytes.
    246   *           getHeader - (function) Call to obtain the string Authorization
    247   *             header value for this invocation.
    248   *           nonce - (string) Nonce value used.
    249   *           ts - (number) Integer seconds since Unix epoch that was used.
    250   */
    251  async computeHTTPMACSHA1(identifier, key, method, uri, extra) {
    252    let ts = extra && extra.ts ? extra.ts : Math.floor(Date.now() / 1000);
    253    let nonce_bytes = extra && extra.nonce_bytes > 0 ? extra.nonce_bytes : 8;
    254 
    255    // We are allowed to use more than the Base64 alphabet if we want.
    256    let nonce =
    257      extra && extra.nonce
    258        ? extra.nonce
    259        : btoa(CryptoUtils.generateRandomBytesLegacy(nonce_bytes));
    260 
    261    let host = uri.asciiHost;
    262    let port;
    263    let usedMethod = method.toUpperCase();
    264 
    265    if (uri.port != -1) {
    266      port = uri.port;
    267    } else if (uri.scheme == "http") {
    268      port = "80";
    269    } else if (uri.scheme == "https") {
    270      port = "443";
    271    } else {
    272      throw new Error("Unsupported URI scheme: " + uri.scheme);
    273    }
    274 
    275    let ext = extra && extra.ext ? extra.ext : "";
    276 
    277    let requestString =
    278      ts.toString(10) +
    279      "\n" +
    280      nonce +
    281      "\n" +
    282      usedMethod +
    283      "\n" +
    284      uri.pathQueryRef +
    285      "\n" +
    286      host +
    287      "\n" +
    288      port +
    289      "\n" +
    290      ext +
    291      "\n";
    292 
    293    const mac = await CryptoUtils.hmacLegacy("SHA-1", key, requestString);
    294 
    295    function getHeader() {
    296      return CryptoUtils.getHTTPMACSHA1Header(
    297        this.identifier,
    298        this.ts,
    299        this.nonce,
    300        this.mac,
    301        this.ext
    302      );
    303    }
    304 
    305    return {
    306      identifier,
    307      key,
    308      method: usedMethod,
    309      hostname: host,
    310      port,
    311      mac,
    312      nonce,
    313      ts,
    314      ext,
    315      getHeader,
    316    };
    317  },
    318 
    319  /**
    320   * Obtain the HTTP MAC Authorization header value from fields.
    321   *
    322   * @param  identifier
    323   *         (string) MAC key identifier.
    324   * @param  ts
    325   *         (number) Integer seconds since Unix epoch.
    326   * @param  nonce
    327   *         (string) Nonce value.
    328   * @param  mac
    329   *         (string) Computed HMAC digest (raw bytes).
    330   * @param  ext
    331   *         (optional) (string) Extra string content.
    332   * @returns
    333   *         (string) Value to put in Authorization header.
    334   */
    335  getHTTPMACSHA1Header: function getHTTPMACSHA1Header(
    336    identifier,
    337    ts,
    338    nonce,
    339    mac,
    340    ext
    341  ) {
    342    let header =
    343      'MAC id="' +
    344      identifier +
    345      '", ' +
    346      'ts="' +
    347      ts +
    348      '", ' +
    349      'nonce="' +
    350      nonce +
    351      '", ' +
    352      'mac="' +
    353      btoa(mac) +
    354      '"';
    355 
    356    if (!ext) {
    357      return header;
    358    }
    359 
    360    return (header += ', ext="' + ext + '"');
    361  },
    362 
    363  /**
    364   * Given an HTTP header value, strip out any attributes.
    365   */
    366 
    367  stripHeaderAttributes(value) {
    368    value = value || "";
    369    let i = value.indexOf(";");
    370    return value
    371      .substring(0, i >= 0 ? i : undefined)
    372      .trim()
    373      .toLowerCase();
    374  },
    375 
    376  /**
    377   * Compute the HAWK client values (mostly the header) for an HTTP request.
    378   *
    379   * @param  URI
    380   *         (nsIURI) HTTP request URI.
    381   * @param  method
    382   *         (string) HTTP request method.
    383   * @param  options
    384   *         (object) extra parameters (all but "credentials" are optional):
    385   *           credentials - (object, mandatory) HAWK credentials object.
    386   *             All three keys are required:
    387   *             id - (string) key identifier
    388   *             key - (string) raw key bytes
    389   *           ext - (string) application-specific data, included in MAC
    390   *           localtimeOffsetMsec - (number) local clock offset (vs server)
    391   *           payload - (string) payload to include in hash, containing the
    392   *                     HTTP request body. If not provided, the HAWK hash
    393   *                     will not cover the request body, and the server
    394   *                     should not check it either. This will be UTF-8
    395   *                     encoded into bytes before hashing. This function
    396   *                     cannot handle arbitrary binary data, sorry (the
    397   *                     UTF-8 encoding process will corrupt any codepoints
    398   *                     between U+0080 and U+00FF). Callers must be careful
    399   *                     to use an HTTP client function which encodes the
    400   *                     payload exactly the same way, otherwise the hash
    401   *                     will not match.
    402   *           contentType - (string) payload Content-Type. This is included
    403   *                         (without any attributes like "charset=") in the
    404   *                         HAWK hash. It does *not* affect interpretation
    405   *                         of the "payload" property.
    406   *           hash - (base64 string) pre-calculated payload hash. If
    407   *                  provided, "payload" is ignored.
    408   *           ts - (number) pre-calculated timestamp, secs since epoch
    409   *           now - (number) current time, ms-since-epoch, for tests
    410   *           nonce - (string) pre-calculated nonce. Should only be defined
    411   *                   for testing as this function will generate a
    412   *                   cryptographically secure random one if not defined.
    413   * @returns
    414   *         Promise<Object> Contains results of operation. The object has the
    415   *         following keys:
    416   *           field - (string) HAWK header, to use in Authorization: header
    417   *           artifacts - (object) other generated values:
    418   *             ts - (number) timestamp, in seconds since epoch
    419   *             nonce - (string)
    420   *             method - (string)
    421   *             resource - (string) path plus querystring
    422   *             host - (string)
    423   *             port - (number)
    424   *             hash - (string) payload hash (base64)
    425   *             ext - (string) app-specific data
    426   *             MAC - (string) request MAC (base64)
    427   */
    428  async computeHAWK(uri, method, options) {
    429    let credentials = options.credentials;
    430    let ts =
    431      options.ts ||
    432      Math.floor(
    433        ((options.now || Date.now()) + (options.localtimeOffsetMsec || 0)) /
    434          1000
    435      );
    436    let port;
    437    if (uri.port != -1) {
    438      port = uri.port;
    439    } else if (uri.scheme == "http") {
    440      port = 80;
    441    } else if (uri.scheme == "https") {
    442      port = 443;
    443    } else {
    444      throw new Error("Unsupported URI scheme: " + uri.scheme);
    445    }
    446 
    447    let artifacts = {
    448      ts,
    449      nonce: options.nonce || btoa(CryptoUtils.generateRandomBytesLegacy(8)),
    450      method: method.toUpperCase(),
    451      resource: uri.pathQueryRef, // This includes both path and search/queryarg.
    452      host: uri.asciiHost.toLowerCase(), // This includes punycoding.
    453      port: port.toString(10),
    454      hash: options.hash,
    455      ext: options.ext,
    456    };
    457 
    458    let contentType = CryptoUtils.stripHeaderAttributes(options.contentType);
    459 
    460    if (
    461      !artifacts.hash &&
    462      options.hasOwnProperty("payload") &&
    463      options.payload
    464    ) {
    465      const buffer = lazy.textEncoder.encode(
    466        `hawk.1.payload\n${contentType}\n${options.payload}\n`
    467      );
    468      const hash = await crypto.subtle.digest("SHA-256", buffer);
    469      // HAWK specifies this .hash to use +/ (not _-) and include the
    470      // trailing "==" padding.
    471      artifacts.hash = ChromeUtils.base64URLEncode(hash, { pad: true })
    472        .replace(/-/g, "+")
    473        .replace(/_/g, "/");
    474    }
    475 
    476    let requestString =
    477      "hawk.1.header\n" +
    478      artifacts.ts.toString(10) +
    479      "\n" +
    480      artifacts.nonce +
    481      "\n" +
    482      artifacts.method +
    483      "\n" +
    484      artifacts.resource +
    485      "\n" +
    486      artifacts.host +
    487      "\n" +
    488      artifacts.port +
    489      "\n" +
    490      (artifacts.hash || "") +
    491      "\n";
    492    if (artifacts.ext) {
    493      requestString += artifacts.ext.replace("\\", "\\\\").replace("\n", "\\n");
    494    }
    495    requestString += "\n";
    496 
    497    const hash = await CryptoUtils.hmacLegacy(
    498      "SHA-256",
    499      credentials.key,
    500      requestString
    501    );
    502    artifacts.mac = btoa(hash);
    503    // The output MAC uses "+" and "/", and padded== .
    504 
    505    function escape(attribute) {
    506      // This is used for "x=y" attributes inside HTTP headers.
    507      return attribute.replace(/\\/g, "\\\\").replace(/\"/g, '\\"');
    508    }
    509    let header =
    510      'Hawk id="' +
    511      credentials.id +
    512      '", ' +
    513      'ts="' +
    514      artifacts.ts +
    515      '", ' +
    516      'nonce="' +
    517      artifacts.nonce +
    518      '", ' +
    519      (artifacts.hash ? 'hash="' + artifacts.hash + '", ' : "") +
    520      (artifacts.ext ? 'ext="' + escape(artifacts.ext) + '", ' : "") +
    521      'mac="' +
    522      artifacts.mac +
    523      '"';
    524    return {
    525      artifacts,
    526      field: header,
    527    };
    528  },
    529 };
    530 
    531 var Svc = {};
    532 
    533 Observers.add("xpcom-shutdown", function unloadServices() {
    534  Observers.remove("xpcom-shutdown", unloadServices);
    535 
    536  for (let k in Svc) {
    537    delete Svc[k];
    538  }
    539 });