/* ***** BEGIN LICENSE BLOCK *****
 * Version: MPL 1.1/GPL 2.0/LGPL 2.1
 *
 * The contents of this file are subject to the Mozilla Public License Version
 * 1.1 (the "License"); you may not use this file except in compliance with
 * the License. You may obtain a copy of the License at
 * http://www.mozilla.org/MPL/
 *
 * Software distributed under the License is distributed on an "AS IS" basis,
 * WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License
 * for the specific language governing rights and limitations under the
 * License.
 *
 * The Original Code is Google Safe Browsing.
 *
 * The Initial Developer of the Original Code is Google Inc.
 * Portions created by the Initial Developer are Copyright (C) 2006
 * the Initial Developer. All Rights Reserved.
 *
 * Contributor(s):
 *   Fritz Schneider <fritz@google.com> (original author)
 *   Monica Chew <mmc@google.com>
 *
 * Alternatively, the contents of this file may be used under the terms of
 * either the GNU General Public License Version 2 or later (the "GPL"), or
 * the GNU Lesser General Public License Version 2.1 or later (the "LGPL"),
 * in which case the provisions of the GPL or the LGPL are applicable instead
 * of those above. If you wish to allow use of your version of this file only
 * under the terms of either the GPL or the LGPL, and not to allow others to
 * use your version of this file under the terms of the MPL, indicate your
 * decision by deleting the provisions above and replace them with the notice
 * and other provisions required by the GPL or the LGPL. If you do not delete
 * the provisions above, a recipient may use your version of this file under
 * the terms of any one of the MPL, the GPL or the LGPL.
 *
 * ***** END LICENSE BLOCK ***** */


// This file implements our query param encryption. You hand it a set
// of query params, and it will hand you a set of (maybe) encrypted
// query params back. It takes the query params you give it, 
// encodes and encrypts them into a encrypted query param, and adds
// the extra query params the server will need to decrypt them
// (e.g., the version of encryption and the decryption key).
// 
// The key manager provides the keys we need; this class just focuses
// on encrypting query params. See the url crypto key manager for
// details of our protocol, but essentially encryption is
// RC4_key(input) with key == MD5(K_C || nonce) where nonce is a
// 32-bit integer appended big-endian and K_C is the client's key.
//
// If for some reason we don't have an encryption key, encrypting is the 
// identity function.

/**
 * This class knows how to encrypt query parameters that will be
 * understood by the lookupserver.
 * 
 * @constructor
 */
function PROT_UrlCrypto() {
  this.debugZone = "urlcrypto";
  this.hasher_ = new G_CryptoHasher();
  this.base64_ = new G_Base64();
  this.streamCipher_ = Cc["@mozilla.org/security/streamcipher;1"]
                       .createInstance(Ci.nsIStreamCipher);

  if (!this.manager_) {
    // Create a UrlCryptoKeyManager to reads keys from profile directory if
    // one doesn't already exist.  UrlCryptoKeyManager puts a reference to
    // itself on PROT_UrlCrypto.prototype (this also prevents garbage
    // collection).
    new PROT_UrlCryptoKeyManager();
  }

  // Convenience properties
  this.VERSION = PROT_UrlCrypto.VERSION;
  this.RC4_DISCARD_BYTES = PROT_UrlCrypto.RC4_DISCARD_BYTES;
  this.VERSION_QUERY_PARAM_NAME = PROT_UrlCrypto.QPS.VERSION_QUERY_PARAM_NAME;
  this.ENCRYPTED_PARAMS_PARAM_NAME = 
    PROT_UrlCrypto.QPS.ENCRYPTED_PARAMS_PARAM_NAME;
  this.COUNT_QUERY_PARAM_NAME = PROT_UrlCrypto.QPS.COUNT_QUERY_PARAM_NAME;
  this.WRAPPEDKEY_QUERY_PARAM_NAME = 
    PROT_UrlCrypto.QPS.WRAPPEDKEY_QUERY_PARAM_NAME;

  // Properties for computing macs
  this.macer_ = new G_CryptoHasher(); // don't use hasher_
  this.macInitialized_ = false;
  // Separator to prevent leakage between key and data when computing mac
  this.separator_ = ":coolgoog:";
  this.separatorArray_ = this.base64_.arrayifyString(this.separator_);
}

