cygprofile: increase timeouts to allow showing web contents
[chromium-blink-merge.git] / chrome / browser / resources / cryptotoken / enroller.js
blob496ffa6c32a761afc4a365b9c181f8fd3b1dd79b
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 Handles web page requests for gnubby enrollment.
7 */
9 'use strict';
11 /**
12 * Handles a U2F enroll request.
13 * @param {MessageSender} messageSender The message sender.
14 * @param {Object} request The web page's enroll request.
15 * @param {Function} sendResponse Called back with the result of the enroll.
16 * @return {Closeable} A handler object to be closed when the browser channel
17 * closes.
19 function handleU2fEnrollRequest(messageSender, request, sendResponse) {
20 var sentResponse = false;
21 var closeable = null;
23 function sendErrorResponse(error) {
24 var response = makeU2fErrorResponse(request, error.errorCode,
25 error.errorMessage);
26 sendResponseOnce(sentResponse, closeable, response, sendResponse);
29 function sendSuccessResponse(u2fVersion, info, clientData) {
30 var enrollChallenges = request['registerRequests'];
31 var enrollChallenge =
32 findEnrollChallengeOfVersion(enrollChallenges, u2fVersion);
33 if (!enrollChallenge) {
34 sendErrorResponse({errorCode: ErrorCodes.OTHER_ERROR});
35 return;
37 var responseData =
38 makeEnrollResponseData(enrollChallenge, u2fVersion, info, clientData);
39 var response = makeU2fSuccessResponse(request, responseData);
40 sendResponseOnce(sentResponse, closeable, response, sendResponse);
43 function timeout() {
44 sendErrorResponse({errorCode: ErrorCodes.TIMEOUT});
47 var sender = createSenderFromMessageSender(messageSender);
48 if (!sender) {
49 sendErrorResponse({errorCode: ErrorCodes.BAD_REQUEST});
50 return null;
52 if (sender.origin.indexOf('http://') == 0 && !HTTP_ORIGINS_ALLOWED) {
53 sendErrorResponse({errorCode: ErrorCodes.BAD_REQUEST});
54 return null;
57 if (!isValidEnrollRequest(request)) {
58 sendErrorResponse({errorCode: ErrorCodes.BAD_REQUEST});
59 return null;
62 var timeoutValueSeconds = getTimeoutValueFromRequest(request);
63 // Attenuate watchdog timeout value less than the enroller's timeout, so the
64 // watchdog only fires after the enroller could reasonably have called back,
65 // not before.
66 var watchdogTimeoutValueSeconds = attenuateTimeoutInSeconds(
67 timeoutValueSeconds, MINIMUM_TIMEOUT_ATTENUATION_SECONDS / 2);
68 var watchdog = new WatchdogRequestHandler(watchdogTimeoutValueSeconds,
69 timeout);
70 var wrappedErrorCb = watchdog.wrapCallback(sendErrorResponse);
71 var wrappedSuccessCb = watchdog.wrapCallback(sendSuccessResponse);
73 var timer = createAttenuatedTimer(
74 FACTORY_REGISTRY.getCountdownFactory(), timeoutValueSeconds);
75 var logMsgUrl = request['logMsgUrl'];
76 var enroller = new Enroller(timer, sender, sendErrorResponse,
77 sendSuccessResponse, logMsgUrl);
78 watchdog.setCloseable(/** @type {!Closeable} */ (enroller));
79 closeable = watchdog;
81 var registerRequests = request['registerRequests'];
82 var signRequests = getSignRequestsFromEnrollRequest(request);
83 enroller.doEnroll(registerRequests, signRequests, request['appId']);
85 return closeable;
88 /**
89 * Returns whether the request appears to be a valid enroll request.
90 * @param {Object} request The request.
91 * @return {boolean} Whether the request appears valid.
93 function isValidEnrollRequest(request) {
94 if (!request.hasOwnProperty('registerRequests'))
95 return false;
96 var enrollChallenges = request['registerRequests'];
97 if (!enrollChallenges.length)
98 return false;
99 var hasAppId = request.hasOwnProperty('appId');
100 if (!isValidEnrollChallengeArray(enrollChallenges, !hasAppId))
101 return false;
102 var signChallenges = getSignChallenges(request);
103 // A missing sign challenge array is ok, in the case the user is not already
104 // enrolled.
105 // A challenge value need not necessarily be supplied with every challenge.
106 var challengeRequired = false;
107 if (signChallenges &&
108 !isValidSignChallengeArray(signChallenges, challengeRequired, !hasAppId))
109 return false;
110 return true;
114 * @typedef {{
115 * version: (string|undefined),
116 * challenge: string,
117 * appId: string
118 * }}
120 var EnrollChallenge;
123 * @param {Array<EnrollChallenge>} enrollChallenges The enroll challenges to
124 * validate.
125 * @param {boolean} appIdRequired Whether the appId property is required on
126 * each challenge.
127 * @return {boolean} Whether the given array of challenges is a valid enroll
128 * challenges array.
130 function isValidEnrollChallengeArray(enrollChallenges, appIdRequired) {
131 var seenVersions = {};
132 for (var i = 0; i < enrollChallenges.length; i++) {
133 var enrollChallenge = enrollChallenges[i];
134 var version = enrollChallenge['version'];
135 if (!version) {
136 // Version is implicitly V1 if not specified.
137 version = 'U2F_V1';
139 if (version != 'U2F_V1' && version != 'U2F_V2') {
140 return false;
142 if (seenVersions[version]) {
143 // Each version can appear at most once.
144 return false;
146 seenVersions[version] = version;
147 if (appIdRequired && !enrollChallenge['appId']) {
148 return false;
150 if (!enrollChallenge['challenge']) {
151 // The challenge is required.
152 return false;
155 return true;
159 * Finds the enroll challenge of the given version in the enroll challlenge
160 * array.
161 * @param {Array<EnrollChallenge>} enrollChallenges The enroll challenges to
162 * search.
163 * @param {string} version Version to search for.
164 * @return {?EnrollChallenge} The enroll challenge with the given versions, or
165 * null if it isn't found.
167 function findEnrollChallengeOfVersion(enrollChallenges, version) {
168 for (var i = 0; i < enrollChallenges.length; i++) {
169 if (enrollChallenges[i]['version'] == version) {
170 return enrollChallenges[i];
173 return null;
177 * Makes a responseData object for the enroll request with the given parameters.
178 * @param {EnrollChallenge} enrollChallenge The enroll challenge used to
179 * register.
180 * @param {string} u2fVersion Version of gnubby that enrolled.
181 * @param {string} registrationData The registration data.
182 * @param {string=} opt_clientData The client data, if available.
183 * @return {Object} The responseData object.
185 function makeEnrollResponseData(enrollChallenge, u2fVersion, registrationData,
186 opt_clientData) {
187 var responseData = {};
188 responseData['registrationData'] = registrationData;
189 // Echo the used challenge back in the reply.
190 for (var k in enrollChallenge) {
191 responseData[k] = enrollChallenge[k];
193 if (u2fVersion == 'U2F_V2') {
194 // For U2F_V2, the challenge sent to the gnubby is modified to be the
195 // hash of the client data. Include the client data.
196 responseData['clientData'] = opt_clientData;
198 return responseData;
202 * Gets the expanded sign challenges from an enroll request, potentially by
203 * modifying the request to contain a challenge value where one was omitted.
204 * (For enrolling, the server isn't interested in the value of a signature,
205 * only whether the presented key handle is already enrolled.)
206 * @param {Object} request The request.
207 * @return {Array<SignChallenge>}
209 function getSignRequestsFromEnrollRequest(request) {
210 var signChallenges;
211 if (request.hasOwnProperty('registeredKeys')) {
212 signChallenges = request['registeredKeys'];
213 } else {
214 signChallenges = request['signRequests'];
216 if (signChallenges) {
217 for (var i = 0; i < signChallenges.length; i++) {
218 // Make sure each sign challenge has a challenge value.
219 // The actual value doesn't matter, as long as it's a string.
220 if (!signChallenges[i].hasOwnProperty('challenge')) {
221 signChallenges[i]['challenge'] = '';
225 return signChallenges;
229 * Creates a new object to track enrolling with a gnubby.
230 * @param {!Countdown} timer Timer for enroll request.
231 * @param {!WebRequestSender} sender The sender of the request.
232 * @param {function(U2fError)} errorCb Called upon enroll failure.
233 * @param {function(string, string, (string|undefined))} successCb Called upon
234 * enroll success with the version of the succeeding gnubby, the enroll
235 * data, and optionally the browser data associated with the enrollment.
236 * @param {string=} opt_logMsgUrl The url to post log messages to.
237 * @constructor
239 function Enroller(timer, sender, errorCb, successCb, opt_logMsgUrl) {
240 /** @private {Countdown} */
241 this.timer_ = timer;
242 /** @private {WebRequestSender} */
243 this.sender_ = sender;
244 /** @private {function(U2fError)} */
245 this.errorCb_ = errorCb;
246 /** @private {function(string, string, (string|undefined))} */
247 this.successCb_ = successCb;
248 /** @private {string|undefined} */
249 this.logMsgUrl_ = opt_logMsgUrl;
251 /** @private {boolean} */
252 this.done_ = false;
254 /** @private {Object<string, string>} */
255 this.browserData_ = {};
256 /** @private {Array<EnrollHelperChallenge>} */
257 this.encodedEnrollChallenges_ = [];
258 /** @private {Array<SignHelperChallenge>} */
259 this.encodedSignChallenges_ = [];
260 // Allow http appIds for http origins. (Broken, but the caller deserves
261 // what they get.)
262 /** @private {boolean} */
263 this.allowHttp_ =
264 this.sender_.origin ? this.sender_.origin.indexOf('http://') == 0 : false;
265 /** @private {Closeable} */
266 this.handler_ = null;
270 * Default timeout value in case the caller never provides a valid timeout.
272 Enroller.DEFAULT_TIMEOUT_MILLIS = 30 * 1000;
275 * Performs an enroll request with the given enroll and sign challenges.
276 * @param {Array<EnrollChallenge>} enrollChallenges A set of enroll challenges.
277 * @param {Array<SignChallenge>} signChallenges A set of sign challenges for
278 * existing enrollments for this user and appId.
279 * @param {string=} opt_appId The app id for the entire request.
281 Enroller.prototype.doEnroll = function(enrollChallenges, signChallenges,
282 opt_appId) {
283 /** @private {Array<EnrollChallenge>} */
284 this.enrollChallenges_ = enrollChallenges;
285 /** @private {Array<SignChallenge>} */
286 this.signChallenges_ = signChallenges;
287 /** @private {(string|undefined)} */
288 this.appId_ = opt_appId;
289 var self = this;
290 getTabIdWhenPossible(this.sender_).then(function() {
291 if (self.done_) return;
292 self.approveOrigin_();
293 }, function() {
294 self.close();
295 self.notifyError_({errorCode: ErrorCodes.BAD_REQUEST});
300 * Ensures the user has approved this origin to use security keys, sending
301 * to the request to the handler if/when the user has done so.
302 * @private
304 Enroller.prototype.approveOrigin_ = function() {
305 var self = this;
306 FACTORY_REGISTRY.getApprovedOrigins()
307 .isApprovedOrigin(this.sender_.origin, this.sender_.tabId)
308 .then(function(result) {
309 if (self.done_) return;
310 if (!result) {
311 // Origin not approved: rather than give an explicit indication to
312 // the web page, let a timeout occur.
313 if (self.timer_.expired()) {
314 self.notifyTimeout_();
315 return;
317 var newTimer = self.timer_.clone(self.notifyTimeout_.bind(self));
318 self.timer_.clearTimeout();
319 self.timer_ = newTimer;
320 return;
322 self.sendEnrollRequestToHelper_();
327 * Notifies the caller of a timeout error.
328 * @private
330 Enroller.prototype.notifyTimeout_ = function() {
331 this.notifyError_({errorCode: ErrorCodes.TIMEOUT});
335 * Performs an enroll request with this instance's enroll and sign challenges,
336 * by encoding them into a helper request and passing the resulting request to
337 * the factory registry's helper.
338 * @private
340 Enroller.prototype.sendEnrollRequestToHelper_ = function() {
341 var encodedEnrollChallenges =
342 this.encodeEnrollChallenges_(this.enrollChallenges_, this.appId_);
343 // If the request didn't contain a sign challenge, provide one. The value
344 // doesn't matter.
345 var defaultSignChallenge = '';
346 var encodedSignChallenges =
347 encodeSignChallenges(this.signChallenges_, defaultSignChallenge,
348 this.appId_);
349 var request = {
350 type: 'enroll_helper_request',
351 enrollChallenges: encodedEnrollChallenges,
352 signData: encodedSignChallenges,
353 logMsgUrl: this.logMsgUrl_
355 if (!this.timer_.expired()) {
356 request.timeout = this.timer_.millisecondsUntilExpired() / 1000.0;
357 request.timeoutSeconds = this.timer_.millisecondsUntilExpired() / 1000.0;
360 // Begin fetching/checking the app ids.
361 var enrollAppIds = [];
362 if (this.appId_) {
363 enrollAppIds.push(this.appId_);
365 for (var i = 0; i < this.enrollChallenges_.length; i++) {
366 if (this.enrollChallenges_[i].hasOwnProperty('appId')) {
367 enrollAppIds.push(this.enrollChallenges_[i]['appId']);
370 // Sanity check
371 if (!enrollAppIds.length) {
372 console.warn(UTIL_fmt('empty enroll app ids?'));
373 this.notifyError_({errorCode: ErrorCodes.BAD_REQUEST});
374 return;
376 var self = this;
377 this.checkAppIds_(enrollAppIds, function(result) {
378 if (self.done_) return;
379 if (result) {
380 self.handler_ = FACTORY_REGISTRY.getRequestHelper().getHandler(request);
381 if (self.handler_) {
382 var helperComplete =
383 /** @type {function(HelperReply)} */
384 (self.helperComplete_.bind(self));
385 self.handler_.run(helperComplete);
386 } else {
387 self.notifyError_({errorCode: ErrorCodes.OTHER_ERROR});
389 } else {
390 self.notifyError_({errorCode: ErrorCodes.BAD_REQUEST});
396 * Encodes the enroll challenge as an enroll helper challenge.
397 * @param {EnrollChallenge} enrollChallenge The enroll challenge to encode.
398 * @param {string=} opt_appId The app id for the entire request.
399 * @return {EnrollHelperChallenge} The encoded challenge.
400 * @private
402 Enroller.encodeEnrollChallenge_ = function(enrollChallenge, opt_appId) {
403 var encodedChallenge = {};
404 var version;
405 if (enrollChallenge['version']) {
406 version = enrollChallenge['version'];
407 } else {
408 // Version is implicitly V1 if not specified.
409 version = 'U2F_V1';
411 encodedChallenge['version'] = version;
412 encodedChallenge['challengeHash'] = enrollChallenge['challenge'];
413 var appId;
414 if (enrollChallenge['appId']) {
415 appId = enrollChallenge['appId'];
416 } else {
417 appId = opt_appId;
419 if (!appId) {
420 // Sanity check. (Other code should fail if it's not set.)
421 console.warn(UTIL_fmt('No appId?'));
423 encodedChallenge['appIdHash'] = B64_encode(sha256HashOfString(appId));
424 return /** @type {EnrollHelperChallenge} */ (encodedChallenge);
428 * Encodes the given enroll challenges using this enroller's state.
429 * @param {Array<EnrollChallenge>} enrollChallenges The enroll challenges.
430 * @param {string=} opt_appId The app id for the entire request.
431 * @return {!Array<EnrollHelperChallenge>} The encoded enroll challenges.
432 * @private
434 Enroller.prototype.encodeEnrollChallenges_ = function(enrollChallenges,
435 opt_appId) {
436 var challenges = [];
437 for (var i = 0; i < enrollChallenges.length; i++) {
438 var enrollChallenge = enrollChallenges[i];
439 var version = enrollChallenge.version;
440 if (!version) {
441 // Version is implicitly V1 if not specified.
442 version = 'U2F_V1';
445 if (version == 'U2F_V2') {
446 var modifiedChallenge = {};
447 for (var k in enrollChallenge) {
448 modifiedChallenge[k] = enrollChallenge[k];
450 // V2 enroll responses contain signatures over a browser data object,
451 // which we're constructing here. The browser data object contains, among
452 // other things, the server challenge.
453 var serverChallenge = enrollChallenge['challenge'];
454 var browserData = makeEnrollBrowserData(
455 serverChallenge, this.sender_.origin, this.sender_.tlsChannelId);
456 // Replace the challenge with the hash of the browser data.
457 modifiedChallenge['challenge'] =
458 B64_encode(sha256HashOfString(browserData));
459 this.browserData_[version] =
460 B64_encode(UTIL_StringToBytes(browserData));
461 challenges.push(Enroller.encodeEnrollChallenge_(
462 /** @type {EnrollChallenge} */ (modifiedChallenge), opt_appId));
463 } else {
464 challenges.push(
465 Enroller.encodeEnrollChallenge_(enrollChallenge, opt_appId));
468 return challenges;
472 * Checks the app ids associated with this enroll request, and calls a callback
473 * with the result of the check.
474 * @param {!Array<string>} enrollAppIds The app ids in the enroll challenge
475 * portion of the enroll request.
476 * @param {function(boolean)} cb Called with the result of the check.
477 * @private
479 Enroller.prototype.checkAppIds_ = function(enrollAppIds, cb) {
480 var appIds =
481 UTIL_unionArrays(enrollAppIds, getDistinctAppIds(this.signChallenges_));
482 FACTORY_REGISTRY.getOriginChecker()
483 .canClaimAppIds(this.sender_.origin, appIds)
484 .then(this.originChecked_.bind(this, appIds, cb));
488 * Called with the result of checking the origin. When the origin is allowed
489 * to claim the app ids, begins checking whether the app ids also list the
490 * origin.
491 * @param {!Array<string>} appIds The app ids.
492 * @param {function(boolean)} cb Called with the result of the check.
493 * @param {boolean} result Whether the origin could claim the app ids.
494 * @private
496 Enroller.prototype.originChecked_ = function(appIds, cb, result) {
497 if (!result) {
498 this.notifyError_({errorCode: ErrorCodes.BAD_REQUEST});
499 return;
501 var appIdChecker = FACTORY_REGISTRY.getAppIdCheckerFactory().create();
502 appIdChecker.
503 checkAppIds(
504 this.timer_.clone(), this.sender_.origin, appIds, this.allowHttp_,
505 this.logMsgUrl_)
506 .then(cb);
509 /** Closes this enroller. */
510 Enroller.prototype.close = function() {
511 if (this.handler_) {
512 this.handler_.close();
513 this.handler_ = null;
515 this.done_ = true;
519 * Notifies the caller with the error.
520 * @param {U2fError} error Error.
521 * @private
523 Enroller.prototype.notifyError_ = function(error) {
524 if (this.done_)
525 return;
526 this.close();
527 this.done_ = true;
528 this.errorCb_(error);
532 * Notifies the caller of success with the provided response data.
533 * @param {string} u2fVersion Protocol version
534 * @param {string} info Response data
535 * @param {string|undefined} opt_browserData Browser data used
536 * @private
538 Enroller.prototype.notifySuccess_ =
539 function(u2fVersion, info, opt_browserData) {
540 if (this.done_)
541 return;
542 this.close();
543 this.done_ = true;
544 this.successCb_(u2fVersion, info, opt_browserData);
548 * Called by the helper upon completion.
549 * @param {EnrollHelperReply} reply The result of the enroll request.
550 * @private
552 Enroller.prototype.helperComplete_ = function(reply) {
553 if (reply.code) {
554 var reportedError = mapDeviceStatusCodeToU2fError(reply.code);
555 console.log(UTIL_fmt('helper reported ' + reply.code.toString(16) +
556 ', returning ' + reportedError.errorCode));
557 this.notifyError_(reportedError);
558 } else {
559 console.log(UTIL_fmt('Gnubby enrollment succeeded!!!!!'));
560 var browserData;
562 if (reply.version == 'U2F_V2') {
563 // For U2F_V2, the challenge sent to the gnubby is modified to be the hash
564 // of the browser data. Include the browser data.
565 browserData = this.browserData_[reply.version];
568 this.notifySuccess_(/** @type {string} */ (reply.version),
569 /** @type {string} */ (reply.enrollData),
570 browserData);