Home | History | Annotate | Download | only in cryptotoken
      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  * 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
     32  */
     33 function getOriginsFromJson(text) {
     34   try {
     35     var urls = JSON.parse(text);
     36     var origins = [];
     37     for (var i = 0, url; url = urls[i]; i++) {
     38       var origin = getOriginFromUrl(url);
     39       if (origin)
     40         origins.push(origin);
     41     }
     42     return origins;
     43   } catch (e) {
     44     console.log(UTIL_fmt('could not parse ' + text));
     45     return [];
     46   }
     47 }
     48 
     49 /**
     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.
     53  */
     54 function fetchAppId(appId, cb) {
     55   var origin = getOriginFromUrl(appId);
     56   if (!origin) {
     57     cb(404, appId);
     58     return;
     59   }
     60   var xhr = new XMLHttpRequest();
     61   var origins = [];
     62   xhr.open('GET', appId, true);
     63   xhr.onloadend = function() {
     64     if (xhr.status != 200) {
     65       cb(xhr.status, appId);
     66       return;
     67     }
     68     cb(xhr.status, appId, getOriginsFromJson(xhr.responseText));
     69   };
     70   xhr.send();
     71 }
     72 
     73 /**
     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.
     77  */
     78 function getDistinctAppIds(signData) {
     79   var appIds = [];
     80   if (!signData) {
     81     return appIds;
     82   }
     83   for (var i = 0, request; request = signData[i]; i++) {
     84     var appId = request['appId'];
     85     if (appId && appIds.indexOf(appId) == -1) {
     86       appIds.push(appId);
     87     }
     88   }
     89   return appIds;
     90 }
     91 
     92 /**
     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.
     98  */
     99 function requestsByAppId(signData) {
    100   var requests = {};
    101   var appIdOrder = {};
    102   var orderToAppId = {};
    103   var lastOrder = 0;
    104   for (var i = 0, request; request = signData[i]; i++) {
    105     var appId = request['appId'];
    106     if (appId) {
    107       if (!appIdOrder.hasOwnProperty(appId)) {
    108         appIdOrder[appId] = lastOrder;
    109         orderToAppId[lastOrder] = appId;
    110         lastOrder++;
    111       }
    112       if (requests[appId]) {
    113         requests[appId].push(request);
    114       } else {
    115         requests[appId] = [request];
    116       }
    117     }
    118   }
    119   var orderedRequests = [];
    120   for (var order = 0; order < lastOrder; order++) {
    121     appId = orderToAppId[order];
    122     orderedRequests.push([appId, requests[appId]]);
    123   }
    124   return orderedRequests;
    125 }
    126 
    127 /**
    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.
    134  */
    135 function fetchAllowedOriginsForAppId(appId, allowHttp, cb) {
    136   var allowedOrigins = [];
    137   if (!appId) {
    138     cb(200, allowedOrigins);
    139     return;
    140   }
    141   if (appId.indexOf('http://') == 0 && !allowHttp) {
    142     console.log(UTIL_fmt('http app ids disallowed, ' + appId + ' requested'));
    143     cb(200, allowedOrigins);
    144     return;
    145   }
    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);
    152     return;
    153   }
    154   if (appId == 'https://login.corp.google.com') {
    155     allowedOrigins = ['https://accounts.google.com'];
    156     cb(200, allowedOrigins);
    157     return;
    158   }
    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) {
    163     if (rc != 200) {
    164       console.log(UTIL_fmt('fetching ' + fetchedAppId + ' failed: ' + rc));
    165       allowedOrigins = [];
    166     } else {
    167       allowedOrigins = origins;
    168     }
    169     cb(rc, allowedOrigins);
    170   });
    171 }
    172 
    173 /**
    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
    178  *    appId.
    179  * @return {boolean} whether the appId is allowed for the origin.
    180  */
    181 function isValidAppIdForOrigin(appId, origin, allowedOrigins) {
    182   if (!appId)
    183     return false;
    184   if (appId == origin) {
    185     // trivially allowed
    186     return true;
    187   }
    188   if (!allowedOrigins)
    189     return false;
    190   return allowedOrigins.indexOf(origin) >= 0;
    191 }
    192 
    193 /**
    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.
    197  */
    198 function isValidSignData(signData) {
    199   for (var i = 0; i < signData.length; i++) {
    200     var incomingChallenge = signData[i];
    201     if (!incomingChallenge.hasOwnProperty('challenge'))
    202       return false;
    203     if (!incomingChallenge.hasOwnProperty('appId')) {
    204       return false;
    205     }
    206     if (!incomingChallenge.hasOwnProperty('keyHandle'))
    207       return false;
    208     if (incomingChallenge['version']) {
    209       if (incomingChallenge['version'] != 'U2F_V1' &&
    210           incomingChallenge['version'] != 'U2F_V2') {
    211         return false;
    212       }
    213     }
    214   }
    215   return true;
    216 }
    217 
    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.
    221  */
    222 function logMessage(logMsg, opt_logMsgUrl) {
    223   console.log(UTIL_fmt('logMessage("' + logMsg + '")'));
    224 
    225   if (!opt_logMsgUrl) {
    226     return;
    227   }
    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;
    232 }
    233 
    234 /**
    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.
    240  */
    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);
    245 }
    246 
    247 /**
    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
    252  */
    253 function logInvalidOriginForAppId(origin, appId, opt_logMsgUrl) {
    254   var logMsg = 'log=originrejected&origin=' + origin + '&appid=' + appId;
    255   logMessage(logMsg, opt_logMsgUrl);
    256 }
    257 
    258 /**
    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.
    264  */
    265 function formatWebPageResponse(type, code, responseData) {
    266   var responseJsonObject = {};
    267   responseJsonObject['type'] = type;
    268   responseJsonObject['code'] = code;
    269   if (responseData)
    270     responseJsonObject['responseData'] = responseData;
    271   return responseJsonObject;
    272 }
    273 
    274 /**
    275  * @param {!string} string Input string
    276  * @return {Array.<number>} SHA256 hash value of string.
    277  */
    278 function sha256HashOfString(string) {
    279   var s = new SHA256();
    280   s.update(UTIL_StringToBytes(string));
    281   return s.digest();
    282 }
    283 
    284 /**
    285  * Normalizes the TLS channel ID value:
    286  * 1. Converts semantically empty values (undefined, null, 0) to the empty
    287  *     string.
    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.
    292  */
    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.
    297     return '';
    298   }
    299   if (typeof opt_tlsChannelId === 'string') {
    300     try {
    301       var obj = JSON.parse(opt_tlsChannelId);
    302       if (!obj) {
    303         // Case 1: The string value 'null' parses as the Javascript object null,
    304         // so return an empty string: the browser definitely supports TLS
    305         // channel id.
    306         return '';
    307       }
    308       // Case 2: return the value as a JS object.
    309       return /** @type {Object} */ (obj);
    310     } catch (e) {
    311       console.warn('Unparseable TLS channel ID value ' + opt_tlsChannelId);
    312       // Case 3: return the value unmodified.
    313     }
    314   }
    315   return opt_tlsChannelId;
    316 }
    317 
    318 /**
    319  * Creates a browser data object with the given values.
    320  * @param {!string} type A string representing the "type" of this browser data
    321  *     object.
    322  * @param {!string} serverChallenge The server's challenge, as a base64-
    323  *     encoded string.
    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.
    327  */
    328 function makeBrowserData(type, serverChallenge, origin, opt_tlsChannelId) {
    329   var browserData = {
    330     'typ' : type,
    331     'challenge' : serverChallenge,
    332     'origin' : origin
    333   };
    334   browserData['cid_pubkey'] = tlsChannelIdValue(opt_tlsChannelId);
    335   return JSON.stringify(browserData);
    336 }
    337 
    338 /**
    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-
    341  *     encoded string.
    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.
    345  */
    346 function makeEnrollBrowserData(serverChallenge, origin, opt_tlsChannelId) {
    347   return makeBrowserData(
    348       'navigator.id.finishEnrollment', serverChallenge, origin,
    349       opt_tlsChannelId);
    350 }
    351 
    352 /**
    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-
    355  *     encoded string.
    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.
    359  */
    360 function makeSignBrowserData(serverChallenge, origin, opt_tlsChannelId) {
    361   return makeBrowserData(
    362       'navigator.id.getAssertion', serverChallenge, origin, opt_tlsChannelId);
    363 }
    364 
    365 /**
    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
    371  */
    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;
    376 
    377   var challenge = {
    378     'challengeHash': browserDataHash,
    379     'appIdHash': appIdHash,
    380     'keyHandle': keyHandle
    381   };
    382   // Version is implicitly U2F_V1 if not specified.
    383   challenge['version'] = (version || 'U2F_V1');
    384   return challenge;
    385 }
    386