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 }