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. 4 5 /** 6 * @fileoverview Handles web page requests for gnubby sign requests. 7 * 8 */ 9 10 'use strict'; 11 12 var signRequestQueue = new OriginKeyedRequestQueue(); 13 14 /** 15 * Handles a web sign request. 16 * @param {MessageSender} sender The sender of the message. 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(sender, request, sendResponse) { 23 var sentResponse = false; 24 var queuedSignRequest; 25 26 function sendErrorResponse(error) { 27 sendResponseOnce(sentResponse, queuedSignRequest, 28 makeWebErrorResponse(request, 29 mapErrorCodeToGnubbyCodeType(error.errorCode, true /* forSign */)), 30 sendResponse); 31 } 32 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 } 40 41 queuedSignRequest = 42 validateAndEnqueueSignRequest( 43 sender, request, 'signData', sendErrorResponse, 44 sendSuccessResponse); 45 return queuedSignRequest; 46 } 47 48 /** 49 * Handles a U2F sign request. 50 * @param {MessageSender} sender The sender of the message. 51 * @param {Object} request The web page's sign request. 52 * @param {Function} sendResponse Called back with the result of the sign. 53 * @return {Closeable} Request handler that should be closed when the browser 54 * message channel is closed. 55 */ 56 function handleU2fSignRequest(sender, request, sendResponse) { 57 var sentResponse = false; 58 var queuedSignRequest; 59 60 function sendErrorResponse(error) { 61 sendResponseOnce(sentResponse, queuedSignRequest, 62 makeU2fErrorResponse(request, error.errorCode, error.errorMessage), 63 sendResponse); 64 } 65 66 function sendSuccessResponse(challenge, info, browserData) { 67 var responseData = makeU2fSignResponseDataFromChallenge(challenge); 68 addSignatureAndBrowserDataToResponseData(responseData, info, browserData, 69 'clientData'); 70 var response = makeU2fSuccessResponse(request, responseData); 71 sendResponseOnce(sentResponse, queuedSignRequest, response, sendResponse); 72 } 73 74 queuedSignRequest = 75 validateAndEnqueueSignRequest( 76 sender, request, 'signRequests', sendErrorResponse, 77 sendSuccessResponse); 78 return queuedSignRequest; 79 } 80 81 /** 82 * Creates a base U2F responseData object from the server challenge. 83 * @param {SignChallenge} challenge The server challenge. 84 * @return {Object} The responseData object. 85 */ 86 function makeU2fSignResponseDataFromChallenge(challenge) { 87 var responseData = { 88 'keyHandle': challenge['keyHandle'] 89 }; 90 return responseData; 91 } 92 93 /** 94 * Creates a base web responseData object from the server challenge. 95 * @param {SignChallenge} challenge The server challenge. 96 * @return {Object} The responseData object. 97 */ 98 function makeWebSignResponseDataFromChallenge(challenge) { 99 var responseData = {}; 100 for (var k in challenge) { 101 responseData[k] = challenge[k]; 102 } 103 return responseData; 104 } 105 106 /** 107 * Adds the browser data and signature values to a responseData object. 108 * @param {Object} responseData The "base" responseData object. 109 * @param {string} signatureData The signature data. 110 * @param {string} browserData The browser data generated from the challenge. 111 * @param {string} browserDataName The name of the browser data key in the 112 * responseData object. 113 */ 114 function addSignatureAndBrowserDataToResponseData(responseData, signatureData, 115 browserData, browserDataName) { 116 responseData[browserDataName] = B64_encode(UTIL_StringToBytes(browserData)); 117 responseData['signatureData'] = signatureData; 118 } 119 120 /** 121 * Validates a sign request using the given sign challenges name, and, if valid, 122 * enqueues the sign request for eventual processing. 123 * @param {MessageSender} sender The sender of the message. 124 * @param {Object} request The web page's sign request. 125 * @param {string} signChallengesName The name of the sign challenges value in 126 * the request. 127 * @param {function(U2fError)} errorCb Error callback. 128 * @param {function(SignChallenge, string, string)} successCb Success callback. 129 * @return {Closeable} Request handler that should be closed when the browser 130 * message channel is closed. 131 */ 132 function validateAndEnqueueSignRequest(sender, request, 133 signChallengesName, errorCb, successCb) { 134 var origin = getOriginFromUrl(/** @type {string} */ (sender.url)); 135 if (!origin) { 136 errorCb({errorCode: ErrorCodes.BAD_REQUEST}); 137 return null; 138 } 139 // More closure type inference fail. 140 var nonNullOrigin = /** @type {string} */ (origin); 141 142 if (!isValidSignRequest(request, signChallengesName)) { 143 errorCb({errorCode: ErrorCodes.BAD_REQUEST}); 144 return null; 145 } 146 147 var signChallenges = request[signChallengesName]; 148 var appId; 149 if (request['appId']) { 150 appId = request['appId']; 151 } else { 152 // A valid sign data has at least one challenge, so get the appId from 153 // the first challenge. 154 appId = signChallenges[0]['appId']; 155 } 156 // Sanity check 157 if (!appId) { 158 console.warn(UTIL_fmt('empty sign appId?')); 159 errorCb({errorCode: ErrorCodes.BAD_REQUEST}); 160 return null; 161 } 162 var timer = createTimerForRequest( 163 FACTORY_REGISTRY.getCountdownFactory(), request); 164 var logMsgUrl = request['logMsgUrl']; 165 166 // Queue sign requests from the same origin, to protect against simultaneous 167 // sign-out on many tabs resulting in repeated sign-in requests. 168 var queuedSignRequest = new QueuedSignRequest(signChallenges, 169 timer, nonNullOrigin, errorCb, successCb, appId, sender.tlsChannelId, 170 logMsgUrl); 171 var requestToken = signRequestQueue.queueRequest(appId, nonNullOrigin, 172 queuedSignRequest.begin.bind(queuedSignRequest), timer); 173 queuedSignRequest.setToken(requestToken); 174 return queuedSignRequest; 175 } 176 177 /** 178 * Returns whether the request appears to be a valid sign request. 179 * @param {Object} request The request. 180 * @param {string} signChallengesName The name of the sign challenges value in 181 * the request. 182 * @return {boolean} Whether the request appears valid. 183 */ 184 function isValidSignRequest(request, signChallengesName) { 185 if (!request.hasOwnProperty(signChallengesName)) 186 return false; 187 var signChallenges = request[signChallengesName]; 188 // If a sign request contains an empty array of challenges, it could never 189 // be fulfilled. Fail. 190 if (!signChallenges.length) 191 return false; 192 var hasAppId = request.hasOwnProperty('appId'); 193 return isValidSignChallengeArray(signChallenges, !hasAppId); 194 } 195 196 /** 197 * Adapter class representing a queued sign request. 198 * @param {!Array.<SignChallenge>} signChallenges The sign challenges. 199 * @param {Countdown} timer Timeout timer 200 * @param {string} origin Signature origin 201 * @param {function(U2fError)} errorCb Error callback 202 * @param {function(SignChallenge, string, string)} successCb Success callback 203 * @param {string|undefined} opt_appId The app id for the entire request. 204 * @param {string|undefined} opt_tlsChannelId TLS Channel Id 205 * @param {string|undefined} opt_logMsgUrl Url to post log messages to 206 * @constructor 207 * @implements {Closeable} 208 */ 209 function QueuedSignRequest(signChallenges, timer, origin, errorCb, 210 successCb, opt_appId, opt_tlsChannelId, opt_logMsgUrl) { 211 /** @private {!Array.<SignChallenge>} */ 212 this.signChallenges_ = signChallenges; 213 /** @private {Countdown} */ 214 this.timer_ = timer; 215 /** @private {string} */ 216 this.origin_ = origin; 217 /** @private {function(U2fError)} */ 218 this.errorCb_ = errorCb; 219 /** @private {function(SignChallenge, string, string)} */ 220 this.successCb_ = successCb; 221 /** @private {string|undefined} */ 222 this.appId_ = opt_appId; 223 /** @private {string|undefined} */ 224 this.tlsChannelId_ = opt_tlsChannelId; 225 /** @private {string|undefined} */ 226 this.logMsgUrl_ = opt_logMsgUrl; 227 /** @private {boolean} */ 228 this.begun_ = false; 229 /** @private {boolean} */ 230 this.closed_ = false; 231 } 232 233 /** Closes this sign request. */ 234 QueuedSignRequest.prototype.close = function() { 235 if (this.closed_) return; 236 if (this.begun_ && this.signer_) { 237 this.signer_.close(); 238 } 239 if (this.token_) { 240 this.token_.complete(); 241 } 242 this.closed_ = true; 243 }; 244 245 /** 246 * @param {QueuedRequestToken} token Token for this sign request. 247 */ 248 QueuedSignRequest.prototype.setToken = function(token) { 249 /** @private {QueuedRequestToken} */ 250 this.token_ = token; 251 }; 252 253 /** 254 * Called when this sign request may begin work. 255 * @param {QueuedRequestToken} token Token for this sign request. 256 */ 257 QueuedSignRequest.prototype.begin = function(token) { 258 this.begun_ = true; 259 this.setToken(token); 260 this.signer_ = new Signer(this.timer_, this.origin_, 261 this.signerFailed_.bind(this), this.signerSucceeded_.bind(this), 262 this.tlsChannelId_, this.logMsgUrl_); 263 if (!this.signer_.setChallenges(this.signChallenges_, this.appId_)) { 264 token.complete(); 265 this.errorCb_({errorCode: ErrorCodes.BAD_REQUEST}); 266 } 267 }; 268 269 /** 270 * Called when this request's signer fails. 271 * @param {U2fError} error The failure reported by the signer. 272 * @private 273 */ 274 QueuedSignRequest.prototype.signerFailed_ = function(error) { 275 this.token_.complete(); 276 this.errorCb_(error); 277 }; 278 279 /** 280 * Called when this request's signer succeeds. 281 * @param {SignChallenge} challenge The challenge that was signed. 282 * @param {string} info The sign result. 283 * @param {string} browserData Browser data JSON 284 * @private 285 */ 286 QueuedSignRequest.prototype.signerSucceeded_ = 287 function(challenge, info, browserData) { 288 this.token_.complete(); 289 this.successCb_(challenge, info, browserData); 290 }; 291 292 /** 293 * Creates an object to track signing with a gnubby. 294 * @param {Countdown} timer Timer for sign request. 295 * @param {string} origin The origin making the request. 296 * @param {function(U2fError)} errorCb Called when the sign operation fails. 297 * @param {function(SignChallenge, string, string)} successCb Called when the 298 * sign operation succeeds. 299 * @param {string=} opt_tlsChannelId the TLS channel ID, if any, of the origin 300 * making the request. 301 * @param {string=} opt_logMsgUrl The url to post log messages to. 302 * @constructor 303 */ 304 function Signer(timer, origin, errorCb, successCb, 305 opt_tlsChannelId, opt_logMsgUrl) { 306 /** @private {Countdown} */ 307 this.timer_ = timer; 308 /** @private {string} */ 309 this.origin_ = origin; 310 /** @private {function(U2fError)} */ 311 this.errorCb_ = errorCb; 312 /** @private {function(SignChallenge, string, string)} */ 313 this.successCb_ = successCb; 314 /** @private {string|undefined} */ 315 this.tlsChannelId_ = opt_tlsChannelId; 316 /** @private {string|undefined} */ 317 this.logMsgUrl_ = opt_logMsgUrl; 318 319 /** @private {boolean} */ 320 this.challengesSet_ = false; 321 /** @private {boolean} */ 322 this.done_ = false; 323 324 /** @private {Object.<string, string>} */ 325 this.browserData_ = {}; 326 /** @private {Object.<string, SignChallenge>} */ 327 this.serverChallenges_ = {}; 328 // Allow http appIds for http origins. (Broken, but the caller deserves 329 // what they get.) 330 /** @private {boolean} */ 331 this.allowHttp_ = this.origin_ ? this.origin_.indexOf('http://') == 0 : false; 332 /** @private {Closeable} */ 333 this.handler_ = null; 334 } 335 336 /** 337 * Sets the challenges to be signed. 338 * @param {Array.<SignChallenge>} signChallenges The challenges to set. 339 * @param {string=} opt_appId The app id for the entire request. 340 * @return {boolean} Whether the challenges could be set. 341 */ 342 Signer.prototype.setChallenges = function(signChallenges, opt_appId) { 343 if (this.challengesSet_ || this.done_) 344 return false; 345 /** @private {Array.<SignChallenge>} */ 346 this.signChallenges_ = signChallenges; 347 /** @private {string|undefined} */ 348 this.appId_ = opt_appId; 349 /** @private {boolean} */ 350 this.challengesSet_ = true; 351 352 this.checkAppIds_(); 353 return true; 354 }; 355 356 /** 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().canClaimAppIds(this.origin_, appIds) 374 .then(this.originChecked_.bind(this, appIds)); 375 }; 376 377 /** 378 * Called with the result of checking the origin. When the origin is allowed 379 * to claim the app ids, begins checking whether the app ids also list the 380 * origin. 381 * @param {!Array.<string>} appIds The app ids. 382 * @param {boolean} result Whether the origin could claim the app ids. 383 * @private 384 */ 385 Signer.prototype.originChecked_ = function(appIds, result) { 386 if (!result) { 387 var error = { 388 errorCode: ErrorCodes.BAD_REQUEST, 389 errorMessage: 'bad appId' 390 }; 391 this.notifyError_(error); 392 return; 393 } 394 /** @private {!AppIdChecker} */ 395 this.appIdChecker_ = new AppIdChecker(FACTORY_REGISTRY.getTextFetcher(), 396 this.timer_.clone(), this.origin_, 397 /** @type {!Array.<string>} */ (appIds), this.allowHttp_, 398 this.logMsgUrl_); 399 this.appIdChecker_.doCheck().then(this.appIdChecked_.bind(this)); 400 }; 401 402 /** 403 * Called with the result of checking app ids. When the app ids are valid, 404 * adds the sign challenges to those being signed. 405 * @param {boolean} result Whether the app ids are valid. 406 * @private 407 */ 408 Signer.prototype.appIdChecked_ = function(result) { 409 if (!result) { 410 var error = { 411 errorCode: ErrorCodes.BAD_REQUEST, 412 errorMessage: 'bad appId' 413 }; 414 this.notifyError_(error); 415 return; 416 } 417 if (!this.doSign_()) { 418 this.notifyError_({errorCode: ErrorCodes.BAD_REQUEST}); 419 return; 420 } 421 }; 422 423 /** 424 * Begins signing this signer's challenges. 425 * @return {boolean} Whether the challenge could be added. 426 * @private 427 */ 428 Signer.prototype.doSign_ = function() { 429 // Create the browser data for each challenge. 430 for (var i = 0; i < this.signChallenges_.length; i++) { 431 var challenge = this.signChallenges_[i]; 432 var serverChallenge = challenge['challenge']; 433 var keyHandle = challenge['keyHandle']; 434 435 var browserData = 436 makeSignBrowserData(serverChallenge, this.origin_, this.tlsChannelId_); 437 this.browserData_[keyHandle] = browserData; 438 this.serverChallenges_[keyHandle] = challenge; 439 } 440 441 var encodedChallenges = encodeSignChallenges(this.signChallenges_, 442 this.appId_, this.getChallengeHash_.bind(this)); 443 444 var timeoutSeconds = this.timer_.millisecondsUntilExpired() / 1000.0; 445 var request = makeSignHelperRequest(encodedChallenges, timeoutSeconds, 446 this.logMsgUrl_); 447 this.handler_ = 448 FACTORY_REGISTRY.getRequestHelper() 449 .getHandler(/** @type {HelperRequest} */ (request)); 450 if (!this.handler_) 451 return false; 452 return this.handler_.run(this.helperComplete_.bind(this)); 453 }; 454 455 /** 456 * @param {string} keyHandle The key handle used with the challenge. 457 * @param {string} challenge The challenge. 458 * @return {string} The hashed challenge associated with the key 459 * handle/challenge pair. 460 * @private 461 */ 462 Signer.prototype.getChallengeHash_ = function(keyHandle, challenge) { 463 return B64_encode(sha256HashOfString(this.browserData_[keyHandle])); 464 }; 465 466 /** Closes this signer. */ 467 Signer.prototype.close = function() { 468 if (this.appIdChecker_) { 469 this.appIdChecker_.close(); 470 } 471 if (this.handler_) { 472 this.handler_.close(); 473 this.handler_ = null; 474 } 475 this.timer_.clearTimeout(); 476 }; 477 478 /** 479 * Notifies the caller of error. 480 * @param {U2fError} error Error. 481 * @private 482 */ 483 Signer.prototype.notifyError_ = function(error) { 484 if (this.done_) 485 return; 486 this.close(); 487 this.done_ = true; 488 this.errorCb_(error); 489 }; 490 491 /** 492 * Notifies the caller of success. 493 * @param {SignChallenge} challenge The challenge that was signed. 494 * @param {string} info The sign result. 495 * @param {string} browserData Browser data JSON 496 * @private 497 */ 498 Signer.prototype.notifySuccess_ = function(challenge, info, browserData) { 499 if (this.done_) 500 return; 501 this.close(); 502 this.done_ = true; 503 this.successCb_(challenge, info, browserData); 504 }; 505 506 /** 507 * Called by the helper upon completion. 508 * @param {HelperReply} helperReply The result of the sign request. 509 * @param {string=} opt_source The source of the sign result. 510 * @private 511 */ 512 Signer.prototype.helperComplete_ = function(helperReply, opt_source) { 513 if (helperReply.type != 'sign_helper_reply') { 514 this.notifyError_({errorCode: ErrorCodes.OTHER_ERROR}); 515 return; 516 } 517 var reply = /** @type {SignHelperReply} */ (helperReply); 518 519 if (reply.code) { 520 var reportedError = mapDeviceStatusCodeToU2fError(reply.code); 521 console.log(UTIL_fmt('helper reported ' + reply.code.toString(16) + 522 ', returning ' + reportedError.errorCode)); 523 this.notifyError_(reportedError); 524 } else { 525 if (this.logMsgUrl_ && opt_source) { 526 var logMsg = 'signed&source=' + opt_source; 527 logMessage(logMsg, this.logMsgUrl_); 528 } 529 530 var key = reply.responseData['keyHandle']; 531 var browserData = this.browserData_[key]; 532 // Notify with server-provided challenge, not the encoded one: the 533 // server-provided challenge contains additional fields it relies on. 534 var serverChallenge = this.serverChallenges_[key]; 535 this.notifySuccess_(serverChallenge, reply.responseData.signatureData, 536 browserData); 537 } 538 }; 539