Allow only one bookmark to be added for multiple fast starring
[chromium-blink-merge.git] / chrome / browser / resources / cryptotoken / enroller.js
blob859ff6d045a551aaac3cc93e9f19f669f89246d0
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 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
17 * closes.
19 function handleWebEnrollRequest(messageSender, request, sendResponse) {
20 var sentResponse = false;
21 var closeable = null;
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'];
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,
39 'enrollData', info, 'browserData', browserData);
40 var response = makeWebSuccessResponse(request, responseData);
41 sendResponseOnce(sentResponse, closeable, response, sendResponse);
44 function timeout() {
45 sendErrorResponse({errorCode: ErrorCodes.TIMEOUT});
48 var sender = createSenderFromMessageSender(messageSender);
49 if (!sender) {
50 sendErrorResponse({errorCode: ErrorCodes.BAD_REQUEST});
51 return null;
53 if (sender.origin.indexOf('http://') == 0 && !HTTP_ORIGINS_ALLOWED) {
54 sendErrorResponse({errorCode: ErrorCodes.BAD_REQUEST});
55 return null;
58 if (!isValidEnrollRequest(request, 'enrollChallenges', 'signData')) {
59 sendErrorResponse({errorCode: ErrorCodes.BAD_REQUEST});
60 return null;
63 var timeoutValueSeconds = getTimeoutValueFromRequest(request);
64 // Attenuate watchdog timeout value less than the enroller's timeout, so the
65 // watchdog only fires after the enroller could reasonably have called back,
66 // not before.
67 var watchdogTimeoutValueSeconds = attenuateTimeoutInSeconds(
68 timeoutValueSeconds, MINIMUM_TIMEOUT_ATTENUATION_SECONDS / 2);
69 var watchdog = new WatchdogRequestHandler(watchdogTimeoutValueSeconds,
70 timeout);
71 var wrappedErrorCb = watchdog.wrapCallback(sendErrorResponse);
72 var wrappedSuccessCb = watchdog.wrapCallback(sendSuccessResponse);
74 var timer = createAttenuatedTimer(
75 FACTORY_REGISTRY.getCountdownFactory(), timeoutValueSeconds);
76 var logMsgUrl = request['logMsgUrl'];
77 var enroller = new Enroller(timer, sender, wrappedErrorCb, wrappedSuccessCb,
78 logMsgUrl);
79 watchdog.setCloseable(/** @type {!Closeable} */ (enroller));
80 closeable = watchdog;
82 var registerRequests = request['enrollChallenges'];
83 var signRequests = getSignRequestsFromEnrollRequest(request, 'signData');
84 enroller.doEnroll(registerRequests, signRequests, request['appId']);
86 return closeable;
89 /**
90 * Handles a U2F enroll request.
91 * @param {MessageSender} messageSender The message sender.
92 * @param {Object} request The web page's enroll request.
93 * @param {Function} sendResponse Called back with the result of the enroll.
94 * @return {Closeable} A handler object to be closed when the browser channel
95 * closes.
97 function handleU2fEnrollRequest(messageSender, request, sendResponse) {
98 var sentResponse = false;
99 var closeable = null;
101 function sendErrorResponse(error) {
102 var response = makeU2fErrorResponse(request, error.errorCode,
103 error.errorMessage);
104 sendResponseOnce(sentResponse, closeable, response, sendResponse);
107 function sendSuccessResponse(u2fVersion, info, browserData) {
108 var enrollChallenges = request['registerRequests'];
109 var enrollChallenge =
110 findEnrollChallengeOfVersion(enrollChallenges, u2fVersion);
111 if (!enrollChallenge) {
112 sendErrorResponse({errorCode: ErrorCodes.OTHER_ERROR});
113 return;
115 var responseData =
116 makeEnrollResponseData(enrollChallenge, u2fVersion,
117 'registrationData', info, 'clientData', browserData);
118 var response = makeU2fSuccessResponse(request, responseData);
119 sendResponseOnce(sentResponse, closeable, response, sendResponse);
122 function timeout() {
123 sendErrorResponse({errorCode: ErrorCodes.TIMEOUT});
126 var sender = createSenderFromMessageSender(messageSender);
127 if (!sender) {
128 sendErrorResponse({errorCode: ErrorCodes.BAD_REQUEST});
129 return null;
131 if (sender.origin.indexOf('http://') == 0 && !HTTP_ORIGINS_ALLOWED) {
132 sendErrorResponse({errorCode: ErrorCodes.BAD_REQUEST});
133 return null;
136 if (!isValidEnrollRequest(request, 'registerRequests', 'signRequests',
137 'registeredKeys')) {
138 sendErrorResponse({errorCode: ErrorCodes.BAD_REQUEST});
139 return null;
142 var timeoutValueSeconds = getTimeoutValueFromRequest(request);
143 // Attenuate watchdog timeout value less than the enroller's timeout, so the
144 // watchdog only fires after the enroller could reasonably have called back,
145 // not before.
146 var watchdogTimeoutValueSeconds = attenuateTimeoutInSeconds(
147 timeoutValueSeconds, MINIMUM_TIMEOUT_ATTENUATION_SECONDS / 2);
148 var watchdog = new WatchdogRequestHandler(watchdogTimeoutValueSeconds,
149 timeout);
150 var wrappedErrorCb = watchdog.wrapCallback(sendErrorResponse);
151 var wrappedSuccessCb = watchdog.wrapCallback(sendSuccessResponse);
153 var timer = createAttenuatedTimer(
154 FACTORY_REGISTRY.getCountdownFactory(), timeoutValueSeconds);
155 var logMsgUrl = request['logMsgUrl'];
156 var enroller = new Enroller(timer, sender, sendErrorResponse,
157 sendSuccessResponse, logMsgUrl);
158 watchdog.setCloseable(/** @type {!Closeable} */ (enroller));
159 closeable = watchdog;
161 var registerRequests = request['registerRequests'];
162 var signRequests = getSignRequestsFromEnrollRequest(request,
163 'signRequests', 'registeredKeys');
164 enroller.doEnroll(registerRequests, signRequests, request['appId']);
166 return closeable;
170 * Returns whether the request appears to be a valid enroll request.
171 * @param {Object} request The request.
172 * @param {string} enrollChallengesName The name of the enroll challenges value
173 * in the request.
174 * @param {string} signChallengesName The name of the sign challenges value in
175 * the request.
176 * @param {string=} opt_registeredKeysName The name of the registered keys
177 * value in the request.
178 * @return {boolean} Whether the request appears valid.
180 function isValidEnrollRequest(request, enrollChallengesName,
181 signChallengesName, opt_registeredKeysName) {
182 if (!request.hasOwnProperty(enrollChallengesName))
183 return false;
184 var enrollChallenges = request[enrollChallengesName];
185 if (!enrollChallenges.length)
186 return false;
187 var hasAppId = request.hasOwnProperty('appId');
188 if (!isValidEnrollChallengeArray(enrollChallenges, !hasAppId))
189 return false;
190 var signChallenges = request[signChallengesName];
191 // A missing sign challenge array is ok, in the case the user is not already
192 // enrolled.
193 // A challenge value need not necessarily be supplied with every challenge.
194 var challengeRequired = false;
195 if (signChallenges &&
196 !isValidSignChallengeArray(signChallenges, challengeRequired, !hasAppId))
197 return false;
198 if (opt_registeredKeysName) {
199 var registeredKeys = request[opt_registeredKeysName];
200 if (registeredKeys &&
201 !isValidRegisteredKeyArray(registeredKeys, !hasAppId)) {
202 return false;
205 return true;
209 * @typedef {{
210 * version: (string|undefined),
211 * challenge: string,
212 * appId: string
213 * }}
215 var EnrollChallenge;
218 * @param {Array<EnrollChallenge>} enrollChallenges The enroll challenges to
219 * validate.
220 * @param {boolean} appIdRequired Whether the appId property is required on
221 * each challenge.
222 * @return {boolean} Whether the given array of challenges is a valid enroll
223 * challenges array.
225 function isValidEnrollChallengeArray(enrollChallenges, appIdRequired) {
226 var seenVersions = {};
227 for (var i = 0; i < enrollChallenges.length; i++) {
228 var enrollChallenge = enrollChallenges[i];
229 var version = enrollChallenge['version'];
230 if (!version) {
231 // Version is implicitly V1 if not specified.
232 version = 'U2F_V1';
234 if (version != 'U2F_V1' && version != 'U2F_V2') {
235 return false;
237 if (seenVersions[version]) {
238 // Each version can appear at most once.
239 return false;
241 seenVersions[version] = version;
242 if (appIdRequired && !enrollChallenge['appId']) {
243 return false;
245 if (!enrollChallenge['challenge']) {
246 // The challenge is required.
247 return false;
250 return true;
254 * Finds the enroll challenge of the given version in the enroll challlenge
255 * array.
256 * @param {Array<EnrollChallenge>} enrollChallenges The enroll challenges to
257 * search.
258 * @param {string} version Version to search for.
259 * @return {?EnrollChallenge} The enroll challenge with the given versions, or
260 * null if it isn't found.
262 function findEnrollChallengeOfVersion(enrollChallenges, version) {
263 for (var i = 0; i < enrollChallenges.length; i++) {
264 if (enrollChallenges[i]['version'] == version) {
265 return enrollChallenges[i];
268 return null;
272 * Makes a responseData object for the enroll request with the given parameters.
273 * @param {EnrollChallenge} enrollChallenge The enroll challenge used to
274 * register.
275 * @param {string} u2fVersion Version of gnubby that enrolled.
276 * @param {string} enrollDataName The name of the enroll data key in the
277 * responseData object.
278 * @param {string} enrollData The enroll data.
279 * @param {string} browserDataName The name of the browser data key in the
280 * responseData object.
281 * @param {string=} browserData The browser data, if available.
282 * @return {Object} The responseData object.
284 function makeEnrollResponseData(enrollChallenge, u2fVersion, enrollDataName,
285 enrollData, browserDataName, browserData) {
286 var responseData = {};
287 responseData[enrollDataName] = enrollData;
288 // Echo the used challenge back in the reply.
289 for (var k in enrollChallenge) {
290 responseData[k] = enrollChallenge[k];
292 if (u2fVersion == 'U2F_V2') {
293 // For U2F_V2, the challenge sent to the gnubby is modified to be the
294 // hash of the browser data. Include the browser data.
295 responseData[browserDataName] = browserData;
297 return responseData;
301 * Gets the expanded sign challenges from an enroll request, potentially by
302 * modifying the request to contain a challenge value where one was omitted.
303 * (For enrolling, the server isn't interested in the value of a signature,
304 * only whether the presented key handle is already enrolled.)
305 * @param {Object} request The request.
306 * @param {string} signChallengesName The name of the sign challenges value in
307 * the request.
308 * @param {string=} opt_registeredKeysName The name of the registered keys
309 * value in the request.
310 * @return {Array<SignChallenge>}
312 function getSignRequestsFromEnrollRequest(request, signChallengesName,
313 opt_registeredKeysName) {
314 var signChallenges;
315 if (opt_registeredKeysName &&
316 request.hasOwnProperty(opt_registeredKeysName)) {
317 signChallenges = request[opt_registeredKeysName];
318 } else {
319 signChallenges = request[signChallengesName];
321 if (signChallenges) {
322 for (var i = 0; i < signChallenges.length; i++) {
323 // Make sure each sign challenge has a challenge value.
324 // The actual value doesn't matter, as long as it's a string.
325 if (!signChallenges[i].hasOwnProperty('challenge')) {
326 signChallenges[i]['challenge'] = '';
330 return signChallenges;
334 * Creates a new object to track enrolling with a gnubby.
335 * @param {!Countdown} timer Timer for enroll request.
336 * @param {!WebRequestSender} sender The sender of the request.
337 * @param {function(U2fError)} errorCb Called upon enroll failure.
338 * @param {function(string, string, (string|undefined))} successCb Called upon
339 * enroll success with the version of the succeeding gnubby, the enroll
340 * data, and optionally the browser data associated with the enrollment.
341 * @param {string=} opt_logMsgUrl The url to post log messages to.
342 * @constructor
344 function Enroller(timer, sender, errorCb, successCb, opt_logMsgUrl) {
345 /** @private {Countdown} */
346 this.timer_ = timer;
347 /** @private {WebRequestSender} */
348 this.sender_ = sender;
349 /** @private {function(U2fError)} */
350 this.errorCb_ = errorCb;
351 /** @private {function(string, string, (string|undefined))} */
352 this.successCb_ = successCb;
353 /** @private {string|undefined} */
354 this.logMsgUrl_ = opt_logMsgUrl;
356 /** @private {boolean} */
357 this.done_ = false;
359 /** @private {Object<string>} */
360 this.browserData_ = {};
361 /** @private {Array<EnrollHelperChallenge>} */
362 this.encodedEnrollChallenges_ = [];
363 /** @private {Array<SignHelperChallenge>} */
364 this.encodedSignChallenges_ = [];
365 // Allow http appIds for http origins. (Broken, but the caller deserves
366 // what they get.)
367 /** @private {boolean} */
368 this.allowHttp_ =
369 this.sender_.origin ? this.sender_.origin.indexOf('http://') == 0 : false;
370 /** @private {Closeable} */
371 this.handler_ = null;
375 * Default timeout value in case the caller never provides a valid timeout.
377 Enroller.DEFAULT_TIMEOUT_MILLIS = 30 * 1000;
380 * Performs an enroll request with the given enroll and sign challenges.
381 * @param {Array<EnrollChallenge>} enrollChallenges A set of enroll challenges.
382 * @param {Array<SignChallenge>} signChallenges A set of sign challenges for
383 * existing enrollments for this user and appId.
384 * @param {string=} opt_appId The app id for the entire request.
386 Enroller.prototype.doEnroll = function(enrollChallenges, signChallenges,
387 opt_appId) {
388 /** @private {Array<EnrollChallenge>} */
389 this.enrollChallenges_ = enrollChallenges;
390 /** @private {Array<SignChallenge>} */
391 this.signChallenges_ = signChallenges;
392 /** @private {(string|undefined)} */
393 this.appId_ = opt_appId;
394 var self = this;
395 getTabIdWhenPossible(this.sender_).then(function() {
396 if (self.done_) return;
397 self.approveOrigin_();
398 }, function() {
399 self.close();
400 self.notifyError_({errorCode: ErrorCodes.BAD_REQUEST});
405 * Ensures the user has approved this origin to use security keys, sending
406 * to the request to the handler if/when the user has done so.
407 * @private
409 Enroller.prototype.approveOrigin_ = function() {
410 var self = this;
411 FACTORY_REGISTRY.getApprovedOrigins()
412 .isApprovedOrigin(this.sender_.origin, this.sender_.tabId)
413 .then(function(result) {
414 if (self.done_) return;
415 if (!result) {
416 // Origin not approved: rather than give an explicit indication to
417 // the web page, let a timeout occur.
418 if (self.timer_.expired()) {
419 self.notifyTimeout_();
420 return;
422 var newTimer = self.timer_.clone(self.notifyTimeout_.bind(self));
423 self.timer_.clearTimeout();
424 self.timer_ = newTimer;
425 return;
427 self.sendEnrollRequestToHelper_();
432 * Notifies the caller of a timeout error.
433 * @private
435 Enroller.prototype.notifyTimeout_ = function() {
436 this.notifyError_({errorCode: ErrorCodes.TIMEOUT});
440 * Performs an enroll request with this instance's enroll and sign challenges,
441 * by encoding them into a helper request and passing the resulting request to
442 * the factory registry's helper.
443 * @private
445 Enroller.prototype.sendEnrollRequestToHelper_ = function() {
446 var encodedEnrollChallenges =
447 this.encodeEnrollChallenges_(this.enrollChallenges_, this.appId_);
448 // If the request didn't contain a sign challenge, provide one. The value
449 // doesn't matter.
450 var defaultSignChallenge = '';
451 var encodedSignChallenges =
452 encodeSignChallenges(this.signChallenges_, defaultSignChallenge,
453 this.appId_);
454 var request = {
455 type: 'enroll_helper_request',
456 enrollChallenges: encodedEnrollChallenges,
457 signData: encodedSignChallenges,
458 logMsgUrl: this.logMsgUrl_
460 if (!this.timer_.expired()) {
461 request.timeout = this.timer_.millisecondsUntilExpired() / 1000.0;
462 request.timeoutSeconds = this.timer_.millisecondsUntilExpired() / 1000.0;
465 // Begin fetching/checking the app ids.
466 var enrollAppIds = [];
467 if (this.appId_) {
468 enrollAppIds.push(this.appId_);
470 for (var i = 0; i < this.enrollChallenges_.length; i++) {
471 if (this.enrollChallenges_[i].hasOwnProperty('appId')) {
472 enrollAppIds.push(this.enrollChallenges_[i]['appId']);
475 // Sanity check
476 if (!enrollAppIds.length) {
477 console.warn(UTIL_fmt('empty enroll app ids?'));
478 this.notifyError_({errorCode: ErrorCodes.BAD_REQUEST});
479 return;
481 var self = this;
482 this.checkAppIds_(enrollAppIds, function(result) {
483 if (self.done_) return;
484 if (result) {
485 self.handler_ = FACTORY_REGISTRY.getRequestHelper().getHandler(request);
486 if (self.handler_) {
487 var helperComplete =
488 /** @type {function(HelperReply)} */
489 (self.helperComplete_.bind(self));
490 self.handler_.run(helperComplete);
491 } else {
492 self.notifyError_({errorCode: ErrorCodes.OTHER_ERROR});
494 } else {
495 self.notifyError_({errorCode: ErrorCodes.BAD_REQUEST});
501 * Encodes the enroll challenge as an enroll helper challenge.
502 * @param {EnrollChallenge} enrollChallenge The enroll challenge to encode.
503 * @param {string=} opt_appId The app id for the entire request.
504 * @return {EnrollHelperChallenge} The encoded challenge.
505 * @private
507 Enroller.encodeEnrollChallenge_ = function(enrollChallenge, opt_appId) {
508 var encodedChallenge = {};
509 var version;
510 if (enrollChallenge['version']) {
511 version = enrollChallenge['version'];
512 } else {
513 // Version is implicitly V1 if not specified.
514 version = 'U2F_V1';
516 encodedChallenge['version'] = version;
517 encodedChallenge['challengeHash'] = enrollChallenge['challenge'];
518 var appId;
519 if (enrollChallenge['appId']) {
520 appId = enrollChallenge['appId'];
521 } else {
522 appId = opt_appId;
524 if (!appId) {
525 // Sanity check. (Other code should fail if it's not set.)
526 console.warn(UTIL_fmt('No appId?'));
528 encodedChallenge['appIdHash'] = B64_encode(sha256HashOfString(appId));
529 return /** @type {EnrollHelperChallenge} */ (encodedChallenge);
533 * Encodes the given enroll challenges using this enroller's state.
534 * @param {Array<EnrollChallenge>} enrollChallenges The enroll challenges.
535 * @param {string=} opt_appId The app id for the entire request.
536 * @return {!Array<EnrollHelperChallenge>} The encoded enroll challenges.
537 * @private
539 Enroller.prototype.encodeEnrollChallenges_ = function(enrollChallenges,
540 opt_appId) {
541 var challenges = [];
542 for (var i = 0; i < enrollChallenges.length; i++) {
543 var enrollChallenge = enrollChallenges[i];
544 var version = enrollChallenge.version;
545 if (!version) {
546 // Version is implicitly V1 if not specified.
547 version = 'U2F_V1';
550 if (version == 'U2F_V2') {
551 var modifiedChallenge = {};
552 for (var k in enrollChallenge) {
553 modifiedChallenge[k] = enrollChallenge[k];
555 // V2 enroll responses contain signatures over a browser data object,
556 // which we're constructing here. The browser data object contains, among
557 // other things, the server challenge.
558 var serverChallenge = enrollChallenge['challenge'];
559 var browserData = makeEnrollBrowserData(
560 serverChallenge, this.sender_.origin, this.sender_.tlsChannelId);
561 // Replace the challenge with the hash of the browser data.
562 modifiedChallenge['challenge'] =
563 B64_encode(sha256HashOfString(browserData));
564 this.browserData_[version] =
565 B64_encode(UTIL_StringToBytes(browserData));
566 challenges.push(Enroller.encodeEnrollChallenge_(
567 /** @type {EnrollChallenge} */ (modifiedChallenge), opt_appId));
568 } else {
569 challenges.push(
570 Enroller.encodeEnrollChallenge_(enrollChallenge, opt_appId));
573 return challenges;
577 * Checks the app ids associated with this enroll request, and calls a callback
578 * with the result of the check.
579 * @param {!Array<string>} enrollAppIds The app ids in the enroll challenge
580 * portion of the enroll request.
581 * @param {function(boolean)} cb Called with the result of the check.
582 * @private
584 Enroller.prototype.checkAppIds_ = function(enrollAppIds, cb) {
585 var appIds =
586 UTIL_unionArrays(enrollAppIds, getDistinctAppIds(this.signChallenges_));
587 FACTORY_REGISTRY.getOriginChecker()
588 .canClaimAppIds(this.sender_.origin, appIds)
589 .then(this.originChecked_.bind(this, appIds, cb));
593 * Called with the result of checking the origin. When the origin is allowed
594 * to claim the app ids, begins checking whether the app ids also list the
595 * origin.
596 * @param {!Array<string>} appIds The app ids.
597 * @param {function(boolean)} cb Called with the result of the check.
598 * @param {boolean} result Whether the origin could claim the app ids.
599 * @private
601 Enroller.prototype.originChecked_ = function(appIds, cb, result) {
602 if (!result) {
603 this.notifyError_({errorCode: ErrorCodes.BAD_REQUEST});
604 return;
606 /** @private {!AppIdChecker} */
607 this.appIdChecker_ = new AppIdChecker(FACTORY_REGISTRY.getTextFetcher(),
608 this.timer_.clone(), this.sender_.origin, appIds, this.allowHttp_,
609 this.logMsgUrl_);
610 this.appIdChecker_.doCheck().then(cb);
613 /** Closes this enroller. */
614 Enroller.prototype.close = function() {
615 if (this.appIdChecker_) {
616 this.appIdChecker_.close();
618 if (this.handler_) {
619 this.handler_.close();
620 this.handler_ = null;
622 this.done_ = true;
626 * Notifies the caller with the error.
627 * @param {U2fError} error Error.
628 * @private
630 Enroller.prototype.notifyError_ = function(error) {
631 if (this.done_)
632 return;
633 this.close();
634 this.done_ = true;
635 this.errorCb_(error);
639 * Notifies the caller of success with the provided response data.
640 * @param {string} u2fVersion Protocol version
641 * @param {string} info Response data
642 * @param {string|undefined} opt_browserData Browser data used
643 * @private
645 Enroller.prototype.notifySuccess_ =
646 function(u2fVersion, info, opt_browserData) {
647 if (this.done_)
648 return;
649 this.close();
650 this.done_ = true;
651 this.successCb_(u2fVersion, info, opt_browserData);
655 * Called by the helper upon completion.
656 * @param {EnrollHelperReply} reply The result of the enroll request.
657 * @private
659 Enroller.prototype.helperComplete_ = function(reply) {
660 if (reply.code) {
661 var reportedError = mapDeviceStatusCodeToU2fError(reply.code);
662 console.log(UTIL_fmt('helper reported ' + reply.code.toString(16) +
663 ', returning ' + reportedError.errorCode));
664 this.notifyError_(reportedError);
665 } else {
666 console.log(UTIL_fmt('Gnubby enrollment succeeded!!!!!'));
667 var browserData;
669 if (reply.version == 'U2F_V2') {
670 // For U2F_V2, the challenge sent to the gnubby is modified to be the hash
671 // of the browser data. Include the browser data.
672 browserData = this.browserData_[reply.version];
675 this.notifySuccess_(/** @type {string} */ (reply.version),
676 /** @type {string} */ (reply.enrollData),
677 browserData);