tor-browser

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

hawkrequest.sys.mjs (5452B)


      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 { Log } from "resource://gre/modules/Log.sys.mjs";
      6 
      7 import { RESTRequest } from "resource://services-common/rest.sys.mjs";
      8 import { CommonUtils } from "resource://services-common/utils.sys.mjs";
      9 import { Credentials } from "resource://gre/modules/Credentials.sys.mjs";
     10 
     11 const lazy = {};
     12 
     13 ChromeUtils.defineESModuleGetters(lazy, {
     14  CryptoUtils: "moz-src:///services/crypto/modules/utils.sys.mjs",
     15 });
     16 
     17 /**
     18 * Single-use HAWK-authenticated HTTP requests to RESTish resources.
     19 *
     20 * @param uri
     21 *        (String) URI for the RESTRequest constructor
     22 *
     23 * @param credentials
     24 *        (Object) Optional credentials for computing HAWK authentication
     25 *        header.
     26 *
     27 * @param payloadObj
     28 *        (Object) Optional object to be converted to JSON payload
     29 *
     30 * @param extra
     31 *        (Object) Optional extra params for HAWK header computation.
     32 *        Valid properties are:
     33 *
     34 *          now:                 <current time in milliseconds>,
     35 *          localtimeOffsetMsec: <local clock offset vs server>,
     36 *          headers:             <An object with header/value pairs to be sent
     37 *                                as headers on the request>
     38 *
     39 * extra.localtimeOffsetMsec is the value in milliseconds that must be added to
     40 * the local clock to make it agree with the server's clock.  For instance, if
     41 * the local clock is two minutes ahead of the server, the time offset in
     42 * milliseconds will be -120000.
     43 */
     44 
     45 export var HAWKAuthenticatedRESTRequest = function HawkAuthenticatedRESTRequest(
     46  uri,
     47  credentials,
     48  extra = {}
     49 ) {
     50  RESTRequest.call(this, uri);
     51 
     52  this.credentials = credentials;
     53  this.now = extra.now || Date.now();
     54  this.localtimeOffsetMsec = extra.localtimeOffsetMsec || 0;
     55  this._log.trace(
     56    "local time, offset: " + this.now + ", " + this.localtimeOffsetMsec
     57  );
     58  this.extraHeaders = extra.headers || {};
     59 
     60  // Expose for testing
     61  this._intl = getIntl();
     62 };
     63 
     64 HAWKAuthenticatedRESTRequest.prototype = {
     65  async dispatch(method, data) {
     66    let contentType = "text/plain";
     67    if (method == "POST" || method == "PUT" || method == "PATCH") {
     68      contentType = "application/json";
     69    }
     70    if (this.credentials) {
     71      let options = {
     72        now: this.now,
     73        localtimeOffsetMsec: this.localtimeOffsetMsec,
     74        credentials: this.credentials,
     75        payload: (data && JSON.stringify(data)) || "",
     76        contentType,
     77      };
     78      let header = await lazy.CryptoUtils.computeHAWK(
     79        this.uri,
     80        method,
     81        options
     82      );
     83      this.setHeader("Authorization", header.field);
     84    }
     85 
     86    for (let header in this.extraHeaders) {
     87      this.setHeader(header, this.extraHeaders[header]);
     88    }
     89 
     90    this.setHeader("Content-Type", contentType);
     91 
     92    this.setHeader("Accept-Language", this._intl.accept_languages);
     93 
     94    return super.dispatch(method, data);
     95  },
     96 };
     97 
     98 Object.setPrototypeOf(
     99  HAWKAuthenticatedRESTRequest.prototype,
    100  RESTRequest.prototype
    101 );
    102 
    103 /**
    104 * Generic function to derive Hawk credentials.
    105 *
    106 * Hawk credentials are derived using shared secrets, which depend on the token
    107 * in use.
    108 *
    109 * @param tokenHex
    110 *        The current session token encoded in hex
    111 * @param context
    112 *        A context for the credentials. A protocol version will be prepended
    113 *        to the context, see Credentials.keyWord for more information.
    114 * @param size
    115 *        The size in bytes of the expected derived buffer,
    116 *        defaults to 3 * 32.
    117 * @return credentials
    118 *        Returns an object:
    119 *        {
    120 *          id: the Hawk id (from the first 32 bytes derived)
    121 *          key: the Hawk key (from bytes 32 to 64)
    122 *          extra: size - 64 extra bytes (if size > 64)
    123 *        }
    124 */
    125 export async function deriveHawkCredentials(tokenHex, context, size = 96) {
    126  let token = CommonUtils.hexToBytes(tokenHex);
    127  let out = await lazy.CryptoUtils.hkdfLegacy(
    128    token,
    129    undefined,
    130    Credentials.keyWord(context),
    131    size
    132  );
    133 
    134  let result = {
    135    key: out.slice(32, 64),
    136    id: CommonUtils.bytesAsHex(out.slice(0, 32)),
    137  };
    138  if (size > 64) {
    139    result.extra = out.slice(64);
    140  }
    141 
    142  return result;
    143 }
    144 
    145 // With hawk request, we send the user's accepted-languages with each request.
    146 // To keep the number of times we read this pref at a minimum, maintain the
    147 // preference in a stateful object that notices and updates itself when the
    148 // pref is changed.
    149 class HawkIntl {
    150  // We won't actually query the pref until the first time we need it
    151  #accepted = "";
    152  #everRead = false;
    153 
    154  constructor() {
    155    Services.prefs.addObserver("intl.accept_languages", this);
    156  }
    157 
    158  uninit() {
    159    Services.prefs.removeObserver("intl.accept_languages", this);
    160  }
    161 
    162  observe() {
    163    this.readPref();
    164  }
    165 
    166  readPref() {
    167    this.#everRead = true;
    168    try {
    169      this.#accepted = Services.locale.acceptLanguages;
    170    } catch (err) {
    171      let log = Log.repository.getLogger("Services.Common.RESTRequest");
    172      log.error("Error reading Services.locale.acceptLanguages", err);
    173    }
    174  }
    175 
    176  get accept_languages() {
    177    if (!this.#everRead) {
    178      this.readPref();
    179    }
    180    return this.#accepted;
    181  }
    182 }
    183 
    184 // Singleton getter for Intl, creating an instance only when we first need it.
    185 var intl = null;
    186 function getIntl() {
    187  intl ??= new HawkIntl();
    188  return intl;
    189 }