// The version of encryption we implement
PROT_UrlCrypto.VERSION = "1";

PROT_UrlCrypto.RC4_DISCARD_BYTES = 1600;

// The query params are we going to send to let the server know what is
// encrypted, and how
PROT_UrlCrypto.QPS = {};
PROT_UrlCrypto.QPS.VERSION_QUERY_PARAM_NAME = "encver";
PROT_UrlCrypto.QPS.ENCRYPTED_PARAMS_PARAM_NAME = "encparams";
PROT_UrlCrypto.QPS.COUNT_QUERY_PARAM_NAME = "nonce";
PROT_UrlCrypto.QPS.WRAPPEDKEY_QUERY_PARAM_NAME = "wrkey";

/**
 * @returns Reference to the keymanager (if one exists), else undefined
 */
PROT_UrlCrypto.prototype.getManager = function() {
  return this.manager_;
}

/**
 * Helper method that takes a map of query params (param name ->
 * value) and turns them into a query string. Note that it encodes
 * the values as it writes the string.
 *
 * @param params Object (map) of query names to values. Values should
 *               not be uriencoded.
 *
 * @returns String of query params from the map. Values will be uri
 *          encoded
 */
PROT_UrlCrypto.prototype.appendParams_ = function(params) {
  var queryString = "";
  for (var param in params)
    queryString += "&" + param + "=" + encodeURIComponent(params[param]);
                   
  return queryString;
}

/**
 * Encrypt a set of query params if we can. If we can, we return a new
 * set of query params that should be added to a query string. The set
 * of query params WILL BE different than the input query params if we
 * can encrypt (e.g., there will be extra query params with meta-
 * information such as the version of encryption we're using). If we
 * can't encrypt, we just return the query params we're passed.
 *
 * @param params Object (map) of query param names to values. Values should
 *               not be uriencoded.
 *
 * @returns Object (map) of query param names to values. Values are NOT
 *          uriencoded; the caller should encode them as it writes them
 *          to a proper query string.
 */
PROT_UrlCrypto.prototype.maybeCryptParams = function(params) {
  if (!this.manager_)
    throw new Error("Need a key manager for UrlCrypto");
  if (typeof params != "object")
    throw new Error("params is an associative array of name/value params");

  var clientKeyArray = this.manager_.getClientKeyArray();
  var wrappedKey = this.manager_.getWrappedKey();

  // No keys? Can't encrypt. Damn.
  if (!clientKeyArray || !wrappedKey) {
    G_Debug(this, "No key; can't encrypt query params");
    return params;
  }

  // Serialize query params to a query string that we will then
  // encrypt and place in a special query param the front-end knows is
  // encrypted.
  var queryString = this.appendParams_(params);
  
  // Nonce, really. We want 32 bits; make it so.
  var counter = this.getCount_();
  counter = counter & 0xFFFFFFFF;
  
  var encrypted = this.encryptV1(clientKeyArray, 
                                 this.VERSION,
                                 counter,
                                 queryString);

  params = {};
  params[this.VERSION_QUERY_PARAM_NAME] = this.VERSION;
  params[this.COUNT_QUERY_PARAM_NAME] = counter;
  params[this.WRAPPEDKEY_QUERY_PARAM_NAME] = wrappedKey;
  params[this.ENCRYPTED_PARAMS_PARAM_NAME] = encrypted;

  return params;
}

/**
 * Encrypts text and returns a base64 string of the results.
 *
 * This method runs in about ~2ms on a 2Ghz P4. (Turn debugging off if
 * you see it much slower).
 *
 * @param clientKeyArray Array of bytes (numbers in [0,255]) composing K_C
 *
 * @param version String indicating the version of encryption we should use.
 *
 * @param counter Number that acts as a nonce for this encryption
 *
 * @param text String to be encrypted
 *
 * @returns String containing the websafe base64-encoded ciphertext
 */
