Upstream version 10.39.225.0
[platform/framework/web/crosswalk.git] / src / chrome / browser / resources / cryptotoken / multiplesigner.js
1 // Copyright 2014 The Chromium Authors. All rights reserved.
2 // Use of this source code is governed by a BSD-style license that can be
3 // found in the LICENSE file.
4
5 /**
6  * @fileoverview A multiple gnubby signer wraps the process of opening a number
7  * of gnubbies, signing each challenge in an array of challenges until a
8  * success condition is satisfied, and yielding each succeeding gnubby.
9  *
10  */
11 'use strict';
12
13 /**
14  * @typedef {{
15  *   code: number,
16  *   gnubbyId: GnubbyDeviceId,
17  *   challenge: (SignHelperChallenge|undefined),
18  *   info: (ArrayBuffer|undefined)
19  * }}
20  */
21 var MultipleSignerResult;
22
23 /**
24  * Creates a new sign handler that manages signing with all the available
25  * gnubbies.
26  * @param {boolean} forEnroll Whether this signer is signing for an attempted
27  *     enroll operation.
28  * @param {function(boolean)} allCompleteCb Called when this signer completes
29  *     sign attempts, i.e. no further results will be produced. The parameter
30  *     indicates whether any gnubbies are present that have not yet produced a
31  *     final result.
32  * @param {function(MultipleSignerResult, boolean)} gnubbyCompleteCb
33  *     Called with each gnubby/challenge that yields a final result, along with
34  *     whether this signer expects to produce more results. The boolean is a
35  *     hint rather than a promise: it's possible for this signer to produce
36  *     further results after saying it doesn't expect more, or to fail to
37  *     produce further results after saying it does.
38  * @param {number} timeoutMillis A timeout value, beyond whose expiration the
39  *     signer will not attempt any new operations, assuming the caller is no
40  *     longer interested in the outcome.
41  * @param {string=} opt_logMsgUrl A URL to post log messages to.
42  * @constructor
43  */
44 function MultipleGnubbySigner(forEnroll, allCompleteCb, gnubbyCompleteCb,
45     timeoutMillis, opt_logMsgUrl) {
46   /** @private {boolean} */
47   this.forEnroll_ = forEnroll;
48   /** @private {function(boolean)} */
49   this.allCompleteCb_ = allCompleteCb;
50   /** @private {function(MultipleSignerResult, boolean)} */
51   this.gnubbyCompleteCb_ = gnubbyCompleteCb;
52   /** @private {string|undefined} */
53   this.logMsgUrl_ = opt_logMsgUrl;
54
55   /** @private {Array.<SignHelperChallenge>} */
56   this.challenges_ = [];
57   /** @private {boolean} */
58   this.challengesSet_ = false;
59   /** @private {boolean} */
60   this.complete_ = false;
61   /** @private {number} */
62   this.numComplete_ = 0;
63   /** @private {!Object.<string, GnubbyTracker>} */
64   this.gnubbies_ = {};
65   /** @private {Countdown} */
66   this.timer_ = DEVICE_FACTORY_REGISTRY.getCountdownFactory()
67       .createTimer(timeoutMillis);
68   /** @private {Countdown} */
69   this.reenumerateTimer_ = DEVICE_FACTORY_REGISTRY.getCountdownFactory()
70       .createTimer(timeoutMillis);
71 }
72
73 /**
74  * @typedef {{
75  *   index: string,
76  *   signer: SingleGnubbySigner,
77  *   stillGoing: boolean,
78  *   errorStatus: number
79  * }}
80  */
81 var GnubbyTracker;
82
83 /**
84  * Closes this signer's gnubbies, if any are open.
85  */
86 MultipleGnubbySigner.prototype.close = function() {
87   for (var k in this.gnubbies_) {
88     this.gnubbies_[k].signer.close();
89   }
90   this.reenumerateTimer_.clearTimeout();
91   this.timer_.clearTimeout();
92   if (this.reenumerateIntervalTimer_) {
93     this.reenumerateIntervalTimer_.clearTimeout();
94   }
95 };
96
97 /**
98  * Begins signing the given challenges.
99  * @param {Array.<SignHelperChallenge>} challenges The challenges to sign.
100  * @return {boolean} whether the challenges were successfully added.
101  */
102 MultipleGnubbySigner.prototype.doSign = function(challenges) {
103   if (this.challengesSet_) {
104     // Can't add new challenges once they're finalized.
105     return false;
106   }
107
108   if (challenges) {
109     for (var i = 0; i < challenges.length; i++) {
110       var decodedChallenge = {};
111       var challenge = challenges[i];
112       decodedChallenge['challengeHash'] =
113           B64_decode(challenge['challengeHash']);
114       decodedChallenge['appIdHash'] = B64_decode(challenge['appIdHash']);
115       decodedChallenge['keyHandle'] = B64_decode(challenge['keyHandle']);
116       if (challenge['version']) {
117         decodedChallenge['version'] = challenge['version'];
118       }
119       this.challenges_.push(decodedChallenge);
120     }
121   }
122   this.challengesSet_ = true;
123   this.enumerateGnubbies_();
124   return true;
125 };
126
127 /**
128  * Signals this signer to rescan for gnubbies. Useful when the caller has
129  * knowledge that the last device has been removed, and can notify this class
130  * before it will discover it on its own.
131  */
132 MultipleGnubbySigner.prototype.reScanDevices = function() {
133   if (this.reenumerateIntervalTimer_) {
134     this.reenumerateIntervalTimer_.clearTimeout();
135   }
136   this.maybeReEnumerateGnubbies_(true);
137 };
138
139 /**
140  * Enumerates gnubbies.
141  * @private
142  */
143 MultipleGnubbySigner.prototype.enumerateGnubbies_ = function() {
144   DEVICE_FACTORY_REGISTRY.getGnubbyFactory().enumerate(
145       this.enumerateCallback_.bind(this));
146 };
147
148 /**
149  * Called with the result of enumerating gnubbies.
150  * @param {number} rc The return code from enumerating.
151  * @param {Array.<GnubbyDeviceId>} ids The gnubbies enumerated.
152  * @private
153  */
154 MultipleGnubbySigner.prototype.enumerateCallback_ = function(rc, ids) {
155   if (this.complete_) {
156     return;
157   }
158   if (rc || !ids || !ids.length) {
159     this.maybeReEnumerateGnubbies_(true);
160     return;
161   }
162   for (var i = 0; i < ids.length; i++) {
163     this.addGnubby_(ids[i]);
164   }
165   this.maybeReEnumerateGnubbies_(false);
166 };
167
168 /**
169  * How frequently to reenumerate gnubbies when none are found, in milliseconds.
170  * @const
171  */
172 MultipleGnubbySigner.ACTIVE_REENUMERATE_INTERVAL_MILLIS = 200;
173
174 /**
175  * How frequently to reenumerate gnubbies when some are found, in milliseconds.
176  * @const
177  */
178 MultipleGnubbySigner.PASSIVE_REENUMERATE_INTERVAL_MILLIS = 3000;
179
180 /**
181  * Reenumerates gnubbies if there's still time.
182  * @param {boolean} activeScan Whether to poll more aggressively, e.g. if
183  *     there are no devices present.
184  * @private
185  */
186 MultipleGnubbySigner.prototype.maybeReEnumerateGnubbies_ =
187     function(activeScan) {
188   if (this.reenumerateTimer_.expired()) {
189     // If the timer is expired, call timeout_ if there aren't any still-running
190     // gnubbies. (If there are some still running, the last will call timeout_
191     // itself.)
192     if (!this.anyPending_()) {
193       this.timeout_(false);
194     }
195     return;
196   }
197   // Reenumerate more aggressively if there are no gnubbies present than if
198   // there are any.
199   var reenumerateTimeoutMillis;
200   if (activeScan) {
201     reenumerateTimeoutMillis =
202         MultipleGnubbySigner.ACTIVE_REENUMERATE_INTERVAL_MILLIS;
203   } else {
204     reenumerateTimeoutMillis =
205         MultipleGnubbySigner.PASSIVE_REENUMERATE_INTERVAL_MILLIS;
206   }
207   if (reenumerateTimeoutMillis >
208       this.reenumerateTimer_.millisecondsUntilExpired()) {
209     reenumerateTimeoutMillis =
210         this.reenumerateTimer_.millisecondsUntilExpired();
211   }
212   /** @private {Countdown} */
213   this.reenumerateIntervalTimer_ =
214       DEVICE_FACTORY_REGISTRY.getCountdownFactory().createTimer(
215           reenumerateTimeoutMillis, this.enumerateGnubbies_.bind(this));
216 };
217
218 /**
219  * Adds a new gnubby to this signer's list of gnubbies. (Only possible while
220  * this signer is still signing: without this restriction, the completed
221  * callback could be called more than once, in violation of its contract.)
222  * If this signer has challenges to sign, begins signing on the new gnubby with
223  * them.
224  * @param {GnubbyDeviceId} gnubbyId The id of the gnubby to add.
225  * @return {boolean} Whether the gnubby was added successfully.
226  * @private
227  */
228 MultipleGnubbySigner.prototype.addGnubby_ = function(gnubbyId) {
229   var index = JSON.stringify(gnubbyId);
230   if (this.gnubbies_.hasOwnProperty(index)) {
231     // Can't add the same gnubby twice.
232     return false;
233   }
234   var tracker = {
235       index: index,
236       errorStatus: 0,
237       stillGoing: false,
238       signer: null
239   };
240   tracker.signer = new SingleGnubbySigner(
241       gnubbyId,
242       this.forEnroll_,
243       this.signCompletedCallback_.bind(this, tracker),
244       this.timer_.clone(),
245       this.logMsgUrl_);
246   this.gnubbies_[index] = tracker;
247   this.gnubbies_[index].stillGoing =
248       tracker.signer.doSign(this.challenges_);
249   if (!this.gnubbies_[index].errorStatus) {
250     this.gnubbies_[index].errorStatus = 0;
251   }
252   return true;
253 };
254
255 /**
256  * Called by a SingleGnubbySigner upon completion.
257  * @param {GnubbyTracker} tracker The tracker object of the gnubby whose result
258  *     this is.
259  * @param {SingleSignerResult} result The result of the sign operation.
260  * @private
261  */
262 MultipleGnubbySigner.prototype.signCompletedCallback_ =
263     function(tracker, result) {
264   console.log(
265       UTIL_fmt((result.code ? 'failure.' : 'success!') +
266           ' gnubby ' + tracker.index +
267           ' got code ' + result.code.toString(16)));
268   if (!tracker.stillGoing) {
269     console.log(UTIL_fmt('gnubby ' + tracker.index + ' no longer running!'));
270     // Shouldn't ever happen? Disregard.
271     return;
272   }
273   tracker.stillGoing = false;
274   tracker.errorStatus = result.code;
275   var moreExpected = this.tallyCompletedGnubby_();
276   switch (result.code) {
277     case DeviceStatusCodes.GONE_STATUS:
278       // Squelch removed gnubbies: the caller can't act on them. But if this
279       // was the last one, speed up reenumerating.
280       if (!moreExpected) {
281         this.maybeReEnumerateGnubbies_(true);
282       }
283       break;
284
285     default:
286       // Report any other results directly to the caller.
287       this.notifyGnubbyComplete_(tracker, result, moreExpected);
288       break;
289   }
290   if (!moreExpected && this.timer_.expired()) {
291     this.timeout_(false);
292   }
293 };
294
295 /**
296  * Counts another gnubby has having completed, and returns whether more results
297  * are expected.
298  * @return {boolean} Whether more gnubbies are still running.
299  * @private
300  */
301 MultipleGnubbySigner.prototype.tallyCompletedGnubby_ = function() {
302   this.numComplete_++;
303   return this.anyPending_();
304 };
305
306 /**
307  * @return {boolean} Whether more gnubbies are still running.
308  * @private
309  */
310 MultipleGnubbySigner.prototype.anyPending_ = function() {
311   return this.numComplete_ < Object.keys(this.gnubbies_).length;
312 };
313
314 /**
315  * Called upon timeout.
316  * @param {boolean} anyPending Whether any gnubbies are awaiting results.
317  * @private
318  */
319 MultipleGnubbySigner.prototype.timeout_ = function(anyPending) {
320   if (this.complete_) return;
321   this.complete_ = true;
322   // Defer notifying the caller that all are complete, in case the caller is
323   // doing work in response to a gnubbyFound callback and has an inconsistent
324   // view of the state of this signer.
325   var self = this;
326   window.setTimeout(function() {
327     self.allCompleteCb_(anyPending);
328   }, 0);
329 };
330
331 /**
332  * @param {GnubbyTracker} tracker The tracker object of the gnubby whose result
333  *     this is.
334  * @param {SingleSignerResult} result Result object.
335  * @param {boolean} moreExpected Whether more gnubbies may still produce an
336  *     outcome.
337  * @private
338  */
339 MultipleGnubbySigner.prototype.notifyGnubbyComplete_ =
340     function(tracker, result, moreExpected) {
341   console.log(UTIL_fmt('gnubby ' + tracker.index + ' complete (' +
342       result.code.toString(16) + ')'));
343   var signResult = {
344     'code': result.code,
345     'gnubby': result.gnubby,
346     'gnubbyId': tracker.signer.getDeviceId()
347   };
348   if (result['challenge'])
349     signResult['challenge'] = result['challenge'];
350   if (result['info'])
351     signResult['info'] = result['info'];
352   this.gnubbyCompleteCb_(signResult, moreExpected);
353 };