summaryrefslogtreecommitdiff
path: root/services/common/hawkrequest.js
blob: 454960b7b95cb5b2581d69d2a75446cbda276106 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
/* 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/. */

"use strict";

var {classes: Cc, interfaces: Ci, utils: Cu, results: Cr} = Components;

this.EXPORTED_SYMBOLS = [
  "HAWKAuthenticatedRESTRequest",
  "deriveHawkCredentials"
];

Cu.import("resource://gre/modules/Preferences.jsm");
Cu.import("resource://gre/modules/Services.jsm");
Cu.import("resource://gre/modules/XPCOMUtils.jsm");
Cu.import("resource://gre/modules/Log.jsm");
Cu.import("resource://services-common/rest.js");
Cu.import("resource://services-common/utils.js");
Cu.import("resource://gre/modules/Credentials.jsm");

XPCOMUtils.defineLazyModuleGetter(this, "CryptoUtils",
                                  "resource://services-crypto/utils.js");

const Prefs = new Preferences("services.common.rest.");

/**
 * Single-use HAWK-authenticated HTTP requests to RESTish resources.
 *
 * @param uri
 *        (String) URI for the RESTRequest constructor
 *
 * @param credentials
 *        (Object) Optional credentials for computing HAWK authentication
 *        header.
 *
 * @param payloadObj
 *        (Object) Optional object to be converted to JSON payload
 *
 * @param extra
 *        (Object) Optional extra params for HAWK header computation.
 *        Valid properties are:
 *
 *          now:                 <current time in milliseconds>,
 *          localtimeOffsetMsec: <local clock offset vs server>,
 *          headers:             <An object with header/value pairs to be sent
 *                                as headers on the request>
 *
 * extra.localtimeOffsetMsec is the value in milliseconds that must be added to
 * the local clock to make it agree with the server's clock.  For instance, if
 * the local clock is two minutes ahead of the server, the time offset in
 * milliseconds will be -120000.
 */

this.HAWKAuthenticatedRESTRequest =
 function HawkAuthenticatedRESTRequest(uri, credentials, extra={}) {
  RESTRequest.call(this, uri);

  this.credentials = credentials;
  this.now = extra.now || Date.now();
  this.localtimeOffsetMsec = extra.localtimeOffsetMsec || 0;
  this._log.trace("local time, offset: " + this.now + ", " + (this.localtimeOffsetMsec));
  this.extraHeaders = extra.headers || {};

  // Expose for testing
  this._intl = getIntl();
};
HAWKAuthenticatedRESTRequest.prototype = {
  __proto__: RESTRequest.prototype,

  dispatch: function dispatch(method, data, onComplete, onProgress) {
    let contentType = "text/plain";
    if (method == "POST" || method == "PUT" || method == "PATCH") {
      contentType = "application/json";
    }
    if (this.credentials) {
      let options = {
        now: this.now,
        localtimeOffsetMsec: this.localtimeOffsetMsec,
        credentials: this.credentials,
        payload: data && JSON.stringify(data) || "",
        contentType: contentType,
      };
      let header = CryptoUtils.computeHAWK(this.uri, method, options);
      this.setHeader("Authorization", header.field);
      this._log.trace("hawk auth header: " + header.field);
    }

    for (let header in this.extraHeaders) {
      this.setHeader(header, this.extraHeaders[header]);
    }

    this.setHeader("Content-Type", contentType);

    this.setHeader("Accept-Language", this._intl.accept_languages);

    return RESTRequest.prototype.dispatch.call(
      this, method, data, onComplete, onProgress
    );
  }
};


/**
  * Generic function to derive Hawk credentials.
  *
  * Hawk credentials are derived using shared secrets, which depend on the token
  * in use.
  *
  * @param tokenHex
  *        The current session token encoded in hex
  * @param context
  *        A context for the credentials. A protocol version will be prepended
  *        to the context, see Credentials.keyWord for more information.
  * @param size
  *        The size in bytes of the expected derived buffer,
  *        defaults to 3 * 32.
  * @return credentials
  *        Returns an object:
  *        {
  *          algorithm: sha256
  *          id: the Hawk id (from the first 32 bytes derived)
  *          key: the Hawk key (from bytes 32 to 64)
  *          extra: size - 64 extra bytes (if size > 64)
  *        }
  */
this.deriveHawkCredentials = function deriveHawkCredentials(tokenHex,
                                                            context,
                                                            size = 96,
                                                            hexKey = false) {
  let token = CommonUtils.hexToBytes(tokenHex);
  let out = CryptoUtils.hkdf(token, undefined, Credentials.keyWord(context), size);

  let result = {
    algorithm: "sha256",
    key: hexKey ? CommonUtils.bytesAsHex(out.slice(32, 64)) : out.slice(32, 64),
    id: CommonUtils.bytesAsHex(out.slice(0, 32))
  };
  if (size > 64) {
    result.extra = out.slice(64);
  }

  return result;
}

// With hawk request, we send the user's accepted-languages with each request.
// To keep the number of times we read this pref at a minimum, maintain the
// preference in a stateful object that notices and updates itself when the
// pref is changed.
this.Intl = function Intl() {
  // We won't actually query the pref until the first time we need it
  this._accepted = "";
  this._everRead = false;
  this._log = Log.repository.getLogger("Services.common.RESTRequest");
  this._log.level = Log.Level[Prefs.get("log.logger.rest.request")];
  this.init();
};

this.Intl.prototype = {
  init: function() {
    Services.prefs.addObserver("intl.accept_languages", this, false);
  },

  uninit: function() {
    Services.prefs.removeObserver("intl.accept_languages", this);
  },

  observe: function(subject, topic, data) {
    this.readPref();
  },

  readPref: function() {
    this._everRead = true;
    try {
      this._accepted = Services.prefs.getComplexValue(
        "intl.accept_languages", Ci.nsIPrefLocalizedString).data;
    } catch (err) {
      this._log.error("Error reading intl.accept_languages pref", err);
    }
  },

  get accept_languages() {
    if (!this._everRead) {
      this.readPref();
    }
    return this._accepted;
  },
};

// Singleton getter for Intl, creating an instance only when we first need it.
var intl = null;
function getIntl() {
  if (!intl) {
    intl = new Intl();
  }
  return intl;
}