diff --git a/browser/app/profile/firefox.js b/browser/app/profile/firefox.js index 1a428968d739..0edbf931bdef 100644 --- a/browser/app/profile/firefox.js +++ b/browser/app/profile/firefox.js @@ -580,7 +580,7 @@ pref("browser.safebrowsing.dataProvider", 0); // Does the provider name need to be localizable? pref("browser.safebrowsing.provider.0.name", "Google"); pref("browser.safebrowsing.provider.0.lookupURL", "http://safebrowsing.clients.google.com/safebrowsing/lookup?sourceid=firefox-antiphish&features=TrustRank&client={moz:client}&appver={moz:version}&"); -pref("browser.safebrowsing.provider.0.keyURL", "https://sb-ssl.google.com/safebrowsing/getkey?client={moz:client}&"); +pref("browser.safebrowsing.provider.0.keyURL", "https://sb-ssl.google.com/safebrowsing/newkey?client={moz:client}&appver={moz:version}&pver=2.1"); pref("browser.safebrowsing.provider.0.reportURL", "http://safebrowsing.clients.google.com/safebrowsing/report?"); pref("browser.safebrowsing.provider.0.gethashURL", "http://safebrowsing.clients.google.com/safebrowsing/gethash?client={moz:client}&appver={moz:version}&pver=2.1"); diff --git a/browser/components/safebrowsing/content/globalstore.js b/browser/components/safebrowsing/content/globalstore.js index 62e54e6bb79c..821df84ef9c5 100644 --- a/browser/components/safebrowsing/content/globalstore.js +++ b/browser/components/safebrowsing/content/globalstore.js @@ -131,9 +131,13 @@ PROT_DataProvider.prototype.updateListManager_ = function() { listManager.setUpdateUrl(this.getUpdateURL()); // setKeyUrl has the side effect of fetching a key from the server. - // This shouldn't happen if anti-phishing is disabled or we're in local - // list mode, so we need to check for that. - var isEnabled = this.prefs_.getPref(kPhishWardenEnabledPref, false); + // This shouldn't happen if anti-phishing/anti-malware is disabled. + var isEnabled = this.prefs_.getPref(kPhishWardenEnabledPref, false) || + this.prefs_.getPref(kMalwareWardenEnabledPref, false); + if (isEnabled) { + listManager.setKeyUrl(this.keyURL_); + } + listManager.setGethashUrl(this.getGethashURL()); } diff --git a/browser/components/safebrowsing/content/malware-warden.js b/browser/components/safebrowsing/content/malware-warden.js index c45af37916a8..a59d1bff0e38 100644 --- a/browser/components/safebrowsing/content/malware-warden.js +++ b/browser/components/safebrowsing/content/malware-warden.js @@ -89,8 +89,8 @@ function PROT_MalwareWarden() { }; try { - dbService_.beginUpdate(listener); - dbService_.beginStream(""); + dbService_.beginUpdate(listener, ""); + dbService_.beginStream("", ""); dbService_.updateStream(testUpdate); dbService_.finishStream(); dbService_.finishUpdate(); diff --git a/toolkit/components/url-classifier/content/listmanager.js b/toolkit/components/url-classifier/content/listmanager.js index 315d164be83c..e42ad2f37593 100644 --- a/toolkit/components/url-classifier/content/listmanager.js +++ b/toolkit/components/url-classifier/content/listmanager.js @@ -81,9 +81,21 @@ function PROT_ListManager() { BindToObject(this.shutdown_, this), true /*only once*/); - // Lazily create urlCrypto (see tr-fetcher.js) - this.urlCrypto_ = null; - + // Lazily create the key manager (to avoid fetching keys when they + // aren't needed). + this.keyManager_ = null; + + this.rekeyObserver_ = new G_ObserverServiceObserver( + 'url-classifier-rekey-requested', + BindToObject(this.rekey_, this), + false); + this.updateWaitingForKey_ = false; + + this.cookieObserver_ = new G_ObserverServiceObserver( + 'cookie-changed', + BindToObject(this.cookieChanged_, this), + false); + this.requestBackoff_ = new RequestBackoff(3 /* num errors */, 10*60*1000 /* error time, 10min */, 60*60*1000 /* backoff interval, 60min */, @@ -145,10 +157,15 @@ PROT_ListManager.prototype.setGethashUrl = function(url) { */ PROT_ListManager.prototype.setKeyUrl = function(url) { G_Debug(this, "Set key url: " + url); - if (!this.urlCrypto_) - this.urlCrypto_ = new PROT_UrlCrypto(); - - this.urlCrypto_.manager_.setKeyUrl(url); + if (!this.keyManager_) { + this.keyManager_ = new PROT_UrlCryptoKeyManager(); + this.keyManager_.onNewKey(BindToObject(this.newKey_, this)); + + this.hashCompleter_.setKeys(this.keyManager_.getClientKey(), + this.keyManager_.getWrappedKey()); + } + + this.keyManager_.setKeyUrl(url); } /** @@ -371,6 +388,27 @@ PROT_ListManager.prototype.checkForUpdates = function() { * tablename;\n */ PROT_ListManager.prototype.makeUpdateRequest_ = function(tableData) { + if (!this.keyManager_) + return; + + if (!this.keyManager_.hasKey()) { + // We don't have a client key yet. Schedule a rekey, and rerequest + // when we have one. + + // If there's already an update waiting for a new key, don't bother. + if (this.updateWaitingForKey_) + return; + + // If maybeReKey() returns false we have asked for too many keys, + // and won't be getting a new one. Since we don't want to do + // updates without a client key, we'll skip this update if maybeReKey() + // fails. + if (this.keyManager_.maybeReKey()) + this.updateWaitingForKey_ = true; + + return; + } + var tableNames = {}; for (var tableName in this.tablesData) { if (this.tablesData[tableName].needsUpdate) @@ -385,7 +423,7 @@ PROT_ListManager.prototype.makeUpdateRequest_ = function(tableData) { for (var i = 0; i < lines.length; i++) { var fields = lines[i].split(";"); if (tableNames[fields[0]]) { - request += lines[i] + "\n"; + request += lines[i] + ":mac\n"; delete tableNames[fields[0]]; } } @@ -393,20 +431,22 @@ PROT_ListManager.prototype.makeUpdateRequest_ = function(tableData) { // For each requested table that didn't have chunk data in the database, // request it fresh for (var tableName in tableNames) { - request += tableName + ";\n"; + request += tableName + ";:mac\n"; } G_Debug(this, 'checkForUpdates: scheduling request..'); var streamer = Cc["@mozilla.org/url-classifier/streamupdater;1"] .getService(Ci.nsIUrlClassifierStreamUpdater); try { - streamer.updateUrl = this.updateserverURL_; + streamer.updateUrl = this.updateserverURL_ + + "&wrkey=" + this.keyManager_.getWrappedKey(); } catch (e) { G_Debug(this, 'invalid url'); return; } if (!streamer.downloadUpdates(request, + this.keyManager_.getClientKey(), BindToObject(this.updateSuccess_, this), BindToObject(this.updateError_, this), BindToObject(this.downloadError_, this))) { @@ -458,6 +498,46 @@ PROT_ListManager.prototype.downloadError_ = function(status) { new G_Alarm(BindToObject(this.checkForUpdates, this), 60000); } +/** + * Called when either the update process or a gethash request signals + * that the server requested a rekey. + */ +PROT_ListManager.prototype.rekey_ = function() { + G_Debug(this, "rekey requested"); + + // The current key is no good anymore. + this.keyManager_.dropKey(); + this.keyManager_.maybeReKey(); +} + +/** + * Called when cookies are cleared - clears the current MAC keys. + */ +PROT_ListManager.prototype.cookieChanged_ = function(subject, topic, data) { + if (data != "cleared") + return; + + G_Debug(this, "cookies cleared"); + this.keyManager_.dropKey(); +} + +/** + * Called when we've received a new key from the server. + */ +PROT_ListManager.prototype.newKey_ = function() { + G_Debug(this, "got a new MAC key"); + + this.hashCompleter_.setKeys(this.keyManager_.getClientKey(), + this.keyManager_.getWrappedKey()); + + if (this.keyManager_.hasKey()) { + if (this.updateWaitingForKey_) { + this.updateWaitingForKey_ = false; + this.checkForUpdates(); + } + } +} + PROT_ListManager.prototype.QueryInterface = function(iid) { if (iid.equals(Ci.nsISupports) || iid.equals(Ci.nsIUrlListManager) || diff --git a/toolkit/components/url-classifier/content/url-crypto-key-manager.js b/toolkit/components/url-classifier/content/url-crypto-key-manager.js index 2cceff4ad586..635c5ce3acbc 100644 --- a/toolkit/components/url-classifier/content/url-crypto-key-manager.js +++ b/toolkit/components/url-classifier/content/url-crypto-key-manager.js @@ -67,7 +67,7 @@ // We store the user key in this file. The key can be used to verify signed // server updates. -const kKeyFilename = "kf.txt"; +const kKeyFilename = "urlclassifierkey3.txt"; /** * A key manager for UrlCrypto. There should be exactly one of these @@ -96,6 +96,7 @@ function PROT_UrlCryptoKeyManager(opt_keyFilename, opt_testing) { this.clientKeyArray_ = null; // Base64-decoded into an array of numbers this.wrappedKey_ = null; // Opaque websafe base64-encoded server key this.rekeyTries_ = 0; + this.updating_ = false; // Don't do anything until keyUrl_ is set. this.keyUrl_ = null; @@ -103,16 +104,14 @@ function PROT_UrlCryptoKeyManager(opt_keyFilename, opt_testing) { this.keyFilename_ = opt_keyFilename ? opt_keyFilename : kKeyFilename; + this.onNewKey_ = null; + // Convenience properties this.MAX_REKEY_TRIES = PROT_UrlCryptoKeyManager.MAX_REKEY_TRIES; this.CLIENT_KEY_NAME = PROT_UrlCryptoKeyManager.CLIENT_KEY_NAME; this.WRAPPED_KEY_NAME = PROT_UrlCryptoKeyManager.WRAPPED_KEY_NAME; if (!this.testing_) { - G_Assert(this, !PROT_UrlCrypto.prototype.manager_, - "Already have manager?"); - PROT_UrlCrypto.prototype.manager_ = this; - this.maybeLoadOldKey(); } } @@ -124,13 +123,20 @@ PROT_UrlCryptoKeyManager.MAX_REKEY_TRIES = 2; // We store the time as seconds since the epoch. PROT_UrlCryptoKeyManager.NEXT_REKEY_PREF = "urlclassifier.keyupdatetime."; -// Once a day (interval in seconds) -PROT_UrlCryptoKeyManager.KEY_MIN_UPDATE_TIME = 24 * 60 * 60; +// Once every 30 days (interval in seconds) +PROT_UrlCryptoKeyManager.KEY_MIN_UPDATE_TIME = 30 * 24 * 60 * 60; // These are the names the server will respond with in protocol4 format PROT_UrlCryptoKeyManager.CLIENT_KEY_NAME = "clientkey"; PROT_UrlCryptoKeyManager.WRAPPED_KEY_NAME = "wrappedkey"; +/** + * Called to get ClientKey + * @returns urlsafe-base64-encoded client key or null if we haven't gotten one. + */ +PROT_UrlCryptoKeyManager.prototype.getClientKey = function() { + return this.clientKey_; +} /** * Called by a UrlCrypto to get the current K_C @@ -193,7 +199,11 @@ PROT_UrlCryptoKeyManager.prototype.getPrefName_ = function(url) { * hit max-tries, but not an error to call maybeReKey(). */ PROT_UrlCryptoKeyManager.prototype.reKey = function() { - + if (this.updating_) { + G_Debug(this, "Already re-keying, ignoring this request"); + return true; + } + if (this.rekeyTries_ > this.MAX_REKEY_TRIES) throw new Error("Have already rekeyed " + this.rekeyTries_ + " times"); @@ -204,6 +214,7 @@ PROT_UrlCryptoKeyManager.prototype.reKey = function() { if (!this.testing_ && this.keyUrl_) { (new PROT_XMLFetcher()).get(this.keyUrl_, BindToObject(this.onGetKeyResponse, this)); + this.updating_ = true; // Calculate the next time we're allowed to re-key. var prefs = new G_Preferences(PROT_UrlCryptoKeyManager.NEXT_REKEY_PREF); @@ -231,10 +242,19 @@ PROT_UrlCryptoKeyManager.prototype.maybeReKey = function() { return true; } +/** + * Drop the existing set of keys. Resets the rekeyTries variable to + * allow a rekey to succeed. + */ +PROT_UrlCryptoKeyManager.prototype.dropKey = function() { + this.rekeyTries_ = 0; + this.replaceKey_(null, null); +} + /** * @returns Boolean indicating if we have a key we can use */ -PROT_UrlCryptoKeyManager.prototype.hasKey_ = function() { +PROT_UrlCryptoKeyManager.prototype.hasKey = function() { return this.clientKey_ != null && this.wrappedKey_ != null; } @@ -258,6 +278,10 @@ PROT_UrlCryptoKeyManager.prototype.replaceKey_ = function(clientKey, this.wrappedKey_ = wrappedKey; this.serializeKey_(this.clientKey_, this.wrappedKey_); + + if (this.onNewKey_) { + this.onNewKey_(); + } } /** @@ -278,6 +302,12 @@ PROT_UrlCryptoKeyManager.prototype.serializeKey_ = function() { .getService(Ci.nsIProperties) .get("ProfD", Ci.nsILocalFile); /* profile directory */ keyfile.append(this.keyFilename_); + + if (!this.clientKey_ || !this.wrappedKey_) { + keyfile.remove(true); + return; + } + var data = (new G_Protocol4Parser()).serialize(map); try { @@ -312,14 +342,26 @@ PROT_UrlCryptoKeyManager.prototype.onGetKeyResponse = function(responseText) { var clientKey = response[this.CLIENT_KEY_NAME]; var wrappedKey = response[this.WRAPPED_KEY_NAME]; + this.updating_ = false; + if (response && clientKey && wrappedKey) { G_Debug(this, "Got new key from: " + responseText); this.replaceKey_(clientKey, wrappedKey); } else { - G_Debug(this, "Not a valid response for /getkey"); + G_Debug(this, "Not a valid response for /newkey"); } } +/** + * Set the callback to be called whenever we get a new key. + * + * @param callback The callback. + */ +PROT_UrlCryptoKeyManager.prototype.onNewKey = function(callback) +{ + this.onNewKey_ = callback; +} + /** * Attempt to read a key we've previously serialized from disk, so * that we can fall back on it in case we can't get one from the @@ -365,7 +407,7 @@ PROT_UrlCryptoKeyManager.prototype.maybeLoadOldKey = function() { var clientKey = oldKey[this.CLIENT_KEY_NAME]; var wrappedKey = oldKey[this.WRAPPED_KEY_NAME]; - if (oldKey && clientKey && wrappedKey && !this.hasKey_()) { + if (oldKey && clientKey && wrappedKey && !this.hasKey()) { G_Debug(this, "Read old key from disk."); this.replaceKey_(clientKey, wrappedKey); } @@ -401,21 +443,21 @@ function TEST_PROT_UrlCryptoKeyManager() { // CASE: simulate nothing on disk, then get something from server - G_Assert(z, !km.hasKey_(), "KM already has key?"); + G_Assert(z, !km.hasKey(), "KM already has key?"); km.maybeLoadOldKey(); - G_Assert(z, !km.hasKey_(), "KM loaded non-existent key?"); + G_Assert(z, !km.hasKey(), "KM loaded non-existent key?"); km.onGetKeyResponse(null); - G_Assert(z, !km.hasKey_(), "KM got key from null response?"); + G_Assert(z, !km.hasKey(), "KM got key from null response?"); km.onGetKeyResponse(""); - G_Assert(z, !km.hasKey_(), "KM got key from empty response?"); + G_Assert(z, !km.hasKey(), "KM got key from empty response?"); km.onGetKeyResponse("aslkaslkdf:34:a230\nskdjfaljsie"); - G_Assert(z, !km.hasKey_(), "KM got key from garbage response?"); + G_Assert(z, !km.hasKey(), "KM got key from garbage response?"); var realResponse = "clientkey:24:zGbaDbx1pxoYe7siZYi8VA==\n" + "wrappedkey:24:MTr1oDt6TSOFQDTvKCWz9PEn"; km.onGetKeyResponse(realResponse); // Will have written it to file as a side effect - G_Assert(z, km.hasKey_(), "KM couldn't get key from real response?"); + G_Assert(z, km.hasKey(), "KM couldn't get key from real response?"); G_Assert(z, km.clientKey_ == "zGbaDbx1pxoYe7siZYi8VA==", "Parsed wrong client key from response?"); G_Assert(z, km.wrappedKey_ == "MTr1oDt6TSOFQDTvKCWz9PEn", @@ -424,9 +466,9 @@ function TEST_PROT_UrlCryptoKeyManager() { // CASE: simulate something on disk, then get something from server km = new PROT_UrlCryptoKeyManager(kf, true /* testing */); - G_Assert(z, !km.hasKey_(), "KM already has key?"); + G_Assert(z, !km.hasKey(), "KM already has key?"); km.maybeLoadOldKey(); - G_Assert(z, km.hasKey_(), "KM couldn't load existing key from disk?"); + G_Assert(z, km.hasKey(), "KM couldn't load existing key from disk?"); G_Assert(z, km.clientKey_ == "zGbaDbx1pxoYe7siZYi8VA==", "Parsed wrong client key from disk?"); G_Assert(z, km.wrappedKey_ == "MTr1oDt6TSOFQDTvKCWz9PEn", @@ -435,18 +477,18 @@ function TEST_PROT_UrlCryptoKeyManager() { "wrappedkey:24:MTpPH3pnLDKihecOci+0W5dk"; km.onGetKeyResponse(realResponse2); // Will have written it to disk - G_Assert(z, km.hasKey_(), "KM couldn't replace key from server response?"); + G_Assert(z, km.hasKey(), "KM couldn't replace key from server response?"); G_Assert(z, km.clientKey_ == "dtmbEN1kgN/LmuEoYifaFw==", "Replace client key from server failed?"); - G_Assert(z, km.wrappedKey_ == "MTpPH3pnLDKihecOci+0W5dk", + G_Assert(z, km.wrappedKey == "MTpPH3pnLDKihecOci+0W5dk", "Replace wrapped key from server failed?"); // CASE: check overwriting a key on disk km = new PROT_UrlCryptoKeyManager(kf, true /* testing */); - G_Assert(z, !km.hasKey_(), "KM already has key?"); + G_Assert(z, !km.hasKey(), "KM already has key?"); km.maybeLoadOldKey(); - G_Assert(z, km.hasKey_(), "KM couldn't load existing key from disk?"); + G_Assert(z, km.hasKey(), "KM couldn't load existing key from disk?"); G_Assert(z, km.clientKey_ == "dtmbEN1kgN/LmuEoYifaFw==", "Replace client on from disk failed?"); G_Assert(z, km.wrappedKey_ == "MTpPH3pnLDKihecOci+0W5dk", diff --git a/toolkit/components/url-classifier/content/url-crypto.js b/toolkit/components/url-classifier/content/url-crypto.js deleted file mode 100644 index cac07c76cb53..000000000000 --- a/toolkit/components/url-classifier/content/url-crypto.js +++ /dev/null @@ -1,707 +0,0 @@ -# ***** 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 (original author) -# Monica Chew -# -# 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.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_ = Array.map(this.separator_, - function(c) { return c.charCodeAt(0); }); -} - -// 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 stream = Cc['@mozilla.org/io/string-input-stream;1'] - .createInstance(Ci.nsIStringInputStream); - stream.setData(s, s.length); - if (stream.available() > 0) - this.macer_.updateFromStream(stream); -} - -/** - * 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); - - var stream = Cc['@mozilla.org/io/string-input-stream;1'] - .createInstance(Ci.nsIStringInputStream); - stream.setData(data, data.length); - if (stream.available() > 0) - this.macer_.updateFromStream(stream); - - 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 file = Cc["@mozilla.org/file/directory_service;1"] - .getService(Ci.nsIProperties) - .get("ProfD", Ci.nsILocalFile); /* profile directory */ - 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 - - // 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; - - // Helper function to arrayify string. - function toCharCode(c) { - return c.charCodeAt(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 counts, 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 = Array.map(payload, toCharCode); - var plaintext2 = Array.map(payload, toCharCode); - var plaintext3 = Array.map(payload, toCharCode); - - // 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 b64arr = Array.map(atob(ciphertext2), toCharCode); - var ciphertext3 = c.encryptV1(clientKeyArray, - "1", - count, - b64arr, - true /* websafe */); - - // note: ciphertext3 was websafe - reverting to plain base64 first - var b64str = atob(ciphertext3).replace(/-/g, "+").replace(/_/g, "/"); - b64arr = Array.map(b64str, toCharCode) - G_Assert(z, arrayEquals(plaintext3, b64arr), - "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 clientKey = "dtmbEN1kgN/LmuEoYifaFw=="; - var clientKeyArray = Array.map(atob(clientKey), toCharCode); - // 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 = Array.map(plaintext.slice(0, i), toCharCode); - 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 diff --git a/toolkit/components/url-classifier/public/nsIUrlClassifierDBService.idl b/toolkit/components/url-classifier/public/nsIUrlClassifierDBService.idl index 010fc03f3fec..b296e9f6caca 100644 --- a/toolkit/components/url-classifier/public/nsIUrlClassifierDBService.idl +++ b/toolkit/components/url-classifier/public/nsIUrlClassifierDBService.idl @@ -57,7 +57,7 @@ interface nsIUrlClassifierCallback : nsISupports { * clients streaming updates to the url-classifier (usually * nsUrlClassifierStreamUpdater. */ -[scriptable, uuid(bb0528b3-71e2-4795-8732-d60a4476e6df)] +[scriptable, uuid(1c9bd1c2-f6fe-43a8-a2b9-48359eb4a9b1)] interface nsIUrlClassifierUpdateObserver : nsISupports { /** * The update requested a new URL whose contents should be downloaded @@ -65,9 +65,19 @@ interface nsIUrlClassifierUpdateObserver : nsISupports { * * @param url The url that was requested. * @param table The table name that this URL's contents will be associated - * with. + * with. This should be passed back to beginStream(). + * @param serverMAC The server-supplied MAC of the data at this URL. This + * should be passed back to beginStream(). */ - void updateUrlRequested(in ACString url, in ACString table); + void updateUrlRequested(in ACString url, + in ACString table, + in ACString serverMAC); + + /** + * The server has requested that the client get a new client key for + * MAC requests. + */ + void rekeyRequested(); /** * A stream update has completed. @@ -93,7 +103,7 @@ interface nsIUrlClassifierUpdateObserver : nsISupports { * It provides async methods for querying and updating the database. As the * methods complete, they call the callback function. */ -[scriptable, uuid(bcc32b18-78be-49f6-a895-a1a341a9e94b)] +[scriptable, uuid(aaf2c3b2-b7cd-4368-9d46-3fe92e5f78b1)] interface nsIUrlClassifierDBService : nsISupports { /** @@ -150,8 +160,13 @@ interface nsIUrlClassifierDBService : nsISupports /** * Begin an update process. Will throw NS_ERROR_NOT_AVAILABLE if there * is already an update in progress. + * + * @param updater The update observer tied to this update. + * @param clientKey The client key for calculating an update's MAC, + * or empty to ignore MAC. */ - void beginUpdate(in nsIUrlClassifierUpdateObserver updater); + void beginUpdate(in nsIUrlClassifierUpdateObserver updater, + in ACString clientKey); /** * Begin a stream update. This should be called once per url being @@ -159,8 +174,12 @@ interface nsIUrlClassifierDBService : nsISupports * * @param table The table the contents of this stream will be associated * with, or empty for the initial stream. + * @param serverMAC The MAC specified by the update server for this stream. + * If the server has not specified a MAC (which is the case + * for the initial stream), this will be empty. */ - void beginStream(in ACString table); + void beginStream(in ACString table, + in ACString serverMAC); /** * Update the table incrementally. diff --git a/toolkit/components/url-classifier/public/nsIUrlClassifierHashCompleter.idl b/toolkit/components/url-classifier/public/nsIUrlClassifierHashCompleter.idl index f6ad65cb45c8..22f45151d29d 100644 --- a/toolkit/components/url-classifier/public/nsIUrlClassifierHashCompleter.idl +++ b/toolkit/components/url-classifier/public/nsIUrlClassifierHashCompleter.idl @@ -54,10 +54,13 @@ interface nsIUrlClassifierHashCompleterCallback : nsISupports * The name of the table that this hash belongs to. * @param chunkId * The database chunk that this hash belongs to. + * @param trusted + * The completion was verified with a MAC and can be cached. */ void completion(in ACString hash, in ACString table, - in PRUint32 chunkId); + in PRUint32 chunkId, + in PRBool trusted); /** * The completion is complete. This method is called once per @@ -77,7 +80,7 @@ interface nsIUrlClassifierHashCompleterCallback : nsISupports * an nsIUrlClassifierCompleter instance to asynchronously provide the * complete hash, along with some associated metadata. */ -[scriptable, uuid(1a3c19d9-ccd6-4d1a-a48a-1ab662e56e60)] +[scriptable, uuid(ade9b72b-3562-44f5-aba6-e63246be53ae)] interface nsIUrlClassifierHashCompleter : nsISupports { /** @@ -91,9 +94,13 @@ interface nsIUrlClassifierHashCompleter : nsISupports void complete(in ACString partialHash, in nsIUrlClassifierHashCompleterCallback callback); + /** + * Set the client and wrapped key for verified updates. + */ + void setKeys(in ACString clientKey, in ACString wrappedKey); + /** * The URL for the gethash request */ attribute ACString gethashUrl; }; - diff --git a/toolkit/components/url-classifier/public/nsIUrlClassifierStreamUpdater.idl b/toolkit/components/url-classifier/public/nsIUrlClassifierStreamUpdater.idl index 1e58e58d2a88..5303daefe0c0 100644 --- a/toolkit/components/url-classifier/public/nsIUrlClassifierStreamUpdater.idl +++ b/toolkit/components/url-classifier/public/nsIUrlClassifierStreamUpdater.idl @@ -44,7 +44,7 @@ * downloading the whole update and then updating the sqlite database, we * update tables as the data is streaming in. */ -[scriptable, uuid(adf0dfaa-ce91-4cf2-ab15-f5810408e2ec)] +[scriptable, uuid(1a1f8b01-4221-4897-b030-9d301b8b8cc9)] interface nsIUrlClassifierStreamUpdater : nsISupports { /** @@ -57,12 +57,14 @@ interface nsIUrlClassifierStreamUpdater : nsISupports * runs at a time, so we return false if another instance is already * running. * @param aRequestBody The body for the request. + * @param aClientKey The client key for checking the update's MAC. * @param aSuccessCallback Called after a successful update. * @param aUpdateErrorCallback Called for problems applying the update * @param aDownloadErrorCallback Called if we get an http error or a * connection refused error. */ boolean downloadUpdates(in ACString aRequestBody, + in ACString aClientKey, in nsIUrlClassifierCallback aSuccessCallback, in nsIUrlClassifierCallback aUpdateErrorCallback, in nsIUrlClassifierCallback aDownloadErrorCallback); diff --git a/toolkit/components/url-classifier/src/nsUrlClassifierDBService.cpp b/toolkit/components/url-classifier/src/nsUrlClassifierDBService.cpp index 130be76c8314..8eed3aa79e5d 100644 --- a/toolkit/components/url-classifier/src/nsUrlClassifierDBService.cpp +++ b/toolkit/components/url-classifier/src/nsUrlClassifierDBService.cpp @@ -49,6 +49,7 @@ #include "nsAutoLock.h" #include "nsCRT.h" #include "nsICryptoHash.h" +#include "nsICryptoHMAC.h" #include "nsIDirectoryService.h" #include "nsIObserverService.h" #include "nsIPrefBranch.h" @@ -59,6 +60,7 @@ #include "nsToolkitCompsCID.h" #include "nsIUrlClassifierUtils.h" #include "nsUrlClassifierDBService.h" +#include "nsUrlClassifierUtils.h" #include "nsURILoader.h" #include "nsString.h" #include "nsTArray.h" @@ -1156,6 +1158,13 @@ private: PRBool mHaveCachedSubChunks; nsTArray mCachedSubChunks; + // The client key with which the data from the server will be MAC'ed. + nsCString mUpdateClientKey; + + // The MAC stated by the server. + nsCString mServerMAC; + + nsCOMPtr mHMAC; // The number of noise entries to add to the set of lookup results. PRInt32 mGethashNoise; @@ -2453,23 +2462,35 @@ nsUrlClassifierDBServiceWorker::ProcessResponseLines(PRBool* done) LOG(("Processing %s\n", PromiseFlatCString(line).get())); - if (StringBeginsWith(line, NS_LITERAL_CSTRING("n:"))) { + if (mHMAC && mServerMAC.IsEmpty()) { + // If we did not receive a server MAC during BeginStream(), we + // require the first line of the update to be either a MAC or + // a request to rekey. + if (StringBeginsWith(line, NS_LITERAL_CSTRING("m:"))) { + mServerMAC = Substring(line, 2); + nsUrlClassifierUtils::UnUrlsafeBase64(mServerMAC); + + // The remainder of the pending update needs to be digested. + const nsCSubstring &toDigest = Substring(updateString, cur); + rv = mHMAC->Update(reinterpret_cast(toDigest.BeginReading()), + toDigest.Length()); + NS_ENSURE_SUCCESS(rv, rv); + } else if (line.EqualsLiteral("e:pleaserekey")) { + mUpdateObserver->RekeyRequested(); + } else { + LOG(("No MAC specified!")); + return NS_ERROR_FAILURE; + } + } else if (StringBeginsWith(line, NS_LITERAL_CSTRING("n:"))) { if (PR_sscanf(PromiseFlatCString(line).get(), "n:%d", &mUpdateWait) != 1) { LOG(("Error parsing n: field: %s", PromiseFlatCString(line).get())); mUpdateWait = 0; } - } else if (StringBeginsWith(line, NS_LITERAL_CSTRING("k:"))) { - // XXX: pleaserekey + } else if (line.EqualsLiteral("e:pleaserekey")) { + mUpdateObserver->RekeyRequested(); } else if (StringBeginsWith(line, NS_LITERAL_CSTRING("i:"))) { - const nsCSubstring& data = Substring(line, 2); - PRInt32 comma; - if ((comma = data.FindChar(',')) == kNotFound) { - mUpdateTable = data; - } else { - mUpdateTable = Substring(data, 0, comma); - // The rest is the mac, which we don't support for now - } + mUpdateTable.Assign(Substring(line, 2)); GetTableId(mUpdateTable, &mUpdateTableId); LOG(("update table: '%s' (%d)", mUpdateTable.get(), mUpdateTableId)); } else if (StringBeginsWith(line, NS_LITERAL_CSTRING("u:"))) { @@ -2479,13 +2500,29 @@ nsUrlClassifierDBServiceWorker::ProcessResponseLines(PRBool* done) } const nsCSubstring& data = Substring(line, 2); - PRInt32 space; - if ((space = data.FindChar(' ')) == kNotFound) { - mUpdateObserver->UpdateUrlRequested(data, mUpdateTable); + if (mHMAC) { + // We're expecting MACs alongside any url forwards. + nsCSubstring::const_iterator begin, end, sepBegin, sepEnd; + data.BeginReading(begin); + sepBegin = begin; + + data.EndReading(end); + sepEnd = end; + + if (!RFindInReadable(NS_LITERAL_CSTRING(","), sepBegin, sepEnd)) { + NS_WARNING("No MAC specified for a redirect in a request that expects a MAC"); + return NS_ERROR_FAILURE; + } + + nsCString serverMAC(Substring(sepEnd, end)); + nsUrlClassifierUtils::UnUrlsafeBase64(serverMAC); + mUpdateObserver->UpdateUrlRequested(Substring(begin, sepBegin), + mUpdateTable, + serverMAC); } else { - mUpdateObserver->UpdateUrlRequested(Substring(data, 0, space), - mUpdateTable); - // The rest is the mac, which we don't support for now + // We didn't ask for a MAC, none should have been specified. + mUpdateObserver->UpdateUrlRequested(data, mUpdateTable, + NS_LITERAL_CSTRING("")); } } else if (StringBeginsWith(line, NS_LITERAL_CSTRING("a:")) || StringBeginsWith(line, NS_LITERAL_CSTRING("s:"))) { @@ -2563,6 +2600,8 @@ nsUrlClassifierDBServiceWorker::ResetStream() mPrimaryStream = PR_FALSE; mUpdateTable.Truncate(); mPendingStreamUpdate.Truncate(); + mServerMAC.Truncate(); + mHMAC = nsnull; } void @@ -2571,6 +2610,7 @@ nsUrlClassifierDBServiceWorker::ResetUpdate() mUpdateWait = 0; mUpdateStatus = NS_OK; mUpdateObserver = nsnull; + mUpdateClientKey.Truncate(); } NS_IMETHODIMP @@ -2581,7 +2621,8 @@ nsUrlClassifierDBServiceWorker::SetHashCompleter(const nsACString &tableName, } NS_IMETHODIMP -nsUrlClassifierDBServiceWorker::BeginUpdate(nsIUrlClassifierUpdateObserver *observer) +nsUrlClassifierDBServiceWorker::BeginUpdate(nsIUrlClassifierUpdateObserver *observer, + const nsACString &clientKey) { if (gShuttingDownThread) return NS_ERROR_NOT_INITIALIZED; @@ -2615,6 +2656,11 @@ nsUrlClassifierDBServiceWorker::BeginUpdate(nsIUrlClassifierUpdateObserver *obse mUpdateObserver = observer; + if (!clientKey.IsEmpty()) { + rv = nsUrlClassifierUtils::DecodeClientKey(clientKey, mUpdateClientKey); + NS_ENSURE_SUCCESS(rv, rv); + } + // The first stream in an update is the only stream that may request // forwarded updates. mPrimaryStream = PR_TRUE; @@ -2623,7 +2669,8 @@ nsUrlClassifierDBServiceWorker::BeginUpdate(nsIUrlClassifierUpdateObserver *obse } NS_IMETHODIMP -nsUrlClassifierDBServiceWorker::BeginStream(const nsACString &table) +nsUrlClassifierDBServiceWorker::BeginStream(const nsACString &table, + const nsACString &serverMAC) { if (gShuttingDownThread) return NS_ERROR_NOT_INITIALIZED; @@ -2633,6 +2680,30 @@ nsUrlClassifierDBServiceWorker::BeginStream(const nsACString &table) mInStream = PR_TRUE; + nsresult rv; + + // If we're expecting a MAC, create the nsICryptoHMAC component now. + if (!mUpdateClientKey.IsEmpty()) { + mHMAC = do_CreateInstance(NS_CRYPTO_HMAC_CONTRACTID, &rv); + if (NS_FAILED(rv)) { + NS_WARNING("Failed to create nsICryptoHMAC instance"); + mUpdateStatus = rv; + return mUpdateStatus; + } + NS_ENSURE_SUCCESS(rv, rv); + + rv = mHMAC->Init(nsICryptoHMAC::SHA1, + reinterpret_cast(mUpdateClientKey.BeginReading()), + mUpdateClientKey.Length()); + if (NS_FAILED(rv)) { + NS_WARNING("Failed to initialize nsICryptoHMAC instance"); + mUpdateStatus = rv; + return mUpdateStatus; + } + } + + mServerMAC = serverMAC; + if (!table.IsEmpty()) { mUpdateTable = table; GetTableId(mUpdateTable, &mUpdateTableId); @@ -2695,6 +2766,15 @@ nsUrlClassifierDBServiceWorker::UpdateStream(const nsACString& chunk) return mUpdateStatus; } + if (mHMAC && !mServerMAC.IsEmpty()) { + rv = mHMAC->Update(reinterpret_cast(chunk.BeginReading()), + chunk.Length()); + if (NS_FAILED(rv)) { + mUpdateStatus = rv; + return mUpdateStatus; + } + } + LOG(("Got %s\n", PromiseFlatCString(chunk).get())); mPendingStreamUpdate.Append(chunk); @@ -2724,6 +2804,18 @@ nsUrlClassifierDBServiceWorker::FinishStream() NS_ENSURE_STATE(mInStream); NS_ENSURE_STATE(mUpdateObserver); + if (NS_SUCCEEDED(mUpdateStatus) && mHMAC) { + nsCAutoString clientMAC; + mHMAC->Finish(PR_TRUE, clientMAC); + + if (clientMAC != mServerMAC) { + NS_WARNING("Invalid update MAC!"); + LOG(("Invalid update MAC: expected %s, got %s", + mServerMAC.get(), clientMAC.get())); + mUpdateStatus = NS_ERROR_FAILURE; + } + } + mUpdateObserver->StreamFinished(mUpdateStatus); ResetStream(); @@ -3147,9 +3239,10 @@ nsUrlClassifierLookupCallback::CompletionFinished(nsresult status) NS_IMETHODIMP nsUrlClassifierLookupCallback::Completion(const nsACString& completeHash, const nsACString& tableName, - PRUint32 chunkId) + PRUint32 chunkId, + PRBool verified) { - LOG(("nsUrlClassifierLookupCallback::Completion [%p]", this)); + LOG(("nsUrlClassifierLookupCallback::Completion [%p, %d]", this, verified)); nsUrlClassifierCompleteHash hash; hash.Assign(completeHash); @@ -3172,13 +3265,17 @@ nsUrlClassifierLookupCallback::Completion(const nsACString& completeHash, if (result.mLookupFragment == hash) result.mConfirmed = PR_TRUE; - if (!mCacheResults) { - mCacheResults = new nsTArray(); - if (!mCacheResults) - return NS_ERROR_OUT_OF_MEMORY; - } + // If this result is guaranteed to come from our list provider, + // we can cache the results. + if (verified) { + if (!mCacheResults) { + mCacheResults = new nsTArray(); + if (!mCacheResults) + return NS_ERROR_OUT_OF_MEMORY; + } - mCacheResults->AppendElement(result); + mCacheResults->AppendElement(result); + } } } @@ -3520,7 +3617,8 @@ nsUrlClassifierDBService::SetHashCompleter(const nsACString &tableName, } NS_IMETHODIMP -nsUrlClassifierDBService::BeginUpdate(nsIUrlClassifierUpdateObserver *observer) +nsUrlClassifierDBService::BeginUpdate(nsIUrlClassifierUpdateObserver *observer, + const nsACString &clientKey) { NS_ENSURE_TRUE(gDbBackgroundThread, NS_ERROR_NOT_INITIALIZED); @@ -3540,15 +3638,16 @@ nsUrlClassifierDBService::BeginUpdate(nsIUrlClassifierUpdateObserver *observer) getter_AddRefs(proxyObserver)); NS_ENSURE_SUCCESS(rv, rv); - return mWorkerProxy->BeginUpdate(proxyObserver); + return mWorkerProxy->BeginUpdate(proxyObserver, clientKey); } NS_IMETHODIMP -nsUrlClassifierDBService::BeginStream(const nsACString &table) +nsUrlClassifierDBService::BeginStream(const nsACString &table, + const nsACString &serverMAC) { NS_ENSURE_TRUE(gDbBackgroundThread, NS_ERROR_NOT_INITIALIZED); - return mWorkerProxy->BeginStream(table); + return mWorkerProxy->BeginStream(table, serverMAC); } NS_IMETHODIMP diff --git a/toolkit/components/url-classifier/src/nsUrlClassifierHashCompleter.cpp b/toolkit/components/url-classifier/src/nsUrlClassifierHashCompleter.cpp index 69576b5a8968..66ad464e82f8 100644 --- a/toolkit/components/url-classifier/src/nsUrlClassifierHashCompleter.cpp +++ b/toolkit/components/url-classifier/src/nsUrlClassifierHashCompleter.cpp @@ -38,6 +38,7 @@ #include "nsUrlClassifierHashCompleter.h" #include "nsIChannel.h" +#include "nsICryptoHMAC.h" #include "nsIHttpChannel.h" #include "nsIObserverService.h" #include "nsIUploadChannel.h" @@ -47,6 +48,7 @@ #include "nsServiceManagerUtils.h" #include "nsThreadUtils.h" #include "nsUrlClassifierDBService.h" +#include "nsUrlClassifierUtils.h" #include "prlog.h" #include "prprf.h" @@ -172,6 +174,84 @@ nsUrlClassifierHashCompleterRequest::AddRequestBody(const nsACString &aRequestBo return NS_OK; } +void +nsUrlClassifierHashCompleterRequest::RescheduleItems() +{ + // This request has failed in a way that we expect might succeed if + // we try again. Schedule the individual hashes for another attempt. + for (PRUint32 i = 0; i < mRequests.Length(); i++) { + Request &request = mRequests[i]; + nsresult rv = mCompleter->Complete(request.partialHash, request.callback); + if (NS_FAILED(rv)) { + // We couldn't reschedule the request - the best we can do here is + // tell it that we failed to complete the request. + request.callback->CompletionFinished(rv); + } + } + + mRescheduled = PR_TRUE; +} + +/** + * Reads the MAC from the response and checks it against the + * locally-computed MAC. + */ +nsresult +nsUrlClassifierHashCompleterRequest::HandleMAC(nsACString::const_iterator& begin, + const nsACString::const_iterator& end) +{ + mVerified = PR_FALSE; + + // First line should be either the MAC or a k:pleaserekey request. + nsACString::const_iterator iter = begin; + if (!FindCharInReadable('\n', iter, end)) { + return NS_ERROR_FAILURE; + } + + nsCAutoString serverMAC(Substring(begin, iter++)); + begin = iter; + + if (serverMAC.EqualsLiteral("e:pleaserekey")) { + LOG(("Rekey requested")); + + // Reschedule our items to be requested again. + RescheduleItems(); + + // Let the hash completer know that we need a new key. + return mCompleter->RekeyRequested(); + } + + nsUrlClassifierUtils::UnUrlsafeBase64(serverMAC); + + nsresult rv; + nsCOMPtr hmac = + do_CreateInstance(NS_CRYPTO_HMAC_CONTRACTID, &rv); + NS_ENSURE_SUCCESS(rv, rv); + + rv = hmac->Init(nsICryptoHMAC::SHA1, + reinterpret_cast(mClientKey.BeginReading()), + mClientKey.Length()); + NS_ENSURE_SUCCESS(rv, rv); + + const nsCSubstring &remaining = Substring(begin, end); + rv = hmac->Update(reinterpret_cast(remaining.BeginReading()), + remaining.Length()); + NS_ENSURE_SUCCESS(rv, rv); + + nsCAutoString clientMAC; + rv = hmac->Finish(PR_TRUE, clientMAC); + NS_ENSURE_SUCCESS(rv, rv); + + if (clientMAC != serverMAC) { + NS_WARNING("Invalid MAC in gethash response."); + return NS_ERROR_FAILURE; + } + + mVerified = PR_TRUE; + + return NS_OK; +} + nsresult nsUrlClassifierHashCompleterRequest::HandleItem(const nsACString& item, const nsACString& tableName, @@ -194,18 +274,16 @@ nsUrlClassifierHashCompleterRequest::HandleItem(const nsACString& item, return NS_OK; } - /** * Reads one table of results from the response. Leaves begin pointing at the * next table. */ nsresult -nsUrlClassifierHashCompleterRequest::HandleTable(const nsACString& response, - nsACString::const_iterator& begin) +nsUrlClassifierHashCompleterRequest::HandleTable(nsACString::const_iterator& begin, + const nsACString::const_iterator& end) { - nsACString::const_iterator iter, end; + nsACString::const_iterator iter; iter = begin; - response.EndReading(end); if (!FindCharInReadable(':', iter, end)) { // No table line. NS_WARNING("Received badly-formatted gethash response."); @@ -273,8 +351,22 @@ nsUrlClassifierHashCompleterRequest::HandleResponse() mResponse.BeginReading(begin); mResponse.EndReading(end); + nsresult rv; + + // If we have a client key, we're expecting a MAC. + if (!mClientKey.IsEmpty()) { + rv = HandleMAC(begin, end); + NS_ENSURE_SUCCESS(rv, rv); + + if (mRescheduled) { + // We were rescheduled due to a k:pleaserekey request from the + // server. Don't bother reading the rest of the response. + return NS_OK; + } + } + while (begin != end) { - nsresult rv = HandleTable(mResponse, begin); + rv = HandleTable(begin, end); NS_ENSURE_SUCCESS(rv, rv); } @@ -293,7 +385,8 @@ nsUrlClassifierHashCompleterRequest::NotifySuccess() Response &response = request.responses[j]; request.callback->Completion(response.completeHash, response.tableName, - response.chunkId); + response.chunkId, + mVerified); } request.callback->CompletionFinished(NS_OK); @@ -370,10 +463,13 @@ nsUrlClassifierHashCompleterRequest::OnStopRequest(nsIRequest *request, if (NS_SUCCEEDED(status)) status = HandleResponse(); - if (NS_SUCCEEDED(status)) - NotifySuccess(); - else - NotifyFailure(status); + // If we were rescheduled, don't bother notifying success or failure. + if (!mRescheduled) { + if (NS_SUCCEEDED(status)) + NotifySuccess(); + else + NotifyFailure(status); + } mChannel = nsnull; @@ -426,15 +522,10 @@ nsUrlClassifierHashCompleter::Complete(const nsACString &partialHash, if (mShuttingDown) return NS_ERROR_NOT_INITIALIZED; - if (!mURI) { - NS_WARNING("Trying to use nsUrlClassifierHashCompleter without setting the gethash URI."); - return NS_ERROR_NOT_INITIALIZED; - } - // We batch all of the requested completions in a single request until the // next time we reach the main loop. if (!mRequest) { - mRequest = new nsUrlClassifierHashCompleterRequest(mURI); + mRequest = new nsUrlClassifierHashCompleterRequest(this); if (!mRequest) { return NS_ERROR_OUT_OF_MEMORY; } @@ -449,16 +540,36 @@ nsUrlClassifierHashCompleter::Complete(const nsACString &partialHash, NS_IMETHODIMP nsUrlClassifierHashCompleter::SetGethashUrl(const nsACString &url) { - return NS_NewURI(getter_AddRefs(mURI), url); + mGethashUrl = url; + return NS_OK; } NS_IMETHODIMP nsUrlClassifierHashCompleter::GetGethashUrl(nsACString &url) { - url.Truncate(); - if (mURI) { - return mURI->GetSpec(url); + url = mGethashUrl; + return NS_OK; +} + +NS_IMETHODIMP +nsUrlClassifierHashCompleter::SetKeys(const nsACString &clientKey, + const nsACString &wrappedKey) +{ + LOG(("nsUrlClassifierHashCompleter::SetKeys [%p]", this)); + + NS_ASSERTION(clientKey.IsEmpty() == wrappedKey.IsEmpty(), + "Must either have both a client key and a wrapped key or neither."); + + if (clientKey.IsEmpty()) { + mClientKey.Truncate(); + mWrappedKey.Truncate(); + return NS_OK; } + + nsresult rv = nsUrlClassifierUtils::DecodeClientKey(clientKey, mClientKey); + NS_ENSURE_SUCCESS(rv, rv); + mWrappedKey = wrappedKey; + return NS_OK; } @@ -475,8 +586,28 @@ nsUrlClassifierHashCompleter::Run() if (!mRequest) return NS_OK; + NS_ASSERTION(!mGethashUrl.IsEmpty(), + "Request dispatched without a gethash url specified."); + + nsCOMPtr uri; + nsresult rv; + if (mClientKey.IsEmpty()) { + rv = NS_NewURI(getter_AddRefs(uri), mGethashUrl); + NS_ENSURE_SUCCESS(rv, rv); + } else { + mRequest->SetClientKey(mClientKey); + + nsCAutoString requestURL(mGethashUrl); + requestURL.Append("&wrkey="); + requestURL.Append(mWrappedKey); + rv = NS_NewURI(getter_AddRefs(uri), requestURL); + NS_ENSURE_SUCCESS(rv, rv); + } + + mRequest->SetURI(uri); + // Dispatch the http request. - nsresult rv = mRequest->Begin(); + rv = mRequest->Begin(); mRequest = nsnull; return rv; } @@ -491,3 +622,25 @@ nsUrlClassifierHashCompleter::Observe(nsISupports *subject, const char *topic, return NS_OK; } + +nsresult +nsUrlClassifierHashCompleter::RekeyRequested() +{ + // Our keys are no longer valid. + SetKeys(EmptyCString(), EmptyCString()); + + // Notify the key manager that we need a new key. Until we get a + // new key, gethash requests will be unauthenticated (and therefore + // uncacheable). + nsresult rv; + nsCOMPtr observerService = + do_GetService("@mozilla.org/observer-service;1", &rv); + NS_ENSURE_SUCCESS(rv, rv); + + rv = observerService->NotifyObservers(static_cast(this), + "url-classifier-rekey-requested", + nsnull); + NS_ENSURE_SUCCESS(rv, rv); + + return NS_OK; +} diff --git a/toolkit/components/url-classifier/src/nsUrlClassifierHashCompleter.h b/toolkit/components/url-classifier/src/nsUrlClassifierHashCompleter.h index 4a193cdd46f6..b3b23f4f393c 100644 --- a/toolkit/components/url-classifier/src/nsUrlClassifierHashCompleter.h +++ b/toolkit/components/url-classifier/src/nsUrlClassifierHashCompleter.h @@ -52,6 +52,8 @@ #include "nsString.h" #include "nsWeakReference.h" +class nsUrlClassifierHashCompleter; + class nsUrlClassifierHashCompleterRequest : public nsIStreamListener , public nsIObserver { @@ -61,32 +63,45 @@ public: NS_DECL_NSISTREAMLISTENER NS_DECL_NSIOBSERVER - nsUrlClassifierHashCompleterRequest(nsIURI *uri) + nsUrlClassifierHashCompleterRequest(nsUrlClassifierHashCompleter *completer) : mShuttingDown(PR_FALSE) - , mURI(uri) { } + , mCompleter(completer) + , mVerified(PR_FALSE) + , mRescheduled(PR_FALSE) + { } ~nsUrlClassifierHashCompleterRequest() { } nsresult Begin(); nsresult Add(const nsACString &partialHash, nsIUrlClassifierHashCompleterCallback *c); + void SetURI(nsIURI *uri) { mURI = uri; } + void SetClientKey(const nsACString &clientKey) { mClientKey = clientKey; } + private: nsresult OpenChannel(); nsresult BuildRequest(nsCAutoString &request); nsresult AddRequestBody(const nsACString &requestBody); + void RescheduleItems(); + nsresult HandleMAC(nsACString::const_iterator &begin, + const nsACString::const_iterator &end); nsresult HandleItem(const nsACString &item, const nsACString &table, PRUint32 chunkId); - nsresult HandleTable(const nsACString &response, - nsACString::const_iterator &begin); + nsresult HandleTable(nsACString::const_iterator &begin, + const nsACString::const_iterator &end); nsresult HandleResponse(); void NotifySuccess(); void NotifyFailure(nsresult status); PRBool mShuttingDown; + nsRefPtr mCompleter; nsCOMPtr mURI; + nsCString mClientKey; nsCOMPtr mChannel; nsCString mResponse; + PRBool mVerified; + PRBool mRescheduled; struct Response { nsCString completeHash; @@ -114,16 +129,22 @@ public: NS_DECL_NSIRUNNABLE NS_DECL_NSIOBSERVER - nsUrlClassifierHashCompleter() : mShuttingDown(PR_FALSE) {} + nsUrlClassifierHashCompleter() + : mShuttingDown(PR_FALSE) + {} ~nsUrlClassifierHashCompleter() {} nsresult Init(); + nsresult RekeyRequested(); + private: nsRefPtr mRequest; - nsCOMPtr mURI; - PRBool mShuttingDown; + nsCString mGethashUrl; + nsCString mClientKey; + nsCString mWrappedKey; + PRBool mShuttingDown; }; #endif // nsUrlClassifierHashCompleter_h_ diff --git a/toolkit/components/url-classifier/src/nsUrlClassifierLib.js b/toolkit/components/url-classifier/src/nsUrlClassifierLib.js index 104d0aa8bba8..66048cd0dc07 100644 --- a/toolkit/components/url-classifier/src/nsUrlClassifierLib.js +++ b/toolkit/components/url-classifier/src/nsUrlClassifierLib.js @@ -52,7 +52,6 @@ const G_GDEBUG = false; #include ../content/moz/protocol4.js #include ../content/request-backoff.js -#include ../content/url-crypto.js #include ../content/url-crypto-key-manager.js #include ../content/xml-fetcher.js diff --git a/toolkit/components/url-classifier/src/nsUrlClassifierListManager.js b/toolkit/components/url-classifier/src/nsUrlClassifierListManager.js index fff83d22ec2f..71e4d184b796 100644 --- a/toolkit/components/url-classifier/src/nsUrlClassifierListManager.js +++ b/toolkit/components/url-classifier/src/nsUrlClassifierListManager.js @@ -55,7 +55,7 @@ function Init() { modScope.G_Alarm = jslib.G_Alarm; modScope.BindToObject = jslib.BindToObject; modScope.PROT_XMLFetcher = jslib.PROT_XMLFetcher; - modScope.PROT_UrlCrypto = jslib.PROT_UrlCrypto; + modScope.PROT_UrlCryptoKeyManager = jslib.PROT_UrlCryptoKeyManager; modScope.RequestBackoff = jslib.RequestBackoff; // We only need to call Init once. diff --git a/toolkit/components/url-classifier/src/nsUrlClassifierStreamUpdater.cpp b/toolkit/components/url-classifier/src/nsUrlClassifierStreamUpdater.cpp index f686aaa02a47..7b5a41b293da 100644 --- a/toolkit/components/url-classifier/src/nsUrlClassifierStreamUpdater.cpp +++ b/toolkit/components/url-classifier/src/nsUrlClassifierStreamUpdater.cpp @@ -128,7 +128,8 @@ nsUrlClassifierStreamUpdater::SetUpdateUrl(const nsACString & aUpdateUrl) nsresult nsUrlClassifierStreamUpdater::FetchUpdate(nsIURI *aUpdateUrl, const nsACString & aRequestBody, - const nsACString & aStreamTable) + const nsACString & aStreamTable, + const nsACString & aServerMAC) { nsresult rv; rv = NS_NewChannel(getter_AddRefs(mChannel), aUpdateUrl, nsnull, nsnull, this); @@ -144,6 +145,7 @@ nsUrlClassifierStreamUpdater::FetchUpdate(nsIURI *aUpdateUrl, NS_ENSURE_SUCCESS(rv, rv); mStreamTable = aStreamTable; + mServerMAC = aServerMAC; return NS_OK; } @@ -151,7 +153,8 @@ nsUrlClassifierStreamUpdater::FetchUpdate(nsIURI *aUpdateUrl, nsresult nsUrlClassifierStreamUpdater::FetchUpdate(const nsACString & aUpdateUrl, const nsACString & aRequestBody, - const nsACString & aStreamTable) + const nsACString & aStreamTable, + const nsACString & aServerMAC) { nsCOMPtr uri; nsresult rv = NS_NewURI(getter_AddRefs(uri), aUpdateUrl); @@ -159,12 +162,13 @@ nsUrlClassifierStreamUpdater::FetchUpdate(const nsACString & aUpdateUrl, LOG(("Fetching update from %s\n", PromiseFlatCString(aUpdateUrl).get())); - return FetchUpdate(uri, aRequestBody, aStreamTable); + return FetchUpdate(uri, aRequestBody, aStreamTable, aServerMAC); } NS_IMETHODIMP nsUrlClassifierStreamUpdater::DownloadUpdates( const nsACString &aRequestBody, + const nsACString &aClientKey, nsIUrlClassifierCallback *aSuccessCallback, nsIUrlClassifierCallback *aUpdateErrorCallback, nsIUrlClassifierCallback *aDownloadErrorCallback, @@ -204,7 +208,7 @@ nsUrlClassifierStreamUpdater::DownloadUpdates( mInitialized = PR_TRUE; } - rv = mDBService->BeginUpdate(this); + rv = mDBService->BeginUpdate(this, aClientKey); if (rv == NS_ERROR_NOT_AVAILABLE) { LOG(("already updating, skipping update")); *_retval = PR_FALSE; @@ -221,7 +225,7 @@ nsUrlClassifierStreamUpdater::DownloadUpdates( *_retval = PR_TRUE; - return FetchUpdate(mUpdateUrl, aRequestBody, EmptyCString()); + return FetchUpdate(mUpdateUrl, aRequestBody, EmptyCString(), EmptyCString()); } /////////////////////////////////////////////////////////////////////////////// @@ -229,7 +233,8 @@ nsUrlClassifierStreamUpdater::DownloadUpdates( NS_IMETHODIMP nsUrlClassifierStreamUpdater::UpdateUrlRequested(const nsACString &aUrl, - const nsACString &aTable) + const nsACString &aTable, + const nsACString &aServerMAC) { LOG(("Queuing requested update from %s\n", PromiseFlatCString(aUrl).get())); @@ -244,10 +249,24 @@ nsUrlClassifierStreamUpdater::UpdateUrlRequested(const nsACString &aUrl, update->mUrl = NS_LITERAL_CSTRING("http://") + aUrl; } update->mTable = aTable; + update->mServerMAC = aServerMAC; return NS_OK; } +NS_IMETHODIMP +nsUrlClassifierStreamUpdater::RekeyRequested() +{ + nsresult rv; + nsCOMPtr observerService = + do_GetService("@mozilla.org/observer-service;1", &rv); + NS_ENSURE_SUCCESS(rv, rv); + + return observerService->NotifyObservers(static_cast(this), + "url-classifier-rekey-requested", + nsnull); +} + NS_IMETHODIMP nsUrlClassifierStreamUpdater::StreamFinished(nsresult status) { @@ -256,7 +275,8 @@ nsUrlClassifierStreamUpdater::StreamFinished(nsresult status) // Pop off a pending URL and update it. if (NS_SUCCEEDED(status) && mPendingUpdates.Length() > 0) { PendingUpdate &update = mPendingUpdates[0]; - rv = FetchUpdate(update.mUrl, EmptyCString(), update.mTable); + rv = FetchUpdate(update.mUrl, EmptyCString(), + update.mTable, update.mServerMAC); if (NS_FAILED(rv)) { LOG(("Error fetching update url: %s\n", update.mUrl.get())); mDBService->CancelUpdate(); @@ -348,9 +368,10 @@ nsUrlClassifierStreamUpdater::OnStartRequest(nsIRequest *request, { nsresult rv; - rv = mDBService->BeginStream(mStreamTable); + rv = mDBService->BeginStream(mStreamTable, mServerMAC); NS_ENSURE_SUCCESS(rv, rv); mStreamTable.Truncate(); + mServerMAC.Truncate(); nsCOMPtr httpChannel = do_QueryInterface(request); if (httpChannel) { diff --git a/toolkit/components/url-classifier/src/nsUrlClassifierStreamUpdater.h b/toolkit/components/url-classifier/src/nsUrlClassifierStreamUpdater.h index b3b0ad656ea7..45c755a2a194 100644 --- a/toolkit/components/url-classifier/src/nsUrlClassifierStreamUpdater.h +++ b/toolkit/components/url-classifier/src/nsUrlClassifierStreamUpdater.h @@ -89,21 +89,25 @@ private: nsresult FetchUpdate(nsIURI *aURI, const nsACString &aRequestBody, - const nsACString &aTable); + const nsACString &aTable, + const nsACString &aServerMAC); nsresult FetchUpdate(const nsACString &aURI, const nsACString &aRequestBody, - const nsACString &aTable); + const nsACString &aTable, + const nsACString &aServerMAC); PRBool mIsUpdating; PRBool mInitialized; nsCOMPtr mUpdateUrl; nsCString mStreamTable; + nsCString mServerMAC; nsCOMPtr mChannel; nsCOMPtr mDBService; struct PendingUpdate { nsCString mUrl; nsCString mTable; + nsCString mServerMAC; }; nsTArray mPendingUpdates; diff --git a/toolkit/components/url-classifier/src/nsUrlClassifierUtils.cpp b/toolkit/components/url-classifier/src/nsUrlClassifierUtils.cpp index 20a02978a530..afbf89911b52 100644 --- a/toolkit/components/url-classifier/src/nsUrlClassifierUtils.cpp +++ b/toolkit/components/url-classifier/src/nsUrlClassifierUtils.cpp @@ -40,6 +40,8 @@ #include "nsNetUtil.h" #include "nsUrlClassifierUtils.h" #include "nsVoidArray.h" +#include "plbase64.h" +#include "prmem.h" #include "prprf.h" static char int_to_hex_digit(PRInt32 i) @@ -395,3 +397,54 @@ nsUrlClassifierUtils::ShouldURLEscape(const unsigned char c) const { return c <= 32 || c == '%' || c >=127; } + +/* static */ +void +nsUrlClassifierUtils::UnUrlsafeBase64(nsACString &str) +{ + nsACString::iterator iter, end; + str.BeginWriting(iter); + str.EndWriting(end); + while (iter != end) { + if (*iter == '-') { + *iter = '+'; + } else if (*iter == '_') { + *iter = '/'; + } + iter++; + } +} + +/* static */ +nsresult +nsUrlClassifierUtils::DecodeClientKey(const nsACString &key, + nsACString &_retval) +{ + // Client key is sent in urlsafe base64, we need to decode it first. + nsCAutoString base64(key); + UnUrlsafeBase64(base64); + + // PL_Base64Decode doesn't null-terminate unless we let it allocate, + // so we need to calculate the length ourselves. + PRUint32 destLength; + destLength = base64.Length(); + if (destLength > 0 && base64[destLength - 1] == '=') { + if (destLength > 1 && base64[destLength - 2] == '=') { + destLength -= 2; + } else { + destLength -= 1; + } + } + + destLength = ((destLength * 3) / 4); + _retval.SetLength(destLength); + if (destLength != _retval.Length()) + return NS_ERROR_OUT_OF_MEMORY; + + if (!PL_Base64Decode(base64.BeginReading(), base64.Length(), + _retval.BeginWriting())) { + return NS_ERROR_FAILURE; + } + + return NS_OK; +} diff --git a/toolkit/components/url-classifier/src/nsUrlClassifierUtils.h b/toolkit/components/url-classifier/src/nsUrlClassifierUtils.h index d4897637af1e..f42c30ac0996 100644 --- a/toolkit/components/url-classifier/src/nsUrlClassifierUtils.h +++ b/toolkit/components/url-classifier/src/nsUrlClassifierUtils.h @@ -101,6 +101,15 @@ public: PRUint32 bytes, PRBool allowOctal, nsACString & _retval); + + // Convert an urlsafe base64 string to a normal base64 string. + // This method will leave an already-normal base64 string alone. + static void UnUrlsafeBase64(nsACString & str); + + // Takes an urlsafe-base64 encoded client key and gives back binary + // key data + static nsresult DecodeClientKey(const nsACString & clientKey, + nsACString & _retval); private: // Disallow copy constructor nsUrlClassifierUtils(const nsUrlClassifierUtils&); diff --git a/toolkit/components/url-classifier/tests/unit/head_urlclassifier.js b/toolkit/components/url-classifier/tests/unit/head_urlclassifier.js index 7d1327d0bbf5..a3ca3fe7c6ab 100644 --- a/toolkit/components/url-classifier/tests/unit/head_urlclassifier.js +++ b/toolkit/components/url-classifier/tests/unit/head_urlclassifier.js @@ -109,7 +109,7 @@ function buildBareUpdate(chunks, hashSize) { /** * Performs an update of the dbservice manually, bypassing the stream updater */ -function doSimpleUpdate(updateText, success, failure) { +function doSimpleUpdate(updateText, success, failure, clientKey) { var listener = { QueryInterface: function(iid) { @@ -125,8 +125,8 @@ function doSimpleUpdate(updateText, success, failure) { updateSuccess: function(requestedTimeout) { success(requestedTimeout); } }; - dbservice.beginUpdate(listener); - dbservice.beginStream(""); + dbservice.beginUpdate(listener, clientKey); + dbservice.beginStream("", ""); dbservice.updateStream(updateText); dbservice.finishStream(); dbservice.finishUpdate(); @@ -136,14 +136,14 @@ function doSimpleUpdate(updateText, success, failure) { * Performs an update of the dbservice using the stream updater and a * data: uri */ -function doStreamUpdate(updateText, success, failure, downloadFailure) { +function doStreamUpdate(updateText, success, failure, downloadFailure, clientKey) { var dataUpdate = "data:," + encodeURIComponent(updateText); if (!downloadFailure) downloadFailure = failure; streamUpdater.updateUrl = dataUpdate; - streamUpdater.downloadUpdates("", success, failure, downloadFailure); + streamUpdater.downloadUpdates("", clientKey, success, failure, downloadFailure); } var gAssertions = { @@ -153,12 +153,13 @@ tableData : function(expectedTables, cb) dbservice.getTables(function(tables) { // rebuild the tables in a predictable order. var parts = tables.split("\n"); + while (parts[parts.length - 1] == '') { + parts.pop(); + } parts.sort(); tables = parts.join("\n"); - // tables now has a leading empty newline, because split left an - // empty string after the trailing newline; - do_check_eq(tables, "\n" + expectedTables); + do_check_eq(tables, expectedTables); cb(); }); }, @@ -236,11 +237,15 @@ function updateError(arg) } // Runs a set of updates, and then checks a set of assertions. -function doUpdateTest(updates, assertions, successCallback, errorCallback) { +function doUpdateTest(updates, assertions, successCallback, errorCallback, clientKey) { + var errorUpdate = function() { + checkAssertions(assertions, errorCallback); + } + var runUpdate = function() { if (updates.length > 0) { var update = updates.shift(); - doStreamUpdate(update, runUpdate, errorCallback); + doStreamUpdate(update, runUpdate, errorUpdate, null, clientKey); } else { checkAssertions(assertions, successCallback); } diff --git a/toolkit/components/url-classifier/tests/unit/test_partial.js b/toolkit/components/url-classifier/tests/unit/test_partial.js index 4bb9dac791e0..34fdf2047587 100644 --- a/toolkit/components/url-classifier/tests/unit/test_partial.js +++ b/toolkit/components/url-classifier/tests/unit/test_partial.js @@ -6,6 +6,7 @@ function DummyCompleter() { this.fragments = {}; this.queries = []; + this.cachable = true; } DummyCompleter.prototype = @@ -23,8 +24,9 @@ complete: function(partialHash, cb) { this.queries.push(partialHash); var fragments = this.fragments; + var self = this; var doCallback = function() { - if (this.alwaysFail) { + if (self.alwaysFail) { cb.completionFinished(1); return; } @@ -33,7 +35,7 @@ complete: function(partialHash, cb) for (var i = 0; i < fragments[partialHash].length; i++) { var chunkId = fragments[partialHash][i][0]; var hash = fragments[partialHash][i][1]; - cb.completion(hash, "test-phish-simple", chunkId); + cb.completion(hash, "test-phish-simple", chunkId, self.cachable); } } cb.completionFinished(0); @@ -94,7 +96,7 @@ compareQueries: function(fragments) } }; -function setupCompleter(table, hits, conflicts, alwaysFail) +function setupCompleter(table, hits, conflicts) { var completer = new DummyCompleter(); for (var i = 0; i < hits.length; i++) { @@ -119,11 +121,20 @@ function setupCompleter(table, hits, conflicts, alwaysFail) function installCompleter(table, fragments, conflictFragments) { - return setupCompleter(table, fragments, conflictFragments, false); + return setupCompleter(table, fragments, conflictFragments); } function installFailingCompleter(table) { - return setupCompleter(table, [], [], true); + var completer = setupCompleter(table, [], []); + completer.alwaysFail = true; + return completer; +} + +function installUncachableCompleter(table, fragments, conflictFragments) +{ + var completer = setupCompleter(table, fragments, conflictFragments); + completer.cachable = false; + return completer; } // Helper assertion for checking dummy completer queries @@ -466,6 +477,49 @@ function testCachedResultsWithExpire() { }); } +function setupUncachedResults(addUrls, part2) +{ + var update = buildPhishingUpdate( + [ + { "chunkNum" : 1, + "urls" : addUrls + }], + 4); + + var completer = installUncachableCompleter('test-phish-simple', [[1, addUrls]], []); + + var assertions = { + "tableData" : "test-phish-simple;a:1", + // Request the add url. This should cause the completion to be cached. + "urlsExist" : addUrls, + // Make sure the completer was actually queried. + "completerQueried" : [completer, addUrls] + }; + + doUpdateTest([update], assertions, + function() { + // Give the dbservice a chance to cache the result. + var timer = new Timer(3000, part2); + }, updateError); +} + +function testUncachedResults() +{ + setupUncachedResults(["foo.com/a"], function(add) { + // This is called after setupCachedResults(). Verify that + // checking the url again does not cause a completer request. + + // install a new completer, this one should be queried. + var newCompleter = installCompleter('test-phish-simple', [[1, ["foo.com/a"]]], []); + var assertions = { + "urlsExist" : ["foo.com/a"], + "completerQueried" : [newCompleter, ["foo.com/a"]] + }; + checkAssertions(assertions, runNextTest); + }); +} + + function run_test() { runTests([ @@ -481,6 +535,7 @@ function run_test() testCachedResults, testCachedResultsWithSub, testCachedResultsWithExpire, + testUncachedResults, ]); } diff --git a/toolkit/components/url-classifier/tests/unit/test_streamupdater.js b/toolkit/components/url-classifier/tests/unit/test_streamupdater.js index dd8d3fe46de5..82f59e8319c9 100644 --- a/toolkit/components/url-classifier/tests/unit/test_streamupdater.js +++ b/toolkit/components/url-classifier/tests/unit/test_streamupdater.js @@ -1,9 +1,29 @@ -function doTest(updates, assertions, expectError) +var gClientKeyRaw="TESTCLIENTKEY"; +// no btoa() available in xpcshell, precalculated for TESTCLIENTKEY. +var gClientKey = "VEVTVENMSUVOVEtFWQ=="; + +function MAC(content, clientKey) +{ + var hmac = Cc["@mozilla.org/security/hmac;1"].createInstance(Ci.nsICryptoHMAC); + var converter = Cc["@mozilla.org/intl/scriptableunicodeconverter"]. + createInstance(Ci.nsIScriptableUnicodeConverter); + converter.charset = "UTF-8"; + var result = {}; + var data = converter.convertToByteArray(clientKey, result); + hmac.init(Ci.nsICryptoHMAC.SHA1, data, data.length); + + result = {}; + data = converter.convertToByteArray(content, result); + hmac.update(data, data.length); + return hmac.finish(true); +} + +function doTest(updates, assertions, expectError, clientKey) { if (expectError) { - doUpdateTest(updates, assertions, updateError, runNextTest); + doUpdateTest(updates, assertions, updateError, runNextTest, clientKey); } else { - doUpdateTest(updates, assertions, runNextTest, updateError); + doUpdateTest(updates, assertions, runNextTest, updateError, clientKey); } } @@ -138,6 +158,234 @@ function testMultipleTables() { doTest([update], assertions, false); } +// Test a simple update with a valid message authentication code. +function testValidMAC() { + var addUrls = [ "foo.com/a", "foo.com/b", "bar.com/c" ]; + var update = buildPhishingUpdate( + [ + { "chunkNum" : 1, + "urls" : addUrls + }]); + update = "m:" + MAC(update, gClientKeyRaw) + "\n" + update; + + var assertions = { + "tableData" : "test-phish-simple;a:1", + "urlsExist" : addUrls + }; + + doTest([update], assertions, false, gClientKey); +} + +// Test a simple update with an invalid message authentication code. +function testInvalidMAC() { + var addUrls = [ "foo.com/a", "foo.com/b", "bar.com/c" ]; + var update = buildPhishingUpdate( + [ + { "chunkNum" : 1, + "urls" : addUrls + }]); + update = "m:INVALIDMAC\n" + update; + + var assertions = { + "tableData" : "", + "urlsDontExist" : addUrls + }; + + doTest([update], assertions, true, gClientKey); +} + +// Test a simple update without a message authentication code, when it is +// expecting one. +function testNoMAC() { + var addUrls = [ "foo.com/a", "foo.com/b", "bar.com/c" ]; + var update = buildPhishingUpdate( + [ + { "chunkNum" : 1, + "urls" : addUrls + }]); + + var assertions = { + "tableData" : "", + "urlsDontExist" : addUrls + }; + + doTest([update], assertions, true, gClientKey); +} + +// Test an update with a valid message authentication code, with forwards. +function testValidForwardMAC() { + var add1Urls = [ "foo.com/a", "bar.com/c" ]; + var add2Urls = [ "foo.com/b" ]; + var add3Urls = [ "bar.com/d" ]; + + var update = "n:1000\n"; + update += "i:test-phish-simple\n"; + + var update1 = buildBareUpdate( + [{ "chunkNum" : 1, + "urls" : add1Urls }]); + update += "u:data:," + encodeURIComponent(update1) + + "," + MAC(update1, gClientKeyRaw) + "\n"; + + var update2 = buildBareUpdate( + [{ "chunkNum" : 2, + "urls" : add2Urls }]); + update += "u:data:," + encodeURIComponent(update2) + + "," + MAC(update2, gClientKeyRaw) + "\n"; + + + var update3 = buildBareUpdate( + [{ "chunkNum" : 3, + "urls" : add3Urls }]); + update += "u:data:," + encodeURIComponent(update3) + + "," + MAC(update3, gClientKeyRaw) + "\n"; + + var assertions = { + "tableData" : "test-phish-simple;a:1-3", + "urlsExist" : add1Urls.concat(add2Urls).concat(add3Urls) + }; + + update = "m:" + MAC(update, gClientKeyRaw) + "\n" + update; + + doTest([update], assertions, false, gClientKey); +} + +// Test an update with a valid message authentication code, but with +// invalid MACs on the forwards. +function testInvalidForwardMAC() { + var add1Urls = [ "foo.com/a", "bar.com/c" ]; + var add2Urls = [ "foo.com/b" ]; + var add3Urls = [ "bar.com/d" ]; + + var update = "n:1000\n"; + update += "i:test-phish-simple\n"; + + var update1 = buildBareUpdate( + [{ "chunkNum" : 1, + "urls" : add1Urls }]); + update += "u:data:," + encodeURIComponent(update1) + + ",BADMAC\n"; + + var update2 = buildBareUpdate( + [{ "chunkNum" : 2, + "urls" : add2Urls }]); + update += "u:data:," + encodeURIComponent(update2) + + ",BADMAC\n"; + + + var update3 = buildBareUpdate( + [{ "chunkNum" : 3, + "urls" : add3Urls }]); + update += "u:data:," + encodeURIComponent(update3) + + ",BADMAC\n"; + + var assertions = { + "tableData" : "", + "urlsDontExist" : add1Urls.concat(add2Urls).concat(add3Urls) + }; + + update = "m:" + MAC(update, gClientKeyRaw) + "\n" + update; + + doTest([update], assertions, true, gClientKey); +} + +// Test an update with a valid message authentication code, but no MAC +// specified for sub-urls. +function testNoForwardMAC() { + var add1Urls = [ "foo.com/a", "bar.com/c" ]; + var add2Urls = [ "foo.com/b" ]; + var add3Urls = [ "bar.com/d" ]; + + var update = "n:1000\n"; + update += "i:test-phish-simple\n"; + + // XXX : This test presents invalid data: urls as forwards. A valid + // data url requires a comma, which the code will interpret as the + // separator for a MAC. + + // Unfortunately this means that the update will fail even if the code + // isn't properly detecting a missing MAC update. I'm not sure how to + // test that :/ + + var update1 = buildBareUpdate( + [{ "chunkNum" : 1, + "urls" : add1Urls }]); + update += "u:data:" + encodeURIComponent(update1) + "\n"; + + var update2 = buildBareUpdate( + [{ "chunkNum" : 2, + "urls" : add2Urls }]); + update += "u:data:" + encodeURIComponent(update2) + "\n"; + + var update3 = buildBareUpdate( + [{ "chunkNum" : 3, + "urls" : add3Urls }]); + update += "u:data:" + encodeURIComponent(update3) + "\n"; + + var assertions = { + "tableData" : "", + "urlsDontExist" : add1Urls.concat(add2Urls).concat(add3Urls) + }; + + update = "m:" + MAC(update, gClientKeyRaw) + "\n" + update; + + doTest([update], assertions, true, gClientKey); +} + +function Observer(callback) { + this.observe = callback; +} + +Observer.prototype = +{ +QueryInterface: function(iid) +{ + if (!iid.equals(Ci.nsISupports) && + !iid.equals(Ci.nsIObserver)) { + throw Cr.NS_ERROR_NO_INTERFACE; + } + return this; +} +}; + +var gGotRekey; + +gAssertions.gotRekey = function(data, cb) +{ + do_check_eq(gGotRekey, data); + cb(); +} + +// Tests a rekey request. +function testRekey() { + var addUrls = [ "foo.com/a", "foo.com/b", "bar.com/c" ]; + var update = buildPhishingUpdate( + [ + { "chunkNum" : 1, + "urls" : addUrls + }]); + update = "e:pleaserekey\n" + update; + + var assertions = { + "tableData" : "", + "urlsDontExist" : addUrls, + "gotRekey" : true + }; + + gGotRekey = false; + var observerService = Cc["@mozilla.org/observer-service;1"].getService(Ci.nsIObserverService); + + observerService.addObserver(new Observer(function(subject, topic, data) { + if (topic == "url-classifier-rekey-requested") { + gGotRekey = true; + } + }), + "url-classifier-rekey-requested", + false); + + doTest([update], assertions, true, gClientKey); +} + function run_test() { runTests([ @@ -145,7 +393,14 @@ function run_test() testNestedForward, testInvalidUrlForward, testErrorUrlForward, - testMultipleTables + testMultipleTables, + testValidMAC, + testInvalidMAC, + testNoMAC, + testValidForwardMAC, + testInvalidForwardMAC, + testNoForwardMAC, + testRekey, ]); }