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

Side by Side Diff: chrome/browser/resources/cryptotoken/signer.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 Handles web page requests for gnubby sign requests.
7 *
8 * @author juanlang@google.com (Juan Lang)
9 */
10
11 'use strict';
12
13 var signRequestQueue = new OriginKeyedRequestQueue();
14
15 /**
16 * Handles a sign request.
17 * @param {!SignHelperFactory} factory Factory to create a sign helper.
18 * @param {MessageSender} sender The sender of the message.
19 * @param {Object} request The web page's sign request.
20 * @param {boolean} enforceAppIdValid Whether to enforce that the app id in the
21 * request matches the sender's origin.
22 * @param {Function} sendResponse Called back with the result of the sign.
23 * @param {boolean} toleratesMultipleResponses Whether the sendResponse
24 * callback can be called more than once, e.g. for progress updates.
25 * @return {Closeable}
26 */
27 function handleSignRequest(factory, sender, request, enforceAppIdValid,
28 sendResponse, toleratesMultipleResponses) {
29 var sentResponse = false;
30 function sendResponseOnce(r) {
31 if (queuedSignRequest) {
32 queuedSignRequest.close();
33 queuedSignRequest = null;
34 }
35 if (!sentResponse) {
36 sentResponse = true;
37 try {
38 // If the page has gone away or the connection has otherwise gone,
39 // sendResponse fails.
40 sendResponse(r);
41 } catch (exception) {
42 console.warn('sendResponse failed: ' + exception);
43 }
44 } else {
45 console.warn(UTIL_fmt('Tried to reply more than once! Juan, FIX ME'));
46 }
47 }
48
49 function sendErrorResponse(code) {
50 var response = formatWebPageResponse(GnubbyMsgTypes.SIGN_WEB_REPLY, code);
51 sendResponseOnce(response);
52 }
53
54 function sendSuccessResponse(challenge, info, browserData) {
55 var responseData = {};
56 for (var k in challenge) {
57 responseData[k] = challenge[k];
58 }
59 responseData['browserData'] = B64_encode(UTIL_StringToBytes(browserData));
60 responseData['signatureData'] = info;
61 var response = formatWebPageResponse(GnubbyMsgTypes.SIGN_WEB_REPLY,
62 GnubbyCodeTypes.OK, responseData);
63 sendResponseOnce(response);
64 }
65
66 function sendNotification(code) {
67 console.log(UTIL_fmt('notification, code=' + code));
68 // Can the callback handle progress updates? If so, send one.
69 if (toleratesMultipleResponses) {
70 var response = formatWebPageResponse(
71 GnubbyMsgTypes.SIGN_WEB_NOTIFICATION, code);
72 if (request['requestId']) {
73 response['requestId'] = request['requestId'];
74 }
75 sendResponse(response);
76 }
77 }
78
79 var origin = getOriginFromUrl(/** @type {string} */ (sender.url));
80 if (!origin) {
81 sendErrorResponse(GnubbyCodeTypes.BAD_REQUEST);
82 return null;
83 }
84 // More closure type inference fail.
85 var nonNullOrigin = /** @type {string} */ (origin);
86
87 if (!isValidSignRequest(request)) {
88 sendErrorResponse(GnubbyCodeTypes.BAD_REQUEST);
89 return null;
90 }
91
92 var signData = request['signData'];
93 // A valid sign data has at least one challenge, so get the first appId from
94 // the first challenge.
95 var firstAppId = signData[0]['appId'];
96 var timeoutMillis = Signer.DEFAULT_TIMEOUT_MILLIS;
97 if (request['timeout']) {
98 // Request timeout is in seconds.
99 timeoutMillis = request['timeout'] * 1000;
100 }
101 var timer = new CountdownTimer(timeoutMillis);
102 var logMsgUrl = request['logMsgUrl'];
103
104 // Queue sign requests from the same origin, to protect against simultaneous
105 // sign-out on many tabs resulting in repeated sign-in requests.
106 var queuedSignRequest = new QueuedSignRequest(signData, factory, timer,
107 nonNullOrigin, enforceAppIdValid, sendErrorResponse, sendSuccessResponse,
108 sendNotification, sender.tlsChannelId, logMsgUrl);
109 var requestToken = signRequestQueue.queueRequest(firstAppId, nonNullOrigin,
110 queuedSignRequest.begin.bind(queuedSignRequest), timer);
111 queuedSignRequest.setToken(requestToken);
112 return queuedSignRequest;
113 }
114
115 /**
116 * Returns whether the request appears to be a valid sign request.
117 * @param {Object} request the request.
118 * @return {boolean} whether the request appears valid.
119 */
120 function isValidSignRequest(request) {
121 if (!request.hasOwnProperty('signData'))
122 return false;
123 var signData = request['signData'];
124 // If a sign request contains an empty array of challenges, it could never
125 // be fulfilled. Fail.
126 if (!signData.length)
127 return false;
128 return isValidSignData(signData);
129 }
130
131 /**
132 * Adapter class representing a queued sign request.
133 * @param {!SignData} signData
134 * @param {!SignHelperFactory} factory
135 * @param {Countdown} timer
136 * @param {string} origin
137 * @param {boolean} enforceAppIdValid
138 * @param {function(number)} errorCb
139 * @param {function(SignChallenge, string, string)} successCb
140 * @param {(function(number)|undefined)} opt_progressCb
141 * @param {string|undefined} opt_tlsChannelId
142 * @param {string|undefined} opt_logMsgUrl
143 * @constructor
144 * @implements {Closeable}
145 */
146 function QueuedSignRequest(signData, factory, timer, origin, enforceAppIdValid,
147 errorCb, successCb, opt_progressCb, opt_tlsChannelId, opt_logMsgUrl) {
148 /** @private {!SignData} */
149 this.signData_ = signData;
150 /** @private {!SignHelperFactory} */
151 this.factory_ = factory;
152 /** @private {Countdown} */
153 this.timer_ = timer;
154 /** @private {string} */
155 this.origin_ = origin;
156 /** @private {boolean} */
157 this.enforceAppIdValid_ = enforceAppIdValid;
158 /** @private {function(number)} */
159 this.errorCb_ = errorCb;
160 /** @private {function(SignChallenge, string, string)} */
161 this.successCb_ = successCb;
162 /** @private {(function(number)|undefined)} */
163 this.progressCb_ = opt_progressCb;
164 /** @private {string|undefined} */
165 this.tlsChannelId_ = opt_tlsChannelId;
166 /** @private {string|undefined} */
167 this.logMsgUrl_ = opt_logMsgUrl;
168 /** @private {boolean} */
169 this.begun_ = false;
170 /** @private {boolean} */
171 this.closed_ = false;
172 }
173
174 /** Closes this sign request. */
175 QueuedSignRequest.prototype.close = function() {
176 if (this.closed_) return;
177 if (this.begun_ && this.signer_) {
178 this.signer_.close();
179 }
180 if (this.token_) {
181 this.token_.complete();
182 }
183 this.closed_ = true;
184 };
185
186 /**
187 * @param {QueuedRequestToken} token Token for this sign request.
188 */
189 QueuedSignRequest.prototype.setToken = function(token) {
190 /** @private {QueuedRequestToken} */
191 this.token_ = token;
192 };
193
194 /**
195 * Called when this sign request may begin work.
196 * @param {QueuedRequestToken} token Token for this sign request.
197 */
198 QueuedSignRequest.prototype.begin = function(token) {
199 this.begun_ = true;
200 this.setToken(token);
201 this.signer_ = new Signer(this.factory_, this.timer_, this.origin_,
202 this.enforceAppIdValid_, this.signerFailed_.bind(this),
203 this.signerSucceeded_.bind(this), this.progressCb_,
204 this.tlsChannelId_, this.logMsgUrl_);
205 if (!this.signer_.setChallenges(this.signData_)) {
206 token.complete();
207 this.errorCb_(GnubbyCodeTypes.BAD_REQUEST);
208 }
209 };
210
211 /**
212 * Called when this request's signer fails.
213 * @param {number} code The failure code reported by the signer.
214 * @private
215 */
216 QueuedSignRequest.prototype.signerFailed_ = function(code) {
217 this.token_.complete();
218 this.errorCb_(code);
219 };
220
221 /**
222 * Called when this request's signer succeeds.
223 * @param {SignChallenge} challenge The challenge that was signed.
224 * @param {string} info The sign result.
225 * @param {string} browserData
226 * @private
227 */
228 QueuedSignRequest.prototype.signerSucceeded_ =
229 function(challenge, info, browserData) {
230 this.token_.complete();
231 this.successCb_(challenge, info, browserData);
232 };
233
234 /**
235 * Creates an object to track signing with a gnubby.
236 * @param {!SignHelperFactory} helperFactory Factory to create a sign helper.
237 * @param {Countdown} timer Timer for sign request.
238 * @param {string} origin The origin making the request.
239 * @param {boolean} enforceAppIdValid Whether to enforce that the appId in the
240 * request matches the sender's origin.
241 * @param {function(number)} errorCb Called when the sign operation fails.
242 * @param {function(SignChallenge, string, string)} successCb Called when the
243 * sign operation succeeds.
244 * @param {(function(number)|undefined)} opt_progressCb Called with progress
245 * updates to the sign request.
246 * @param {string=} opt_tlsChannelId the TLS channel ID, if any, of the origin
247 * making the request.
248 * @param {string=} opt_logMsgUrl The url to post log messages to.
249 * @constructor
250 */
251 function Signer(helperFactory, timer, origin, enforceAppIdValid,
252 errorCb, successCb, opt_progressCb, opt_tlsChannelId, opt_logMsgUrl) {
253 /** @private {Countdown} */
254 this.timer_ = timer;
255 /** @private {string} */
256 this.origin_ = origin;
257 /** @private {boolean} */
258 this.enforceAppIdValid_ = enforceAppIdValid;
259 /** @private {function(number)} */
260 this.errorCb_ = errorCb;
261 /** @private {function(SignChallenge, string, string)} */
262 this.successCb_ = successCb;
263 /** @private {(function(number)|undefined)} */
264 this.progressCb_ = opt_progressCb;
265 /** @private {string|undefined} */
266 this.tlsChannelId_ = opt_tlsChannelId;
267 /** @private {string|undefined} */
268 this.logMsgUrl_ = opt_logMsgUrl;
269
270 /** @private {boolean} */
271 this.challengesSet_ = false;
272 /** @private {Array.<SignHelperChallenge>} */
273 this.pendingChallenges_ = [];
274 /** @private {boolean} */
275 this.done_ = false;
276
277 /** @private {Object.<string, string>} */
278 this.browserData_ = {};
279 /** @private {Object.<string, SignChallenge>} */
280 this.serverChallenges_ = {};
281 // Allow http appIds for http origins. (Broken, but the caller deserves
282 // what they get.)
283 /** @private {boolean} */
284 this.allowHttp_ = this.origin_ ? this.origin_.indexOf('http://') == 0 : false;
285
286 // Protect against helper failure with a watchdog.
287 this.createWatchdog_(timer);
288 /** @private {SignHelper} */
289 this.helper_ = helperFactory.createHelper(
290 timer, this.helperError_.bind(this), this.helperSuccess_.bind(this),
291 this.helperProgress_.bind(this), this.logMsgUrl_);
292 }
293
294 /**
295 * Creates a timer with an expiry greater than the expiration time of the given
296 * timer.
297 * @param {Countdown} timer
298 * @private
299 */
300 Signer.prototype.createWatchdog_ = function(timer) {
301 var millis = timer.millisecondsUntilExpired();
302 millis += CountdownTimer.TIMER_INTERVAL_MILLIS;
303 /** @private {Countdown|undefined} */
304 this.watchdogTimer_ = new CountdownTimer(millis, this.timeout_.bind(this));
305 };
306
307 /**
308 * Default timeout value in case the caller never provides a valid timeout.
309 */
310 Signer.DEFAULT_TIMEOUT_MILLIS = 30 * 1000;
311
312 /**
313 * Sets the challenges to be signed.
314 * @param {SignData} signData The challenges to set.
315 * @return {boolean} Whether the challenges could be set.
316 */
317 Signer.prototype.setChallenges = function(signData) {
318 if (this.challengesSet_ || this.done_)
319 return false;
320 /** @private {SignData} */
321 this.signData_ = signData;
322 /** @private {boolean} */
323 this.challengesSet_ = true;
324
325 // If app id enforcing isn't in effect, go ahead and start the helper with
326 // all of the incoming challenges.
327 var success = true;
328 if (!this.enforceAppIdValid_) {
329 success = this.addChallenges(signData, true /* finalChallenges */);
330 }
331
332 this.checkAppIds_();
333 return success;
334 };
335
336 /**
337 * Adds new challenges to the challenges being signed.
338 * @param {SignData} signData Challenges to add.
339 * @param {boolean} finalChallenges Whether these are the final challenges.
340 * @return {boolean} Whether the challenge could be added.
341 */
342 Signer.prototype.addChallenges = function(signData, finalChallenges) {
343 var newChallenges = this.encodeSignChallenges_(signData);
344 for (var i = 0; i < newChallenges.length; i++) {
345 this.pendingChallenges_.push(newChallenges[i]);
346 }
347 if (!finalChallenges) {
348 return true;
349 }
350 return this.helper_.doSign(this.pendingChallenges_);
351 };
352
353 /**
354 * Creates challenges for helper from challenges.
355 * @param {Array.<SignChallenge>} challenges Challenges to add.
356 * @return {Array.<SignHelperChallenge>}
357 * @private
358 */
359 Signer.prototype.encodeSignChallenges_ = function(challenges) {
360 var newChallenges = [];
361 for (var i = 0; i < challenges.length; i++) {
362 var incomingChallenge = challenges[i];
363 var serverChallenge = incomingChallenge['challenge'];
364 var appId = incomingChallenge['appId'];
365 var encodedKeyHandle = incomingChallenge['keyHandle'];
366 var version = incomingChallenge['version'];
367
368 var browserData =
369 makeSignBrowserData(serverChallenge, this.origin_, this.tlsChannelId_);
370 var encodedChallenge = makeChallenge(browserData, appId, encodedKeyHandle,
371 version);
372
373 var key = encodedKeyHandle + encodedChallenge['challengeHash'];
374 this.browserData_[key] = browserData;
375 this.serverChallenges_[key] = incomingChallenge;
376
377 newChallenges.push(encodedChallenge);
378 }
379 return newChallenges;
380 };
381
382 /**
383 * Checks the app ids of incoming requests, and, when this signer is enforcing
384 * that app ids are valid, adds successful challenges to those being signed.
385 * @private
386 */
387 Signer.prototype.checkAppIds_ = function() {
388 // Check the incoming challenges' app ids.
389 /** @private {Array.<[string, Array.<Request>]>} */
390 this.orderedRequests_ = requestsByAppId(this.signData_);
391 if (!this.orderedRequests_.length) {
392 // Safety check: if the challenges are somehow empty, the helper will never
393 // be fed any data, so the request could never be satisfied. You lose.
394 this.notifyError_(GnubbyCodeTypes.BAD_REQUEST);
395 return;
396 }
397 /** @private {number} */
398 this.fetchedAppIds_ = 0;
399 /** @private {number} */
400 this.validAppIds_ = 0;
401 for (var i = 0, appIdRequestsPair; i < this.orderedRequests_.length; i++) {
402 var appIdRequestsPair = this.orderedRequests_[i];
403 var appId = appIdRequestsPair[0];
404 var requests = appIdRequestsPair[1];
405 if (appId == this.origin_) {
406 // Trivially allowed.
407 this.fetchedAppIds_++;
408 this.validAppIds_++;
409 // Only add challenges if in enforcing mode, i.e. they weren't added
410 // earlier.
411 if (this.enforceAppIdValid_) {
412 this.addChallenges(requests,
413 this.fetchedAppIds_ == this.orderedRequests_.length);
414 }
415 } else {
416 var start = new Date();
417 fetchAllowedOriginsForAppId(appId, this.allowHttp_,
418 this.fetchedAllowedOriginsForAppId_.bind(this, appId, start,
419 requests));
420 }
421 }
422 };
423
424 /**
425 * Called with the result of an app id fetch.
426 * @param {string} appId the app id that was fetched.
427 * @param {Date} start the time the fetch request started.
428 * @param {Array.<SignChallenge>} challenges Challenges for this app id.
429 * @param {number} rc The HTTP response code for the app id fetch.
430 * @param {!Array.<string>} allowedOrigins The origins allowed for this app id.
431 * @private
432 */
433 Signer.prototype.fetchedAllowedOriginsForAppId_ = function(appId, start,
434 challenges, rc, allowedOrigins) {
435 var end = new Date();
436 logFetchAppIdResult(appId, end - start, allowedOrigins, this.logMsgUrl_);
437 if (rc != 200 && !(rc >= 400 && rc < 500)) {
438 if (this.timer_.expired()) {
439 // Act as though the helper timed out.
440 this.helperError_(DeviceStatusCodes.TIMEOUT_STATUS, false);
441 } else {
442 start = new Date();
443 fetchAllowedOriginsForAppId(appId, this.allowHttp_,
444 this.fetchedAllowedOriginsForAppId_.bind(this, appId, start,
445 challenges));
446 }
447 return;
448 }
449 this.fetchedAppIds_++;
450 var finalChallenges = (this.fetchedAppIds_ == this.orderedRequests_.length);
451 if (isValidAppIdForOrigin(appId, this.origin_, allowedOrigins)) {
452 this.validAppIds_++;
453 // Only add challenges if in enforcing mode, i.e. they weren't added
454 // earlier.
455 if (this.enforceAppIdValid_) {
456 this.addChallenges(challenges, finalChallenges);
457 }
458 } else {
459 logInvalidOriginForAppId(this.origin_, appId, this.logMsgUrl_);
460 // If in enforcing mode and this is the final request, sign the valid
461 // challenges.
462 if (this.enforceAppIdValid_ && finalChallenges) {
463 if (!this.helper_.doSign(this.pendingChallenges_)) {
464 this.notifyError_(GnubbyCodeTypes.BAD_REQUEST);
465 return;
466 }
467 }
468 }
469 if (this.enforceAppIdValid_ && finalChallenges && !this.validAppIds_) {
470 // If all app ids are invalid, notify the caller, otherwise implicitly
471 // allow the helper to report whether any of the valid challenges succeeded.
472 this.notifyError_(GnubbyCodeTypes.BAD_APP_ID);
473 }
474 };
475
476 /**
477 * Called when the timeout expires on this signer.
478 * @private
479 */
480 Signer.prototype.timeout_ = function() {
481 this.watchdogTimer_ = undefined;
482 // The web page gets grumpy if it doesn't get WAIT_TOUCH within a reasonable
483 // time.
484 this.notifyError_(GnubbyCodeTypes.WAIT_TOUCH);
485 };
486
487 /** Closes this signer. */
488 Signer.prototype.close = function() {
489 if (this.helper_) this.helper_.close();
490 };
491
492 /**
493 * Notifies the caller of error with the given error code.
494 * @param {number} code
495 * @private
496 */
497 Signer.prototype.notifyError_ = function(code) {
498 if (this.done_)
499 return;
500 this.close();
501 this.done_ = true;
502 this.errorCb_(code);
503 };
504
505 /**
506 * Notifies the caller of success.
507 * @param {SignChallenge} challenge The challenge that was signed.
508 * @param {string} info The sign result.
509 * @param {string} browserData
510 * @private
511 */
512 Signer.prototype.notifySuccess_ = function(challenge, info, browserData) {
513 if (this.done_)
514 return;
515 this.close();
516 this.done_ = true;
517 this.successCb_(challenge, info, browserData);
518 };
519
520 /**
521 * Notifies the caller of progress with the error code.
522 * @param {number} code
523 * @private
524 */
525 Signer.prototype.notifyProgress_ = function(code) {
526 if (this.done_)
527 return;
528 if (code != this.lastProgressUpdate_) {
529 this.lastProgressUpdate_ = code;
530 // If there is no progress callback, treat it like an error and clean up.
531 if (this.progressCb_) {
532 this.progressCb_(code);
533 } else {
534 this.notifyError_(code);
535 }
536 }
537 };
538
539 /**
540 * Maps a sign helper's error code namespace to the page's error code namespace.
541 * @param {number} code Error code from DeviceStatusCodes namespace.
542 * @param {boolean} anyGnubbies Whether any gnubbies were found.
543 * @return {number} A GnubbyCodeTypes error code.
544 * @private
545 */
546 Signer.mapError_ = function(code, anyGnubbies) {
547 var reportedError;
548 switch (code) {
549 case DeviceStatusCodes.WRONG_DATA_STATUS:
550 reportedError = anyGnubbies ? GnubbyCodeTypes.NONE_PLUGGED_ENROLLED :
551 GnubbyCodeTypes.NO_GNUBBIES;
552 break;
553
554 case DeviceStatusCodes.OK_STATUS:
555 // If the error callback is called with OK, it means the signature was
556 // empty, which we treat the same as...
557 case DeviceStatusCodes.WAIT_TOUCH_STATUS:
558 reportedError = GnubbyCodeTypes.WAIT_TOUCH;
559 break;
560
561 case DeviceStatusCodes.BUSY_STATUS:
562 reportedError = GnubbyCodeTypes.BUSY;
563 break;
564
565 default:
566 reportedError = GnubbyCodeTypes.UNKNOWN_ERROR;
567 break;
568 }
569 return reportedError;
570 };
571
572 /**
573 * Called by the helper upon error.
574 * @param {number} code
575 * @param {boolean} anyGnubbies
576 * @private
577 */
578 Signer.prototype.helperError_ = function(code, anyGnubbies) {
579 this.clearTimeout_();
580 var reportedError = Signer.mapError_(code, anyGnubbies);
581 console.log(UTIL_fmt('helper reported ' + code.toString(16) +
582 ', returning ' + reportedError));
583 this.notifyError_(reportedError);
584 };
585
586 /**
587 * Called by helper upon success.
588 * @param {SignHelperChallenge} challenge The challenge that was signed.
589 * @param {string} info The sign result.
590 * @private
591 */
592 Signer.prototype.helperSuccess_ = function(challenge, info) {
593 // Got a good reply, kill timer.
594 this.clearTimeout_();
595
596 var key = challenge['keyHandle'] + challenge['challengeHash'];
597 var browserData = this.browserData_[key];
598 // Notify with server-provided challenge, not the encoded one: the
599 // server-provided challenge contains additional fields it relies on.
600 var serverChallenge = this.serverChallenges_[key];
601 this.notifySuccess_(serverChallenge, info, browserData);
602 };
603
604 /**
605 * Called by helper to notify progress.
606 * @param {number} code
607 * @param {boolean} anyGnubbies
608 * @private
609 */
610 Signer.prototype.helperProgress_ = function(code, anyGnubbies) {
611 var reportedError = Signer.mapError_(code, anyGnubbies);
612 console.log(UTIL_fmt('helper notified ' + code.toString(16) +
613 ', returning ' + reportedError));
614 this.notifyProgress_(reportedError);
615 };
616
617 /**
618 * Clears the timeout for this signer.
619 * @private
620 */
621 Signer.prototype.clearTimeout_ = function() {
622 if (this.watchdogTimer_) {
623 this.watchdogTimer_.clearTimeout();
624 this.watchdogTimer_ = undefined;
625 }
626 };
OLDNEW
« no previous file with comments | « chrome/browser/resources/cryptotoken/sha256.js ('k') | chrome/browser/resources/cryptotoken/signhelper.js » ('j') | no next file with comments »

Powered by Google App Engine
This is Rietveld 408576698