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 * Gets the scheme + origin from a web url.
12 * @param {string} url Input url
13 * @return {?string} Scheme and origin part if url parses
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);
23 if (origin
== 'http:' || origin
== 'https:')
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
33 function getOriginsFromJson(text
) {
35 var urls
= JSON
.parse(text
);
37 for (var i
= 0, url
; url
= urls
[i
]; i
++) {
38 var origin
= getOriginFromUrl(url
);
44 console
.log(UTIL_fmt('could not parse ' + text
));
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.
54 function fetchAppId(appId
, cb
) {
55 var origin
= getOriginFromUrl(appId
);
60 var xhr
= new XMLHttpRequest();
62 xhr
.open('GET', appId
, true);
63 xhr
.onloadend = function() {
64 if (xhr
.status
!= 200) {
65 cb(xhr
.status
, appId
);
68 cb(xhr
.status
, appId
, getOriginsFromJson(xhr
.responseText
));
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.
78 function getDistinctAppIds(signData
) {
83 for (var i
= 0, request
; request
= signData
[i
]; i
++) {
84 var appId
= request
['appId'];
85 if (appId
&& appIds
.indexOf(appId
) == -1) {
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.
99 function requestsByAppId(signData
) {
102 var orderToAppId
= {};
104 for (var i
= 0, request
; request
= signData
[i
]; i
++) {
105 var appId
= request
['appId'];
107 if (!appIdOrder
.hasOwnProperty(appId
)) {
108 appIdOrder
[appId
] = lastOrder
;
109 orderToAppId
[lastOrder
] = appId
;
112 if (requests
[appId
]) {
113 requests
[appId
].push(request
);
115 requests
[appId
] = [request
];
119 var orderedRequests
= [];
120 for (var order
= 0; order
< lastOrder
; order
++) {
121 appId
= orderToAppId
[order
];
122 orderedRequests
.push([appId
, requests
[appId
]]);
124 return orderedRequests
;
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.
135 function fetchAllowedOriginsForAppId(appId
, allowHttp
, cb
) {
136 var allowedOrigins
= [];
138 cb(200, allowedOrigins
);
141 if (appId
.indexOf('http://') == 0 && !allowHttp
) {
142 console
.log(UTIL_fmt('http app ids disallowed, ' + appId
+ ' requested'));
143 cb(200, allowedOrigins
);
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
);
154 if (appId
== 'https://login.corp.google.com') {
155 allowedOrigins
= ['https://accounts.google.com'];
156 cb(200, allowedOrigins
);
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
) {
164 console
.log(UTIL_fmt('fetching ' + fetchedAppId
+ ' failed: ' + rc
));
167 allowedOrigins
= origins
;
169 cb(rc
, allowedOrigins
);
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
179 * @return {boolean} whether the appId is allowed for the origin.
181 function isValidAppIdForOrigin(appId
, origin
, allowedOrigins
) {
184 if (appId
== origin
) {
190 return allowedOrigins
.indexOf(origin
) >= 0;
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.
198 function isValidSignData(signData
) {
199 for (var i
= 0; i
< signData
.length
; i
++) {
200 var incomingChallenge
= signData
[i
];
201 if (!incomingChallenge
.hasOwnProperty('challenge'))
203 if (!incomingChallenge
.hasOwnProperty('appId')) {
206 if (!incomingChallenge
.hasOwnProperty('keyHandle'))
208 if (incomingChallenge
['version']) {
209 if (incomingChallenge
['version'] != 'U2F_V1' &&
210 incomingChallenge
['version'] != 'U2F_V2') {
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.
222 function logMessage(logMsg
, opt_logMsgUrl
) {
223 console
.log(UTIL_fmt('logMessage("' + logMsg
+ '")'));
225 if (!opt_logMsgUrl
) {
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
;
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.
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
);
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
253 function logInvalidOriginForAppId(origin
, appId
, opt_logMsgUrl
) {
254 var logMsg
= 'log=originrejected&origin=' + origin
+ '&appid=' + appId
;
255 logMessage(logMsg
, opt_logMsgUrl
);
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.
265 function formatWebPageResponse(type
, code
, responseData
) {
266 var responseJsonObject
= {};
267 responseJsonObject
['type'] = type
;
268 responseJsonObject
['code'] = code
;
270 responseJsonObject
['responseData'] = responseData
;
271 return responseJsonObject
;
275 * @param {!string} string Input string
276 * @return {Array.<number>} SHA256 hash value of string.
278 function sha256HashOfString(string
) {
279 var s
= new SHA256();
280 s
.update(UTIL_StringToBytes(string
));
285 * Normalizes the TLS channel ID value:
286 * 1. Converts semantically empty values (undefined, null, 0) to the empty
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.
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.
299 if (typeof opt_tlsChannelId
=== 'string') {
301 var obj
= JSON
.parse(opt_tlsChannelId
);
303 // Case 1: The string value 'null' parses as the Javascript object null,
304 // so return an empty string: the browser definitely supports TLS
308 // Case 2: return the value as a JS object.
309 return /** @type {Object} */ (obj
);
311 console
.warn('Unparseable TLS channel ID value ' + opt_tlsChannelId
);
312 // Case 3: return the value unmodified.
315 return opt_tlsChannelId
;
319 * Creates a browser data object with the given values.
320 * @param {!string} type A string representing the "type" of this browser data
322 * @param {!string} serverChallenge The server's challenge, as a base64-
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.
328 function makeBrowserData(type
, serverChallenge
, origin
, opt_tlsChannelId
) {
331 'challenge' : serverChallenge
,
334 browserData
['cid_pubkey'] = tlsChannelIdValue(opt_tlsChannelId
);
335 return JSON
.stringify(browserData
);
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-
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.
346 function makeEnrollBrowserData(serverChallenge
, origin
, opt_tlsChannelId
) {
347 return makeBrowserData(
348 'navigator.id.finishEnrollment', serverChallenge
, origin
,
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-
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.
360 function makeSignBrowserData(serverChallenge
, origin
, opt_tlsChannelId
) {
361 return makeBrowserData(
362 'navigator.id.getAssertion', serverChallenge
, origin
, opt_tlsChannelId
);
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
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
;
378 'challengeHash': browserDataHash
,
379 'appIdHash': appIdHash
,
380 'keyHandle': keyHandle
382 // Version is implicitly U2F_V1 if not specified.
383 challenge
['version'] = (version
|| 'U2F_V1');