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.
6 * @fileoverview Handles web page requests for gnubby sign requests.
12 var gnubbySignRequestQueue
;
14 function initRequestQueue() {
15 gnubbySignRequestQueue
= new OriginKeyedRequestQueue(
16 FACTORY_REGISTRY
.getSystemTimer());
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
),
37 function sendSuccessResponse(challenge
, info
, browserData
) {
38 var responseData
= makeU2fSignResponseDataFromChallenge(challenge
);
39 addSignatureAndBrowserDataToResponseData(responseData
, info
, browserData
,
41 var response
= makeU2fSuccessResponse(request
, responseData
);
42 sendResponseOnce(sentResponse
, queuedSignRequest
, response
, sendResponse
);
45 var sender
= createSenderFromMessageSender(messageSender
);
47 sendErrorResponse({errorCode
: ErrorCodes
.BAD_REQUEST
});
50 if (sender
.origin
.indexOf('http://') == 0 && !HTTP_ORIGINS_ALLOWED
) {
51 sendErrorResponse({errorCode
: ErrorCodes
.BAD_REQUEST
});
56 validateAndEnqueueSignRequest(
57 sender
, request
, sendErrorResponse
, sendSuccessResponse
);
58 return queuedSignRequest
;
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
) {
68 'keyHandle': challenge
['keyHandle']
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
;
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
) {
99 errorCb({errorCode
: ErrorCodes
.TIMEOUT
});
102 if (!isValidSignRequest(request
)) {
103 errorCb({errorCode
: ErrorCodes
.BAD_REQUEST
});
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
));
113 if (request
['appId']) {
114 appId
= request
['appId'];
115 } else if (signChallenges
.length
) {
116 appId
= signChallenges
[0]['appId'];
120 console
.warn(UTIL_fmt('empty sign appId?'));
121 errorCb({errorCode
: ErrorCodes
.BAD_REQUEST
});
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,
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'],
143 if (!gnubbySignRequestQueue
) {
146 var requestToken
= gnubbySignRequestQueue
.queueRequest(appId
, sender
.origin
,
147 queuedSignRequest
.begin
.bind(queuedSignRequest
), timer
);
148 queuedSignRequest
.setToken(requestToken
);
150 watchdog
.setCloseable(queuedSignRequest
);
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
) {
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
)) {
170 return isValidSignChallengeArray(signChallenges
, !hasDefaultChallenge
,
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
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} */
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;
221 if (hadBegunSigning
) {
222 console
.log(UTIL_fmt('closing in-progress request'));
224 console
.log(UTIL_fmt('closing timed-out request before processing'));
226 this.token_
.complete();
232 * @param {QueuedRequestToken} token Token for this sign request.
234 QueuedSignRequest
.prototype.setToken = function(token
) {
235 /** @private {QueuedRequestToken} */
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'));
247 this.errorCb_({errorCode
: ErrorCodes
.TIMEOUT
});
251 this.setToken(token
);
252 this.signer_
= new Signer(this.timer_
, this.sender_
,
253 this.signerFailed_
.bind(this), this.signerSucceeded_
.bind(this),
255 if (!this.signer_
.setChallenges(this.signChallenges_
, this.defaultChallenge_
,
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.
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
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.
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} */
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
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
,
337 if (this.challengesSet_
|| this.done_
)
339 if (this.timer_
.expired()) {
340 this.notifyError_({errorCode
: ErrorCodes
.TIMEOUT
});
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;
357 * Checks the app ids of incoming requests.
360 Signer
.prototype.checkAppIds_ = function() {
361 var appIds
= getDistinctAppIds(this.signChallenges_
);
363 appIds
= UTIL_unionArrays([this.appId_
], appIds
);
365 if (!appIds
|| !appIds
.length
) {
367 errorCode
: ErrorCodes
.BAD_REQUEST
,
368 errorMessage
: 'missing appId'
370 this.notifyError_(error
);
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
382 * @param {!Array<string>} appIds The app ids.
383 * @param {boolean} result Whether the origin could claim the app ids.
386 Signer
.prototype.originChecked_ = function(appIds
, result
) {
389 errorCode
: ErrorCodes
.BAD_REQUEST
,
390 errorMessage
: 'bad appId'
392 this.notifyError_(error
);
395 var appIdChecker
= FACTORY_REGISTRY
.getAppIdCheckerFactory().create();
398 this.timer_
.clone(), this.sender_
.origin
,
399 /** @type {!Array<string>} */ (appIds
), this.allowHttp_
,
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.
410 Signer
.prototype.appIdChecked_ = function(result
) {
413 errorCode
: ErrorCodes
.BAD_REQUEST
,
414 errorMessage
: 'bad appId'
416 this.notifyError_(error
);
419 if (!this.doSign_()) {
420 this.notifyError_({errorCode
: ErrorCodes
.BAD_REQUEST
});
426 * Begins signing this signer's challenges.
427 * @return {boolean} Whether the challenge could be added.
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
];
435 if (challenge
.hasOwnProperty('challenge')) {
436 serverChallenge
= challenge
['challenge'];
438 serverChallenge
= this.defaultChallenge_
;
440 if (!serverChallenge
) {
441 console
.warn(UTIL_fmt('challenge missing'));
444 var keyHandle
= challenge
['keyHandle'];
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
,
460 FACTORY_REGISTRY
.getRequestHelper()
461 .getHandler(/** @type {HelperRequest} */ (request
));
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.
474 Signer
.prototype.getChallengeHash_ = function(keyHandle
, challenge
) {
475 return B64_encode(sha256HashOfString(this.browserData_
[keyHandle
]));
478 /** Closes this signer. */
479 Signer
.prototype.close = function() {
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.
490 Signer
.prototype.close_ = function(opt_notifying
) {
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.
506 Signer
.prototype.notifyError_ = function(error
) {
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
521 Signer
.prototype.notifySuccess_ = function(challenge
, info
, browserData
) {
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.
535 Signer
.prototype.helperComplete_ = function(helperReply
, opt_source
) {
536 if (helperReply
.type
!= 'sign_helper_reply') {
537 this.notifyError_({errorCode
: ErrorCodes
.OTHER_ERROR
});
540 var reply
= /** @type {SignHelperReply} */ (helperReply
);
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
);
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
,