hawkrequest.js 6.2 KB

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