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 signRequestQueue
= new OriginKeyedRequestQueue();
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.
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 */)),
33 function sendSuccessResponse(challenge
, info
, browserData
) {
34 var responseData
= makeWebSignResponseDataFromChallenge(challenge
);
35 addSignatureAndBrowserDataToResponseData(responseData
, info
, browserData
,
37 var response
= makeWebSuccessResponse(request
, responseData
);
38 sendResponseOnce(sentResponse
, queuedSignRequest
, response
, sendResponse
);
41 var sender
= createSenderFromMessageSender(messageSender
);
43 sendErrorResponse({errorCode
: ErrorCodes
.BAD_REQUEST
});
46 if (sender
.origin
.indexOf('http://') == 0 && !HTTP_ORIGINS_ALLOWED
) {
47 sendErrorResponse({errorCode
: ErrorCodes
.BAD_REQUEST
});
52 validateAndEnqueueSignRequest(
53 sender
, request
, 'signData', sendErrorResponse
,
55 return queuedSignRequest
;
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.
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
),
76 function sendSuccessResponse(challenge
, info
, browserData
) {
77 var responseData
= makeU2fSignResponseDataFromChallenge(challenge
);
78 addSignatureAndBrowserDataToResponseData(responseData
, info
, browserData
,
80 var response
= makeU2fSuccessResponse(request
, responseData
);
81 sendResponseOnce(sentResponse
, queuedSignRequest
, response
, sendResponse
);
84 var sender
= createSenderFromMessageSender(messageSender
);
86 sendErrorResponse({errorCode
: ErrorCodes
.BAD_REQUEST
});
89 if (sender
.origin
.indexOf('http://') == 0 && !HTTP_ORIGINS_ALLOWED
) {
90 sendErrorResponse({errorCode
: ErrorCodes
.BAD_REQUEST
});
95 validateAndEnqueueSignRequest(
96 sender
, request
, 'signRequests', sendErrorResponse
,
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.
106 function makeU2fSignResponseDataFromChallenge(challenge
) {
108 'keyHandle': challenge
['keyHandle']
114 * Creates a base web responseData object from the server challenge.
115 * @param {SignChallenge} challenge The server challenge.
116 * @return {Object} The responseData object.
118 function makeWebSignResponseDataFromChallenge(challenge
) {
119 var responseData
= {};
120 for (var k
in challenge
) {
121 responseData
[k
] = challenge
[k
];
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.
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
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.
152 function validateAndEnqueueSignRequest(sender
, request
,
153 signChallengesName
, errorCb
, successCb
) {
155 errorCb({errorCode
: ErrorCodes
.TIMEOUT
});
158 if (!isValidSignRequest(request
, signChallengesName
)) {
159 errorCb({errorCode
: ErrorCodes
.BAD_REQUEST
});
163 var signChallenges
= request
[signChallengesName
];
165 if (request
['appId']) {
166 appId
= request
['appId'];
167 } else if (signChallenges
.length
) {
168 appId
= signChallenges
[0]['appId'];
172 console
.warn(UTIL_fmt('empty sign appId?'));
173 errorCb({errorCode
: ErrorCodes
.BAD_REQUEST
});
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,
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'],
195 var requestToken
= signRequestQueue
.queueRequest(appId
, sender
.origin
,
196 queuedSignRequest
.begin
.bind(queuedSignRequest
), timer
);
197 queuedSignRequest
.setToken(requestToken
);
199 watchdog
.setCloseable(queuedSignRequest
);
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
208 * @return {boolean} Whether the request appears valid.
210 function isValidSignRequest(request
, signChallengesName
) {
211 if (!request
.hasOwnProperty(signChallengesName
))
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
)) {
220 return isValidSignChallengeArray(signChallenges
, hasDefaultChallenge
,
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
236 * @implements {Closeable}
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} */
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;
271 if (hadBegunSigning
) {
272 console
.log(UTIL_fmt('closing in-progress request'));
274 console
.log(UTIL_fmt('closing timed-out request before processing'));
276 this.token_
.complete();
282 * @param {QueuedRequestToken} token Token for this sign request.
284 QueuedSignRequest
.prototype.setToken = function(token
) {
285 /** @private {QueuedRequestToken} */
290 * Called when this sign request may begin work.
291 * @param {QueuedRequestToken} token Token for this sign request.
293 QueuedSignRequest
.prototype.begin = function(token
) {
294 if (this.timer_
.expired()) {
295 console
.log(UTIL_fmt('Queued request begun after timeout'));
297 this.errorCb_({errorCode
: ErrorCodes
.TIMEOUT
});
301 this.setToken(token
);
302 this.signer_
= new Signer(this.timer_
, this.sender_
,
303 this.signerFailed_
.bind(this), this.signerSucceeded_
.bind(this),
305 if (!this.signer_
.setChallenges(this.signChallenges_
, this.defaultChallenge_
,
308 this.errorCb_({errorCode
: ErrorCodes
.BAD_REQUEST
});
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.
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
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.
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} */
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
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.
385 Signer
.prototype.setChallenges = function(signChallenges
, opt_defaultChallenge
,
387 if (this.challengesSet_
|| this.done_
)
389 if (this.timer_
.expired()) {
390 this.notifyError_({errorCode
: ErrorCodes
.TIMEOUT
});
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;
407 * Checks the app ids of incoming requests.
410 Signer
.prototype.checkAppIds_ = function() {
411 var appIds
= getDistinctAppIds(this.signChallenges_
);
413 appIds
= UTIL_unionArrays([this.appId_
], appIds
);
415 if (!appIds
|| !appIds
.length
) {
417 errorCode
: ErrorCodes
.BAD_REQUEST
,
418 errorMessage
: 'missing appId'
420 this.notifyError_(error
);
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
432 * @param {!Array<string>} appIds The app ids.
433 * @param {boolean} result Whether the origin could claim the app ids.
436 Signer
.prototype.originChecked_ = function(appIds
, result
) {
439 errorCode
: ErrorCodes
.BAD_REQUEST
,
440 errorMessage
: 'bad appId'
442 this.notifyError_(error
);
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_
,
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.
459 Signer
.prototype.appIdChecked_ = function(result
) {
462 errorCode
: ErrorCodes
.BAD_REQUEST
,
463 errorMessage
: 'bad appId'
465 this.notifyError_(error
);
468 if (!this.doSign_()) {
469 this.notifyError_({errorCode
: ErrorCodes
.BAD_REQUEST
});
475 * Begins signing this signer's challenges.
476 * @return {boolean} Whether the challenge could be added.
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
];
484 if (challenge
.hasOwnProperty('challenge')) {
485 serverChallenge
= challenge
['challenge'];
487 serverChallenge
= this.defaultChallenge_
;
489 if (!serverChallenge
) {
490 console
.warn(UTIL_fmt('challenge missing'));
493 var keyHandle
= challenge
['keyHandle'];
496 makeSignBrowserData(serverChallenge
, this.sender_
.origin
,
497 this.sender_
.tlsChannelId
);
498 this.browserData_
[keyHandle
] = browserData
;
499 this.serverChallenges_
[keyHandle
] = challenge
;
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
,
509 FACTORY_REGISTRY
.getRequestHelper()
510 .getHandler(/** @type {HelperRequest} */ (request
));
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.
523 Signer
.prototype.getChallengeHash_ = function(keyHandle
, challenge
) {
524 return B64_encode(sha256HashOfString(this.browserData_
[keyHandle
]));
527 /** Closes this signer. */
528 Signer
.prototype.close = function() {
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.
539 Signer
.prototype.close_ = function(opt_notifying
) {
540 if (this.appIdChecker_
) {
541 this.appIdChecker_
.close();
544 this.handler_
.close();
545 this.handler_
= null;
547 this.timer_
.clearTimeout();
548 if (!opt_notifying
) {
549 this.notifyError_({errorCode
: ErrorCodes
.TIMEOUT
});
554 * Notifies the caller of error.
555 * @param {U2fError} error Error.
558 Signer
.prototype.notifyError_ = function(error
) {
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
573 Signer
.prototype.notifySuccess_ = function(challenge
, info
, browserData
) {
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.
587 Signer
.prototype.helperComplete_ = function(helperReply
, opt_source
) {
588 if (helperReply
.type
!= 'sign_helper_reply') {
589 this.notifyError_({errorCode
: ErrorCodes
.OTHER_ERROR
});
592 var reply
= /** @type {SignHelperReply} */ (helperReply
);
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
);
600 if (this.logMsgUrl_
&& opt_source
) {
601 var logMsg
= 'signed&source=' + opt_source
;
602 logMessage(logMsg
, this.logMsgUrl_
);
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
,