services/crypto/modules/utils.js

Tue, 06 Jan 2015 21:39:09 +0100

author
Michael Schloh von Bennewitz <michael@schloh.com>
date
Tue, 06 Jan 2015 21:39:09 +0100
branch
TOR_BUG_9701
changeset 8
97036ab72558
permissions
-rw-r--r--

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 });

mercurial