PROT_UrlCrypto.prototype.encryptV1 = function(clientKeyArray,
                                              version, 
                                              counter,
                                              text) {

  // We're a version1 encrypter, after all
  if (version != "1") 
    throw new Error("Unknown encryption version");

  var key = this.deriveEncryptionKey(clientKeyArray, counter);

  this.streamCipher_.init(key);

  if (this.RC4_DISCARD_BYTES > 0)
    this.streamCipher_.discard(this.RC4_DISCARD_BYTES);
  
  this.streamCipher_.updateFromString(text);

  var encrypted = this.streamCipher_.finish(true /* base64 encoded */);
  // The base64 version we get has new lines, we want to remove those.
  
  return encrypted.replace(/\r\n/g, "");
}
  
/**
 * Create an encryption key from K_C and a nonce
 *
 * @param clientKeyArray Array of bytes comprising K_C
 *
 * @param count Number that acts as a nonce for this key
 *
 * @return nsIKeyObject
 */
PROT_UrlCrypto.prototype.deriveEncryptionKey = function(clientKeyArray, 
                                                        count) {
  G_Assert(this, clientKeyArray instanceof Array,
           "Client key should be an array of bytes");
  G_Assert(this, typeof count == "number", "Count should be a number");
  
  // Don't clobber the client key by appending the nonce; use another array
  var paddingArray = [];
  paddingArray.push(count >> 24);
  paddingArray.push((count >> 16) & 0xFF);
  paddingArray.push((count >> 8) & 0xFF);
  paddingArray.push(count & 0xFF);

  this.hasher_.init(G_CryptoHasher.algorithms.MD5);
  this.hasher_.updateFromArray(clientKeyArray);
  this.hasher_.updateFromArray(paddingArray);

  // Create the nsIKeyObject
  var keyFactory = Cc["@mozilla.org/security/keyobjectfactory;1"]
                   .getService(Ci.nsIKeyObjectFactory);
  var key = keyFactory.keyFromString(Ci.nsIKeyObject.RC4,
                                     this.hasher_.digestRaw());
  return key;
}

/**
 * Return a new nonce for us to use. Rather than keeping a counter and
 * the headaches that entails, just use the low ms since the epoch.
 *
 * @returns 32-bit number that is the nonce to use for this encryption
 */
PROT_UrlCrypto.prototype.getCount_ = function() {
  return ((new Date).getTime() & 0xFFFFFFFF);
}

/**
 * Init the mac.  This function is called by WireFormatReader if the update
 * server has sent along a mac param.  The caller must not call initMac again
 * before calling finishMac; instead, the caller should just use another
 * UrlCrypto object.
 *
 * @param opt_clientKeyArray Optional clientKeyArray, for testing
 */
PROT_UrlCrypto.prototype.initMac = function(opt_clientKeyArray) {
  if (this.macInitialized_) {
    throw new Error("Can't interleave calls to initMac.  Please use another " +
                    "UrlCrypto object.");
  }

  this.macInitialized_ = true;

  var clientKeyArray = null;

  if (!!opt_clientKeyArray) {
    clientKeyArray = opt_clientKeyArray;
  } else {
    clientKeyArray = this.manager_.getClientKeyArray();
  }

  // Don't re-use this.hasher_, in case someone calls deriveEncryptionKey
  // between initMac and finishMac
  this.macer_.init(G_CryptoHasher.algorithms.MD5);

  this.macer_.updateFromArray(clientKeyArray);
  this.macer_.updateFromArray(this.separatorArray_);
}

/**
 * Add a line to the mac.  Called by WireFormatReader.processLine.  Not thread
 * safe.
 *
 * @param s The string to add
 */
PROT_UrlCrypto.prototype.updateMacFromString = function(s) {
  if (!this.macInitialized_) {
    throw new Error ("Initialize mac first");
  }

  var arr = this.base64_.arrayifyString(s);
  this.macer_.updateFromArray(arr);
}

