Bug 360387: Verify HMAC of safebrowsing updates. r=tony, blocking-firefox3=beltzner

This commit is contained in:
dcamp@mozilla.com 2008-02-27 00:51:02 -08:00
Родитель 36091cba2c
Коммит 9d06b2c6f1
21 изменённых файлов: 968 добавлений и 847 удалений

Просмотреть файл

@ -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");

Просмотреть файл

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

Просмотреть файл

@ -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();

Просмотреть файл

@ -81,8 +81,20 @@ 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 */,
@ -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();
if (!this.keyManager_) {
this.keyManager_ = new PROT_UrlCryptoKeyManager();
this.keyManager_.onNewKey(BindToObject(this.newKey_, this));
this.urlCrypto_.manager_.setKeyUrl(url);
this.hashCompleter_.setKeys(this.keyManager_.getClientKey(),
this.keyManager_.getWrappedKey());
}
this.keyManager_.setKeyUrl(url);
}
/**
@ -371,6 +388,27 @@ PROT_ListManager.prototype.checkForUpdates = function() {
* tablename;<chunk ranges>\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) ||

Просмотреть файл

@ -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,6 +199,10 @@ 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",

Просмотреть файл

@ -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 <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.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

Просмотреть файл

@ -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.

Просмотреть файл

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

Просмотреть файл

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

Просмотреть файл

@ -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<PRUint32> 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<nsICryptoHMAC> 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<const PRUint8*>(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<const PRUint8*>(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<const PRUint8*>(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,6 +3265,9 @@ nsUrlClassifierLookupCallback::Completion(const nsACString& completeHash,
if (result.mLookupFragment == hash)
result.mConfirmed = PR_TRUE;
// If this result is guaranteed to come from our list provider,
// we can cache the results.
if (verified) {
if (!mCacheResults) {
mCacheResults = new nsTArray<nsUrlClassifierLookupResult>();
if (!mCacheResults)
@ -3181,6 +3277,7 @@ nsUrlClassifierLookupCallback::Completion(const nsACString& completeHash,
mCacheResults->AppendElement(result);
}
}
}
return NS_OK;
}
@ -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

Просмотреть файл

@ -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<nsICryptoHMAC> hmac =
do_CreateInstance(NS_CRYPTO_HMAC_CONTRACTID, &rv);
NS_ENSURE_SUCCESS(rv, rv);
rv = hmac->Init(nsICryptoHMAC::SHA1,
reinterpret_cast<const PRUint8*>(mClientKey.BeginReading()),
mClientKey.Length());
NS_ENSURE_SUCCESS(rv, rv);
const nsCSubstring &remaining = Substring(begin, end);
rv = hmac->Update(reinterpret_cast<const PRUint8*>(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 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<nsIURI> 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<nsIObserverService> observerService =
do_GetService("@mozilla.org/observer-service;1", &rv);
NS_ENSURE_SUCCESS(rv, rv);
rv = observerService->NotifyObservers(static_cast<nsIUrlClassifierHashCompleter*>(this),
"url-classifier-rekey-requested",
nsnull);
NS_ENSURE_SUCCESS(rv, rv);
return NS_OK;
}

Просмотреть файл

@ -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<nsUrlClassifierHashCompleter> mCompleter;
nsCOMPtr<nsIURI> mURI;
nsCString mClientKey;
nsCOMPtr<nsIChannel> 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<nsUrlClassifierHashCompleterRequest> mRequest;
nsCOMPtr<nsIURI> mURI;
PRBool mShuttingDown;
nsCString mGethashUrl;
nsCString mClientKey;
nsCString mWrappedKey;
PRBool mShuttingDown;
};
#endif // nsUrlClassifierHashCompleter_h_

Просмотреть файл

@ -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

Просмотреть файл

@ -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.

Просмотреть файл

@ -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<nsIURI> 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<nsIObserverService> observerService =
do_GetService("@mozilla.org/observer-service;1", &rv);
NS_ENSURE_SUCCESS(rv, rv);
return observerService->NotifyObservers(static_cast<nsIUrlClassifierStreamUpdater*>(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<nsIHttpChannel> httpChannel = do_QueryInterface(request);
if (httpChannel) {

Просмотреть файл

@ -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<nsIURI> mUpdateUrl;
nsCString mStreamTable;
nsCString mServerMAC;
nsCOMPtr<nsIChannel> mChannel;
nsCOMPtr<nsIUrlClassifierDBService> mDBService;
struct PendingUpdate {
nsCString mUrl;
nsCString mTable;
nsCString mServerMAC;
};
nsTArray<PendingUpdate> mPendingUpdates;

Просмотреть файл

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

Просмотреть файл

@ -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&);

Просмотреть файл

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

Просмотреть файл

@ -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,
]);
}

Просмотреть файл

@ -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,
]);
}