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 */ 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 Input JSON 31 * @return {Array.<string>} Array of origins 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 Input signature data 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 Input signature data 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 Application id 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: 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: 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 Application id 176 * @param {!string} origin 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 Application Id 237 * @param {number} millis elapsed time while fetching the appId. 238 * @param {Array.<string>} allowedOrigins the allowed origins retrieved. 239 * @param {string=} opt_logMsgUrl the url to post log messages to. 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 Origin 250 * @param {!string} appId Application id 251 * @param {string=} opt_logMsgUrl the url to post log messages to 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 Input 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 TLS Channel id 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 TLS Channel Id 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 TLS Channel Id 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 TLS Channel Id 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 Browser data as JSON 367 * @param {string} appId Application Id 368 * @param {string} encodedKeyHandle B64 encoded key handle 369 * @param {string=} version Protocol version 370 * @return {SignHelperChallenge} Challenge object 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 } 386