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 Does common handling for requests coming from web pages and
7 * routes them to the provided handler.
8 */
9
10/**
11 * Gets the scheme + origin from a web url.
12 * @param {string} url Input url
13 * @return {?string} Scheme and origin part if url parses
14 */
15function getOriginFromUrl(url) {
16  var re = new RegExp('^(https?://)[^/]*/?');
17  var originarray = re.exec(url);
18  if (originarray == null) return originarray;
19  var origin = originarray[0];
20  while (origin.charAt(origin.length - 1) == '/') {
21    origin = origin.substring(0, origin.length - 1);
22  }
23  if (origin == 'http:' || origin == 'https:')
24    return null;
25  return origin;
26}
27
28/**
29 * Returns whether the registered key appears to be valid.
30 * @param {Object} registeredKey The registered key object.
31 * @param {boolean} appIdRequired Whether the appId property is required on
32 *     each challenge.
33 * @return {boolean} Whether the object appears valid.
34 */
35function isValidRegisteredKey(registeredKey, appIdRequired) {
36  if (appIdRequired && !registeredKey.hasOwnProperty('appId')) {
37    return false;
38  }
39  if (!registeredKey.hasOwnProperty('keyHandle'))
40    return false;
41  if (registeredKey['version']) {
42    if (registeredKey['version'] != 'U2F_V1' &&
43        registeredKey['version'] != 'U2F_V2') {
44      return false;
45    }
46  }
47  return true;
48}
49
50/**
51 * Returns whether the array of registered keys appears to be valid.
52 * @param {Array.<Object>} registeredKeys The array of registered keys.
53 * @param {boolean} appIdRequired Whether the appId property is required on
54 *     each challenge.
55 * @return {boolean} Whether the array appears valid.
56 */
57function isValidRegisteredKeyArray(registeredKeys, appIdRequired) {
58  return registeredKeys.every(function(key) {
59    return isValidRegisteredKey(key, appIdRequired);
60  });
61}
62
63/**
64 * Returns whether the array of SignChallenges appears to be valid.
65 * @param {Array.<SignChallenge>} signChallenges The array of sign challenges.
66 * @param {boolean} appIdRequired Whether the appId property is required on
67 *     each challenge.
68 * @return {boolean} Whether the array appears valid.
69 */
70function isValidSignChallengeArray(signChallenges, appIdRequired) {
71  for (var i = 0; i < signChallenges.length; i++) {
72    var incomingChallenge = signChallenges[i];
73    if (!incomingChallenge.hasOwnProperty('challenge'))
74      return false;
75    if (!isValidRegisteredKey(incomingChallenge, appIdRequired)) {
76      return false;
77    }
78  }
79  return true;
80}
81
82/** Posts the log message to the log url.
83 * @param {string} logMsg the log message to post.
84 * @param {string=} opt_logMsgUrl the url to post log messages to.
85 */
86function logMessage(logMsg, opt_logMsgUrl) {
87  console.log(UTIL_fmt('logMessage("' + logMsg + '")'));
88
89  if (!opt_logMsgUrl) {
90    return;
91  }
92  // Image fetching is not allowed per packaged app CSP.
93  // But video and audio is.
94  var audio = new Audio();
95  audio.src = opt_logMsgUrl + logMsg;
96}
97
98/**
99 * @param {Object} request Request object
100 * @param {MessageSender} sender Sender frame
101 * @param {Function} sendResponse Response callback
102 * @return {?Closeable} Optional handler object that should be closed when port
103 *     closes
104 */
105function handleWebPageRequest(request, sender, sendResponse) {
106  switch (request.type) {
107    case GnubbyMsgTypes.ENROLL_WEB_REQUEST:
108      return handleWebEnrollRequest(sender, request, sendResponse);
109
110    case GnubbyMsgTypes.SIGN_WEB_REQUEST:
111      return handleWebSignRequest(sender, request, sendResponse);
112
113    case MessageTypes.U2F_REGISTER_REQUEST:
114      return handleU2fEnrollRequest(sender, request, sendResponse);
115
116    case MessageTypes.U2F_SIGN_REQUEST:
117      return handleU2fSignRequest(sender, request, sendResponse);
118
119    default:
120      sendResponse(
121          makeU2fErrorResponse(request, ErrorCodes.BAD_REQUEST, undefined,
122              MessageTypes.U2F_REGISTER_RESPONSE));
123      return null;
124  }
125}
126
127/**
128 * Makes a response to a request.
129 * @param {Object} request The request to make a response to.
130 * @param {string} responseSuffix How to name the response's type.
131 * @param {string=} opt_defaultType The default response type, if none is
132 *     present in the request.
133 * @return {Object} The response object.
134 */
135function makeResponseForRequest(request, responseSuffix, opt_defaultType) {
136  var type;
137  if (request && request.type) {
138    type = request.type.replace(/_request$/, responseSuffix);
139  } else {
140    type = opt_defaultType;
141  }
142  var reply = { 'type': type };
143  if (request && request.requestId) {
144    reply.requestId = request.requestId;
145  }
146  return reply;
147}
148
149/**
150 * Makes a response to a U2F request with an error code.
151 * @param {Object} request The request to make a response to.
152 * @param {ErrorCodes} code The error code to return.
153 * @param {string=} opt_detail An error detail string.
154 * @param {string=} opt_defaultType The default response type, if none is
155 *     present in the request.
156 * @return {Object} The U2F error.
157 */
158function makeU2fErrorResponse(request, code, opt_detail, opt_defaultType) {
159  var reply = makeResponseForRequest(request, '_response', opt_defaultType);
160  var error = {'errorCode': code};
161  if (opt_detail) {
162    error['errorMessage'] = opt_detail;
163  }
164  reply['responseData'] = error;
165  return reply;
166}
167
168/**
169 * Makes a success response to a web request with a responseData object.
170 * @param {Object} request The request to make a response to.
171 * @param {Object} responseData The response data.
172 * @return {Object} The web error.
173 */
174function makeU2fSuccessResponse(request, responseData) {
175  var reply = makeResponseForRequest(request, '_response');
176  reply['responseData'] = responseData;
177  return reply;
178}
179
180/**
181 * Makes a response to a web request with an error code.
182 * @param {Object} request The request to make a response to.
183 * @param {GnubbyCodeTypes} code The error code to return.
184 * @param {string=} opt_defaultType The default response type, if none is
185 *     present in the request.
186 * @return {Object} The web error.
187 */
188function makeWebErrorResponse(request, code, opt_defaultType) {
189  var reply = makeResponseForRequest(request, '_reply', opt_defaultType);
190  reply['code'] = code;
191  return reply;
192}
193
194/**
195 * Makes a success response to a web request with a responseData object.
196 * @param {Object} request The request to make a response to.
197 * @param {Object} responseData The response data.
198 * @return {Object} The web error.
199 */
200function makeWebSuccessResponse(request, responseData) {
201  var reply = makeResponseForRequest(request, '_reply');
202  reply['code'] = GnubbyCodeTypes.OK;
203  reply['responseData'] = responseData;
204  return reply;
205}
206
207/**
208 * Maps an error code from the ErrorCodes namespace to the GnubbyCodeTypes
209 * namespace.
210 * @param {ErrorCodes} errorCode Error in the ErrorCodes namespace.
211 * @param {boolean} forSign Whether the error is for a sign request.
212 * @return {GnubbyCodeTypes} Error code in the GnubbyCodeTypes namespace.
213 */
214function mapErrorCodeToGnubbyCodeType(errorCode, forSign) {
215  var code;
216  switch (errorCode) {
217    case ErrorCodes.BAD_REQUEST:
218      return GnubbyCodeTypes.BAD_REQUEST;
219
220    case ErrorCodes.DEVICE_INELIGIBLE:
221      return forSign ? GnubbyCodeTypes.NONE_PLUGGED_ENROLLED :
222          GnubbyCodeTypes.ALREADY_ENROLLED;
223
224    case ErrorCodes.TIMEOUT:
225      return GnubbyCodeTypes.WAIT_TOUCH;
226  }
227  return GnubbyCodeTypes.UNKNOWN_ERROR;
228}
229
230/**
231 * Maps a helper's error code from the DeviceStatusCodes namespace to a
232 * U2fError.
233 * @param {number} code Error code from DeviceStatusCodes namespace.
234 * @return {U2fError} An error.
235 */
236function mapDeviceStatusCodeToU2fError(code) {
237  switch (code) {
238    case DeviceStatusCodes.WRONG_DATA_STATUS:
239      return {errorCode: ErrorCodes.DEVICE_INELIGIBLE};
240
241    case DeviceStatusCodes.TIMEOUT_STATUS:
242    case DeviceStatusCodes.WAIT_TOUCH_STATUS:
243      return {errorCode: ErrorCodes.TIMEOUT};
244
245    default:
246      var reportedError = {
247        errorCode: ErrorCodes.OTHER_ERROR,
248        errorMessage: 'device status code: ' + code.toString(16)
249      };
250      return reportedError;
251  }
252}
253
254/**
255 * Sends a response, using the given sentinel to ensure at most one response is
256 * sent. Also closes the closeable, if it's given.
257 * @param {boolean} sentResponse Whether a response has already been sent.
258 * @param {?Closeable} closeable A thing to close.
259 * @param {*} response The response to send.
260 * @param {Function} sendResponse A function to send the response.
261 */
262function sendResponseOnce(sentResponse, closeable, response, sendResponse) {
263  if (closeable) {
264    closeable.close();
265  }
266  if (!sentResponse) {
267    sentResponse = true;
268    try {
269      // If the page has gone away or the connection has otherwise gone,
270      // sendResponse fails.
271      sendResponse(response);
272    } catch (exception) {
273      console.warn('sendResponse failed: ' + exception);
274    }
275  } else {
276    console.warn(UTIL_fmt('Tried to reply more than once! Juan, FIX ME'));
277  }
278}
279
280/**
281 * @param {!string} string Input string
282 * @return {Array.<number>} SHA256 hash value of string.
283 */
284function sha256HashOfString(string) {
285  var s = new SHA256();
286  s.update(UTIL_StringToBytes(string));
287  return s.digest();
288}
289
290/**
291 * Normalizes the TLS channel ID value:
292 * 1. Converts semantically empty values (undefined, null, 0) to the empty
293 *     string.
294 * 2. Converts valid JSON strings to a JS object.
295 * 3. Otherwise, returns the input value unmodified.
296 * @param {Object|string|undefined} opt_tlsChannelId TLS Channel id
297 * @return {Object|string} The normalized TLS channel ID value.
298 */
299function tlsChannelIdValue(opt_tlsChannelId) {
300  if (!opt_tlsChannelId) {
301    // Case 1: Always set some value for  TLS channel ID, even if it's the empty
302    // string: this browser definitely supports them.
303    return '';
304  }
305  if (typeof opt_tlsChannelId === 'string') {
306    try {
307      var obj = JSON.parse(opt_tlsChannelId);
308      if (!obj) {
309        // Case 1: The string value 'null' parses as the Javascript object null,
310        // so return an empty string: the browser definitely supports TLS
311        // channel id.
312        return '';
313      }
314      // Case 2: return the value as a JS object.
315      return /** @type {Object} */ (obj);
316    } catch (e) {
317      console.warn('Unparseable TLS channel ID value ' + opt_tlsChannelId);
318      // Case 3: return the value unmodified.
319    }
320  }
321  return opt_tlsChannelId;
322}
323
324/**
325 * Creates a browser data object with the given values.
326 * @param {!string} type A string representing the "type" of this browser data
327 *     object.
328 * @param {!string} serverChallenge The server's challenge, as a base64-
329 *     encoded string.
330 * @param {!string} origin The server's origin, as seen by the browser.
331 * @param {Object|string|undefined} opt_tlsChannelId TLS Channel Id
332 * @return {string} A string representation of the browser data object.
333 */
334function makeBrowserData(type, serverChallenge, origin, opt_tlsChannelId) {
335  var browserData = {
336    'typ' : type,
337    'challenge' : serverChallenge,
338    'origin' : origin
339  };
340  browserData['cid_pubkey'] = tlsChannelIdValue(opt_tlsChannelId);
341  return JSON.stringify(browserData);
342}
343
344/**
345 * Creates a browser data object for an enroll request with the given values.
346 * @param {!string} serverChallenge The server's challenge, as a base64-
347 *     encoded string.
348 * @param {!string} origin The server's origin, as seen by the browser.
349 * @param {Object|string|undefined} opt_tlsChannelId TLS Channel Id
350 * @return {string} A string representation of the browser data object.
351 */
352function makeEnrollBrowserData(serverChallenge, origin, opt_tlsChannelId) {
353  return makeBrowserData(
354      'navigator.id.finishEnrollment', serverChallenge, origin,
355      opt_tlsChannelId);
356}
357
358/**
359 * Creates a browser data object for a sign request with the given values.
360 * @param {!string} serverChallenge The server's challenge, as a base64-
361 *     encoded string.
362 * @param {!string} origin The server's origin, as seen by the browser.
363 * @param {Object|string|undefined} opt_tlsChannelId TLS Channel Id
364 * @return {string} A string representation of the browser data object.
365 */
366function makeSignBrowserData(serverChallenge, origin, opt_tlsChannelId) {
367  return makeBrowserData(
368      'navigator.id.getAssertion', serverChallenge, origin, opt_tlsChannelId);
369}
370
371/**
372 * Encodes the sign data as an array of sign helper challenges.
373 * @param {Array.<SignChallenge>} signChallenges The sign challenges to encode.
374 * @param {string=} opt_defaultAppId The app id to use for each challenge, if
375 *     the challenge contains none.
376 * @param {function(string, string): string=} opt_challengeHashFunction
377 *     A function that produces, from a key handle and a raw challenge, a hash
378 *     of the raw challenge. If none is provided, a default hash function is
379 *     used.
380 * @return {!Array.<SignHelperChallenge>} The sign challenges, encoded.
381 */
382function encodeSignChallenges(signChallenges, opt_defaultAppId,
383    opt_challengeHashFunction) {
384  function encodedSha256(keyHandle, challenge) {
385    return B64_encode(sha256HashOfString(challenge));
386  }
387  var challengeHashFn = opt_challengeHashFunction || encodedSha256;
388  var encodedSignChallenges = [];
389  if (signChallenges) {
390    for (var i = 0; i < signChallenges.length; i++) {
391      var challenge = signChallenges[i];
392      var challengeHash =
393          challengeHashFn(challenge['keyHandle'], challenge['challenge']);
394      var appId;
395      if (challenge.hasOwnProperty('appId')) {
396        appId = challenge['appId'];
397      } else {
398        appId = opt_defaultAppId;
399      }
400      var encodedChallenge = {
401        'challengeHash': challengeHash,
402        'appIdHash': B64_encode(sha256HashOfString(appId)),
403        'keyHandle': challenge['keyHandle'],
404        'version': (challenge['version'] || 'U2F_V1')
405      };
406      encodedSignChallenges.push(encodedChallenge);
407    }
408  }
409  return encodedSignChallenges;
410}
411
412/**
413 * Makes a sign helper request from an array of challenges.
414 * @param {Array.<SignHelperChallenge>} challenges The sign challenges.
415 * @param {number=} opt_timeoutSeconds Timeout value.
416 * @param {string=} opt_logMsgUrl URL to log to.
417 * @return {SignHelperRequest} The sign helper request.
418 */
419function makeSignHelperRequest(challenges, opt_timeoutSeconds, opt_logMsgUrl) {
420  var request = {
421    'type': 'sign_helper_request',
422    'signData': challenges,
423    'timeout': opt_timeoutSeconds || 0,
424    'timeoutSeconds': opt_timeoutSeconds || 0
425  };
426  if (opt_logMsgUrl !== undefined) {
427    request.logMsgUrl = opt_logMsgUrl;
428  }
429  return request;
430}
431