Chromium Code Reviews
chromiumcodereview-hr@appspot.gserviceaccount.com (chromiumcodereview-hr) | Please choose your nickname with Settings | Help | Chromium Project | Gerrit Changes | Sign out
(105)

Side by Side Diff: chrome/browser/resources/cryptotoken/singlesigner.js

Issue 249913002: FIDO U2F component extension (Closed) Base URL: https://chromium.googlesource.com/chromium/src.git@master
Patch Set: Merge with HEAD Created 6 years, 7 months ago
Use n/p to move between diff chunks; N/P to move between comments. Draft comments are only viewable by you.
Jump to:
View unified diff | Download patch
OLDNEW
(Empty)
1 // Copyright (c) 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 * @author juanlang@google.com (Juan Lang)
11 */
12
13 'use strict';
14
15 /**
16 * Creates a new sign handler with a gnubby. This handler will perform a sign
17 * operation using each challenge in an array of challenges until its success
18 * condition is satisified, or an error or timeout occurs. The success condition
19 * is defined differently depending whether this signer is used for enrolling
20 * or for signing:
21 *
22 * For enroll, success is defined as each challenge yielding wrong data. This
23 * means this gnubby is not currently enrolled for any of the appIds in any
24 * challenge.
25 *
26 * For sign, success is defined as any challenge yielding ok or waiting for
27 * touch.
28 *
29 * At most one of the success or failure callbacks will be called, and it will
30 * be called at most once. Neither callback is guaranteed to be called: if
31 * a final set of challenges is never given to this gnubby, or if the gnubby
32 * stays busy, the signer has no way to know whether the set of challenges it's
33 * been given has succeeded or failed.
34 * The callback is called only when the signer reaches success or failure, i.e.
35 * when there is no need for this signer to continue trying new challenges.
36 *
37 * @param {!GnubbyFactory} factory Used to create and open the gnubby.
38 * @param {llGnubbyDeviceId} gnubbyIndex Which gnubby to open.
39 * @param {boolean} forEnroll Whether this signer is signing for an attempted
40 * enroll operation.
41 * @param {function(number)} errorCb Called when this signer fails, i.e. no
42 * further attempts can succeed.
43 * @param {function(usbGnubby, number, (SingleSignerResult|undefined))}
44 * successCb Called when this signer succeeds.
45 * @param {Countdown=} opt_timer An advisory timer, beyond whose expiration the
46 * signer will not attempt any new operations, assuming the caller is no
47 * longer interested in the outcome.
48 * @param {string=} opt_logMsgUrl A URL to post log messages to.
49 * @constructor
50 */
51 function SingleGnubbySigner(factory, gnubbyIndex, forEnroll, errorCb, successCb,
52 opt_timer, opt_logMsgUrl) {
53 /** @private {GnubbyFactory} */
54 this.factory_ = factory;
55 /** @private {llGnubbyDeviceId} */
56 this.gnubbyIndex_ = gnubbyIndex;
57 /** @private {SingleGnubbySigner.State} */
58 this.state_ = SingleGnubbySigner.State.INIT;
59 /** @private {boolean} */
60 this.forEnroll_ = forEnroll;
61 /** @private {function(number)} */
62 this.errorCb_ = errorCb;
63 /** @private {function(usbGnubby, number, (SingleSignerResult|undefined))} */
64 this.successCb_ = successCb;
65 /** @private {Countdown|undefined} */
66 this.timer_ = opt_timer;
67 /** @private {string|undefined} */
68 this.logMsgUrl_ = opt_logMsgUrl;
69
70 /** @private {!Array.<!SignHelperChallenge>} */
71 this.challenges_ = [];
72 /** @private {number} */
73 this.challengeIndex_ = 0;
74 /** @private {boolean} */
75 this.challengesFinal_ = false;
76
77 /** @private {!Array.<string>} */
78 this.notForMe_ = [];
79 }
80
81 /** @enum {number} */
82 SingleGnubbySigner.State = {
83 /** Initial state. */
84 INIT: 0,
85 /** The signer is attempting to open a gnubby. */
86 OPENING: 1,
87 /** The signer's gnubby opened, but is busy. */
88 BUSY: 2,
89 /** The signer has an open gnubby, but no challenges to sign. */
90 IDLE: 3,
91 /** The signer is currently signing a challenge. */
92 SIGNING: 4,
93 /** The signer encountered an error. */
94 ERROR: 5,
95 /** The signer got a successful outcome. */
96 SUCCESS: 6,
97 /** The signer is closing its gnubby. */
98 CLOSING: 7,
99 /** The signer is closed. */
100 CLOSED: 8
101 };
102
103 /**
104 * Attempts to open this signer's gnubby, if it's not already open.
105 * (This is implicitly done by addChallenges.)
106 */
107 SingleGnubbySigner.prototype.open = function() {
108 if (this.state_ == SingleGnubbySigner.State.INIT) {
109 this.state_ = SingleGnubbySigner.State.OPENING;
110 this.factory_.openGnubby(this.gnubbyIndex_,
111 this.forEnroll_,
112 this.openCallback_.bind(this),
113 this.logMsgUrl_);
114 }
115 };
116
117 /**
118 * Closes this signer's gnubby, if it's held.
119 */
120 SingleGnubbySigner.prototype.close = function() {
121 if (!this.gnubby_) return;
122 this.state_ = SingleGnubbySigner.State.CLOSING;
123 this.gnubby_.closeWhenIdle(this.closed_.bind(this));
124 };
125
126 /**
127 * Called when this signer's gnubby is closed.
128 * @private
129 */
130 SingleGnubbySigner.prototype.closed_ = function() {
131 this.gnubby_ = null;
132 this.state_ = SingleGnubbySigner.State.CLOSED;
133 };
134
135 /**
136 * Adds challenges to the set of challenges being tried by this signer.
137 * If the signer is currently idle, begins signing the new challenges.
138 *
139 * @param {Array.<SignHelperChallenge>} challenges
140 * @param {boolean} finalChallenges
141 * @return {boolean} Whether the challenges were accepted.
142 */
143 SingleGnubbySigner.prototype.addChallenges =
144 function(challenges, finalChallenges) {
145 if (this.challengesFinal_) {
146 // Can't add new challenges once they're finalized.
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.challengesFinal_ = finalChallenges;
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 added challenges, but
165 // don't do anything.
166 break;
167 case SingleGnubbySigner.State.IDLE:
168 if (this.challengeIndex_ < challenges.length) {
169 // New challenges added: restart signing.
170 this.doSign_(this.challengeIndex_);
171 } else if (finalChallenges) {
172 // Finalized with no new challenges can happen when the caller rejects
173 // the appId for some challenge.
174 // If this signer is for enroll, the request must be rejected: this
175 // signer can't determine whether the gnubby is or is not enrolled for
176 // the origin.
177 // If this signer is for sign, the request must also be rejected: there
178 // are no new challenges to sign, and all previous ones did not yield
179 // success.
180 var self = this;
181 window.setTimeout(function() {
182 self.goToError_(DeviceStatusCodes.WRONG_DATA_STATUS);
183 }, 0);
184 }
185 break;
186 case SingleGnubbySigner.State.SIGNING:
187 // Already signing, so don't kick off a new sign, but accept the added
188 // challenges.
189 break;
190 default:
191 return false;
192 }
193 return true;
194 };
195
196 /**
197 * How long to delay retrying a failed open.
198 */
199 SingleGnubbySigner.OPEN_DELAY_MILLIS = 200;
200
201 /**
202 * @param {number} rc The result of the open operation.
203 * @param {usbGnubby=} gnubby The opened gnubby, if open was successful (or
204 * busy).
205 * @private
206 */
207 SingleGnubbySigner.prototype.openCallback_ = function(rc, gnubby) {
208 if (this.state_ != SingleGnubbySigner.State.OPENING &&
209 this.state_ != SingleGnubbySigner.State.BUSY) {
210 // Open completed after close, perhaps? Ignore.
211 return;
212 }
213
214 switch (rc) {
215 case DeviceStatusCodes.OK_STATUS:
216 if (!gnubby) {
217 console.warn(UTIL_fmt('open succeeded but gnubby is null, WTF?'));
218 } else {
219 this.gnubby_ = gnubby;
220 this.gnubby_.version(this.versionCallback_.bind(this));
221 }
222 break;
223 case DeviceStatusCodes.BUSY_STATUS:
224 this.gnubby_ = gnubby;
225 this.openedBusy_ = true;
226 this.state_ = SingleGnubbySigner.State.BUSY;
227 // If there's still time, retry the open.
228 if (!this.timer_ || !this.timer_.expired()) {
229 var self = this;
230 window.setTimeout(function() {
231 if (self.gnubby_) {
232 self.factory_.openGnubby(self.gnubbyIndex_,
233 self.forEnroll_,
234 self.openCallback_.bind(self),
235 self.logMsgUrl_);
236 }
237 }, SingleGnubbySigner.OPEN_DELAY_MILLIS);
238 } else {
239 this.goToError_(DeviceStatusCodes.BUSY_STATUS);
240 }
241 break;
242 default:
243 // TODO(juanlang): This won't be confused with success, but should it be
244 // part of the same namespace as the other error codes, which are
245 // always in DeviceStatusCodes.*?
246 this.goToError_(rc);
247 }
248 };
249
250 /**
251 * Called with the result of a version command.
252 * @param {number} rc Result of version command.
253 * @param {ArrayBuffer=} opt_data Version.
254 * @private
255 */
256 SingleGnubbySigner.prototype.versionCallback_ = function(rc, opt_data) {
257 if (rc) {
258 this.goToError_(rc);
259 return;
260 }
261 this.state_ = SingleGnubbySigner.State.IDLE;
262 this.version_ = UTIL_BytesToString(new Uint8Array(opt_data || []));
263 this.doSign_(this.challengeIndex_);
264 };
265
266 /**
267 * @param {number} challengeIndex
268 * @private
269 */
270 SingleGnubbySigner.prototype.doSign_ = function(challengeIndex) {
271 if (this.timer_ && this.timer_.expired()) {
272 // If the timer is expired, that means we never got a success or a touch
273 // required response: either always implies completion of this signer's
274 // state machine (see signCallback's cases for OK_STATUS and
275 // WAIT_TOUCH_STATUS.) We could have gotten wrong data on a partial set of
276 // challenges, but this means we don't yet know the final outcome. In any
277 // event, we don't yet know the final outcome: return busy.
278 this.goToError_(DeviceStatusCodes.BUSY_STATUS);
279 return;
280 }
281
282 this.state_ = SingleGnubbySigner.State.SIGNING;
283
284 if (challengeIndex >= this.challenges_.length) {
285 this.signCallback_(challengeIndex, DeviceStatusCodes.WRONG_DATA_STATUS);
286 return;
287 }
288
289 var challenge = this.challenges_[challengeIndex];
290 var challengeHash = challenge.challengeHash;
291 var appIdHash = challenge.appIdHash;
292 var keyHandle = challenge.keyHandle;
293 if (this.notForMe_.indexOf(keyHandle) != -1) {
294 // Cache hit: return wrong data again.
295 this.signCallback_(challengeIndex, DeviceStatusCodes.WRONG_DATA_STATUS);
296 } else if (challenge.version && challenge.version != this.version_) {
297 // Sign challenge for a different version of gnubby: return wrong data.
298 this.signCallback_(challengeIndex, DeviceStatusCodes.WRONG_DATA_STATUS);
299 } else {
300 var opt_nowink = this.forEnroll_;
301 this.gnubby_.sign(challengeHash, appIdHash, keyHandle,
302 this.signCallback_.bind(this, challengeIndex),
303 opt_nowink);
304 }
305 };
306
307 /**
308 * Called with the result of a single sign operation.
309 * @param {number} challengeIndex the index of the challenge just attempted
310 * @param {number} code the result of the sign operation
311 * @param {ArrayBuffer=} opt_info
312 * @private
313 */
314 SingleGnubbySigner.prototype.signCallback_ =
315 function(challengeIndex, code, opt_info) {
316 console.log(UTIL_fmt('gnubby ' + JSON.stringify(this.gnubbyIndex_) +
317 ', challenge ' + challengeIndex + ' yielded ' + code.toString(16)));
318 if (this.state_ != SingleGnubbySigner.State.SIGNING) {
319 console.log(UTIL_fmt('already done!'));
320 // We're done, the caller's no longer interested.
321 return;
322 }
323
324 // Cache wrong data result, re-asking the gnubby to sign it won't produce
325 // different results.
326 if (code == DeviceStatusCodes.WRONG_DATA_STATUS) {
327 if (challengeIndex < this.challenges_.length) {
328 var challenge = this.challenges_[challengeIndex];
329 if (this.notForMe_.indexOf(challenge.keyHandle) == -1) {
330 this.notForMe_.push(challenge.keyHandle);
331 }
332 }
333 }
334
335 switch (code) {
336 case DeviceStatusCodes.GONE_STATUS:
337 this.goToError_(code);
338 break;
339
340 case DeviceStatusCodes.TIMEOUT_STATUS:
341 // TODO(juanlang): On a TIMEOUT_STATUS, sync first, then retry.
342 case DeviceStatusCodes.BUSY_STATUS:
343 this.doSign_(this.challengeIndex_);
344 break;
345
346 case DeviceStatusCodes.OK_STATUS:
347 if (this.forEnroll_) {
348 this.goToError_(code);
349 } else {
350 this.goToSuccess_(code, this.challenges_[challengeIndex], opt_info);
351 }
352 break;
353
354 case DeviceStatusCodes.WAIT_TOUCH_STATUS:
355 if (this.forEnroll_) {
356 this.goToError_(code);
357 } else {
358 this.goToSuccess_(code, this.challenges_[challengeIndex]);
359 }
360 break;
361
362 case DeviceStatusCodes.WRONG_DATA_STATUS:
363 if (this.challengeIndex_ < this.challenges_.length - 1) {
364 this.doSign_(++this.challengeIndex_);
365 } else if (!this.challengesFinal_) {
366 this.state_ = SingleGnubbySigner.State.IDLE;
367 } else if (this.forEnroll_) {
368 // Signal the caller whether the open was busy, because it may take
369 // an unusually long time when opened for enroll. Use an empty
370 // "challenge" as the signal for a busy open.
371 var challenge = undefined;
372 if (this.openedBusy) {
373 challenge = { appIdHash: '', challengeHash: '', keyHandle: '' };
374 }
375 this.goToSuccess_(code, challenge);
376 } else {
377 this.goToError_(code);
378 }
379 break;
380
381 default:
382 if (this.forEnroll_) {
383 this.goToError_(code);
384 } else if (this.challengeIndex_ < this.challenges_.length - 1) {
385 this.doSign_(++this.challengeIndex_);
386 } else if (!this.challengesFinal_) {
387 // Increment the challenge index, as this one isn't useful any longer,
388 // but a subsequent challenge may appear, and it might be useful.
389 this.challengeIndex_++;
390 this.state_ = SingleGnubbySigner.State.IDLE;
391 } else {
392 this.goToError_(code);
393 }
394 }
395 };
396
397 /**
398 * Switches to the error state, and notifies caller.
399 * @param {number} code
400 * @private
401 */
402 SingleGnubbySigner.prototype.goToError_ = function(code) {
403 this.state_ = SingleGnubbySigner.State.ERROR;
404 console.log(UTIL_fmt('failed (' + code.toString(16) + ')'));
405 this.errorCb_(code);
406 // Since this gnubby can no longer produce a useful result, go ahead and
407 // close it.
408 this.close();
409 };
410
411 /**
412 * Switches to the success state, and notifies caller.
413 * @param {number} code
414 * @param {SignHelperChallenge=} opt_challenge
415 * @param {ArrayBuffer=} opt_info
416 * @private
417 */
418 SingleGnubbySigner.prototype.goToSuccess_ =
419 function(code, opt_challenge, opt_info) {
420 this.state_ = SingleGnubbySigner.State.SUCCESS;
421 console.log(UTIL_fmt('success (' + code.toString(16) + ')'));
422 if (opt_challenge || opt_info) {
423 var singleSignerResult = {};
424 if (opt_challenge) {
425 singleSignerResult['challenge'] = opt_challenge;
426 }
427 if (opt_info) {
428 singleSignerResult['info'] = opt_info;
429 }
430 }
431 this.successCb_(this.gnubby_, code, singleSignerResult);
432 // this.gnubby_ is now owned by successCb.
433 this.gnubby_ = null;
434 };
OLDNEW
« no previous file with comments | « chrome/browser/resources/cryptotoken/signhelper.js ('k') | chrome/browser/resources/cryptotoken/usbenrollhelper.js » ('j') | no next file with comments »

Powered by Google App Engine
This is Rietveld 408576698