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} sender The sender of the message.
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(sender, 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(ErrorCodes.OTHER_ERROR);
38 makeEnrollResponseData(enrollChallenge, u2fVersion,
39 'enrollData', info, 'browserData', browserData);
40 var response = makeWebSuccessResponse(request, responseData);
41 sendResponseOnce(sentResponse, closeable, response, sendResponse);
45 validateEnrollRequest(
46 sender, request, 'enrollChallenges', 'signData',
47 sendErrorResponse, sendSuccessResponse);
49 var registerRequests = request['enrollChallenges'];
50 var signRequests = getSignRequestsFromEnrollRequest(request, 'signData');
51 closeable = /** @type {Closeable} */ (enroller);
52 enroller.doEnroll(registerRequests, signRequests, request['appId']);
58 * Handles a U2F enroll request.
59 * @param {MessageSender} sender The sender of the message.
60 * @param {Object} request The web page's enroll request.
61 * @param {Function} sendResponse Called back with the result of the enroll.
62 * @return {Closeable} A handler object to be closed when the browser channel
65 function handleU2fEnrollRequest(sender, request, sendResponse) {
66 var sentResponse = false;
69 function sendErrorResponse(error) {
70 var response = makeU2fErrorResponse(request, error.errorCode,
72 sendResponseOnce(sentResponse, closeable, response, sendResponse);
75 function sendSuccessResponse(u2fVersion, info, browserData) {
76 var enrollChallenges = request['registerRequests'];
78 findEnrollChallengeOfVersion(enrollChallenges, u2fVersion);
79 if (!enrollChallenge) {
80 sendErrorResponse(ErrorCodes.OTHER_ERROR);
84 makeEnrollResponseData(enrollChallenge, u2fVersion,
85 'registrationData', info, 'clientData', browserData);
86 var response = makeU2fSuccessResponse(request, responseData);
87 sendResponseOnce(sentResponse, closeable, response, sendResponse);
91 validateEnrollRequest(
92 sender, request, 'registerRequests', 'signRequests',
93 sendErrorResponse, sendSuccessResponse, 'registeredKeys');
95 var registerRequests = request['registerRequests'];
96 var signRequests = getSignRequestsFromEnrollRequest(request,
97 'signRequests', 'registeredKeys');
98 closeable = /** @type {Closeable} */ (enroller);
99 enroller.doEnroll(registerRequests, signRequests, request['appId']);
105 * Validates an enroll request using the given parameters.
106 * @param {MessageSender} sender The sender of the message.
107 * @param {Object} request The web page's enroll request.
108 * @param {string} enrollChallengesName The name of the enroll challenges value
110 * @param {string} signChallengesName The name of the sign challenges value in
112 * @param {function(U2fError)} errorCb Error callback.
113 * @param {function(string, string, (string|undefined))} successCb Success
115 * @param {string=} opt_registeredKeysName The name of the registered keys
116 * value in the request.
117 * @return {Enroller} Enroller object representing the request, if the request
118 * is valid, or null if the request is invalid.
120 function validateEnrollRequest(sender, request,
121 enrollChallengesName, signChallengesName, errorCb, successCb,
122 opt_registeredKeysName) {
123 var origin = getOriginFromUrl(/** @type {string} */ (sender.url));
125 errorCb({errorCode: ErrorCodes.BAD_REQUEST});
129 if (!isValidEnrollRequest(request, enrollChallengesName,
130 signChallengesName, opt_registeredKeysName)) {
131 errorCb({errorCode: ErrorCodes.BAD_REQUEST});
135 var timer = createTimerForRequest(
136 FACTORY_REGISTRY.getCountdownFactory(), request);
137 var logMsgUrl = request['logMsgUrl'];
138 var enroller = new Enroller(timer, origin, errorCb, successCb,
139 sender.tlsChannelId, logMsgUrl);
144 * Returns whether the request appears to be a valid enroll request.
145 * @param {Object} request The request.
146 * @param {string} enrollChallengesName The name of the enroll challenges value
148 * @param {string} signChallengesName The name of the sign challenges value in
150 * @param {string=} opt_registeredKeysName The name of the registered keys
151 * value in the request.
152 * @return {boolean} Whether the request appears valid.
154 function isValidEnrollRequest(request, enrollChallengesName,
155 signChallengesName, opt_registeredKeysName) {
156 if (!request.hasOwnProperty(enrollChallengesName))
158 var enrollChallenges = request[enrollChallengesName];
159 if (!enrollChallenges.length)
161 var hasAppId = request.hasOwnProperty('appId');
162 if (!isValidEnrollChallengeArray(enrollChallenges, !hasAppId))
164 var signChallenges = request[signChallengesName];
165 // A missing sign challenge array is ok, in the case the user is not already
167 if (signChallenges && !isValidSignChallengeArray(signChallenges, !hasAppId))
169 if (opt_registeredKeysName) {
170 var registeredKeys = request[opt_registeredKeysName];
171 if (registeredKeys &&
172 !isValidRegisteredKeyArray(registeredKeys, !hasAppId)) {
181 * version: (string|undefined),
189 * @param {Array.<EnrollChallenge>} enrollChallenges The enroll challenges to
191 * @param {boolean} appIdRequired Whether the appId property is required on
193 * @return {boolean} Whether the given array of challenges is a valid enroll
196 function isValidEnrollChallengeArray(enrollChallenges, appIdRequired) {
197 var seenVersions = {};
198 for (var i = 0; i < enrollChallenges.length; i++) {
199 var enrollChallenge = enrollChallenges[i];
200 var version = enrollChallenge['version'];
202 // Version is implicitly V1 if not specified.
205 if (version != 'U2F_V1' && version != 'U2F_V2') {
208 if (seenVersions[version]) {
209 // Each version can appear at most once.
212 seenVersions[version] = version;
213 if (appIdRequired && !enrollChallenge['appId']) {
216 if (!enrollChallenge['challenge']) {
217 // The challenge is required.
225 * Finds the enroll challenge of the given version in the enroll challlenge
227 * @param {Array.<EnrollChallenge>} enrollChallenges The enroll challenges to
229 * @param {string} version Version to search for.
230 * @return {?EnrollChallenge} The enroll challenge with the given versions, or
231 * null if it isn't found.
233 function findEnrollChallengeOfVersion(enrollChallenges, version) {
234 for (var i = 0; i < enrollChallenges.length; i++) {
235 if (enrollChallenges[i]['version'] == version) {
236 return enrollChallenges[i];
243 * Makes a responseData object for the enroll request with the given parameters.
244 * @param {EnrollChallenge} enrollChallenge The enroll challenge used to
246 * @param {string} u2fVersion Version of gnubby that enrolled.
247 * @param {string} enrollDataName The name of the enroll data key in the
248 * responseData object.
249 * @param {string} enrollData The enroll data.
250 * @param {string} browserDataName The name of the browser data key in the
251 * responseData object.
252 * @param {string=} browserData The browser data, if available.
253 * @return {Object} The responseData object.
255 function makeEnrollResponseData(enrollChallenge, u2fVersion, enrollDataName,
256 enrollData, browserDataName, browserData) {
257 var responseData = {};
258 responseData[enrollDataName] = enrollData;
259 // Echo the used challenge back in the reply.
260 for (var k in enrollChallenge) {
261 responseData[k] = enrollChallenge[k];
263 if (u2fVersion == 'U2F_V2') {
264 // For U2F_V2, the challenge sent to the gnubby is modified to be the
265 // hash of the browser data. Include the browser data.
266 responseData[browserDataName] = browserData;
272 * Gets the expanded sign challenges from an enroll request, potentially by
273 * modifying the request to contain a challenge value where one was omitted.
274 * (For enrolling, the server isn't interested in the value of a signature,
275 * only whether the presented key handle is already enrolled.)
276 * @param {Object} request The request.
277 * @param {string} signChallengesName The name of the sign challenges value in
279 * @param {string=} opt_registeredKeysName The name of the registered keys
280 * value in the request.
281 * @return {Array.<SignChallenge>}
283 function getSignRequestsFromEnrollRequest(request, signChallengesName,
284 opt_registeredKeysName) {
286 if (opt_registeredKeysName &&
287 request.hasOwnProperty(opt_registeredKeysName)) {
288 // Convert registered keys to sign challenges by adding a challenge value.
289 signChallenges = request[opt_registeredKeysName];
290 for (var i = 0; i < signChallenges.length; i++) {
291 // The actual value doesn't matter, as long as it's a string.
292 signChallenges[i]['challenge'] = '';
295 signChallenges = request[signChallengesName];
297 return signChallenges;
301 * Creates a new object to track enrolling with a gnubby.
302 * @param {!Countdown} timer Timer for enroll request.
303 * @param {string} origin The origin making the request.
304 * @param {function(U2fError)} errorCb Called upon enroll failure.
305 * @param {function(string, string, (string|undefined))} successCb Called upon
306 * enroll success with the version of the succeeding gnubby, the enroll
307 * data, and optionally the browser data associated with the enrollment.
308 * @param {string=} opt_tlsChannelId the TLS channel ID, if any, of the origin
309 * making the request.
310 * @param {string=} opt_logMsgUrl The url to post log messages to.
313 function Enroller(timer, origin, errorCb, successCb, opt_tlsChannelId,
315 /** @private {Countdown} */
317 /** @private {string} */
318 this.origin_ = origin;
319 /** @private {function(U2fError)} */
320 this.errorCb_ = errorCb;
321 /** @private {function(string, string, (string|undefined))} */
322 this.successCb_ = successCb;
323 /** @private {string|undefined} */
324 this.tlsChannelId_ = opt_tlsChannelId;
325 /** @private {string|undefined} */
326 this.logMsgUrl_ = opt_logMsgUrl;
328 /** @private {boolean} */
331 /** @private {Object.<string, string>} */
332 this.browserData_ = {};
333 /** @private {Array.<EnrollHelperChallenge>} */
334 this.encodedEnrollChallenges_ = [];
335 /** @private {Array.<SignHelperChallenge>} */
336 this.encodedSignChallenges_ = [];
337 // Allow http appIds for http origins. (Broken, but the caller deserves
339 /** @private {boolean} */
340 this.allowHttp_ = this.origin_ ? this.origin_.indexOf('http://') == 0 : false;
341 /** @private {Closeable} */
342 this.handler_ = null;
346 * Default timeout value in case the caller never provides a valid timeout.
348 Enroller.DEFAULT_TIMEOUT_MILLIS = 30 * 1000;
351 * Performs an enroll request with the given enroll and sign challenges.
352 * @param {Array.<EnrollChallenge>} enrollChallenges A set of enroll challenges.
353 * @param {Array.<SignChallenge>} signChallenges A set of sign challenges for
354 * existing enrollments for this user and appId.
355 * @param {string=} opt_appId The app id for the entire request.
357 Enroller.prototype.doEnroll = function(enrollChallenges, signChallenges,
359 var encodedEnrollChallenges =
360 this.encodeEnrollChallenges_(enrollChallenges, opt_appId);
361 var encodedSignChallenges = encodeSignChallenges(signChallenges, opt_appId);
363 type: 'enroll_helper_request',
364 enrollChallenges: encodedEnrollChallenges,
365 signData: encodedSignChallenges,
366 logMsgUrl: this.logMsgUrl_
368 if (!this.timer_.expired()) {
369 request.timeout = this.timer_.millisecondsUntilExpired() / 1000.0;
370 request.timeoutSeconds = this.timer_.millisecondsUntilExpired() / 1000.0;
373 // Begin fetching/checking the app ids.
374 var enrollAppIds = [];
376 enrollAppIds.push(opt_appId);
378 for (var i = 0; i < enrollChallenges.length; i++) {
379 if (enrollChallenges[i].hasOwnProperty('appId')) {
380 enrollAppIds.push(enrollChallenges[i]['appId']);
384 if (!enrollAppIds.length) {
385 console.warn(UTIL_fmt('empty enroll app ids?'));
386 this.notifyError_({errorCode: ErrorCodes.BAD_REQUEST});
390 this.checkAppIds_(enrollAppIds, signChallenges, function(result) {
392 self.handler_ = FACTORY_REGISTRY.getRequestHelper().getHandler(request);
395 /** @type {function(HelperReply)} */
396 (self.helperComplete_.bind(self));
397 self.handler_.run(helperComplete);
399 self.notifyError_({errorCode: ErrorCodes.OTHER_ERROR});
402 self.notifyError_({errorCode: ErrorCodes.BAD_REQUEST});
408 * Encodes the enroll challenge as an enroll helper challenge.
409 * @param {EnrollChallenge} enrollChallenge The enroll challenge to encode.
410 * @param {string=} opt_appId The app id for the entire request.
411 * @return {EnrollHelperChallenge} The encoded challenge.
414 Enroller.encodeEnrollChallenge_ = function(enrollChallenge, opt_appId) {
415 var encodedChallenge = {};
417 if (enrollChallenge['version']) {
418 version = enrollChallenge['version'];
420 // Version is implicitly V1 if not specified.
423 encodedChallenge['version'] = version;
424 encodedChallenge['challengeHash'] = enrollChallenge['challenge'];
426 if (enrollChallenge['appId']) {
427 appId = enrollChallenge['appId'];
432 // Sanity check. (Other code should fail if it's not set.)
433 console.warn(UTIL_fmt('No appId?'));
435 encodedChallenge['appIdHash'] = B64_encode(sha256HashOfString(appId));
436 return /** @type {EnrollHelperChallenge} */ (encodedChallenge);
440 * Encodes the given enroll challenges using this enroller's state.
441 * @param {Array.<EnrollChallenge>} enrollChallenges The enroll challenges.
442 * @param {string=} opt_appId The app id for the entire request.
443 * @return {!Array.<EnrollHelperChallenge>} The encoded enroll challenges.
446 Enroller.prototype.encodeEnrollChallenges_ = function(enrollChallenges,
449 for (var i = 0; i < enrollChallenges.length; i++) {
450 var enrollChallenge = enrollChallenges[i];
451 var version = enrollChallenge.version;
453 // Version is implicitly V1 if not specified.
457 if (version == 'U2F_V2') {
458 var modifiedChallenge = {};
459 for (var k in enrollChallenge) {
460 modifiedChallenge[k] = enrollChallenge[k];
462 // V2 enroll responses contain signatures over a browser data object,
463 // which we're constructing here. The browser data object contains, among
464 // other things, the server challenge.
465 var serverChallenge = enrollChallenge['challenge'];
466 var browserData = makeEnrollBrowserData(
467 serverChallenge, this.origin_, this.tlsChannelId_);
468 // Replace the challenge with the hash of the browser data.
469 modifiedChallenge['challenge'] =
470 B64_encode(sha256HashOfString(browserData));
471 this.browserData_[version] =
472 B64_encode(UTIL_StringToBytes(browserData));
473 challenges.push(Enroller.encodeEnrollChallenge_(
474 /** @type {EnrollChallenge} */ (modifiedChallenge), opt_appId));
477 Enroller.encodeEnrollChallenge_(enrollChallenge, opt_appId));
484 * Checks the app ids associated with this enroll request, and calls a callback
485 * with the result of the check.
486 * @param {!Array.<string>} enrollAppIds The app ids in the enroll challenge
487 * portion of the enroll request.
488 * @param {Array.<SignChallenge>} signChallenges The sign challenges associated
490 * @param {function(boolean)} cb Called with the result of the check.
493 Enroller.prototype.checkAppIds_ = function(enrollAppIds, signChallenges, cb) {
495 UTIL_unionArrays(enrollAppIds, getDistinctAppIds(signChallenges));
496 FACTORY_REGISTRY.getOriginChecker().canClaimAppIds(this.origin_, appIds)
497 .then(this.originChecked_.bind(this, appIds, cb));
501 * Called with the result of checking the origin. When the origin is allowed
502 * to claim the app ids, begins checking whether the app ids also list the
504 * @param {!Array.<string>} appIds The app ids.
505 * @param {function(boolean)} cb Called with the result of the check.
506 * @param {boolean} result Whether the origin could claim the app ids.
509 Enroller.prototype.originChecked_ = function(appIds, cb, result) {
511 this.notifyError_({errorCode: ErrorCodes.BAD_REQUEST});
514 /** @private {!AppIdChecker} */
515 this.appIdChecker_ = new AppIdChecker(FACTORY_REGISTRY.getTextFetcher(),
516 this.timer_.clone(), this.origin_, appIds, this.allowHttp_,
518 this.appIdChecker_.doCheck().then(cb);
521 /** Closes this enroller. */
522 Enroller.prototype.close = function() {
523 if (this.appIdChecker_) {
524 this.appIdChecker_.close();
527 this.handler_.close();
528 this.handler_ = null;
533 * Notifies the caller with the error.
534 * @param {U2fError} error Error.
537 Enroller.prototype.notifyError_ = function(error) {
542 this.errorCb_(error);
546 * Notifies the caller of success with the provided response data.
547 * @param {string} u2fVersion Protocol version
548 * @param {string} info Response data
549 * @param {string|undefined} opt_browserData Browser data used
552 Enroller.prototype.notifySuccess_ =
553 function(u2fVersion, info, opt_browserData) {
558 this.successCb_(u2fVersion, info, opt_browserData);
562 * Called by the helper upon completion.
563 * @param {EnrollHelperReply} reply The result of the enroll request.
566 Enroller.prototype.helperComplete_ = function(reply) {
568 var reportedError = mapDeviceStatusCodeToU2fError(reply.code);
569 console.log(UTIL_fmt('helper reported ' + reply.code.toString(16) +
570 ', returning ' + reportedError.errorCode));
571 this.notifyError_(reportedError);
573 console.log(UTIL_fmt('Gnubby enrollment succeeded!!!!!'));
576 if (reply.version == 'U2F_V2') {
577 // For U2F_V2, the challenge sent to the gnubby is modified to be the hash
578 // of the browser data. Include the browser data.
579 browserData = this.browserData_[reply.version];
582 this.notifySuccess_(/** @type {string} */ (reply.version),
583 /** @type {string} */ (reply.enrollData),