Upstream version 10.39.225.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} 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
17  *     closes.
18  */
19 function handleWebEnrollRequest(sender, 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(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 enroller =
45       validateEnrollRequest(
46           sender, request, 'enrollChallenges', 'signData',
47           sendErrorResponse, sendSuccessResponse);
48   if (enroller) {
49     var registerRequests = request['enrollChallenges'];
50     var signRequests = getSignRequestsFromEnrollRequest(request, 'signData');
51     closeable = /** @type {Closeable} */ (enroller);
52     enroller.doEnroll(registerRequests, signRequests, request['appId']);
53   }
54   return closeable;
55 }
56
57 /**
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
63  *     closes.
64  */
65 function handleU2fEnrollRequest(sender, request, sendResponse) {
66   var sentResponse = false;
67   var closeable = null;
68
69   function sendErrorResponse(error) {
70     var response = makeU2fErrorResponse(request, error.errorCode,
71         error.errorMessage);
72     sendResponseOnce(sentResponse, closeable, response, sendResponse);
73   }
74
75   function sendSuccessResponse(u2fVersion, info, browserData) {
76     var enrollChallenges = request['registerRequests'];
77     var enrollChallenge =
78         findEnrollChallengeOfVersion(enrollChallenges, u2fVersion);
79     if (!enrollChallenge) {
80       sendErrorResponse(ErrorCodes.OTHER_ERROR);
81       return;
82     }
83     var responseData =
84         makeEnrollResponseData(enrollChallenge, u2fVersion,
85             'registrationData', info, 'clientData', browserData);
86     var response = makeU2fSuccessResponse(request, responseData);
87     sendResponseOnce(sentResponse, closeable, response, sendResponse);
88   }
89
90   var enroller =
91       validateEnrollRequest(
92           sender, request, 'registerRequests', 'signRequests',
93           sendErrorResponse, sendSuccessResponse, 'registeredKeys');
94   if (enroller) {
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']);
100   }
101   return closeable;
102 }
103
104 /**
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
109  *     in the request.
110  * @param {string} signChallengesName The name of the sign challenges value in
111  *     the request.
112  * @param {function(U2fError)} errorCb Error callback.
113  * @param {function(string, string, (string|undefined))} successCb Success
114  *     callback.
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.
119  */
120 function validateEnrollRequest(sender, request,
121     enrollChallengesName, signChallengesName, errorCb, successCb,
122     opt_registeredKeysName) {
123   var origin = getOriginFromUrl(/** @type {string} */ (sender.url));
124   if (!origin) {
125     errorCb({errorCode: ErrorCodes.BAD_REQUEST});
126     return null;
127   }
128
129   if (!isValidEnrollRequest(request, enrollChallengesName,
130       signChallengesName, opt_registeredKeysName)) {
131     errorCb({errorCode: ErrorCodes.BAD_REQUEST});
132     return null;
133   }
134
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);
140   return enroller;
141 }
142
143 /**
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
147  *     in the request.
148  * @param {string} signChallengesName The name of the sign challenges value in
149  *     the request.
150  * @param {string=} opt_registeredKeysName The name of the registered keys
151  *     value in the request.
152  * @return {boolean} Whether the request appears valid.
153  */
154 function isValidEnrollRequest(request, enrollChallengesName,
155     signChallengesName, opt_registeredKeysName) {
156   if (!request.hasOwnProperty(enrollChallengesName))
157     return false;
158   var enrollChallenges = request[enrollChallengesName];
159   if (!enrollChallenges.length)
160     return false;
161   var hasAppId = request.hasOwnProperty('appId');
162   if (!isValidEnrollChallengeArray(enrollChallenges, !hasAppId))
163     return false;
164   var signChallenges = request[signChallengesName];
165   // A missing sign challenge array is ok, in the case the user is not already
166   // enrolled.
167   if (signChallenges && !isValidSignChallengeArray(signChallenges, !hasAppId))
168     return false;
169   if (opt_registeredKeysName) {
170     var registeredKeys = request[opt_registeredKeysName];
171     if (registeredKeys &&
172         !isValidRegisteredKeyArray(registeredKeys, !hasAppId)) {
173       return false;
174     }
175   }
176   return true;
177 }
178
179 /**
180  * @typedef {{
181  *   version: (string|undefined),
182  *   challenge: string,
183  *   appId: string
184  * }}
185  */
186 var EnrollChallenge;
187
188 /**
189  * @param {Array.<EnrollChallenge>} enrollChallenges The enroll challenges to
190  *     validate.
191  * @param {boolean} appIdRequired Whether the appId property is required on
192  *     each challenge.
193  * @return {boolean} Whether the given array of challenges is a valid enroll
194  *     challenges array.
195  */
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'];
201     if (!version) {
202       // Version is implicitly V1 if not specified.
203       version = 'U2F_V1';
204     }
205     if (version != 'U2F_V1' && version != 'U2F_V2') {
206       return false;
207     }
208     if (seenVersions[version]) {
209       // Each version can appear at most once.
210       return false;
211     }
212     seenVersions[version] = version;
213     if (appIdRequired && !enrollChallenge['appId']) {
214       return false;
215     }
216     if (!enrollChallenge['challenge']) {
217       // The challenge is required.
218       return false;
219     }
220   }
221   return true;
222 }
223
224 /**
225  * Finds the enroll challenge of the given version in the enroll challlenge
226  * array.
227  * @param {Array.<EnrollChallenge>} enrollChallenges The enroll challenges to
228  *     search.
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.
232  */
233 function findEnrollChallengeOfVersion(enrollChallenges, version) {
234   for (var i = 0; i < enrollChallenges.length; i++) {
235     if (enrollChallenges[i]['version'] == version) {
236       return enrollChallenges[i];
237     }
238   }
239   return null;
240 }
241
242 /**
243  * Makes a responseData object for the enroll request with the given parameters.
244  * @param {EnrollChallenge} enrollChallenge The enroll challenge used to
245  *     register.
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.
254  */
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];
262   }
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;
267   }
268   return responseData;
269 }
270
271 /**
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
278  *     the request.
279  * @param {string=} opt_registeredKeysName The name of the registered keys
280  *     value in the request.
281  * @return {Array.<SignChallenge>}
282  */
283 function getSignRequestsFromEnrollRequest(request, signChallengesName,
284     opt_registeredKeysName) {
285   var signChallenges;
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'] = '';
293     }
294   } else {
295     signChallenges = request[signChallengesName];
296   }
297   return signChallenges;
298 }
299
300 /**
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.
311  * @constructor
312  */
313 function Enroller(timer, origin, errorCb, successCb, opt_tlsChannelId,
314     opt_logMsgUrl) {
315   /** @private {Countdown} */
316   this.timer_ = timer;
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;
327
328   /** @private {boolean} */
329   this.done_ = false;
330
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
338   // what they get.)
339   /** @private {boolean} */
340   this.allowHttp_ = this.origin_ ? this.origin_.indexOf('http://') == 0 : false;
341   /** @private {Closeable} */
342   this.handler_ = null;
343 }
344
345 /**
346  * Default timeout value in case the caller never provides a valid timeout.
347  */
348 Enroller.DEFAULT_TIMEOUT_MILLIS = 30 * 1000;
349
350 /**
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.
356  */
357 Enroller.prototype.doEnroll = function(enrollChallenges, signChallenges,
358     opt_appId) {
359   var encodedEnrollChallenges =
360       this.encodeEnrollChallenges_(enrollChallenges, opt_appId);
361   var encodedSignChallenges = encodeSignChallenges(signChallenges, opt_appId);
362   var request = {
363     type: 'enroll_helper_request',
364     enrollChallenges: encodedEnrollChallenges,
365     signData: encodedSignChallenges,
366     logMsgUrl: this.logMsgUrl_
367   };
368   if (!this.timer_.expired()) {
369     request.timeout = this.timer_.millisecondsUntilExpired() / 1000.0;
370     request.timeoutSeconds = this.timer_.millisecondsUntilExpired() / 1000.0;
371   }
372
373   // Begin fetching/checking the app ids.
374   var enrollAppIds = [];
375   if (opt_appId) {
376     enrollAppIds.push(opt_appId);
377   }
378   for (var i = 0; i < enrollChallenges.length; i++) {
379     if (enrollChallenges[i].hasOwnProperty('appId')) {
380       enrollAppIds.push(enrollChallenges[i]['appId']);
381     }
382   }
383   // Sanity check
384   if (!enrollAppIds.length) {
385     console.warn(UTIL_fmt('empty enroll app ids?'));
386     this.notifyError_({errorCode: ErrorCodes.BAD_REQUEST});
387     return;
388   }
389   var self = this;
390   this.checkAppIds_(enrollAppIds, signChallenges, function(result) {
391     if (result) {
392       self.handler_ = FACTORY_REGISTRY.getRequestHelper().getHandler(request);
393       if (self.handler_) {
394         var helperComplete =
395             /** @type {function(HelperReply)} */
396             (self.helperComplete_.bind(self));
397         self.handler_.run(helperComplete);
398       } else {
399         self.notifyError_({errorCode: ErrorCodes.OTHER_ERROR});
400       }
401     } else {
402       self.notifyError_({errorCode: ErrorCodes.BAD_REQUEST});
403     }
404   });
405 };
406
407 /**
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.
412  * @private
413  */
414 Enroller.encodeEnrollChallenge_ = function(enrollChallenge, opt_appId) {
415   var encodedChallenge = {};
416   var version;
417   if (enrollChallenge['version']) {
418     version = enrollChallenge['version'];
419   } else {
420     // Version is implicitly V1 if not specified.
421     version = 'U2F_V1';
422   }
423   encodedChallenge['version'] = version;
424   encodedChallenge['challengeHash'] = enrollChallenge['challenge'];
425   var appId;
426   if (enrollChallenge['appId']) {
427     appId = enrollChallenge['appId'];
428   } else {
429     appId = opt_appId;
430   }
431   if (!appId) {
432     // Sanity check. (Other code should fail if it's not set.)
433     console.warn(UTIL_fmt('No appId?'));
434   }
435   encodedChallenge['appIdHash'] = B64_encode(sha256HashOfString(appId));
436   return /** @type {EnrollHelperChallenge} */ (encodedChallenge);
437 };
438
439 /**
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.
444  * @private
445  */
446 Enroller.prototype.encodeEnrollChallenges_ = function(enrollChallenges,
447     opt_appId) {
448   var challenges = [];
449   for (var i = 0; i < enrollChallenges.length; i++) {
450     var enrollChallenge = enrollChallenges[i];
451     var version = enrollChallenge.version;
452     if (!version) {
453       // Version is implicitly V1 if not specified.
454       version = 'U2F_V1';
455     }
456
457     if (version == 'U2F_V2') {
458       var modifiedChallenge = {};
459       for (var k in enrollChallenge) {
460         modifiedChallenge[k] = enrollChallenge[k];
461       }
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));
475     } else {
476       challenges.push(
477           Enroller.encodeEnrollChallenge_(enrollChallenge, opt_appId));
478     }
479   }
480   return challenges;
481 };
482
483 /**
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
489  *     with the request.
490  * @param {function(boolean)} cb Called with the result of the check.
491  * @private
492  */
493 Enroller.prototype.checkAppIds_ = function(enrollAppIds, signChallenges, cb) {
494   var appIds =
495       UTIL_unionArrays(enrollAppIds, getDistinctAppIds(signChallenges));
496   FACTORY_REGISTRY.getOriginChecker().canClaimAppIds(this.origin_, appIds)
497       .then(this.originChecked_.bind(this, appIds, cb));
498 };
499
500 /**
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
503  * origin.
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.
507  * @private
508  */
509 Enroller.prototype.originChecked_ = function(appIds, cb, result) {
510   if (!result) {
511     this.notifyError_({errorCode: ErrorCodes.BAD_REQUEST});
512     return;
513   }
514   /** @private {!AppIdChecker} */
515   this.appIdChecker_ = new AppIdChecker(FACTORY_REGISTRY.getTextFetcher(),
516       this.timer_.clone(), this.origin_, appIds, this.allowHttp_,
517       this.logMsgUrl_);
518   this.appIdChecker_.doCheck().then(cb);
519 };
520
521 /** Closes this enroller. */
522 Enroller.prototype.close = function() {
523   if (this.appIdChecker_) {
524     this.appIdChecker_.close();
525   }
526   if (this.handler_) {
527     this.handler_.close();
528     this.handler_ = null;
529   }
530 };
531
532 /**
533  * Notifies the caller with the error.
534  * @param {U2fError} error Error.
535  * @private
536  */
537 Enroller.prototype.notifyError_ = function(error) {
538   if (this.done_)
539     return;
540   this.close();
541   this.done_ = true;
542   this.errorCb_(error);
543 };
544
545 /**
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
550  * @private
551  */
552 Enroller.prototype.notifySuccess_ =
553     function(u2fVersion, info, opt_browserData) {
554   if (this.done_)
555     return;
556   this.close();
557   this.done_ = true;
558   this.successCb_(u2fVersion, info, opt_browserData);
559 };
560
561 /**
562  * Called by the helper upon completion.
563  * @param {EnrollHelperReply} reply The result of the enroll request.
564  * @private
565  */
566 Enroller.prototype.helperComplete_ = function(reply) {
567   if (reply.code) {
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);
572   } else {
573     console.log(UTIL_fmt('Gnubby enrollment succeeded!!!!!'));
574     var browserData;
575
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];
580     }
581
582     this.notifySuccess_(/** @type {string} */ (reply.version),
583                         /** @type {string} */ (reply.enrollData),
584                         browserData);
585   }
586 };