Add ICU message format support
[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.
18  */
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);
27   }
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;
36     }
37     var responseData =
38         makeEnrollResponseData(enrollChallenge, u2fVersion,
39             'enrollData', info, 'browserData', browserData);
40     var response = makeWebSuccessResponse(request, responseData);
41     sendResponseOnce(sentResponse, closeable, response, sendResponse);
42   }
44   function timeout() {
45     sendErrorResponse({errorCode: ErrorCodes.TIMEOUT});
46   }
48   var sender = createSenderFromMessageSender(messageSender);
49   if (!sender) {
50     sendErrorResponse({errorCode: ErrorCodes.BAD_REQUEST});
51     return null;
52   }
53   if (sender.origin.indexOf('http://') == 0 && !HTTP_ORIGINS_ALLOWED) {
54     sendErrorResponse({errorCode: ErrorCodes.BAD_REQUEST});
55     return null;
56   }
58   if (!isValidEnrollRequest(request, 'enrollChallenges', 'signData')) {
59     sendErrorResponse({errorCode: ErrorCodes.BAD_REQUEST});
60     return null;
61   }
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.
96  */
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);
105   }
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;
114     }
115     var responseData =
116         makeEnrollResponseData(enrollChallenge, u2fVersion,
117             'registrationData', info, 'clientData', browserData);
118     var response = makeU2fSuccessResponse(request, responseData);
119     sendResponseOnce(sentResponse, closeable, response, sendResponse);
120   }
122   function timeout() {
123     sendErrorResponse({errorCode: ErrorCodes.TIMEOUT});
124   }
126   var sender = createSenderFromMessageSender(messageSender);
127   if (!sender) {
128     sendErrorResponse({errorCode: ErrorCodes.BAD_REQUEST});
129     return null;
130   }
131   if (sender.origin.indexOf('http://') == 0 && !HTTP_ORIGINS_ALLOWED) {
132     sendErrorResponse({errorCode: ErrorCodes.BAD_REQUEST});
133     return null;
134   }
136   if (!isValidEnrollRequest(request, 'registerRequests', 'signRequests',
137       'registeredKeys')) {
138     sendErrorResponse({errorCode: ErrorCodes.BAD_REQUEST});
139     return null;
140   }
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.
179  */
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;
203     }
204   }
205   return true;
209  * @typedef {{
210  *   version: (string|undefined),
211  *   challenge: string,
212  *   appId: string
213  * }}
214  */
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.
224  */
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';
233     }
234     if (version != 'U2F_V1' && version != 'U2F_V2') {
235       return false;
236     }
237     if (seenVersions[version]) {
238       // Each version can appear at most once.
239       return false;
240     }
241     seenVersions[version] = version;
242     if (appIdRequired && !enrollChallenge['appId']) {
243       return false;
244     }
245     if (!enrollChallenge['challenge']) {
246       // The challenge is required.
247       return false;
248     }
249   }
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.
261  */
262 function findEnrollChallengeOfVersion(enrollChallenges, version) {
263   for (var i = 0; i < enrollChallenges.length; i++) {
264     if (enrollChallenges[i]['version'] == version) {
265       return enrollChallenges[i];
266     }
267   }
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.
283  */
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];
291   }
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;
296   }
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>}
311  */
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];
320   }
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'] = '';
327       }
328     }
329   }
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
343  */
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.
376  */
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.
385  */
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});
401   });
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
408  */
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;
421           }
422           var newTimer = self.timer_.clone(self.notifyTimeout_.bind(self));
423           self.timer_.clearTimeout();
424           self.timer_ = newTimer;
425           return;
426         }
427         self.sendEnrollRequestToHelper_();
428       });
432  * Notifies the caller of a timeout error.
433  * @private
434  */
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
444  */
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_
459   };
460   if (!this.timer_.expired()) {
461     request.timeout = this.timer_.millisecondsUntilExpired() / 1000.0;
462     request.timeoutSeconds = this.timer_.millisecondsUntilExpired() / 1000.0;
463   }
465   // Begin fetching/checking the app ids.
466   var enrollAppIds = [];
467   if (this.appId_) {
468     enrollAppIds.push(this.appId_);
469   }
470   for (var i = 0; i < this.enrollChallenges_.length; i++) {
471     if (this.enrollChallenges_[i].hasOwnProperty('appId')) {
472       enrollAppIds.push(this.enrollChallenges_[i]['appId']);
473     }
474   }
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;
480   }
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});
493       }
494     } else {
495       self.notifyError_({errorCode: ErrorCodes.BAD_REQUEST});
496     }
497   });
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
506  */
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';
515   }
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;
523   }
524   if (!appId) {
525     // Sanity check. (Other code should fail if it's not set.)
526     console.warn(UTIL_fmt('No appId?'));
527   }
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
538  */
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';
548     }
550     if (version == 'U2F_V2') {
551       var modifiedChallenge = {};
552       for (var k in enrollChallenge) {
553         modifiedChallenge[k] = enrollChallenge[k];
554       }
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));
571     }
572   }
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
583  */
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
600  */
601 Enroller.prototype.originChecked_ = function(appIds, cb, result) {
602   if (!result) {
603     this.notifyError_({errorCode: ErrorCodes.BAD_REQUEST});
604     return;
605   }
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();
617   }
618   if (this.handler_) {
619     this.handler_.close();
620     this.handler_ = null;
621   }
622   this.done_ = true;
626  * Notifies the caller with the error.
627  * @param {U2fError} error Error.
628  * @private
629  */
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
644  */
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
658  */
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];
673     }
675     this.notifySuccess_(/** @type {string} */ (reply.version),
676                         /** @type {string} */ (reply.enrollData),
677                         browserData);
678   }