[MacViews] Show comboboxes with a native NSMenu
[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.
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.
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);
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);
45 var sender = createSenderFromMessageSender(messageSender);
46 if (!sender) {
47 sendErrorResponse({errorCode: ErrorCodes.BAD_REQUEST});
48 return null;
50 if (sender.origin.indexOf('http://') == 0 && !HTTP_ORIGINS_ALLOWED) {
51 sendErrorResponse({errorCode: ErrorCodes.BAD_REQUEST});
52 return null;
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.
66 function makeU2fSignResponseDataFromChallenge(challenge) {
67 var responseData = {
68 'keyHandle': challenge['keyHandle']
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.
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.
97 function validateAndEnqueueSignRequest(sender, request, errorCb, successCb) {
98 function timeout() {
99 errorCb({errorCode: ErrorCodes.TIMEOUT});
102 if (!isValidSignRequest(request)) {
103 errorCb({errorCode: ErrorCodes.BAD_REQUEST});
104 return null;
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'];
118 // Sanity check
119 if (!appId) {
120 console.warn(UTIL_fmt('empty sign appId?'));
121 errorCb({errorCode: ErrorCodes.BAD_REQUEST});
122 return null;
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();
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.
159 function isValidSignRequest(request) {
160 var signChallenges = getSignChallenges(request);
161 if (!signChallenges) {
162 return false;
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;
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}
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;
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'));
226 this.token_.complete();
228 this.closed_ = true;
232 * @param {QueuedRequestToken} token Token for this sign request.
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.
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;
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});
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
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
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
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.
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;
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
360 Signer.prototype.checkAppIds_ = function() {
361 var appIds = getDistinctAppIds(this.signChallenges_);
362 if (this.appId_) {
363 appIds = UTIL_unionArrays([this.appId_], appIds);
365 if (!appIds || !appIds.length) {
366 var error = {
367 errorCode: ErrorCodes.BAD_REQUEST,
368 errorMessage: 'missing appId'
370 this.notifyError_(error);
371 return;
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
386 Signer.prototype.originChecked_ = function(appIds, result) {
387 if (!result) {
388 var error = {
389 errorCode: ErrorCodes.BAD_REQUEST,
390 errorMessage: 'bad appId'
392 this.notifyError_(error);
393 return;
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
410 Signer.prototype.appIdChecked_ = function(result) {
411 if (!result) {
412 var error = {
413 errorCode: ErrorCodes.BAD_REQUEST,
414 errorMessage: 'bad appId'
416 this.notifyError_(error);
417 return;
419 if (!this.doSign_()) {
420 this.notifyError_({errorCode: ErrorCodes.BAD_REQUEST});
421 return;
426 * Begins signing this signer's challenges.
427 * @return {boolean} Whether the challenge could be added.
428 * @private
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_;
440 if (!serverChallenge) {
441 console.warn(UTIL_fmt('challenge missing'));
442 return false;
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;
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
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
490 Signer.prototype.close_ = function(opt_notifying) {
491 if (this.handler_) {
492 this.handler_.close();
493 this.handler_ = null;
495 this.timer_.clearTimeout();
496 if (!opt_notifying) {
497 this.notifyError_({errorCode: ErrorCodes.TIMEOUT});
502 * Notifies the caller of error.
503 * @param {U2fError} error Error.
504 * @private
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
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
535 Signer.prototype.helperComplete_ = function(helperReply, opt_source) {
536 if (helperReply.type != 'sign_helper_reply') {
537 this.notifyError_({errorCode: ErrorCodes.OTHER_ERROR});
538 return;
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_);
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);