Merge Chromium + Blink git repositories
[chromium-blink-merge.git] / chrome / browser / resources / cryptotoken / signer.js
blob6a0fe53c09b824b835cb8a79f0ba65d54ac2d74f
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 sign requests.
7  *
8  */
10 'use strict';
12 var gnubbySignRequestQueue;
14 function initRequestQueue() {
15   gnubbySignRequestQueue = new OriginKeyedRequestQueue(
16       FACTORY_REGISTRY.getSystemTimer());
19 /**
20  * Handles a U2F sign request.
21  * @param {MessageSender} messageSender The message sender.
22  * @param {Object} request The web page's sign request.
23  * @param {Function} sendResponse Called back with the result of the sign.
24  * @return {Closeable} Request handler that should be closed when the browser
25  *     message channel is closed.
26  */
27 function handleU2fSignRequest(messageSender, request, sendResponse) {
28   var sentResponse = false;
29   var queuedSignRequest;
31   function sendErrorResponse(error) {
32     sendResponseOnce(sentResponse, queuedSignRequest,
33         makeU2fErrorResponse(request, error.errorCode, error.errorMessage),
34         sendResponse);
35   }
37   function sendSuccessResponse(challenge, info, browserData) {
38     var responseData = makeU2fSignResponseDataFromChallenge(challenge);
39     addSignatureAndBrowserDataToResponseData(responseData, info, browserData,
40         'clientData');
41     var response = makeU2fSuccessResponse(request, responseData);
42     sendResponseOnce(sentResponse, queuedSignRequest, response, sendResponse);
43   }
45   var sender = createSenderFromMessageSender(messageSender);
46   if (!sender) {
47     sendErrorResponse({errorCode: ErrorCodes.BAD_REQUEST});
48     return null;
49   }
50   if (sender.origin.indexOf('http://') == 0 && !HTTP_ORIGINS_ALLOWED) {
51     sendErrorResponse({errorCode: ErrorCodes.BAD_REQUEST});
52     return null;
53   }
55   queuedSignRequest =
56       validateAndEnqueueSignRequest(
57           sender, request, sendErrorResponse, sendSuccessResponse);
58   return queuedSignRequest;
61 /**
62  * Creates a base U2F responseData object from the server challenge.
63  * @param {SignChallenge} challenge The server challenge.
64  * @return {Object} The responseData object.
65  */
66 function makeU2fSignResponseDataFromChallenge(challenge) {
67   var responseData = {
68     'keyHandle': challenge['keyHandle']
69   };
70   return responseData;
73 /**
74  * Adds the browser data and signature values to a responseData object.
75  * @param {Object} responseData The "base" responseData object.
76  * @param {string} signatureData The signature data.
77  * @param {string} browserData The browser data generated from the challenge.
78  * @param {string} browserDataName The name of the browser data key in the
79  *     responseData object.
80  */
81 function addSignatureAndBrowserDataToResponseData(responseData, signatureData,
82     browserData, browserDataName) {
83   responseData[browserDataName] = B64_encode(UTIL_StringToBytes(browserData));
84   responseData['signatureData'] = signatureData;
87 /**
88  * Validates a sign request using the given sign challenges name, and, if valid,
89  * enqueues the sign request for eventual processing.
90  * @param {WebRequestSender} sender The sender of the message.
91  * @param {Object} request The web page's sign request.
92  * @param {function(U2fError)} errorCb Error callback.
93  * @param {function(SignChallenge, string, string)} successCb Success callback.
94  * @return {Closeable} Request handler that should be closed when the browser
95  *     message channel is closed.
96  */
97 function validateAndEnqueueSignRequest(sender, request, errorCb, successCb) {
98   function timeout() {
99     errorCb({errorCode: ErrorCodes.TIMEOUT});
100   }
102   if (!isValidSignRequest(request)) {
103     errorCb({errorCode: ErrorCodes.BAD_REQUEST});
104     return null;
105   }
107   // The typecast is necessary because getSignChallenges can return undefined.
108   // On the other hand, a valid sign request can't contain an undefined sign
109   // challenge list, so the typecast is safe.
110   var signChallenges = /** @type {!Array<SignChallenge>} */ (
111       getSignChallenges(request));
112   var appId;
113   if (request['appId']) {
114     appId = request['appId'];
115   } else if (signChallenges.length) {
116     appId = signChallenges[0]['appId'];
117   }
118   // Sanity check
119   if (!appId) {
120     console.warn(UTIL_fmt('empty sign appId?'));
121     errorCb({errorCode: ErrorCodes.BAD_REQUEST});
122     return null;
123   }
124   var timeoutValueSeconds = getTimeoutValueFromRequest(request);
125   // Attenuate watchdog timeout value less than the signer's timeout, so the
126   // watchdog only fires after the signer could reasonably have called back,
127   // not before.
128   timeoutValueSeconds = attenuateTimeoutInSeconds(timeoutValueSeconds,
129       MINIMUM_TIMEOUT_ATTENUATION_SECONDS / 2);
130   var watchdog = new WatchdogRequestHandler(timeoutValueSeconds, timeout);
131   var wrappedErrorCb = watchdog.wrapCallback(errorCb);
132   var wrappedSuccessCb = watchdog.wrapCallback(successCb);
134   var timer = createAttenuatedTimer(
135       FACTORY_REGISTRY.getCountdownFactory(), timeoutValueSeconds);
136   var logMsgUrl = request['logMsgUrl'];
138   // Queue sign requests from the same origin, to protect against simultaneous
139   // sign-out on many tabs resulting in repeated sign-in requests.
140   var queuedSignRequest = new QueuedSignRequest(signChallenges,
141       timer, sender, wrappedErrorCb, wrappedSuccessCb, request['challenge'],
142       appId, logMsgUrl);
143   if (!gnubbySignRequestQueue) {
144     initRequestQueue();
145   }
146   var requestToken = gnubbySignRequestQueue.queueRequest(appId, sender.origin,
147       queuedSignRequest.begin.bind(queuedSignRequest), timer);
148   queuedSignRequest.setToken(requestToken);
150   watchdog.setCloseable(queuedSignRequest);
151   return watchdog;
155  * Returns whether the request appears to be a valid sign request.
156  * @param {Object} request The request.
157  * @return {boolean} Whether the request appears valid.
158  */
159 function isValidSignRequest(request) {
160   var signChallenges = getSignChallenges(request);
161   if (!signChallenges) {
162     return false;
163   }
164   var hasDefaultChallenge = request.hasOwnProperty('challenge');
165   var hasAppId = request.hasOwnProperty('appId');
166   // If the sign challenge array is empty, the global appId is required.
167   if (!hasAppId && (!signChallenges || !signChallenges.length)) {
168     return false;
169   }
170   return isValidSignChallengeArray(signChallenges, !hasDefaultChallenge,
171       !hasAppId);
175  * Adapter class representing a queued sign request.
176  * @param {!Array<SignChallenge>} signChallenges The sign challenges.
177  * @param {Countdown} timer Timeout timer
178  * @param {WebRequestSender} sender Message sender.
179  * @param {function(U2fError)} errorCb Error callback
180  * @param {function(SignChallenge, string, string)} successCb Success callback
181  * @param {string|undefined} opt_defaultChallenge A default sign challenge
182  *     value, if a request does not provide one.
183  * @param {string|undefined} opt_appId The app id for the entire request.
184  * @param {string|undefined} opt_logMsgUrl Url to post log messages to
185  * @constructor
186  * @implements {Closeable}
187  */
188 function QueuedSignRequest(signChallenges, timer, sender, errorCb,
189     successCb, opt_defaultChallenge, opt_appId, opt_logMsgUrl) {
190   /** @private {!Array<SignChallenge>} */
191   this.signChallenges_ = signChallenges;
192   /** @private {Countdown} */
193   this.timer_ = timer.clone(this.close.bind(this));
194   /** @private {WebRequestSender} */
195   this.sender_ = sender;
196   /** @private {function(U2fError)} */
197   this.errorCb_ = errorCb;
198   /** @private {function(SignChallenge, string, string)} */
199   this.successCb_ = successCb;
200   /** @private {string|undefined} */
201   this.defaultChallenge_ = opt_defaultChallenge;
202   /** @private {string|undefined} */
203   this.appId_ = opt_appId;
204   /** @private {string|undefined} */
205   this.logMsgUrl_ = opt_logMsgUrl;
206   /** @private {boolean} */
207   this.begun_ = false;
208   /** @private {boolean} */
209   this.closed_ = false;
212 /** Closes this sign request. */
213 QueuedSignRequest.prototype.close = function() {
214   if (this.closed_) return;
215   var hadBegunSigning = false;
216   if (this.begun_ && this.signer_) {
217     this.signer_.close();
218     hadBegunSigning = true;
219   }
220   if (this.token_) {
221     if (hadBegunSigning) {
222       console.log(UTIL_fmt('closing in-progress request'));
223     } else {
224       console.log(UTIL_fmt('closing timed-out request before processing'));
225     }
226     this.token_.complete();
227   }
228   this.closed_ = true;
232  * @param {QueuedRequestToken} token Token for this sign request.
233  */
234 QueuedSignRequest.prototype.setToken = function(token) {
235   /** @private {QueuedRequestToken} */
236   this.token_ = token;
240  * Called when this sign request may begin work.
241  * @param {QueuedRequestToken} token Token for this sign request.
242  */
243 QueuedSignRequest.prototype.begin = function(token) {
244   if (this.timer_.expired()) {
245     console.log(UTIL_fmt('Queued request begun after timeout'));
246     this.close();
247     this.errorCb_({errorCode: ErrorCodes.TIMEOUT});
248     return;
249   }
250   this.begun_ = true;
251   this.setToken(token);
252   this.signer_ = new Signer(this.timer_, this.sender_,
253       this.signerFailed_.bind(this), this.signerSucceeded_.bind(this),
254       this.logMsgUrl_);
255   if (!this.signer_.setChallenges(this.signChallenges_, this.defaultChallenge_,
256       this.appId_)) {
257     token.complete();
258     this.errorCb_({errorCode: ErrorCodes.BAD_REQUEST});
259   }
260   // Signer now has responsibility for maintaining timeout.
261   this.timer_.clearTimeout();
265  * Called when this request's signer fails.
266  * @param {U2fError} error The failure reported by the signer.
267  * @private
268  */
269 QueuedSignRequest.prototype.signerFailed_ = function(error) {
270   this.token_.complete();
271   this.errorCb_(error);
275  * Called when this request's signer succeeds.
276  * @param {SignChallenge} challenge The challenge that was signed.
277  * @param {string} info The sign result.
278  * @param {string} browserData Browser data JSON
279  * @private
280  */
281 QueuedSignRequest.prototype.signerSucceeded_ =
282     function(challenge, info, browserData) {
283   this.token_.complete();
284   this.successCb_(challenge, info, browserData);
288  * Creates an object to track signing with a gnubby.
289  * @param {Countdown} timer Timer for sign request.
290  * @param {WebRequestSender} sender The message sender.
291  * @param {function(U2fError)} errorCb Called when the sign operation fails.
292  * @param {function(SignChallenge, string, string)} successCb Called when the
293  *     sign operation succeeds.
294  * @param {string=} opt_logMsgUrl The url to post log messages to.
295  * @constructor
296  */
297 function Signer(timer, sender, errorCb, successCb, opt_logMsgUrl) {
298   /** @private {Countdown} */
299   this.timer_ = timer.clone();
300   /** @private {WebRequestSender} */
301   this.sender_ = sender;
302   /** @private {function(U2fError)} */
303   this.errorCb_ = errorCb;
304   /** @private {function(SignChallenge, string, string)} */
305   this.successCb_ = successCb;
306   /** @private {string|undefined} */
307   this.logMsgUrl_ = opt_logMsgUrl;
309   /** @private {boolean} */
310   this.challengesSet_ = false;
311   /** @private {boolean} */
312   this.done_ = false;
314   /** @private {Object<string, string>} */
315   this.browserData_ = {};
316   /** @private {Object<string, SignChallenge>} */
317   this.serverChallenges_ = {};
318   // Allow http appIds for http origins. (Broken, but the caller deserves
319   // what they get.)
320   /** @private {boolean} */
321   this.allowHttp_ = this.sender_.origin ?
322       this.sender_.origin.indexOf('http://') == 0 : false;
323   /** @private {Closeable} */
324   this.handler_ = null;
328  * Sets the challenges to be signed.
329  * @param {Array<SignChallenge>} signChallenges The challenges to set.
330  * @param {string=} opt_defaultChallenge A default sign challenge
331  *     value, if a request does not provide one.
332  * @param {string=} opt_appId The app id for the entire request.
333  * @return {boolean} Whether the challenges could be set.
334  */
335 Signer.prototype.setChallenges = function(signChallenges, opt_defaultChallenge,
336     opt_appId) {
337   if (this.challengesSet_ || this.done_)
338     return false;
339   if (this.timer_.expired()) {
340     this.notifyError_({errorCode: ErrorCodes.TIMEOUT});
341     return true;
342   }
343   /** @private {Array<SignChallenge>} */
344   this.signChallenges_ = signChallenges;
345   /** @private {string|undefined} */
346   this.defaultChallenge_ = opt_defaultChallenge;
347   /** @private {string|undefined} */
348   this.appId_ = opt_appId;
349   /** @private {boolean} */
350   this.challengesSet_ = true;
352   this.checkAppIds_();
353   return true;
357  * Checks the app ids of incoming requests.
358  * @private
359  */
360 Signer.prototype.checkAppIds_ = function() {
361   var appIds = getDistinctAppIds(this.signChallenges_);
362   if (this.appId_) {
363     appIds = UTIL_unionArrays([this.appId_], appIds);
364   }
365   if (!appIds || !appIds.length) {
366     var error = {
367       errorCode: ErrorCodes.BAD_REQUEST,
368       errorMessage: 'missing appId'
369     };
370     this.notifyError_(error);
371     return;
372   }
373   FACTORY_REGISTRY.getOriginChecker()
374       .canClaimAppIds(this.sender_.origin, appIds)
375       .then(this.originChecked_.bind(this, appIds));
379  * Called with the result of checking the origin. When the origin is allowed
380  * to claim the app ids, begins checking whether the app ids also list the
381  * origin.
382  * @param {!Array<string>} appIds The app ids.
383  * @param {boolean} result Whether the origin could claim the app ids.
384  * @private
385  */
386 Signer.prototype.originChecked_ = function(appIds, result) {
387   if (!result) {
388     var error = {
389       errorCode: ErrorCodes.BAD_REQUEST,
390       errorMessage: 'bad appId'
391     };
392     this.notifyError_(error);
393     return;
394   }
395   var appIdChecker = FACTORY_REGISTRY.getAppIdCheckerFactory().create();
396   appIdChecker.
397       checkAppIds(
398           this.timer_.clone(), this.sender_.origin,
399           /** @type {!Array<string>} */ (appIds), this.allowHttp_,
400           this.logMsgUrl_)
401       .then(this.appIdChecked_.bind(this));
405  * Called with the result of checking app ids.  When the app ids are valid,
406  * adds the sign challenges to those being signed.
407  * @param {boolean} result Whether the app ids are valid.
408  * @private
409  */
410 Signer.prototype.appIdChecked_ = function(result) {
411   if (!result) {
412     var error = {
413       errorCode: ErrorCodes.BAD_REQUEST,
414       errorMessage: 'bad appId'
415     };
416     this.notifyError_(error);
417     return;
418   }
419   if (!this.doSign_()) {
420     this.notifyError_({errorCode: ErrorCodes.BAD_REQUEST});
421     return;
422   }
426  * Begins signing this signer's challenges.
427  * @return {boolean} Whether the challenge could be added.
428  * @private
429  */
430 Signer.prototype.doSign_ = function() {
431   // Create the browser data for each challenge.
432   for (var i = 0; i < this.signChallenges_.length; i++) {
433     var challenge = this.signChallenges_[i];
434     var serverChallenge;
435     if (challenge.hasOwnProperty('challenge')) {
436       serverChallenge = challenge['challenge'];
437     } else {
438       serverChallenge = this.defaultChallenge_;
439     }
440     if (!serverChallenge) {
441       console.warn(UTIL_fmt('challenge missing'));
442       return false;
443     }
444     var keyHandle = challenge['keyHandle'];
446     var browserData =
447         makeSignBrowserData(serverChallenge, this.sender_.origin,
448             this.sender_.tlsChannelId);
449     this.browserData_[keyHandle] = browserData;
450     this.serverChallenges_[keyHandle] = challenge;
451   }
453   var encodedChallenges = encodeSignChallenges(this.signChallenges_,
454       this.defaultChallenge_, this.appId_, this.getChallengeHash_.bind(this));
456   var timeoutSeconds = this.timer_.millisecondsUntilExpired() / 1000.0;
457   var request = makeSignHelperRequest(encodedChallenges, timeoutSeconds,
458       this.logMsgUrl_);
459   this.handler_ =
460       FACTORY_REGISTRY.getRequestHelper()
461           .getHandler(/** @type {HelperRequest} */ (request));
462   if (!this.handler_)
463     return false;
464   return this.handler_.run(this.helperComplete_.bind(this));
468  * @param {string} keyHandle The key handle used with the challenge.
469  * @param {string} challenge The challenge.
470  * @return {string} The hashed challenge associated with the key
471  *     handle/challenge pair.
472  * @private
473  */
474 Signer.prototype.getChallengeHash_ = function(keyHandle, challenge) {
475   return B64_encode(sha256HashOfString(this.browserData_[keyHandle]));
478 /** Closes this signer. */
479 Signer.prototype.close = function() {
480   this.close_();
484  * Closes this signer, and optionally notifies the caller of error.
485  * @param {boolean=} opt_notifying When true, this method is being called in the
486  *     process of notifying the caller of an existing status. When false,
487  *     the caller is notified with a default error value, ErrorCodes.TIMEOUT.
488  * @private
489  */
490 Signer.prototype.close_ = function(opt_notifying) {
491   if (this.handler_) {
492     this.handler_.close();
493     this.handler_ = null;
494   }
495   this.timer_.clearTimeout();
496   if (!opt_notifying) {
497     this.notifyError_({errorCode: ErrorCodes.TIMEOUT});
498   }
502  * Notifies the caller of error.
503  * @param {U2fError} error Error.
504  * @private
505  */
506 Signer.prototype.notifyError_ = function(error) {
507   if (this.done_)
508     return;
509   this.done_ = true;
510   this.close_(true);
511   this.errorCb_(error);
515  * Notifies the caller of success.
516  * @param {SignChallenge} challenge The challenge that was signed.
517  * @param {string} info The sign result.
518  * @param {string} browserData Browser data JSON
519  * @private
520  */
521 Signer.prototype.notifySuccess_ = function(challenge, info, browserData) {
522   if (this.done_)
523     return;
524   this.done_ = true;
525   this.close_(true);
526   this.successCb_(challenge, info, browserData);
530  * Called by the helper upon completion.
531  * @param {HelperReply} helperReply The result of the sign request.
532  * @param {string=} opt_source The source of the sign result.
533  * @private
534  */
535 Signer.prototype.helperComplete_ = function(helperReply, opt_source) {
536   if (helperReply.type != 'sign_helper_reply') {
537     this.notifyError_({errorCode: ErrorCodes.OTHER_ERROR});
538     return;
539   }
540   var reply = /** @type {SignHelperReply} */ (helperReply);
542   if (reply.code) {
543     var reportedError = mapDeviceStatusCodeToU2fError(reply.code);
544     console.log(UTIL_fmt('helper reported ' + reply.code.toString(16) +
545         ', returning ' + reportedError.errorCode));
546     this.notifyError_(reportedError);
547   } else {
548     if (this.logMsgUrl_ && opt_source) {
549       var logMsg = 'signed&source=' + opt_source;
550       logMessage(logMsg, this.logMsgUrl_);
551     }
553     var key = reply.responseData['keyHandle'];
554     var browserData = this.browserData_[key];
555     // Notify with server-provided challenge, not the encoded one: the
556     // server-provided challenge contains additional fields it relies on.
557     var serverChallenge = this.serverChallenges_[key];
558     this.notifySuccess_(serverChallenge, reply.responseData.signatureData,
559         browserData);
560   }