Update To 11.40.268.0
[platform/framework/web/crosswalk.git] / src / chrome / browser / resources / cryptotoken / webrequest.js
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.
4
5 /**
6  * @fileoverview Does common handling for requests coming from web pages and
7  * routes them to the provided handler.
8  */
9
10 /**
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
14  */
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);
22   }
23   if (origin == 'http:' || origin == 'https:')
24     return null;
25   return origin;
26 }
27
28 /**
29  * Returns whether the registered key appears to be valid.
30  * @param {Object} registeredKey The registered key object.
31  * @param {boolean} appIdRequired Whether the appId property is required on
32  *     each challenge.
33  * @return {boolean} Whether the object appears valid.
34  */
35 function isValidRegisteredKey(registeredKey, appIdRequired) {
36   if (appIdRequired && !registeredKey.hasOwnProperty('appId')) {
37     return false;
38   }
39   if (!registeredKey.hasOwnProperty('keyHandle'))
40     return false;
41   if (registeredKey['version']) {
42     if (registeredKey['version'] != 'U2F_V1' &&
43         registeredKey['version'] != 'U2F_V2') {
44       return false;
45     }
46   }
47   return true;
48 }
49
50 /**
51  * Returns whether the array of registered keys appears to be valid.
52  * @param {Array.<Object>} registeredKeys The array of registered keys.
53  * @param {boolean} appIdRequired Whether the appId property is required on
54  *     each challenge.
55  * @return {boolean} Whether the array appears valid.
56  */
57 function isValidRegisteredKeyArray(registeredKeys, appIdRequired) {
58   return registeredKeys.every(function(key) {
59     return isValidRegisteredKey(key, appIdRequired);
60   });
61 }
62
63 /**
64  * Returns whether the array of SignChallenges appears to be valid.
65  * @param {Array.<SignChallenge>} signChallenges The array of sign challenges.
66  * @param {boolean} challengeValueRequired Whether each challenge object
67  *     requires a challenge value.
68  * @param {boolean} appIdRequired Whether the appId property is required on
69  *     each challenge.
70  * @return {boolean} Whether the array appears valid.
71  */
72 function isValidSignChallengeArray(signChallenges, challengeValueRequired,
73     appIdRequired) {
74   for (var i = 0; i < signChallenges.length; i++) {
75     var incomingChallenge = signChallenges[i];
76     if (challengeValueRequired &&
77         !incomingChallenge.hasOwnProperty('challenge'))
78       return false;
79     if (!isValidRegisteredKey(incomingChallenge, appIdRequired)) {
80       return false;
81     }
82   }
83   return true;
84 }
85
86 /** Posts the log message to the log url.
87  * @param {string} logMsg the log message to post.
88  * @param {string=} opt_logMsgUrl the url to post log messages to.
89  */
90 function logMessage(logMsg, opt_logMsgUrl) {
91   console.log(UTIL_fmt('logMessage("' + logMsg + '")'));
92
93   if (!opt_logMsgUrl) {
94     return;
95   }
96   // Image fetching is not allowed per packaged app CSP.
97   // But video and audio is.
98   var audio = new Audio();
99   audio.src = opt_logMsgUrl + logMsg;
100 }
101
102 /**
103  * @param {Object} request Request object
104  * @param {MessageSender} sender Sender frame
105  * @param {Function} sendResponse Response callback
106  * @return {?Closeable} Optional handler object that should be closed when port
107  *     closes
108  */
109 function handleWebPageRequest(request, sender, sendResponse) {
110   switch (request.type) {
111     case GnubbyMsgTypes.ENROLL_WEB_REQUEST:
112       return handleWebEnrollRequest(sender, request, sendResponse);
113
114     case GnubbyMsgTypes.SIGN_WEB_REQUEST:
115       return handleWebSignRequest(sender, request, sendResponse);
116
117     case MessageTypes.U2F_REGISTER_REQUEST:
118       return handleU2fEnrollRequest(sender, request, sendResponse);
119
120     case MessageTypes.U2F_SIGN_REQUEST:
121       return handleU2fSignRequest(sender, request, sendResponse);
122
123     default:
124       sendResponse(
125           makeU2fErrorResponse(request, ErrorCodes.BAD_REQUEST, undefined,
126               MessageTypes.U2F_REGISTER_RESPONSE));
127       return null;
128   }
129 }
130
131 /**
132  * Set-up listeners for webpage connect.
133  * @param {Object} port connection is on.
134  * @param {Object} request that got received on port.
135  */
136 function handleWebPageConnect(port, request) {
137   var closeable;
138
139   var onMessage = function(request) {
140     console.log(UTIL_fmt('request'));
141     console.log(request);
142     closeable = handleWebPageRequest(request, port.sender,
143         function(response) {
144           response['requestId'] = request['requestId'];
145           port.postMessage(response);
146         });
147   };
148
149   var onDisconnect = function() {
150     port.onMessage.removeListener(onMessage);
151     port.onDisconnect.removeListener(onDisconnect);
152     if (closeable) closeable.close();
153   };
154
155   port.onMessage.addListener(onMessage);
156   port.onDisconnect.addListener(onDisconnect);
157
158   // Start work on initial message.
159   onMessage(request);
160 }
161
162 /**
163  * Makes a response to a request.
164  * @param {Object} request The request to make a response to.
165  * @param {string} responseSuffix How to name the response's type.
166  * @param {string=} opt_defaultType The default response type, if none is
167  *     present in the request.
168  * @return {Object} The response object.
169  */
170 function makeResponseForRequest(request, responseSuffix, opt_defaultType) {
171   var type;
172   if (request && request.type) {
173     type = request.type.replace(/_request$/, responseSuffix);
174   } else {
175     type = opt_defaultType;
176   }
177   var reply = { 'type': type };
178   if (request && request.requestId) {
179     reply.requestId = request.requestId;
180   }
181   return reply;
182 }
183
184 /**
185  * Makes a response to a U2F request with an error code.
186  * @param {Object} request The request to make a response to.
187  * @param {ErrorCodes} code The error code to return.
188  * @param {string=} opt_detail An error detail string.
189  * @param {string=} opt_defaultType The default response type, if none is
190  *     present in the request.
191  * @return {Object} The U2F error.
192  */
193 function makeU2fErrorResponse(request, code, opt_detail, opt_defaultType) {
194   var reply = makeResponseForRequest(request, '_response', opt_defaultType);
195   var error = {'errorCode': code};
196   if (opt_detail) {
197     error['errorMessage'] = opt_detail;
198   }
199   reply['responseData'] = error;
200   return reply;
201 }
202
203 /**
204  * Makes a success response to a web request with a responseData object.
205  * @param {Object} request The request to make a response to.
206  * @param {Object} responseData The response data.
207  * @return {Object} The web error.
208  */
209 function makeU2fSuccessResponse(request, responseData) {
210   var reply = makeResponseForRequest(request, '_response');
211   reply['responseData'] = responseData;
212   return reply;
213 }
214
215 /**
216  * Makes a response to a web request with an error code.
217  * @param {Object} request The request to make a response to.
218  * @param {GnubbyCodeTypes} code The error code to return.
219  * @param {string=} opt_defaultType The default response type, if none is
220  *     present in the request.
221  * @return {Object} The web error.
222  */
223 function makeWebErrorResponse(request, code, opt_defaultType) {
224   var reply = makeResponseForRequest(request, '_reply', opt_defaultType);
225   reply['code'] = code;
226   return reply;
227 }
228
229 /**
230  * Makes a success response to a web request with a responseData object.
231  * @param {Object} request The request to make a response to.
232  * @param {Object} responseData The response data.
233  * @return {Object} The web error.
234  */
235 function makeWebSuccessResponse(request, responseData) {
236   var reply = makeResponseForRequest(request, '_reply');
237   reply['code'] = GnubbyCodeTypes.OK;
238   reply['responseData'] = responseData;
239   return reply;
240 }
241
242 /**
243  * Maps an error code from the ErrorCodes namespace to the GnubbyCodeTypes
244  * namespace.
245  * @param {ErrorCodes} errorCode Error in the ErrorCodes namespace.
246  * @param {boolean} forSign Whether the error is for a sign request.
247  * @return {GnubbyCodeTypes} Error code in the GnubbyCodeTypes namespace.
248  */
249 function mapErrorCodeToGnubbyCodeType(errorCode, forSign) {
250   var code;
251   switch (errorCode) {
252     case ErrorCodes.BAD_REQUEST:
253       return GnubbyCodeTypes.BAD_REQUEST;
254
255     case ErrorCodes.DEVICE_INELIGIBLE:
256       return forSign ? GnubbyCodeTypes.NONE_PLUGGED_ENROLLED :
257           GnubbyCodeTypes.ALREADY_ENROLLED;
258
259     case ErrorCodes.TIMEOUT:
260       return GnubbyCodeTypes.WAIT_TOUCH;
261
262   }
263   return GnubbyCodeTypes.UNKNOWN_ERROR;
264 }
265
266 /**
267  * Maps a helper's error code from the DeviceStatusCodes namespace to a
268  * U2fError.
269  * @param {number} code Error code from DeviceStatusCodes namespace.
270  * @return {U2fError} An error.
271  */
272 function mapDeviceStatusCodeToU2fError(code) {
273   switch (code) {
274     case DeviceStatusCodes.WRONG_DATA_STATUS:
275       return {errorCode: ErrorCodes.DEVICE_INELIGIBLE};
276
277     case DeviceStatusCodes.TIMEOUT_STATUS:
278     case DeviceStatusCodes.WAIT_TOUCH_STATUS:
279       return {errorCode: ErrorCodes.TIMEOUT};
280
281
282     default:
283       var reportedError = {
284         errorCode: ErrorCodes.OTHER_ERROR,
285         errorMessage: 'device status code: ' + code.toString(16)
286       };
287       return reportedError;
288   }
289 }
290
291 /**
292  * Sends a response, using the given sentinel to ensure at most one response is
293  * sent. Also closes the closeable, if it's given.
294  * @param {boolean} sentResponse Whether a response has already been sent.
295  * @param {?Closeable} closeable A thing to close.
296  * @param {*} response The response to send.
297  * @param {Function} sendResponse A function to send the response.
298  */
299 function sendResponseOnce(sentResponse, closeable, response, sendResponse) {
300   if (closeable) {
301     closeable.close();
302   }
303   if (!sentResponse) {
304     sentResponse = true;
305     try {
306       // If the page has gone away or the connection has otherwise gone,
307       // sendResponse fails.
308       sendResponse(response);
309     } catch (exception) {
310       console.warn('sendResponse failed: ' + exception);
311     }
312   } else {
313     console.warn(UTIL_fmt('Tried to reply more than once! Juan, FIX ME'));
314   }
315 }
316
317 /**
318  * @param {!string} string Input string
319  * @return {Array.<number>} SHA256 hash value of string.
320  */
321 function sha256HashOfString(string) {
322   var s = new SHA256();
323   s.update(UTIL_StringToBytes(string));
324   return s.digest();
325 }
326
327 /**
328  * Normalizes the TLS channel ID value:
329  * 1. Converts semantically empty values (undefined, null, 0) to the empty
330  *     string.
331  * 2. Converts valid JSON strings to a JS object.
332  * 3. Otherwise, returns the input value unmodified.
333  * @param {Object|string|undefined} opt_tlsChannelId TLS Channel id
334  * @return {Object|string} The normalized TLS channel ID value.
335  */
336 function tlsChannelIdValue(opt_tlsChannelId) {
337   if (!opt_tlsChannelId) {
338     // Case 1: Always set some value for  TLS channel ID, even if it's the empty
339     // string: this browser definitely supports them.
340     return '';
341   }
342   if (typeof opt_tlsChannelId === 'string') {
343     try {
344       var obj = JSON.parse(opt_tlsChannelId);
345       if (!obj) {
346         // Case 1: The string value 'null' parses as the Javascript object null,
347         // so return an empty string: the browser definitely supports TLS
348         // channel id.
349         return '';
350       }
351       // Case 2: return the value as a JS object.
352       return /** @type {Object} */ (obj);
353     } catch (e) {
354       console.warn('Unparseable TLS channel ID value ' + opt_tlsChannelId);
355       // Case 3: return the value unmodified.
356     }
357   }
358   return opt_tlsChannelId;
359 }
360
361 /**
362  * Creates a browser data object with the given values.
363  * @param {!string} type A string representing the "type" of this browser data
364  *     object.
365  * @param {!string} serverChallenge The server's challenge, as a base64-
366  *     encoded string.
367  * @param {!string} origin The server's origin, as seen by the browser.
368  * @param {Object|string|undefined} opt_tlsChannelId TLS Channel Id
369  * @return {string} A string representation of the browser data object.
370  */
371 function makeBrowserData(type, serverChallenge, origin, opt_tlsChannelId) {
372   var browserData = {
373     'typ' : type,
374     'challenge' : serverChallenge,
375     'origin' : origin
376   };
377   if (BROWSER_SUPPORTS_TLS_CHANNEL_ID) {
378     browserData['cid_pubkey'] = tlsChannelIdValue(opt_tlsChannelId);
379   }
380   return JSON.stringify(browserData);
381 }
382
383 /**
384  * Creates a browser data object for an enroll request with the given values.
385  * @param {!string} serverChallenge The server's challenge, as a base64-
386  *     encoded string.
387  * @param {!string} origin The server's origin, as seen by the browser.
388  * @param {Object|string|undefined} opt_tlsChannelId TLS Channel Id
389  * @return {string} A string representation of the browser data object.
390  */
391 function makeEnrollBrowserData(serverChallenge, origin, opt_tlsChannelId) {
392   return makeBrowserData(
393       'navigator.id.finishEnrollment', serverChallenge, origin,
394       opt_tlsChannelId);
395 }
396
397 /**
398  * Creates a browser data object for a sign request with the given values.
399  * @param {!string} serverChallenge The server's challenge, as a base64-
400  *     encoded string.
401  * @param {!string} origin The server's origin, as seen by the browser.
402  * @param {Object|string|undefined} opt_tlsChannelId TLS Channel Id
403  * @return {string} A string representation of the browser data object.
404  */
405 function makeSignBrowserData(serverChallenge, origin, opt_tlsChannelId) {
406   return makeBrowserData(
407       'navigator.id.getAssertion', serverChallenge, origin, opt_tlsChannelId);
408 }
409
410 /**
411  * Encodes the sign data as an array of sign helper challenges.
412  * @param {Array.<SignChallenge>} signChallenges The sign challenges to encode.
413  * @param {string|undefined} opt_defaultChallenge A default sign challenge
414  *     value, if a request does not provide one.
415  * @param {string=} opt_defaultAppId The app id to use for each challenge, if
416  *     the challenge contains none.
417  * @param {function(string, string): string=} opt_challengeHashFunction
418  *     A function that produces, from a key handle and a raw challenge, a hash
419  *     of the raw challenge. If none is provided, a default hash function is
420  *     used.
421  * @return {!Array.<SignHelperChallenge>} The sign challenges, encoded.
422  */
423 function encodeSignChallenges(signChallenges, opt_defaultChallenge,
424     opt_defaultAppId, opt_challengeHashFunction) {
425   function encodedSha256(keyHandle, challenge) {
426     return B64_encode(sha256HashOfString(challenge));
427   }
428   var challengeHashFn = opt_challengeHashFunction || encodedSha256;
429   var encodedSignChallenges = [];
430   if (signChallenges) {
431     for (var i = 0; i < signChallenges.length; i++) {
432       var challenge = signChallenges[i];
433       var keyHandle = challenge['keyHandle'];
434       var challengeValue;
435       if (challenge.hasOwnProperty('challenge')) {
436         challengeValue = challenge['challenge'];
437       } else {
438         challengeValue = opt_defaultChallenge;
439       }
440       var challengeHash = challengeHashFn(keyHandle, challengeValue);
441       var appId;
442       if (challenge.hasOwnProperty('appId')) {
443         appId = challenge['appId'];
444       } else {
445         appId = opt_defaultAppId;
446       }
447       var encodedChallenge = {
448         'challengeHash': challengeHash,
449         'appIdHash': B64_encode(sha256HashOfString(appId)),
450         'keyHandle': keyHandle,
451         'version': (challenge['version'] || 'U2F_V1')
452       };
453       encodedSignChallenges.push(encodedChallenge);
454     }
455   }
456   return encodedSignChallenges;
457 }
458
459 /**
460  * Makes a sign helper request from an array of challenges.
461  * @param {Array.<SignHelperChallenge>} challenges The sign challenges.
462  * @param {number=} opt_timeoutSeconds Timeout value.
463  * @param {string=} opt_logMsgUrl URL to log to.
464  * @return {SignHelperRequest} The sign helper request.
465  */
466 function makeSignHelperRequest(challenges, opt_timeoutSeconds, opt_logMsgUrl) {
467   var request = {
468     'type': 'sign_helper_request',
469     'signData': challenges,
470     'timeout': opt_timeoutSeconds || 0,
471     'timeoutSeconds': opt_timeoutSeconds || 0
472   };
473   if (opt_logMsgUrl !== undefined) {
474     request.logMsgUrl = opt_logMsgUrl;
475   }
476   return request;
477 }