mirror of
https://github.com/mozilla/gecko-dev.git
synced 2024-11-25 05:41:12 +00:00
Bug 617531 - Merge fx-sync to mozilla-central. a=blockers
This commit is contained in:
commit
9de6f8bfc9
@ -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);
|
||||
}
|
||||
};
|
||||
|
@ -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",
|
||||
|
588
services/sync/modules/jpakeclient.js
Normal file
588
services/sync/modules/jpakeclient.js
Normal 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");
|
||||
}
|
||||
|
||||
};
|
@ -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"],
|
||||
|
@ -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;
|
||||
}
|
||||
|
||||
|
@ -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("");
|
||||
},
|
||||
|
@ -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");
|
||||
|
@ -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};
|
||||
}
|
||||
|
366
services/sync/tests/unit/test_jpakeclient.js
Normal file
366
services/sync/tests/unit/test_jpakeclient.js
Normal 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);
|
||||
}
|
@ -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.
|
||||
|
@ -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.
|
||||
|
||||
|
@ -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));
|
||||
|
@ -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("");
|
||||
|
@ -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);
|
||||
|
@ -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();
|
||||
|
@ -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(
|
||||
|
100
services/sync/tests/unit/test_utils_hkdfExpand.js
Normal file
100
services/sync/tests/unit/test_utils_hkdfExpand.js
Normal 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);
|
||||
}
|
@ -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"));
|
||||
}
|
||||
|
Loading…
Reference in New Issue
Block a user