• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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
12var 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 */
22function 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 */
56function 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 */
86function 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 */
98function 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 */
114function 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 */
132function 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 */
184function 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 */
209function 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. */
234QueuedSignRequest.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 */
248QueuedSignRequest.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 */
257QueuedSignRequest.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 */
274QueuedSignRequest.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 */
286QueuedSignRequest.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 */
304function 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 */
342Signer.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 */
360Signer.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 */
385Signer.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 */
408Signer.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 */
428Signer.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 */
462Signer.prototype.getChallengeHash_ = function(keyHandle, challenge) {
463  return B64_encode(sha256HashOfString(this.browserData_[keyHandle]));
464};
465
466/** Closes this signer. */
467Signer.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 */
483Signer.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 */
498Signer.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 */
512Signer.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