1 # ***** BEGIN LICENSE BLOCK *****
2 # Version: MPL 1.1/GPL 2.0/LGPL 2.1
4 # The contents of this file are subject to the Mozilla Public License Version
5 # 1.1 (the "License"); you may not use this file except in compliance with
6 # the License. You may obtain a copy of the License at
7 # http://www.mozilla.org/MPL/
9 # Software distributed under the License is distributed on an "AS IS" basis,
10 # WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License
11 # for the specific language governing rights and limitations under the
14 # The Original Code is Google Safe Browsing.
16 # The Initial Developer of the Original Code is Google Inc.
17 # Portions created by the Initial Developer are Copyright (C) 2006
18 # the Initial Developer. All Rights Reserved.
21 # Fritz Schneider <fritz@google.com> (original author)
23 # Alternatively, the contents of this file may be used under the terms of
24 # either the GNU General Public License Version 2 or later (the "GPL"), or
25 # the GNU Lesser General Public License Version 2.1 or later (the "LGPL"),
26 # in which case the provisions of the GPL or the LGPL are applicable instead
27 # of those above. If you wish to allow use of your version of this file only
28 # under the terms of either the GPL or the LGPL, and not to allow others to
29 # use your version of this file under the terms of the MPL, indicate your
30 # decision by deleting the provisions above and replace them with the notice
31 # and other provisions required by the GPL or the LGPL. If you do not delete
32 # the provisions above, a recipient may use your version of this file under
33 # the terms of any one of the MPL, the GPL or the LGPL.
35 # ***** END LICENSE BLOCK *****
38 // This file implements the tricky business of managing the keys for our
39 // URL encryption. The protocol is:
41 // - Server generates secret key K_S
42 // - Client starts up and requests a new key K_C from the server via HTTPS
43 // - Server generates K_C and WrappedKey, which is K_C encrypted with K_S
44 // - Server resonse with K_C and WrappedKey
45 // - When client wants to encrypt a URL, it encrypts it with K_C and sends
46 // the encrypted URL along with WrappedKey
47 // - Server decrypts WrappedKey with K_S to get K_C, and the URL with K_C
49 // This is, however, trickier than it sounds for two reasons. First,
50 // we want to keep the number of HTTPS requests to an aboslute minimum
51 // (like 1 or 2 per browser session). Second, the HTTPS request at
52 // startup might fail, for example the user might be offline or a URL
53 // fetch might need to be issued before the HTTPS request has
56 // We implement the following policy:
58 // - Firefox will issue at most two HTTPS getkey requests per session
59 // - Firefox will issue one HTTPS getkey request at startup if more than 24
60 // hours has passed since the last getkey request.
61 // - Firefox will serialize to disk any key it gets
62 // - Firefox will fall back on this serialized key until it has a
64 // - The front-end can respond with a flag in a lookup request that tells
65 // the client to re-key. Firefox will issue a new HTTPS getkey request
66 // at this time if it has only issued one before
68 // We store the user key in this file. The key can be used to verify signed
70 const kKeyFilename = "urlclassifierkey3.txt";
73 * A key manager for UrlCrypto. There should be exactly one of these
74 * per appplication, and all UrlCrypto's should share it. This is
75 * currently implemented by having the manager attach itself to the
76 * UrlCrypto's prototype at startup. We could've opted for a global
77 * instead, but I like this better, even though it is spooky action
79 * XXX: Should be an XPCOM service
81 * @param opt_keyFilename String containing the name of the
82 * file we should serialize keys to/from. Used
85 * @param opt_testing Boolean indicating whether we are testing. If we
86 * are, then we skip trying to read the old key from
87 * file and automatically trying to rekey; presumably
88 * the tester will drive these manually.
92 function PROT_UrlCryptoKeyManager(opt_keyFilename, opt_testing) {
93 this.debugZone = "urlcryptokeymanager";
94 this.testing_ = !!opt_testing;
95 this.clientKey_ = null; // Base64-encoded, as fetched from server
96 this.clientKeyArray_ = null; // Base64-decoded into an array of numbers
97 this.wrappedKey_ = null; // Opaque websafe base64-encoded server key
99 this.updating_ = false;
101 // Don't do anything until keyUrl_ is set.
104 this.keyFilename_ = opt_keyFilename ?
105 opt_keyFilename : kKeyFilename;
107 this.onNewKey_ = null;
109 // Convenience properties
110 this.MAX_REKEY_TRIES = PROT_UrlCryptoKeyManager.MAX_REKEY_TRIES;
111 this.CLIENT_KEY_NAME = PROT_UrlCryptoKeyManager.CLIENT_KEY_NAME;
112 this.WRAPPED_KEY_NAME = PROT_UrlCryptoKeyManager.WRAPPED_KEY_NAME;
114 if (!this.testing_) {
115 this.maybeLoadOldKey();
119 // Do ***** NOT ***** set this higher; HTTPS is expensive
120 PROT_UrlCryptoKeyManager.MAX_REKEY_TRIES = 2;
122 // Base pref for keeping track of when we updated our key.
123 // We store the time as seconds since the epoch.
124 PROT_UrlCryptoKeyManager.NEXT_REKEY_PREF = "urlclassifier.keyupdatetime.";
126 // Once every 30 days (interval in seconds)
127 PROT_UrlCryptoKeyManager.KEY_MIN_UPDATE_TIME = 30 * 24 * 60 * 60;
129 // These are the names the server will respond with in protocol4 format
130 PROT_UrlCryptoKeyManager.CLIENT_KEY_NAME = "clientkey";
131 PROT_UrlCryptoKeyManager.WRAPPED_KEY_NAME = "wrappedkey";
134 * Called to get ClientKey
135 * @returns urlsafe-base64-encoded client key or null if we haven't gotten one.
137 PROT_UrlCryptoKeyManager.prototype.getClientKey = function() {
138 return this.clientKey_;
142 * Called by a UrlCrypto to get the current K_C
144 * @returns Array of numbers making up the client key or null if we
147 PROT_UrlCryptoKeyManager.prototype.getClientKeyArray = function() {
148 return this.clientKeyArray_;
152 * Called by a UrlCrypto to get WrappedKey
154 * @returns Opaque base64-encoded WrappedKey or null if we haven't
157 PROT_UrlCryptoKeyManager.prototype.getWrappedKey = function() {
158 return this.wrappedKey_;
162 * Change the key url. When we do this, we go ahead and rekey.
163 * @param keyUrl String
165 PROT_UrlCryptoKeyManager.prototype.setKeyUrl = function(keyUrl) {
166 // If it's the same key url, do nothing.
167 if (keyUrl == this.keyUrl_)
170 this.keyUrl_ = keyUrl;
171 this.rekeyTries_ = 0;
173 // Check to see if we should make a new getkey request.
174 var prefs = new G_Preferences(PROT_UrlCryptoKeyManager.NEXT_REKEY_PREF);
175 var nextRekey = prefs.getPref(this.getPrefName_(this.keyUrl_), 0);
176 if (nextRekey < parseInt(Date.now() / 1000, 10)) {
182 * Given a url, return the pref value to use (pref contains last update time).
183 * We basically use the url up until query parameters. This avoids duplicate
184 * pref entries as version number changes over time.
185 * @param url String getkey URL
187 PROT_UrlCryptoKeyManager.prototype.getPrefName_ = function(url) {
188 var queryParam = url.indexOf("?");
189 if (queryParam != -1) {
190 return url.substring(0, queryParam);
196 * Tell the manager to re-key. For safety, this method still obeys the
197 * max-tries limit. Clients should generally use maybeReKey() if they
198 * want to try a re-keying: it's an error to call reKey() after we've
199 * hit max-tries, but not an error to call maybeReKey().
201 PROT_UrlCryptoKeyManager.prototype.reKey = function() {
202 if (this.updating_) {
203 G_Debug(this, "Already re-keying, ignoring this request");
207 if (this.rekeyTries_ > this.MAX_REKEY_TRIES)
208 throw new Error("Have already rekeyed " + this.rekeyTries_ + " times");
212 G_Debug(this, "Attempting to re-key");
213 // If the keyUrl isn't set, we don't do anything.
214 if (!this.testing_ && this.keyUrl_) {
215 (new PROT_XMLFetcher()).get(this.keyUrl_,
216 BindToObject(this.onGetKeyResponse, this));
217 this.updating_ = true;
219 // Calculate the next time we're allowed to re-key.
220 var prefs = new G_Preferences(PROT_UrlCryptoKeyManager.NEXT_REKEY_PREF);
221 var nextRekey = parseInt(Date.now() / 1000, 10)
222 + PROT_UrlCryptoKeyManager.KEY_MIN_UPDATE_TIME;
223 prefs.setPref(this.getPrefName_(this.keyUrl_), nextRekey);
228 * Try to re-key if we haven't already hit our limit. It's OK to call
229 * this method multiple times, even if we've already tried to rekey
230 * more than the max. It will simply refuse to do so.
232 * @returns Boolean indicating if it actually issued a rekey request (that
233 * is, if we haven' already hit the max)
235 PROT_UrlCryptoKeyManager.prototype.maybeReKey = function() {
236 if (this.rekeyTries_ > this.MAX_REKEY_TRIES) {
237 G_Debug(this, "Not re-keying; already at max");
246 * Drop the existing set of keys. Resets the rekeyTries variable to
247 * allow a rekey to succeed.
249 PROT_UrlCryptoKeyManager.prototype.dropKey = function() {
250 this.rekeyTries_ = 0;
251 this.replaceKey_(null, null);
255 * @returns Boolean indicating if we have a key we can use
257 PROT_UrlCryptoKeyManager.prototype.hasKey = function() {
258 return this.clientKey_ != null && this.wrappedKey_ != null;
261 PROT_UrlCryptoKeyManager.prototype.unUrlSafe = function(key)
263 return key ? key.replace("-", "+").replace("_", "/") : "";
267 * Set a new key and serialize it to disk.
269 * @param clientKey String containing the base64-encoded client key
272 * @param wrappedKey String containing the opaque base64-encoded WrappedKey
273 * the server gave us (i.e., K_C encrypted with K_S)
275 PROT_UrlCryptoKeyManager.prototype.replaceKey_ = function(clientKey,
278 G_Debug(this, "Replacing " + this.clientKey_ + " with " + clientKey);
280 this.clientKey_ = clientKey;
281 this.clientKeyArray_ = Array.map(atob(this.unUrlSafe(clientKey)),
282 function(c) { return c.charCodeAt(0); });
283 this.wrappedKey_ = wrappedKey;
285 this.serializeKey_(this.clientKey_, this.wrappedKey_);
287 if (this.onNewKey_) {
293 * Try to write the key to disk so we can fall back on it. Fail
294 * silently if we cannot. The keys are serialized in protocol4 format.
296 * @returns Boolean indicating whether we succeeded in serializing
298 PROT_UrlCryptoKeyManager.prototype.serializeKey_ = function() {
301 map[this.CLIENT_KEY_NAME] = this.clientKey_;
302 map[this.WRAPPED_KEY_NAME] = this.wrappedKey_;
306 var keyfile = Cc["@mozilla.org/file/directory_service;1"]
307 .getService(Ci.nsIProperties)
308 .get("ProfD", Ci.nsILocalFile); /* profile directory */
309 keyfile.append(this.keyFilename_);
311 if (!this.clientKey_ || !this.wrappedKey_) {
312 keyfile.remove(true);
316 var data = (new G_Protocol4Parser()).serialize(map);
319 var stream = Cc["@mozilla.org/network/file-output-stream;1"]
320 .createInstance(Ci.nsIFileOutputStream);
322 0x02 | 0x08 | 0x20 /* PR_WRONLY | PR_CREATE_FILE | PR_TRUNCATE */,
323 -1 /* default perms */, 0 /* no special behavior */);
324 stream.write(data, data.length);
332 G_Error(this, "Failed to serialize new key: " + e);
339 * Invoked when we've received a protocol4 response to our getkey
340 * request. Try to parse it and set this key as the new one if we can.
342 * @param responseText String containing the protocol4 getkey response
344 PROT_UrlCryptoKeyManager.prototype.onGetKeyResponse = function(responseText) {
346 var response = (new G_Protocol4Parser).parse(responseText);
347 var clientKey = response[this.CLIENT_KEY_NAME];
348 var wrappedKey = response[this.WRAPPED_KEY_NAME];
350 this.updating_ = false;
352 if (response && clientKey && wrappedKey) {
353 G_Debug(this, "Got new key from: " + responseText);
354 this.replaceKey_(clientKey, wrappedKey);
356 G_Debug(this, "Not a valid response for /newkey");
361 * Set the callback to be called whenever we get a new key.
363 * @param callback The callback.
365 PROT_UrlCryptoKeyManager.prototype.onNewKey = function(callback)
367 this.onNewKey_ = callback;
371 * Attempt to read a key we've previously serialized from disk, so
372 * that we can fall back on it in case we can't get one from the
373 * server. If we get a key, only use it if we don't already have one
374 * (i.e., if our startup HTTPS request died or hasn't yet completed).
376 * This method should be invoked early, like when the user's profile
379 PROT_UrlCryptoKeyManager.prototype.maybeLoadOldKey = function() {
383 var keyfile = Cc["@mozilla.org/file/directory_service;1"]
384 .getService(Ci.nsIProperties)
385 .get("ProfD", Ci.nsILocalFile); /* profile directory */
386 keyfile.append(this.keyFilename_);
387 if (keyfile.exists()) {
389 var fis = Cc["@mozilla.org/network/file-input-stream;1"]
390 .createInstance(Ci.nsIFileInputStream);
391 fis.init(keyfile, 0x01 /* PR_RDONLY */, 0444, 0);
392 var stream = Cc["@mozilla.org/scriptableinputstream;1"]
393 .createInstance(Ci.nsIScriptableInputStream);
395 oldKey = stream.read(stream.available());
402 G_Debug(this, "Caught " + e + " trying to read keyfile");
407 G_Debug(this, "Couldn't find old key.");
411 oldKey = (new G_Protocol4Parser).parse(oldKey);
412 var clientKey = oldKey[this.CLIENT_KEY_NAME];
413 var wrappedKey = oldKey[this.WRAPPED_KEY_NAME];
415 if (oldKey && clientKey && wrappedKey && !this.hasKey()) {
416 G_Debug(this, "Read old key from disk.");
417 this.replaceKey_(clientKey, wrappedKey);
426 function TEST_PROT_UrlCryptoKeyManager() {
428 var z = "urlcryptokeymanager UNITTEST";
429 G_debugService.enableZone(z);
431 G_Debug(z, "Starting");
433 // Let's not clobber any real keyfile out there
434 var kf = "keytest.txt";
436 // Let's be able to clean up after ourselves
437 function removeTestFile(f) {
438 var file = Cc["@mozilla.org/file/directory_service;1"]
439 .getService(Ci.nsIProperties)
440 .get("ProfD", Ci.nsILocalFile); /* profile directory */
443 file.remove(false /* do not recurse */);
447 var km = new PROT_UrlCryptoKeyManager(kf, true /* testing */);
449 // CASE: simulate nothing on disk, then get something from server
451 G_Assert(z, !km.hasKey(), "KM already has key?");
452 km.maybeLoadOldKey();
453 G_Assert(z, !km.hasKey(), "KM loaded non-existent key?");
454 km.onGetKeyResponse(null);
455 G_Assert(z, !km.hasKey(), "KM got key from null response?");
456 km.onGetKeyResponse("");
457 G_Assert(z, !km.hasKey(), "KM got key from empty response?");
458 km.onGetKeyResponse("aslkaslkdf:34:a230\nskdjfaljsie");
459 G_Assert(z, !km.hasKey(), "KM got key from garbage response?");
461 var realResponse = "clientkey:24:zGbaDbx1pxoYe7siZYi8VA==\n" +
462 "wrappedkey:24:MTr1oDt6TSOFQDTvKCWz9PEn";
463 km.onGetKeyResponse(realResponse);
464 // Will have written it to file as a side effect
465 G_Assert(z, km.hasKey(), "KM couldn't get key from real response?");
466 G_Assert(z, km.clientKey_ == "zGbaDbx1pxoYe7siZYi8VA==",
467 "Parsed wrong client key from response?");
468 G_Assert(z, km.wrappedKey_ == "MTr1oDt6TSOFQDTvKCWz9PEn",
469 "Parsed wrong wrapped key from response?");
471 // CASE: simulate something on disk, then get something from server
473 km = new PROT_UrlCryptoKeyManager(kf, true /* testing */);
474 G_Assert(z, !km.hasKey(), "KM already has key?");
475 km.maybeLoadOldKey();
476 G_Assert(z, km.hasKey(), "KM couldn't load existing key from disk?");
477 G_Assert(z, km.clientKey_ == "zGbaDbx1pxoYe7siZYi8VA==",
478 "Parsed wrong client key from disk?");
479 G_Assert(z, km.wrappedKey_ == "MTr1oDt6TSOFQDTvKCWz9PEn",
480 "Parsed wrong wrapped key from disk?");
481 var realResponse2 = "clientkey:24:dtmbEN1kgN/LmuEoYifaFw==\n" +
482 "wrappedkey:24:MTpPH3pnLDKihecOci+0W5dk";
483 km.onGetKeyResponse(realResponse2);
484 // Will have written it to disk
485 G_Assert(z, km.hasKey(), "KM couldn't replace key from server response?");
486 G_Assert(z, km.clientKey_ == "dtmbEN1kgN/LmuEoYifaFw==",
487 "Replace client key from server failed?");
488 G_Assert(z, km.wrappedKey == "MTpPH3pnLDKihecOci+0W5dk",
489 "Replace wrapped key from server failed?");
491 // CASE: check overwriting a key on disk
493 km = new PROT_UrlCryptoKeyManager(kf, true /* testing */);
494 G_Assert(z, !km.hasKey(), "KM already has key?");
495 km.maybeLoadOldKey();
496 G_Assert(z, km.hasKey(), "KM couldn't load existing key from disk?");
497 G_Assert(z, km.clientKey_ == "dtmbEN1kgN/LmuEoYifaFw==",
498 "Replace client on from disk failed?");
499 G_Assert(z, km.wrappedKey_ == "MTpPH3pnLDKihecOci+0W5dk",
500 "Replace wrapped key on disk failed?");
502 // Test that we only fetch at most two getkey's per lifetime of the manager
504 km = new PROT_UrlCryptoKeyManager(kf, true /* testing */);
506 for (var i = 0; i < km.MAX_REKEY_TRIES; i++)
507 G_Assert(z, km.maybeReKey(), "Couldn't rekey?");
508 G_Assert(z, !km.maybeReKey(), "Rekeyed when max hit");
512 G_Debug(z, "PASSED");