/**
 * Finish up computing the mac.  Not thread safe.
 *
 * @param opt_clientKeyArray Optional clientKeyArray, for testing
 */
PROT_UrlCrypto.prototype.finishMac = function(opt_clientKeyArray) {
  var clientKeyArray = null;
  if (!!opt_clientKeyArray) {
    clientKeyArray = opt_clientKeyArray;
  } else {
    clientKeyArray = this.manager_.getClientKeyArray();
  }

  if (!this.macInitialized_) {
    throw new Error ("Initialize mac first");
  }
  this.macer_.updateFromArray(this.separatorArray_);
  this.macer_.updateFromArray(clientKeyArray);

  this.macInitialized_ = false;

  return this.macer_.digestBase64();
}

/**
 * Compute a mac over the whole data string, and return the base64-encoded
 * string
 *
 * @param data A string
 * @param opt_outputRaw True for raw output, false for base64
 * @param opt_clientKeyArray An optional key to pass in for testing
 * @param opt_separatorArray An optional separator array to pass in for testing
 * @returns MD5(key+separator+data+separator+key)
 */
PROT_UrlCrypto.prototype.computeMac = function(data, 
                                               opt_outputRaw,
                                               opt_clientKeyArray,
                                               opt_separatorArray) {
  var clientKeyArray = null;
  var separatorArray = null;

  // Get keys and such for testing
  if (!!opt_clientKeyArray) {
    clientKeyArray = opt_clientKeyArray;
  } else {
    clientKeyArray = this.manager_.getClientKeyArray();
  }

  if (!!opt_separatorArray) {
    separatorArray = opt_separatorArray;
  } else {
    separatorArray = this.separatorArray_;
  }

  this.macer_.init(G_CryptoHasher.algorithms.MD5);

  this.macer_.updateFromArray(clientKeyArray);
  this.macer_.updateFromArray(separatorArray);

  // Note to self: calling G_CryptoHasher.updateFromString ain't the same as
  // arrayifying the string and then calling updateFromArray.  Not sure if
  // that's a bug in G_CryptoHasher or not.  Niels, what do you think?
  var arr = this.base64_.arrayifyString(data);
  this.macer_.updateFromArray(arr);

  this.macer_.updateFromArray(separatorArray);
  this.macer_.updateFromArray(clientKeyArray);

  if (!!opt_outputRaw) {
    return this.macer_.digestRaw();
  }
  return this.macer_.digestBase64();
}

#ifdef DEBUG
/**
 * Cheeseball unittest
 */ 
