blob: 6c77e3f3b7d42f1bbcd4efcc11b5440c7cfe2b03 [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.
*/
/**
* FIDO U2F Javascript API Version
* @const
* @type {number}
*/
var JS_API_VERSION = 1.1;
/**
* 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;
}
/**
* Returns whether the registered key appears to be valid.
* @param {Object} registeredKey The registered key object.
* @param {boolean} appIdRequired Whether the appId property is required on
* each challenge.
* @return {boolean} Whether the object appears valid.
*/
function isValidRegisteredKey(registeredKey, appIdRequired) {
if (appIdRequired && !registeredKey.hasOwnProperty('appId')) {
return false;
}
if (!registeredKey.hasOwnProperty('keyHandle'))
return false;
if (registeredKey['version']) {
if (registeredKey['version'] != 'U2F_V1' &&
registeredKey['version'] != 'U2F_V2') {
return false;
}
}
return true;
}
/**
* Returns whether the array of registered keys appears to be valid.
* @param {Array<Object>} registeredKeys The array of registered keys.
* @param {boolean} appIdRequired Whether the appId property is required on
* each challenge.
* @return {boolean} Whether the array appears valid.
*/
function isValidRegisteredKeyArray(registeredKeys, appIdRequired) {
return registeredKeys.every(function(key) {
return isValidRegisteredKey(key, appIdRequired);
});
}
/**
* Gets the sign challenges from the request. The sign challenges may be the
* U2F 1.0 variant, signRequests, or the U2F 1.1 version, registeredKeys.
* @param {Object} request The request.
* @return {!Array<SignChallenge>|undefined} The sign challenges, if found.
*/
function getSignChallenges(request) {
if (!request) {
return undefined;
}
var signChallenges;
if (request.hasOwnProperty('signRequests')) {
signChallenges = request['signRequests'];
} else if (request.hasOwnProperty('registeredKeys')) {
signChallenges = request['registeredKeys'];
}
return signChallenges;
}
/**
* Returns whether the array of SignChallenges appears to be valid.
* @param {Array<SignChallenge>} signChallenges The array of sign challenges.
* @param {boolean} challengeValueRequired Whether each challenge object
* requires a challenge value.
* @param {boolean} appIdRequired Whether the appId property is required on
* each challenge.
* @return {boolean} Whether the array appears valid.
*/
function isValidSignChallengeArray(signChallenges, challengeValueRequired,
appIdRequired) {
for (var i = 0; i < signChallenges.length; i++) {
var incomingChallenge = signChallenges[i];
if (challengeValueRequired &&
!incomingChallenge.hasOwnProperty('challenge'))
return false;
if (!isValidRegisteredKey(incomingChallenge, appIdRequired)) {
return false;
}
}
return true;
}
/**
* @param {Object} request Request object
* @param {MessageSender} sender Sender frame
* @param {Function} sendResponse Response callback
* @return {?Closeable} Optional handler object that should be closed when port
* closes
*/
function handleWebPageRequest(request, sender, sendResponse) {
switch (request.type) {
case MessageTypes.U2F_REGISTER_REQUEST:
return handleU2fEnrollRequest(sender, request, sendResponse);
case MessageTypes.U2F_SIGN_REQUEST:
return handleU2fSignRequest(sender, request, sendResponse);
case MessageTypes.U2F_GET_API_VERSION_REQUEST:
sendResponse(
makeU2fGetApiVersionResponse(request, JS_API_VERSION,
MessageTypes.U2F_GET_API_VERSION_RESPONSE));
return null;
default:
sendResponse(
makeU2fErrorResponse(request, ErrorCodes.BAD_REQUEST, undefined,
MessageTypes.U2F_REGISTER_RESPONSE));
return null;
}
}
/**
* Makes a response to a request.
* @param {Object} request The request to make a response to.
* @param {string} responseSuffix How to name the response's type.
* @param {string=} opt_defaultType The default response type, if none is
* present in the request.
* @return {Object} The response object.
*/
function makeResponseForRequest(request, responseSuffix, opt_defaultType) {
var type;
if (request && request.type) {
type = request.type.replace(/_request$/, responseSuffix);
} else {
type = opt_defaultType;
}
var reply = { 'type': type };
if (request && request.requestId) {
reply.requestId = request.requestId;
}
return reply;
}
/**
* Makes a response to a U2F request with an error code.
* @param {Object} request The request to make a response to.
* @param {ErrorCodes} code The error code to return.
* @param {string=} opt_detail An error detail string.
* @param {string=} opt_defaultType The default response type, if none is
* present in the request.
* @return {Object} The U2F error.
*/
function makeU2fErrorResponse(request, code, opt_detail, opt_defaultType) {
var reply = makeResponseForRequest(request, '_response', opt_defaultType);
var error = {'errorCode': code};
if (opt_detail) {
error['errorMessage'] = opt_detail;
}
reply['responseData'] = error;
return reply;
}
/**
* Makes a success response to a web request with a responseData object.
* @param {Object} request The request to make a response to.
* @param {Object} responseData The response data.
* @return {Object} The web error.
*/
function makeU2fSuccessResponse(request, responseData) {
var reply = makeResponseForRequest(request, '_response');
reply['responseData'] = responseData;
return reply;
}
/**
* Maps a helper's error code from the DeviceStatusCodes namespace to a
* U2fError.
* @param {number} code Error code from DeviceStatusCodes namespace.
* @return {U2fError} An error.
*/
function mapDeviceStatusCodeToU2fError(code) {
switch (code) {
case DeviceStatusCodes.WRONG_DATA_STATUS:
return {errorCode: ErrorCodes.DEVICE_INELIGIBLE};
case DeviceStatusCodes.TIMEOUT_STATUS:
case DeviceStatusCodes.WAIT_TOUCH_STATUS:
return {errorCode: ErrorCodes.TIMEOUT};
default:
var reportedError = {
errorCode: ErrorCodes.OTHER_ERROR,
errorMessage: 'device status code: ' + code.toString(16)
};
return reportedError;
}
}
/**
* Sends a response, using the given sentinel to ensure at most one response is
* sent. Also closes the closeable, if it's given.
* @param {boolean} sentResponse Whether a response has already been sent.
* @param {?Closeable} closeable A thing to close.
* @param {*} response The response to send.
* @param {Function} sendResponse A function to send the response.
*/
function sendResponseOnce(sentResponse, closeable, response, sendResponse) {
if (closeable) {
closeable.close();
}
if (!sentResponse) {
sentResponse = true;
try {
// If the page has gone away or the connection has otherwise gone,
// sendResponse fails.
sendResponse(response);
} catch (exception) {
console.warn('sendResponse failed: ' + exception);
}
} else {
console.warn(UTIL_fmt('Tried to reply more than once!'));
}
}
/**
* @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();
}
var UNUSED_CID_PUBKEY_VALUE = 'unused';
/**
* 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 UNUSED_CID_PUBKEY_VALUE;
}
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 UNUSED_CID_PUBKEY_VALUE;
}
// 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
};
if (BROWSER_SUPPORTS_TLS_CHANNEL_ID) {
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);
}
/**
* Makes a response to a U2F request with an error code.
* @param {Object} request The request to make a response to.
* @param {number=} version The JS API version to return.
* @param {string=} opt_defaultType The default response type, if none is
* present in the request.
* @return {Object} The GetJsApiVersionResponse.
*/
function makeU2fGetApiVersionResponse(request, version, opt_defaultType) {
var reply = makeResponseForRequest(request, '_response', opt_defaultType);
var data = {'js_api_version': version};
reply['responseData'] = data;
return reply;
}
/**
* Encodes the sign data as an array of sign helper challenges.
* @param {Array<SignChallenge>} signChallenges The sign challenges to encode.
* @param {string|undefined} opt_defaultChallenge A default sign challenge
* value, if a request does not provide one.
* @param {string=} opt_defaultAppId The app id to use for each challenge, if
* the challenge contains none.
* @param {function(string, string): string=} opt_challengeHashFunction
* A function that produces, from a key handle and a raw challenge, a hash
* of the raw challenge. If none is provided, a default hash function is
* used.
* @return {!Array<SignHelperChallenge>} The sign challenges, encoded.
*/
function encodeSignChallenges(signChallenges, opt_defaultChallenge,
opt_defaultAppId, opt_challengeHashFunction) {
function encodedSha256(keyHandle, challenge) {
return B64_encode(sha256HashOfString(challenge));
}
var challengeHashFn = opt_challengeHashFunction || encodedSha256;
var encodedSignChallenges = [];
if (signChallenges) {
for (var i = 0; i < signChallenges.length; i++) {
var challenge = signChallenges[i];
var keyHandle = challenge['keyHandle'];
var challengeValue;
if (challenge.hasOwnProperty('challenge')) {
challengeValue = challenge['challenge'];
} else {
challengeValue = opt_defaultChallenge;
}
var challengeHash = challengeHashFn(keyHandle, challengeValue);
var appId;
if (challenge.hasOwnProperty('appId')) {
appId = challenge['appId'];
} else {
appId = opt_defaultAppId;
}
var encodedChallenge = {
'challengeHash': challengeHash,
'appIdHash': B64_encode(sha256HashOfString(appId)),
'keyHandle': keyHandle,
'version': (challenge['version'] || 'U2F_V1')
};
encodedSignChallenges.push(encodedChallenge);
}
}
return encodedSignChallenges;
}
/**
* Makes a sign helper request from an array of challenges.
* @param {Array<SignHelperChallenge>} challenges The sign challenges.
* @param {number=} opt_timeoutSeconds Timeout value.
* @param {string=} opt_logMsgUrl URL to log to.
* @return {SignHelperRequest} The sign helper request.
*/
function makeSignHelperRequest(challenges, opt_timeoutSeconds, opt_logMsgUrl) {
var request = {
'type': 'sign_helper_request',
'signData': challenges,
'timeout': opt_timeoutSeconds || 0,
'timeoutSeconds': opt_timeoutSeconds || 0
};
if (opt_logMsgUrl !== undefined) {
request.logMsgUrl = opt_logMsgUrl;
}
return request;
}