Bug 617531 - Merge fx-sync to mozilla-central. a=blockers

This commit is contained in:
Philipp von Weitershausen 2010-12-09 18:26:31 -08:00
commit 9de6f8bfc9
18 changed files with 1555 additions and 137 deletions

View File

@ -103,7 +103,7 @@ CryptoWrapper.prototype = {
let computedHMAC = this.ciphertextHMAC(keyBundle);
if (computedHMAC != this.hmac) {
throw "Record SHA256 HMAC mismatch: " + this.hmac + ", not " + computedHMAC;
Utils.throwHMACMismatch(this.hmac, computedHMAC);
}
// Handle invalid data here. Elsewhere we assume that cleartext is an object.
@ -168,6 +168,37 @@ function CollectionKeyManager() {
// Note that the last modified time needs to be preserved.
CollectionKeyManager.prototype = {
// Return information about old vs new keys:
// * same: true if two collections are equal
// * changed: an array of collection names that changed.
_compareKeyBundleCollections: function _compareKeyBundleCollections(m1, m2) {
let changed = [];
function process(m1, m2) {
for (let k1 in m1) {
let v1 = m1[k1];
let v2 = m2[k1];
if (!(v1 && v2 && v1.equals(v2)))
changed.push(k1);
}
}
// Diffs both ways.
process(m1, m2);
process(m2, m1);
// Return a sorted, unique array.
changed.sort();
let last;
changed = [x for each (x in changed) if ((x != last) && (last = x))];
return {same: changed.length == 0,
changed: changed};
},
get isClear() {
return !this._default;
},
clear: function clear() {
this._log.info("Clearing CollectionKeys...");
this._lastModified = 0;
@ -242,40 +273,85 @@ CollectionKeyManager.prototype = {
return (info_collections["crypto"] > this._lastModified);
},
//
// Set our keys and modified time to the values fetched from the server.
// Returns one of three values:
//
// * If the default key was modified, return true.
// * If the default key was not modified, but per-collection keys were,
// return an array of such.
// * Otherwise, return false -- we were up-to-date.
//
setContents: function setContents(payload, modified) {
if ("collections" in payload) {
let out_coll = {};
let colls = payload["collections"];
for (let k in colls) {
let v = colls[k];
if (v) {
let keyObj = new BulkKeyBundle(null, k);
keyObj.keyPair = v;
if (keyObj) {
out_coll[k] = keyObj;
}
}
}
this._collections = out_coll;
}
if ("default" in payload) {
if (payload.default) {
let b = new BulkKeyBundle(null, DEFAULT_KEYBUNDLE_NAME);
b.keyPair = payload.default;
this._default = b;
}
else {
this._default = null;
}
}
let self = this;
// The server will round the time, which can lead to us having spurious
// key refreshes. Do the best we can to get an accurate timestamp, but
// rounded to 2 decimal places.
// We could use .toFixed(2), but that's a little more multiplication and
// division...
this._lastModified = modified || (Math.round(Date.now()/10)/100);
return payload;
function bumpModified() {
let lm = modified || (Math.round(Date.now()/10)/100);
self._log.info("Bumping last modified to " + lm);
self._lastModified = lm;
}
this._log.info("Setting CollectionKeys contents. Our last modified: "
+ this._lastModified + ", input modified: " + modified + ".");
if (!payload)
throw "No payload in CollectionKeys.setContents().";
if (!payload.default) {
this._log.warn("No downloaded default key: this should not occur.");
this._log.warn("Not clearing local keys.");
throw "No default key in CollectionKeys.setContents(). Cannot proceed.";
}
// Process the incoming default key.
let b = new BulkKeyBundle(null, DEFAULT_KEYBUNDLE_NAME);
b.keyPair = payload.default;
let newDefault = b;
// Process the incoming collections.
let newCollections = {};
if ("collections" in payload) {
this._log.info("Processing downloaded per-collection keys.");
let colls = payload.collections;
for (let k in colls) {
let v = colls[k];
if (v) {
let keyObj = new BulkKeyBundle(null, k);
keyObj.keyPair = v;
if (keyObj) {
newCollections[k] = keyObj;
}
}
}
}
// Check to see if these are already our keys.
let sameDefault = (this._default && this._default.equals(newDefault));
let collComparison = this._compareKeyBundleCollections(newCollections, this._collections);
let sameColls = collComparison.same;
if (sameDefault && sameColls) {
this._log.info("New keys are the same as our old keys! Bumping local modified time and returning.");
bumpModified();
return false;
}
// Make sure things are nice and tidy before we set.
this.clear();
this._log.info("Saving downloaded keys.");
this._default = newDefault;
this._collections = newCollections;
bumpModified();
return sameDefault ? collComparison.changed : true;
},
updateContents: function updateContents(syncKeyBundle, storage_keys) {
@ -298,7 +374,7 @@ CollectionKeyManager.prototype = {
let r = this.setContents(payload, storage_keys.modified);
log.info("Collection keys updated.");
return r;
}
}
}
/**
@ -338,6 +414,12 @@ function KeyBundle(realm, collectionName, keyStr) {
KeyBundle.prototype = {
__proto__: Identity.prototype,
equals: function equals(bundle) {
return bundle &&
(bundle.hmacKey == this.hmacKey) &&
(bundle.encryptionKey == this.encryptionKey);
},
/*
* Accessors for the two keys.
*/
@ -458,29 +540,21 @@ SyncKeyBundle.prototype = {
* If we've got a string, hash it into keys and store them.
*/
generateEntry: function generateEntry() {
let m = this.keyStr;
if (m) {
// Decode into a 16-byte string before we go any further.
m = Utils.decodeKeyBase32(m);
// Reuse the hasher.
let h = Utils.makeHMACHasher();
// First key.
let u = this.username;
let k1 = Utils.makeHMACKey("" + HMAC_INPUT + u + "\x01");
let enc = Utils.sha256HMACBytes(m, k1, h);
// Second key: depends on the output of the first run.
let k2 = Utils.makeHMACKey(enc + HMAC_INPUT + u + "\x02");
let hmac = Utils.sha256HMACBytes(m, k2, h);
// Save them.
this._encrypt = btoa(enc);
// Individual sets: cheaper than calling parent setter.
this._hmac = hmac;
this._hmacObj = Utils.makeHMACKey(hmac);
}
let syncKey = this.keyStr;
if (!syncKey)
return;
// Expand the base32 Sync Key to an AES 256 and 256 bit HMAC key.
let prk = Utils.decodeKeyBase32(syncKey);
let info = HMAC_INPUT + this.username;
let okm = Utils.hkdfExpand(prk, info, 32 * 2);
let enc = okm.slice(0, 32);
let hmac = okm.slice(32, 64);
// Save them.
this._encrypt = btoa(enc);
// Individual sets: cheaper than calling parent setter.
this._hmac = hmac;
this._hmacObj = Utils.makeHMACKey(hmac);
}
};

View File

@ -45,7 +45,7 @@ WEAVE_ID: "@weave_id@",
// Version of the data format this client supports. The data format describes
// how records are packaged; this is separate from the Server API version and
// the per-engine cleartext formats.
STORAGE_VERSION: 4,
STORAGE_VERSION: 5,
UPDATED_DEV_URL: "https://services.mozilla.com/sync/updated/?version=@weave_version@&channel=@xpi_type@",
UPDATED_REL_URL: "http://www.mozilla.com/firefox/sync/updated.html",
@ -145,6 +145,16 @@ ENGINE_UNKNOWN_FAIL: "error.engine.reason.unknown_fail",
ENGINE_METARECORD_DOWNLOAD_FAIL: "error.engine.reason.metarecord_download_fail",
ENGINE_METARECORD_UPLOAD_FAIL: "error.engine.reason.metarecord_upload_fail",
JPAKE_ERROR_CHANNEL: "jpake.error.channel",
JPAKE_ERROR_NETWORK: "jpake.error.network",
JPAKE_ERROR_SERVER: "jpake.error.server",
JPAKE_ERROR_TIMEOUT: "jpake.error.timeout",
JPAKE_ERROR_INTERNAL: "jpake.error.internal",
JPAKE_ERROR_INVALID: "jpake.error.invalid",
JPAKE_ERROR_NODATA: "jpake.error.nodata",
JPAKE_ERROR_KEYMISMATCH: "jpake.error.keymismatch",
JPAKE_ERROR_WRONGMESSAGE: "jpake.error.wrongmessage",
// Ways that a sync can be disabled (messages only to be printed in debug log)
kSyncWeaveDisabled: "Weave is disabled",
kSyncNotLoggedIn: "User is not logged in",

View File

@ -0,0 +1,588 @@
/* ***** 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 Firefox Sync.
*
* The Initial Developer of the Original Code is
* Mozilla Foundation.
* Portions created by the Initial Developer are Copyright (C) 2010
* the Initial Developer. All Rights Reserved.
*
* Contributor(s):
* Philipp von Weitershausen <philipp@weitershausen.de>
*
* 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 ***** */
const Cc = Components.classes;
const Ci = Components.interfaces;
const Cr = Components.results;
const Cu = Components.utils;
Cu.import("resource://services-sync/log4moz.js");
Cu.import("resource://services-sync/auth.js");
Cu.import("resource://services-sync/resource.js");
Cu.import("resource://services-sync/constants.js");
Cu.import("resource://services-sync/util.js");
const EXPORTED_SYMBOLS = ["JPAKEClient"];
const JPAKE_SIGNERID_SENDER = "sender";
const JPAKE_SIGNERID_RECEIVER = "receiver";
const JPAKE_LENGTH_SECRET = 8;
const JPAKE_LENGTH_CLIENTID = 256;
const JPAKE_VERIFY_VALUE = "0123456789ABCDEF";
/*
* Client to exchange encrypted data using the J-PAKE algorithm.
* The exchange between two clients of this type looks like this:
*
*
* Client A Server Client B
* ==================================================================
* |
* retrieve channel <---------------|
* generate random secret |
* show PIN = secret + channel | ask user for PIN
* upload A's message 1 ----------->|
* |--------> retrieve A's message 1
* |<---------- upload B's message 1
* retrieve B's message 1 <---------|
* upload A's message 2 ----------->|
* |--------> retrieve A's message 2
* | compute key
* |<---------- upload B's message 2
* retrieve B's message 2 <---------|
* compute key |
* upload sha256d(key) ------------>|
* |---------> retrieve sha256d(key)
* | verify against own key
* | encrypt data
* |<------------------- upload data
* retrieve data <------------------|
* verify HMAC |
* decrypt data |
*
*
* Create a client object like so:
*
* let client = new JPAKEClient(observer);
*
* The 'observer' object must implement the following methods:
*
* displayPIN(pin) -- Display the PIN to the user, only called on the client
* that didn't provide the PIN.
*
* onComplete(data) -- Called after transfer has been completed. On
* the sending side this is called with no parameter and as soon as the
* data has been uploaded, which this doesn't mean the receiving side
* has actually retrieved them yet.
*
* onAbort(error) -- Called whenever an error is encountered. All errors lead
* to an abort and the process has to be started again on both sides.
*
* To start the data transfer on the receiving side, call
*
* client.receiveNoPIN();
*
* This will allocate a new channel on the server, generate a PIN, have it
* displayed and then do the transfer once the protocol has been completed
* with the sending side.
*
* To initiate the transfer from the sending side, call
*
* client.sendWithPIN(pin, data)
*
* To abort the process, call
*
* client.abort();
*
* Note that after completion or abort, the 'client' instance may not be reused.
* You will have to create a new one in case you'd like to restart the process.
*/
function JPAKEClient(observer) {
this.observer = observer;
this._log = Log4Moz.repository.getLogger("Service.JPAKEClient");
this._log.level = Log4Moz.Level[Svc.Prefs.get(
"log.logger.service.jpakeclient", "Debug")];
this._serverUrl = Svc.Prefs.get("jpake.serverURL");
this._pollInterval = Svc.Prefs.get("jpake.pollInterval");
this._maxTries = Svc.Prefs.get("jpake.maxTries");
if (this._serverUrl.slice(-1) != "/")
this._serverUrl += "/";
this._jpake = Cc["@mozilla.org/services-crypto/sync-jpake;1"]
.createInstance(Ci.nsISyncJPAKE);
this._auth = new NoOpAuthenticator();
this._setClientID();
}
JPAKEClient.prototype = {
_chain: Utils.asyncChain,
/*
* Public API
*/
receiveNoPIN: function receiveNoPIN() {
this._my_signerid = JPAKE_SIGNERID_RECEIVER;
this._their_signerid = JPAKE_SIGNERID_SENDER;
this._secret = this._createSecret();
// Allow a large number of tries first while we wait for the PIN
// to be entered on the other device.
this._maxTries = Svc.Prefs.get("jpake.firstMsgMaxTries");
this._chain(this._getChannel,
this._computeStepOne,
this._putStep,
this._getStep,
function(callback) {
// Now we can switch back to the smaller timeout.
this._maxTries = Svc.Prefs.get("jpake.maxTries");
callback();
},
this._computeStepTwo,
this._putStep,
this._getStep,
this._computeFinal,
this._computeKeyVerification,
this._putStep,
this._getStep,
this._decryptData,
this._complete)();
},
sendWithPIN: function sendWithPIN(pin, obj) {
this._my_signerid = JPAKE_SIGNERID_SENDER;
this._their_signerid = JPAKE_SIGNERID_RECEIVER;
this._channel = pin.slice(JPAKE_LENGTH_SECRET);
this._channelUrl = this._serverUrl + this._channel;
this._secret = pin.slice(0, JPAKE_LENGTH_SECRET);
this._data = JSON.stringify(obj);
this._chain(this._computeStepOne,
this._getStep,
this._putStep,
this._computeStepTwo,
this._getStep,
this._putStep,
this._computeFinal,
this._getStep,
this._encryptData,
this._putStep,
this._complete)();
},
abort: function abort(error) {
this._log.debug("Aborting...");
this._finished = true;
let self = this;
if (error == JPAKE_ERROR_CHANNEL
|| error == JPAKE_ERROR_NETWORK
|| error == JPAKE_ERROR_NODATA) {
Utils.delay(function() { this.observer.onAbort(error); }, 0,
this, "_timer_onAbort");
} else {
this._reportFailure(error, function() { self.observer.onAbort(error); });
}
},
/*
* Utilities
*/
_setClientID: function _setClientID() {
let rng = Cc["@mozilla.org/security/random-generator;1"]
.createInstance(Ci.nsIRandomGenerator);
let bytes = rng.generateRandomBytes(JPAKE_LENGTH_CLIENTID / 2);
this._clientID = [("0" + byte.toString(16)).slice(-2)
for each (byte in bytes)].join("");
},
_createSecret: function _createSecret() {
// 0-9a-z without 1,l,o,0
const key = "23456789abcdefghijkmnpqrstuvwxyz";
let rng = Cc["@mozilla.org/security/random-generator;1"]
.createInstance(Ci.nsIRandomGenerator);
let bytes = rng.generateRandomBytes(JPAKE_LENGTH_SECRET);
return [key[Math.floor(byte * key.length / 256)]
for each (byte in bytes)].join("");
},
/*
* Steps of J-PAKE procedure
*/
_getChannel: function _getChannel(callback) {
this._log.trace("Requesting channel.");
let resource = new AsyncResource(this._serverUrl + "new_channel");
resource.authenticator = this._auth;
resource.setHeader("X-KeyExchange-Id", this._clientID);
resource.get(Utils.bind2(this, function handleChannel(error, response) {
if (this._finished)
return;
if (error) {
this._log.error("Error acquiring channel ID. " + error);
this.abort(JPAKE_ERROR_CHANNEL);
return;
}
if (response.status != 200) {
this._log.error("Error acquiring channel ID. Server responded with HTTP "
+ response.status);
this.abort(JPAKE_ERROR_CHANNEL);
return;
}
let channel;
try {
this._channel = response.obj;
} catch (ex) {
this._log.error("Server responded with invalid JSON.");
this.abort(JPAKE_ERROR_CHANNEL);
return;
}
this._log.debug("Using channel " + this._channel);
this._channelUrl = this._serverUrl + this._channel;
// Don't block on UI code.
let pin = this._secret + this._channel;
Utils.delay(function() { this.observer.displayPIN(pin); }, 0,
this, "_timer_displayPIN");
callback();
}));
},
// Generic handler for uploading data.
_putStep: function _putStep(callback) {
this._log.trace("Uploading message " + this._outgoing.type);
let resource = new AsyncResource(this._channelUrl);
resource.authenticator = this._auth;
resource.setHeader("X-KeyExchange-Id", this._clientID);
resource.put(this._outgoing, Utils.bind2(this, function (error, response) {
if (this._finished)
return;
if (error) {
this._log.error("Error uploading data. " + error);
this.abort(JPAKE_ERROR_NETWORK);
return;
}
if (response.status != 200) {
this._log.error("Could not upload data. Server responded with HTTP "
+ response.status);
this.abort(JPAKE_ERROR_SERVER);
return;
}
// There's no point in returning early here since the next step will
// always be a GET so let's pause for twice the poll interval.
this._etag = response.headers["etag"];
Utils.delay(function () { callback(); }, this._pollInterval * 2, this,
"_pollTimer");
}));
},
// Generic handler for polling for and retrieving data.
_pollTries: 0,
_getStep: function _getStep(callback) {
this._log.trace("Retrieving next message.");
let resource = new AsyncResource(this._channelUrl);
resource.authenticator = this._auth;
resource.setHeader("X-KeyExchange-Id", this._clientID);
if (this._etag)
resource.setHeader("If-None-Match", this._etag);
resource.get(Utils.bind2(this, function (error, response) {
if (this._finished)
return;
if (error) {
this._log.error("Error fetching data. " + error);
this.abort(JPAKE_ERROR_NETWORK);
return;
}
if (response.status == 304) {
this._log.trace("Channel hasn't been updated yet. Will try again later.");
if (this._pollTries >= this._maxTries) {
this._log.error("Tried for " + this._pollTries + " times, aborting.");
this.abort(JPAKE_ERROR_TIMEOUT);
return;
}
this._pollTries += 1;
Utils.delay(function() { this._getStep(callback); },
this._pollInterval, this, "_pollTimer");
return;
}
this._pollTries = 0;
if (response.status == 404) {
this._log.error("No data found in the channel.");
this.abort(JPAKE_ERROR_NODATA);
return;
}
if (response.status != 200) {
this._log.error("Could not retrieve data. Server responded with HTTP "
+ response.status);
this.abort(JPAKE_ERROR_SERVER);
return;
}
try {
this._incoming = response.obj;
} catch (ex) {
this._log.error("Server responded with invalid JSON.");
this.abort(JPAKE_ERROR_INVALID);
return;
}
this._log.trace("Fetched message " + this._incoming.type);
callback();
}));
},
_reportFailure: function _reportFailure(reason, callback) {
this._log.debug("Reporting failure to server.");
let resource = new AsyncResource(this._serverUrl + "report");
resource.authenticator = this._auth;
resource.setHeader("X-KeyExchange-Id", this._clientID);
resource.setHeader("X-KeyExchange-Cid", this._channel);
resource.setHeader("X-KeyExchange-Log", reason);
resource.post("", Utils.bind2(this, function (error, response) {
if (error)
this._log.warn("Report failed: " + error);
else if (response.status != 200)
this._log.warn("Report failed. Server responded with HTTP "
+ response.status);
// Do not block on errors, we're done or aborted by now anyway.
callback();
}));
},
_computeStepOne: function _computeStepOne(callback) {
this._log.trace("Computing round 1.");
let gx1 = {};
let gv1 = {};
let r1 = {};
let gx2 = {};
let gv2 = {};
let r2 = {};
try {
this._jpake.round1(this._my_signerid, gx1, gv1, r1, gx2, gv2, r2);
} catch (ex) {
this._log.error("JPAKE round 1 threw: " + ex);
this.abort(JPAKE_ERROR_INTERNAL);
return;
}
let one = {gx1: gx1.value,
gx2: gx2.value,
zkp_x1: {gr: gv1.value, b: r1.value, id: this._my_signerid},
zkp_x2: {gr: gv2.value, b: r2.value, id: this._my_signerid}};
this._outgoing = {type: this._my_signerid + "1", payload: one};
this._log.trace("Generated message " + this._outgoing.type);
callback();
},
_computeStepTwo: function _computeStepTwo(callback) {
this._log.trace("Computing round 2.");
if (this._incoming.type != this._their_signerid + "1") {
this._log.error("Invalid round 1 message: "
+ JSON.stringify(this._incoming));
this.abort(JPAKE_ERROR_WRONGMESSAGE);
return;
}
let step1 = this._incoming.payload;
if (!step1 || !step1.zkp_x1 || step1.zkp_x1.id != this._their_signerid
|| !step1.zkp_x2 || step1.zkp_x2.id != this._their_signerid) {
this._log.error("Invalid round 1 payload: " + JSON.stringify(step1));
this.abort(JPAKE_ERROR_WRONGMESSAGE);
return;
}
let A = {};
let gvA = {};
let rA = {};
try {
this._jpake.round2(this._their_signerid, this._secret,
step1.gx1, step1.zkp_x1.gr, step1.zkp_x1.b,
step1.gx2, step1.zkp_x2.gr, step1.zkp_x2.b,
A, gvA, rA);
} catch (ex) {
this._log.error("JPAKE round 2 threw: " + ex);
this.abort(JPAKE_ERROR_INTERNAL);
return;
}
let two = {A: A.value,
zkp_A: {gr: gvA.value, b: rA.value, id: this._my_signerid}};
this._outgoing = {type: this._my_signerid + "2", payload: two};
this._log.trace("Generated message " + this._outgoing.type);
callback();
},
_computeFinal: function _computeFinal(callback) {
if (this._incoming.type != this._their_signerid + "2") {
this._log.error("Invalid round 2 message: "
+ JSON.stringify(this._incoming));
this.abort(JPAKE_ERROR_WRONGMESSAGE);
return;
}
let step2 = this._incoming.payload;
if (!step2 || !step2.zkp_A || step2.zkp_A.id != this._their_signerid) {
this._log.error("Invalid round 2 payload: " + JSON.stringify(step1));
this.abort(JPAKE_ERROR_WRONGMESSAGE);
return;
}
let aes256Key = {};
let hmac256Key = {};
try {
this._jpake.final(step2.A, step2.zkp_A.gr, step2.zkp_A.b, HMAC_INPUT,
aes256Key, hmac256Key);
} catch (ex) {
this._log.error("JPAKE final round threw: " + ex);
this.abort(JPAKE_ERROR_INTERNAL);
return;
}
this._crypto_key = aes256Key.value;
this._hmac_key = Utils.makeHMACKey(Utils.safeAtoB(hmac256Key.value));
callback();
},
_computeKeyVerification: function _computeKeyVerification(callback) {
this._log.trace("Encrypting key verification value.");
let iv, ciphertext;
try {
iv = Svc.Crypto.generateRandomIV();
ciphertext = Svc.Crypto.encrypt(JPAKE_VERIFY_VALUE,
this._crypto_key, iv);
} catch (ex) {
this._log.error("Failed to encrypt key verification value.");
this.abort(JPAKE_ERROR_INTERNAL);
return;
}
this._outgoing = {type: this._my_signerid + "3",
payload: {ciphertext: ciphertext, IV: iv}};
this._log.trace("Generated message " + this._outgoing.type);
callback();
},
_encryptData: function _encryptData(callback) {
this._log.trace("Verifying their key.");
if (this._incoming.type != this._their_signerid + "3") {
this._log.error("Invalid round 3 data: " +
JSON.stringify(this._incoming));
this.abort(JPAKE_ERROR_WRONGMESSAGE);
return;
}
let step3 = this._incoming.payload;
try {
ciphertext = Svc.Crypto.encrypt(JPAKE_VERIFY_VALUE,
this._crypto_key, step3.IV);
if (ciphertext != step3.ciphertext)
throw "Key mismatch!";
} catch (ex) {
this._log.error("Keys don't match!");
this.abort(JPAKE_ERROR_KEYMISMATCH);
return;
}
this._log.trace("Encrypting data.");
let iv, ciphertext, hmac;
try {
iv = Svc.Crypto.generateRandomIV();
ciphertext = Svc.Crypto.encrypt(this._data, this._crypto_key, iv);
hmac = Utils.sha256HMAC(ciphertext, this._hmac_key);
} catch (ex) {
this._log.error("Failed to encrypt data.");
this.abort(JPAKE_ERROR_INTERNAL);
return;
}
this._outgoing = {type: this._my_signerid + "3",
payload: {ciphertext: ciphertext, IV: iv, hmac: hmac}};
this._log.trace("Generated message " + this._outgoing.type);
callback();
},
_decryptData: function _decryptData(callback) {
this._log.trace("Verifying their key.");
if (this._incoming.type != this._their_signerid + "3") {
this._log.error("Invalid round 3 data: "
+ JSON.stringify(this._incoming));
this.abort(JPAKE_ERROR_WRONGMESSAGE);
return;
}
let step3 = this._incoming.payload;
try {
let hmac = Utils.sha256HMAC(step3.ciphertext, this._hmac_key);
if (hmac != step3.hmac)
throw "HMAC validation failed!";
} catch (ex) {
this._log.error("HMAC validation failed.");
this.abort(JPAKE_ERROR_KEYMISMATCH);
return;
}
this._log.trace("Decrypting data.");
let cleartext;
try {
cleartext = Svc.Crypto.decrypt(step3.ciphertext, this._crypto_key,
step3.IV);
} catch (ex) {
this._log.error("Failed to decrypt data.");
this.abort(JPAKE_ERROR_INTERNAL);
return;
}
try {
this._newData = JSON.parse(cleartext);
} catch (ex) {
this._log.error("Invalid data data: " + JSON.stringify(cleartext));
this.abort(JPAKE_ERROR_INVALID);
return;
}
this._log.trace("Decrypted data.");
callback();
},
_complete: function _complete() {
this._log.debug("Exchange completed.");
this._finished = true;
Utils.delay(function () { this.observer.onComplete(this._newData); },
0, this, "_timer_onComplete");
}
};

View File

@ -53,6 +53,7 @@ let lazies = {
"engines/passwords.js": ["PasswordEngine"],
"engines/tabs.js": ["TabEngine"],
"identity.js": ["Identity", "ID"],
"jpakeclient.js": ["JPAKEClient"],
"notifications.js": ["Notifications", "Notification", "NotificationButton"],
"resource.js": ["Resource"],
"service.js": ["Service"],

View File

@ -628,8 +628,27 @@ WeaveSvc.prototype = {
let cryptoResp = cryptoKeys.fetch(this.cryptoKeysURL).response;
if (cryptoResp.success) {
// On success, pass to CollectionKeys.
CollectionKeys.updateContents(syncKey, cryptoKeys);
// Don't want to wipe if we're just starting up!
// This is largely relevant because we don't persist
// CollectionKeys yet: Bug 610913.
let wasBlank = CollectionKeys.isClear;
let keysChanged = CollectionKeys.updateContents(syncKey, cryptoKeys);
if (keysChanged && !wasBlank) {
this._log.debug("Keys changed: " + JSON.stringify(keysChanged));
this._log.info("Resetting client to reflect key change.");
if (keysChanged.length) {
// Collection keys only. Reset individual engines.
this.resetClient(keysChanged);
}
else {
// Default key changed: wipe it all.
this.resetClient();
}
this._log.info("Downloaded new keys, client reset. Proceeding.");
}
return true;
}
else if (cryptoResp.status == 404) {
@ -650,8 +669,7 @@ WeaveSvc.prototype = {
// TODO: Um, what exceptions might we get here? Should we re-throw any?
// One kind of exception: HMAC failure.
let hmacFail = "Record SHA256 HMAC mismatch: ";
if (ex && ex.substr && (ex.substr(0, hmacFail.length) == hmacFail)) {
if (Utils.isHMACMismatch(ex)) {
Status.login = LOGIN_FAILED_INVALID_PASSPHRASE;
Status.sync = CREDENTIALS_CHANGED;
}
@ -669,6 +687,9 @@ WeaveSvc.prototype = {
// Must have got a 404, or no reported collection.
// Better make some and upload them.
this.generateNewSymmetricKeys();
// Oh, and reset the client so we reupload, too.
this.resetClient();
return true;
}

View File

@ -524,7 +524,19 @@ let Utils = {
return "No traceback available";
},
// Generator and discriminator for HMAC exceptions.
// Split these out in case we want to make them richer in future, and to
// avoid inevitable confusion if the message changes.
throwHMACMismatch: function throwHMACMismatch(shouldBe, is) {
throw "Record SHA256 HMAC mismatch: should be " + shouldBe + ", is " + is;
},
isHMACMismatch: function isHMACMismatch(ex) {
const hmacFail = "Record SHA256 HMAC mismatch: ";
return ex && ex.indexOf && (ex.indexOf(hmacFail) == 0);
},
checkStatus: function Weave_checkStatus(code, msg, ranges) {
if (!ranges)
ranges = [[200,300]];
@ -662,6 +674,23 @@ let Utils = {
return h.finish(false);
},
/**
* HMAC-based Key Derivation Step 2 according to RFC 5869.
*/
hkdfExpand: function hkdfExpand(prk, info, len) {
const BLOCKSIZE = 256 / 8;
let h = Utils.makeHMACHasher();
let T = "";
let Tn = "";
let iterations = Math.ceil(len/BLOCKSIZE);
for (let i = 0; i < iterations; i++) {
Tn = Utils.sha256HMACBytes(Tn + info + String.fromCharCode(i + 1),
Utils.makeHMACKey(prk), h);
T += Tn;
}
return T.slice(0, len);
},
byteArrayToString: function byteArrayToString(bytes) {
return [String.fromCharCode(byte) for each (byte in bytes)].join("");
},

View File

@ -17,6 +17,11 @@ pref("services.sync.engine.prefs", true);
pref("services.sync.engine.tabs", true);
pref("services.sync.engine.tabs.filteredUrls", "^(about:.*|chrome://weave/.*|wyciwyg:.*|file:.*)$");
pref("services.sync.jpake.serverURL", "https://setup.services.mozilla.com/");
pref("services.sync.jpake.pollInterval", 1000);
pref("services.sync.jpake.firstMsgMaxTries", 300);
pref("services.sync.jpake.maxTries", 10);
pref("services.sync.log.appender.console", "Warn");
pref("services.sync.log.appender.dump", "Error");
pref("services.sync.log.appender.debugLog", "Trace");
@ -25,6 +30,7 @@ pref("services.sync.log.rootLogger", "Debug");
pref("services.sync.log.logger.service.main", "Debug");
pref("services.sync.log.logger.authenticator", "Debug");
pref("services.sync.log.logger.network.resources", "Debug");
pref("services.sync.log.logger.service.jpakeclient", "Debug");
pref("services.sync.log.logger.engine.bookmarks", "Debug");
pref("services.sync.log.logger.engine.clients", "Debug");
pref("services.sync.log.logger.engine.forms", "Debug");

View File

@ -268,3 +268,57 @@ function sync_httpd_setup(handlers) {
= (new ServerWBO('global', {})).handler();
return httpd_setup(handlers);
}
/*
* Track collection modified times. Return closures.
*/
function track_collections_helper() {
/*
* Our tracking object.
*/
let collections = {};
/*
* Update the timestamp of a collection.
*/
function update_collection(coll) {
let timestamp = Date.now() / 1000;
collections[coll] = timestamp;
}
/*
* Invoke a handler, updating the collection's modified timestamp unless
* it's a GET request.
*/
function with_updated_collection(coll, f) {
return function(request, response) {
if (request.method != "GET")
update_collection(coll);
f.call(this, request, response);
};
}
/*
* Return the info/collections object.
*/
function info_collections(request, response) {
let body = "Error.";
switch(request.method) {
case "GET":
body = JSON.stringify(collections);
break;
default:
throw "Non-GET on info_collections.";
}
response.setHeader('X-Weave-Timestamp', ''+Date.now()/1000, false);
response.setStatusLine(request.httpVersion, 200, "OK");
response.bodyOutputStream.write(body, body.length);
}
return {"collections": collections,
"handler": info_collections,
"with_updated_collection": with_updated_collection,
"update_collection": update_collection};
}

View File

@ -0,0 +1,366 @@
Cu.import("resource://services-sync/log4moz.js");
Cu.import("resource://services-sync/jpakeclient.js");
Cu.import("resource://services-sync/constants.js");
Cu.import("resource://services-sync/auth.js");
Cu.import("resource://services-sync/util.js");
const JPAKE_LENGTH_SECRET = 8;
const JPAKE_LENGTH_CLIENTID = 256;
/*
* Simple server.
*/
function check_headers(request) {
// There shouldn't be any Basic auth
do_check_false(request.hasHeader("Authorization"));
// Ensure key exchange ID is set and the right length
do_check_true(request.hasHeader("X-KeyExchange-Id"));
do_check_eq(request.getHeader("X-KeyExchange-Id").length,
JPAKE_LENGTH_CLIENTID);
}
function new_channel() {
// Create a new channel and register it with the server.
let cid = Math.floor(Math.random() * 10000);
while (channels[cid])
cid = Math.floor(Math.random() * 10000);
let channel = channels[cid] = new ServerChannel();
server.registerPathHandler("/" + cid, channel.handler());
return cid;
}
let server;
let channels = {}; // Map channel -> ServerChannel object
function server_new_channel(request, response) {
check_headers(request);
let cid = new_channel();
let body = JSON.stringify("" + cid);
response.setStatusLine(request.httpVersion, 200, "OK");
response.bodyOutputStream.write(body, body.length);
}
let error_report;
function server_report(request, response) {
check_headers(request);
if (request.hasHeader("X-KeyExchange-Log"))
error_report = request.getHeader("X-KeyExchange-Log");
if (request.hasHeader("X-KeyExchange-Cid")) {
let cid = request.getHeader("X-KeyExchange-Cid");
let channel = channels[cid];
if (channel)
channel.clear();
}
response.setStatusLine(request.httpVersion, 200, "OK");
}
function ServerChannel() {
this.data = "{}";
this.getCount = 0;
}
ServerChannel.prototype = {
GET: function GET(request, response) {
if (!this.data) {
response.setStatusLine(request.httpVersion, 404, "Not Found");
return;
}
if (request.hasHeader("If-None-Match")) {
let etag = request.getHeader("If-None-Match");
if (etag == this._etag) {
response.setStatusLine(request.httpVersion, 304, "Not Modified");
return;
}
}
response.setStatusLine(request.httpVersion, 200, "OK");
response.bodyOutputStream.write(this.data, this.data.length);
// Automatically clear the channel after 6 successful GETs.
this.getCount += 1;
if (this.getCount == 6)
this.clear();
},
PUT: function PUT(request, response) {
this.data = readBytesFromInputStream(request.bodyInputStream);
this._etag = '"' + Utils.sha1(this.data) + '"';
response.setHeader("ETag", this._etag);
response.setStatusLine(request.httpVersion, 200, "OK");
},
clear: function clear() {
delete this.data;
},
handler: function handler() {
let self = this;
return function(request, response) {
check_headers(request);
let method = self[request.method];
return method.apply(self, arguments);
};
}
};
const DATA = {"msg": "eggstreamly sekrit"};
const POLLINTERVAL = 50;
function run_test() {
Svc.Prefs.set("jpake.serverURL", "http://localhost:8080/");
Svc.Prefs.set("jpake.pollInterval", POLLINTERVAL);
Svc.Prefs.set("jpake.maxTries", 5);
Svc.Prefs.set("jpake.firstMsgMaxTries", 5);
// Ensure clean up
Svc.Obs.add("profile-before-change", function() {
Svc.Prefs.resetBranch("");
});
// Ensure PSM is initialized.
Cc["@mozilla.org/psm;1"].getService(Ci.nsISupports);
// Simulate Sync setup with a default authenticator in place. We
// want to make sure the J-PAKE requests don't include those data.
Auth.defaultAuthenticator = new BasicAuthenticator(
new Identity("Some Realm", "johndoe"));
server = httpd_setup({"/new_channel": server_new_channel,
"/report": server_report});
function tearDown() {
server.stop(do_test_finished);
}
initTestLogging("Trace");
do_test_pending();
Utils.asyncChain(test_success_receiveNoPIN,
test_firstMsgMaxTries,
test_wrongPIN,
test_abort_receiver,
test_abort_sender,
test_wrongmessage,
test_error_channel,
test_error_network,
tearDown
)();
}
function test_success_receiveNoPIN(next) {
_("Test a successful exchange started by receiveNoPIN().");
let snd = new JPAKEClient({
displayPIN: function displayPIN() {
do_throw("displayPIN shouldn't have been called!");
},
onAbort: function onAbort(error) {
do_throw("Shouldn't have aborted!" + error);
},
onComplete: function onComplete() {}
});
let rec = new JPAKEClient({
displayPIN: function displayPIN(pin) {
_("Received PIN " + pin + ". Entering it in the other computer...");
this.cid = pin.slice(JPAKE_LENGTH_SECRET);
Utils.delay(function() { snd.sendWithPIN(pin, DATA); }, 0,
this, "_timer");
},
onAbort: function onAbort(error) {
do_throw("Shouldn't have aborted! " + error);
},
onComplete: function onComplete(a) {
// Ensure channel was cleared, no error report.
do_check_eq(channels[this.cid].data, undefined);
do_check_eq(error_report, undefined);
next();
}
});
rec.receiveNoPIN();
}
function test_firstMsgMaxTries(next) {
_("Test abort when sender doesn't upload anything.");
let rec = new JPAKEClient({
displayPIN: function displayPIN(pin) {
_("Received PIN " + pin + ". Doing nothing...");
this.cid = pin.slice(JPAKE_LENGTH_SECRET);
},
onAbort: function onAbort(error) {
do_check_eq(error, JPAKE_ERROR_TIMEOUT);
// Ensure channel was cleared, error report was sent.
do_check_eq(channels[this.cid].data, undefined);
do_check_eq(error_report, JPAKE_ERROR_TIMEOUT);
error_report = undefined;
next();
},
onComplete: function onComplete() {
do_throw("Shouldn't have completed! ");
}
});
rec.receiveNoPIN();
}
function test_wrongPIN(next) {
_("Test abort when PINs don't match.");
let snd = new JPAKEClient({
displayPIN: function displayPIN() {
do_throw("displayPIN shouldn't have been called!");
},
onAbort: function onAbort(error) {
do_check_eq(error, JPAKE_ERROR_KEYMISMATCH);
do_check_eq(error_report, JPAKE_ERROR_KEYMISMATCH);
error_report = undefined;
},
onComplete: function onComplete() {
do_throw("Shouldn't have completed!");
}
});
let rec = new JPAKEClient({
displayPIN: function displayPIN(pin) {
this.cid = pin.slice(JPAKE_LENGTH_SECRET);
let secret = pin.slice(0, JPAKE_LENGTH_SECRET);
secret = [char for each (char in secret)].reverse().join("");
let new_pin = secret + this.cid;
_("Received PIN " + pin + ", but I'm entering " + new_pin);
Utils.delay(function() { snd.sendWithPIN(new_pin, DATA); }, 0,
this, "_timer");
},
onAbort: function onAbort(error) {
do_check_eq(error, JPAKE_ERROR_NODATA);
// Ensure channel was cleared.
do_check_eq(channels[this.cid].data, undefined);
next();
},
onComplete: function onComplete() {
do_throw("Shouldn't have completed! ");
}
});
rec.receiveNoPIN();
}
function test_abort_receiver(next) {
_("Test user abort on receiving side.");
let rec = new JPAKEClient({
onComplete: function onComplete(data) {
do_throw("onComplete shouldn't be called.");
},
onAbort: function onAbort(error) {
// Manual abort = no error
do_check_eq(error, undefined);
// Ensure channel was cleared, no error report.
do_check_eq(channels[this.cid].data, undefined);
do_check_eq(error_report, undefined);
next();
},
displayPIN: function displayPIN(pin) {
this.cid = pin.slice(JPAKE_LENGTH_SECRET);
Utils.delay(function() { rec.abort(); }, 0, this, "_timer");
}
});
rec.receiveNoPIN();
}
function test_abort_sender(next) {
_("Test user abort on sending side.");
let snd = new JPAKEClient({
displayPIN: function displayPIN() {
do_throw("displayPIN shouldn't have been called!");
},
onAbort: function onAbort(error) {
// Manual abort == no error.
do_check_eq(error, undefined);
},
onComplete: function onComplete() {
do_throw("Shouldn't have completed!");
}
});
let rec = new JPAKEClient({
onComplete: function onComplete(data) {
do_throw("onComplete shouldn't be called.");
},
onAbort: function onAbort(error) {
do_check_eq(error, JPAKE_ERROR_NODATA);
// Ensure channel was cleared, no error report.
do_check_eq(channels[this.cid].data, undefined);
do_check_eq(error_report, undefined);
next();
},
displayPIN: function displayPIN(pin) {
_("Received PIN " + pin + ". Entering it in the other computer...");
this.cid = pin.slice(JPAKE_LENGTH_SECRET);
Utils.delay(function() { snd.sendWithPIN(pin, DATA); }, 0,
this, "_timer");
Utils.delay(function() { snd.abort(); }, POLLINTERVAL,
this, "_abortTimer");
}
});
rec.receiveNoPIN();
}
function test_wrongmessage(next) {
let cid = new_channel();
channels[cid].data = JSON.stringify({type: "receiver2", payload: {}});
let snd = new JPAKEClient({
onComplete: function onComplete(data) {
do_throw("onComplete shouldn't be called.");
},
onAbort: function onAbort(error) {
do_check_eq(error, JPAKE_ERROR_WRONGMESSAGE);
next();
}
});
snd.sendWithPIN("01234567" + cid, DATA);
}
function test_error_channel(next) {
Svc.Prefs.set("jpake.serverURL", "http://localhost:12345/");
let rec = new JPAKEClient({
onComplete: function onComplete(data) {
do_throw("onComplete shouldn't be called.");
},
onAbort: function onAbort(error) {
do_check_eq(error, JPAKE_ERROR_CHANNEL);
Svc.Prefs.reset("jpake.serverURL");
next();
},
displayPIN: function displayPIN(pin) {}
});
rec.receiveNoPIN();
}
function test_error_network(next) {
Svc.Prefs.set("jpake.serverURL", "http://localhost:12345/");
let snd = new JPAKEClient({
onComplete: function onComplete(data) {
do_throw("onComplete shouldn't be called.");
},
onAbort: function onAbort(error) {
do_check_eq(error, JPAKE_ERROR_NETWORK);
Svc.Prefs.reset("jpake.serverURL");
next();
}
});
snd.sendWithPIN("0123456789ab", DATA);
}

View File

@ -28,6 +28,13 @@ function test_repeated_hmac() {
do_check_eq(one, two);
}
function do_check_array_eq(a1, a2) {
do_check_eq(a1.length, a2.length);
for (let i = 0; i < a1.length; ++i) {
do_check_eq(a1[i], a2[i]);
}
}
function test_keymanager() {
let testKey = "ababcdefabcdefabcdefabcdef";
@ -36,11 +43,12 @@ function test_keymanager() {
// Decode the key here to mirror what generateEntry will do,
// but pass it encoded into the KeyBundle call below.
let sha256inputE = Utils.makeHMACKey("" + HMAC_INPUT + username + "\x01");
let encryptKey = Utils.sha256HMACBytes(Utils.decodeKeyBase32(testKey), sha256inputE);
let sha256inputE = "" + HMAC_INPUT + username + "\x01";
let key = Utils.makeHMACKey(Utils.decodeKeyBase32(testKey));
let encryptKey = Utils.sha256HMACBytes(sha256inputE, key);
let sha256inputH = Utils.makeHMACKey(encryptKey + HMAC_INPUT + username + "\x02");
let hmacKey = Utils.sha256HMACBytes(Utils.decodeKeyBase32(testKey), sha256inputH);
let sha256inputH = encryptKey + HMAC_INPUT + username + "\x02";
let hmacKey = Utils.sha256HMACBytes(sha256inputH, key);
// Encryption key is stored in base64 for WeaveCrypto convenience.
do_check_eq(btoa(encryptKey), new SyncKeyBundle(null, username, testKey).encryptionKey);
@ -97,9 +105,10 @@ function test_collections_manager() {
log.info("Updating CollectionKeys.");
// updateContents decrypts the object, but it also returns the payload
// for us to use.
let payload = CollectionKeys.updateContents(keyBundle, storage_keys);
// updateContents decrypts the object, releasing the payload for us to use.
// Returns true, because the default key has changed.
do_check_true(CollectionKeys.updateContents(keyBundle, storage_keys));
let payload = storage_keys.cleartext;
_("CK: " + JSON.stringify(CollectionKeys._collections));
@ -107,6 +116,7 @@ function test_collections_manager() {
let wbo = CollectionKeys.asWBO("crypto", "keys");
_("WBO: " + JSON.stringify(wbo));
_("WBO cleartext: " + JSON.stringify(wbo.cleartext));
// Check the individual contents.
do_check_eq(wbo.collection, "crypto");
@ -119,6 +129,10 @@ function test_collections_manager() {
do_check_true('bookmarks' in CollectionKeys._collections);
do_check_false('tabs' in CollectionKeys._collections);
_("Updating contents twice with the same data doesn't proceed.");
storage_keys.encrypt(keyBundle);
do_check_false(CollectionKeys.updateContents(keyBundle, storage_keys));
/*
* Test that we get the right keys out when we ask for
* a collection's tokens.
@ -128,8 +142,16 @@ function test_collections_manager() {
let b2 = CollectionKeys.keyForCollection("bookmarks");
do_check_keypair_eq(b1.keyPair, b2.keyPair);
// Check key equality.
do_check_true(b1.equals(b2));
do_check_true(b2.equals(b1));
b1 = new BulkKeyBundle(null, "[default]");
b1.keyPair = [default_key64, default_hmac64];
do_check_false(b1.equals(b2));
do_check_false(b2.equals(b1));
b2 = CollectionKeys.keyForCollection(null);
do_check_keypair_eq(b1.keyPair, b2.keyPair);
@ -145,6 +167,51 @@ function test_collections_manager() {
CollectionKeys._lastModified = null;
do_check_true(CollectionKeys.updateNeeded({}));
/*
* Check _compareKeyBundleCollections.
*/
function newBundle(name) {
let r = new BulkKeyBundle(null, name);
r.generateRandom();
return r;
}
let k1 = newBundle("k1");
let k2 = newBundle("k2");
let k3 = newBundle("k3");
let k4 = newBundle("k4");
let k5 = newBundle("k5");
let coll1 = {"foo": k1, "bar": k2};
let coll2 = {"foo": k1, "bar": k2};
let coll3 = {"foo": k1, "bar": k3};
let coll4 = {"foo": k4};
let coll5 = {"baz": k5, "bar": k2};
let coll6 = {};
let d1 = CollectionKeys._compareKeyBundleCollections(coll1, coll2); // []
let d2 = CollectionKeys._compareKeyBundleCollections(coll1, coll3); // ["bar"]
let d3 = CollectionKeys._compareKeyBundleCollections(coll3, coll2); // ["bar"]
let d4 = CollectionKeys._compareKeyBundleCollections(coll1, coll4); // ["bar", "foo"]
let d5 = CollectionKeys._compareKeyBundleCollections(coll5, coll2); // ["baz", "foo"]
let d6 = CollectionKeys._compareKeyBundleCollections(coll6, coll1); // ["bar", "foo"]
let d7 = CollectionKeys._compareKeyBundleCollections(coll5, coll5); // []
let d8 = CollectionKeys._compareKeyBundleCollections(coll6, coll6); // []
do_check_true(d1.same);
do_check_false(d2.same);
do_check_false(d3.same);
do_check_false(d4.same);
do_check_false(d5.same);
do_check_false(d6.same);
do_check_true(d7.same);
do_check_true(d8.same);
do_check_array_eq(d1.changed, []);
do_check_array_eq(d2.changed, ["bar"]);
do_check_array_eq(d3.changed, ["bar"]);
do_check_array_eq(d4.changed, ["bar", "foo"]);
do_check_array_eq(d5.changed, ["baz", "foo"]);
do_check_array_eq(d6.changed, ["bar", "foo"]);
}
// Make sure that KeyBundles work when persisted through Identity.

View File

@ -104,7 +104,7 @@ function run_test() {
catch(ex) {
error = ex;
}
do_check_eq(error.substr(0, 32), "Record SHA256 HMAC mismatch: foo");
do_check_eq(error.substr(0, 42), "Record SHA256 HMAC mismatch: should be foo");
// Checking per-collection keys and default key handling.

View File

@ -3,8 +3,8 @@ Cu.import("resource://services-sync/constants.js");
Cu.import("resource://services-sync/base_records/crypto.js");
/**
* Testing the SHA256-HMAC key derivation process against st3fan's implementation
* in Firefox Home.
* Testing the SHA256-HMAC key derivation process against test vectors
* verified with the Firefox Home implementation.
*/
function run_test() {
@ -12,8 +12,8 @@ function run_test() {
let bundle = new SyncKeyBundle(PWDMGR_PASSPHRASE_REALM, "st3fan", "q7ynpwq7vsc9m34hankbyi3s3i");
// These should be compared to the results from Home, as they once were.
let e = "3fe2d3743fe03d4f460ce2405ec189e68dfd7e42c97d50fab9bda3761263cc87";
let h = "bf05f720423d297e8fd55faee7cdeaf32aa15cfb6e56115268c9c326b999795a";
let e = "14b8c09fa84e92729ee695160af6e0385f8f6215a25d14906e1747bdaa2de426";
let h = "370e3566245d79fe602a3adb5137e42439cd2a571235197e0469d7d541b07875";
// The encryption key is stored as base64 for handing off to WeaveCrypto.
let realE = Utils.bytesAsHex(atob(bundle.encryptionKey));

View File

@ -1,9 +1,12 @@
Cu.import("resource://services-sync/main.js");
Cu.import("resource://services-sync/service.js");
Cu.import("resource://services-sync/engines.js");
Cu.import("resource://services-sync/util.js");
Cu.import("resource://services-sync/status.js");
Cu.import("resource://services-sync/constants.js");
Cu.import("resource://services-sync/base_records/wbo.js"); // For Records.
Cu.import("resource://services-sync/base_records/crypto.js"); // For CollectionKeys.
Cu.import("resource://services-sync/engines/tabs.js");
Cu.import("resource://services-sync/log4moz.js");
function run_test() {
@ -14,22 +17,55 @@ function run_test() {
let clients = new ServerCollection();
let meta_global = new ServerWBO('global');
let collections = {};
function info_collections(request, response) {
let body = JSON.stringify(collections);
response.setStatusLine(request.httpVersion, 200, "OK");
response.bodyOutputStream.write(body, body.length);
}
// Tracking info/collections.
let collectionsHelper = track_collections_helper();
let upd = collectionsHelper.with_updated_collection;
let collections = collectionsHelper.collections;
do_test_pending();
let keysWBO = new ServerWBO("keys");
let server = httpd_setup({
"/1.0/johndoe/storage/crypto/keys": new ServerWBO().handler(),
"/1.0/johndoe/storage/clients": clients.handler(),
"/1.0/johndoe/storage/meta/global": meta_global.handler(),
"/1.0/johndoe/info/collections": info_collections
// Special.
"/1.0/johndoe/info/collections": collectionsHelper.handler,
"/1.0/johndoe/storage/crypto/keys": upd("crypto", keysWBO.handler()),
"/1.0/johndoe/storage/meta/global": upd("meta", meta_global.handler()),
// Track modified times.
"/1.0/johndoe/storage/clients": upd("clients", clients.handler()),
"/1.0/johndoe/storage/tabs": upd("tabs", new ServerCollection().handler()),
// Just so we don't get 404s in the logs.
"/1.0/johndoe/storage/bookmarks": new ServerCollection().handler(),
"/1.0/johndoe/storage/forms": new ServerCollection().handler(),
"/1.0/johndoe/storage/history": new ServerCollection().handler(),
"/1.0/johndoe/storage/passwords": new ServerCollection().handler(),
"/1.0/johndoe/storage/prefs": new ServerCollection().handler()
});
try {
Svc.Prefs.set("registerEngines", "Tab");
_("Set up some tabs.");
let myTabs =
{windows: [{tabs: [{index: 1,
entries: [{
url: "http://foo.com/",
title: "Title"
}],
attributes: {
image: "image"
},
extData: {
weaveLastUsed: 1
}}]}]};
delete Svc.Session;
Svc.Session = {
getBrowserState: function () JSON.stringify(myTabs)
};
Weave.Service._registerEngines();
_("Logging in.");
Weave.Service.serverURL = "http://localhost:8080/";
Weave.Service.clusterURL = "http://localhost:8080/";
@ -39,10 +75,11 @@ function run_test() {
do_check_true(Weave.Service._remoteSetup());
function test_out_of_date() {
_("meta_global: " + JSON.stringify(meta_global));
meta_global.payload = {"syncID": "foooooooooooooooooooooooooo",
"storageVersion": STORAGE_VERSION + 1};
_("meta_global: " + JSON.stringify(meta_global));
_("Old meta/global: " + JSON.stringify(meta_global));
meta_global.payload = JSON.stringify({"syncID": "foooooooooooooooooooooooooo",
"storageVersion": STORAGE_VERSION + 1});
collections.meta = Date.now() / 1000;
_("New meta/global: " + JSON.stringify(meta_global));
Records.set(Weave.Service.metaURL, meta_global);
try {
Weave.Service.sync();
@ -60,6 +97,89 @@ function run_test() {
_("Syncing after server has been upgraded and wiped.");
Weave.Service.wipeServer();
test_out_of_date();
// Now's a great time to test what happens when keys get replaced.
_("Syncing afresh...");
Weave.Service.logout();
CollectionKeys.clear();
Weave.Service.serverURL = "http://localhost:8080/";
Weave.Service.clusterURL = "http://localhost:8080/";
meta_global.payload = JSON.stringify({"syncID": "foooooooooooooobbbbbbbbbbbb",
"storageVersion": STORAGE_VERSION});
collections.meta = Date.now() / 1000;
Records.set(Weave.Service.metaURL, meta_global);
Weave.Service.login("johndoe", "ilovejane", passphrase);
do_check_true(Weave.Service.isLoggedIn);
Weave.Service.sync();
do_check_true(Weave.Service.isLoggedIn);
let serverDecrypted;
let serverKeys;
let serverResp;
function retrieve_server_default() {
serverKeys = serverResp = serverDecrypted = null;
serverKeys = new CryptoWrapper("crypto", "keys");
serverResp = serverKeys.fetch(Weave.Service.cryptoKeysURL).response;
do_check_true(serverResp.success);
serverDecrypted = serverKeys.decrypt(Weave.Service.syncKeyBundle);
_("Retrieved WBO: " + JSON.stringify(serverDecrypted));
_("serverKeys: " + JSON.stringify(serverKeys));
return serverDecrypted.default;
}
function retrieve_and_compare_default(should_succeed) {
let serverDefault = retrieve_server_default();
let localDefault = CollectionKeys.keyForCollection().keyPair;
_("Retrieved keyBundle: " + JSON.stringify(serverDefault));
_("Local keyBundle: " + JSON.stringify(localDefault));
if (should_succeed)
do_check_eq(JSON.stringify(serverDefault), JSON.stringify(localDefault));
else
do_check_neq(JSON.stringify(serverDefault), JSON.stringify(localDefault));
}
// Uses the objects set above.
function set_server_keys(pair) {
serverDecrypted.default = pair;
serverKeys.cleartext = serverDecrypted;
serverKeys.encrypt(Weave.Service.syncKeyBundle);
serverKeys.upload(Weave.Service.cryptoKeysURL);
}
_("Checking we have the latest keys.");
retrieve_and_compare_default(true);
_("Update keys on server.");
set_server_keys(["KaaaaaaaaaaaHAtfmuRY0XEJ7LXfFuqvF7opFdBD/MY=",
"aaaaaaaaaaaapxMO6TEWtLIOv9dj6kBAJdzhWDkkkis="]);
_("Checking that we no longer have the latest keys.");
retrieve_and_compare_default(false);
_("Indeed, they're what we set them to...");
do_check_eq("KaaaaaaaaaaaHAtfmuRY0XEJ7LXfFuqvF7opFdBD/MY=",
retrieve_server_default()[0]);
_("Sync. Should download changed keys automatically.");
let oldClientsModified = collections.clients;
let oldTabsModified = collections.tabs;
Weave.Service.login("johndoe", "ilovejane", passphrase);
Weave.Service.sync();
_("New key should have forced upload of data.");
_("Tabs: " + oldTabsModified + " < " + collections.tabs);
_("Clients: " + oldClientsModified + " < " + collections.clients);
do_check_true(collections.clients > oldClientsModified);
do_check_true(collections.tabs > oldTabsModified);
_("... and keys will now match.");
retrieve_and_compare_default(true);
} finally {
Weave.Svc.Prefs.resetBranch("");

View File

@ -17,13 +17,10 @@ function run_test() {
let clients = new ServerCollection();
let meta_global = new ServerWBO('global');
let collections = {};
function info_collections(request, response) {
let body = JSON.stringify(collections);
response.setStatusLine(request.httpVersion, 200, "OK");
response.bodyOutputStream.write(body, body.length);
}
let collectionsHelper = track_collections_helper();
let upd = collectionsHelper.with_updated_collection;
let collections = collectionsHelper.collections;
function wasCalledHandler(wbo) {
let handler = wbo.handler();
return function() {
@ -34,10 +31,10 @@ function run_test() {
do_test_pending();
let server = httpd_setup({
"/1.0/johndoe/storage/crypto/keys": new ServerWBO().handler(),
"/1.0/johndoe/storage/clients": clients.handler(),
"/1.0/johndoe/storage/meta/global": wasCalledHandler(meta_global),
"/1.0/johndoe/info/collections": info_collections
"/1.0/johndoe/storage/crypto/keys": upd("crypto", new ServerWBO("keys").handler()),
"/1.0/johndoe/storage/clients": upd("clients", clients.handler()),
"/1.0/johndoe/storage/meta/global": upd("meta", wasCalledHandler(meta_global)),
"/1.0/johndoe/info/collections": collectionsHelper.handler
});
try {
@ -50,7 +47,7 @@ function run_test() {
do_check_eq(Status.sync, CREDENTIALS_CHANGED);
do_check_eq(Status.login, LOGIN_FAILED_INVALID_PASSPHRASE);
Weave.Service.login("johndoe", "ilovejane", "foo");
Weave.Service.login("johndoe", "ilovejane", "abcdeabcdeabcdeabcdeabcdea");
do_check_true(Weave.Service.isLoggedIn);
_("Checking that remoteSetup returns true when credentials have changed.");
@ -71,8 +68,8 @@ function run_test() {
do_check_eq(meta_global.data.engines.clients.syncID, Weave.Clients.syncID);
_("Set the collection info hash so that sync() will remember the modified times for future runs.");
collections = {meta: Weave.Clients.lastSync,
clients: Weave.Clients.lastSync};
collections.meta = Weave.Clients.lastSync;
collections.clients = Weave.Clients.lastSync;
Weave.Service.sync();
_("Sync again and verify that meta/global wasn't downloaded again");
@ -104,7 +101,7 @@ function run_test() {
// server, just as might happen with a second client.
_("Attempting to screw up HMAC by re-encrypting keys.");
let keys = CollectionKeys.asWBO();
let b = new BulkKeyBundle();
let b = new BulkKeyBundle("hmacerror", "hmacerror");
b.generateRandom();
collections.crypto = keys.modified = 100 + (Date.now()/1000); // Future modification time.
keys.encrypt(b);

View File

@ -44,49 +44,21 @@ StirlingEngine.prototype = {
};
Engines.register(StirlingEngine);
let collections = {};
// Tracking info/collections.
let collectionsHelper = track_collections_helper();
let upd = collectionsHelper.with_updated_collection;
function update_collection(coll) {
let timestamp = Date.now() / 1000;
collections[coll] = timestamp;
}
function with_updated_collection(coll, f) {
return function(request, response) {
if (request.method != "GET")
update_collection(coll);
f.call(this, request, response);
};
}
function info_collections(request, response) {
let body = "Error.";
switch(request.method) {
case "GET":
body = JSON.stringify(collections);
break;
default:
throw "Non-GET on info_collections.";
}
response.setHeader('X-Weave-Timestamp', ''+Date.now()/1000, false);
response.setStatusLine(request.httpVersion, 200, "OK");
response.bodyOutputStream.write(body, body.length);
}
function sync_httpd_setup(handlers) {
collections = {};
handlers["/1.0/johndoe/info/collections"] = info_collections;
handlers["/1.0/johndoe/info/collections"] = collectionsHelper.handler;
let cr = new ServerWBO("keys");
handlers["/1.0/johndoe/storage/crypto/keys"] =
with_updated_collection("crypto", cr.handler());
upd("crypto", cr.handler());
let cl = new ServerCollection();
handlers["/1.0/johndoe/storage/clients"] =
with_updated_collection("clients", cl.handler());
upd("clients", cl.handler());
return httpd_setup(handlers);
}
@ -250,10 +222,10 @@ function test_disabledRemotelyTwoClients() {
engines: {}});
let server = sync_httpd_setup({
"/1.0/johndoe/storage/meta/global":
with_updated_collection("meta", metaWBO.handler()),
upd("meta", metaWBO.handler()),
"/1.0/johndoe/storage/steam":
with_updated_collection("steam", new ServerWBO("steam", {}).handler())
upd("steam", new ServerWBO("steam", {}).handler())
});
do_test_pending();
setUp();

View File

@ -931,7 +931,7 @@ function test_canDecrypt_noCryptoKeys() {
Svc.Prefs.set("username", "foo");
// Wipe CollectionKeys so we can test the desired scenario.
CollectionKeys.setContents({"collections": {}, "default": null});
CollectionKeys.clear();
let collection = new ServerCollection();
collection.wbos.flying = new ServerWBO(

View File

@ -0,0 +1,100 @@
Cu.import("resource://services-sync/util.js");
// Test vectors from RFC 5869
// Test case 1
let tc1 = {
IKM: "0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b",
salt: "000102030405060708090a0b0c",
info: "f0f1f2f3f4f5f6f7f8f9",
L: 42,
PRK: "077709362c2e32df0ddc3f0dc47bba63" +
"90b6c73bb50f9c3122ec844ad7c2b3e5",
OKM: "3cb25f25faacd57a90434f64d0362f2a" +
"2d2d0a90cf1a5a4c5db02d56ecc4c5bf" +
"34007208d5b887185865"
};
// Test case 2
let tc2 = {
IKM: "000102030405060708090a0b0c0d0e0f" +
"101112131415161718191a1b1c1d1e1f" +
"202122232425262728292a2b2c2d2e2f" +
"303132333435363738393a3b3c3d3e3f" +
"404142434445464748494a4b4c4d4e4f",
salt: "606162636465666768696a6b6c6d6e6f" +
"707172737475767778797a7b7c7d7e7f" +
"808182838485868788898a8b8c8d8e8f" +
"909192939495969798999a9b9c9d9e9f" +
"a0a1a2a3a4a5a6a7a8a9aaabacadaeaf",
info: "b0b1b2b3b4b5b6b7b8b9babbbcbdbebf" +
"c0c1c2c3c4c5c6c7c8c9cacbcccdcecf" +
"d0d1d2d3d4d5d6d7d8d9dadbdcdddedf" +
"e0e1e2e3e4e5e6e7e8e9eaebecedeeef" +
"f0f1f2f3f4f5f6f7f8f9fafbfcfdfeff",
L: 82,
PRK: "06a6b88c5853361a06104c9ceb35b45c" +
"ef760014904671014a193f40c15fc244",
OKM: "b11e398dc80327a1c8e7f78c596a4934" +
"4f012eda2d4efad8a050cc4c19afa97c" +
"59045a99cac7827271cb41c65e590e09" +
"da3275600c2f09b8367793a9aca3db71" +
"cc30c58179ec3e87c14c01d5c1f3434f" +
"1d87"
};
// Test case 3
let tc3 = {
IKM: "0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b",
salt: "",
info: "",
L: 42,
PRK: "19ef24a32c717b167f33a91d6f648bdf" +
"96596776afdb6377ac434c1c293ccb04",
OKM: "8da4e775a563c18f715f802a063c5a31" +
"b8a11f5c5ee1879ec3454e5f3c738d2d" +
"9d201395faa4b61a96c8"
};
function _hexToString(hex) {
let ret = "";
if (hex.length % 2 != 0) {
return false;
}
for (let i = 0; i < hex.length; i += 2) {
let cur = hex[i] + hex[i + 1];
ret += String.fromCharCode(parseInt(cur, 16));
}
return ret;
}
function extract_hex(salt, ikm) {
salt = _hexToString(salt);
ikm = _hexToString(ikm);
return Utils.bytesAsHex(
Utils.sha256HMACBytes(ikm, Utils.makeHMACKey(salt)));
}
function expand_hex(prk, info, len) {
prk = _hexToString(prk);
info = _hexToString(info);
return Utils.bytesAsHex(Utils.hkdfExpand(prk, info, len));
}
function run_test() {
_("Verifying Test Case 1");
do_check_eq(extract_hex(tc1.salt, tc1.IKM), tc1.PRK);
do_check_eq(expand_hex(tc1.PRK, tc1.info, tc1.L), tc1.OKM);
_("Verifying Test Case 2");
do_check_eq(extract_hex(tc2.salt, tc2.IKM), tc2.PRK);
do_check_eq(expand_hex(tc2.PRK, tc2.info, tc2.L), tc2.OKM);
_("Verifying Test Case 3");
do_check_eq(extract_hex(tc3.salt, tc3.IKM), tc3.PRK);
do_check_eq(expand_hex(tc3.PRK, tc3.info, tc3.L), tc3.OKM);
}

View File

@ -36,4 +36,17 @@ function run_test() {
do_check_eq(Utils.sha256HMACBytes(d1, Utils.makeHMACKey(k1)), o1);
do_check_eq(Utils.sha256HMAC(d2, Utils.makeHMACKey(k2)), o2);
// Checking HMAC exceptions.
let ex;
try {
Utils.throwHMACMismatch("aaa", "bbb");
}
catch (e) {
ex = e;
}
do_check_true(Utils.isHMACMismatch(ex));
do_check_false(!!Utils.isHMACMismatch(new Error()));
do_check_false(!!Utils.isHMACMismatch(null));
do_check_false(!!Utils.isHMACMismatch("Error"));
}