// Copyright 2014 The Chromium Authors. All rights reserved. // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. /** * @fileoverview Does common handling for requests coming from web pages and * routes them to the provided handler. */ /** * Gets the scheme + origin from a web url. * @param {string} url Input url * @return {?string} Scheme and origin part if url parses */ function getOriginFromUrl(url) { var re = new RegExp('^(https?://)[^/]*/?'); var originarray = re.exec(url); if (originarray == null) return originarray; var origin = originarray[0]; while (origin.charAt(origin.length - 1) == '/') { origin = origin.substring(0, origin.length - 1); } if (origin == 'http:' || origin == 'https:') return null; return origin; } /** * Parses the text as JSON and returns it as an array of strings. * @param {string} text Input JSON * @return {Array.} Array of origins */ function getOriginsFromJson(text) { try { var urls = JSON.parse(text); var origins = []; for (var i = 0, url; url = urls[i]; i++) { var origin = getOriginFromUrl(url); if (origin) origins.push(origin); } return origins; } catch (e) { console.log(UTIL_fmt('could not parse ' + text)); return []; } } /** * Fetches the app id, and calls a callback with list of allowed origins for it. * @param {string} appId the app id to fetch. * @param {Function} cb called with a list of allowed origins for the app id. */ function fetchAppId(appId, cb) { var origin = getOriginFromUrl(appId); if (!origin) { cb(404, appId); return; } var xhr = new XMLHttpRequest(); var origins = []; xhr.open('GET', appId, true); xhr.onloadend = function() { if (xhr.status != 200) { cb(xhr.status, appId); return; } cb(xhr.status, appId, getOriginsFromJson(xhr.responseText)); }; xhr.send(); } /** * Retrieves a set of distinct app ids from the SignData. * @param {SignData=} signData Input signature data * @return {Array.} array of distinct app ids. */ function getDistinctAppIds(signData) { var appIds = []; if (!signData) { return appIds; } for (var i = 0, request; request = signData[i]; i++) { var appId = request['appId']; if (appId && appIds.indexOf(appId) == -1) { appIds.push(appId); } } return appIds; } /** * Reorganizes the requests from the SignData to an array of * (appId, [Request]) tuples. * @param {SignData} signData Input signature data * @return {Array.<[string, Array.]>} array of * (appId, [Request]) tuples. */ function requestsByAppId(signData) { var requests = {}; var appIdOrder = {}; var orderToAppId = {}; var lastOrder = 0; for (var i = 0, request; request = signData[i]; i++) { var appId = request['appId']; if (appId) { if (!appIdOrder.hasOwnProperty(appId)) { appIdOrder[appId] = lastOrder; orderToAppId[lastOrder] = appId; lastOrder++; } if (requests[appId]) { requests[appId].push(request); } else { requests[appId] = [request]; } } } var orderedRequests = []; for (var order = 0; order < lastOrder; order++) { appId = orderToAppId[order]; orderedRequests.push([appId, requests[appId]]); } return orderedRequests; } /** * Fetches the allowed origins for an appId. * @param {string} appId Application id * @param {boolean} allowHttp Whether http is a valid scheme for an appId. * (This should be false except on test domains.) * @param {function(number, !Array.)} cb Called back with an HTTP * response code and a list of allowed origins for appId. */ function fetchAllowedOriginsForAppId(appId, allowHttp, cb) { var allowedOrigins = []; if (!appId) { cb(200, allowedOrigins); return; } if (appId.indexOf('http://') == 0 && !allowHttp) { console.log(UTIL_fmt('http app ids disallowed, ' + appId + ' requested')); cb(200, allowedOrigins); return; } // TODO: hack for old enrolled gnubbies, don't treat // accounts.google.com/login.corp.google.com specially when cryptauth server // stops reporting them as appId. if (appId == 'https://accounts.google.com') { allowedOrigins = ['https://login.corp.google.com']; cb(200, allowedOrigins); return; } if (appId == 'https://login.corp.google.com') { allowedOrigins = ['https://accounts.google.com']; cb(200, allowedOrigins); return; } // Termination of this function relies in fetchAppId completing. // (Not completing would be a bug in XMLHttpRequest.) // TODO: provide a termination guarantee, e.g. with a timer? fetchAppId(appId, function(rc, fetchedAppId, origins) { if (rc != 200) { console.log(UTIL_fmt('fetching ' + fetchedAppId + ' failed: ' + rc)); allowedOrigins = []; } else { allowedOrigins = origins; } cb(rc, allowedOrigins); }); } /** * Checks whether an appId is valid for a given origin. * @param {!string} appId Application id * @param {!string} origin Origin * @param {!Array.} allowedOrigins the list of allowed origins for each * appId. * @return {boolean} whether the appId is allowed for the origin. */ function isValidAppIdForOrigin(appId, origin, allowedOrigins) { if (!appId) return false; if (appId == origin) { // trivially allowed return true; } if (!allowedOrigins) return false; return allowedOrigins.indexOf(origin) >= 0; } /** * Returns whether the signData object appears to be valid. * @param {Array.} signData the signData object. * @return {boolean} whether the object appears valid. */ function isValidSignData(signData) { for (var i = 0; i < signData.length; i++) { var incomingChallenge = signData[i]; if (!incomingChallenge.hasOwnProperty('challenge')) return false; if (!incomingChallenge.hasOwnProperty('appId')) { return false; } if (!incomingChallenge.hasOwnProperty('keyHandle')) return false; if (incomingChallenge['version']) { if (incomingChallenge['version'] != 'U2F_V1' && incomingChallenge['version'] != 'U2F_V2') { return false; } } } return true; } /** Posts the log message to the log url. * @param {string} logMsg the log message to post. * @param {string=} opt_logMsgUrl the url to post log messages to. */ function logMessage(logMsg, opt_logMsgUrl) { console.log(UTIL_fmt('logMessage("' + logMsg + '")')); if (!opt_logMsgUrl) { return; } // Image fetching is not allowed per packaged app CSP. // But video and audio is. var audio = new Audio(); audio.src = opt_logMsgUrl + logMsg; } /** * Logs the result of fetching an appId. * @param {!string} appId Application Id * @param {number} millis elapsed time while fetching the appId. * @param {Array.} allowedOrigins the allowed origins retrieved. * @param {string=} opt_logMsgUrl the url to post log messages to. */ function logFetchAppIdResult(appId, millis, allowedOrigins, opt_logMsgUrl) { var logMsg = 'log=fetchappid&appid=' + appId + '&millis=' + millis + '&numorigins=' + allowedOrigins.length; logMessage(logMsg, opt_logMsgUrl); } /** * Logs a mismatch between an origin and an appId. * @param {string} origin Origin * @param {!string} appId Application id * @param {string=} opt_logMsgUrl the url to post log messages to */ function logInvalidOriginForAppId(origin, appId, opt_logMsgUrl) { var logMsg = 'log=originrejected&origin=' + origin + '&appid=' + appId; logMessage(logMsg, opt_logMsgUrl); } /** * Formats response parameters as an object. * @param {string} type type of the post message. * @param {number} code status code of the operation. * @param {Object=} responseData the response data of the operation. * @return {Object} formatted response. */ function formatWebPageResponse(type, code, responseData) { var responseJsonObject = {}; responseJsonObject['type'] = type; responseJsonObject['code'] = code; if (responseData) responseJsonObject['responseData'] = responseData; return responseJsonObject; } /** * @param {!string} string Input string * @return {Array.} SHA256 hash value of string. */ function sha256HashOfString(string) { var s = new SHA256(); s.update(UTIL_StringToBytes(string)); return s.digest(); } /** * Normalizes the TLS channel ID value: * 1. Converts semantically empty values (undefined, null, 0) to the empty * string. * 2. Converts valid JSON strings to a JS object. * 3. Otherwise, returns the input value unmodified. * @param {Object|string|undefined} opt_tlsChannelId TLS Channel id * @return {Object|string} The normalized TLS channel ID value. */ function tlsChannelIdValue(opt_tlsChannelId) { if (!opt_tlsChannelId) { // Case 1: Always set some value for TLS channel ID, even if it's the empty // string: this browser definitely supports them. return ''; } if (typeof opt_tlsChannelId === 'string') { try { var obj = JSON.parse(opt_tlsChannelId); if (!obj) { // Case 1: The string value 'null' parses as the Javascript object null, // so return an empty string: the browser definitely supports TLS // channel id. return ''; } // Case 2: return the value as a JS object. return /** @type {Object} */ (obj); } catch (e) { console.warn('Unparseable TLS channel ID value ' + opt_tlsChannelId); // Case 3: return the value unmodified. } } return opt_tlsChannelId; } /** * Creates a browser data object with the given values. * @param {!string} type A string representing the "type" of this browser data * object. * @param {!string} serverChallenge The server's challenge, as a base64- * encoded string. * @param {!string} origin The server's origin, as seen by the browser. * @param {Object|string|undefined} opt_tlsChannelId TLS Channel Id * @return {string} A string representation of the browser data object. */ function makeBrowserData(type, serverChallenge, origin, opt_tlsChannelId) { var browserData = { 'typ' : type, 'challenge' : serverChallenge, 'origin' : origin }; browserData['cid_pubkey'] = tlsChannelIdValue(opt_tlsChannelId); return JSON.stringify(browserData); } /** * Creates a browser data object for an enroll request with the given values. * @param {!string} serverChallenge The server's challenge, as a base64- * encoded string. * @param {!string} origin The server's origin, as seen by the browser. * @param {Object|string|undefined} opt_tlsChannelId TLS Channel Id * @return {string} A string representation of the browser data object. */ function makeEnrollBrowserData(serverChallenge, origin, opt_tlsChannelId) { return makeBrowserData( 'navigator.id.finishEnrollment', serverChallenge, origin, opt_tlsChannelId); } /** * Creates a browser data object for a sign request with the given values. * @param {!string} serverChallenge The server's challenge, as a base64- * encoded string. * @param {!string} origin The server's origin, as seen by the browser. * @param {Object|string|undefined} opt_tlsChannelId TLS Channel Id * @return {string} A string representation of the browser data object. */ function makeSignBrowserData(serverChallenge, origin, opt_tlsChannelId) { return makeBrowserData( 'navigator.id.getAssertion', serverChallenge, origin, opt_tlsChannelId); } /** * @param {string} browserData Browser data as JSON * @param {string} appId Application Id * @param {string} encodedKeyHandle B64 encoded key handle * @param {string=} version Protocol version * @return {SignHelperChallenge} Challenge object */ function makeChallenge(browserData, appId, encodedKeyHandle, version) { var appIdHash = B64_encode(sha256HashOfString(appId)); var browserDataHash = B64_encode(sha256HashOfString(browserData)); var keyHandle = encodedKeyHandle; var challenge = { 'challengeHash': browserDataHash, 'appIdHash': appIdHash, 'keyHandle': keyHandle }; // Version is implicitly U2F_V1 if not specified. challenge['version'] = (version || 'U2F_V1'); return challenge; }