OLD | NEW |
(Empty) | |
| 1 // Copyright (c) 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 |
| 13 * @return {?string} |
| 14 */ |
| 15 function 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 * Parses the text as JSON and returns it as an array of strings. |
| 30 * @param {string} text |
| 31 * @return {Array.<string>} |
| 32 */ |
| 33 function getOriginsFromJson(text) { |
| 34 try { |
| 35 var urls = JSON.parse(text); |
| 36 var origins = []; |
| 37 for (var i = 0, url; url = urls[i]; i++) { |
| 38 var origin = getOriginFromUrl(url); |
| 39 if (origin) |
| 40 origins.push(origin); |
| 41 } |
| 42 return origins; |
| 43 } catch (e) { |
| 44 console.log(UTIL_fmt('could not parse ' + text)); |
| 45 return []; |
| 46 } |
| 47 } |
| 48 |
| 49 /** |
| 50 * Fetches the app id, and calls a callback with list of allowed origins for it. |
| 51 * @param {string} appId the app id to fetch. |
| 52 * @param {Function} cb called with a list of allowed origins for the app id. |
| 53 */ |
| 54 function fetchAppId(appId, cb) { |
| 55 var origin = getOriginFromUrl(appId); |
| 56 if (!origin) { |
| 57 cb(404, appId); |
| 58 return; |
| 59 } |
| 60 var xhr = new XMLHttpRequest(); |
| 61 var origins = []; |
| 62 xhr.open('GET', appId, true); |
| 63 xhr.onloadend = function() { |
| 64 if (xhr.status != 200) { |
| 65 cb(xhr.status, appId); |
| 66 return; |
| 67 } |
| 68 cb(xhr.status, appId, getOriginsFromJson(xhr.responseText)); |
| 69 }; |
| 70 xhr.send(); |
| 71 } |
| 72 |
| 73 /** |
| 74 * Retrieves a set of distinct app ids from the SignData. |
| 75 * @param {SignData=} signData |
| 76 * @return {Array.<string>} array of distinct app ids. |
| 77 */ |
| 78 function getDistinctAppIds(signData) { |
| 79 var appIds = []; |
| 80 if (!signData) { |
| 81 return appIds; |
| 82 } |
| 83 for (var i = 0, request; request = signData[i]; i++) { |
| 84 var appId = request['appId']; |
| 85 if (appId && appIds.indexOf(appId) == -1) { |
| 86 appIds.push(appId); |
| 87 } |
| 88 } |
| 89 return appIds; |
| 90 } |
| 91 |
| 92 /** |
| 93 * Reorganizes the requests from the SignData to an array of |
| 94 * (appId, [Request]) tuples. |
| 95 * @param {SignData} signData |
| 96 * @return {Array.<[string, Array.<Request>]>} array of |
| 97 * (appId, [Request]) tuples. |
| 98 */ |
| 99 function requestsByAppId(signData) { |
| 100 var requests = {}; |
| 101 var appIdOrder = {}; |
| 102 var orderToAppId = {}; |
| 103 var lastOrder = 0; |
| 104 for (var i = 0, request; request = signData[i]; i++) { |
| 105 var appId = request['appId']; |
| 106 if (appId) { |
| 107 if (!appIdOrder.hasOwnProperty(appId)) { |
| 108 appIdOrder[appId] = lastOrder; |
| 109 orderToAppId[lastOrder] = appId; |
| 110 lastOrder++; |
| 111 } |
| 112 if (requests[appId]) { |
| 113 requests[appId].push(request); |
| 114 } else { |
| 115 requests[appId] = [request]; |
| 116 } |
| 117 } |
| 118 } |
| 119 var orderedRequests = []; |
| 120 for (var order = 0; order < lastOrder; order++) { |
| 121 appId = orderToAppId[order]; |
| 122 orderedRequests.push([appId, requests[appId]]); |
| 123 } |
| 124 return orderedRequests; |
| 125 } |
| 126 |
| 127 /** |
| 128 * Fetches the allowed origins for an appId. |
| 129 * @param {string} appId |
| 130 * @param {boolean} allowHttp Whether http is a valid scheme for an appId. |
| 131 * (This should be false except on test domains.) |
| 132 * @param {function(number, !Array.<string>)} cb Called back with an HTTP |
| 133 * response code and a list of allowed origins for appId. |
| 134 */ |
| 135 function fetchAllowedOriginsForAppId(appId, allowHttp, cb) { |
| 136 var allowedOrigins = []; |
| 137 if (!appId) { |
| 138 cb(200, allowedOrigins); |
| 139 return; |
| 140 } |
| 141 if (appId.indexOf('http://') == 0 && !allowHttp) { |
| 142 console.log(UTIL_fmt('http app ids disallowed, ' + appId + ' requested')); |
| 143 cb(200, allowedOrigins); |
| 144 return; |
| 145 } |
| 146 // TODO(juanlang): hack for old enrolled gnubbies, don't treat |
| 147 // accounts.google.com/login.corp.google.com specially when cryptauth server |
| 148 // stops reporting them as appId. |
| 149 if (appId == 'https://accounts.google.com') { |
| 150 allowedOrigins = ['https://login.corp.google.com']; |
| 151 cb(200, allowedOrigins); |
| 152 return; |
| 153 } |
| 154 if (appId == 'https://login.corp.google.com') { |
| 155 allowedOrigins = ['https://accounts.google.com']; |
| 156 cb(200, allowedOrigins); |
| 157 return; |
| 158 } |
| 159 // Termination of this function relies in fetchAppId completing. |
| 160 // (Not completing would be a bug in XMLHttpRequest.) |
| 161 // TODO(juanlang): provide a termination guarantee, e.g. with a timer? |
| 162 fetchAppId(appId, function(rc, fetchedAppId, origins) { |
| 163 if (rc != 200) { |
| 164 console.log(UTIL_fmt('fetching ' + fetchedAppId + ' failed: ' + rc)); |
| 165 allowedOrigins = []; |
| 166 } else { |
| 167 allowedOrigins = origins; |
| 168 } |
| 169 cb(rc, allowedOrigins); |
| 170 }); |
| 171 } |
| 172 |
| 173 /** |
| 174 * Checks whether an appId is valid for a given origin. |
| 175 * @param {!string} appId |
| 176 * @param {!string} origin |
| 177 * @param {!Array.<string>} allowedOrigins the list of allowed origins for each |
| 178 * appId. |
| 179 * @return {boolean} whether the appId is allowed for the origin. |
| 180 */ |
| 181 function isValidAppIdForOrigin(appId, origin, allowedOrigins) { |
| 182 if (!appId) |
| 183 return false; |
| 184 if (appId == origin) { |
| 185 // trivially allowed |
| 186 return true; |
| 187 } |
| 188 if (!allowedOrigins) |
| 189 return false; |
| 190 return allowedOrigins.indexOf(origin) >= 0; |
| 191 } |
| 192 |
| 193 /** |
| 194 * Returns whether the signData object appears to be valid. |
| 195 * @param {Array.<Object>} signData the signData object. |
| 196 * @return {boolean} whether the object appears valid. |
| 197 */ |
| 198 function isValidSignData(signData) { |
| 199 for (var i = 0; i < signData.length; i++) { |
| 200 var incomingChallenge = signData[i]; |
| 201 if (!incomingChallenge.hasOwnProperty('challenge')) |
| 202 return false; |
| 203 if (!incomingChallenge.hasOwnProperty('appId')) { |
| 204 return false; |
| 205 } |
| 206 if (!incomingChallenge.hasOwnProperty('keyHandle')) |
| 207 return false; |
| 208 if (incomingChallenge['version']) { |
| 209 if (incomingChallenge['version'] != 'U2F_V1' && |
| 210 incomingChallenge['version'] != 'U2F_V2') { |
| 211 return false; |
| 212 } |
| 213 } |
| 214 } |
| 215 return true; |
| 216 } |
| 217 |
| 218 /** Posts the log message to the log url. |
| 219 * @param {string} logMsg the log message to post. |
| 220 * @param {string=} opt_logMsgUrl the url to post log messages to. |
| 221 */ |
| 222 function logMessage(logMsg, opt_logMsgUrl) { |
| 223 console.log(UTIL_fmt('logMessage("' + logMsg + '")')); |
| 224 |
| 225 if (!opt_logMsgUrl) { |
| 226 return; |
| 227 } |
| 228 // Image fetching is not allowed per packaged app CSP. |
| 229 // But video and audio is. |
| 230 var audio = new Audio(); |
| 231 audio.src = opt_logMsgUrl + logMsg; |
| 232 } |
| 233 |
| 234 /** |
| 235 * Logs the result of fetching an appId. |
| 236 * @param {!string} appId |
| 237 * @param {number} millis elapsed time while fetching the appId. |
| 238 * @param {Array.<string>} allowedOrigins the allowed origins retrieved. |
| 239 * @param {string=} opt_logMsgUrl |
| 240 */ |
| 241 function logFetchAppIdResult(appId, millis, allowedOrigins, opt_logMsgUrl) { |
| 242 var logMsg = 'log=fetchappid&appid=' + appId + '&millis=' + millis + |
| 243 '&numorigins=' + allowedOrigins.length; |
| 244 logMessage(logMsg, opt_logMsgUrl); |
| 245 } |
| 246 |
| 247 /** |
| 248 * Logs a mismatch between an origin and an appId. |
| 249 * @param {string} origin |
| 250 * @param {!string} appId |
| 251 * @param {string=} opt_logMsgUrl |
| 252 */ |
| 253 function logInvalidOriginForAppId(origin, appId, opt_logMsgUrl) { |
| 254 var logMsg = 'log=originrejected&origin=' + origin + '&appid=' + appId; |
| 255 logMessage(logMsg, opt_logMsgUrl); |
| 256 } |
| 257 |
| 258 /** |
| 259 * Formats response parameters as an object. |
| 260 * @param {string} type type of the post message. |
| 261 * @param {number} code status code of the operation. |
| 262 * @param {Object=} responseData the response data of the operation. |
| 263 * @return {Object} formatted response. |
| 264 */ |
| 265 function formatWebPageResponse(type, code, responseData) { |
| 266 var responseJsonObject = {}; |
| 267 responseJsonObject['type'] = type; |
| 268 responseJsonObject['code'] = code; |
| 269 if (responseData) |
| 270 responseJsonObject['responseData'] = responseData; |
| 271 return responseJsonObject; |
| 272 } |
| 273 |
| 274 /** |
| 275 * @param {!string} string |
| 276 * @return {Array.<number>} SHA256 hash value of string. |
| 277 */ |
| 278 function sha256HashOfString(string) { |
| 279 var s = new SHA256(); |
| 280 s.update(UTIL_StringToBytes(string)); |
| 281 return s.digest(); |
| 282 } |
| 283 |
| 284 /** |
| 285 * Normalizes the TLS channel ID value: |
| 286 * 1. Converts semantically empty values (undefined, null, 0) to the empty |
| 287 * string. |
| 288 * 2. Converts valid JSON strings to a JS object. |
| 289 * 3. Otherwise, returns the input value unmodified. |
| 290 * @param {Object|string|undefined} opt_tlsChannelId |
| 291 * @return {Object|string} The normalized TLS channel ID value. |
| 292 */ |
| 293 function tlsChannelIdValue(opt_tlsChannelId) { |
| 294 if (!opt_tlsChannelId) { |
| 295 // Case 1: Always set some value for TLS channel ID, even if it's the empty |
| 296 // string: this browser definitely supports them. |
| 297 return ''; |
| 298 } |
| 299 if (typeof opt_tlsChannelId === 'string') { |
| 300 try { |
| 301 var obj = JSON.parse(opt_tlsChannelId); |
| 302 if (!obj) { |
| 303 // Case 1: The string value 'null' parses as the Javascript object null, |
| 304 // so return an empty string: the browser definitely supports TLS |
| 305 // channel id. |
| 306 return ''; |
| 307 } |
| 308 // Case 2: return the value as a JS object. |
| 309 return /** @type {Object} */ (obj); |
| 310 } catch (e) { |
| 311 console.warn('Unparseable TLS channel ID value ' + opt_tlsChannelId); |
| 312 // Case 3: return the value unmodified. |
| 313 } |
| 314 } |
| 315 return opt_tlsChannelId; |
| 316 } |
| 317 |
| 318 /** |
| 319 * Creates a browser data object with the given values. |
| 320 * @param {!string} type A string representing the "type" of this browser data |
| 321 * object. |
| 322 * @param {!string} serverChallenge The server's challenge, as a base64- |
| 323 * encoded string. |
| 324 * @param {!string} origin The server's origin, as seen by the browser. |
| 325 * @param {Object|string|undefined} opt_tlsChannelId |
| 326 * @return {string} A string representation of the browser data object. |
| 327 */ |
| 328 function makeBrowserData(type, serverChallenge, origin, opt_tlsChannelId) { |
| 329 var browserData = { |
| 330 'typ' : type, |
| 331 'challenge' : serverChallenge, |
| 332 'origin' : origin |
| 333 }; |
| 334 browserData['cid_pubkey'] = tlsChannelIdValue(opt_tlsChannelId); |
| 335 return JSON.stringify(browserData); |
| 336 } |
| 337 |
| 338 /** |
| 339 * Creates a browser data object for an enroll request with the given values. |
| 340 * @param {!string} serverChallenge The server's challenge, as a base64- |
| 341 * encoded string. |
| 342 * @param {!string} origin The server's origin, as seen by the browser. |
| 343 * @param {Object|string|undefined} opt_tlsChannelId |
| 344 * @return {string} A string representation of the browser data object. |
| 345 */ |
| 346 function makeEnrollBrowserData(serverChallenge, origin, opt_tlsChannelId) { |
| 347 return makeBrowserData( |
| 348 'navigator.id.finishEnrollment', serverChallenge, origin, |
| 349 opt_tlsChannelId); |
| 350 } |
| 351 |
| 352 /** |
| 353 * Creates a browser data object for a sign request with the given values. |
| 354 * @param {!string} serverChallenge The server's challenge, as a base64- |
| 355 * encoded string. |
| 356 * @param {!string} origin The server's origin, as seen by the browser. |
| 357 * @param {Object|string|undefined} opt_tlsChannelId |
| 358 * @return {string} A string representation of the browser data object. |
| 359 */ |
| 360 function makeSignBrowserData(serverChallenge, origin, opt_tlsChannelId) { |
| 361 return makeBrowserData( |
| 362 'navigator.id.getAssertion', serverChallenge, origin, opt_tlsChannelId); |
| 363 } |
| 364 |
| 365 /** |
| 366 * @param {string} browserData |
| 367 * @param {string} appId |
| 368 * @param {string} encodedKeyHandle |
| 369 * @param {string=} version |
| 370 * @return {SignHelperChallenge} |
| 371 */ |
| 372 function makeChallenge(browserData, appId, encodedKeyHandle, version) { |
| 373 var appIdHash = B64_encode(sha256HashOfString(appId)); |
| 374 var browserDataHash = B64_encode(sha256HashOfString(browserData)); |
| 375 var keyHandle = encodedKeyHandle; |
| 376 |
| 377 var challenge = { |
| 378 'challengeHash': browserDataHash, |
| 379 'appIdHash': appIdHash, |
| 380 'keyHandle': keyHandle |
| 381 }; |
| 382 // Version is implicitly U2F_V1 if not specified. |
| 383 challenge['version'] = (version || 'U2F_V1'); |
| 384 return challenge; |
| 385 } |
OLD | NEW |