tor-browser

The Tor Browser
git clone https://git.dasho.dev/tor-browser.git
Log | Files | Refs | README | LICENSE

test_cache_padding.html (9640B)


      1 <!-- Any copyright is dedicated to the Public Domain.
      2   - http://creativecommons.org/publicdomain/zero/1.0/ -->
      3 <!DOCTYPE HTML>
      4 <html>
      5 <head>
      6  <title>Test Cache generate padding size for opaque repsonse</title>
      7  <script src="/tests/SimpleTest/SimpleTest.js"></script>
      8  <script type="text/javascript" src="large_url_list.js"></script>
      9  <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" />
     10 </head>
     11 <body>
     12 <script class="testbody" type="text/javascript">
     13 function setupTestIframe() {
     14  return new Promise(function(resolve) {
     15    var iframe = document.createElement("iframe");
     16    iframe.src = "empty.html";
     17    iframe.onload = function() {
     18      window.caches = iframe.contentWindow.caches;
     19      resolve();
     20    };
     21    document.body.appendChild(iframe);
     22  });
     23 }
     24 
     25 function clearStorage() {
     26  return new Promise(function(resolve) {
     27    var qms = SpecialPowers.Services.qms;
     28    var principal = SpecialPowers.wrap(document).nodePrincipal;
     29    var request = qms.clearStoragesForPrincipal(principal);
     30    var cb = SpecialPowers.wrapCallback(resolve);
     31    request.callback = cb;
     32  });
     33 }
     34 
     35 function resetStorage() {
     36  return new Promise(function(resolve) {
     37    var qms = SpecialPowers.Services.qms;
     38    var principal = SpecialPowers.wrap(document).nodePrincipal;
     39    var request = qms.resetStoragesForPrincipal(principal);
     40    var cb = SpecialPowers.wrapCallback(resolve);
     41    request.callback = cb;
     42  });
     43 }
     44 
     45 function getStorageUsage() {
     46  return new Promise(function(resolve) {
     47    var qms = SpecialPowers.Services.qms;
     48    var principal = SpecialPowers.wrap(document).nodePrincipal;
     49    var cb = SpecialPowers.wrapCallback(function(request) {
     50      var result = request.result;
     51      resolve(result.usage);
     52    });
     53    qms.getUsageForPrincipal(principal, cb);
     54  });
     55 }
     56 
     57 function getCachedStorageUsage() {
     58  return new Promise(function(resolve) {
     59    var qms = SpecialPowers.Services.qms;
     60    var principal = SpecialPowers.wrap(document).nodePrincipal;
     61    var cb = SpecialPowers.wrapCallback(function(request) {
     62      var result = request.result;
     63      resolve(result);
     64    });
     65    var request = qms.getCachedUsageForPrincipal(principal);
     66    request.callback = cb;
     67  });
     68 }
     69 
     70 async function verifyUsage() {
     71  // This returns origin usage by calculating it from an in-memory object.
     72  let memoryUsage = await getCachedStorageUsage();
     73  // This returns origin usage by calculating it from file sizes on disk.
     74  let diskUsage = await getStorageUsage();
     75 
     76  is(memoryUsage, diskUsage,
     77     "In-memory usage and disk usage should be the same.");
     78  return memoryUsage;
     79 }
     80 
     81 async function waitForIOToComplete(cache, request) {
     82  info("Wait for IO complete.");
     83  // The following lines ensure we've deleted orphaned body.
     84  // First, wait for cache operation delete the orphaned body.
     85  await cache.match(request);
     86 
     87  // Finally, wait for -wal file finish its job.
     88  return resetStorage();
     89 }
     90 
     91 function fetchOpaqueResponse(url) {
     92  return fetch(url, { mode: "no-cors" });
     93 }
     94 
     95 // This test (and the underlying implementation being tested) relies on the
     96 // implementation details of sqlite. In particular, when using the obfuscating
     97 // VFS in private contexts [0][1], reserving 32 bytes per page [2] for the
     98 // encryption IV causes sqlite to grow the database under some conditions when
     99 // the serialized security info [3] exceeds 8119 bytes. The growth size is
    100 // 32KiB [4], which means that in private contexts, the given values that are
    101 // supposed to be equal here will be off by that amount.
    102 // [0] https://searchfox.org/firefox-main/rev/3aff965e839b7872bf48a9803b80669ca49276ac/dom/cache/Manager.cpp#79
    103 // [1] https://searchfox.org/firefox-main/rev/3aff965e839b7872bf48a9803b80669ca49276ac/storage/mozStorageConnection.cpp#1175
    104 // [2] https://searchfox.org/firefox-main/rev/1b3787c361452ef9c9e37d8ffd4c414135d547f5/storage/ObfuscatingVFS.cpp#309
    105 // [3] https://searchfox.org/firefox-main/rev/3aff965e839b7872bf48a9803b80669ca49276ac/security/manager/ssl/TransportSecurityInfo.cpp#114
    106 // [4] https://searchfox.org/firefox-main/rev/3aff965e839b7872bf48a9803b80669ca49276ac/dom/cache/DBSchema.cpp#255
    107 function equalOrOffByOneGrowthChunk(a, b, message) {
    108  if (SpecialPowers.getBoolPref("browser.privatebrowsing.autostart") && a != b) {
    109    b += 32768;
    110  }
    111  is(a, b, message);
    112 }
    113 
    114 SimpleTest.waitForExplicitFinish();
    115 SpecialPowers.pushPrefEnv({
    116  "set": [["dom.caches.enabled", true],
    117          ["dom.caches.testing.enabled", true],
    118          ["dom.quotaManager.testing", true]],
    119 }, async function() {
    120  // This test is mainly to verify we only generate different padding size for
    121  // the opaque response which is comming from netwrok.
    122  // Besides, this test utilizes verifyUsage() to ensure Cache Acions does
    123  // update thier usage/padding size to the QM, does record padding size to
    124  // the directory padding file and does do above two things synchronously.
    125  // So that, opaque response's size is bigger than the normal response's size
    126  // and we always have the same usage bewteen from in-memory and from
    127  // the file-system.
    128  // Note: For the cloned and cached opaque response, the padding size shouldn't
    129  // be changed. Thus, it makes the attacker harder to get the padding size.
    130 
    131  const name = "cachePadding";
    132  const other_name = "cachePaddingOther";
    133  const cors_base = "https://example.com/tests/dom/cache/test/mochitest/";
    134  const url = "test_cache_add.js";
    135 
    136  await setupTestIframe();
    137 
    138  info("Stage 1: Clean storage.");
    139  await clearStorage();
    140 
    141  let cache = await caches.open(name);
    142 
    143  // XXX This arbitrary loop is a hack to restore the same growth database
    144  // behavior as it was before the schema upgrade from version 28 to 29.
    145  // XXX Obviously, this test shouldn't use the total usage (which includes
    146  // both the database and the file usage) to compute the disk size of
    147  // responses. It should use the file usage only. The problem is that the
    148  // quota client implementation currently doesn't differentiate between the
    149  // database and the file usage. Even if that gets fixed, there would be a
    150  // problem with checking cached usage which currently doesn't differentiate
    151  // between the database usage and the file usage as well.
    152  for (let i = 0; i < 19; i++) {
    153    const request = new Request("https://example.com/index" + i + ".html");
    154    const response = new Response("hello world");
    155    await cache.put(request, response);
    156  }
    157 
    158  await waitForIOToComplete(cache, url);
    159  let usage1 = await verifyUsage();
    160 
    161  info("Stage 2: Verify opaque responses have padding.");
    162  cache = await caches.open(name);
    163  await cache.add(url);
    164  await waitForIOToComplete(cache, url);
    165  let usage2 = await verifyUsage();
    166  let sizeForNormalResponse = usage2 - usage1;
    167 
    168  let opaqueResponse = await fetchOpaqueResponse(cors_base + url);
    169  cache = await caches.open(name);
    170  await cache.put(cors_base + url, opaqueResponse.clone());
    171  await waitForIOToComplete(cache, url);
    172  let usage3 = await verifyUsage();
    173  let sizeForOpaqueResponse = usage3 - usage2;
    174  ok(sizeForOpaqueResponse > sizeForNormalResponse,
    175     "The opaque response should have larger size than the normal response.");
    176 
    177  info("Stage 3: Verify the cloned response has the same size.");
    178  cache = await caches.open(name);
    179  await cache.put(cors_base + url, opaqueResponse.clone());
    180  await waitForIOToComplete(cache, url);
    181  let usage4 = await verifyUsage();
    182  // Since we put the same request and response again, the size should be the
    183  // same (DOM Cache removes the previous cached request and response)
    184  equalOrOffByOneGrowthChunk(usage4, usage3,
    185     "We won't generate different padding for cloned response");
    186 
    187  info("Stage 4: Verify the cached response has the same size.");
    188  cache = await caches.open(name);
    189  opaqueResponse = await cache.match(cors_base + url);
    190  ok(opaqueResponse);
    191 
    192  await cache.put(cors_base + url, opaqueResponse);
    193  await waitForIOToComplete(cache, url);
    194  let usage5 = await verifyUsage();
    195  equalOrOffByOneGrowthChunk(usage5, usage3,
    196     "We won't generate different padding for cached response");
    197 
    198  info("Stage 5: Verify padding size may changes in different fetch()s.");
    199  let paddingSizeChange = false;
    200  // Since we randomly generate padding size and rounding the overall size up,
    201  // we will probably have the same size. So, fetch it multiple times.
    202  for (let i = 0; i < 10; i++) {
    203    opaqueResponse = await fetchOpaqueResponse(cors_base + url);
    204    cache = await caches.open(name);
    205    await cache.put(cors_base + url, opaqueResponse);
    206    await waitForIOToComplete(cache, url);
    207    let usage6  = await verifyUsage();
    208    if (usage6 != usage5) {
    209      paddingSizeChange = true;
    210      break;
    211    }
    212  }
    213  ok(paddingSizeChange,
    214     "We should generate different padding size for fetching response");
    215 
    216  info("Stage 6: Verify the padding is removed once on caches.delete() and " +
    217       "cache.delete().");
    218  // Add an opauqe response on other cache storage and then delete that storage.
    219  cache = await caches.open(other_name);
    220  opaqueResponse = await fetchOpaqueResponse(cors_base + url);
    221  await cache.put(cors_base + url, opaqueResponse);
    222  await caches.delete(other_name);
    223  await caches.has(other_name);
    224  // Force remove orphaned cached in the next action
    225  await resetStorage();
    226 
    227  // Delete the opauqe repsonse on current cache storage.
    228  cache = await caches.open(name);
    229  await cache.delete(cors_base + url);
    230  await waitForIOToComplete(cache, url);
    231  let usage7  = await verifyUsage();
    232  equalOrOffByOneGrowthChunk(usage7, usage2,
    233     "The opaque response should be removed by caches.delete() and " +
    234     "cache.delete()");
    235 
    236  await SimpleTest.finish();
    237 });
    238 </script>
    239 </body>
    240 </html>