Roll src/third_party/WebKit bf18a82:a9cee16 (svn 185297:185304)
[chromium-blink-merge.git] / chrome / browser / resources / cryptotoken / singlesigner.js
blob5538ff7bc70ec8415c46a85d186d832c5229995b
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.
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.
12 'use strict';
14 /**
15 * @typedef {{
16 * code: number,
17 * gnubby: (Gnubby|undefined),
18 * challenge: (SignHelperChallenge|undefined),
19 * info: (ArrayBuffer|undefined)
20 * }}
22 var SingleSignerResult;
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:
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.
35 * For sign, success is defined as any challenge yielding ok.
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.
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
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;
68 /** @private {!Array.<!SignHelperChallenge>} */
69 this.challenges_ = [];
70 /** @private {number} */
71 this.challengeIndex_ = 0;
72 /** @private {boolean} */
73 this.challengesSet_ = false;
75 /** @private {!Object.<string, number>} */
76 this.cachedError_ = [];
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
99 /**
100 * @return {GnubbyDeviceId} This device id of the gnubby for this signer.
102 SingleGnubbySigner.prototype.getDeviceId = function() {
103 return this.gnubbyId_;
107 * Attempts to open this signer's gnubby, if it's not already open.
108 * (This is implicitly done by addChallenges.)
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_);
122 * Closes this signer's gnubby, if it's held.
124 SingleGnubbySigner.prototype.close = function() {
125 if (!this.gnubby_) return;
126 this.state_ = SingleGnubbySigner.State.CLOSING;
127 this.gnubby_.closeWhenIdle(this.closed_.bind(this));
131 * Called when this signer's gnubby is closed.
132 * @private
134 SingleGnubbySigner.prototype.closed_ = function() {
135 this.gnubby_ = null;
136 this.state_ = SingleGnubbySigner.State.CLOSED;
140 * Begins signing the given challenges.
141 * @param {Array.<SignHelperChallenge>} challenges The challenges to sign.
142 * @return {boolean} Whether the challenges were accepted.
144 SingleGnubbySigner.prototype.doSign = function(challenges) {
145 if (this.challengesSet_) {
146 // Can't add new challenges once they've been set.
147 return false;
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]);
157 this.challengesSet_ = true;
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);
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;
188 return true;
192 * How long to delay retrying a failed open.
194 SingleGnubbySigner.OPEN_DELAY_MILLIS = 200;
197 * How long to delay retrying a sign requiring touch.
199 SingleGnubbySigner.SIGN_DELAY_MILLIS = 200;
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
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;
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));
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_);
236 }, SingleGnubbySigner.OPEN_DELAY_MILLIS);
237 } else {
238 this.goToError_(DeviceStatusCodes.BUSY_STATUS);
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, true);
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
255 SingleGnubbySigner.prototype.versionCallback_ = function(rc, opt_data) {
256 if (rc) {
257 this.goToError_(rc, true);
258 return;
260 this.state_ = SingleGnubbySigner.State.IDLE;
261 this.version_ = UTIL_BytesToString(new Uint8Array(opt_data || []));
262 this.doSign_(this.challengeIndex_);
266 * @param {number} challengeIndex Index of challenge to sign
267 * @private
269 SingleGnubbySigner.prototype.doSign_ = function(challengeIndex) {
270 if (!this.gnubby_) {
271 // Already closed? Nothing to do.
272 return;
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;
282 if (!this.challengesSet_) {
283 this.state_ = SingleGnubbySigner.State.IDLE;
284 return;
287 this.state_ = SingleGnubbySigner.State.SIGNING;
289 if (challengeIndex >= this.challenges_.length) {
290 this.signCallback_(challengeIndex, DeviceStatusCodes.WRONG_DATA_STATUS);
291 return;
294 var challenge = this.challenges_[challengeIndex];
295 var challengeHash = challenge.challengeHash;
296 var appIdHash = challenge.appIdHash;
297 var keyHandle = challenge.keyHandle;
298 if (this.cachedError_.hasOwnProperty(keyHandle)) {
299 // Cache hit: return wrong data again.
300 this.signCallback_(challengeIndex, this.cachedError_[keyHandle]);
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);
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
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;
329 // Cache wrong data or wrong length results, re-asking the gnubby to sign it
330 // won't produce different results.
331 if (code == DeviceStatusCodes.WRONG_DATA_STATUS ||
332 code == DeviceStatusCodes.WRONG_LENGTH_STATUS) {
333 if (challengeIndex < this.challenges_.length) {
334 var challenge = this.challenges_[challengeIndex];
335 if (!this.cachedError_.hasOwnProperty(challenge.keyHandle)) {
336 this.cachedError_[challenge.keyHandle] = code;
341 var self = this;
342 switch (code) {
343 case DeviceStatusCodes.GONE_STATUS:
344 this.goToError_(code);
345 break;
347 case DeviceStatusCodes.TIMEOUT_STATUS:
348 // TODO: On a TIMEOUT_STATUS, sync first, then retry.
349 case DeviceStatusCodes.BUSY_STATUS:
350 this.doSign_(this.challengeIndex_);
351 break;
353 case DeviceStatusCodes.OK_STATUS:
354 if (this.forEnroll_) {
355 this.goToError_(code);
356 } else {
357 this.goToSuccess_(code, this.challenges_[challengeIndex], opt_info);
359 break;
361 case DeviceStatusCodes.WAIT_TOUCH_STATUS:
362 window.setTimeout(function() {
363 self.doSign_(self.challengeIndex_);
364 }, SingleGnubbySigner.SIGN_DELAY_MILLIS);
365 break;
367 case DeviceStatusCodes.WRONG_DATA_STATUS:
368 case DeviceStatusCodes.WRONG_LENGTH_STATUS:
369 if (this.challengeIndex_ < this.challenges_.length - 1) {
370 this.doSign_(++this.challengeIndex_);
371 } else if (this.forEnroll_) {
372 this.goToSuccess_(code);
373 } else {
374 this.goToError_(code);
376 break;
378 default:
379 if (this.forEnroll_) {
380 this.goToError_(code, true);
381 } else if (this.challengeIndex_ < this.challenges_.length - 1) {
382 this.doSign_(++this.challengeIndex_);
383 } else {
384 this.goToError_(code, true);
390 * Switches to the error state, and notifies caller.
391 * @param {number} code Error code
392 * @param {boolean=} opt_warn Whether to warn in the console about the error.
393 * @private
395 SingleGnubbySigner.prototype.goToError_ = function(code, opt_warn) {
396 this.state_ = SingleGnubbySigner.State.COMPLETE;
397 var logFn = opt_warn ? console.warn.bind(console) : console.log.bind(console);
398 logFn(UTIL_fmt('failed (' + code.toString(16) + ')'));
399 var result = { code: code };
400 if (!this.forEnroll_ && code == DeviceStatusCodes.WRONG_DATA_STATUS) {
401 // When a device yields WRONG_DATA to all sign challenges, and this is a
402 // sign request, we don't want to yield to the web page that it's not
403 // enrolled just yet: we want the user to tap the device first. We'll
404 // report the gnubby to the caller and let it close it instead of closing
405 // it here.
406 result.gnubby = this.gnubby_;
407 } else {
408 // Since this gnubby can no longer produce a useful result, go ahead and
409 // close it.
410 this.close();
412 this.completeCb_(result);
416 * Switches to the success state, and notifies caller.
417 * @param {number} code Status code
418 * @param {SignHelperChallenge=} opt_challenge The challenge signed
419 * @param {ArrayBuffer=} opt_info Optional result data
420 * @private
422 SingleGnubbySigner.prototype.goToSuccess_ =
423 function(code, opt_challenge, opt_info) {
424 this.state_ = SingleGnubbySigner.State.COMPLETE;
425 console.log(UTIL_fmt('success (' + code.toString(16) + ')'));
426 var result = { code: code, gnubby: this.gnubby_ };
427 if (opt_challenge || opt_info) {
428 if (opt_challenge) {
429 result['challenge'] = opt_challenge;
431 if (opt_info) {
432 result['info'] = opt_info;
435 this.completeCb_(result);
436 // this.gnubby_ is now owned by completeCb_.
437 this.gnubby_ = null;