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
parent 9e8935d63d
commit 3eac5b5e4f
4 changed files with 362 additions and 108 deletions

View File

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

View File

@ -48,6 +48,8 @@ Cu.import("resource://services-sync/util.js");
const EXPORTED_SYMBOLS = ["JPAKEClient"];
const REQUEST_TIMEOUT = 60; // 1 minute
const KEYEXCHANGE_VERSION = 3;
const JPAKE_SIGNERID_SENDER = "sender";
const JPAKE_SIGNERID_RECEIVER = "receiver";
const JPAKE_LENGTH_SECRET = 8;
@ -55,50 +57,63 @@ 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 |
* Mobile Server Desktop
* ===================================================================
* |
* retrieve channel <---------------|
* generate random secret |
* show PIN = secret + channel | ask user for PIN
* upload Mobile's message 1 ------>|
* |----> retrieve Mobile's message 1
* |<----- upload Desktop's message 1
* retrieve Desktop's message 1 <---|
* upload Mobile's message 2 ------>|
* |----> retrieve Mobile's message 2
* | compute key
* |<----- upload Desktop's message 2
* retrieve Desktop's message 2 <---|
* compute key |
* encrypt known value ------------>|
* |-------> retrieve encrypted value
* | verify against local known value
*
* At this point Desktop knows whether the PIN was entered correctly.
* If it wasn't, Desktop deletes the session. If it was, the account
* setup can proceed. If Desktop doesn't yet have an account set up,
* 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:
*
* 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
* 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
* 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.
* data has been uploaded. This does not 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.
@ -113,7 +128,12 @@ const JPAKE_VERIFY_VALUE = "0123456789ABCDEF";
*
* 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
*
@ -122,8 +142,8 @@ const JPAKE_VERIFY_VALUE = "0123456789ABCDEF";
* 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;
function JPAKEClient(controller) {
this.controller = controller;
this._log = Log4Moz.repository.getLogger("Sync.JPAKEClient");
this._log.level = Log4Moz.Level[Svc.Prefs.get(
@ -149,6 +169,12 @@ JPAKEClient.prototype = {
* 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() {
this._my_signerid = JPAKE_SIGNERID_RECEIVER;
this._their_signerid = JPAKE_SIGNERID_SENDER;
@ -173,33 +199,87 @@ JPAKEClient.prototype = {
this._computeFinal,
this._computeKeyVerification,
this._putStep,
function(callback) {
// Allow longer time-out for the last message.
this._maxTries = Svc.Prefs.get("jpake.lastMsgMaxTries");
callback();
},
this._getStep,
this._decryptData,
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._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,
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._computeStepTwo,
this._getStep,
this._putStep,
this._computeFinal,
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._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) {
this._log.debug("Aborting...");
this._finished = true;
@ -213,10 +293,9 @@ JPAKEClient.prototype = {
if (error == JPAKE_ERROR_CHANNEL ||
error == JPAKE_ERROR_NETWORK ||
error == JPAKE_ERROR_NODATA) {
Utils.namedTimer(function() { this.observer.onAbort(error); }, 0,
this, "_timer_onAbort");
Utils.nextTick(function() { this.controller.onAbort(error); }, this);
} 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.
let pin = this._secret + this._channel;
Utils.namedTimer(function() { this.observer.displayPIN(pin); }, 0,
this, "_timer_displayPIN");
Utils.nextTick(function() { this.controller.displayPIN(pin); }, this);
callback();
}));
},
@ -295,6 +373,11 @@ JPAKEClient.prototype = {
_putStep: function _putStep(callback) {
this._log.trace("Uploading message " + this._outgoing.type);
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) {
if (this._finished) {
return;
@ -313,7 +396,7 @@ JPAKEClient.prototype = {
}
// 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 = request.response.headers["etag"];
this._my_etag = request.response.headers["etag"];
Utils.namedTimer(function () { callback(); }, this._pollInterval * 2,
this, "_pollTimer");
}));
@ -324,8 +407,8 @@ JPAKEClient.prototype = {
_getStep: function _getStep(callback) {
this._log.trace("Retrieving next message.");
let request = this._newRequest(this._channelURL);
if (this._etag) {
request.setHeader("If-None-Match", this._etag);
if (this._my_etag) {
request.setHeader("If-None-Match", this._my_etag);
}
request.get(Utils.bind2(this, function (error) {
@ -365,6 +448,14 @@ JPAKEClient.prototype = {
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 {
this._incoming = JSON.parse(request.response.body);
} catch (ex) {
@ -414,7 +505,9 @@ JPAKEClient.prototype = {
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._outgoing = {type: this._my_signerid + "1",
version: KEYEXCHANGE_VERSION,
payload: one};
this._log.trace("Generated message " + this._outgoing.type);
callback();
},
@ -452,7 +545,9 @@ JPAKEClient.prototype = {
}
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._outgoing = {type: this._my_signerid + "2",
version: KEYEXCHANGE_VERSION,
payload: two};
this._log.trace("Generated message " + this._outgoing.type);
callback();
},
@ -504,12 +599,13 @@ JPAKEClient.prototype = {
return;
}
this._outgoing = {type: this._my_signerid + "3",
version: KEYEXCHANGE_VERSION,
payload: {ciphertext: ciphertext, IV: iv}};
this._log.trace("Generated message " + this._outgoing.type);
callback();
},
_encryptData: function _encryptData(callback) {
_verifyPairing: function _verifyPairing(callback) {
this._log.trace("Verifying their key.");
if (this._incoming.type != this._their_signerid + "3") {
this._log.error("Invalid round 3 data: " +
@ -518,6 +614,7 @@ JPAKEClient.prototype = {
return;
}
let step3 = this._incoming.payload;
let ciphertext;
try {
ciphertext = Svc.Crypto.encrypt(JPAKE_VERIFY_VALUE,
this._crypto_key, step3.IV);
@ -530,6 +627,13 @@ JPAKEClient.prototype = {
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.");
let iv, ciphertext, hmac;
try {
@ -542,6 +646,7 @@ JPAKEClient.prototype = {
return;
}
this._outgoing = {type: this._my_signerid + "3",
version: KEYEXCHANGE_VERSION,
payload: {ciphertext: ciphertext, IV: iv, hmac: hmac}};
this._log.trace("Generated message " + this._outgoing.type);
callback();
@ -594,8 +699,8 @@ JPAKEClient.prototype = {
_complete: function _complete() {
this._log.debug("Exchange completed.");
this._finished = true;
Utils.namedTimer(function () { this.observer.onComplete(this._newData); },
0, this, "_timer_onComplete");
Utils.nextTick(function () { this.controller.onComplete(this._newData); },
this);
}
};

View File

@ -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.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.log.appender.console", "Warn");

View File

@ -6,26 +6,32 @@ Cu.import("resource://services-sync/util.js");
const JPAKE_LENGTH_SECRET = 8;
const JPAKE_LENGTH_CLIENTID = 256;
const KEYEXCHANGE_VERSION = 3;
/*
* Simple server.
*/
const SERVER_MAX_GETS = 6;
function check_headers(request) {
let stack = Components.stack.caller;
// 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
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,
JPAKE_LENGTH_CLIENTID);
JPAKE_LENGTH_CLIENTID, stack);
}
function new_channel() {
// Create a new channel and register it with the server.
let cid = Math.floor(Math.random() * 10000);
while (channels[cid])
while (channels[cid]) {
cid = Math.floor(Math.random() * 10000);
}
let channel = channels[cid] = new ServerChannel();
server.registerPathHandler("/" + cid, channel.handler());
return cid;
@ -45,21 +51,24 @@ let error_report;
function server_report(request, response) {
check_headers(request);
if (request.hasHeader("X-KeyExchange-Log"))
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)
if (channel) {
channel.clear();
}
}
response.setStatusLine(request.httpVersion, 200, "OK");
}
function ServerChannel() {
this.data = "{}";
this.data = "";
this.etag = "";
this.getCount = 0;
}
ServerChannel.prototype = {
@ -69,26 +78,42 @@ ServerChannel.prototype = {
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) {
if (etag == this.etag) {
response.setStatusLine(request.httpVersion, 304, "Not Modified");
return;
}
}
response.setHeader("ETag", this.etag);
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)
if (this.getCount == SERVER_MAX_GETS) {
this.clear();
}
},
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._etag = '"' + Utils.sha1(this.data) + '"';
response.setHeader("ETag", this._etag);
this.etag = '"' + Utils.sha1(this.data) + '"';
response.setHeader("ETag", this.etag);
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 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.maxTries", 2);
Svc.Prefs.set("jpake.firstMsgMaxTries", 5);
Svc.Prefs.set("jpake.lastMsgMaxTries", 5);
// Ensure clean up
Svc.Obs.add("profile-before-change", function() {
Svc.Prefs.resetBranch("");
@ -134,6 +179,8 @@ function run_test() {
"/report": server_report});
initTestLogging("Trace");
Log4Moz.repository.getLogger("Sync.JPAKEClient").level = Log4Moz.Level.Trace;
Log4Moz.repository.getLogger("Sync.RESTRequest").level = Log4Moz.Level.Trace;
run_next_test();
}
@ -142,23 +189,20 @@ add_test(function test_success_receiveNoPIN() {
_("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);
__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) {
_("Received PIN " + pin + ". Entering it in the other computer...");
this.cid = pin.slice(JPAKE_LENGTH_SECRET);
Utils.nextTick(function() { snd.sendWithPIN(pin, DATA); });
},
onAbort: function onAbort(error) {
do_throw("Shouldn't have aborted! " + error);
Utils.nextTick(function() { snd.pairWithPIN(pin, false); });
},
onComplete: function onComplete(a) {
// 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.");
let rec = new JPAKEClient({
__proto__: BaseController,
displayPIN: function displayPIN(pin) {
_("Received PIN " + pin + ". Doing nothing...");
this.cid = pin.slice(JPAKE_LENGTH_SECRET);
@ -186,33 +231,98 @@ add_test(function test_firstMsgMaxTries() {
do_check_eq(error_report, JPAKE_ERROR_TIMEOUT);
error_report = undefined;
run_next_test();
},
onComplete: function onComplete() {
do_throw("Shouldn't have completed! ");
}
});
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() {
_("Test abort when PINs don't match.");
let snd = new JPAKEClient({
displayPIN: function displayPIN() {
do_throw("displayPIN shouldn't have been called!");
},
__proto__: BaseController,
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({
__proto__: BaseController,
displayPIN: function displayPIN(pin) {
this.cid = pin.slice(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;
_("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) {
do_check_eq(error, JPAKE_ERROR_NODATA);
// Ensure channel was cleared.
do_check_eq(channels[this.cid].data, undefined);
run_next_test();
},
onComplete: function onComplete() {
do_throw("Shouldn't have completed! ");
}
});
rec.receiveNoPIN();
@ -240,9 +347,7 @@ add_test(function test_abort_receiver() {
_("Test user abort on receiving side.");
let rec = new JPAKEClient({
onComplete: function onComplete(data) {
do_throw("onComplete shouldn't be called.");
},
__proto__: BaseController,
onAbort: function onAbort(error) {
// Manual abort = userabort.
do_check_eq(error, JPAKE_ERROR_USERABORT);
@ -265,24 +370,17 @@ add_test(function test_abort_sender() {
_("Test user abort on sending side.");
let snd = new JPAKEClient({
displayPIN: function displayPIN() {
do_throw("displayPIN shouldn't have been called!");
},
__proto__: BaseController,
onAbort: function onAbort(error) {
// Manual abort == userabort.
do_check_eq(error, JPAKE_ERROR_USERABORT);
do_check_eq(error_report, JPAKE_ERROR_USERABORT);
error_report = 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.");
},
__proto__: BaseController,
onAbort: function onAbort(error) {
do_check_eq(error, JPAKE_ERROR_NODATA);
// Ensure channel was cleared, no error report.
@ -293,7 +391,7 @@ add_test(function test_abort_sender() {
displayPIN: function displayPIN(pin) {
_("Received PIN " + pin + ". Entering it in the other computer...");
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(); },
POLLINTERVAL, this, "_abortTimer");
}
@ -304,8 +402,13 @@ add_test(function test_abort_sender() {
add_test(function test_wrongmessage() {
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({
__proto__: BaseController,
onComplete: function onComplete(data) {
do_throw("onComplete shouldn't be called.");
},
@ -314,20 +417,19 @@ add_test(function test_wrongmessage() {
run_next_test();
}
});
snd.sendWithPIN("01234567" + cid, DATA);
snd.pairWithPIN("01234567" + cid, false);
});
add_test(function test_error_channel() {
let serverURL = Svc.Prefs.get("jpake.serverURL");
Svc.Prefs.set("jpake.serverURL", "http://localhost:12345/");
let rec = new JPAKEClient({
onComplete: function onComplete(data) {
do_throw("onComplete shouldn't be called.");
},
__proto__: BaseController,
onAbort: function onAbort(error) {
do_check_eq(error, JPAKE_ERROR_CHANNEL);
Svc.Prefs.reset("jpake.serverURL");
Svc.Prefs.set("jpake.serverURL", serverURL);
run_next_test();
},
displayPIN: function displayPIN(pin) {}
@ -337,19 +439,64 @@ add_test(function test_error_channel() {
add_test(function test_error_network() {
let serverURL = Svc.Prefs.get("jpake.serverURL");
Svc.Prefs.set("jpake.serverURL", "http://localhost:12345/");
let snd = new JPAKEClient({
onComplete: function onComplete(data) {
do_throw("onComplete shouldn't be called.");
},
__proto__: BaseController,
onAbort: function onAbort(error) {
do_check_eq(error, JPAKE_ERROR_NETWORK);
Svc.Prefs.reset("jpake.serverURL");
Svc.Prefs.set("jpake.serverURL", serverURL);
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();
});