0d4bedeb00df79de39cb150a52a501f89a366c84
[platform/framework/web/crosswalk.git] / src / chrome / browser / resources / cryptotoken / singlesigner.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 single gnubby signer wraps the process of opening a gnubby,
7  * signing each challenge in an array of challenges until a success condition
8  * is satisfied, and finally yielding the gnubby upon success.
9  *
10  */
11
12 'use strict';
13
14 /**
15  * @typedef {{
16  *   code: number,
17  *   gnubby: (Gnubby|undefined),
18  *   challenge: (SignHelperChallenge|undefined),
19  *   info: (ArrayBuffer|undefined)
20  * }}
21  */
22 var SingleSignerResult;
23
24 /**
25  * Creates a new sign handler with a gnubby. This handler will perform a sign
26  * operation using each challenge in an array of challenges until its success
27  * condition is satisified, or an error or timeout occurs. The success condition
28  * is defined differently depending whether this signer is used for enrolling
29  * or for signing:
30  *
31  * For enroll, success is defined as each challenge yielding wrong data. This
32  * means this gnubby is not currently enrolled for any of the appIds in any
33  * challenge.
34  *
35  * For sign, success is defined as any challenge yielding ok.
36  *
37  * The complete callback is called only when the signer reaches success or
38  * failure, i.e.  when there is no need for this signer to continue trying new
39  * challenges.
40  *
41  * @param {GnubbyDeviceId} gnubbyId Which gnubby to open.
42  * @param {boolean} forEnroll Whether this signer is signing for an attempted
43  *     enroll operation.
44  * @param {function(SingleSignerResult)}
45  *     completeCb Called when this signer completes, i.e. no further results are
46  *     possible.
47  * @param {Countdown} timer An advisory timer, beyond whose expiration the
48  *     signer will not attempt any new operations, assuming the caller is no
49  *     longer interested in the outcome.
50  * @param {string=} opt_logMsgUrl A URL to post log messages to.
51  * @constructor
52  */
53 function SingleGnubbySigner(gnubbyId, forEnroll, completeCb, timer,
54     opt_logMsgUrl) {
55   /** @private {GnubbyDeviceId} */
56   this.gnubbyId_ = gnubbyId;
57   /** @private {SingleGnubbySigner.State} */
58   this.state_ = SingleGnubbySigner.State.INIT;
59   /** @private {boolean} */
60   this.forEnroll_ = forEnroll;
61   /** @private {function(SingleSignerResult)} */
62   this.completeCb_ = completeCb;
63   /** @private {Countdown} */
64   this.timer_ = timer;
65   /** @private {string|undefined} */
66   this.logMsgUrl_ = opt_logMsgUrl;
67
68   /** @private {!Array.<!SignHelperChallenge>} */
69   this.challenges_ = [];
70   /** @private {number} */
71   this.challengeIndex_ = 0;
72   /** @private {boolean} */
73   this.challengesSet_ = false;
74
75   /** @private {!Array.<string>} */
76   this.notForMe_ = [];
77 }
78
79 /** @enum {number} */
80 SingleGnubbySigner.State = {
81   /** Initial state. */
82   INIT: 0,
83   /** The signer is attempting to open a gnubby. */
84   OPENING: 1,
85   /** The signer's gnubby opened, but is busy. */
86   BUSY: 2,
87   /** The signer has an open gnubby, but no challenges to sign. */
88   IDLE: 3,
89   /** The signer is currently signing a challenge. */
90   SIGNING: 4,
91   /** The signer got a final outcome. */
92   COMPLETE: 5,
93   /** The signer is closing its gnubby. */
94   CLOSING: 6,
95   /** The signer is closed. */
96   CLOSED: 7
97 };
98
99 /**
100  * @return {GnubbyDeviceId} This device id of the gnubby for this signer.
101  */
102 SingleGnubbySigner.prototype.getDeviceId = function() {
103   return this.gnubbyId_;
104 };
105
106 /**
107  * Attempts to open this signer's gnubby, if it's not already open.
108  * (This is implicitly done by addChallenges.)
109  */
110 SingleGnubbySigner.prototype.open = function() {
111   if (this.state_ == SingleGnubbySigner.State.INIT) {
112     this.state_ = SingleGnubbySigner.State.OPENING;
113     DEVICE_FACTORY_REGISTRY.getGnubbyFactory().openGnubby(
114         this.gnubbyId_,
115         this.forEnroll_,
116         this.openCallback_.bind(this),
117         this.logMsgUrl_);
118   }
119 };
120
121 /**
122  * Closes this signer's gnubby, if it's held.
123  */
124 SingleGnubbySigner.prototype.close = function() {
125   if (!this.gnubby_) return;
126   this.state_ = SingleGnubbySigner.State.CLOSING;
127   this.gnubby_.closeWhenIdle(this.closed_.bind(this));
128 };
129
130 /**
131  * Called when this signer's gnubby is closed.
132  * @private
133  */
134 SingleGnubbySigner.prototype.closed_ = function() {
135   this.gnubby_ = null;
136   this.state_ = SingleGnubbySigner.State.CLOSED;
137 };
138
139 /**
140  * Begins signing the given challenges.
141  * @param {Array.<SignHelperChallenge>} challenges The challenges to sign.
142  * @return {boolean} Whether the challenges were accepted.
143  */
144 SingleGnubbySigner.prototype.doSign = function(challenges) {
145   if (this.challengesSet_) {
146     // Can't add new challenges once they've been set.
147     return false;
148   }
149
150   if (challenges) {
151     console.log(this.gnubby_);
152     console.log(UTIL_fmt('adding ' + challenges.length + ' challenges'));
153     for (var i = 0; i < challenges.length; i++) {
154       this.challenges_.push(challenges[i]);
155     }
156   }
157   this.challengesSet_ = true;
158
159   switch (this.state_) {
160     case SingleGnubbySigner.State.INIT:
161       this.open();
162       break;
163     case SingleGnubbySigner.State.OPENING:
164       // The open has already commenced, so accept the challenges, but don't do
165       // anything.
166       break;
167     case SingleGnubbySigner.State.IDLE:
168       if (this.challengeIndex_ < challenges.length) {
169         // Challenges set: start signing.
170         this.doSign_(this.challengeIndex_);
171       } else {
172         // An empty list of challenges can be set during enroll, when the user
173         // has no existing enrolled gnubbies. It's unexpected during sign, but
174         // returning WRONG_DATA satisfies the caller in either case.
175         var self = this;
176         window.setTimeout(function() {
177           self.goToError_(DeviceStatusCodes.WRONG_DATA_STATUS);
178         }, 0);
179       }
180       break;
181     case SingleGnubbySigner.State.SIGNING:
182       // Already signing, so don't kick off a new sign, but accept the added
183       // challenges.
184       break;
185     default:
186       return false;
187   }
188   return true;
189 };
190
191 /**
192  * How long to delay retrying a failed open.
193  */
194 SingleGnubbySigner.OPEN_DELAY_MILLIS = 200;
195
196 /**
197  * How long to delay retrying a sign requiring touch.
198  */
199 SingleGnubbySigner.SIGN_DELAY_MILLIS = 200;
200
201 /**
202  * @param {number} rc The result of the open operation.
203  * @param {Gnubby=} gnubby The opened gnubby, if open was successful (or busy).
204  * @private
205  */
206 SingleGnubbySigner.prototype.openCallback_ = function(rc, gnubby) {
207   if (this.state_ != SingleGnubbySigner.State.OPENING &&
208       this.state_ != SingleGnubbySigner.State.BUSY) {
209     // Open completed after close, perhaps? Ignore.
210     return;
211   }
212
213   switch (rc) {
214     case DeviceStatusCodes.OK_STATUS:
215       if (!gnubby) {
216         console.warn(UTIL_fmt('open succeeded but gnubby is null, WTF?'));
217       } else {
218         this.gnubby_ = gnubby;
219         this.gnubby_.version(this.versionCallback_.bind(this));
220       }
221       break;
222     case DeviceStatusCodes.BUSY_STATUS:
223       this.gnubby_ = gnubby;
224       this.state_ = SingleGnubbySigner.State.BUSY;
225       // If there's still time, retry the open.
226       if (!this.timer_ || !this.timer_.expired()) {
227         var self = this;
228         window.setTimeout(function() {
229           if (self.gnubby_) {
230             DEVICE_FACTORY_REGISTRY.getGnubbyFactory().openGnubby(
231                 self.gnubbyId_,
232                 self.forEnroll_,
233                 self.openCallback_.bind(self),
234                 self.logMsgUrl_);
235           }
236         }, SingleGnubbySigner.OPEN_DELAY_MILLIS);
237       } else {
238         this.goToError_(DeviceStatusCodes.BUSY_STATUS);
239       }
240       break;
241     default:
242       // TODO: This won't be confused with success, but should it be
243       // part of the same namespace as the other error codes, which are
244       // always in DeviceStatusCodes.*?
245       this.goToError_(rc);
246   }
247 };
248
249 /**
250  * Called with the result of a version command.
251  * @param {number} rc Result of version command.
252  * @param {ArrayBuffer=} opt_data Version.
253  * @private
254  */
255 SingleGnubbySigner.prototype.versionCallback_ = function(rc, opt_data) {
256   if (rc) {
257     this.goToError_(rc);
258     return;
259   }
260   this.state_ = SingleGnubbySigner.State.IDLE;
261   this.version_ = UTIL_BytesToString(new Uint8Array(opt_data || []));
262   this.doSign_(this.challengeIndex_);
263 };
264
265 /**
266  * @param {number} challengeIndex Index of challenge to sign
267  * @private
268  */
269 SingleGnubbySigner.prototype.doSign_ = function(challengeIndex) {
270   if (!this.gnubby_) {
271     // Already closed? Nothing to do.
272     return;
273   }
274   if (this.timer_ && this.timer_.expired()) {
275     // If the timer is expired, that means we never got a success response.
276     // We could have gotten wrong data on a partial set of challenges, but this
277     // means we don't yet know the final outcome. In any event, we don't yet
278     // know the final outcome: return timeout.
279     this.goToError_(DeviceStatusCodes.TIMEOUT_STATUS);
280     return;
281   }
282   if (!this.challengesSet_) {
283     this.state_ = SingleGnubbySigner.State.IDLE;
284     return;
285   }
286
287   this.state_ = SingleGnubbySigner.State.SIGNING;
288
289   if (challengeIndex >= this.challenges_.length) {
290     this.signCallback_(challengeIndex, DeviceStatusCodes.WRONG_DATA_STATUS);
291     return;
292   }
293
294   var challenge = this.challenges_[challengeIndex];
295   var challengeHash = challenge.challengeHash;
296   var appIdHash = challenge.appIdHash;
297   var keyHandle = challenge.keyHandle;
298   if (this.notForMe_.indexOf(keyHandle) != -1) {
299     // Cache hit: return wrong data again.
300     this.signCallback_(challengeIndex, DeviceStatusCodes.WRONG_DATA_STATUS);
301   } else if (challenge.version && challenge.version != this.version_) {
302     // Sign challenge for a different version of gnubby: return wrong data.
303     this.signCallback_(challengeIndex, DeviceStatusCodes.WRONG_DATA_STATUS);
304   } else {
305     var nowink = false;
306     this.gnubby_.sign(challengeHash, appIdHash, keyHandle,
307         this.signCallback_.bind(this, challengeIndex),
308         nowink);
309   }
310 };
311
312 /**
313  * Called with the result of a single sign operation.
314  * @param {number} challengeIndex the index of the challenge just attempted
315  * @param {number} code the result of the sign operation
316  * @param {ArrayBuffer=} opt_info Optional result data
317  * @private
318  */
319 SingleGnubbySigner.prototype.signCallback_ =
320     function(challengeIndex, code, opt_info) {
321   console.log(UTIL_fmt('gnubby ' + JSON.stringify(this.gnubbyId_) +
322       ', challenge ' + challengeIndex + ' yielded ' + code.toString(16)));
323   if (this.state_ != SingleGnubbySigner.State.SIGNING) {
324     console.log(UTIL_fmt('already done!'));
325     // We're done, the caller's no longer interested.
326     return;
327   }
328
329   // Cache wrong data result, re-asking the gnubby to sign it won't produce
330   // different results.
331   if (code == DeviceStatusCodes.WRONG_DATA_STATUS) {
332     if (challengeIndex < this.challenges_.length) {
333       var challenge = this.challenges_[challengeIndex];
334       if (this.notForMe_.indexOf(challenge.keyHandle) == -1) {
335         this.notForMe_.push(challenge.keyHandle);
336       }
337     }
338   }
339
340   var self = this;
341   switch (code) {
342     case DeviceStatusCodes.GONE_STATUS:
343       this.goToError_(code);
344       break;
345
346     case DeviceStatusCodes.TIMEOUT_STATUS:
347       // TODO: On a TIMEOUT_STATUS, sync first, then retry.
348     case DeviceStatusCodes.BUSY_STATUS:
349       this.doSign_(this.challengeIndex_);
350       break;
351
352     case DeviceStatusCodes.OK_STATUS:
353       if (this.forEnroll_) {
354         this.goToError_(code);
355       } else {
356         this.goToSuccess_(code, this.challenges_[challengeIndex], opt_info);
357       }
358       break;
359
360     case DeviceStatusCodes.WAIT_TOUCH_STATUS:
361       window.setTimeout(function() {
362         self.doSign_(self.challengeIndex_);
363       }, SingleGnubbySigner.SIGN_DELAY_MILLIS);
364       break;
365
366     case DeviceStatusCodes.WRONG_DATA_STATUS:
367       if (this.challengeIndex_ < this.challenges_.length - 1) {
368         this.doSign_(++this.challengeIndex_);
369       } else if (this.forEnroll_) {
370         this.goToSuccess_(code);
371       } else {
372         this.goToError_(code);
373       }
374       break;
375
376     default:
377       if (this.forEnroll_) {
378         this.goToError_(code);
379       } else if (this.challengeIndex_ < this.challenges_.length - 1) {
380         this.doSign_(++this.challengeIndex_);
381       } else {
382         this.goToError_(code);
383       }
384   }
385 };
386
387 /**
388  * Switches to the error state, and notifies caller.
389  * @param {number} code Error code
390  * @private
391  */
392 SingleGnubbySigner.prototype.goToError_ = function(code) {
393   this.state_ = SingleGnubbySigner.State.COMPLETE;
394   console.log(UTIL_fmt('failed (' + code.toString(16) + ')'));
395   // Since this gnubby can no longer produce a useful result, go ahead and
396   // close it.
397   this.close();
398   var result = { code: code };
399   this.completeCb_(result);
400 };
401
402 /**
403  * Switches to the success state, and notifies caller.
404  * @param {number} code Status code
405  * @param {SignHelperChallenge=} opt_challenge The challenge signed
406  * @param {ArrayBuffer=} opt_info Optional result data
407  * @private
408  */
409 SingleGnubbySigner.prototype.goToSuccess_ =
410     function(code, opt_challenge, opt_info) {
411   this.state_ = SingleGnubbySigner.State.COMPLETE;
412   console.log(UTIL_fmt('success (' + code.toString(16) + ')'));
413   var result = { code: code, gnubby: this.gnubby_ };
414   if (opt_challenge || opt_info) {
415     if (opt_challenge) {
416       result['challenge'] = opt_challenge;
417     }
418     if (opt_info) {
419       result['info'] = opt_info;
420     }
421   }
422   this.completeCb_(result);
423   // this.gnubby_ is now owned by completeCb_.
424   this.gnubby_ = null;
425 };