function TEST_PROT_UrlCrypto() {
  if (G_GDEBUG) {
    var z = "urlcrypto UNITTEST";
    G_debugService.enableZone(z);

    G_Debug(z, "Starting");

    // We set keys on the keymanager to ensure data flows properly, so 
    // make sure we can clean up after it

    var kf = "test.txt";
    function removeTestFile(f) {
      var appDir = new PROT_ApplicationDirectory();
      var file = appDir.getAppDirFileInterface();
      file.append(f);
      if (file.exists())
        file.remove(false /* do not recurse */ );
    };
    removeTestFile(kf);

    var km = new PROT_UrlCryptoKeyManager(kf, true /* testing */);

    // Test operation when it's not intialized

    var c = new PROT_UrlCrypto();

    var fakeManager = {
      getClientKeyArray: function() { return null; },
      getWrappedKey: function() { return null; },
    };
    c.manager_ = fakeManager;

    var params = {
      foo: "bar",
      baz: "bomb",
    };
    G_Assert(z, c.maybeCryptParams(params)["foo"] === "bar",
             "How can we encrypt if we don't have a key?");
    c.manager_ = km;
    G_Assert(z, c.maybeCryptParams(params)["foo"] === "bar",
             "Again, how can we encrypt if we don't have a key?");

    // Now we have a key! See if we can get a crypted url
    var realResponse = "clientkey:24:dtmbEN1kgN/LmuEoYifaFw==\n" +
                       "wrappedkey:24:MTpPH3pnLDKihecOci+0W5dk";
    km.onGetKeyResponse(realResponse);
    var crypted = c.maybeCryptParams(params);
    G_Assert(z, crypted["foo"] === undefined, "We have a key but can't crypt");
    G_Assert(z, crypted["bomb"] === undefined, "We have a key but can't crypt");

    // Now check to make sure all the expected query params are there
    for (var p in PROT_UrlCrypto.QPS)
      G_Assert(z, crypted[PROT_UrlCrypto.QPS[p]] != undefined, 
               "Output query params doesn't have: " + PROT_UrlCrypto.QPS[p]);
    
    // Now test that encryption is determinisitic
    var b64 = new G_Base64();
    
    // Some helper functions
    function arrayEquals(a1, a2) {
      if (a1.length != a2.length)
        return false;
      
      for (var i = 0; i < a1.length; i++)
        if (typeof a1[i] != typeof a2[i] || a1[i] != a2[i])
          return false;
      return true;
    };

    function arrayAsString(a) {
      var s = "[";
      for (var i = 0; i < a.length; i++)
        s += a[i] + ",";
      return s + "]";
    };

    function printArray(a) {
      var s = arrayAsString(a);
      G_Debug(z, s);
    };

    var keySizeBytes = km.clientKeyArray_.length;

    var startCrypt = (new Date).getTime();
    var numCrypts = 0;

    // Set this to true for extended testing
    var doLongTest = false;
    if (doLongTest) {
      
      // Now run it through its paces. For a variety of keys of a
      // variety of lengths, and a variety of coutns, encrypt
      // plaintexts of different lengths

      // For a variety of key lengths...
      for (var i = 0; i < 2 * keySizeBytes; i++) {
        var clientKeyArray = [];
        
        // For a variety of keys...
        for (var j = 0; j < keySizeBytes; j++)
          clientKeyArray[j] = i + j;
        
        // For a variety of counts...
        for (var count = 0; count < 40; count++) {
        
          var payload = "";

          // For a variety of plaintexts of different lengths
          for (var payloadPadding = 0; payloadPadding < count; payloadPadding++)
            payload += "a";
          
          var plaintext1 = b64.arrayifyString(payload);
          var plaintext2 = b64.arrayifyString(payload);
          var plaintext3 = b64.arrayifyString(payload);
          
          // Verify that encryption is deterministic given set parameters
          numCrypts++;
          var ciphertext1 = c.encryptV1(clientKeyArray, 
                                      "1", 
                                        count,
                                        plaintext1);
          
          numCrypts++;        
          var ciphertext2 = c.encryptV1(clientKeyArray, 
                                        "1", 
                                        count,
                                        plaintext2);
          
          G_Assert(z, ciphertext1 === ciphertext2,
                   "Two plaintexts having different ciphertexts:" +
                   ciphertext1 + " " + ciphertext2);
          
          numCrypts++;

          // Now verify that it is symmetrical
          var ciphertext3 = c.encryptV1(clientKeyArray, 
                                        "1", 
                                        count,
                                        b64.decodeString(ciphertext2), 
                                        true /* websafe */);
          
          G_Assert(z, arrayEquals(plaintext3, b64.decodeString(ciphertext3, 
                                                              true/*websafe*/)),
                   "Encryption and decryption not symmetrical");
        }
      }
      
      // Output some interesting info
      var endCrypt = (new Date).getTime();
      var totalMS = endCrypt - startCrypt;
      G_Debug(z, "Info: Did " + numCrypts + " encryptions in " +
              totalMS + "ms, for an average of " + 
              (totalMS / numCrypts) + "ms per crypt()");
    }
      
    // Now check for compatability with C++

    var ciphertexts = {}; 
    // Generated below, and tested in C++ as well. Ciphertexts is a map
    // from substring lengths to encrypted values.

    ciphertexts[0]="";
    ciphertexts[1]="dA==";
    ciphertexts[2]="akY=";
    ciphertexts[3]="u5mV";
    ciphertexts[4]="bhtioQ==";
    ciphertexts[5]="m2wSZnQ=";
    ciphertexts[6]="zd6gWyDO";
    ciphertexts[7]="bBN0WVrlCg==";
    ciphertexts[8]="Z6U_6bMelFM=";
    ciphertexts[9]="UVoiytL-gHzp";
    ciphertexts[10]="3Xr_ZMmdmvg7zw==";
    ciphertexts[11]="PIIyif7NFRS57mY=";
    ciphertexts[12]="QEKXrRWdZ3poJVSp";
    ciphertexts[13]="T3zsAsooHuAnflNsNQ==";
    ciphertexts[14]="qgYtOJjZSIByo0KtOG0=";
    ciphertexts[15]="NsEGHGK6Ju6FjD59Byai";
    ciphertexts[16]="1RVIsC0HYoUEycoA_0UL2w==";
    ciphertexts[17]="0xXe6Lsb1tZ79T96AJTT-ps=";
    ciphertexts[18]="cVXQCYoA4RV8t1CODXuCS88y";
    ciphertexts[19]="hVf4pd4WP4wPwSyqEXRRkQZSQA==";
    ciphertexts[20]="F6Y9MHwhd1e-bDHhqNSonZbR2Sg=";
    ciphertexts[21]="TiMClYbLUdyYweW8IDytU_HD2wTM";
    ciphertexts[22]="tYQtNqz83KXE4eqn6GhAu6ZZ23SqYw==";
    ciphertexts[23]="qjL-dMpiQ2LYgkYT5IfmE1FlN36wHek=";
    ciphertexts[24]="cL7HHiOZ9PbkvZ9yrJLiv4HXcw4Nf7y7";
    ciphertexts[25]="k4I-fdR6CyzxOpR_QEG5rnvPB8IbmRnpFg==";
    ciphertexts[26]="7LjCfA1dCMjAVT_O8DpiTQ0G7igwQ1HTUMU=";
    ciphertexts[27]="CAtijc6nB-REwAkqimToMn8RC_eZAaJy9Gn4";
    ciphertexts[28]="z8sEB1lDI32wsOkgYbVZ5pxIbpCrha9BmcqxFQ==";
    ciphertexts[29]="2eysfzsfGav0vPRsSnFl8H8fg9dQCT_bSiZwno0=";
    ciphertexts[30]="2BBNlF_mtV9TB2jZHHqCAtzkJQFdVKFn7N8YxsI9";
    ciphertexts[31]="9h4-nldHAr77Boks7lPzsi8TwVCIQzSkiJp2xatbGg==";
    ciphertexts[32]="DHTB8bDTXpUIrZ2ZlAujXLi-501NoWUVIEQJLaKCpqQ=";
    ciphertexts[33]="E9Av2GgnZg_q5r-JLSzM_ShCu1yPF2VeCaQfPPXSSE4I";
    ciphertexts[34]="UJzEucVBnGEfRNBQ6tvbaro0_I_-mQeJMpU2zQnfFdBuFg==";
    ciphertexts[35]="_p0OYras-Vn2rQ9X-J0dFRnhCfytuTEjheUTU7Ueaf1rIA4=";
    ciphertexts[36]="Q0nZXFPJbpx1WZPP-lLPuSGR-pD08B4CAW-6Uf0eEkS05-oM";
    ciphertexts[37]="XeKfieZGc9bPh7nRtCgujF8OY14zbIZSK20Lwg1HTpHi9HfXVQ==";
    
    var clientKeyArray = b64.decodeString("dtmbEN1kgN/LmuEoYifaFw==");
    // wrappedKey was "MTpPH3pnLDKihecOci+0W5dk"
    var count = 0xFEDCBA09;
    var plaintext = "http://www.foobar.com/this?is&some=url";
    
    // For every substring of the plaintext, change the count and verify
    // that we get what we expect when we encrypt

    for (var i = 0; i < plaintext.length; i++) {
      var plaintextArray = b64.arrayifyString(plaintext.substring(0, i));
      var crypted = c.encryptV1(clientKeyArray,
                                "1",
                                count + i,
                                plaintextArray);
      G_Assert(z, crypted === ciphertexts[i], 
               "Generated unexpected ciphertext");

      // Uncomment to generate
      // dump("\nciphertexts[" + i + "]=\"" + crypted + "\";");
    }

    // Cribbed from the MD5 rfc to make sure we're computing plain'ol md5
    // values correctly
    // http://www.faqs.org/rfcs/rfc1321.html
    var md5texts = [ "",
                     "a", 
                     "abc", 
                     "message digest",
                     "abcdefghijklmnopqrstuvwxyz",
                     "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789",
                     "12345678901234567890123456789012345678901234567890123456789012345678901234567890"
		   ];

    var md5digests = [ "d41d8cd98f00b204e9800998ecf8427e", 
                       "0cc175b9c0f1b6a831c399e269772661", 
                       "900150983cd24fb0d6963f7d28e17f72", 
                       "f96b697d7cb7938d525a2f31aaf161d0", 
                       "c3fcd3d76192e4007dfb496cca67e13b", 
                       "d174ab98d277d9f5a5611c2c9f419d9f", 
                       "57edf4a22be3c955ac49da2e2107b67a"
		     ];

    var expected_mac = [];
    expected_mac[0] = "ZOJ6Mk+ccC6R7BwseqCYQQ==";
    expected_mac[1] = "zWM7tvcsuH/MSEviNiRbOA==";
    expected_mac[2] = "ZAUVyls/6ZVN3Np8v3pX3g==";
    expected_mac[3] = "Zq6gF7RkPwKqlicuxrO4mg==";
    expected_mac[4] = "/LOJETSnqSW3q4u1hs/0Pg==";
    expected_mac[5] = "jjOEX7H2uchOznxIGuqzJg==";
    expected_mac[6] = "Tje7aP/Rk/gkSH4he0KMQQ==";

    // Check plain'ol MD5
    var hasher = new G_CryptoHasher();
    for (var i = 0; i < md5texts.length; i++) {
      var computedMac = c.computeMac(md5texts[i], true /* output raw */, [], []);
      var hex = hasher.toHex_(computedMac).toLowerCase();
      G_Assert(z, hex == md5digests[i], 
               "MD5(" + md5texts[i] + ") = " + md5digests[i] + ", not " + hex);
    }

    for (var i = 0; i < md5texts.length; i++) {
      var computedMac = c.computeMac(md5texts[i], 
                                  false /* output Base64 */,
                                  clientKeyArray);
      G_Assert(z, computedMac == expected_mac[i], 
               "Wrong mac generated for " + md5texts[i]);
      // Uncomment to generate
      // dump("\nexpected_mac[" + i + "] = \"" + computedMac + "\";");
    }

    // Now check if adding line by line is the same as computing over the whole
    // thing
    var wholeString = md5texts[0] + md5texts[1];
    var wholeStringMac = c.computeMac(wholeString,
                                      false /* output Base64 */,
				      clientKeyArray);

    expected_mac = "zWM7tvcsuH/MSEviNiRbOA==";
    c.initMac(clientKeyArray);
    c.updateMacFromString(md5texts[0]);
    c.updateMacFromString(md5texts[1]);
    var piecemealMac = c.finishMac(clientKeyArray);
    G_Assert(z, piecemealMac == wholeStringMac,
             "Computed different values for mac when adding line by line!");
    G_Assert(z, piecemealMac == expected_mac, "Didn't generate expected mac");

    // Tested also in cheesey-test-frontend.sh and urlcrypto_unittest.cc on
    // the server side
    expected_mac = "iA5vLUidpXAPwfcAH9+8OQ==";
    var set3data = "";
    for (var i = 1; i <= 3; i++) {
      set3data += "+white" + i + ".com\t1\n";
    }
    var computedMac = c.computeMac(set3data, false /*Base64*/, clientKeyArray);
    G_Assert(z, expected_mac == computedMac, "Expected " + expected_mac, " got " + computedMac);

    removeTestFile(kf);

    G_Debug(z, "PASS");
  }
}
#endif
