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.
6 * @fileoverview Does common handling for requests coming from web pages and
7 * routes them to the provided handler.
11 * FIDO U2F Javascript API Version
15 var JS_API_VERSION = 1.1;
18 * Gets the scheme + origin from a web url.
19 * @param {string} url Input url
20 * @return {?string} Scheme and origin part if url parses
22 function getOriginFromUrl(url) {
23 var re = new RegExp('^(https?://)[^/]*/?');
24 var originarray = re.exec(url);
25 if (originarray == null) return originarray;
26 var origin = originarray[0];
27 while (origin.charAt(origin.length - 1) == '/') {
28 origin = origin.substring(0, origin.length - 1);
30 if (origin == 'http:' || origin == 'https:')
36 * Returns whether the registered key appears to be valid.
37 * @param {Object} registeredKey The registered key object.
38 * @param {boolean} appIdRequired Whether the appId property is required on
40 * @return {boolean} Whether the object appears valid.
42 function isValidRegisteredKey(registeredKey, appIdRequired) {
43 if (appIdRequired && !registeredKey.hasOwnProperty('appId')) {
46 if (!registeredKey.hasOwnProperty('keyHandle'))
48 if (registeredKey['version']) {
49 if (registeredKey['version'] != 'U2F_V1' &&
50 registeredKey['version'] != 'U2F_V2') {
58 * Returns whether the array of registered keys appears to be valid.
59 * @param {Array<Object>} registeredKeys The array of registered keys.
60 * @param {boolean} appIdRequired Whether the appId property is required on
62 * @return {boolean} Whether the array appears valid.
64 function isValidRegisteredKeyArray(registeredKeys, appIdRequired) {
65 return registeredKeys.every(function(key) {
66 return isValidRegisteredKey(key, appIdRequired);
71 * Gets the sign challenges from the request. The sign challenges may be the
72 * U2F 1.0 variant, signRequests, or the U2F 1.1 version, registeredKeys.
73 * @param {Object} request The request.
74 * @return {!Array<SignChallenge>|undefined} The sign challenges, if found.
76 function getSignChallenges(request) {
81 if (request.hasOwnProperty('signRequests')) {
82 signChallenges = request['signRequests'];
83 } else if (request.hasOwnProperty('registeredKeys')) {
84 signChallenges = request['registeredKeys'];
86 return signChallenges;
90 * Returns whether the array of SignChallenges appears to be valid.
91 * @param {Array<SignChallenge>} signChallenges The array of sign challenges.
92 * @param {boolean} challengeValueRequired Whether each challenge object
93 * requires a challenge value.
94 * @param {boolean} appIdRequired Whether the appId property is required on
96 * @return {boolean} Whether the array appears valid.
98 function isValidSignChallengeArray(signChallenges, challengeValueRequired,
100 for (var i = 0; i < signChallenges.length; i++) {
101 var incomingChallenge = signChallenges[i];
102 if (challengeValueRequired &&
103 !incomingChallenge.hasOwnProperty('challenge'))
105 if (!isValidRegisteredKey(incomingChallenge, appIdRequired)) {
113 * @param {Object} request Request object
114 * @param {MessageSender} sender Sender frame
115 * @param {Function} sendResponse Response callback
116 * @return {?Closeable} Optional handler object that should be closed when port
119 function handleWebPageRequest(request, sender, sendResponse) {
120 switch (request.type) {
121 case MessageTypes.U2F_REGISTER_REQUEST:
122 return handleU2fEnrollRequest(sender, request, sendResponse);
124 case MessageTypes.U2F_SIGN_REQUEST:
125 return handleU2fSignRequest(sender, request, sendResponse);
127 case MessageTypes.U2F_GET_API_VERSION_REQUEST:
129 makeU2fGetApiVersionResponse(request, JS_API_VERSION,
130 MessageTypes.U2F_GET_API_VERSION_RESPONSE));
135 makeU2fErrorResponse(request, ErrorCodes.BAD_REQUEST, undefined,
136 MessageTypes.U2F_REGISTER_RESPONSE));
142 * Makes a response to a request.
143 * @param {Object} request The request to make a response to.
144 * @param {string} responseSuffix How to name the response's type.
145 * @param {string=} opt_defaultType The default response type, if none is
146 * present in the request.
147 * @return {Object} The response object.
149 function makeResponseForRequest(request, responseSuffix, opt_defaultType) {
151 if (request && request.type) {
152 type = request.type.replace(/_request$/, responseSuffix);
154 type = opt_defaultType;
156 var reply = { 'type': type };
157 if (request && request.requestId) {
158 reply.requestId = request.requestId;
164 * Makes a response to a U2F request with an error code.
165 * @param {Object} request The request to make a response to.
166 * @param {ErrorCodes} code The error code to return.
167 * @param {string=} opt_detail An error detail string.
168 * @param {string=} opt_defaultType The default response type, if none is
169 * present in the request.
170 * @return {Object} The U2F error.
172 function makeU2fErrorResponse(request, code, opt_detail, opt_defaultType) {
173 var reply = makeResponseForRequest(request, '_response', opt_defaultType);
174 var error = {'errorCode': code};
176 error['errorMessage'] = opt_detail;
178 reply['responseData'] = error;
183 * Makes a success response to a web request with a responseData object.
184 * @param {Object} request The request to make a response to.
185 * @param {Object} responseData The response data.
186 * @return {Object} The web error.
188 function makeU2fSuccessResponse(request, responseData) {
189 var reply = makeResponseForRequest(request, '_response');
190 reply['responseData'] = responseData;
195 * Maps a helper's error code from the DeviceStatusCodes namespace to a
197 * @param {number} code Error code from DeviceStatusCodes namespace.
198 * @return {U2fError} An error.
200 function mapDeviceStatusCodeToU2fError(code) {
202 case DeviceStatusCodes.WRONG_DATA_STATUS:
203 return {errorCode: ErrorCodes.DEVICE_INELIGIBLE};
205 case DeviceStatusCodes.TIMEOUT_STATUS:
206 case DeviceStatusCodes.WAIT_TOUCH_STATUS:
207 return {errorCode: ErrorCodes.TIMEOUT};
210 var reportedError = {
211 errorCode: ErrorCodes.OTHER_ERROR,
212 errorMessage: 'device status code: ' + code.toString(16)
214 return reportedError;
219 * Sends a response, using the given sentinel to ensure at most one response is
220 * sent. Also closes the closeable, if it's given.
221 * @param {boolean} sentResponse Whether a response has already been sent.
222 * @param {?Closeable} closeable A thing to close.
223 * @param {*} response The response to send.
224 * @param {Function} sendResponse A function to send the response.
226 function sendResponseOnce(sentResponse, closeable, response, sendResponse) {
233 // If the page has gone away or the connection has otherwise gone,
234 // sendResponse fails.
235 sendResponse(response);
236 } catch (exception) {
237 console.warn('sendResponse failed: ' + exception);
240 console.warn(UTIL_fmt('Tried to reply more than once! Juan, FIX ME'));
245 * @param {!string} string Input string
246 * @return {Array<number>} SHA256 hash value of string.
248 function sha256HashOfString(string) {
249 var s = new SHA256();
250 s.update(UTIL_StringToBytes(string));
255 * Normalizes the TLS channel ID value:
256 * 1. Converts semantically empty values (undefined, null, 0) to the empty
258 * 2. Converts valid JSON strings to a JS object.
259 * 3. Otherwise, returns the input value unmodified.
260 * @param {Object|string|undefined} opt_tlsChannelId TLS Channel id
261 * @return {Object|string} The normalized TLS channel ID value.
263 function tlsChannelIdValue(opt_tlsChannelId) {
264 if (!opt_tlsChannelId) {
265 // Case 1: Always set some value for TLS channel ID, even if it's the empty
266 // string: this browser definitely supports them.
269 if (typeof opt_tlsChannelId === 'string') {
271 var obj = JSON.parse(opt_tlsChannelId);
273 // Case 1: The string value 'null' parses as the Javascript object null,
274 // so return an empty string: the browser definitely supports TLS
278 // Case 2: return the value as a JS object.
279 return /** @type {Object} */ (obj);
281 console.warn('Unparseable TLS channel ID value ' + opt_tlsChannelId);
282 // Case 3: return the value unmodified.
285 return opt_tlsChannelId;
289 * Creates a browser data object with the given values.
290 * @param {!string} type A string representing the "type" of this browser data
292 * @param {!string} serverChallenge The server's challenge, as a base64-
294 * @param {!string} origin The server's origin, as seen by the browser.
295 * @param {Object|string|undefined} opt_tlsChannelId TLS Channel Id
296 * @return {string} A string representation of the browser data object.
298 function makeBrowserData(type, serverChallenge, origin, opt_tlsChannelId) {
301 'challenge' : serverChallenge,
304 if (BROWSER_SUPPORTS_TLS_CHANNEL_ID) {
305 browserData['cid_pubkey'] = tlsChannelIdValue(opt_tlsChannelId);
307 return JSON.stringify(browserData);
311 * Creates a browser data object for an enroll request with the given values.
312 * @param {!string} serverChallenge The server's challenge, as a base64-
314 * @param {!string} origin The server's origin, as seen by the browser.
315 * @param {Object|string|undefined} opt_tlsChannelId TLS Channel Id
316 * @return {string} A string representation of the browser data object.
318 function makeEnrollBrowserData(serverChallenge, origin, opt_tlsChannelId) {
319 return makeBrowserData(
320 'navigator.id.finishEnrollment', serverChallenge, origin,
325 * Creates a browser data object for a sign request with the given values.
326 * @param {!string} serverChallenge The server's challenge, as a base64-
328 * @param {!string} origin The server's origin, as seen by the browser.
329 * @param {Object|string|undefined} opt_tlsChannelId TLS Channel Id
330 * @return {string} A string representation of the browser data object.
332 function makeSignBrowserData(serverChallenge, origin, opt_tlsChannelId) {
333 return makeBrowserData(
334 'navigator.id.getAssertion', serverChallenge, origin, opt_tlsChannelId);
338 * Makes a response to a U2F request with an error code.
339 * @param {Object} request The request to make a response to.
340 * @param {number=} version The JS API version to return.
341 * @param {string=} opt_defaultType The default response type, if none is
342 * present in the request.
343 * @return {Object} The GetJsApiVersionResponse.
345 function makeU2fGetApiVersionResponse(request, version, opt_defaultType) {
346 var reply = makeResponseForRequest(request, '_response', opt_defaultType);
347 var data = {'js_api_version': version};
348 reply['responseData'] = data;
353 * Encodes the sign data as an array of sign helper challenges.
354 * @param {Array<SignChallenge>} signChallenges The sign challenges to encode.
355 * @param {string|undefined} opt_defaultChallenge A default sign challenge
356 * value, if a request does not provide one.
357 * @param {string=} opt_defaultAppId The app id to use for each challenge, if
358 * the challenge contains none.
359 * @param {function(string, string): string=} opt_challengeHashFunction
360 * A function that produces, from a key handle and a raw challenge, a hash
361 * of the raw challenge. If none is provided, a default hash function is
363 * @return {!Array<SignHelperChallenge>} The sign challenges, encoded.
365 function encodeSignChallenges(signChallenges, opt_defaultChallenge,
366 opt_defaultAppId, opt_challengeHashFunction) {
367 function encodedSha256(keyHandle, challenge) {
368 return B64_encode(sha256HashOfString(challenge));
370 var challengeHashFn = opt_challengeHashFunction || encodedSha256;
371 var encodedSignChallenges = [];
372 if (signChallenges) {
373 for (var i = 0; i < signChallenges.length; i++) {
374 var challenge = signChallenges[i];
375 var keyHandle = challenge['keyHandle'];
377 if (challenge.hasOwnProperty('challenge')) {
378 challengeValue = challenge['challenge'];
380 challengeValue = opt_defaultChallenge;
382 var challengeHash = challengeHashFn(keyHandle, challengeValue);
384 if (challenge.hasOwnProperty('appId')) {
385 appId = challenge['appId'];
387 appId = opt_defaultAppId;
389 var encodedChallenge = {
390 'challengeHash': challengeHash,
391 'appIdHash': B64_encode(sha256HashOfString(appId)),
392 'keyHandle': keyHandle,
393 'version': (challenge['version'] || 'U2F_V1')
395 encodedSignChallenges.push(encodedChallenge);
398 return encodedSignChallenges;
402 * Makes a sign helper request from an array of challenges.
403 * @param {Array<SignHelperChallenge>} challenges The sign challenges.
404 * @param {number=} opt_timeoutSeconds Timeout value.
405 * @param {string=} opt_logMsgUrl URL to log to.
406 * @return {SignHelperRequest} The sign helper request.
408 function makeSignHelperRequest(challenges, opt_timeoutSeconds, opt_logMsgUrl) {
410 'type': 'sign_helper_request',
411 'signData': challenges,
412 'timeout': opt_timeoutSeconds || 0,
413 'timeoutSeconds': opt_timeoutSeconds || 0
415 if (opt_logMsgUrl !== undefined) {
416 request.logMsgUrl = opt_logMsgUrl;