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.
6 * @fileoverview Handles web page requests for gnubby enrollment.
12 * Handles a web 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
19 function handleWebEnrollRequest(messageSender, request, sendResponse) {
20 var sentResponse = false;
23 function sendErrorResponse(error) {
24 var response = makeWebErrorResponse(request,
25 mapErrorCodeToGnubbyCodeType(error.errorCode, false /* forSign */));
26 sendResponseOnce(sentResponse, closeable, response, sendResponse);
29 function sendSuccessResponse(u2fVersion, info, browserData) {
30 var enrollChallenges = request['enrollChallenges'];
32 findEnrollChallengeOfVersion(enrollChallenges, u2fVersion);
33 if (!enrollChallenge) {
34 sendErrorResponse({errorCode: ErrorCodes.OTHER_ERROR});
38 makeEnrollResponseData(enrollChallenge, u2fVersion,
39 'enrollData', info, 'browserData', browserData);
40 var response = makeWebSuccessResponse(request, responseData);
41 sendResponseOnce(sentResponse, closeable, response, sendResponse);
44 var sender = createSenderFromMessageSender(messageSender);
46 sendErrorResponse({errorCode: ErrorCodes.BAD_REQUEST});
51 validateEnrollRequest(
52 sender, request, 'enrollChallenges', 'signData',
53 sendErrorResponse, sendSuccessResponse);
55 var registerRequests = request['enrollChallenges'];
56 var signRequests = getSignRequestsFromEnrollRequest(request, 'signData');
57 closeable = /** @type {Closeable} */ (enroller);
58 enroller.doEnroll(registerRequests, signRequests, request['appId']);
64 * Handles a U2F enroll request.
65 * @param {MessageSender} messageSender The message sender.
66 * @param {Object} request The web page's enroll request.
67 * @param {Function} sendResponse Called back with the result of the enroll.
68 * @return {Closeable} A handler object to be closed when the browser channel
71 function handleU2fEnrollRequest(messageSender, request, sendResponse) {
72 var sentResponse = false;
75 function sendErrorResponse(error) {
76 var response = makeU2fErrorResponse(request, error.errorCode,
78 sendResponseOnce(sentResponse, closeable, response, sendResponse);
81 function sendSuccessResponse(u2fVersion, info, browserData) {
82 var enrollChallenges = request['registerRequests'];
84 findEnrollChallengeOfVersion(enrollChallenges, u2fVersion);
85 if (!enrollChallenge) {
86 sendErrorResponse({errorCode: ErrorCodes.OTHER_ERROR});
90 makeEnrollResponseData(enrollChallenge, u2fVersion,
91 'registrationData', info, 'clientData', browserData);
92 var response = makeU2fSuccessResponse(request, responseData);
93 sendResponseOnce(sentResponse, closeable, response, sendResponse);
96 var sender = createSenderFromMessageSender(messageSender);
98 sendErrorResponse({errorCode: ErrorCodes.BAD_REQUEST});
103 validateEnrollRequest(
104 sender, request, 'registerRequests', 'signRequests',
105 sendErrorResponse, sendSuccessResponse, 'registeredKeys');
107 var registerRequests = request['registerRequests'];
108 var signRequests = getSignRequestsFromEnrollRequest(request,
109 'signRequests', 'registeredKeys');
110 closeable = /** @type {Closeable} */ (enroller);
111 enroller.doEnroll(registerRequests, signRequests, request['appId']);
117 * Validates an enroll request using the given parameters.
118 * @param {WebRequestSender} sender The sender of the message.
119 * @param {Object} request The web page's enroll request.
120 * @param {string} enrollChallengesName The name of the enroll challenges value
122 * @param {string} signChallengesName The name of the sign challenges value in
124 * @param {function(U2fError)} errorCb Error callback.
125 * @param {function(string, string, (string|undefined))} successCb Success
127 * @param {string=} opt_registeredKeysName The name of the registered keys
128 * value in the request.
129 * @return {Enroller} Enroller object representing the request, if the request
130 * is valid, or null if the request is invalid.
132 function validateEnrollRequest(sender, request,
133 enrollChallengesName, signChallengesName, errorCb, successCb,
134 opt_registeredKeysName) {
135 if (!isValidEnrollRequest(request, enrollChallengesName,
136 signChallengesName, opt_registeredKeysName)) {
137 errorCb({errorCode: ErrorCodes.BAD_REQUEST});
141 var timeoutValueSeconds = getTimeoutValueFromRequest(request);
142 var timer = createAttenuatedTimer(
143 FACTORY_REGISTRY.getCountdownFactory(), timeoutValueSeconds);
144 var logMsgUrl = request['logMsgUrl'];
145 var enroller = new Enroller(timer, sender, errorCb, successCb, logMsgUrl);
150 * Returns whether the request appears to be a valid enroll request.
151 * @param {Object} request The request.
152 * @param {string} enrollChallengesName The name of the enroll challenges value
154 * @param {string} signChallengesName The name of the sign challenges value in
156 * @param {string=} opt_registeredKeysName The name of the registered keys
157 * value in the request.
158 * @return {boolean} Whether the request appears valid.
160 function isValidEnrollRequest(request, enrollChallengesName,
161 signChallengesName, opt_registeredKeysName) {
162 if (!request.hasOwnProperty(enrollChallengesName))
164 var enrollChallenges = request[enrollChallengesName];
165 if (!enrollChallenges.length)
167 var hasAppId = request.hasOwnProperty('appId');
168 if (!isValidEnrollChallengeArray(enrollChallenges, !hasAppId))
170 var signChallenges = request[signChallengesName];
171 // A missing sign challenge array is ok, in the case the user is not already
173 // A challenge value need not necessarily be supplied with every challenge.
174 var challengeRequired = false;
175 if (signChallenges &&
176 !isValidSignChallengeArray(signChallenges, challengeRequired, !hasAppId))
178 if (opt_registeredKeysName) {
179 var registeredKeys = request[opt_registeredKeysName];
180 if (registeredKeys &&
181 !isValidRegisteredKeyArray(registeredKeys, !hasAppId)) {
190 * version: (string|undefined),
198 * @param {Array.<EnrollChallenge>} enrollChallenges The enroll challenges to
200 * @param {boolean} appIdRequired Whether the appId property is required on
202 * @return {boolean} Whether the given array of challenges is a valid enroll
205 function isValidEnrollChallengeArray(enrollChallenges, appIdRequired) {
206 var seenVersions = {};
207 for (var i = 0; i < enrollChallenges.length; i++) {
208 var enrollChallenge = enrollChallenges[i];
209 var version = enrollChallenge['version'];
211 // Version is implicitly V1 if not specified.
214 if (version != 'U2F_V1' && version != 'U2F_V2') {
217 if (seenVersions[version]) {
218 // Each version can appear at most once.
221 seenVersions[version] = version;
222 if (appIdRequired && !enrollChallenge['appId']) {
225 if (!enrollChallenge['challenge']) {
226 // The challenge is required.
234 * Finds the enroll challenge of the given version in the enroll challlenge
236 * @param {Array.<EnrollChallenge>} enrollChallenges The enroll challenges to
238 * @param {string} version Version to search for.
239 * @return {?EnrollChallenge} The enroll challenge with the given versions, or
240 * null if it isn't found.
242 function findEnrollChallengeOfVersion(enrollChallenges, version) {
243 for (var i = 0; i < enrollChallenges.length; i++) {
244 if (enrollChallenges[i]['version'] == version) {
245 return enrollChallenges[i];
252 * Makes a responseData object for the enroll request with the given parameters.
253 * @param {EnrollChallenge} enrollChallenge The enroll challenge used to
255 * @param {string} u2fVersion Version of gnubby that enrolled.
256 * @param {string} enrollDataName The name of the enroll data key in the
257 * responseData object.
258 * @param {string} enrollData The enroll data.
259 * @param {string} browserDataName The name of the browser data key in the
260 * responseData object.
261 * @param {string=} browserData The browser data, if available.
262 * @return {Object} The responseData object.
264 function makeEnrollResponseData(enrollChallenge, u2fVersion, enrollDataName,
265 enrollData, browserDataName, browserData) {
266 var responseData = {};
267 responseData[enrollDataName] = enrollData;
268 // Echo the used challenge back in the reply.
269 for (var k in enrollChallenge) {
270 responseData[k] = enrollChallenge[k];
272 if (u2fVersion == 'U2F_V2') {
273 // For U2F_V2, the challenge sent to the gnubby is modified to be the
274 // hash of the browser data. Include the browser data.
275 responseData[browserDataName] = browserData;
281 * Gets the expanded sign challenges from an enroll request, potentially by
282 * modifying the request to contain a challenge value where one was omitted.
283 * (For enrolling, the server isn't interested in the value of a signature,
284 * only whether the presented key handle is already enrolled.)
285 * @param {Object} request The request.
286 * @param {string} signChallengesName The name of the sign challenges value in
288 * @param {string=} opt_registeredKeysName The name of the registered keys
289 * value in the request.
290 * @return {Array.<SignChallenge>}
292 function getSignRequestsFromEnrollRequest(request, signChallengesName,
293 opt_registeredKeysName) {
295 if (opt_registeredKeysName &&
296 request.hasOwnProperty(opt_registeredKeysName)) {
297 signChallenges = request[opt_registeredKeysName];
299 signChallenges = request[signChallengesName];
301 if (signChallenges) {
302 for (var i = 0; i < signChallenges.length; i++) {
303 // Make sure each sign challenge has a challenge value.
304 // The actual value doesn't matter, as long as it's a string.
305 if (!signChallenges[i].hasOwnProperty('challenge')) {
306 signChallenges[i]['challenge'] = '';
310 return signChallenges;
314 * Creates a new object to track enrolling with a gnubby.
315 * @param {!Countdown} timer Timer for enroll request.
316 * @param {!WebRequestSender} sender The sender of the request.
317 * @param {function(U2fError)} errorCb Called upon enroll failure.
318 * @param {function(string, string, (string|undefined))} successCb Called upon
319 * enroll success with the version of the succeeding gnubby, the enroll
320 * data, and optionally the browser data associated with the enrollment.
321 * @param {string=} opt_logMsgUrl The url to post log messages to.
324 function Enroller(timer, sender, errorCb, successCb, opt_logMsgUrl) {
325 /** @private {Countdown} */
327 /** @private {WebRequestSender} */
328 this.sender_ = sender;
329 /** @private {function(U2fError)} */
330 this.errorCb_ = errorCb;
331 /** @private {function(string, string, (string|undefined))} */
332 this.successCb_ = successCb;
333 /** @private {string|undefined} */
334 this.logMsgUrl_ = opt_logMsgUrl;
336 /** @private {boolean} */
339 /** @private {Object.<string, string>} */
340 this.browserData_ = {};
341 /** @private {Array.<EnrollHelperChallenge>} */
342 this.encodedEnrollChallenges_ = [];
343 /** @private {Array.<SignHelperChallenge>} */
344 this.encodedSignChallenges_ = [];
345 // Allow http appIds for http origins. (Broken, but the caller deserves
347 /** @private {boolean} */
349 this.sender_.origin ? this.sender_.origin.indexOf('http://') == 0 : false;
350 /** @private {Closeable} */
351 this.handler_ = null;
355 * Default timeout value in case the caller never provides a valid timeout.
357 Enroller.DEFAULT_TIMEOUT_MILLIS = 30 * 1000;
360 * Performs an enroll request with the given enroll and sign challenges.
361 * @param {Array.<EnrollChallenge>} enrollChallenges A set of enroll challenges.
362 * @param {Array.<SignChallenge>} signChallenges A set of sign challenges for
363 * existing enrollments for this user and appId.
364 * @param {string=} opt_appId The app id for the entire request.
366 Enroller.prototype.doEnroll = function(enrollChallenges, signChallenges,
368 /** @private {Array.<EnrollChallenge>} */
369 this.enrollChallenges_ = enrollChallenges;
370 /** @private {Array.<SignChallenge>} */
371 this.signChallenges_ = signChallenges;
372 /** @private {(string|undefined)} */
373 this.appId_ = opt_appId;
375 getTabIdWhenPossible(this.sender_).then(function() {
376 if (self.done_) return;
377 self.approveOrigin_();
380 self.notifyError_({errorCode: ErrorCodes.BAD_REQUEST});
385 * Ensures the user has approved this origin to use security keys, sending
386 * to the request to the handler if/when the user has done so.
389 Enroller.prototype.approveOrigin_ = function() {
391 FACTORY_REGISTRY.getApprovedOrigins()
392 .isApprovedOrigin(this.sender_.origin, this.sender_.tabId)
393 .then(function(result) {
394 if (self.done_) return;
396 // Origin not approved: fail the result.
397 self.notifyError_({errorCode: ErrorCodes.BAD_REQUEST});
400 self.sendEnrollRequestToHelper_();
405 * Performs an enroll request with this instance's enroll and sign challenges,
406 * by encoding them into a helper request and passing the resulting request to
407 * the factory registry's helper.
410 Enroller.prototype.sendEnrollRequestToHelper_ = function() {
411 var encodedEnrollChallenges =
412 this.encodeEnrollChallenges_(this.enrollChallenges_, this.appId_);
413 // If the request didn't contain a sign challenge, provide one. The value
415 var defaultSignChallenge = '';
416 var encodedSignChallenges =
417 encodeSignChallenges(this.signChallenges_, defaultSignChallenge,
420 type: 'enroll_helper_request',
421 enrollChallenges: encodedEnrollChallenges,
422 signData: encodedSignChallenges,
423 logMsgUrl: this.logMsgUrl_
425 if (!this.timer_.expired()) {
426 request.timeout = this.timer_.millisecondsUntilExpired() / 1000.0;
427 request.timeoutSeconds = this.timer_.millisecondsUntilExpired() / 1000.0;
430 // Begin fetching/checking the app ids.
431 var enrollAppIds = [];
433 enrollAppIds.push(this.appId_);
435 for (var i = 0; i < this.enrollChallenges_.length; i++) {
436 if (this.enrollChallenges_[i].hasOwnProperty('appId')) {
437 enrollAppIds.push(this.enrollChallenges_[i]['appId']);
441 if (!enrollAppIds.length) {
442 console.warn(UTIL_fmt('empty enroll app ids?'));
443 this.notifyError_({errorCode: ErrorCodes.BAD_REQUEST});
447 this.checkAppIds_(enrollAppIds, function(result) {
448 if (self.done_) return;
450 self.handler_ = FACTORY_REGISTRY.getRequestHelper().getHandler(request);
453 /** @type {function(HelperReply)} */
454 (self.helperComplete_.bind(self));
455 self.handler_.run(helperComplete);
457 self.notifyError_({errorCode: ErrorCodes.OTHER_ERROR});
460 self.notifyError_({errorCode: ErrorCodes.BAD_REQUEST});
466 * Encodes the enroll challenge as an enroll helper challenge.
467 * @param {EnrollChallenge} enrollChallenge The enroll challenge to encode.
468 * @param {string=} opt_appId The app id for the entire request.
469 * @return {EnrollHelperChallenge} The encoded challenge.
472 Enroller.encodeEnrollChallenge_ = function(enrollChallenge, opt_appId) {
473 var encodedChallenge = {};
475 if (enrollChallenge['version']) {
476 version = enrollChallenge['version'];
478 // Version is implicitly V1 if not specified.
481 encodedChallenge['version'] = version;
482 encodedChallenge['challengeHash'] = enrollChallenge['challenge'];
484 if (enrollChallenge['appId']) {
485 appId = enrollChallenge['appId'];
490 // Sanity check. (Other code should fail if it's not set.)
491 console.warn(UTIL_fmt('No appId?'));
493 encodedChallenge['appIdHash'] = B64_encode(sha256HashOfString(appId));
494 return /** @type {EnrollHelperChallenge} */ (encodedChallenge);
498 * Encodes the given enroll challenges using this enroller's state.
499 * @param {Array.<EnrollChallenge>} enrollChallenges The enroll challenges.
500 * @param {string=} opt_appId The app id for the entire request.
501 * @return {!Array.<EnrollHelperChallenge>} The encoded enroll challenges.
504 Enroller.prototype.encodeEnrollChallenges_ = function(enrollChallenges,
507 for (var i = 0; i < enrollChallenges.length; i++) {
508 var enrollChallenge = enrollChallenges[i];
509 var version = enrollChallenge.version;
511 // Version is implicitly V1 if not specified.
515 if (version == 'U2F_V2') {
516 var modifiedChallenge = {};
517 for (var k in enrollChallenge) {
518 modifiedChallenge[k] = enrollChallenge[k];
520 // V2 enroll responses contain signatures over a browser data object,
521 // which we're constructing here. The browser data object contains, among
522 // other things, the server challenge.
523 var serverChallenge = enrollChallenge['challenge'];
524 var browserData = makeEnrollBrowserData(
525 serverChallenge, this.sender_.origin, this.sender_.tlsChannelId);
526 // Replace the challenge with the hash of the browser data.
527 modifiedChallenge['challenge'] =
528 B64_encode(sha256HashOfString(browserData));
529 this.browserData_[version] =
530 B64_encode(UTIL_StringToBytes(browserData));
531 challenges.push(Enroller.encodeEnrollChallenge_(
532 /** @type {EnrollChallenge} */ (modifiedChallenge), opt_appId));
535 Enroller.encodeEnrollChallenge_(enrollChallenge, opt_appId));
542 * Checks the app ids associated with this enroll request, and calls a callback
543 * with the result of the check.
544 * @param {!Array.<string>} enrollAppIds The app ids in the enroll challenge
545 * portion of the enroll request.
546 * @param {function(boolean)} cb Called with the result of the check.
549 Enroller.prototype.checkAppIds_ = function(enrollAppIds, cb) {
551 UTIL_unionArrays(enrollAppIds, getDistinctAppIds(this.signChallenges_));
552 FACTORY_REGISTRY.getOriginChecker()
553 .canClaimAppIds(this.sender_.origin, appIds)
554 .then(this.originChecked_.bind(this, appIds, cb));
558 * Called with the result of checking the origin. When the origin is allowed
559 * to claim the app ids, begins checking whether the app ids also list the
561 * @param {!Array.<string>} appIds The app ids.
562 * @param {function(boolean)} cb Called with the result of the check.
563 * @param {boolean} result Whether the origin could claim the app ids.
566 Enroller.prototype.originChecked_ = function(appIds, cb, result) {
568 this.notifyError_({errorCode: ErrorCodes.BAD_REQUEST});
571 /** @private {!AppIdChecker} */
572 this.appIdChecker_ = new AppIdChecker(FACTORY_REGISTRY.getTextFetcher(),
573 this.timer_.clone(), this.sender_.origin, appIds, this.allowHttp_,
575 this.appIdChecker_.doCheck().then(cb);
578 /** Closes this enroller. */
579 Enroller.prototype.close = function() {
580 if (this.appIdChecker_) {
581 this.appIdChecker_.close();
584 this.handler_.close();
585 this.handler_ = null;
591 * Notifies the caller with the error.
592 * @param {U2fError} error Error.
595 Enroller.prototype.notifyError_ = function(error) {
600 this.errorCb_(error);
604 * Notifies the caller of success with the provided response data.
605 * @param {string} u2fVersion Protocol version
606 * @param {string} info Response data
607 * @param {string|undefined} opt_browserData Browser data used
610 Enroller.prototype.notifySuccess_ =
611 function(u2fVersion, info, opt_browserData) {
616 this.successCb_(u2fVersion, info, opt_browserData);
620 * Called by the helper upon completion.
621 * @param {EnrollHelperReply} reply The result of the enroll request.
624 Enroller.prototype.helperComplete_ = function(reply) {
626 var reportedError = mapDeviceStatusCodeToU2fError(reply.code);
627 console.log(UTIL_fmt('helper reported ' + reply.code.toString(16) +
628 ', returning ' + reportedError.errorCode));
629 this.notifyError_(reportedError);
631 console.log(UTIL_fmt('Gnubby enrollment succeeded!!!!!'));
634 if (reply.version == 'U2F_V2') {
635 // For U2F_V2, the challenge sent to the gnubby is modified to be the hash
636 // of the browser data. Include the browser data.
637 browserData = this.browserData_[reply.version];
640 this.notifySuccess_(/** @type {string} */ (reply.version),
641 /** @type {string} */ (reply.enrollData),