Add ICU message format support
[chromium-blink-merge.git] / chrome / browser / resources / cryptotoken / signer.js
blobdca744880d10afa079280a10a7ffb2da24de0a41
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 signRequestQueue = new OriginKeyedRequestQueue();
14 /**
15  * Handles a web sign request.
16  * @param {MessageSender} messageSender The message sender.
17  * @param {Object} request The web page's sign request.
18  * @param {Function} sendResponse Called back with the result of the sign.
19  * @return {Closeable} Request handler that should be closed when the browser
20  *     message channel is closed.
21  */
22 function handleWebSignRequest(messageSender, request, sendResponse) {
23   var sentResponse = false;
24   var queuedSignRequest;
26   function sendErrorResponse(error) {
27     sendResponseOnce(sentResponse, queuedSignRequest,
28         makeWebErrorResponse(request,
29             mapErrorCodeToGnubbyCodeType(error.errorCode, true /* forSign */)),
30         sendResponse);
31   }
33   function sendSuccessResponse(challenge, info, browserData) {
34     var responseData = makeWebSignResponseDataFromChallenge(challenge);
35     addSignatureAndBrowserDataToResponseData(responseData, info, browserData,
36         'browserData');
37     var response = makeWebSuccessResponse(request, responseData);
38     sendResponseOnce(sentResponse, queuedSignRequest, response, sendResponse);
39   }
41   var sender = createSenderFromMessageSender(messageSender);
42   if (!sender) {
43     sendErrorResponse({errorCode: ErrorCodes.BAD_REQUEST});
44     return null;
45   }
46   if (sender.origin.indexOf('http://') == 0 && !HTTP_ORIGINS_ALLOWED) {
47     sendErrorResponse({errorCode: ErrorCodes.BAD_REQUEST});
48     return null;
49   }
51   queuedSignRequest =
52       validateAndEnqueueSignRequest(
53           sender, request, 'signData', sendErrorResponse,
54           sendSuccessResponse);
55   return queuedSignRequest;
58 /**
59  * Handles a U2F sign request.
60  * @param {MessageSender} messageSender The message sender.
61  * @param {Object} request The web page's sign request.
62  * @param {Function} sendResponse Called back with the result of the sign.
63  * @return {Closeable} Request handler that should be closed when the browser
64  *     message channel is closed.
65  */
66 function handleU2fSignRequest(messageSender, request, sendResponse) {
67   var sentResponse = false;
68   var queuedSignRequest;
70   function sendErrorResponse(error) {
71     sendResponseOnce(sentResponse, queuedSignRequest,
72         makeU2fErrorResponse(request, error.errorCode, error.errorMessage),
73         sendResponse);
74   }
76   function sendSuccessResponse(challenge, info, browserData) {
77     var responseData = makeU2fSignResponseDataFromChallenge(challenge);
78     addSignatureAndBrowserDataToResponseData(responseData, info, browserData,
79         'clientData');
80     var response = makeU2fSuccessResponse(request, responseData);
81     sendResponseOnce(sentResponse, queuedSignRequest, response, sendResponse);
82   }
84   var sender = createSenderFromMessageSender(messageSender);
85   if (!sender) {
86     sendErrorResponse({errorCode: ErrorCodes.BAD_REQUEST});
87     return null;
88   }
89   if (sender.origin.indexOf('http://') == 0 && !HTTP_ORIGINS_ALLOWED) {
90     sendErrorResponse({errorCode: ErrorCodes.BAD_REQUEST});
91     return null;
92   }
94   queuedSignRequest =
95       validateAndEnqueueSignRequest(
96           sender, request, 'signRequests', sendErrorResponse,
97           sendSuccessResponse);
98   return queuedSignRequest;
102  * Creates a base U2F responseData object from the server challenge.
103  * @param {SignChallenge} challenge The server challenge.
104  * @return {Object} The responseData object.
105  */
106 function makeU2fSignResponseDataFromChallenge(challenge) {
107   var responseData = {
108     'keyHandle': challenge['keyHandle']
109   };
110   return responseData;
114  * Creates a base web responseData object from the server challenge.
115  * @param {SignChallenge} challenge The server challenge.
116  * @return {Object} The responseData object.
117  */
118 function makeWebSignResponseDataFromChallenge(challenge) {
119   var responseData = {};
120   for (var k in challenge) {
121     responseData[k] = challenge[k];
122   }
123   return responseData;
127  * Adds the browser data and signature values to a responseData object.
128  * @param {Object} responseData The "base" responseData object.
129  * @param {string} signatureData The signature data.
130  * @param {string} browserData The browser data generated from the challenge.
131  * @param {string} browserDataName The name of the browser data key in the
132  *     responseData object.
133  */
134 function addSignatureAndBrowserDataToResponseData(responseData, signatureData,
135     browserData, browserDataName) {
136   responseData[browserDataName] = B64_encode(UTIL_StringToBytes(browserData));
137   responseData['signatureData'] = signatureData;
141  * Validates a sign request using the given sign challenges name, and, if valid,
142  * enqueues the sign request for eventual processing.
143  * @param {WebRequestSender} sender The sender of the message.
144  * @param {Object} request The web page's sign request.
145  * @param {string} signChallengesName The name of the sign challenges value in
146  *     the request.
147  * @param {function(U2fError)} errorCb Error callback.
148  * @param {function(SignChallenge, string, string)} successCb Success callback.
149  * @return {Closeable} Request handler that should be closed when the browser
150  *     message channel is closed.
151  */
152 function validateAndEnqueueSignRequest(sender, request,
153     signChallengesName, errorCb, successCb) {
154   function timeout() {
155     errorCb({errorCode: ErrorCodes.TIMEOUT});
156   }
158   if (!isValidSignRequest(request, signChallengesName)) {
159     errorCb({errorCode: ErrorCodes.BAD_REQUEST});
160     return null;
161   }
163   var signChallenges = request[signChallengesName];
164   var appId;
165   if (request['appId']) {
166     appId = request['appId'];
167   } else if (signChallenges.length) {
168     appId = signChallenges[0]['appId'];
169   }
170   // Sanity check
171   if (!appId) {
172     console.warn(UTIL_fmt('empty sign appId?'));
173     errorCb({errorCode: ErrorCodes.BAD_REQUEST});
174     return null;
175   }
176   var timeoutValueSeconds = getTimeoutValueFromRequest(request);
177   // Attenuate watchdog timeout value less than the signer's timeout, so the
178   // watchdog only fires after the signer could reasonably have called back,
179   // not before.
180   timeoutValueSeconds = attenuateTimeoutInSeconds(timeoutValueSeconds,
181       MINIMUM_TIMEOUT_ATTENUATION_SECONDS / 2);
182   var watchdog = new WatchdogRequestHandler(timeoutValueSeconds, timeout);
183   var wrappedErrorCb = watchdog.wrapCallback(errorCb);
184   var wrappedSuccessCb = watchdog.wrapCallback(successCb);
186   var timer = createAttenuatedTimer(
187       FACTORY_REGISTRY.getCountdownFactory(), timeoutValueSeconds);
188   var logMsgUrl = request['logMsgUrl'];
190   // Queue sign requests from the same origin, to protect against simultaneous
191   // sign-out on many tabs resulting in repeated sign-in requests.
192   var queuedSignRequest = new QueuedSignRequest(signChallenges,
193       timer, sender, wrappedErrorCb, wrappedSuccessCb, request['challenge'],
194       appId, logMsgUrl);
195   var requestToken = signRequestQueue.queueRequest(appId, sender.origin,
196       queuedSignRequest.begin.bind(queuedSignRequest), timer);
197   queuedSignRequest.setToken(requestToken);
199   watchdog.setCloseable(queuedSignRequest);
200   return watchdog;
204  * Returns whether the request appears to be a valid sign request.
205  * @param {Object} request The request.
206  * @param {string} signChallengesName The name of the sign challenges value in
207  *     the request.
208  * @return {boolean} Whether the request appears valid.
209  */
210 function isValidSignRequest(request, signChallengesName) {
211   if (!request.hasOwnProperty(signChallengesName))
212     return false;
213   var signChallenges = request[signChallengesName];
214   var hasDefaultChallenge = request.hasOwnProperty('challenge');
215   var hasAppId = request.hasOwnProperty('appId');
216   // If the sign challenge array is empty, the global appId is required.
217   if (!hasAppId && (!signChallenges || !signChallenges.length)) {
218     return false;
219   }
220   return isValidSignChallengeArray(signChallenges, hasDefaultChallenge,
221       !hasAppId);
225  * Adapter class representing a queued sign request.
226  * @param {!Array<SignChallenge>} signChallenges The sign challenges.
227  * @param {Countdown} timer Timeout timer
228  * @param {WebRequestSender} sender Message sender.
229  * @param {function(U2fError)} errorCb Error callback
230  * @param {function(SignChallenge, string, string)} successCb Success callback
231  * @param {string|undefined} opt_defaultChallenge A default sign challenge
232  *     value, if a request does not provide one.
233  * @param {string|undefined} opt_appId The app id for the entire request.
234  * @param {string|undefined} opt_logMsgUrl Url to post log messages to
235  * @constructor
236  * @implements {Closeable}
237  */
238 function QueuedSignRequest(signChallenges, timer, sender, errorCb,
239     successCb, opt_defaultChallenge, opt_appId, opt_logMsgUrl) {
240   /** @private {!Array<SignChallenge>} */
241   this.signChallenges_ = signChallenges;
242   /** @private {Countdown} */
243   this.timer_ = timer.clone(this.close.bind(this));
244   /** @private {WebRequestSender} */
245   this.sender_ = sender;
246   /** @private {function(U2fError)} */
247   this.errorCb_ = errorCb;
248   /** @private {function(SignChallenge, string, string)} */
249   this.successCb_ = successCb;
250   /** @private {string|undefined} */
251   this.defaultChallenge_ = opt_defaultChallenge;
252   /** @private {string|undefined} */
253   this.appId_ = opt_appId;
254   /** @private {string|undefined} */
255   this.logMsgUrl_ = opt_logMsgUrl;
256   /** @private {boolean} */
257   this.begun_ = false;
258   /** @private {boolean} */
259   this.closed_ = false;
262 /** Closes this sign request. */
263 QueuedSignRequest.prototype.close = function() {
264   if (this.closed_) return;
265   var hadBegunSigning = false;
266   if (this.begun_ && this.signer_) {
267     this.signer_.close();
268     hadBegunSigning = true;
269   }
270   if (this.token_) {
271     if (hadBegunSigning) {
272       console.log(UTIL_fmt('closing in-progress request'));
273     } else {
274       console.log(UTIL_fmt('closing timed-out request before processing'));
275     }
276     this.token_.complete();
277   }
278   this.closed_ = true;
282  * @param {QueuedRequestToken} token Token for this sign request.
283  */
284 QueuedSignRequest.prototype.setToken = function(token) {
285   /** @private {QueuedRequestToken} */
286   this.token_ = token;
290  * Called when this sign request may begin work.
291  * @param {QueuedRequestToken} token Token for this sign request.
292  */
293 QueuedSignRequest.prototype.begin = function(token) {
294   if (this.timer_.expired()) {
295     console.log(UTIL_fmt('Queued request begun after timeout'));
296     this.close();
297     this.errorCb_({errorCode: ErrorCodes.TIMEOUT});
298     return;
299   }
300   this.begun_ = true;
301   this.setToken(token);
302   this.signer_ = new Signer(this.timer_, this.sender_,
303       this.signerFailed_.bind(this), this.signerSucceeded_.bind(this),
304       this.logMsgUrl_);
305   if (!this.signer_.setChallenges(this.signChallenges_, this.defaultChallenge_,
306       this.appId_)) {
307     token.complete();
308     this.errorCb_({errorCode: ErrorCodes.BAD_REQUEST});
309   }
310   // Signer now has responsibility for maintaining timeout.
311   this.timer_.clearTimeout();
315  * Called when this request's signer fails.
316  * @param {U2fError} error The failure reported by the signer.
317  * @private
318  */
319 QueuedSignRequest.prototype.signerFailed_ = function(error) {
320   this.token_.complete();
321   this.errorCb_(error);
325  * Called when this request's signer succeeds.
326  * @param {SignChallenge} challenge The challenge that was signed.
327  * @param {string} info The sign result.
328  * @param {string} browserData Browser data JSON
329  * @private
330  */
331 QueuedSignRequest.prototype.signerSucceeded_ =
332     function(challenge, info, browserData) {
333   this.token_.complete();
334   this.successCb_(challenge, info, browserData);
338  * Creates an object to track signing with a gnubby.
339  * @param {Countdown} timer Timer for sign request.
340  * @param {WebRequestSender} sender The message sender.
341  * @param {function(U2fError)} errorCb Called when the sign operation fails.
342  * @param {function(SignChallenge, string, string)} successCb Called when the
343  *     sign operation succeeds.
344  * @param {string=} opt_logMsgUrl The url to post log messages to.
345  * @constructor
346  */
347 function Signer(timer, sender, errorCb, successCb, opt_logMsgUrl) {
348   /** @private {Countdown} */
349   this.timer_ = timer.clone();
350   /** @private {WebRequestSender} */
351   this.sender_ = sender;
352   /** @private {function(U2fError)} */
353   this.errorCb_ = errorCb;
354   /** @private {function(SignChallenge, string, string)} */
355   this.successCb_ = successCb;
356   /** @private {string|undefined} */
357   this.logMsgUrl_ = opt_logMsgUrl;
359   /** @private {boolean} */
360   this.challengesSet_ = false;
361   /** @private {boolean} */
362   this.done_ = false;
364   /** @private {Object<string>} */
365   this.browserData_ = {};
366   /** @private {Object<SignChallenge>} */
367   this.serverChallenges_ = {};
368   // Allow http appIds for http origins. (Broken, but the caller deserves
369   // what they get.)
370   /** @private {boolean} */
371   this.allowHttp_ = this.sender_.origin ?
372       this.sender_.origin.indexOf('http://') == 0 : false;
373   /** @private {Closeable} */
374   this.handler_ = null;
378  * Sets the challenges to be signed.
379  * @param {Array<SignChallenge>} signChallenges The challenges to set.
380  * @param {string=} opt_defaultChallenge A default sign challenge
381  *     value, if a request does not provide one.
382  * @param {string=} opt_appId The app id for the entire request.
383  * @return {boolean} Whether the challenges could be set.
384  */
385 Signer.prototype.setChallenges = function(signChallenges, opt_defaultChallenge,
386     opt_appId) {
387   if (this.challengesSet_ || this.done_)
388     return false;
389   if (this.timer_.expired()) {
390     this.notifyError_({errorCode: ErrorCodes.TIMEOUT});
391     return true;
392   }
393   /** @private {Array<SignChallenge>} */
394   this.signChallenges_ = signChallenges;
395   /** @private {string|undefined} */
396   this.defaultChallenge_ = opt_defaultChallenge;
397   /** @private {string|undefined} */
398   this.appId_ = opt_appId;
399   /** @private {boolean} */
400   this.challengesSet_ = true;
402   this.checkAppIds_();
403   return true;
407  * Checks the app ids of incoming requests.
408  * @private
409  */
410 Signer.prototype.checkAppIds_ = function() {
411   var appIds = getDistinctAppIds(this.signChallenges_);
412   if (this.appId_) {
413     appIds = UTIL_unionArrays([this.appId_], appIds);
414   }
415   if (!appIds || !appIds.length) {
416     var error = {
417       errorCode: ErrorCodes.BAD_REQUEST,
418       errorMessage: 'missing appId'
419     };
420     this.notifyError_(error);
421     return;
422   }
423   FACTORY_REGISTRY.getOriginChecker()
424       .canClaimAppIds(this.sender_.origin, appIds)
425       .then(this.originChecked_.bind(this, appIds));
429  * Called with the result of checking the origin. When the origin is allowed
430  * to claim the app ids, begins checking whether the app ids also list the
431  * origin.
432  * @param {!Array<string>} appIds The app ids.
433  * @param {boolean} result Whether the origin could claim the app ids.
434  * @private
435  */
436 Signer.prototype.originChecked_ = function(appIds, result) {
437   if (!result) {
438     var error = {
439       errorCode: ErrorCodes.BAD_REQUEST,
440       errorMessage: 'bad appId'
441     };
442     this.notifyError_(error);
443     return;
444   }
445   /** @private {!AppIdChecker} */
446   this.appIdChecker_ = new AppIdChecker(FACTORY_REGISTRY.getTextFetcher(),
447       this.timer_.clone(), this.sender_.origin,
448       /** @type {!Array<string>} */ (appIds), this.allowHttp_,
449       this.logMsgUrl_);
450   this.appIdChecker_.doCheck().then(this.appIdChecked_.bind(this));
454  * Called with the result of checking app ids.  When the app ids are valid,
455  * adds the sign challenges to those being signed.
456  * @param {boolean} result Whether the app ids are valid.
457  * @private
458  */
459 Signer.prototype.appIdChecked_ = function(result) {
460   if (!result) {
461     var error = {
462       errorCode: ErrorCodes.BAD_REQUEST,
463       errorMessage: 'bad appId'
464     };
465     this.notifyError_(error);
466     return;
467   }
468   if (!this.doSign_()) {
469     this.notifyError_({errorCode: ErrorCodes.BAD_REQUEST});
470     return;
471   }
475  * Begins signing this signer's challenges.
476  * @return {boolean} Whether the challenge could be added.
477  * @private
478  */
479 Signer.prototype.doSign_ = function() {
480   // Create the browser data for each challenge.
481   for (var i = 0; i < this.signChallenges_.length; i++) {
482     var challenge = this.signChallenges_[i];
483     var serverChallenge;
484     if (challenge.hasOwnProperty('challenge')) {
485       serverChallenge = challenge['challenge'];
486     } else {
487       serverChallenge = this.defaultChallenge_;
488     }
489     if (!serverChallenge) {
490       console.warn(UTIL_fmt('challenge missing'));
491       return false;
492     }
493     var keyHandle = challenge['keyHandle'];
495     var browserData =
496         makeSignBrowserData(serverChallenge, this.sender_.origin,
497             this.sender_.tlsChannelId);
498     this.browserData_[keyHandle] = browserData;
499     this.serverChallenges_[keyHandle] = challenge;
500   }
502   var encodedChallenges = encodeSignChallenges(this.signChallenges_,
503       this.defaultChallenge_, this.appId_, this.getChallengeHash_.bind(this));
505   var timeoutSeconds = this.timer_.millisecondsUntilExpired() / 1000.0;
506   var request = makeSignHelperRequest(encodedChallenges, timeoutSeconds,
507       this.logMsgUrl_);
508   this.handler_ =
509       FACTORY_REGISTRY.getRequestHelper()
510           .getHandler(/** @type {HelperRequest} */ (request));
511   if (!this.handler_)
512     return false;
513   return this.handler_.run(this.helperComplete_.bind(this));
517  * @param {string} keyHandle The key handle used with the challenge.
518  * @param {string} challenge The challenge.
519  * @return {string} The hashed challenge associated with the key
520  *     handle/challenge pair.
521  * @private
522  */
523 Signer.prototype.getChallengeHash_ = function(keyHandle, challenge) {
524   return B64_encode(sha256HashOfString(this.browserData_[keyHandle]));
527 /** Closes this signer. */
528 Signer.prototype.close = function() {
529   this.close_();
533  * Closes this signer, and optionally notifies the caller of error.
534  * @param {boolean=} opt_notifying When true, this method is being called in the
535  *     process of notifying the caller of an existing status. When false,
536  *     the caller is notified with a default error value, ErrorCodes.TIMEOUT.
537  * @private
538  */
539 Signer.prototype.close_ = function(opt_notifying) {
540   if (this.appIdChecker_) {
541     this.appIdChecker_.close();
542   }
543   if (this.handler_) {
544     this.handler_.close();
545     this.handler_ = null;
546   }
547   this.timer_.clearTimeout();
548   if (!opt_notifying) {
549     this.notifyError_({errorCode: ErrorCodes.TIMEOUT});
550   }
554  * Notifies the caller of error.
555  * @param {U2fError} error Error.
556  * @private
557  */
558 Signer.prototype.notifyError_ = function(error) {
559   if (this.done_)
560     return;
561   this.done_ = true;
562   this.close_(true);
563   this.errorCb_(error);
567  * Notifies the caller of success.
568  * @param {SignChallenge} challenge The challenge that was signed.
569  * @param {string} info The sign result.
570  * @param {string} browserData Browser data JSON
571  * @private
572  */
573 Signer.prototype.notifySuccess_ = function(challenge, info, browserData) {
574   if (this.done_)
575     return;
576   this.done_ = true;
577   this.close_(true);
578   this.successCb_(challenge, info, browserData);
582  * Called by the helper upon completion.
583  * @param {HelperReply} helperReply The result of the sign request.
584  * @param {string=} opt_source The source of the sign result.
585  * @private
586  */
587 Signer.prototype.helperComplete_ = function(helperReply, opt_source) {
588   if (helperReply.type != 'sign_helper_reply') {
589     this.notifyError_({errorCode: ErrorCodes.OTHER_ERROR});
590     return;
591   }
592   var reply = /** @type {SignHelperReply} */ (helperReply);
594   if (reply.code) {
595     var reportedError = mapDeviceStatusCodeToU2fError(reply.code);
596     console.log(UTIL_fmt('helper reported ' + reply.code.toString(16) +
597         ', returning ' + reportedError.errorCode));
598     this.notifyError_(reportedError);
599   } else {
600     if (this.logMsgUrl_ && opt_source) {
601       var logMsg = 'signed&source=' + opt_source;
602       logMessage(logMsg, this.logMsgUrl_);
603     }
605     var key = reply.responseData['keyHandle'];
606     var browserData = this.browserData_[key];
607     // Notify with server-provided challenge, not the encoded one: the
608     // server-provided challenge contains additional fields it relies on.
609     var serverChallenge = this.serverChallenges_[key];
610     this.notifySuccess_(serverChallenge, reply.responseData.signatureData,
611         browserData);
612   }