Thu, 22 Jan 2015 13:21:57 +0100
Incorporate requested changes from Mozilla in review:
https://bugzilla.mozilla.org/show_bug.cgi?id=1123480#c6
michael@0 | 1 | /* This Source Code Form is subject to the terms of the Mozilla Public |
michael@0 | 2 | * License, v. 2.0. If a copy of the MPL was not distributed with this file, |
michael@0 | 3 | * You can obtain one at http://mozilla.org/MPL/2.0/. */ |
michael@0 | 4 | |
michael@0 | 5 | const {classes: Cc, interfaces: Ci, results: Cr, utils: Cu} = Components; |
michael@0 | 6 | |
michael@0 | 7 | this.EXPORTED_SYMBOLS = ["CryptoUtils"]; |
michael@0 | 8 | |
michael@0 | 9 | Cu.import("resource://services-common/observers.js"); |
michael@0 | 10 | Cu.import("resource://services-common/utils.js"); |
michael@0 | 11 | Cu.import("resource://gre/modules/XPCOMUtils.jsm"); |
michael@0 | 12 | |
michael@0 | 13 | this.CryptoUtils = { |
michael@0 | 14 | xor: function xor(a, b) { |
michael@0 | 15 | let bytes = []; |
michael@0 | 16 | |
michael@0 | 17 | if (a.length != b.length) { |
michael@0 | 18 | throw new Error("can't xor unequal length strings: "+a.length+" vs "+b.length); |
michael@0 | 19 | } |
michael@0 | 20 | |
michael@0 | 21 | for (let i = 0; i < a.length; i++) { |
michael@0 | 22 | bytes[i] = a.charCodeAt(i) ^ b.charCodeAt(i); |
michael@0 | 23 | } |
michael@0 | 24 | |
michael@0 | 25 | return String.fromCharCode.apply(String, bytes); |
michael@0 | 26 | }, |
michael@0 | 27 | |
michael@0 | 28 | /** |
michael@0 | 29 | * Generate a string of random bytes. |
michael@0 | 30 | */ |
michael@0 | 31 | generateRandomBytes: function generateRandomBytes(length) { |
michael@0 | 32 | let rng = Cc["@mozilla.org/security/random-generator;1"] |
michael@0 | 33 | .createInstance(Ci.nsIRandomGenerator); |
michael@0 | 34 | let bytes = rng.generateRandomBytes(length); |
michael@0 | 35 | return CommonUtils.byteArrayToString(bytes); |
michael@0 | 36 | }, |
michael@0 | 37 | |
michael@0 | 38 | /** |
michael@0 | 39 | * UTF8-encode a message and hash it with the given hasher. Returns a |
michael@0 | 40 | * string containing bytes. The hasher is reset if it's an HMAC hasher. |
michael@0 | 41 | */ |
michael@0 | 42 | digestUTF8: function digestUTF8(message, hasher) { |
michael@0 | 43 | let data = this._utf8Converter.convertToByteArray(message, {}); |
michael@0 | 44 | hasher.update(data, data.length); |
michael@0 | 45 | let result = hasher.finish(false); |
michael@0 | 46 | if (hasher instanceof Ci.nsICryptoHMAC) { |
michael@0 | 47 | hasher.reset(); |
michael@0 | 48 | } |
michael@0 | 49 | return result; |
michael@0 | 50 | }, |
michael@0 | 51 | |
michael@0 | 52 | /** |
michael@0 | 53 | * Treat the given message as a bytes string and hash it with the given |
michael@0 | 54 | * hasher. Returns a string containing bytes. The hasher is reset if it's |
michael@0 | 55 | * an HMAC hasher. |
michael@0 | 56 | */ |
michael@0 | 57 | digestBytes: function digestBytes(message, hasher) { |
michael@0 | 58 | // No UTF-8 encoding for you, sunshine. |
michael@0 | 59 | let bytes = [b.charCodeAt() for each (b in message)]; |
michael@0 | 60 | hasher.update(bytes, bytes.length); |
michael@0 | 61 | let result = hasher.finish(false); |
michael@0 | 62 | if (hasher instanceof Ci.nsICryptoHMAC) { |
michael@0 | 63 | hasher.reset(); |
michael@0 | 64 | } |
michael@0 | 65 | return result; |
michael@0 | 66 | }, |
michael@0 | 67 | |
michael@0 | 68 | /** |
michael@0 | 69 | * Encode the message into UTF-8 and feed the resulting bytes into the |
michael@0 | 70 | * given hasher. Does not return a hash. This can be called multiple times |
michael@0 | 71 | * with a single hasher, but eventually you must extract the result |
michael@0 | 72 | * yourself. |
michael@0 | 73 | */ |
michael@0 | 74 | updateUTF8: function(message, hasher) { |
michael@0 | 75 | let bytes = this._utf8Converter.convertToByteArray(message, {}); |
michael@0 | 76 | hasher.update(bytes, bytes.length); |
michael@0 | 77 | }, |
michael@0 | 78 | |
michael@0 | 79 | /** |
michael@0 | 80 | * UTF-8 encode a message and perform a SHA-1 over it. |
michael@0 | 81 | * |
michael@0 | 82 | * @param message |
michael@0 | 83 | * (string) Buffer to perform operation on. Should be a JS string. |
michael@0 | 84 | * It is possible to pass in a string representing an array |
michael@0 | 85 | * of bytes. But, you probably don't want to UTF-8 encode |
michael@0 | 86 | * such data and thus should not be using this function. |
michael@0 | 87 | * |
michael@0 | 88 | * @return string |
michael@0 | 89 | * Raw bytes constituting SHA-1 hash. Value is a JS string. Each |
michael@0 | 90 | * character is the byte value for that offset. Returned string |
michael@0 | 91 | * always has .length == 20. |
michael@0 | 92 | */ |
michael@0 | 93 | UTF8AndSHA1: function UTF8AndSHA1(message) { |
michael@0 | 94 | let hasher = Cc["@mozilla.org/security/hash;1"] |
michael@0 | 95 | .createInstance(Ci.nsICryptoHash); |
michael@0 | 96 | hasher.init(hasher.SHA1); |
michael@0 | 97 | |
michael@0 | 98 | return CryptoUtils.digestUTF8(message, hasher); |
michael@0 | 99 | }, |
michael@0 | 100 | |
michael@0 | 101 | sha1: function sha1(message) { |
michael@0 | 102 | return CommonUtils.bytesAsHex(CryptoUtils.UTF8AndSHA1(message)); |
michael@0 | 103 | }, |
michael@0 | 104 | |
michael@0 | 105 | sha1Base32: function sha1Base32(message) { |
michael@0 | 106 | return CommonUtils.encodeBase32(CryptoUtils.UTF8AndSHA1(message)); |
michael@0 | 107 | }, |
michael@0 | 108 | |
michael@0 | 109 | /** |
michael@0 | 110 | * Produce an HMAC key object from a key string. |
michael@0 | 111 | */ |
michael@0 | 112 | makeHMACKey: function makeHMACKey(str) { |
michael@0 | 113 | return Svc.KeyFactory.keyFromString(Ci.nsIKeyObject.HMAC, str); |
michael@0 | 114 | }, |
michael@0 | 115 | |
michael@0 | 116 | /** |
michael@0 | 117 | * Produce an HMAC hasher and initialize it with the given HMAC key. |
michael@0 | 118 | */ |
michael@0 | 119 | makeHMACHasher: function makeHMACHasher(type, key) { |
michael@0 | 120 | let hasher = Cc["@mozilla.org/security/hmac;1"] |
michael@0 | 121 | .createInstance(Ci.nsICryptoHMAC); |
michael@0 | 122 | hasher.init(type, key); |
michael@0 | 123 | return hasher; |
michael@0 | 124 | }, |
michael@0 | 125 | |
michael@0 | 126 | /** |
michael@0 | 127 | * HMAC-based Key Derivation (RFC 5869). |
michael@0 | 128 | */ |
michael@0 | 129 | hkdf: function hkdf(ikm, xts, info, len) { |
michael@0 | 130 | const BLOCKSIZE = 256 / 8; |
michael@0 | 131 | if (typeof xts === undefined) |
michael@0 | 132 | xts = String.fromCharCode(0, 0, 0, 0, 0, 0, 0, 0, |
michael@0 | 133 | 0, 0, 0, 0, 0, 0, 0, 0, |
michael@0 | 134 | 0, 0, 0, 0, 0, 0, 0, 0, |
michael@0 | 135 | 0, 0, 0, 0, 0, 0, 0, 0); |
michael@0 | 136 | let h = CryptoUtils.makeHMACHasher(Ci.nsICryptoHMAC.SHA256, |
michael@0 | 137 | CryptoUtils.makeHMACKey(xts)); |
michael@0 | 138 | let prk = CryptoUtils.digestBytes(ikm, h); |
michael@0 | 139 | return CryptoUtils.hkdfExpand(prk, info, len); |
michael@0 | 140 | }, |
michael@0 | 141 | |
michael@0 | 142 | /** |
michael@0 | 143 | * HMAC-based Key Derivation Step 2 according to RFC 5869. |
michael@0 | 144 | */ |
michael@0 | 145 | hkdfExpand: function hkdfExpand(prk, info, len) { |
michael@0 | 146 | const BLOCKSIZE = 256 / 8; |
michael@0 | 147 | let h = CryptoUtils.makeHMACHasher(Ci.nsICryptoHMAC.SHA256, |
michael@0 | 148 | CryptoUtils.makeHMACKey(prk)); |
michael@0 | 149 | let T = ""; |
michael@0 | 150 | let Tn = ""; |
michael@0 | 151 | let iterations = Math.ceil(len/BLOCKSIZE); |
michael@0 | 152 | for (let i = 0; i < iterations; i++) { |
michael@0 | 153 | Tn = CryptoUtils.digestBytes(Tn + info + String.fromCharCode(i + 1), h); |
michael@0 | 154 | T += Tn; |
michael@0 | 155 | } |
michael@0 | 156 | return T.slice(0, len); |
michael@0 | 157 | }, |
michael@0 | 158 | |
michael@0 | 159 | /** |
michael@0 | 160 | * PBKDF2 implementation in Javascript. |
michael@0 | 161 | * |
michael@0 | 162 | * The arguments to this function correspond to items in |
michael@0 | 163 | * PKCS #5, v2.0 pp. 9-10 |
michael@0 | 164 | * |
michael@0 | 165 | * P: the passphrase, an octet string: e.g., "secret phrase" |
michael@0 | 166 | * S: the salt, an octet string: e.g., "DNXPzPpiwn" |
michael@0 | 167 | * c: the number of iterations, a positive integer: e.g., 4096 |
michael@0 | 168 | * dkLen: the length in octets of the destination |
michael@0 | 169 | * key, a positive integer: e.g., 16 |
michael@0 | 170 | * hmacAlg: The algorithm to use for hmac |
michael@0 | 171 | * hmacLen: The hmac length |
michael@0 | 172 | * |
michael@0 | 173 | * The default value of 20 for hmacLen is appropriate for SHA1. For SHA256, |
michael@0 | 174 | * hmacLen should be 32. |
michael@0 | 175 | * |
michael@0 | 176 | * The output is an octet string of length dkLen, which you |
michael@0 | 177 | * can encode as you wish. |
michael@0 | 178 | */ |
michael@0 | 179 | pbkdf2Generate : function pbkdf2Generate(P, S, c, dkLen, |
michael@0 | 180 | hmacAlg=Ci.nsICryptoHMAC.SHA1, hmacLen=20) { |
michael@0 | 181 | |
michael@0 | 182 | // We don't have a default in the algo itself, as NSS does. |
michael@0 | 183 | // Use the constant. |
michael@0 | 184 | if (!dkLen) { |
michael@0 | 185 | dkLen = SYNC_KEY_DECODED_LENGTH; |
michael@0 | 186 | } |
michael@0 | 187 | |
michael@0 | 188 | function F(S, c, i, h) { |
michael@0 | 189 | |
michael@0 | 190 | function XOR(a, b, isA) { |
michael@0 | 191 | if (a.length != b.length) { |
michael@0 | 192 | return false; |
michael@0 | 193 | } |
michael@0 | 194 | |
michael@0 | 195 | let val = []; |
michael@0 | 196 | for (let i = 0; i < a.length; i++) { |
michael@0 | 197 | if (isA) { |
michael@0 | 198 | val[i] = a[i] ^ b[i]; |
michael@0 | 199 | } else { |
michael@0 | 200 | val[i] = a.charCodeAt(i) ^ b.charCodeAt(i); |
michael@0 | 201 | } |
michael@0 | 202 | } |
michael@0 | 203 | |
michael@0 | 204 | return val; |
michael@0 | 205 | } |
michael@0 | 206 | |
michael@0 | 207 | let ret; |
michael@0 | 208 | let U = []; |
michael@0 | 209 | |
michael@0 | 210 | /* Encode i into 4 octets: _INT */ |
michael@0 | 211 | let I = []; |
michael@0 | 212 | I[0] = String.fromCharCode((i >> 24) & 0xff); |
michael@0 | 213 | I[1] = String.fromCharCode((i >> 16) & 0xff); |
michael@0 | 214 | I[2] = String.fromCharCode((i >> 8) & 0xff); |
michael@0 | 215 | I[3] = String.fromCharCode(i & 0xff); |
michael@0 | 216 | |
michael@0 | 217 | U[0] = CryptoUtils.digestBytes(S + I.join(''), h); |
michael@0 | 218 | for (let j = 1; j < c; j++) { |
michael@0 | 219 | U[j] = CryptoUtils.digestBytes(U[j - 1], h); |
michael@0 | 220 | } |
michael@0 | 221 | |
michael@0 | 222 | ret = U[0]; |
michael@0 | 223 | for (let j = 1; j < c; j++) { |
michael@0 | 224 | ret = CommonUtils.byteArrayToString(XOR(ret, U[j])); |
michael@0 | 225 | } |
michael@0 | 226 | |
michael@0 | 227 | return ret; |
michael@0 | 228 | } |
michael@0 | 229 | |
michael@0 | 230 | let l = Math.ceil(dkLen / hmacLen); |
michael@0 | 231 | let r = dkLen - ((l - 1) * hmacLen); |
michael@0 | 232 | |
michael@0 | 233 | // Reuse the key and the hasher. Remaking them 4096 times is 'spensive. |
michael@0 | 234 | let h = CryptoUtils.makeHMACHasher(hmacAlg, |
michael@0 | 235 | CryptoUtils.makeHMACKey(P)); |
michael@0 | 236 | |
michael@0 | 237 | let T = []; |
michael@0 | 238 | for (let i = 0; i < l;) { |
michael@0 | 239 | T[i] = F(S, c, ++i, h); |
michael@0 | 240 | } |
michael@0 | 241 | |
michael@0 | 242 | let ret = ""; |
michael@0 | 243 | for (let i = 0; i < l-1;) { |
michael@0 | 244 | ret += T[i++]; |
michael@0 | 245 | } |
michael@0 | 246 | ret += T[l - 1].substr(0, r); |
michael@0 | 247 | |
michael@0 | 248 | return ret; |
michael@0 | 249 | }, |
michael@0 | 250 | |
michael@0 | 251 | deriveKeyFromPassphrase: function deriveKeyFromPassphrase(passphrase, |
michael@0 | 252 | salt, |
michael@0 | 253 | keyLength, |
michael@0 | 254 | forceJS) { |
michael@0 | 255 | if (Svc.Crypto.deriveKeyFromPassphrase && !forceJS) { |
michael@0 | 256 | return Svc.Crypto.deriveKeyFromPassphrase(passphrase, salt, keyLength); |
michael@0 | 257 | } |
michael@0 | 258 | else { |
michael@0 | 259 | // Fall back to JS implementation. |
michael@0 | 260 | // 4096 is hardcoded in WeaveCrypto, so do so here. |
michael@0 | 261 | return CryptoUtils.pbkdf2Generate(passphrase, atob(salt), 4096, |
michael@0 | 262 | keyLength); |
michael@0 | 263 | } |
michael@0 | 264 | }, |
michael@0 | 265 | |
michael@0 | 266 | /** |
michael@0 | 267 | * Compute the HTTP MAC SHA-1 for an HTTP request. |
michael@0 | 268 | * |
michael@0 | 269 | * @param identifier |
michael@0 | 270 | * (string) MAC Key Identifier. |
michael@0 | 271 | * @param key |
michael@0 | 272 | * (string) MAC Key. |
michael@0 | 273 | * @param method |
michael@0 | 274 | * (string) HTTP request method. |
michael@0 | 275 | * @param URI |
michael@0 | 276 | * (nsIURI) HTTP request URI. |
michael@0 | 277 | * @param extra |
michael@0 | 278 | * (object) Optional extra parameters. Valid keys are: |
michael@0 | 279 | * nonce_bytes - How many bytes the nonce should be. This defaults |
michael@0 | 280 | * to 8. Note that this many bytes are Base64 encoded, so the |
michael@0 | 281 | * string length of the nonce will be longer than this value. |
michael@0 | 282 | * ts - Timestamp to use. Should only be defined for testing. |
michael@0 | 283 | * nonce - String nonce. Should only be defined for testing as this |
michael@0 | 284 | * function will generate a cryptographically secure random one |
michael@0 | 285 | * if not defined. |
michael@0 | 286 | * ext - Extra string to be included in MAC. Per the HTTP MAC spec, |
michael@0 | 287 | * the format is undefined and thus application specific. |
michael@0 | 288 | * @returns |
michael@0 | 289 | * (object) Contains results of operation and input arguments (for |
michael@0 | 290 | * symmetry). The object has the following keys: |
michael@0 | 291 | * |
michael@0 | 292 | * identifier - (string) MAC Key Identifier (from arguments). |
michael@0 | 293 | * key - (string) MAC Key (from arguments). |
michael@0 | 294 | * method - (string) HTTP request method (from arguments). |
michael@0 | 295 | * hostname - (string) HTTP hostname used (derived from arguments). |
michael@0 | 296 | * port - (string) HTTP port number used (derived from arguments). |
michael@0 | 297 | * mac - (string) Raw HMAC digest bytes. |
michael@0 | 298 | * getHeader - (function) Call to obtain the string Authorization |
michael@0 | 299 | * header value for this invocation. |
michael@0 | 300 | * nonce - (string) Nonce value used. |
michael@0 | 301 | * ts - (number) Integer seconds since Unix epoch that was used. |
michael@0 | 302 | */ |
michael@0 | 303 | computeHTTPMACSHA1: function computeHTTPMACSHA1(identifier, key, method, |
michael@0 | 304 | uri, extra) { |
michael@0 | 305 | let ts = (extra && extra.ts) ? extra.ts : Math.floor(Date.now() / 1000); |
michael@0 | 306 | let nonce_bytes = (extra && extra.nonce_bytes > 0) ? extra.nonce_bytes : 8; |
michael@0 | 307 | |
michael@0 | 308 | // We are allowed to use more than the Base64 alphabet if we want. |
michael@0 | 309 | let nonce = (extra && extra.nonce) |
michael@0 | 310 | ? extra.nonce |
michael@0 | 311 | : btoa(CryptoUtils.generateRandomBytes(nonce_bytes)); |
michael@0 | 312 | |
michael@0 | 313 | let host = uri.asciiHost; |
michael@0 | 314 | let port; |
michael@0 | 315 | let usedMethod = method.toUpperCase(); |
michael@0 | 316 | |
michael@0 | 317 | if (uri.port != -1) { |
michael@0 | 318 | port = uri.port; |
michael@0 | 319 | } else if (uri.scheme == "http") { |
michael@0 | 320 | port = "80"; |
michael@0 | 321 | } else if (uri.scheme == "https") { |
michael@0 | 322 | port = "443"; |
michael@0 | 323 | } else { |
michael@0 | 324 | throw new Error("Unsupported URI scheme: " + uri.scheme); |
michael@0 | 325 | } |
michael@0 | 326 | |
michael@0 | 327 | let ext = (extra && extra.ext) ? extra.ext : ""; |
michael@0 | 328 | |
michael@0 | 329 | let requestString = ts.toString(10) + "\n" + |
michael@0 | 330 | nonce + "\n" + |
michael@0 | 331 | usedMethod + "\n" + |
michael@0 | 332 | uri.path + "\n" + |
michael@0 | 333 | host + "\n" + |
michael@0 | 334 | port + "\n" + |
michael@0 | 335 | ext + "\n"; |
michael@0 | 336 | |
michael@0 | 337 | let hasher = CryptoUtils.makeHMACHasher(Ci.nsICryptoHMAC.SHA1, |
michael@0 | 338 | CryptoUtils.makeHMACKey(key)); |
michael@0 | 339 | let mac = CryptoUtils.digestBytes(requestString, hasher); |
michael@0 | 340 | |
michael@0 | 341 | function getHeader() { |
michael@0 | 342 | return CryptoUtils.getHTTPMACSHA1Header(this.identifier, this.ts, |
michael@0 | 343 | this.nonce, this.mac, this.ext); |
michael@0 | 344 | } |
michael@0 | 345 | |
michael@0 | 346 | return { |
michael@0 | 347 | identifier: identifier, |
michael@0 | 348 | key: key, |
michael@0 | 349 | method: usedMethod, |
michael@0 | 350 | hostname: host, |
michael@0 | 351 | port: port, |
michael@0 | 352 | mac: mac, |
michael@0 | 353 | nonce: nonce, |
michael@0 | 354 | ts: ts, |
michael@0 | 355 | ext: ext, |
michael@0 | 356 | getHeader: getHeader |
michael@0 | 357 | }; |
michael@0 | 358 | }, |
michael@0 | 359 | |
michael@0 | 360 | |
michael@0 | 361 | /** |
michael@0 | 362 | * Obtain the HTTP MAC Authorization header value from fields. |
michael@0 | 363 | * |
michael@0 | 364 | * @param identifier |
michael@0 | 365 | * (string) MAC key identifier. |
michael@0 | 366 | * @param ts |
michael@0 | 367 | * (number) Integer seconds since Unix epoch. |
michael@0 | 368 | * @param nonce |
michael@0 | 369 | * (string) Nonce value. |
michael@0 | 370 | * @param mac |
michael@0 | 371 | * (string) Computed HMAC digest (raw bytes). |
michael@0 | 372 | * @param ext |
michael@0 | 373 | * (optional) (string) Extra string content. |
michael@0 | 374 | * @returns |
michael@0 | 375 | * (string) Value to put in Authorization header. |
michael@0 | 376 | */ |
michael@0 | 377 | getHTTPMACSHA1Header: function getHTTPMACSHA1Header(identifier, ts, nonce, |
michael@0 | 378 | mac, ext) { |
michael@0 | 379 | let header ='MAC id="' + identifier + '", ' + |
michael@0 | 380 | 'ts="' + ts + '", ' + |
michael@0 | 381 | 'nonce="' + nonce + '", ' + |
michael@0 | 382 | 'mac="' + btoa(mac) + '"'; |
michael@0 | 383 | |
michael@0 | 384 | if (!ext) { |
michael@0 | 385 | return header; |
michael@0 | 386 | } |
michael@0 | 387 | |
michael@0 | 388 | return header += ', ext="' + ext +'"'; |
michael@0 | 389 | }, |
michael@0 | 390 | |
michael@0 | 391 | /** |
michael@0 | 392 | * Given an HTTP header value, strip out any attributes. |
michael@0 | 393 | */ |
michael@0 | 394 | |
michael@0 | 395 | stripHeaderAttributes: function(value) { |
michael@0 | 396 | let value = value || ""; |
michael@0 | 397 | let i = value.indexOf(";"); |
michael@0 | 398 | return value.substring(0, (i >= 0) ? i : undefined).trim().toLowerCase(); |
michael@0 | 399 | }, |
michael@0 | 400 | |
michael@0 | 401 | /** |
michael@0 | 402 | * Compute the HAWK client values (mostly the header) for an HTTP request. |
michael@0 | 403 | * |
michael@0 | 404 | * @param URI |
michael@0 | 405 | * (nsIURI) HTTP request URI. |
michael@0 | 406 | * @param method |
michael@0 | 407 | * (string) HTTP request method. |
michael@0 | 408 | * @param options |
michael@0 | 409 | * (object) extra parameters (all but "credentials" are optional): |
michael@0 | 410 | * credentials - (object, mandatory) HAWK credentials object. |
michael@0 | 411 | * All three keys are required: |
michael@0 | 412 | * id - (string) key identifier |
michael@0 | 413 | * key - (string) raw key bytes |
michael@0 | 414 | * algorithm - (string) which hash to use: "sha1" or "sha256" |
michael@0 | 415 | * ext - (string) application-specific data, included in MAC |
michael@0 | 416 | * localtimeOffsetMsec - (number) local clock offset (vs server) |
michael@0 | 417 | * payload - (string) payload to include in hash, containing the |
michael@0 | 418 | * HTTP request body. If not provided, the HAWK hash |
michael@0 | 419 | * will not cover the request body, and the server |
michael@0 | 420 | * should not check it either. This will be UTF-8 |
michael@0 | 421 | * encoded into bytes before hashing. This function |
michael@0 | 422 | * cannot handle arbitrary binary data, sorry (the |
michael@0 | 423 | * UTF-8 encoding process will corrupt any codepoints |
michael@0 | 424 | * between U+0080 and U+00FF). Callers must be careful |
michael@0 | 425 | * to use an HTTP client function which encodes the |
michael@0 | 426 | * payload exactly the same way, otherwise the hash |
michael@0 | 427 | * will not match. |
michael@0 | 428 | * contentType - (string) payload Content-Type. This is included |
michael@0 | 429 | * (without any attributes like "charset=") in the |
michael@0 | 430 | * HAWK hash. It does *not* affect interpretation |
michael@0 | 431 | * of the "payload" property. |
michael@0 | 432 | * hash - (base64 string) pre-calculated payload hash. If |
michael@0 | 433 | * provided, "payload" is ignored. |
michael@0 | 434 | * ts - (number) pre-calculated timestamp, secs since epoch |
michael@0 | 435 | * now - (number) current time, ms-since-epoch, for tests |
michael@0 | 436 | * nonce - (string) pre-calculated nonce. Should only be defined |
michael@0 | 437 | * for testing as this function will generate a |
michael@0 | 438 | * cryptographically secure random one if not defined. |
michael@0 | 439 | * @returns |
michael@0 | 440 | * (object) Contains results of operation. The object has the |
michael@0 | 441 | * following keys: |
michael@0 | 442 | * field - (string) HAWK header, to use in Authorization: header |
michael@0 | 443 | * artifacts - (object) other generated values: |
michael@0 | 444 | * ts - (number) timestamp, in seconds since epoch |
michael@0 | 445 | * nonce - (string) |
michael@0 | 446 | * method - (string) |
michael@0 | 447 | * resource - (string) path plus querystring |
michael@0 | 448 | * host - (string) |
michael@0 | 449 | * port - (number) |
michael@0 | 450 | * hash - (string) payload hash (base64) |
michael@0 | 451 | * ext - (string) app-specific data |
michael@0 | 452 | * MAC - (string) request MAC (base64) |
michael@0 | 453 | */ |
michael@0 | 454 | computeHAWK: function(uri, method, options) { |
michael@0 | 455 | let credentials = options.credentials; |
michael@0 | 456 | let ts = options.ts || Math.floor(((options.now || Date.now()) + |
michael@0 | 457 | (options.localtimeOffsetMsec || 0)) |
michael@0 | 458 | / 1000); |
michael@0 | 459 | |
michael@0 | 460 | let hash_algo, hmac_algo; |
michael@0 | 461 | if (credentials.algorithm == "sha1") { |
michael@0 | 462 | hash_algo = Ci.nsICryptoHash.SHA1; |
michael@0 | 463 | hmac_algo = Ci.nsICryptoHMAC.SHA1; |
michael@0 | 464 | } else if (credentials.algorithm == "sha256") { |
michael@0 | 465 | hash_algo = Ci.nsICryptoHash.SHA256; |
michael@0 | 466 | hmac_algo = Ci.nsICryptoHMAC.SHA256; |
michael@0 | 467 | } else { |
michael@0 | 468 | throw new Error("Unsupported algorithm: " + credentials.algorithm); |
michael@0 | 469 | } |
michael@0 | 470 | |
michael@0 | 471 | let port; |
michael@0 | 472 | if (uri.port != -1) { |
michael@0 | 473 | port = uri.port; |
michael@0 | 474 | } else if (uri.scheme == "http") { |
michael@0 | 475 | port = 80; |
michael@0 | 476 | } else if (uri.scheme == "https") { |
michael@0 | 477 | port = 443; |
michael@0 | 478 | } else { |
michael@0 | 479 | throw new Error("Unsupported URI scheme: " + uri.scheme); |
michael@0 | 480 | } |
michael@0 | 481 | |
michael@0 | 482 | let artifacts = { |
michael@0 | 483 | ts: ts, |
michael@0 | 484 | nonce: options.nonce || btoa(CryptoUtils.generateRandomBytes(8)), |
michael@0 | 485 | method: method.toUpperCase(), |
michael@0 | 486 | resource: uri.path, // This includes both path and search/queryarg. |
michael@0 | 487 | host: uri.asciiHost.toLowerCase(), // This includes punycoding. |
michael@0 | 488 | port: port.toString(10), |
michael@0 | 489 | hash: options.hash, |
michael@0 | 490 | ext: options.ext, |
michael@0 | 491 | }; |
michael@0 | 492 | |
michael@0 | 493 | let contentType = CryptoUtils.stripHeaderAttributes(options.contentType); |
michael@0 | 494 | |
michael@0 | 495 | if (!artifacts.hash && options.hasOwnProperty("payload") |
michael@0 | 496 | && options.payload) { |
michael@0 | 497 | let hasher = Cc["@mozilla.org/security/hash;1"] |
michael@0 | 498 | .createInstance(Ci.nsICryptoHash); |
michael@0 | 499 | hasher.init(hash_algo); |
michael@0 | 500 | CryptoUtils.updateUTF8("hawk.1.payload\n", hasher); |
michael@0 | 501 | CryptoUtils.updateUTF8(contentType+"\n", hasher); |
michael@0 | 502 | CryptoUtils.updateUTF8(options.payload, hasher); |
michael@0 | 503 | CryptoUtils.updateUTF8("\n", hasher); |
michael@0 | 504 | let hash = hasher.finish(false); |
michael@0 | 505 | // HAWK specifies this .hash to use +/ (not _-) and include the |
michael@0 | 506 | // trailing "==" padding. |
michael@0 | 507 | let hash_b64 = btoa(hash); |
michael@0 | 508 | artifacts.hash = hash_b64; |
michael@0 | 509 | } |
michael@0 | 510 | |
michael@0 | 511 | let requestString = ("hawk.1.header" + "\n" + |
michael@0 | 512 | artifacts.ts.toString(10) + "\n" + |
michael@0 | 513 | artifacts.nonce + "\n" + |
michael@0 | 514 | artifacts.method + "\n" + |
michael@0 | 515 | artifacts.resource + "\n" + |
michael@0 | 516 | artifacts.host + "\n" + |
michael@0 | 517 | artifacts.port + "\n" + |
michael@0 | 518 | (artifacts.hash || "") + "\n"); |
michael@0 | 519 | if (artifacts.ext) { |
michael@0 | 520 | requestString += artifacts.ext.replace("\\", "\\\\").replace("\n", "\\n"); |
michael@0 | 521 | } |
michael@0 | 522 | requestString += "\n"; |
michael@0 | 523 | |
michael@0 | 524 | let hasher = CryptoUtils.makeHMACHasher(hmac_algo, |
michael@0 | 525 | CryptoUtils.makeHMACKey(credentials.key)); |
michael@0 | 526 | artifacts.mac = btoa(CryptoUtils.digestBytes(requestString, hasher)); |
michael@0 | 527 | // The output MAC uses "+" and "/", and padded== . |
michael@0 | 528 | |
michael@0 | 529 | function escape(attribute) { |
michael@0 | 530 | // This is used for "x=y" attributes inside HTTP headers. |
michael@0 | 531 | return attribute.replace(/\\/g, "\\\\").replace(/\"/g, '\\"'); |
michael@0 | 532 | } |
michael@0 | 533 | let header = ('Hawk id="' + credentials.id + '", ' + |
michael@0 | 534 | 'ts="' + artifacts.ts + '", ' + |
michael@0 | 535 | 'nonce="' + artifacts.nonce + '", ' + |
michael@0 | 536 | (artifacts.hash ? ('hash="' + artifacts.hash + '", ') : "") + |
michael@0 | 537 | (artifacts.ext ? ('ext="' + escape(artifacts.ext) + '", ') : "") + |
michael@0 | 538 | 'mac="' + artifacts.mac + '"'); |
michael@0 | 539 | return { |
michael@0 | 540 | artifacts: artifacts, |
michael@0 | 541 | field: header, |
michael@0 | 542 | }; |
michael@0 | 543 | }, |
michael@0 | 544 | |
michael@0 | 545 | }; |
michael@0 | 546 | |
michael@0 | 547 | XPCOMUtils.defineLazyGetter(CryptoUtils, "_utf8Converter", function() { |
michael@0 | 548 | let converter = Cc["@mozilla.org/intl/scriptableunicodeconverter"] |
michael@0 | 549 | .createInstance(Ci.nsIScriptableUnicodeConverter); |
michael@0 | 550 | converter.charset = "UTF-8"; |
michael@0 | 551 | |
michael@0 | 552 | return converter; |
michael@0 | 553 | }); |
michael@0 | 554 | |
michael@0 | 555 | let Svc = {}; |
michael@0 | 556 | |
michael@0 | 557 | XPCOMUtils.defineLazyServiceGetter(Svc, |
michael@0 | 558 | "KeyFactory", |
michael@0 | 559 | "@mozilla.org/security/keyobjectfactory;1", |
michael@0 | 560 | "nsIKeyObjectFactory"); |
michael@0 | 561 | |
michael@0 | 562 | Svc.__defineGetter__("Crypto", function() { |
michael@0 | 563 | let ns = {}; |
michael@0 | 564 | Cu.import("resource://services-crypto/WeaveCrypto.js", ns); |
michael@0 | 565 | |
michael@0 | 566 | let wc = new ns.WeaveCrypto(); |
michael@0 | 567 | delete Svc.Crypto; |
michael@0 | 568 | return Svc.Crypto = wc; |
michael@0 | 569 | }); |
michael@0 | 570 | |
michael@0 | 571 | Observers.add("xpcom-shutdown", function unloadServices() { |
michael@0 | 572 | Observers.remove("xpcom-shutdown", unloadServices); |
michael@0 | 573 | |
michael@0 | 574 | for (let k in Svc) { |
michael@0 | 575 | delete Svc[k]; |
michael@0 | 576 | } |
michael@0 | 577 | }); |