Bug 689428 - Part 1: Implement KeyExchange v3 in JPAKEClient. r=rnewman

This commit is contained in:
Philipp von Weitershausen 2011-10-02 01:15:39 -07:00
Родитель 018072a7c4
Коммит f0713e0255
4 изменённых файлов: 362 добавлений и 108 удалений

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

@ -185,6 +185,7 @@ JPAKE_ERROR_NODATA: "jpake.error.nodata",
JPAKE_ERROR_KEYMISMATCH: "jpake.error.keymismatch", JPAKE_ERROR_KEYMISMATCH: "jpake.error.keymismatch",
JPAKE_ERROR_WRONGMESSAGE: "jpake.error.wrongmessage", JPAKE_ERROR_WRONGMESSAGE: "jpake.error.wrongmessage",
JPAKE_ERROR_USERABORT: "jpake.error.userabort", JPAKE_ERROR_USERABORT: "jpake.error.userabort",
JPAKE_ERROR_DELAYUNSUPPORTED: "jpake.error.delayunsupported",
// info types for Service.getStorageInfo // info types for Service.getStorageInfo
INFO_COLLECTIONS: "collections", INFO_COLLECTIONS: "collections",

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

@ -48,6 +48,8 @@ Cu.import("resource://services-sync/util.js");
const EXPORTED_SYMBOLS = ["JPAKEClient"]; const EXPORTED_SYMBOLS = ["JPAKEClient"];
const REQUEST_TIMEOUT = 60; // 1 minute const REQUEST_TIMEOUT = 60; // 1 minute
const KEYEXCHANGE_VERSION = 3;
const JPAKE_SIGNERID_SENDER = "sender"; const JPAKE_SIGNERID_SENDER = "sender";
const JPAKE_SIGNERID_RECEIVER = "receiver"; const JPAKE_SIGNERID_RECEIVER = "receiver";
const JPAKE_LENGTH_SECRET = 8; const JPAKE_LENGTH_SECRET = 8;
@ -55,50 +57,63 @@ const JPAKE_LENGTH_CLIENTID = 256;
const JPAKE_VERIFY_VALUE = "0123456789ABCDEF"; const JPAKE_VERIFY_VALUE = "0123456789ABCDEF";
/* /**
* Client to exchange encrypted data using the J-PAKE algorithm. * Client to exchange encrypted data using the J-PAKE algorithm.
* The exchange between two clients of this type looks like this: * The exchange between two clients of this type looks like this:
* *
* *
* Client A Server Client B * Mobile Server Desktop
* ================================================================== * ===================================================================
* | * |
* retrieve channel <---------------| * retrieve channel <---------------|
* generate random secret | * generate random secret |
* show PIN = secret + channel | ask user for PIN * show PIN = secret + channel | ask user for PIN
* upload A's message 1 ----------->| * upload Mobile's message 1 ------>|
* |--------> retrieve A's message 1 * |----> retrieve Mobile's message 1
* |<---------- upload B's message 1 * |<----- upload Desktop's message 1
* retrieve B's message 1 <---------| * retrieve Desktop's message 1 <---|
* upload A's message 2 ----------->| * upload Mobile's message 2 ------>|
* |--------> retrieve A's message 2 * |----> retrieve Mobile's message 2
* | compute key * | compute key
* |<---------- upload B's message 2 * |<----- upload Desktop's message 2
* retrieve B's message 2 <---------| * retrieve Desktop's message 2 <---|
* compute key | * compute key |
* upload sha256d(key) ------------>| * encrypt known value ------------>|
* |---------> retrieve sha256d(key) * |-------> retrieve encrypted value
* | verify against own key * | verify against local known value
* | encrypt data *
* |<------------------- upload data * At this point Desktop knows whether the PIN was entered correctly.
* retrieve data <------------------| * If it wasn't, Desktop deletes the session. If it was, the account
* verify HMAC | * setup can proceed. If Desktop doesn't yet have an account set up,
* decrypt data | * it will keep the channel open and let the user connect to or
* create an account.
*
* | encrypt credentials
* |<------------- upload credentials
* retrieve credentials <-----------|
* verify HMAC |
* decrypt credentials |
* delete session ----------------->|
* start syncing |
* *
* *
* Create a client object like so: * Create a client object like so:
* *
* let client = new JPAKEClient(observer); * let client = new JPAKEClient(controller);
* *
* The 'observer' object must implement the following methods: * The 'controller' object must implement the following methods:
* *
* displayPIN(pin) -- Display the PIN to the user, only called on the client * displayPIN(pin) -- Display the PIN to the user, only called on the client
* that didn't provide the PIN. * that didn't provide the PIN.
* *
* onPaired() -- Called when the device pairing has been established and
* we're ready to send the credentials over. To do that, the controller
* must call 'sendAndComplete()' while the channel is active.
*
* onComplete(data) -- Called after transfer has been completed. On * onComplete(data) -- Called after transfer has been completed. On
* the sending side this is called with no parameter and as soon as the * 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 * data has been uploaded. This does not mean the receiving side has
* has actually retrieved them yet. * actually retrieved them yet.
* *
* onAbort(error) -- Called whenever an error is encountered. All errors lead * 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 an abort and the process has to be started again on both sides.
@ -113,7 +128,12 @@ const JPAKE_VERIFY_VALUE = "0123456789ABCDEF";
* *
* To initiate the transfer from the sending side, call * To initiate the transfer from the sending side, call
* *
* client.sendWithPIN(pin, data) * client.pairWithPIN(pin, true);
*
* Once the pairing has been established, the controller's 'onPaired()' method
* will be called. To then transmit the data, call
*
* client.sendAndComplete(data);
* *
* To abort the process, call * To abort the process, call
* *
@ -122,8 +142,8 @@ const JPAKE_VERIFY_VALUE = "0123456789ABCDEF";
* Note that after completion or abort, the 'client' instance may not be reused. * 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. * You will have to create a new one in case you'd like to restart the process.
*/ */
function JPAKEClient(observer) { function JPAKEClient(controller) {
this.observer = observer; this.controller = controller;
this._log = Log4Moz.repository.getLogger("Sync.JPAKEClient"); this._log = Log4Moz.repository.getLogger("Sync.JPAKEClient");
this._log.level = Log4Moz.Level[Svc.Prefs.get( this._log.level = Log4Moz.Level[Svc.Prefs.get(
@ -149,6 +169,12 @@ JPAKEClient.prototype = {
* Public API * Public API
*/ */
/**
* Initiate pairing and receive data without providing a PIN. The PIN will
* be generated and passed on to the controller to be displayed to the user.
*
* This is typically called on mobile devices where typing is tedious.
*/
receiveNoPIN: function receiveNoPIN() { receiveNoPIN: function receiveNoPIN() {
this._my_signerid = JPAKE_SIGNERID_RECEIVER; this._my_signerid = JPAKE_SIGNERID_RECEIVER;
this._their_signerid = JPAKE_SIGNERID_SENDER; this._their_signerid = JPAKE_SIGNERID_SENDER;
@ -173,33 +199,87 @@ JPAKEClient.prototype = {
this._computeFinal, this._computeFinal,
this._computeKeyVerification, this._computeKeyVerification,
this._putStep, this._putStep,
function(callback) {
// Allow longer time-out for the last message.
this._maxTries = Svc.Prefs.get("jpake.lastMsgMaxTries");
callback();
},
this._getStep, this._getStep,
this._decryptData, this._decryptData,
this._complete)(); this._complete)();
}, },
sendWithPIN: function sendWithPIN(pin, obj) { /**
* Initiate pairing based on the PIN entered by the user.
*
* This is typically called on desktop devices where typing is easier than
* on mobile.
*
* @param pin
* 12 character string (in human-friendly base32) containing the PIN
* entered by the user.
* @param expectDelay
* Flag that indicates that a significant delay between the pairing
* and the sending should be expected. v2 and earlier of the protocol
* did not allow for this and the pairing to a v2 or earlier client
* will be aborted if this flag is 'true'.
*/
pairWithPIN: function pairWithPIN(pin, expectDelay) {
this._my_signerid = JPAKE_SIGNERID_SENDER; this._my_signerid = JPAKE_SIGNERID_SENDER;
this._their_signerid = JPAKE_SIGNERID_RECEIVER; this._their_signerid = JPAKE_SIGNERID_RECEIVER;
this._channel = pin.slice(JPAKE_LENGTH_SECRET); this._channel = pin.slice(JPAKE_LENGTH_SECRET);
this._channelURL = this._serverURL + this._channel; this._channelURL = this._serverURL + this._channel;
this._secret = pin.slice(0, JPAKE_LENGTH_SECRET); this._secret = pin.slice(0, JPAKE_LENGTH_SECRET);
this._data = JSON.stringify(obj);
this._chain(this._computeStepOne, this._chain(this._computeStepOne,
this._getStep, this._getStep,
function (callback) {
// Ensure that the other client can deal with a delay for
// the last message if that's requested by the caller.
if (!expectDelay) {
return callback();
}
if (!this._incoming.version || this._incoming.version < 3) {
return this.abort(JPAKE_ERROR_DELAYUNSUPPORTED);
}
return callback();
},
this._putStep, this._putStep,
this._computeStepTwo, this._computeStepTwo,
this._getStep, this._getStep,
this._putStep, this._putStep,
this._computeFinal, this._computeFinal,
this._getStep, this._getStep,
this._encryptData, this._verifyPairing)();
},
/**
* Send data after a successful pairing.
*
* @param obj
* Object containing the data to send. It will be serialized as JSON.
*/
sendAndComplete: function sendAndComplete(obj) {
if (!this._paired || this._finished) {
this._log.error("Can't send data, no active pairing!");
throw "No active pairing!";
}
this._data = JSON.stringify(obj);
this._chain(this._encryptData,
this._putStep, this._putStep,
this._complete)(); this._complete)();
}, },
/**
* Abort the current pairing. The channel on the server will be deleted
* if the abort wasn't due to a network or server error. The controller's
* 'onAbort()' method is notified in all cases.
*
* @param error [optional]
* Error constant indicating the reason for the abort. Defaults to
* user abort.
*/
abort: function abort(error) { abort: function abort(error) {
this._log.debug("Aborting..."); this._log.debug("Aborting...");
this._finished = true; this._finished = true;
@ -213,10 +293,9 @@ JPAKEClient.prototype = {
if (error == JPAKE_ERROR_CHANNEL || if (error == JPAKE_ERROR_CHANNEL ||
error == JPAKE_ERROR_NETWORK || error == JPAKE_ERROR_NETWORK ||
error == JPAKE_ERROR_NODATA) { error == JPAKE_ERROR_NODATA) {
Utils.namedTimer(function() { this.observer.onAbort(error); }, 0, Utils.nextTick(function() { this.controller.onAbort(error); }, this);
this, "_timer_onAbort");
} else { } else {
this._reportFailure(error, function() { self.observer.onAbort(error); }); this._reportFailure(error, function() { self.controller.onAbort(error); });
} }
}, },
@ -285,8 +364,7 @@ JPAKEClient.prototype = {
// Don't block on UI code. // Don't block on UI code.
let pin = this._secret + this._channel; let pin = this._secret + this._channel;
Utils.namedTimer(function() { this.observer.displayPIN(pin); }, 0, Utils.nextTick(function() { this.controller.displayPIN(pin); }, this);
this, "_timer_displayPIN");
callback(); callback();
})); }));
}, },
@ -295,6 +373,11 @@ JPAKEClient.prototype = {
_putStep: function _putStep(callback) { _putStep: function _putStep(callback) {
this._log.trace("Uploading message " + this._outgoing.type); this._log.trace("Uploading message " + this._outgoing.type);
let request = this._newRequest(this._channelURL); let request = this._newRequest(this._channelURL);
if (this._their_etag) {
request.setHeader("If-Match", this._their_etag);
} else {
request.setHeader("If-None-Match", "*");
}
request.put(this._outgoing, Utils.bind2(this, function (error) { request.put(this._outgoing, Utils.bind2(this, function (error) {
if (this._finished) { if (this._finished) {
return; return;
@ -313,7 +396,7 @@ JPAKEClient.prototype = {
} }
// There's no point in returning early here since the next step will // 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. // always be a GET so let's pause for twice the poll interval.
this._etag = request.response.headers["etag"]; this._my_etag = request.response.headers["etag"];
Utils.namedTimer(function () { callback(); }, this._pollInterval * 2, Utils.namedTimer(function () { callback(); }, this._pollInterval * 2,
this, "_pollTimer"); this, "_pollTimer");
})); }));
@ -324,8 +407,8 @@ JPAKEClient.prototype = {
_getStep: function _getStep(callback) { _getStep: function _getStep(callback) {
this._log.trace("Retrieving next message."); this._log.trace("Retrieving next message.");
let request = this._newRequest(this._channelURL); let request = this._newRequest(this._channelURL);
if (this._etag) { if (this._my_etag) {
request.setHeader("If-None-Match", this._etag); request.setHeader("If-None-Match", this._my_etag);
} }
request.get(Utils.bind2(this, function (error) { request.get(Utils.bind2(this, function (error) {
@ -365,6 +448,14 @@ JPAKEClient.prototype = {
return; return;
} }
this._their_etag = request.response.headers["etag"];
if (!this._their_etag) {
this._log.error("Server did not supply ETag for message: "
+ request.response.body);
this.abort(JPAKE_ERROR_SERVER);
return;
}
try { try {
this._incoming = JSON.parse(request.response.body); this._incoming = JSON.parse(request.response.body);
} catch (ex) { } catch (ex) {
@ -414,7 +505,9 @@ JPAKEClient.prototype = {
gx2: gx2.value, gx2: gx2.value,
zkp_x1: {gr: gv1.value, b: r1.value, id: this._my_signerid}, zkp_x1: {gr: gv1.value, b: r1.value, id: this._my_signerid},
zkp_x2: {gr: gv2.value, b: r2.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._outgoing = {type: this._my_signerid + "1",
version: KEYEXCHANGE_VERSION,
payload: one};
this._log.trace("Generated message " + this._outgoing.type); this._log.trace("Generated message " + this._outgoing.type);
callback(); callback();
}, },
@ -452,7 +545,9 @@ JPAKEClient.prototype = {
} }
let two = {A: A.value, let two = {A: A.value,
zkp_A: {gr: gvA.value, b: rA.value, id: this._my_signerid}}; zkp_A: {gr: gvA.value, b: rA.value, id: this._my_signerid}};
this._outgoing = {type: this._my_signerid + "2", payload: two}; this._outgoing = {type: this._my_signerid + "2",
version: KEYEXCHANGE_VERSION,
payload: two};
this._log.trace("Generated message " + this._outgoing.type); this._log.trace("Generated message " + this._outgoing.type);
callback(); callback();
}, },
@ -504,12 +599,13 @@ JPAKEClient.prototype = {
return; return;
} }
this._outgoing = {type: this._my_signerid + "3", this._outgoing = {type: this._my_signerid + "3",
version: KEYEXCHANGE_VERSION,
payload: {ciphertext: ciphertext, IV: iv}}; payload: {ciphertext: ciphertext, IV: iv}};
this._log.trace("Generated message " + this._outgoing.type); this._log.trace("Generated message " + this._outgoing.type);
callback(); callback();
}, },
_encryptData: function _encryptData(callback) { _verifyPairing: function _verifyPairing(callback) {
this._log.trace("Verifying their key."); this._log.trace("Verifying their key.");
if (this._incoming.type != this._their_signerid + "3") { if (this._incoming.type != this._their_signerid + "3") {
this._log.error("Invalid round 3 data: " + this._log.error("Invalid round 3 data: " +
@ -518,6 +614,7 @@ JPAKEClient.prototype = {
return; return;
} }
let step3 = this._incoming.payload; let step3 = this._incoming.payload;
let ciphertext;
try { try {
ciphertext = Svc.Crypto.encrypt(JPAKE_VERIFY_VALUE, ciphertext = Svc.Crypto.encrypt(JPAKE_VERIFY_VALUE,
this._crypto_key, step3.IV); this._crypto_key, step3.IV);
@ -530,6 +627,13 @@ JPAKEClient.prototype = {
return; return;
} }
this._log.debug("Verified pairing!");
this._paired = true;
Utils.nextTick(function () { this.controller.onPaired(); }, this);
callback();
},
_encryptData: function _encryptData(callback) {
this._log.trace("Encrypting data."); this._log.trace("Encrypting data.");
let iv, ciphertext, hmac; let iv, ciphertext, hmac;
try { try {
@ -542,6 +646,7 @@ JPAKEClient.prototype = {
return; return;
} }
this._outgoing = {type: this._my_signerid + "3", this._outgoing = {type: this._my_signerid + "3",
version: KEYEXCHANGE_VERSION,
payload: {ciphertext: ciphertext, IV: iv, hmac: hmac}}; payload: {ciphertext: ciphertext, IV: iv, hmac: hmac}};
this._log.trace("Generated message " + this._outgoing.type); this._log.trace("Generated message " + this._outgoing.type);
callback(); callback();
@ -594,8 +699,8 @@ JPAKEClient.prototype = {
_complete: function _complete() { _complete: function _complete() {
this._log.debug("Exchange completed."); this._log.debug("Exchange completed.");
this._finished = true; this._finished = true;
Utils.namedTimer(function () { this.observer.onComplete(this._newData); }, Utils.nextTick(function () { this.controller.onComplete(this._newData); },
0, this, "_timer_onComplete"); this);
} }
}; };

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

@ -26,7 +26,8 @@ pref("services.sync.engine.tabs.filteredUrls", "^(about:.*|chrome://weave/.*|wyc
pref("services.sync.jpake.serverURL", "https://setup.services.mozilla.com/"); pref("services.sync.jpake.serverURL", "https://setup.services.mozilla.com/");
pref("services.sync.jpake.pollInterval", 1000); pref("services.sync.jpake.pollInterval", 1000);
pref("services.sync.jpake.firstMsgMaxTries", 300); pref("services.sync.jpake.firstMsgMaxTries", 300); // 5 minutes
pref("services.sync.jpake.lastMsgMaxTries", 300); // 5 minutes
pref("services.sync.jpake.maxTries", 10); pref("services.sync.jpake.maxTries", 10);
pref("services.sync.log.appender.console", "Warn"); pref("services.sync.log.appender.console", "Warn");

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

@ -6,26 +6,32 @@ Cu.import("resource://services-sync/util.js");
const JPAKE_LENGTH_SECRET = 8; const JPAKE_LENGTH_SECRET = 8;
const JPAKE_LENGTH_CLIENTID = 256; const JPAKE_LENGTH_CLIENTID = 256;
const KEYEXCHANGE_VERSION = 3;
/* /*
* Simple server. * Simple server.
*/ */
const SERVER_MAX_GETS = 6;
function check_headers(request) { function check_headers(request) {
let stack = Components.stack.caller;
// There shouldn't be any Basic auth // There shouldn't be any Basic auth
do_check_false(request.hasHeader("Authorization")); do_check_false(request.hasHeader("Authorization"), stack);
// Ensure key exchange ID is set and the right length // Ensure key exchange ID is set and the right length
do_check_true(request.hasHeader("X-KeyExchange-Id")); do_check_true(request.hasHeader("X-KeyExchange-Id"), stack);
do_check_eq(request.getHeader("X-KeyExchange-Id").length, do_check_eq(request.getHeader("X-KeyExchange-Id").length,
JPAKE_LENGTH_CLIENTID); JPAKE_LENGTH_CLIENTID, stack);
} }
function new_channel() { function new_channel() {
// Create a new channel and register it with the server. // Create a new channel and register it with the server.
let cid = Math.floor(Math.random() * 10000); let cid = Math.floor(Math.random() * 10000);
while (channels[cid]) while (channels[cid]) {
cid = Math.floor(Math.random() * 10000); cid = Math.floor(Math.random() * 10000);
}
let channel = channels[cid] = new ServerChannel(); let channel = channels[cid] = new ServerChannel();
server.registerPathHandler("/" + cid, channel.handler()); server.registerPathHandler("/" + cid, channel.handler());
return cid; return cid;
@ -45,21 +51,24 @@ let error_report;
function server_report(request, response) { function server_report(request, response) {
check_headers(request); check_headers(request);
if (request.hasHeader("X-KeyExchange-Log")) if (request.hasHeader("X-KeyExchange-Log")) {
error_report = request.getHeader("X-KeyExchange-Log"); error_report = request.getHeader("X-KeyExchange-Log");
}
if (request.hasHeader("X-KeyExchange-Cid")) { if (request.hasHeader("X-KeyExchange-Cid")) {
let cid = request.getHeader("X-KeyExchange-Cid"); let cid = request.getHeader("X-KeyExchange-Cid");
let channel = channels[cid]; let channel = channels[cid];
if (channel) if (channel) {
channel.clear(); channel.clear();
}
} }
response.setStatusLine(request.httpVersion, 200, "OK"); response.setStatusLine(request.httpVersion, 200, "OK");
} }
function ServerChannel() { function ServerChannel() {
this.data = "{}"; this.data = "";
this.etag = "";
this.getCount = 0; this.getCount = 0;
} }
ServerChannel.prototype = { ServerChannel.prototype = {
@ -69,26 +78,42 @@ ServerChannel.prototype = {
response.setStatusLine(request.httpVersion, 404, "Not Found"); response.setStatusLine(request.httpVersion, 404, "Not Found");
return; return;
} }
if (request.hasHeader("If-None-Match")) { if (request.hasHeader("If-None-Match")) {
let etag = request.getHeader("If-None-Match"); let etag = request.getHeader("If-None-Match");
if (etag == this._etag) { if (etag == this.etag) {
response.setStatusLine(request.httpVersion, 304, "Not Modified"); response.setStatusLine(request.httpVersion, 304, "Not Modified");
return; return;
} }
} }
response.setHeader("ETag", this.etag);
response.setStatusLine(request.httpVersion, 200, "OK"); response.setStatusLine(request.httpVersion, 200, "OK");
response.bodyOutputStream.write(this.data, this.data.length); response.bodyOutputStream.write(this.data, this.data.length);
// Automatically clear the channel after 6 successful GETs. // Automatically clear the channel after 6 successful GETs.
this.getCount += 1; this.getCount += 1;
if (this.getCount == 6) if (this.getCount == SERVER_MAX_GETS) {
this.clear(); this.clear();
}
}, },
PUT: function PUT(request, response) { PUT: function PUT(request, response) {
if (this.data) {
do_check_true(request.hasHeader("If-Match"));
let etag = request.getHeader("If-Match");
if (etag != this.etag) {
response.setHeader("ETag", this.etag);
response.setStatusLine(request.httpVersion, 412, "Precondition Failed");
return;
}
} else {
do_check_true(request.hasHeader("If-None-Match"));
do_check_eq(request.getHeader("If-None-Match"), "*");
}
this.data = readBytesFromInputStream(request.bodyInputStream); this.data = readBytesFromInputStream(request.bodyInputStream);
this._etag = '"' + Utils.sha1(this.data) + '"'; this.etag = '"' + Utils.sha1(this.data) + '"';
response.setHeader("ETag", this._etag); response.setHeader("ETag", this.etag);
response.setStatusLine(request.httpVersion, 200, "OK"); response.setStatusLine(request.httpVersion, 200, "OK");
}, },
@ -108,14 +133,34 @@ ServerChannel.prototype = {
}; };
/**
* Controller that throws for everything.
*/
let BaseController = {
displayPIN: function displayPIN() {
do_throw("displayPIN() shouldn't have been called!");
},
onAbort: function onAbort(error) {
do_throw("Shouldn't have aborted with " + error + "!");
},
onPaired: function onPaired() {
do_throw("onPaired() shouldn't have been called!");
},
onComplete: function onComplete(data) {
do_throw("Shouldn't have completed with " + data + "!");
}
};
const DATA = {"msg": "eggstreamly sekrit"}; const DATA = {"msg": "eggstreamly sekrit"};
const POLLINTERVAL = 50; const POLLINTERVAL = 50;
function run_test() { function run_test() {
Svc.Prefs.set("jpake.serverURL", "http://localhost:8080/"); Svc.Prefs.set("jpake.serverURL", "http://localhost:8080/");
Svc.Prefs.set("jpake.pollInterval", POLLINTERVAL); Svc.Prefs.set("jpake.pollInterval", POLLINTERVAL);
Svc.Prefs.set("jpake.maxTries", 5); Svc.Prefs.set("jpake.maxTries", 2);
Svc.Prefs.set("jpake.firstMsgMaxTries", 5); Svc.Prefs.set("jpake.firstMsgMaxTries", 5);
Svc.Prefs.set("jpake.lastMsgMaxTries", 5);
// Ensure clean up // Ensure clean up
Svc.Obs.add("profile-before-change", function() { Svc.Obs.add("profile-before-change", function() {
Svc.Prefs.resetBranch(""); Svc.Prefs.resetBranch("");
@ -134,6 +179,8 @@ function run_test() {
"/report": server_report}); "/report": server_report});
initTestLogging("Trace"); initTestLogging("Trace");
Log4Moz.repository.getLogger("Sync.JPAKEClient").level = Log4Moz.Level.Trace;
Log4Moz.repository.getLogger("Sync.RESTRequest").level = Log4Moz.Level.Trace;
run_next_test(); run_next_test();
} }
@ -142,23 +189,20 @@ add_test(function test_success_receiveNoPIN() {
_("Test a successful exchange started by receiveNoPIN()."); _("Test a successful exchange started by receiveNoPIN().");
let snd = new JPAKEClient({ let snd = new JPAKEClient({
displayPIN: function displayPIN() { __proto__: BaseController,
do_throw("displayPIN shouldn't have been called!"); onPaired: function onPaired() {
}, _("Pairing successful, sending final payload.");
onAbort: function onAbort(error) { Utils.nextTick(function() { snd.sendAndComplete(DATA); });
do_throw("Shouldn't have aborted!" + error);
}, },
onComplete: function onComplete() {} onComplete: function onComplete() {}
}); });
let rec = new JPAKEClient({ let rec = new JPAKEClient({
__proto__: BaseController,
displayPIN: function displayPIN(pin) { displayPIN: function displayPIN(pin) {
_("Received PIN " + pin + ". Entering it in the other computer..."); _("Received PIN " + pin + ". Entering it in the other computer...");
this.cid = pin.slice(JPAKE_LENGTH_SECRET); this.cid = pin.slice(JPAKE_LENGTH_SECRET);
Utils.nextTick(function() { snd.sendWithPIN(pin, DATA); }); Utils.nextTick(function() { snd.pairWithPIN(pin, false); });
},
onAbort: function onAbort(error) {
do_throw("Shouldn't have aborted! " + error);
}, },
onComplete: function onComplete(a) { onComplete: function onComplete(a) {
// Ensure channel was cleared, no error report. // Ensure channel was cleared, no error report.
@ -171,10 +215,11 @@ add_test(function test_success_receiveNoPIN() {
}); });
add_test(function test_firstMsgMaxTries() { add_test(function test_firstMsgMaxTries_timeout() {
_("Test abort when sender doesn't upload anything."); _("Test abort when sender doesn't upload anything.");
let rec = new JPAKEClient({ let rec = new JPAKEClient({
__proto__: BaseController,
displayPIN: function displayPIN(pin) { displayPIN: function displayPIN(pin) {
_("Received PIN " + pin + ". Doing nothing..."); _("Received PIN " + pin + ". Doing nothing...");
this.cid = pin.slice(JPAKE_LENGTH_SECRET); this.cid = pin.slice(JPAKE_LENGTH_SECRET);
@ -186,33 +231,98 @@ add_test(function test_firstMsgMaxTries() {
do_check_eq(error_report, JPAKE_ERROR_TIMEOUT); do_check_eq(error_report, JPAKE_ERROR_TIMEOUT);
error_report = undefined; error_report = undefined;
run_next_test(); run_next_test();
},
onComplete: function onComplete() {
do_throw("Shouldn't have completed! ");
} }
}); });
rec.receiveNoPIN(); rec.receiveNoPIN();
}); });
add_test(function test_firstMsgMaxTries() {
_("Test that receiver can wait longer for the first message.");
let snd = new JPAKEClient({
__proto__: BaseController,
onPaired: function onPaired() {
_("Pairing successful, sending final payload.");
Utils.nextTick(function() { snd.sendAndComplete(DATA); });
},
onComplete: function onComplete() {}
});
let rec = new JPAKEClient({
__proto__: BaseController,
displayPIN: function displayPIN(pin) {
// For the purpose of the tests, the poll interval is 50ms and
// we're polling up to 5 times for the first exchange (as
// opposed to 2 times for most of the other exchanges). So let's
// pretend it took 150ms to enter the PIN on the sender.
_("Received PIN " + pin + ". Waiting 150ms before entering it into sender...");
this.cid = pin.slice(JPAKE_LENGTH_SECRET);
Utils.namedTimer(function() { snd.pairWithPIN(pin, false); },
150, this, "_sendTimer");
},
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);
run_next_test();
}
});
rec.receiveNoPIN();
});
add_test(function test_lastMsgMaxTries() {
_("Test that receiver can wait longer for the last message.");
let snd = new JPAKEClient({
__proto__: BaseController,
onPaired: function onPaired() {
// For the purpose of the tests, the poll interval is 50ms and
// we're polling up to 5 times for the last exchange (as opposed
// to 2 times for other exchanges). So let's pretend it took
// 150ms to come up with the final payload, which should require
// 3 polls.
_("Pairing successful, waiting 150ms to send final payload.");
Utils.namedTimer(function() { snd.sendAndComplete(DATA); },
150, this, "_sendTimer");
},
onComplete: function onComplete() {}
});
let rec = new JPAKEClient({
__proto__: BaseController,
displayPIN: function displayPIN(pin) {
_("Received PIN " + pin + ". Entering it in the other computer...");
this.cid = pin.slice(JPAKE_LENGTH_SECRET);
Utils.nextTick(function() { snd.pairWithPIN(pin, false); });
},
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);
run_next_test();
}
});
rec.receiveNoPIN();
});
add_test(function test_wrongPIN() { add_test(function test_wrongPIN() {
_("Test abort when PINs don't match."); _("Test abort when PINs don't match.");
let snd = new JPAKEClient({ let snd = new JPAKEClient({
displayPIN: function displayPIN() { __proto__: BaseController,
do_throw("displayPIN shouldn't have been called!");
},
onAbort: function onAbort(error) { onAbort: function onAbort(error) {
do_check_eq(error, JPAKE_ERROR_KEYMISMATCH); do_check_eq(error, JPAKE_ERROR_KEYMISMATCH);
do_check_eq(error_report, JPAKE_ERROR_KEYMISMATCH); do_check_eq(error_report, JPAKE_ERROR_KEYMISMATCH);
error_report = undefined; error_report = undefined;
},
onComplete: function onComplete() {
do_throw("Shouldn't have completed!");
} }
}); });
let rec = new JPAKEClient({ let rec = new JPAKEClient({
__proto__: BaseController,
displayPIN: function displayPIN(pin) { displayPIN: function displayPIN(pin) {
this.cid = pin.slice(JPAKE_LENGTH_SECRET); this.cid = pin.slice(JPAKE_LENGTH_SECRET);
let secret = pin.slice(0, JPAKE_LENGTH_SECRET); let secret = pin.slice(0, JPAKE_LENGTH_SECRET);
@ -220,16 +330,13 @@ add_test(function test_wrongPIN() {
let new_pin = secret + this.cid; let new_pin = secret + this.cid;
_("Received PIN " + pin + ", but I'm entering " + new_pin); _("Received PIN " + pin + ", but I'm entering " + new_pin);
Utils.nextTick(function() { snd.sendWithPIN(new_pin, DATA); }); Utils.nextTick(function() { snd.pairWithPIN(new_pin, false); });
}, },
onAbort: function onAbort(error) { onAbort: function onAbort(error) {
do_check_eq(error, JPAKE_ERROR_NODATA); do_check_eq(error, JPAKE_ERROR_NODATA);
// Ensure channel was cleared. // Ensure channel was cleared.
do_check_eq(channels[this.cid].data, undefined); do_check_eq(channels[this.cid].data, undefined);
run_next_test(); run_next_test();
},
onComplete: function onComplete() {
do_throw("Shouldn't have completed! ");
} }
}); });
rec.receiveNoPIN(); rec.receiveNoPIN();
@ -240,9 +347,7 @@ add_test(function test_abort_receiver() {
_("Test user abort on receiving side."); _("Test user abort on receiving side.");
let rec = new JPAKEClient({ let rec = new JPAKEClient({
onComplete: function onComplete(data) { __proto__: BaseController,
do_throw("onComplete shouldn't be called.");
},
onAbort: function onAbort(error) { onAbort: function onAbort(error) {
// Manual abort = userabort. // Manual abort = userabort.
do_check_eq(error, JPAKE_ERROR_USERABORT); do_check_eq(error, JPAKE_ERROR_USERABORT);
@ -265,24 +370,17 @@ add_test(function test_abort_sender() {
_("Test user abort on sending side."); _("Test user abort on sending side.");
let snd = new JPAKEClient({ let snd = new JPAKEClient({
displayPIN: function displayPIN() { __proto__: BaseController,
do_throw("displayPIN shouldn't have been called!");
},
onAbort: function onAbort(error) { onAbort: function onAbort(error) {
// Manual abort == userabort. // Manual abort == userabort.
do_check_eq(error, JPAKE_ERROR_USERABORT); do_check_eq(error, JPAKE_ERROR_USERABORT);
do_check_eq(error_report, JPAKE_ERROR_USERABORT); do_check_eq(error_report, JPAKE_ERROR_USERABORT);
error_report = undefined; error_report = undefined;
},
onComplete: function onComplete() {
do_throw("Shouldn't have completed!");
} }
}); });
let rec = new JPAKEClient({ let rec = new JPAKEClient({
onComplete: function onComplete(data) { __proto__: BaseController,
do_throw("onComplete shouldn't be called.");
},
onAbort: function onAbort(error) { onAbort: function onAbort(error) {
do_check_eq(error, JPAKE_ERROR_NODATA); do_check_eq(error, JPAKE_ERROR_NODATA);
// Ensure channel was cleared, no error report. // Ensure channel was cleared, no error report.
@ -293,7 +391,7 @@ add_test(function test_abort_sender() {
displayPIN: function displayPIN(pin) { displayPIN: function displayPIN(pin) {
_("Received PIN " + pin + ". Entering it in the other computer..."); _("Received PIN " + pin + ". Entering it in the other computer...");
this.cid = pin.slice(JPAKE_LENGTH_SECRET); this.cid = pin.slice(JPAKE_LENGTH_SECRET);
Utils.nextTick(function() { snd.sendWithPIN(pin, DATA); }); Utils.nextTick(function() { snd.pairWithPIN(pin, false); });
Utils.namedTimer(function() { snd.abort(); }, Utils.namedTimer(function() { snd.abort(); },
POLLINTERVAL, this, "_abortTimer"); POLLINTERVAL, this, "_abortTimer");
} }
@ -304,8 +402,13 @@ add_test(function test_abort_sender() {
add_test(function test_wrongmessage() { add_test(function test_wrongmessage() {
let cid = new_channel(); let cid = new_channel();
channels[cid].data = JSON.stringify({type: "receiver2", payload: {}}); let channel = channels[cid];
channel.data = JSON.stringify({type: "receiver2",
version: KEYEXCHANGE_VERSION,
payload: {}});
channel.etag = '"fake-etag"';
let snd = new JPAKEClient({ let snd = new JPAKEClient({
__proto__: BaseController,
onComplete: function onComplete(data) { onComplete: function onComplete(data) {
do_throw("onComplete shouldn't be called."); do_throw("onComplete shouldn't be called.");
}, },
@ -314,20 +417,19 @@ add_test(function test_wrongmessage() {
run_next_test(); run_next_test();
} }
}); });
snd.sendWithPIN("01234567" + cid, DATA); snd.pairWithPIN("01234567" + cid, false);
}); });
add_test(function test_error_channel() { add_test(function test_error_channel() {
let serverURL = Svc.Prefs.get("jpake.serverURL");
Svc.Prefs.set("jpake.serverURL", "http://localhost:12345/"); Svc.Prefs.set("jpake.serverURL", "http://localhost:12345/");
let rec = new JPAKEClient({ let rec = new JPAKEClient({
onComplete: function onComplete(data) { __proto__: BaseController,
do_throw("onComplete shouldn't be called.");
},
onAbort: function onAbort(error) { onAbort: function onAbort(error) {
do_check_eq(error, JPAKE_ERROR_CHANNEL); do_check_eq(error, JPAKE_ERROR_CHANNEL);
Svc.Prefs.reset("jpake.serverURL"); Svc.Prefs.set("jpake.serverURL", serverURL);
run_next_test(); run_next_test();
}, },
displayPIN: function displayPIN(pin) {} displayPIN: function displayPIN(pin) {}
@ -337,19 +439,64 @@ add_test(function test_error_channel() {
add_test(function test_error_network() { add_test(function test_error_network() {
let serverURL = Svc.Prefs.get("jpake.serverURL");
Svc.Prefs.set("jpake.serverURL", "http://localhost:12345/"); Svc.Prefs.set("jpake.serverURL", "http://localhost:12345/");
let snd = new JPAKEClient({ let snd = new JPAKEClient({
onComplete: function onComplete(data) { __proto__: BaseController,
do_throw("onComplete shouldn't be called.");
},
onAbort: function onAbort(error) { onAbort: function onAbort(error) {
do_check_eq(error, JPAKE_ERROR_NETWORK); do_check_eq(error, JPAKE_ERROR_NETWORK);
Svc.Prefs.reset("jpake.serverURL"); Svc.Prefs.set("jpake.serverURL", serverURL);
run_next_test(); run_next_test();
} }
}); });
snd.sendWithPIN("0123456789ab", DATA); snd.pairWithPIN("0123456789ab", false);
});
add_test(function test_error_server_noETag() {
let cid = new_channel();
let channel = channels[cid];
channel.data = JSON.stringify({type: "receiver1",
version: KEYEXCHANGE_VERSION,
payload: {}});
// This naughty server doesn't supply ETag (well, it supplies empty one).
channel.etag = "";
let snd = new JPAKEClient({
__proto__: BaseController,
onAbort: function onAbort(error) {
do_check_eq(error, JPAKE_ERROR_SERVER);
run_next_test();
}
});
snd.pairWithPIN("01234567" + cid, false);
});
add_test(function test_error_delayNotSupported() {
let cid = new_channel();
let channel = channels[cid];
channel.data = JSON.stringify({type: "receiver1",
version: 2,
payload: {}});
channel.etag = '"fake-etag"';
let snd = new JPAKEClient({
__proto__: BaseController,
onAbort: function onAbort(error) {
do_check_eq(error, JPAKE_ERROR_DELAYUNSUPPORTED);
run_next_test();
}
});
snd.pairWithPIN("01234567" + cid, true);
});
add_test(function test_sendAndComplete_notPaired() {
let snd = new JPAKEClient({__proto__: BaseController});
do_check_throws(function () {
snd.sendAndComplete(DATA);
});
run_next_test();
}); });