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