services/sync/modules/jpakeclient.js

branch
TOR_BUG_9701
changeset 15
b8a032363ba2
equal deleted inserted replaced
-1:000000000000 0:a09904b2c1ac
1 /* This Source Code Form is subject to the terms of the Mozilla Public
2 * License, v. 2.0. If a copy of the MPL was not distributed with this
3 * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
4
5 this.EXPORTED_SYMBOLS = ["JPAKEClient", "SendCredentialsController"];
6
7 const {classes: Cc, interfaces: Ci, results: Cr, utils: Cu} = Components;
8
9 Cu.import("resource://gre/modules/Log.jsm");
10 Cu.import("resource://services-common/rest.js");
11 Cu.import("resource://services-sync/constants.js");
12 Cu.import("resource://services-sync/util.js");
13
14 const REQUEST_TIMEOUT = 60; // 1 minute
15 const KEYEXCHANGE_VERSION = 3;
16
17 const JPAKE_SIGNERID_SENDER = "sender";
18 const JPAKE_SIGNERID_RECEIVER = "receiver";
19 const JPAKE_LENGTH_SECRET = 8;
20 const JPAKE_LENGTH_CLIENTID = 256;
21 const JPAKE_VERIFY_VALUE = "0123456789ABCDEF";
22
23
24 /**
25 * Client to exchange encrypted data using the J-PAKE algorithm.
26 * The exchange between two clients of this type looks like this:
27 *
28 *
29 * Mobile Server Desktop
30 * ===================================================================
31 * |
32 * retrieve channel <---------------|
33 * generate random secret |
34 * show PIN = secret + channel | ask user for PIN
35 * upload Mobile's message 1 ------>|
36 * |----> retrieve Mobile's message 1
37 * |<----- upload Desktop's message 1
38 * retrieve Desktop's message 1 <---|
39 * upload Mobile's message 2 ------>|
40 * |----> retrieve Mobile's message 2
41 * | compute key
42 * |<----- upload Desktop's message 2
43 * retrieve Desktop's message 2 <---|
44 * compute key |
45 * encrypt known value ------------>|
46 * |-------> retrieve encrypted value
47 * | verify against local known value
48 *
49 * At this point Desktop knows whether the PIN was entered correctly.
50 * If it wasn't, Desktop deletes the session. If it was, the account
51 * setup can proceed. If Desktop doesn't yet have an account set up,
52 * it will keep the channel open and let the user connect to or
53 * create an account.
54 *
55 * | encrypt credentials
56 * |<------------- upload credentials
57 * retrieve credentials <-----------|
58 * verify HMAC |
59 * decrypt credentials |
60 * delete session ----------------->|
61 * start syncing |
62 *
63 *
64 * Create a client object like so:
65 *
66 * let client = new JPAKEClient(controller);
67 *
68 * The 'controller' object must implement the following methods:
69 *
70 * displayPIN(pin) -- Called when a PIN has been generated and is ready to
71 * be displayed to the user. Only called on the client where the pairing
72 * was initiated with 'receiveNoPIN()'.
73 *
74 * onPairingStart() -- Called when the pairing has started and messages are
75 * being sent back and forth over the channel. Only called on the client
76 * where the pairing was initiated with 'receiveNoPIN()'.
77 *
78 * onPaired() -- Called when the device pairing has been established and
79 * we're ready to send the credentials over. To do that, the controller
80 * must call 'sendAndComplete()' while the channel is active.
81 *
82 * onComplete(data) -- Called after transfer has been completed. On
83 * the sending side this is called with no parameter and as soon as the
84 * data has been uploaded. This does not mean the receiving side has
85 * actually retrieved them yet.
86 *
87 * onAbort(error) -- Called whenever an error is encountered. All errors lead
88 * to an abort and the process has to be started again on both sides.
89 *
90 * To start the data transfer on the receiving side, call
91 *
92 * client.receiveNoPIN();
93 *
94 * This will allocate a new channel on the server, generate a PIN, have it
95 * displayed and then do the transfer once the protocol has been completed
96 * with the sending side.
97 *
98 * To initiate the transfer from the sending side, call
99 *
100 * client.pairWithPIN(pin, true);
101 *
102 * Once the pairing has been established, the controller's 'onPaired()' method
103 * will be called. To then transmit the data, call
104 *
105 * client.sendAndComplete(data);
106 *
107 * To abort the process, call
108 *
109 * client.abort();
110 *
111 * Note that after completion or abort, the 'client' instance may not be reused.
112 * You will have to create a new one in case you'd like to restart the process.
113 */
114 this.JPAKEClient = function JPAKEClient(controller) {
115 this.controller = controller;
116
117 this._log = Log.repository.getLogger("Sync.JPAKEClient");
118 this._log.level = Log.Level[Svc.Prefs.get(
119 "log.logger.service.jpakeclient", "Debug")];
120
121 this._serverURL = Svc.Prefs.get("jpake.serverURL");
122 this._pollInterval = Svc.Prefs.get("jpake.pollInterval");
123 this._maxTries = Svc.Prefs.get("jpake.maxTries");
124 if (this._serverURL.slice(-1) != "/") {
125 this._serverURL += "/";
126 }
127
128 this._jpake = Cc["@mozilla.org/services-crypto/sync-jpake;1"]
129 .createInstance(Ci.nsISyncJPAKE);
130
131 this._setClientID();
132 }
133 JPAKEClient.prototype = {
134
135 _chain: Async.chain,
136
137 /*
138 * Public API
139 */
140
141 /**
142 * Initiate pairing and receive data without providing a PIN. The PIN will
143 * be generated and passed on to the controller to be displayed to the user.
144 *
145 * This is typically called on mobile devices where typing is tedious.
146 */
147 receiveNoPIN: function receiveNoPIN() {
148 this._my_signerid = JPAKE_SIGNERID_RECEIVER;
149 this._their_signerid = JPAKE_SIGNERID_SENDER;
150
151 this._secret = this._createSecret();
152
153 // Allow a large number of tries first while we wait for the PIN
154 // to be entered on the other device.
155 this._maxTries = Svc.Prefs.get("jpake.firstMsgMaxTries");
156 this._chain(this._getChannel,
157 this._computeStepOne,
158 this._putStep,
159 this._getStep,
160 function(callback) {
161 // We fetched the first response from the other client.
162 // Notify controller of the pairing starting.
163 Utils.nextTick(this.controller.onPairingStart,
164 this.controller);
165
166 // Now we can switch back to the smaller timeout.
167 this._maxTries = Svc.Prefs.get("jpake.maxTries");
168 callback();
169 },
170 this._computeStepTwo,
171 this._putStep,
172 this._getStep,
173 this._computeFinal,
174 this._computeKeyVerification,
175 this._putStep,
176 function(callback) {
177 // Allow longer time-out for the last message.
178 this._maxTries = Svc.Prefs.get("jpake.lastMsgMaxTries");
179 callback();
180 },
181 this._getStep,
182 this._decryptData,
183 this._complete)();
184 },
185
186 /**
187 * Initiate pairing based on the PIN entered by the user.
188 *
189 * This is typically called on desktop devices where typing is easier than
190 * on mobile.
191 *
192 * @param pin
193 * 12 character string (in human-friendly base32) containing the PIN
194 * entered by the user.
195 * @param expectDelay
196 * Flag that indicates that a significant delay between the pairing
197 * and the sending should be expected. v2 and earlier of the protocol
198 * did not allow for this and the pairing to a v2 or earlier client
199 * will be aborted if this flag is 'true'.
200 */
201 pairWithPIN: function pairWithPIN(pin, expectDelay) {
202 this._my_signerid = JPAKE_SIGNERID_SENDER;
203 this._their_signerid = JPAKE_SIGNERID_RECEIVER;
204
205 this._channel = pin.slice(JPAKE_LENGTH_SECRET);
206 this._channelURL = this._serverURL + this._channel;
207 this._secret = pin.slice(0, JPAKE_LENGTH_SECRET);
208
209 this._chain(this._computeStepOne,
210 this._getStep,
211 function (callback) {
212 // Ensure that the other client can deal with a delay for
213 // the last message if that's requested by the caller.
214 if (!expectDelay) {
215 return callback();
216 }
217 if (!this._incoming.version || this._incoming.version < 3) {
218 return this.abort(JPAKE_ERROR_DELAYUNSUPPORTED);
219 }
220 return callback();
221 },
222 this._putStep,
223 this._computeStepTwo,
224 this._getStep,
225 this._putStep,
226 this._computeFinal,
227 this._getStep,
228 this._verifyPairing)();
229 },
230
231 /**
232 * Send data after a successful pairing.
233 *
234 * @param obj
235 * Object containing the data to send. It will be serialized as JSON.
236 */
237 sendAndComplete: function sendAndComplete(obj) {
238 if (!this._paired || this._finished) {
239 this._log.error("Can't send data, no active pairing!");
240 throw "No active pairing!";
241 }
242 this._data = JSON.stringify(obj);
243 this._chain(this._encryptData,
244 this._putStep,
245 this._complete)();
246 },
247
248 /**
249 * Abort the current pairing. The channel on the server will be deleted
250 * if the abort wasn't due to a network or server error. The controller's
251 * 'onAbort()' method is notified in all cases.
252 *
253 * @param error [optional]
254 * Error constant indicating the reason for the abort. Defaults to
255 * user abort.
256 */
257 abort: function abort(error) {
258 this._log.debug("Aborting...");
259 this._finished = true;
260 let self = this;
261
262 // Default to "user aborted".
263 if (!error) {
264 error = JPAKE_ERROR_USERABORT;
265 }
266
267 if (error == JPAKE_ERROR_CHANNEL ||
268 error == JPAKE_ERROR_NETWORK ||
269 error == JPAKE_ERROR_NODATA) {
270 Utils.nextTick(function() { this.controller.onAbort(error); }, this);
271 } else {
272 this._reportFailure(error, function() { self.controller.onAbort(error); });
273 }
274 },
275
276 /*
277 * Utilities
278 */
279
280 _setClientID: function _setClientID() {
281 let rng = Cc["@mozilla.org/security/random-generator;1"]
282 .createInstance(Ci.nsIRandomGenerator);
283 let bytes = rng.generateRandomBytes(JPAKE_LENGTH_CLIENTID / 2);
284 this._clientID = [("0" + byte.toString(16)).slice(-2)
285 for each (byte in bytes)].join("");
286 },
287
288 _createSecret: function _createSecret() {
289 // 0-9a-z without 1,l,o,0
290 const key = "23456789abcdefghijkmnpqrstuvwxyz";
291 let rng = Cc["@mozilla.org/security/random-generator;1"]
292 .createInstance(Ci.nsIRandomGenerator);
293 let bytes = rng.generateRandomBytes(JPAKE_LENGTH_SECRET);
294 return [key[Math.floor(byte * key.length / 256)]
295 for each (byte in bytes)].join("");
296 },
297
298 _newRequest: function _newRequest(uri) {
299 let request = new RESTRequest(uri);
300 request.setHeader("X-KeyExchange-Id", this._clientID);
301 request.timeout = REQUEST_TIMEOUT;
302 return request;
303 },
304
305 /*
306 * Steps of J-PAKE procedure
307 */
308
309 _getChannel: function _getChannel(callback) {
310 this._log.trace("Requesting channel.");
311 let request = this._newRequest(this._serverURL + "new_channel");
312 request.get(Utils.bind2(this, function handleChannel(error) {
313 if (this._finished) {
314 return;
315 }
316
317 if (error) {
318 this._log.error("Error acquiring channel ID. " + error);
319 this.abort(JPAKE_ERROR_CHANNEL);
320 return;
321 }
322 if (request.response.status != 200) {
323 this._log.error("Error acquiring channel ID. Server responded with HTTP "
324 + request.response.status);
325 this.abort(JPAKE_ERROR_CHANNEL);
326 return;
327 }
328
329 try {
330 this._channel = JSON.parse(request.response.body);
331 } catch (ex) {
332 this._log.error("Server responded with invalid JSON.");
333 this.abort(JPAKE_ERROR_CHANNEL);
334 return;
335 }
336 this._log.debug("Using channel " + this._channel);
337 this._channelURL = this._serverURL + this._channel;
338
339 // Don't block on UI code.
340 let pin = this._secret + this._channel;
341 Utils.nextTick(function() { this.controller.displayPIN(pin); }, this);
342 callback();
343 }));
344 },
345
346 // Generic handler for uploading data.
347 _putStep: function _putStep(callback) {
348 this._log.trace("Uploading message " + this._outgoing.type);
349 let request = this._newRequest(this._channelURL);
350 if (this._their_etag) {
351 request.setHeader("If-Match", this._their_etag);
352 } else {
353 request.setHeader("If-None-Match", "*");
354 }
355 request.put(this._outgoing, Utils.bind2(this, function (error) {
356 if (this._finished) {
357 return;
358 }
359
360 if (error) {
361 this._log.error("Error uploading data. " + error);
362 this.abort(JPAKE_ERROR_NETWORK);
363 return;
364 }
365 if (request.response.status != 200) {
366 this._log.error("Could not upload data. Server responded with HTTP "
367 + request.response.status);
368 this.abort(JPAKE_ERROR_SERVER);
369 return;
370 }
371 // There's no point in returning early here since the next step will
372 // always be a GET so let's pause for twice the poll interval.
373 this._my_etag = request.response.headers["etag"];
374 Utils.namedTimer(function () { callback(); }, this._pollInterval * 2,
375 this, "_pollTimer");
376 }));
377 },
378
379 // Generic handler for polling for and retrieving data.
380 _pollTries: 0,
381 _getStep: function _getStep(callback) {
382 this._log.trace("Retrieving next message.");
383 let request = this._newRequest(this._channelURL);
384 if (this._my_etag) {
385 request.setHeader("If-None-Match", this._my_etag);
386 }
387
388 request.get(Utils.bind2(this, function (error) {
389 if (this._finished) {
390 return;
391 }
392
393 if (error) {
394 this._log.error("Error fetching data. " + error);
395 this.abort(JPAKE_ERROR_NETWORK);
396 return;
397 }
398
399 if (request.response.status == 304) {
400 this._log.trace("Channel hasn't been updated yet. Will try again later.");
401 if (this._pollTries >= this._maxTries) {
402 this._log.error("Tried for " + this._pollTries + " times, aborting.");
403 this.abort(JPAKE_ERROR_TIMEOUT);
404 return;
405 }
406 this._pollTries += 1;
407 Utils.namedTimer(function() { this._getStep(callback); },
408 this._pollInterval, this, "_pollTimer");
409 return;
410 }
411 this._pollTries = 0;
412
413 if (request.response.status == 404) {
414 this._log.error("No data found in the channel.");
415 this.abort(JPAKE_ERROR_NODATA);
416 return;
417 }
418 if (request.response.status != 200) {
419 this._log.error("Could not retrieve data. Server responded with HTTP "
420 + request.response.status);
421 this.abort(JPAKE_ERROR_SERVER);
422 return;
423 }
424
425 this._their_etag = request.response.headers["etag"];
426 if (!this._their_etag) {
427 this._log.error("Server did not supply ETag for message: "
428 + request.response.body);
429 this.abort(JPAKE_ERROR_SERVER);
430 return;
431 }
432
433 try {
434 this._incoming = JSON.parse(request.response.body);
435 } catch (ex) {
436 this._log.error("Server responded with invalid JSON.");
437 this.abort(JPAKE_ERROR_INVALID);
438 return;
439 }
440 this._log.trace("Fetched message " + this._incoming.type);
441 callback();
442 }));
443 },
444
445 _reportFailure: function _reportFailure(reason, callback) {
446 this._log.debug("Reporting failure to server.");
447 let request = this._newRequest(this._serverURL + "report");
448 request.setHeader("X-KeyExchange-Cid", this._channel);
449 request.setHeader("X-KeyExchange-Log", reason);
450 request.post("", Utils.bind2(this, function (error) {
451 if (error) {
452 this._log.warn("Report failed: " + error);
453 } else if (request.response.status != 200) {
454 this._log.warn("Report failed. Server responded with HTTP "
455 + request.response.status);
456 }
457
458 // Do not block on errors, we're done or aborted by now anyway.
459 callback();
460 }));
461 },
462
463 _computeStepOne: function _computeStepOne(callback) {
464 this._log.trace("Computing round 1.");
465 let gx1 = {};
466 let gv1 = {};
467 let r1 = {};
468 let gx2 = {};
469 let gv2 = {};
470 let r2 = {};
471 try {
472 this._jpake.round1(this._my_signerid, gx1, gv1, r1, gx2, gv2, r2);
473 } catch (ex) {
474 this._log.error("JPAKE round 1 threw: " + ex);
475 this.abort(JPAKE_ERROR_INTERNAL);
476 return;
477 }
478 let one = {gx1: gx1.value,
479 gx2: gx2.value,
480 zkp_x1: {gr: gv1.value, b: r1.value, id: this._my_signerid},
481 zkp_x2: {gr: gv2.value, b: r2.value, id: this._my_signerid}};
482 this._outgoing = {type: this._my_signerid + "1",
483 version: KEYEXCHANGE_VERSION,
484 payload: one};
485 this._log.trace("Generated message " + this._outgoing.type);
486 callback();
487 },
488
489 _computeStepTwo: function _computeStepTwo(callback) {
490 this._log.trace("Computing round 2.");
491 if (this._incoming.type != this._their_signerid + "1") {
492 this._log.error("Invalid round 1 message: "
493 + JSON.stringify(this._incoming));
494 this.abort(JPAKE_ERROR_WRONGMESSAGE);
495 return;
496 }
497
498 let step1 = this._incoming.payload;
499 if (!step1 || !step1.zkp_x1 || step1.zkp_x1.id != this._their_signerid
500 || !step1.zkp_x2 || step1.zkp_x2.id != this._their_signerid) {
501 this._log.error("Invalid round 1 payload: " + JSON.stringify(step1));
502 this.abort(JPAKE_ERROR_WRONGMESSAGE);
503 return;
504 }
505
506 let A = {};
507 let gvA = {};
508 let rA = {};
509
510 try {
511 this._jpake.round2(this._their_signerid, this._secret,
512 step1.gx1, step1.zkp_x1.gr, step1.zkp_x1.b,
513 step1.gx2, step1.zkp_x2.gr, step1.zkp_x2.b,
514 A, gvA, rA);
515 } catch (ex) {
516 this._log.error("JPAKE round 2 threw: " + ex);
517 this.abort(JPAKE_ERROR_INTERNAL);
518 return;
519 }
520 let two = {A: A.value,
521 zkp_A: {gr: gvA.value, b: rA.value, id: this._my_signerid}};
522 this._outgoing = {type: this._my_signerid + "2",
523 version: KEYEXCHANGE_VERSION,
524 payload: two};
525 this._log.trace("Generated message " + this._outgoing.type);
526 callback();
527 },
528
529 _computeFinal: function _computeFinal(callback) {
530 if (this._incoming.type != this._their_signerid + "2") {
531 this._log.error("Invalid round 2 message: "
532 + JSON.stringify(this._incoming));
533 this.abort(JPAKE_ERROR_WRONGMESSAGE);
534 return;
535 }
536
537 let step2 = this._incoming.payload;
538 if (!step2 || !step2.zkp_A || step2.zkp_A.id != this._their_signerid) {
539 this._log.error("Invalid round 2 payload: " + JSON.stringify(step1));
540 this.abort(JPAKE_ERROR_WRONGMESSAGE);
541 return;
542 }
543
544 let aes256Key = {};
545 let hmac256Key = {};
546
547 try {
548 this._jpake.final(step2.A, step2.zkp_A.gr, step2.zkp_A.b, HMAC_INPUT,
549 aes256Key, hmac256Key);
550 } catch (ex) {
551 this._log.error("JPAKE final round threw: " + ex);
552 this.abort(JPAKE_ERROR_INTERNAL);
553 return;
554 }
555
556 this._crypto_key = aes256Key.value;
557 let hmac_key = Utils.makeHMACKey(Utils.safeAtoB(hmac256Key.value));
558 this._hmac_hasher = Utils.makeHMACHasher(Ci.nsICryptoHMAC.SHA256, hmac_key);
559
560 callback();
561 },
562
563 _computeKeyVerification: function _computeKeyVerification(callback) {
564 this._log.trace("Encrypting key verification value.");
565 let iv, ciphertext;
566 try {
567 iv = Svc.Crypto.generateRandomIV();
568 ciphertext = Svc.Crypto.encrypt(JPAKE_VERIFY_VALUE,
569 this._crypto_key, iv);
570 } catch (ex) {
571 this._log.error("Failed to encrypt key verification value.");
572 this.abort(JPAKE_ERROR_INTERNAL);
573 return;
574 }
575 this._outgoing = {type: this._my_signerid + "3",
576 version: KEYEXCHANGE_VERSION,
577 payload: {ciphertext: ciphertext, IV: iv}};
578 this._log.trace("Generated message " + this._outgoing.type);
579 callback();
580 },
581
582 _verifyPairing: function _verifyPairing(callback) {
583 this._log.trace("Verifying their key.");
584 if (this._incoming.type != this._their_signerid + "3") {
585 this._log.error("Invalid round 3 data: " +
586 JSON.stringify(this._incoming));
587 this.abort(JPAKE_ERROR_WRONGMESSAGE);
588 return;
589 }
590 let step3 = this._incoming.payload;
591 let ciphertext;
592 try {
593 ciphertext = Svc.Crypto.encrypt(JPAKE_VERIFY_VALUE,
594 this._crypto_key, step3.IV);
595 if (ciphertext != step3.ciphertext) {
596 throw "Key mismatch!";
597 }
598 } catch (ex) {
599 this._log.error("Keys don't match!");
600 this.abort(JPAKE_ERROR_KEYMISMATCH);
601 return;
602 }
603
604 this._log.debug("Verified pairing!");
605 this._paired = true;
606 Utils.nextTick(function () { this.controller.onPaired(); }, this);
607 callback();
608 },
609
610 _encryptData: function _encryptData(callback) {
611 this._log.trace("Encrypting data.");
612 let iv, ciphertext, hmac;
613 try {
614 iv = Svc.Crypto.generateRandomIV();
615 ciphertext = Svc.Crypto.encrypt(this._data, this._crypto_key, iv);
616 hmac = Utils.bytesAsHex(Utils.digestUTF8(ciphertext, this._hmac_hasher));
617 } catch (ex) {
618 this._log.error("Failed to encrypt data.");
619 this.abort(JPAKE_ERROR_INTERNAL);
620 return;
621 }
622 this._outgoing = {type: this._my_signerid + "3",
623 version: KEYEXCHANGE_VERSION,
624 payload: {ciphertext: ciphertext, IV: iv, hmac: hmac}};
625 this._log.trace("Generated message " + this._outgoing.type);
626 callback();
627 },
628
629 _decryptData: function _decryptData(callback) {
630 this._log.trace("Verifying their key.");
631 if (this._incoming.type != this._their_signerid + "3") {
632 this._log.error("Invalid round 3 data: "
633 + JSON.stringify(this._incoming));
634 this.abort(JPAKE_ERROR_WRONGMESSAGE);
635 return;
636 }
637 let step3 = this._incoming.payload;
638 try {
639 let hmac = Utils.bytesAsHex(
640 Utils.digestUTF8(step3.ciphertext, this._hmac_hasher));
641 if (hmac != step3.hmac) {
642 throw "HMAC validation failed!";
643 }
644 } catch (ex) {
645 this._log.error("HMAC validation failed.");
646 this.abort(JPAKE_ERROR_KEYMISMATCH);
647 return;
648 }
649
650 this._log.trace("Decrypting data.");
651 let cleartext;
652 try {
653 cleartext = Svc.Crypto.decrypt(step3.ciphertext, this._crypto_key,
654 step3.IV);
655 } catch (ex) {
656 this._log.error("Failed to decrypt data.");
657 this.abort(JPAKE_ERROR_INTERNAL);
658 return;
659 }
660
661 try {
662 this._newData = JSON.parse(cleartext);
663 } catch (ex) {
664 this._log.error("Invalid data data: " + JSON.stringify(cleartext));
665 this.abort(JPAKE_ERROR_INVALID);
666 return;
667 }
668
669 this._log.trace("Decrypted data.");
670 callback();
671 },
672
673 _complete: function _complete() {
674 this._log.debug("Exchange completed.");
675 this._finished = true;
676 Utils.nextTick(function () { this.controller.onComplete(this._newData); },
677 this);
678 }
679
680 };
681
682
683 /**
684 * Send credentials over an active J-PAKE channel.
685 *
686 * This object is designed to take over as the JPAKEClient controller,
687 * presumably replacing one that is UI-based which would either cause
688 * DOM objects to leak or the JPAKEClient to be GC'ed when the DOM
689 * context disappears. This object stays alive for the duration of the
690 * transfer by being strong-ref'ed as an nsIObserver.
691 *
692 * Credentials are sent after the first sync has been completed
693 * (successfully or not.)
694 *
695 * Usage:
696 *
697 * jpakeclient.controller = new SendCredentialsController(jpakeclient,
698 * service);
699 *
700 */
701 this.SendCredentialsController =
702 function SendCredentialsController(jpakeclient, service) {
703 this._log = Log.repository.getLogger("Sync.SendCredentialsController");
704 this._log.level = Log.Level[Svc.Prefs.get("log.logger.service.main")];
705
706 this._log.trace("Loading.");
707 this.jpakeclient = jpakeclient;
708 this.service = service;
709
710 // Register ourselves as observers the first Sync finishing (either
711 // successfully or unsuccessfully, we don't care) or for removing
712 // this device's sync configuration, in case that happens while we
713 // haven't finished the first sync yet.
714 Services.obs.addObserver(this, "weave:service:sync:finish", false);
715 Services.obs.addObserver(this, "weave:service:sync:error", false);
716 Services.obs.addObserver(this, "weave:service:start-over", false);
717 }
718 SendCredentialsController.prototype = {
719
720 unload: function unload() {
721 this._log.trace("Unloading.");
722 try {
723 Services.obs.removeObserver(this, "weave:service:sync:finish");
724 Services.obs.removeObserver(this, "weave:service:sync:error");
725 Services.obs.removeObserver(this, "weave:service:start-over");
726 } catch (ex) {
727 // Ignore.
728 }
729 },
730
731 observe: function observe(subject, topic, data) {
732 switch (topic) {
733 case "weave:service:sync:finish":
734 case "weave:service:sync:error":
735 Utils.nextTick(this.sendCredentials, this);
736 break;
737 case "weave:service:start-over":
738 // This will call onAbort which will call unload().
739 this.jpakeclient.abort();
740 break;
741 }
742 },
743
744 sendCredentials: function sendCredentials() {
745 this._log.trace("Sending credentials.");
746 let credentials = {account: this.service.identity.account,
747 password: this.service.identity.basicPassword,
748 synckey: this.service.identity.syncKey,
749 serverURL: this.service.serverURL};
750 this.jpakeclient.sendAndComplete(credentials);
751 },
752
753 // JPAKEClient controller API
754
755 onComplete: function onComplete() {
756 this._log.debug("Exchange was completed successfully!");
757 this.unload();
758
759 // Schedule a Sync for soonish to fetch the data uploaded by the
760 // device with which we just paired.
761 this.service.scheduler.scheduleNextSync(this.service.scheduler.activeInterval);
762 },
763
764 onAbort: function onAbort(error) {
765 // It doesn't really matter why we aborted, but the channel is closed
766 // for sure, so we won't be able to do anything with it.
767 this._log.debug("Exchange was aborted with error: " + error);
768 this.unload();
769 },
770
771 // Irrelevant methods for this controller:
772 displayPIN: function displayPIN() {},
773 onPairingStart: function onPairingStart() {},
774 onPaired: function onPaired() {},
775 };

mercurial