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 * 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
33 * @return {boolean} Whether the object appears valid.
35 function isValidRegisteredKey(registeredKey
, appIdRequired
) {
36 if (appIdRequired
&& !registeredKey
.hasOwnProperty('appId')) {
39 if (!registeredKey
.hasOwnProperty('keyHandle'))
41 if (registeredKey
['version']) {
42 if (registeredKey
['version'] != 'U2F_V1' &&
43 registeredKey
['version'] != 'U2F_V2') {
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
55 * @return {boolean} Whether the array appears valid.
57 function isValidRegisteredKeyArray(registeredKeys
, appIdRequired
) {
58 return registeredKeys
.every(function(key
) {
59 return isValidRegisteredKey(key
, appIdRequired
);
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
70 * @return {boolean} Whether the array appears valid.
72 function isValidSignChallengeArray(signChallenges
, challengeValueRequired
,
74 for (var i
= 0; i
< signChallenges
.length
; i
++) {
75 var incomingChallenge
= signChallenges
[i
];
76 if (challengeValueRequired
&&
77 !incomingChallenge
.hasOwnProperty('challenge'))
79 if (!isValidRegisteredKey(incomingChallenge
, appIdRequired
)) {
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.
90 function logMessage(logMsg
, opt_logMsgUrl
) {
91 console
.log(UTIL_fmt('logMessage("' + logMsg
+ '")'));
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
;
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
109 function handleWebPageRequest(request
, sender
, sendResponse
) {
110 switch (request
.type
) {
111 case GnubbyMsgTypes
.ENROLL_WEB_REQUEST
:
112 return handleWebEnrollRequest(sender
, request
, sendResponse
);
114 case GnubbyMsgTypes
.SIGN_WEB_REQUEST
:
115 return handleWebSignRequest(sender
, request
, sendResponse
);
117 case MessageTypes
.U2F_REGISTER_REQUEST
:
118 return handleU2fEnrollRequest(sender
, request
, sendResponse
);
120 case MessageTypes
.U2F_SIGN_REQUEST
:
121 return handleU2fSignRequest(sender
, request
, sendResponse
);
125 makeU2fErrorResponse(request
, ErrorCodes
.BAD_REQUEST
, undefined,
126 MessageTypes
.U2F_REGISTER_RESPONSE
));
132 * Set-up listeners for webpage connect.
133 * @param {Object} port connection is on.
134 * @param {Object} request that got received on port.
136 function handleWebPageConnect(port
, request
) {
139 var onMessage = function(request
) {
140 console
.log(UTIL_fmt('request'));
141 console
.log(request
);
142 closeable
= handleWebPageRequest(request
, port
.sender
,
144 response
['requestId'] = request
['requestId'];
145 port
.postMessage(response
);
149 var onDisconnect = function() {
150 port
.onMessage
.removeListener(onMessage
);
151 port
.onDisconnect
.removeListener(onDisconnect
);
152 if (closeable
) closeable
.close();
155 port
.onMessage
.addListener(onMessage
);
156 port
.onDisconnect
.addListener(onDisconnect
);
158 // Start work on initial message.
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.
170 function makeResponseForRequest(request
, responseSuffix
, opt_defaultType
) {
172 if (request
&& request
.type
) {
173 type
= request
.type
.replace(/_request$/, responseSuffix
);
175 type
= opt_defaultType
;
177 var reply
= { 'type': type
};
178 if (request
&& request
.requestId
) {
179 reply
.requestId
= request
.requestId
;
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.
193 function makeU2fErrorResponse(request
, code
, opt_detail
, opt_defaultType
) {
194 var reply
= makeResponseForRequest(request
, '_response', opt_defaultType
);
195 var error
= {'errorCode': code
};
197 error
['errorMessage'] = opt_detail
;
199 reply
['responseData'] = error
;
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.
209 function makeU2fSuccessResponse(request
, responseData
) {
210 var reply
= makeResponseForRequest(request
, '_response');
211 reply
['responseData'] = responseData
;
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.
223 function makeWebErrorResponse(request
, code
, opt_defaultType
) {
224 var reply
= makeResponseForRequest(request
, '_reply', opt_defaultType
);
225 reply
['code'] = code
;
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.
235 function makeWebSuccessResponse(request
, responseData
) {
236 var reply
= makeResponseForRequest(request
, '_reply');
237 reply
['code'] = GnubbyCodeTypes
.OK
;
238 reply
['responseData'] = responseData
;
243 * Maps an error code from the ErrorCodes namespace to the GnubbyCodeTypes
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.
249 function mapErrorCodeToGnubbyCodeType(errorCode
, forSign
) {
252 case ErrorCodes
.BAD_REQUEST
:
253 return GnubbyCodeTypes
.BAD_REQUEST
;
255 case ErrorCodes
.DEVICE_INELIGIBLE
:
256 return forSign
? GnubbyCodeTypes
.NONE_PLUGGED_ENROLLED
:
257 GnubbyCodeTypes
.ALREADY_ENROLLED
;
259 case ErrorCodes
.TIMEOUT
:
260 return GnubbyCodeTypes
.WAIT_TOUCH
;
263 return GnubbyCodeTypes
.UNKNOWN_ERROR
;
267 * Maps a helper's error code from the DeviceStatusCodes namespace to a
269 * @param {number} code Error code from DeviceStatusCodes namespace.
270 * @return {U2fError} An error.
272 function mapDeviceStatusCodeToU2fError(code
) {
274 case DeviceStatusCodes
.WRONG_DATA_STATUS
:
275 return {errorCode
: ErrorCodes
.DEVICE_INELIGIBLE
};
277 case DeviceStatusCodes
.TIMEOUT_STATUS
:
278 case DeviceStatusCodes
.WAIT_TOUCH_STATUS
:
279 return {errorCode
: ErrorCodes
.TIMEOUT
};
283 var reportedError
= {
284 errorCode
: ErrorCodes
.OTHER_ERROR
,
285 errorMessage
: 'device status code: ' + code
.toString(16)
287 return reportedError
;
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.
299 function sendResponseOnce(sentResponse
, closeable
, response
, sendResponse
) {
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
);
313 console
.warn(UTIL_fmt('Tried to reply more than once! Juan, FIX ME'));
318 * @param {!string} string Input string
319 * @return {Array.<number>} SHA256 hash value of string.
321 function sha256HashOfString(string
) {
322 var s
= new SHA256();
323 s
.update(UTIL_StringToBytes(string
));
328 * Normalizes the TLS channel ID value:
329 * 1. Converts semantically empty values (undefined, null, 0) to the empty
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.
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.
342 if (typeof opt_tlsChannelId
=== 'string') {
344 var obj
= JSON
.parse(opt_tlsChannelId
);
346 // Case 1: The string value 'null' parses as the Javascript object null,
347 // so return an empty string: the browser definitely supports TLS
351 // Case 2: return the value as a JS object.
352 return /** @type {Object} */ (obj
);
354 console
.warn('Unparseable TLS channel ID value ' + opt_tlsChannelId
);
355 // Case 3: return the value unmodified.
358 return opt_tlsChannelId
;
362 * Creates a browser data object with the given values.
363 * @param {!string} type A string representing the "type" of this browser data
365 * @param {!string} serverChallenge The server's challenge, as a base64-
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.
371 function makeBrowserData(type
, serverChallenge
, origin
, opt_tlsChannelId
) {
374 'challenge' : serverChallenge
,
377 if (BROWSER_SUPPORTS_TLS_CHANNEL_ID
) {
378 browserData
['cid_pubkey'] = tlsChannelIdValue(opt_tlsChannelId
);
380 return JSON
.stringify(browserData
);
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-
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.
391 function makeEnrollBrowserData(serverChallenge
, origin
, opt_tlsChannelId
) {
392 return makeBrowserData(
393 'navigator.id.finishEnrollment', serverChallenge
, origin
,
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-
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.
405 function makeSignBrowserData(serverChallenge
, origin
, opt_tlsChannelId
) {
406 return makeBrowserData(
407 'navigator.id.getAssertion', serverChallenge
, origin
, opt_tlsChannelId
);
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
421 * @return {!Array.<SignHelperChallenge>} The sign challenges, encoded.
423 function encodeSignChallenges(signChallenges
, opt_defaultChallenge
,
424 opt_defaultAppId
, opt_challengeHashFunction
) {
425 function encodedSha256(keyHandle
, challenge
) {
426 return B64_encode(sha256HashOfString(challenge
));
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'];
435 if (challenge
.hasOwnProperty('challenge')) {
436 challengeValue
= challenge
['challenge'];
438 challengeValue
= opt_defaultChallenge
;
440 var challengeHash
= challengeHashFn(keyHandle
, challengeValue
);
442 if (challenge
.hasOwnProperty('appId')) {
443 appId
= challenge
['appId'];
445 appId
= opt_defaultAppId
;
447 var encodedChallenge
= {
448 'challengeHash': challengeHash
,
449 'appIdHash': B64_encode(sha256HashOfString(appId
)),
450 'keyHandle': keyHandle
,
451 'version': (challenge
['version'] || 'U2F_V1')
453 encodedSignChallenges
.push(encodedChallenge
);
456 return encodedSignChallenges
;
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.
466 function makeSignHelperRequest(challenges
, opt_timeoutSeconds
, opt_logMsgUrl
) {
468 'type': 'sign_helper_request',
469 'signData': challenges
,
470 'timeout': opt_timeoutSeconds
|| 0,
471 'timeoutSeconds': opt_timeoutSeconds
|| 0
473 if (opt_logMsgUrl
!== undefined) {
474 request
.logMsgUrl
= opt_logMsgUrl
;