blob: 3dd5a0a8848001968a72a43c21f521d369d3626c [file] [log] [blame]
// 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.<string>} 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.<string>} 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.<Request>]>} 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.<string>)} 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.<string>} 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.<Object>} 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.<string>} 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.<number>} 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;
}