utils.sys.mjs (16411B)
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 { Observers } from "resource://services-common/observers.sys.mjs"; 6 7 import { CommonUtils } from "resource://services-common/utils.sys.mjs"; 8 9 const lazy = {}; 10 11 ChromeUtils.defineLazyGetter(lazy, "textEncoder", function () { 12 return new TextEncoder(); 13 }); 14 15 /** 16 * A number of `Legacy` suffixed functions are exposed by CryptoUtils. 17 * They work with octet strings, which were used before Javascript 18 * got ArrayBuffer and friends. 19 */ 20 export var CryptoUtils = { 21 xor(a, b) { 22 let bytes = []; 23 24 if (a.length != b.length) { 25 throw new Error( 26 "can't xor unequal length strings: " + a.length + " vs " + b.length 27 ); 28 } 29 30 for (let i = 0; i < a.length; i++) { 31 bytes[i] = a.charCodeAt(i) ^ b.charCodeAt(i); 32 } 33 34 return String.fromCharCode.apply(String, bytes); 35 }, 36 37 /** 38 * Generate a string of random bytes. 39 * 40 * @returns {string} Octet string 41 */ 42 generateRandomBytesLegacy(length) { 43 let bytes = CryptoUtils.generateRandomBytes(length); 44 return CommonUtils.arrayBufferToByteString(bytes); 45 }, 46 47 generateRandomBytes(length) { 48 return crypto.getRandomValues(new Uint8Array(length)); 49 }, 50 51 /** 52 * UTF8-encode a message and hash it with the given hasher. Returns a 53 * string containing bytes. 54 */ 55 digestUTF8(message, hasher) { 56 let data = lazy.textEncoder.encode(message); 57 hasher.update(data, data.length); 58 let result = hasher.finish(false); 59 return result; 60 }, 61 62 /** 63 * Treat the given message as a bytes string (if necessary) and hash it with 64 * the given hasher. Returns a string containing bytes. 65 */ 66 digestBytes(bytes, hasher) { 67 if (typeof bytes == "string" || bytes instanceof String) { 68 bytes = CommonUtils.byteStringToArrayBuffer(bytes); 69 } 70 return CryptoUtils.digestBytesArray(bytes, hasher); 71 }, 72 73 digestBytesArray(bytes, hasher) { 74 hasher.update(bytes, bytes.length); 75 let result = hasher.finish(false); 76 return result; 77 }, 78 79 /** 80 * Encode the message into UTF-8 and feed the resulting bytes into the 81 * given hasher. Does not return a hash. This can be called multiple times 82 * with a single hasher, but eventually you must extract the result 83 * yourself. 84 */ 85 updateUTF8(message, hasher) { 86 let bytes = lazy.textEncoder.encode(message); 87 hasher.update(bytes, bytes.length); 88 }, 89 90 sha256(message) { 91 let hasher = Cc["@mozilla.org/security/hash;1"].createInstance( 92 Ci.nsICryptoHash 93 ); 94 hasher.init(hasher.SHA256); 95 return CommonUtils.bytesAsHex(CryptoUtils.digestUTF8(message, hasher)); 96 }, 97 98 sha256Base64(message) { 99 let data = lazy.textEncoder.encode(message); 100 let hasher = Cc["@mozilla.org/security/hash;1"].createInstance( 101 Ci.nsICryptoHash 102 ); 103 hasher.init(hasher.SHA256); 104 hasher.update(data, data.length); 105 return hasher.finish(true); 106 }, 107 108 /** 109 * @param {string} alg Hash algorithm (common values are SHA-1 or SHA-256) 110 * @param {string} key Key as an octet string. 111 * @param {string} data Data as an octet string. 112 */ 113 async hmacLegacy(alg, key, data) { 114 if (!key || !key.length) { 115 key = "\0"; 116 } 117 data = CommonUtils.byteStringToArrayBuffer(data); 118 key = CommonUtils.byteStringToArrayBuffer(key); 119 const result = await CryptoUtils.hmac(alg, key, data); 120 return CommonUtils.arrayBufferToByteString(result); 121 }, 122 123 /** 124 * @param {string} ikm IKM as an octet string. 125 * @param {string} salt Salt as an Hex string. 126 * @param {string} info Info as a regular string. 127 * @param {number} len Desired output length in bytes. 128 */ 129 async hkdfLegacy(ikm, xts, info, len) { 130 ikm = CommonUtils.byteStringToArrayBuffer(ikm); 131 xts = CommonUtils.byteStringToArrayBuffer(xts); 132 info = lazy.textEncoder.encode(info); 133 const okm = await CryptoUtils.hkdf(ikm, xts, info, len); 134 return CommonUtils.arrayBufferToByteString(okm); 135 }, 136 137 /** 138 * @param {string} alg Hash algorithm (common values are SHA-1 or SHA-256) 139 * @param {BufferSource} key 140 * @param {BufferSource} data 141 * @returns {Promise<Uint8Array>} 142 */ 143 async hmac(alg, key, data) { 144 const hmacKey = await crypto.subtle.importKey( 145 "raw", 146 key, 147 { name: "HMAC", hash: alg }, 148 false, 149 ["sign"] 150 ); 151 const result = await crypto.subtle.sign("HMAC", hmacKey, data); 152 return new Uint8Array(result); 153 }, 154 155 /** 156 * @param {ArrayBuffer} ikm 157 * @param {ArrayBuffer} salt 158 * @param {ArrayBuffer} info 159 * @param {number} len Desired output length in bytes. 160 * @returns {Uint8Array} 161 */ 162 async hkdf(ikm, salt, info, len) { 163 const key = await crypto.subtle.importKey( 164 "raw", 165 ikm, 166 { name: "HKDF" }, 167 false, 168 ["deriveBits"] 169 ); 170 const okm = await crypto.subtle.deriveBits( 171 { 172 name: "HKDF", 173 hash: "SHA-256", 174 salt, 175 info, 176 }, 177 key, 178 len * 8 179 ); 180 return new Uint8Array(okm); 181 }, 182 183 /** 184 * PBKDF2 password stretching with SHA-256 hmac. 185 * 186 * @param {string} passphrase Passphrase as an octet string. 187 * @param {string} salt Salt as an octet string. 188 * @param {string} iterations Number of iterations, a positive integer. 189 * @param {string} len Desired output length in bytes. 190 */ 191 async pbkdf2Generate(passphrase, salt, iterations, len) { 192 passphrase = CommonUtils.byteStringToArrayBuffer(passphrase); 193 salt = CommonUtils.byteStringToArrayBuffer(salt); 194 const key = await crypto.subtle.importKey( 195 "raw", 196 passphrase, 197 { name: "PBKDF2" }, 198 false, 199 ["deriveBits"] 200 ); 201 const output = await crypto.subtle.deriveBits( 202 { 203 name: "PBKDF2", 204 hash: "SHA-256", 205 salt, 206 iterations, 207 }, 208 key, 209 len * 8 210 ); 211 return CommonUtils.arrayBufferToByteString(new Uint8Array(output)); 212 }, 213 214 /** 215 * Compute the HTTP MAC SHA-1 for an HTTP request. 216 * 217 * @param identifier 218 * (string) MAC Key Identifier. 219 * @param key 220 * (string) MAC Key. 221 * @param method 222 * (string) HTTP request method. 223 * @param URI 224 * (nsIURI) HTTP request URI. 225 * @param extra 226 * (object) Optional extra parameters. Valid keys are: 227 * nonce_bytes - How many bytes the nonce should be. This defaults 228 * to 8. Note that this many bytes are Base64 encoded, so the 229 * string length of the nonce will be longer than this value. 230 * ts - Timestamp to use. Should only be defined for testing. 231 * nonce - String nonce. Should only be defined for testing as this 232 * function will generate a cryptographically secure random one 233 * if not defined. 234 * ext - Extra string to be included in MAC. Per the HTTP MAC spec, 235 * the format is undefined and thus application specific. 236 * @returns 237 * (object) Contains results of operation and input arguments (for 238 * symmetry). The object has the following keys: 239 * 240 * identifier - (string) MAC Key Identifier (from arguments). 241 * key - (string) MAC Key (from arguments). 242 * method - (string) HTTP request method (from arguments). 243 * hostname - (string) HTTP hostname used (derived from arguments). 244 * port - (string) HTTP port number used (derived from arguments). 245 * mac - (string) Raw HMAC digest bytes. 246 * getHeader - (function) Call to obtain the string Authorization 247 * header value for this invocation. 248 * nonce - (string) Nonce value used. 249 * ts - (number) Integer seconds since Unix epoch that was used. 250 */ 251 async computeHTTPMACSHA1(identifier, key, method, uri, extra) { 252 let ts = extra && extra.ts ? extra.ts : Math.floor(Date.now() / 1000); 253 let nonce_bytes = extra && extra.nonce_bytes > 0 ? extra.nonce_bytes : 8; 254 255 // We are allowed to use more than the Base64 alphabet if we want. 256 let nonce = 257 extra && extra.nonce 258 ? extra.nonce 259 : btoa(CryptoUtils.generateRandomBytesLegacy(nonce_bytes)); 260 261 let host = uri.asciiHost; 262 let port; 263 let usedMethod = method.toUpperCase(); 264 265 if (uri.port != -1) { 266 port = uri.port; 267 } else if (uri.scheme == "http") { 268 port = "80"; 269 } else if (uri.scheme == "https") { 270 port = "443"; 271 } else { 272 throw new Error("Unsupported URI scheme: " + uri.scheme); 273 } 274 275 let ext = extra && extra.ext ? extra.ext : ""; 276 277 let requestString = 278 ts.toString(10) + 279 "\n" + 280 nonce + 281 "\n" + 282 usedMethod + 283 "\n" + 284 uri.pathQueryRef + 285 "\n" + 286 host + 287 "\n" + 288 port + 289 "\n" + 290 ext + 291 "\n"; 292 293 const mac = await CryptoUtils.hmacLegacy("SHA-1", key, requestString); 294 295 function getHeader() { 296 return CryptoUtils.getHTTPMACSHA1Header( 297 this.identifier, 298 this.ts, 299 this.nonce, 300 this.mac, 301 this.ext 302 ); 303 } 304 305 return { 306 identifier, 307 key, 308 method: usedMethod, 309 hostname: host, 310 port, 311 mac, 312 nonce, 313 ts, 314 ext, 315 getHeader, 316 }; 317 }, 318 319 /** 320 * Obtain the HTTP MAC Authorization header value from fields. 321 * 322 * @param identifier 323 * (string) MAC key identifier. 324 * @param ts 325 * (number) Integer seconds since Unix epoch. 326 * @param nonce 327 * (string) Nonce value. 328 * @param mac 329 * (string) Computed HMAC digest (raw bytes). 330 * @param ext 331 * (optional) (string) Extra string content. 332 * @returns 333 * (string) Value to put in Authorization header. 334 */ 335 getHTTPMACSHA1Header: function getHTTPMACSHA1Header( 336 identifier, 337 ts, 338 nonce, 339 mac, 340 ext 341 ) { 342 let header = 343 'MAC id="' + 344 identifier + 345 '", ' + 346 'ts="' + 347 ts + 348 '", ' + 349 'nonce="' + 350 nonce + 351 '", ' + 352 'mac="' + 353 btoa(mac) + 354 '"'; 355 356 if (!ext) { 357 return header; 358 } 359 360 return (header += ', ext="' + ext + '"'); 361 }, 362 363 /** 364 * Given an HTTP header value, strip out any attributes. 365 */ 366 367 stripHeaderAttributes(value) { 368 value = value || ""; 369 let i = value.indexOf(";"); 370 return value 371 .substring(0, i >= 0 ? i : undefined) 372 .trim() 373 .toLowerCase(); 374 }, 375 376 /** 377 * Compute the HAWK client values (mostly the header) for an HTTP request. 378 * 379 * @param URI 380 * (nsIURI) HTTP request URI. 381 * @param method 382 * (string) HTTP request method. 383 * @param options 384 * (object) extra parameters (all but "credentials" are optional): 385 * credentials - (object, mandatory) HAWK credentials object. 386 * All three keys are required: 387 * id - (string) key identifier 388 * key - (string) raw key bytes 389 * ext - (string) application-specific data, included in MAC 390 * localtimeOffsetMsec - (number) local clock offset (vs server) 391 * payload - (string) payload to include in hash, containing the 392 * HTTP request body. If not provided, the HAWK hash 393 * will not cover the request body, and the server 394 * should not check it either. This will be UTF-8 395 * encoded into bytes before hashing. This function 396 * cannot handle arbitrary binary data, sorry (the 397 * UTF-8 encoding process will corrupt any codepoints 398 * between U+0080 and U+00FF). Callers must be careful 399 * to use an HTTP client function which encodes the 400 * payload exactly the same way, otherwise the hash 401 * will not match. 402 * contentType - (string) payload Content-Type. This is included 403 * (without any attributes like "charset=") in the 404 * HAWK hash. It does *not* affect interpretation 405 * of the "payload" property. 406 * hash - (base64 string) pre-calculated payload hash. If 407 * provided, "payload" is ignored. 408 * ts - (number) pre-calculated timestamp, secs since epoch 409 * now - (number) current time, ms-since-epoch, for tests 410 * nonce - (string) pre-calculated nonce. Should only be defined 411 * for testing as this function will generate a 412 * cryptographically secure random one if not defined. 413 * @returns 414 * Promise<Object> Contains results of operation. The object has the 415 * following keys: 416 * field - (string) HAWK header, to use in Authorization: header 417 * artifacts - (object) other generated values: 418 * ts - (number) timestamp, in seconds since epoch 419 * nonce - (string) 420 * method - (string) 421 * resource - (string) path plus querystring 422 * host - (string) 423 * port - (number) 424 * hash - (string) payload hash (base64) 425 * ext - (string) app-specific data 426 * MAC - (string) request MAC (base64) 427 */ 428 async computeHAWK(uri, method, options) { 429 let credentials = options.credentials; 430 let ts = 431 options.ts || 432 Math.floor( 433 ((options.now || Date.now()) + (options.localtimeOffsetMsec || 0)) / 434 1000 435 ); 436 let port; 437 if (uri.port != -1) { 438 port = uri.port; 439 } else if (uri.scheme == "http") { 440 port = 80; 441 } else if (uri.scheme == "https") { 442 port = 443; 443 } else { 444 throw new Error("Unsupported URI scheme: " + uri.scheme); 445 } 446 447 let artifacts = { 448 ts, 449 nonce: options.nonce || btoa(CryptoUtils.generateRandomBytesLegacy(8)), 450 method: method.toUpperCase(), 451 resource: uri.pathQueryRef, // This includes both path and search/queryarg. 452 host: uri.asciiHost.toLowerCase(), // This includes punycoding. 453 port: port.toString(10), 454 hash: options.hash, 455 ext: options.ext, 456 }; 457 458 let contentType = CryptoUtils.stripHeaderAttributes(options.contentType); 459 460 if ( 461 !artifacts.hash && 462 options.hasOwnProperty("payload") && 463 options.payload 464 ) { 465 const buffer = lazy.textEncoder.encode( 466 `hawk.1.payload\n${contentType}\n${options.payload}\n` 467 ); 468 const hash = await crypto.subtle.digest("SHA-256", buffer); 469 // HAWK specifies this .hash to use +/ (not _-) and include the 470 // trailing "==" padding. 471 artifacts.hash = ChromeUtils.base64URLEncode(hash, { pad: true }) 472 .replace(/-/g, "+") 473 .replace(/_/g, "/"); 474 } 475 476 let requestString = 477 "hawk.1.header\n" + 478 artifacts.ts.toString(10) + 479 "\n" + 480 artifacts.nonce + 481 "\n" + 482 artifacts.method + 483 "\n" + 484 artifacts.resource + 485 "\n" + 486 artifacts.host + 487 "\n" + 488 artifacts.port + 489 "\n" + 490 (artifacts.hash || "") + 491 "\n"; 492 if (artifacts.ext) { 493 requestString += artifacts.ext.replace("\\", "\\\\").replace("\n", "\\n"); 494 } 495 requestString += "\n"; 496 497 const hash = await CryptoUtils.hmacLegacy( 498 "SHA-256", 499 credentials.key, 500 requestString 501 ); 502 artifacts.mac = btoa(hash); 503 // The output MAC uses "+" and "/", and padded== . 504 505 function escape(attribute) { 506 // This is used for "x=y" attributes inside HTTP headers. 507 return attribute.replace(/\\/g, "\\\\").replace(/\"/g, '\\"'); 508 } 509 let header = 510 'Hawk id="' + 511 credentials.id + 512 '", ' + 513 'ts="' + 514 artifacts.ts + 515 '", ' + 516 'nonce="' + 517 artifacts.nonce + 518 '", ' + 519 (artifacts.hash ? 'hash="' + artifacts.hash + '", ' : "") + 520 (artifacts.ext ? 'ext="' + escape(artifacts.ext) + '", ' : "") + 521 'mac="' + 522 artifacts.mac + 523 '"'; 524 return { 525 artifacts, 526 field: header, 527 }; 528 }, 529 }; 530 531 var Svc = {}; 532 533 Observers.add("xpcom-shutdown", function unloadServices() { 534 Observers.remove("xpcom-shutdown", unloadServices); 535 536 for (let k in Svc) { 537 delete Svc[k]; 538 } 539 });