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