Update To 11.40.268.0
[platform/framework/web/crosswalk.git] / src / chrome / browser / resources / cryptotoken / enroller.js
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.
4
5 /**
6  * @fileoverview Handles web page requests for gnubby enrollment.
7  */
8
9 'use strict';
10
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;
22
23   function sendErrorResponse(error) {
24     var response = makeWebErrorResponse(request,
25         mapErrorCodeToGnubbyCodeType(error.errorCode, false /* forSign */));
26     sendResponseOnce(sentResponse, closeable, response, sendResponse);
27   }
28
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   }
43
44   var sender = createSenderFromMessageSender(messageSender);
45   if (!sender) {
46     sendErrorResponse({errorCode: ErrorCodes.BAD_REQUEST});
47     return null;
48   }
49
50   var enroller =
51       validateEnrollRequest(
52           sender, request, 'enrollChallenges', 'signData',
53           sendErrorResponse, sendSuccessResponse);
54   if (enroller) {
55     var registerRequests = request['enrollChallenges'];
56     var signRequests = getSignRequestsFromEnrollRequest(request, 'signData');
57     closeable = /** @type {Closeable} */ (enroller);
58     enroller.doEnroll(registerRequests, signRequests, request['appId']);
59   }
60   return closeable;
61 }
62
63 /**
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
69  *     closes.
70  */
71 function handleU2fEnrollRequest(messageSender, request, sendResponse) {
72   var sentResponse = false;
73   var closeable = null;
74
75   function sendErrorResponse(error) {
76     var response = makeU2fErrorResponse(request, error.errorCode,
77         error.errorMessage);
78     sendResponseOnce(sentResponse, closeable, response, sendResponse);
79   }
80
81   function sendSuccessResponse(u2fVersion, info, browserData) {
82     var enrollChallenges = request['registerRequests'];
83     var enrollChallenge =
84         findEnrollChallengeOfVersion(enrollChallenges, u2fVersion);
85     if (!enrollChallenge) {
86       sendErrorResponse({errorCode: ErrorCodes.OTHER_ERROR});
87       return;
88     }
89     var responseData =
90         makeEnrollResponseData(enrollChallenge, u2fVersion,
91             'registrationData', info, 'clientData', browserData);
92     var response = makeU2fSuccessResponse(request, responseData);
93     sendResponseOnce(sentResponse, closeable, response, sendResponse);
94   }
95
96   var sender = createSenderFromMessageSender(messageSender);
97   if (!sender) {
98     sendErrorResponse({errorCode: ErrorCodes.BAD_REQUEST});
99     return null;
100   }
101
102   var enroller =
103       validateEnrollRequest(
104           sender, request, 'registerRequests', 'signRequests',
105           sendErrorResponse, sendSuccessResponse, 'registeredKeys');
106   if (enroller) {
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']);
112   }
113   return closeable;
114 }
115
116 /**
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
121  *     in the request.
122  * @param {string} signChallengesName The name of the sign challenges value in
123  *     the request.
124  * @param {function(U2fError)} errorCb Error callback.
125  * @param {function(string, string, (string|undefined))} successCb Success
126  *     callback.
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.
131  */
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});
138     return null;
139   }
140
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);
146   return enroller;
147 }
148
149 /**
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
153  *     in the request.
154  * @param {string} signChallengesName The name of the sign challenges value in
155  *     the request.
156  * @param {string=} opt_registeredKeysName The name of the registered keys
157  *     value in the request.
158  * @return {boolean} Whether the request appears valid.
159  */
160 function isValidEnrollRequest(request, enrollChallengesName,
161     signChallengesName, opt_registeredKeysName) {
162   if (!request.hasOwnProperty(enrollChallengesName))
163     return false;
164   var enrollChallenges = request[enrollChallengesName];
165   if (!enrollChallenges.length)
166     return false;
167   var hasAppId = request.hasOwnProperty('appId');
168   if (!isValidEnrollChallengeArray(enrollChallenges, !hasAppId))
169     return false;
170   var signChallenges = request[signChallengesName];
171   // A missing sign challenge array is ok, in the case the user is not already
172   // enrolled.
173   // A challenge value need not necessarily be supplied with every challenge.
174   var challengeRequired = false;
175   if (signChallenges &&
176       !isValidSignChallengeArray(signChallenges, challengeRequired, !hasAppId))
177     return false;
178   if (opt_registeredKeysName) {
179     var registeredKeys = request[opt_registeredKeysName];
180     if (registeredKeys &&
181         !isValidRegisteredKeyArray(registeredKeys, !hasAppId)) {
182       return false;
183     }
184   }
185   return true;
186 }
187
188 /**
189  * @typedef {{
190  *   version: (string|undefined),
191  *   challenge: string,
192  *   appId: string
193  * }}
194  */
195 var EnrollChallenge;
196
197 /**
198  * @param {Array.<EnrollChallenge>} enrollChallenges The enroll challenges to
199  *     validate.
200  * @param {boolean} appIdRequired Whether the appId property is required on
201  *     each challenge.
202  * @return {boolean} Whether the given array of challenges is a valid enroll
203  *     challenges array.
204  */
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'];
210     if (!version) {
211       // Version is implicitly V1 if not specified.
212       version = 'U2F_V1';
213     }
214     if (version != 'U2F_V1' && version != 'U2F_V2') {
215       return false;
216     }
217     if (seenVersions[version]) {
218       // Each version can appear at most once.
219       return false;
220     }
221     seenVersions[version] = version;
222     if (appIdRequired && !enrollChallenge['appId']) {
223       return false;
224     }
225     if (!enrollChallenge['challenge']) {
226       // The challenge is required.
227       return false;
228     }
229   }
230   return true;
231 }
232
233 /**
234  * Finds the enroll challenge of the given version in the enroll challlenge
235  * array.
236  * @param {Array.<EnrollChallenge>} enrollChallenges The enroll challenges to
237  *     search.
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.
241  */
242 function findEnrollChallengeOfVersion(enrollChallenges, version) {
243   for (var i = 0; i < enrollChallenges.length; i++) {
244     if (enrollChallenges[i]['version'] == version) {
245       return enrollChallenges[i];
246     }
247   }
248   return null;
249 }
250
251 /**
252  * Makes a responseData object for the enroll request with the given parameters.
253  * @param {EnrollChallenge} enrollChallenge The enroll challenge used to
254  *     register.
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.
263  */
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];
271   }
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;
276   }
277   return responseData;
278 }
279
280 /**
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
287  *     the request.
288  * @param {string=} opt_registeredKeysName The name of the registered keys
289  *     value in the request.
290  * @return {Array.<SignChallenge>}
291  */
292 function getSignRequestsFromEnrollRequest(request, signChallengesName,
293     opt_registeredKeysName) {
294   var signChallenges;
295   if (opt_registeredKeysName &&
296       request.hasOwnProperty(opt_registeredKeysName)) {
297     signChallenges = request[opt_registeredKeysName];
298   } else {
299     signChallenges = request[signChallengesName];
300   }
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'] = '';
307       }
308     }
309   }
310   return signChallenges;
311 }
312
313 /**
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.
322  * @constructor
323  */
324 function Enroller(timer, sender, errorCb, successCb, opt_logMsgUrl) {
325   /** @private {Countdown} */
326   this.timer_ = timer;
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;
335
336   /** @private {boolean} */
337   this.done_ = false;
338
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
346   // what they get.)
347   /** @private {boolean} */
348   this.allowHttp_ =
349       this.sender_.origin ? this.sender_.origin.indexOf('http://') == 0 : false;
350   /** @private {Closeable} */
351   this.handler_ = null;
352 }
353
354 /**
355  * Default timeout value in case the caller never provides a valid timeout.
356  */
357 Enroller.DEFAULT_TIMEOUT_MILLIS = 30 * 1000;
358
359 /**
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.
365  */
366 Enroller.prototype.doEnroll = function(enrollChallenges, signChallenges,
367     opt_appId) {
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;
374   var self = this;
375   getTabIdWhenPossible(this.sender_).then(function() {
376     if (self.done_) return;
377     self.approveOrigin_();
378   }, function() {
379     self.close();
380     self.notifyError_({errorCode: ErrorCodes.BAD_REQUEST});
381   });
382 };
383
384 /**
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.
387  * @private
388  */
389 Enroller.prototype.approveOrigin_ = function() {
390   var self = this;
391   FACTORY_REGISTRY.getApprovedOrigins()
392       .isApprovedOrigin(this.sender_.origin, this.sender_.tabId)
393       .then(function(result) {
394         if (self.done_) return;
395         if (!result) {
396           // Origin not approved: fail the result.
397           self.notifyError_({errorCode: ErrorCodes.BAD_REQUEST});
398           return;
399         }
400         self.sendEnrollRequestToHelper_();
401       });
402 };
403
404 /**
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.
408  * @private
409  */
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
414   // doesn't matter.
415   var defaultSignChallenge = '';
416   var encodedSignChallenges =
417       encodeSignChallenges(this.signChallenges_, defaultSignChallenge,
418           this.appId_);
419   var request = {
420     type: 'enroll_helper_request',
421     enrollChallenges: encodedEnrollChallenges,
422     signData: encodedSignChallenges,
423     logMsgUrl: this.logMsgUrl_
424   };
425   if (!this.timer_.expired()) {
426     request.timeout = this.timer_.millisecondsUntilExpired() / 1000.0;
427     request.timeoutSeconds = this.timer_.millisecondsUntilExpired() / 1000.0;
428   }
429
430   // Begin fetching/checking the app ids.
431   var enrollAppIds = [];
432   if (this.appId_) {
433     enrollAppIds.push(this.appId_);
434   }
435   for (var i = 0; i < this.enrollChallenges_.length; i++) {
436     if (this.enrollChallenges_[i].hasOwnProperty('appId')) {
437       enrollAppIds.push(this.enrollChallenges_[i]['appId']);
438     }
439   }
440   // Sanity check
441   if (!enrollAppIds.length) {
442     console.warn(UTIL_fmt('empty enroll app ids?'));
443     this.notifyError_({errorCode: ErrorCodes.BAD_REQUEST});
444     return;
445   }
446   var self = this;
447   this.checkAppIds_(enrollAppIds, function(result) {
448     if (self.done_) return;
449     if (result) {
450       self.handler_ = FACTORY_REGISTRY.getRequestHelper().getHandler(request);
451       if (self.handler_) {
452         var helperComplete =
453             /** @type {function(HelperReply)} */
454             (self.helperComplete_.bind(self));
455         self.handler_.run(helperComplete);
456       } else {
457         self.notifyError_({errorCode: ErrorCodes.OTHER_ERROR});
458       }
459     } else {
460       self.notifyError_({errorCode: ErrorCodes.BAD_REQUEST});
461     }
462   });
463 };
464
465 /**
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.
470  * @private
471  */
472 Enroller.encodeEnrollChallenge_ = function(enrollChallenge, opt_appId) {
473   var encodedChallenge = {};
474   var version;
475   if (enrollChallenge['version']) {
476     version = enrollChallenge['version'];
477   } else {
478     // Version is implicitly V1 if not specified.
479     version = 'U2F_V1';
480   }
481   encodedChallenge['version'] = version;
482   encodedChallenge['challengeHash'] = enrollChallenge['challenge'];
483   var appId;
484   if (enrollChallenge['appId']) {
485     appId = enrollChallenge['appId'];
486   } else {
487     appId = opt_appId;
488   }
489   if (!appId) {
490     // Sanity check. (Other code should fail if it's not set.)
491     console.warn(UTIL_fmt('No appId?'));
492   }
493   encodedChallenge['appIdHash'] = B64_encode(sha256HashOfString(appId));
494   return /** @type {EnrollHelperChallenge} */ (encodedChallenge);
495 };
496
497 /**
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.
502  * @private
503  */
504 Enroller.prototype.encodeEnrollChallenges_ = function(enrollChallenges,
505     opt_appId) {
506   var challenges = [];
507   for (var i = 0; i < enrollChallenges.length; i++) {
508     var enrollChallenge = enrollChallenges[i];
509     var version = enrollChallenge.version;
510     if (!version) {
511       // Version is implicitly V1 if not specified.
512       version = 'U2F_V1';
513     }
514
515     if (version == 'U2F_V2') {
516       var modifiedChallenge = {};
517       for (var k in enrollChallenge) {
518         modifiedChallenge[k] = enrollChallenge[k];
519       }
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));
533     } else {
534       challenges.push(
535           Enroller.encodeEnrollChallenge_(enrollChallenge, opt_appId));
536     }
537   }
538   return challenges;
539 };
540
541 /**
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.
547  * @private
548  */
549 Enroller.prototype.checkAppIds_ = function(enrollAppIds, cb) {
550   var appIds =
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));
555 };
556
557 /**
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
560  * origin.
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.
564  * @private
565  */
566 Enroller.prototype.originChecked_ = function(appIds, cb, result) {
567   if (!result) {
568     this.notifyError_({errorCode: ErrorCodes.BAD_REQUEST});
569     return;
570   }
571   /** @private {!AppIdChecker} */
572   this.appIdChecker_ = new AppIdChecker(FACTORY_REGISTRY.getTextFetcher(),
573       this.timer_.clone(), this.sender_.origin, appIds, this.allowHttp_,
574       this.logMsgUrl_);
575   this.appIdChecker_.doCheck().then(cb);
576 };
577
578 /** Closes this enroller. */
579 Enroller.prototype.close = function() {
580   if (this.appIdChecker_) {
581     this.appIdChecker_.close();
582   }
583   if (this.handler_) {
584     this.handler_.close();
585     this.handler_ = null;
586   }
587   this.done_ = true;
588 };
589
590 /**
591  * Notifies the caller with the error.
592  * @param {U2fError} error Error.
593  * @private
594  */
595 Enroller.prototype.notifyError_ = function(error) {
596   if (this.done_)
597     return;
598   this.close();
599   this.done_ = true;
600   this.errorCb_(error);
601 };
602
603 /**
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
608  * @private
609  */
610 Enroller.prototype.notifySuccess_ =
611     function(u2fVersion, info, opt_browserData) {
612   if (this.done_)
613     return;
614   this.close();
615   this.done_ = true;
616   this.successCb_(u2fVersion, info, opt_browserData);
617 };
618
619 /**
620  * Called by the helper upon completion.
621  * @param {EnrollHelperReply} reply The result of the enroll request.
622  * @private
623  */
624 Enroller.prototype.helperComplete_ = function(reply) {
625   if (reply.code) {
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);
630   } else {
631     console.log(UTIL_fmt('Gnubby enrollment succeeded!!!!!'));
632     var browserData;
633
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];
638     }
639
640     this.notifySuccess_(/** @type {string} */ (reply.version),
641                         /** @type {string} */ (reply.enrollData),
642                         browserData);
643   }
644 };