Merge Chromium + Blink git repositories
[chromium-blink-merge.git] / chrome / browser / resources / cryptotoken / webrequest.js
blob0564559c2330ffde0a79d19065084e711434c406
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.
5 /**
6  * @fileoverview Does common handling for requests coming from web pages and
7  * routes them to the provided handler.
8  */
10 /**
11  * FIDO U2F Javascript API Version
12  * @const
13  * @type {number}
14  */
15 var JS_API_VERSION = 1.1;
17 /**
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
21  */
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);
29   }
30   if (origin == 'http:' || origin == 'https:')
31     return null;
32   return origin;
35 /**
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
39  *     each challenge.
40  * @return {boolean} Whether the object appears valid.
41  */
42 function isValidRegisteredKey(registeredKey, appIdRequired) {
43   if (appIdRequired && !registeredKey.hasOwnProperty('appId')) {
44     return false;
45   }
46   if (!registeredKey.hasOwnProperty('keyHandle'))
47     return false;
48   if (registeredKey['version']) {
49     if (registeredKey['version'] != 'U2F_V1' &&
50         registeredKey['version'] != 'U2F_V2') {
51       return false;
52     }
53   }
54   return true;
57 /**
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
61  *     each challenge.
62  * @return {boolean} Whether the array appears valid.
63  */
64 function isValidRegisteredKeyArray(registeredKeys, appIdRequired) {
65   return registeredKeys.every(function(key) {
66     return isValidRegisteredKey(key, appIdRequired);
67   });
70 /**
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.
75  */
76 function getSignChallenges(request) {
77   if (!request) {
78     return undefined;
79   }
80   var signChallenges;
81   if (request.hasOwnProperty('signRequests')) {
82     signChallenges = request['signRequests'];
83   } else if (request.hasOwnProperty('registeredKeys')) {
84     signChallenges = request['registeredKeys'];
85   }
86   return signChallenges;
89 /**
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
95  *     each challenge.
96  * @return {boolean} Whether the array appears valid.
97  */
98 function isValidSignChallengeArray(signChallenges, challengeValueRequired,
99     appIdRequired) {
100   for (var i = 0; i < signChallenges.length; i++) {
101     var incomingChallenge = signChallenges[i];
102     if (challengeValueRequired &&
103         !incomingChallenge.hasOwnProperty('challenge'))
104       return false;
105     if (!isValidRegisteredKey(incomingChallenge, appIdRequired)) {
106       return false;
107     }
108   }
109   return true;
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
117  *     closes
118  */
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:
128       sendResponse(
129           makeU2fGetApiVersionResponse(request, JS_API_VERSION,
130               MessageTypes.U2F_GET_API_VERSION_RESPONSE));
131       return null;
133     default:
134       sendResponse(
135           makeU2fErrorResponse(request, ErrorCodes.BAD_REQUEST, undefined,
136               MessageTypes.U2F_REGISTER_RESPONSE));
137       return null;
138   }
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.
148  */
149 function makeResponseForRequest(request, responseSuffix, opt_defaultType) {
150   var type;
151   if (request && request.type) {
152     type = request.type.replace(/_request$/, responseSuffix);
153   } else {
154     type = opt_defaultType;
155   }
156   var reply = { 'type': type };
157   if (request && request.requestId) {
158     reply.requestId = request.requestId;
159   }
160   return reply;
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.
171  */
172 function makeU2fErrorResponse(request, code, opt_detail, opt_defaultType) {
173   var reply = makeResponseForRequest(request, '_response', opt_defaultType);
174   var error = {'errorCode': code};
175   if (opt_detail) {
176     error['errorMessage'] = opt_detail;
177   }
178   reply['responseData'] = error;
179   return reply;
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.
187  */
188 function makeU2fSuccessResponse(request, responseData) {
189   var reply = makeResponseForRequest(request, '_response');
190   reply['responseData'] = responseData;
191   return reply;
195  * Maps a helper's error code from the DeviceStatusCodes namespace to a
196  * U2fError.
197  * @param {number} code Error code from DeviceStatusCodes namespace.
198  * @return {U2fError} An error.
199  */
200 function mapDeviceStatusCodeToU2fError(code) {
201   switch (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};
209     default:
210       var reportedError = {
211         errorCode: ErrorCodes.OTHER_ERROR,
212         errorMessage: 'device status code: ' + code.toString(16)
213       };
214       return reportedError;
215   }
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.
225  */
226 function sendResponseOnce(sentResponse, closeable, response, sendResponse) {
227   if (closeable) {
228     closeable.close();
229   }
230   if (!sentResponse) {
231     sentResponse = true;
232     try {
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);
238     }
239   } else {
240     console.warn(UTIL_fmt('Tried to reply more than once! Juan, FIX ME'));
241   }
245  * @param {!string} string Input string
246  * @return {Array<number>} SHA256 hash value of string.
247  */
248 function sha256HashOfString(string) {
249   var s = new SHA256();
250   s.update(UTIL_StringToBytes(string));
251   return s.digest();
255  * Normalizes the TLS channel ID value:
256  * 1. Converts semantically empty values (undefined, null, 0) to the empty
257  *     string.
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.
262  */
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.
267     return '';
268   }
269   if (typeof opt_tlsChannelId === 'string') {
270     try {
271       var obj = JSON.parse(opt_tlsChannelId);
272       if (!obj) {
273         // Case 1: The string value 'null' parses as the Javascript object null,
274         // so return an empty string: the browser definitely supports TLS
275         // channel id.
276         return '';
277       }
278       // Case 2: return the value as a JS object.
279       return /** @type {Object} */ (obj);
280     } catch (e) {
281       console.warn('Unparseable TLS channel ID value ' + opt_tlsChannelId);
282       // Case 3: return the value unmodified.
283     }
284   }
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
291  *     object.
292  * @param {!string} serverChallenge The server's challenge, as a base64-
293  *     encoded string.
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.
297  */
298 function makeBrowserData(type, serverChallenge, origin, opt_tlsChannelId) {
299   var browserData = {
300     'typ' : type,
301     'challenge' : serverChallenge,
302     'origin' : origin
303   };
304   if (BROWSER_SUPPORTS_TLS_CHANNEL_ID) {
305     browserData['cid_pubkey'] = tlsChannelIdValue(opt_tlsChannelId);
306   }
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-
313  *     encoded string.
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.
317  */
318 function makeEnrollBrowserData(serverChallenge, origin, opt_tlsChannelId) {
319   return makeBrowserData(
320       'navigator.id.finishEnrollment', serverChallenge, origin,
321       opt_tlsChannelId);
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-
327  *     encoded string.
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.
331  */
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.
344  */
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;
349   return reply;
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
362  *     used.
363  * @return {!Array<SignHelperChallenge>} The sign challenges, encoded.
364  */
365 function encodeSignChallenges(signChallenges, opt_defaultChallenge,
366     opt_defaultAppId, opt_challengeHashFunction) {
367   function encodedSha256(keyHandle, challenge) {
368     return B64_encode(sha256HashOfString(challenge));
369   }
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'];
376       var challengeValue;
377       if (challenge.hasOwnProperty('challenge')) {
378         challengeValue = challenge['challenge'];
379       } else {
380         challengeValue = opt_defaultChallenge;
381       }
382       var challengeHash = challengeHashFn(keyHandle, challengeValue);
383       var appId;
384       if (challenge.hasOwnProperty('appId')) {
385         appId = challenge['appId'];
386       } else {
387         appId = opt_defaultAppId;
388       }
389       var encodedChallenge = {
390         'challengeHash': challengeHash,
391         'appIdHash': B64_encode(sha256HashOfString(appId)),
392         'keyHandle': keyHandle,
393         'version': (challenge['version'] || 'U2F_V1')
394       };
395       encodedSignChallenges.push(encodedChallenge);
396     }
397   }
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.
407  */
408 function makeSignHelperRequest(challenges, opt_timeoutSeconds, opt_logMsgUrl) {
409   var request = {
410     'type': 'sign_helper_request',
411     'signData': challenges,
412     'timeout': opt_timeoutSeconds || 0,
413     'timeoutSeconds': opt_timeoutSeconds || 0
414   };
415   if (opt_logMsgUrl !== undefined) {
416     request.logMsgUrl = opt_logMsgUrl;
417   }
418   return request;