1
/* ***** BEGIN LICENSE BLOCK *****
2
* Version: MPL 1.1/GPL 2.0/LGPL 2.1
4
* The contents of this file are subject to the Mozilla Public License Version
5
* 1.1 (the "License"); you may not use this file except in compliance with
6
* the License. You may obtain a copy of the License at
7
* http://www.mozilla.org/MPL/
9
* Software distributed under the License is distributed on an "AS IS" basis,
10
* WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License
11
* for the specific language governing rights and limitations under the
14
* The Original Code is Firefox Sync.
16
* The Initial Developer of the Original Code is
18
* Portions created by the Initial Developer are Copyright (C) 2010
19
* the Initial Developer. All Rights Reserved.
22
* Philipp von Weitershausen <philipp@weitershausen.de>
24
* Alternatively, the contents of this file may be used under the terms of
25
* either the GNU General Public License Version 2 or later (the "GPL"), or
26
* the GNU Lesser General Public License Version 2.1 or later (the "LGPL"),
27
* in which case the provisions of the GPL or the LGPL are applicable instead
28
* of those above. If you wish to allow use of your version of this file only
29
* under the terms of either the GPL or the LGPL, and not to allow others to
30
* use your version of this file under the terms of the MPL, indicate your
31
* decision by deleting the provisions above and replace them with the notice
32
* and other provisions required by the GPL or the LGPL. If you do not delete
33
* the provisions above, a recipient may use your version of this file under
34
* the terms of any one of the MPL, the GPL or the LGPL.
36
* ***** END LICENSE BLOCK ***** */
38
const Cc = Components.classes;
39
const Ci = Components.interfaces;
40
const Cr = Components.results;
41
const Cu = Components.utils;
43
Cu.import("resource://services-sync/log4moz.js");
44
Cu.import("resource://services-sync/resource.js");
45
Cu.import("resource://services-sync/constants.js");
46
Cu.import("resource://services-sync/util.js");
48
const EXPORTED_SYMBOLS = ["JPAKEClient"];
50
const JPAKE_SIGNERID_SENDER = "sender";
51
const JPAKE_SIGNERID_RECEIVER = "receiver";
52
const JPAKE_LENGTH_SECRET = 8;
53
const JPAKE_LENGTH_CLIENTID = 256;
54
const JPAKE_VERIFY_VALUE = "0123456789ABCDEF";
58
* Client to exchange encrypted data using the J-PAKE algorithm.
59
* The exchange between two clients of this type looks like this:
62
* Client A Server Client B
63
* ==================================================================
65
* retrieve channel <---------------|
66
* generate random secret |
67
* show PIN = secret + channel | ask user for PIN
68
* upload A's message 1 ----------->|
69
* |--------> retrieve A's message 1
70
* |<---------- upload B's message 1
71
* retrieve B's message 1 <---------|
72
* upload A's message 2 ----------->|
73
* |--------> retrieve A's message 2
75
* |<---------- upload B's message 2
76
* retrieve B's message 2 <---------|
78
* upload sha256d(key) ------------>|
79
* |---------> retrieve sha256d(key)
80
* | verify against own key
82
* |<------------------- upload data
83
* retrieve data <------------------|
88
* Create a client object like so:
90
* let client = new JPAKEClient(observer);
92
* The 'observer' object must implement the following methods:
94
* displayPIN(pin) -- Display the PIN to the user, only called on the client
95
* that didn't provide the PIN.
97
* onComplete(data) -- Called after transfer has been completed. On
98
* the sending side this is called with no parameter and as soon as the
99
* data has been uploaded, which this doesn't mean the receiving side
100
* has actually retrieved them yet.
102
* onAbort(error) -- Called whenever an error is encountered. All errors lead
103
* to an abort and the process has to be started again on both sides.
105
* To start the data transfer on the receiving side, call
107
* client.receiveNoPIN();
109
* This will allocate a new channel on the server, generate a PIN, have it
110
* displayed and then do the transfer once the protocol has been completed
111
* with the sending side.
113
* To initiate the transfer from the sending side, call
115
* client.sendWithPIN(pin, data)
117
* To abort the process, call
121
* Note that after completion or abort, the 'client' instance may not be reused.
122
* You will have to create a new one in case you'd like to restart the process.
124
function JPAKEClient(observer) {
125
this.observer = observer;
127
this._log = Log4Moz.repository.getLogger("Service.JPAKEClient");
128
this._log.level = Log4Moz.Level[Svc.Prefs.get(
129
"log.logger.service.jpakeclient", "Debug")];
131
this._serverUrl = Svc.Prefs.get("jpake.serverURL");
132
this._pollInterval = Svc.Prefs.get("jpake.pollInterval");
133
this._maxTries = Svc.Prefs.get("jpake.maxTries");
134
if (this._serverUrl.slice(-1) != "/")
135
this._serverUrl += "/";
137
this._jpake = Cc["@mozilla.org/services-crypto/sync-jpake;1"]
138
.createInstance(Ci.nsISyncJPAKE);
139
this._auth = new NoOpAuthenticator();
143
JPAKEClient.prototype = {
145
_chain: Utils.asyncChain,
151
receiveNoPIN: function receiveNoPIN() {
152
this._my_signerid = JPAKE_SIGNERID_RECEIVER;
153
this._their_signerid = JPAKE_SIGNERID_SENDER;
155
this._secret = this._createSecret();
157
// Allow a large number of tries first while we wait for the PIN
158
// to be entered on the other device.
159
this._maxTries = Svc.Prefs.get("jpake.firstMsgMaxTries");
160
this._chain(this._getChannel,
161
this._computeStepOne,
165
// Now we can switch back to the smaller timeout.
166
this._maxTries = Svc.Prefs.get("jpake.maxTries");
169
this._computeStepTwo,
173
this._computeKeyVerification,
180
sendWithPIN: function sendWithPIN(pin, obj) {
181
this._my_signerid = JPAKE_SIGNERID_SENDER;
182
this._their_signerid = JPAKE_SIGNERID_RECEIVER;
184
this._channel = pin.slice(JPAKE_LENGTH_SECRET);
185
this._channelUrl = this._serverUrl + this._channel;
186
this._secret = pin.slice(0, JPAKE_LENGTH_SECRET);
187
this._data = JSON.stringify(obj);
189
this._chain(this._computeStepOne,
192
this._computeStepTwo,
202
abort: function abort(error) {
203
this._log.debug("Aborting...");
204
this._finished = true;
206
if (error == JPAKE_ERROR_CHANNEL
207
|| error == JPAKE_ERROR_NETWORK
208
|| error == JPAKE_ERROR_NODATA) {
209
Utils.delay(function() { this.observer.onAbort(error); }, 0,
210
this, "_timer_onAbort");
212
this._reportFailure(error, function() { self.observer.onAbort(error); });
220
_setClientID: function _setClientID() {
221
let rng = Cc["@mozilla.org/security/random-generator;1"]
222
.createInstance(Ci.nsIRandomGenerator);
223
let bytes = rng.generateRandomBytes(JPAKE_LENGTH_CLIENTID / 2);
224
this._clientID = [("0" + byte.toString(16)).slice(-2)
225
for each (byte in bytes)].join("");
228
_createSecret: function _createSecret() {
229
// 0-9a-z without 1,l,o,0
230
const key = "23456789abcdefghijkmnpqrstuvwxyz";
231
let rng = Cc["@mozilla.org/security/random-generator;1"]
232
.createInstance(Ci.nsIRandomGenerator);
233
let bytes = rng.generateRandomBytes(JPAKE_LENGTH_SECRET);
234
return [key[Math.floor(byte * key.length / 256)]
235
for each (byte in bytes)].join("");
239
* Steps of J-PAKE procedure
242
_getChannel: function _getChannel(callback) {
243
this._log.trace("Requesting channel.");
244
let resource = new AsyncResource(this._serverUrl + "new_channel");
245
resource.authenticator = this._auth;
246
resource.setHeader("X-KeyExchange-Id", this._clientID);
247
resource.get(Utils.bind2(this, function handleChannel(error, response) {
252
this._log.error("Error acquiring channel ID. " + error);
253
this.abort(JPAKE_ERROR_CHANNEL);
256
if (response.status != 200) {
257
this._log.error("Error acquiring channel ID. Server responded with HTTP "
259
this.abort(JPAKE_ERROR_CHANNEL);
265
this._channel = response.obj;
267
this._log.error("Server responded with invalid JSON.");
268
this.abort(JPAKE_ERROR_CHANNEL);
271
this._log.debug("Using channel " + this._channel);
272
this._channelUrl = this._serverUrl + this._channel;
274
// Don't block on UI code.
275
let pin = this._secret + this._channel;
276
Utils.delay(function() { this.observer.displayPIN(pin); }, 0,
277
this, "_timer_displayPIN");
282
// Generic handler for uploading data.
283
_putStep: function _putStep(callback) {
284
this._log.trace("Uploading message " + this._outgoing.type);
285
let resource = new AsyncResource(this._channelUrl);
286
resource.authenticator = this._auth;
287
resource.setHeader("X-KeyExchange-Id", this._clientID);
288
resource.put(this._outgoing, Utils.bind2(this, function (error, response) {
293
this._log.error("Error uploading data. " + error);
294
this.abort(JPAKE_ERROR_NETWORK);
297
if (response.status != 200) {
298
this._log.error("Could not upload data. Server responded with HTTP "
300
this.abort(JPAKE_ERROR_SERVER);
303
// There's no point in returning early here since the next step will
304
// always be a GET so let's pause for twice the poll interval.
305
this._etag = response.headers["etag"];
306
Utils.delay(function () { callback(); }, this._pollInterval * 2, this,
311
// Generic handler for polling for and retrieving data.
313
_getStep: function _getStep(callback) {
314
this._log.trace("Retrieving next message.");
315
let resource = new AsyncResource(this._channelUrl);
316
resource.authenticator = this._auth;
317
resource.setHeader("X-KeyExchange-Id", this._clientID);
319
resource.setHeader("If-None-Match", this._etag);
321
resource.get(Utils.bind2(this, function (error, response) {
326
this._log.error("Error fetching data. " + error);
327
this.abort(JPAKE_ERROR_NETWORK);
331
if (response.status == 304) {
332
this._log.trace("Channel hasn't been updated yet. Will try again later.");
333
if (this._pollTries >= this._maxTries) {
334
this._log.error("Tried for " + this._pollTries + " times, aborting.");
335
this.abort(JPAKE_ERROR_TIMEOUT);
338
this._pollTries += 1;
339
Utils.delay(function() { this._getStep(callback); },
340
this._pollInterval, this, "_pollTimer");
345
if (response.status == 404) {
346
this._log.error("No data found in the channel.");
347
this.abort(JPAKE_ERROR_NODATA);
350
if (response.status != 200) {
351
this._log.error("Could not retrieve data. Server responded with HTTP "
353
this.abort(JPAKE_ERROR_SERVER);
358
this._incoming = response.obj;
360
this._log.error("Server responded with invalid JSON.");
361
this.abort(JPAKE_ERROR_INVALID);
364
this._log.trace("Fetched message " + this._incoming.type);
369
_reportFailure: function _reportFailure(reason, callback) {
370
this._log.debug("Reporting failure to server.");
371
let resource = new AsyncResource(this._serverUrl + "report");
372
resource.authenticator = this._auth;
373
resource.setHeader("X-KeyExchange-Id", this._clientID);
374
resource.setHeader("X-KeyExchange-Cid", this._channel);
375
resource.setHeader("X-KeyExchange-Log", reason);
376
resource.post("", Utils.bind2(this, function (error, response) {
378
this._log.warn("Report failed: " + error);
379
else if (response.status != 200)
380
this._log.warn("Report failed. Server responded with HTTP "
383
// Do not block on errors, we're done or aborted by now anyway.
388
_computeStepOne: function _computeStepOne(callback) {
389
this._log.trace("Computing round 1.");
397
this._jpake.round1(this._my_signerid, gx1, gv1, r1, gx2, gv2, r2);
399
this._log.error("JPAKE round 1 threw: " + ex);
400
this.abort(JPAKE_ERROR_INTERNAL);
403
let one = {gx1: gx1.value,
405
zkp_x1: {gr: gv1.value, b: r1.value, id: this._my_signerid},
406
zkp_x2: {gr: gv2.value, b: r2.value, id: this._my_signerid}};
407
this._outgoing = {type: this._my_signerid + "1", payload: one};
408
this._log.trace("Generated message " + this._outgoing.type);
412
_computeStepTwo: function _computeStepTwo(callback) {
413
this._log.trace("Computing round 2.");
414
if (this._incoming.type != this._their_signerid + "1") {
415
this._log.error("Invalid round 1 message: "
416
+ JSON.stringify(this._incoming));
417
this.abort(JPAKE_ERROR_WRONGMESSAGE);
421
let step1 = this._incoming.payload;
422
if (!step1 || !step1.zkp_x1 || step1.zkp_x1.id != this._their_signerid
423
|| !step1.zkp_x2 || step1.zkp_x2.id != this._their_signerid) {
424
this._log.error("Invalid round 1 payload: " + JSON.stringify(step1));
425
this.abort(JPAKE_ERROR_WRONGMESSAGE);
434
this._jpake.round2(this._their_signerid, this._secret,
435
step1.gx1, step1.zkp_x1.gr, step1.zkp_x1.b,
436
step1.gx2, step1.zkp_x2.gr, step1.zkp_x2.b,
439
this._log.error("JPAKE round 2 threw: " + ex);
440
this.abort(JPAKE_ERROR_INTERNAL);
443
let two = {A: A.value,
444
zkp_A: {gr: gvA.value, b: rA.value, id: this._my_signerid}};
445
this._outgoing = {type: this._my_signerid + "2", payload: two};
446
this._log.trace("Generated message " + this._outgoing.type);
450
_computeFinal: function _computeFinal(callback) {
451
if (this._incoming.type != this._their_signerid + "2") {
452
this._log.error("Invalid round 2 message: "
453
+ JSON.stringify(this._incoming));
454
this.abort(JPAKE_ERROR_WRONGMESSAGE);
458
let step2 = this._incoming.payload;
459
if (!step2 || !step2.zkp_A || step2.zkp_A.id != this._their_signerid) {
460
this._log.error("Invalid round 2 payload: " + JSON.stringify(step1));
461
this.abort(JPAKE_ERROR_WRONGMESSAGE);
469
this._jpake.final(step2.A, step2.zkp_A.gr, step2.zkp_A.b, HMAC_INPUT,
470
aes256Key, hmac256Key);
472
this._log.error("JPAKE final round threw: " + ex);
473
this.abort(JPAKE_ERROR_INTERNAL);
477
this._crypto_key = aes256Key.value;
478
let hmac_key = Utils.makeHMACKey(Utils.safeAtoB(hmac256Key.value));
479
this._hmac_hasher = Utils.makeHMACHasher(Ci.nsICryptoHMAC.SHA256, hmac_key);
484
_computeKeyVerification: function _computeKeyVerification(callback) {
485
this._log.trace("Encrypting key verification value.");
488
iv = Svc.Crypto.generateRandomIV();
489
ciphertext = Svc.Crypto.encrypt(JPAKE_VERIFY_VALUE,
490
this._crypto_key, iv);
492
this._log.error("Failed to encrypt key verification value.");
493
this.abort(JPAKE_ERROR_INTERNAL);
496
this._outgoing = {type: this._my_signerid + "3",
497
payload: {ciphertext: ciphertext, IV: iv}};
498
this._log.trace("Generated message " + this._outgoing.type);
502
_encryptData: function _encryptData(callback) {
503
this._log.trace("Verifying their key.");
504
if (this._incoming.type != this._their_signerid + "3") {
505
this._log.error("Invalid round 3 data: " +
506
JSON.stringify(this._incoming));
507
this.abort(JPAKE_ERROR_WRONGMESSAGE);
510
let step3 = this._incoming.payload;
512
ciphertext = Svc.Crypto.encrypt(JPAKE_VERIFY_VALUE,
513
this._crypto_key, step3.IV);
514
if (ciphertext != step3.ciphertext)
515
throw "Key mismatch!";
517
this._log.error("Keys don't match!");
518
this.abort(JPAKE_ERROR_KEYMISMATCH);
522
this._log.trace("Encrypting data.");
523
let iv, ciphertext, hmac;
525
iv = Svc.Crypto.generateRandomIV();
526
ciphertext = Svc.Crypto.encrypt(this._data, this._crypto_key, iv);
527
hmac = Utils.bytesAsHex(Utils.digestUTF8(ciphertext, this._hmac_hasher));
529
this._log.error("Failed to encrypt data.");
530
this.abort(JPAKE_ERROR_INTERNAL);
533
this._outgoing = {type: this._my_signerid + "3",
534
payload: {ciphertext: ciphertext, IV: iv, hmac: hmac}};
535
this._log.trace("Generated message " + this._outgoing.type);
539
_decryptData: function _decryptData(callback) {
540
this._log.trace("Verifying their key.");
541
if (this._incoming.type != this._their_signerid + "3") {
542
this._log.error("Invalid round 3 data: "
543
+ JSON.stringify(this._incoming));
544
this.abort(JPAKE_ERROR_WRONGMESSAGE);
547
let step3 = this._incoming.payload;
549
let hmac = Utils.bytesAsHex(
550
Utils.digestUTF8(step3.ciphertext, this._hmac_hasher));
551
if (hmac != step3.hmac)
552
throw "HMAC validation failed!";
554
this._log.error("HMAC validation failed.");
555
this.abort(JPAKE_ERROR_KEYMISMATCH);
559
this._log.trace("Decrypting data.");
562
cleartext = Svc.Crypto.decrypt(step3.ciphertext, this._crypto_key,
565
this._log.error("Failed to decrypt data.");
566
this.abort(JPAKE_ERROR_INTERNAL);
571
this._newData = JSON.parse(cleartext);
573
this._log.error("Invalid data data: " + JSON.stringify(cleartext));
574
this.abort(JPAKE_ERROR_INVALID);
578
this._log.trace("Decrypted data.");
582
_complete: function _complete() {
583
this._log.debug("Exchange completed.");
584
this._finished = true;
585
Utils.delay(function () { this.observer.onComplete(this._newData); },
586
0, this, "_timer_onComplete");