mirror of
https://github.com/mozilla/gecko-dev.git
synced 2024-11-27 23:02:20 +00:00
Bug 689428 - Part 1: Implement KeyExchange v3 in JPAKEClient. r=rnewman
This commit is contained in:
parent
4670feeaf3
commit
8921d45b6a
@ -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",
|
||||
|
@ -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);
|
||||
}
|
||||
|
||||
};
|
||||
|
@ -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");
|
||||
|
@ -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();
|
||||
});
|
||||
|
||||
|
||||
|
Loading…
Reference in New Issue
Block a user