# This Source Code Form is subject to the terms of the Mozilla Public # License, v. 2.0. If a copy of the MPL was not distributed with this # file, You can obtain one at http://mozilla.org/MPL/2.0/. # Verifies that flipping the at-rest disk cache encryption pref # (browser.cache.disk.encryption.enabled) purges the disk cache on the next # startup. The encryption state the cache was written with is stored in the # index header (mIsEncrypted); when it no longer matches the pref at startup the # whole cache is purged, so the cache never holds a mix of encrypted and # plaintext entries. import time from pathlib import Path from marionette_harness import MarionetteTestCase URL = "http://cache-encryption-flip-test.example/resource" DATA = "cache-encryption-flip-test-payload-0123456789" # Writes a disk cache entry for the given url and resolves once it is stored. WRITE_SCRIPT = """ const [url, data, resolve] = arguments; const uri = Services.io.newURI(url); const storage = Services.cache2.diskCacheStorage(Services.loadContextInfo.default); storage.asyncOpenURI(uri, "", Ci.nsICacheStorage.OPEN_TRUNCATE, { QueryInterface: ChromeUtils.generateQI(["nsICacheEntryOpenCallback"]), onCacheEntryCheck() { return Ci.nsICacheEntryOpenCallback.ENTRY_WANTED; }, onCacheEntryAvailable(entry, isNew, status) { try { entry.setMetaDataElement("test", "1"); entry.metaDataReady(); const os = entry.openOutputStream(0, -1); os.write(data, data.length); os.close(); resolve(true); } catch (e) { resolve("error: " + e); } }, }); """ # Resolves true iff a disk cache entry for the given url exists. EXISTS_SCRIPT = """ const [url, resolve] = arguments; const uri = Services.io.newURI(url); const storage = Services.cache2.diskCacheStorage(Services.loadContextInfo.default); storage.asyncOpenURI(uri, "", Ci.nsICacheStorage.OPEN_READONLY, { QueryInterface: ChromeUtils.generateQI(["nsICacheEntryOpenCallback"]), onCacheEntryCheck() { return Ci.nsICacheEntryOpenCallback.ENTRY_WANTED; }, onCacheEntryAvailable(entry, isNew, status) { resolve(!!entry && Components.isSuccessCode(status)); }, }); """ class CacheEncryptionFlipPurgeTestCase(MarionetteTestCase): def setUp(self): super().setUp() self.marionette.enforce_gecko_prefs({ "browser.cache.disk.enable": True, # Start unencrypted. "browser.cache.disk.encryption.enabled": False, # Force the index to be written back to disk eagerly so it # exists (carrying the encryption flag) before we restart: start the # index build immediately and write it after a single change. "browser.cache.disk.index.update_start_delay_ms": 0, "browser.cache.disk.index.min_unwritten_changes": 1, "browser.cache.disk.index.min_dump_interval_ms": 0, # Keep anything else from clearing the cache on shutdown. "privacy.sanitize.sanitizeOnShutdown": False, "privacy.clearOnShutdown.cache": False, }) self.index_path = Path(self.marionette.profile_path).joinpath("cache2", "index") self.marionette.set_context("chrome") def tearDown(self): self.marionette.restart(in_app=False, clean=True) super().tearDown() def write_entry(self, url): result = self.marionette.execute_async_script( WRITE_SCRIPT, script_args=(url, DATA) ) self.assertEqual(result, True, "writing the cache entry should succeed") def entry_exists(self, url): return self.marionette.execute_async_script(EXISTS_SCRIPT, script_args=(url,)) def populate_and_persist_index(self): # The index is written back to disk only from the READY state (after the # initial build finishes), and only as a side effect of an entry # operation. The write thresholds are lowered to 1 change / 0ms in # setUp, so keep writing entries until the index file appears on disk. self.write_entry(URL) deadline = time.monotonic() + 30 i = 0 while not self.index_path.exists() and time.monotonic() < deadline: self.write_entry(f"{URL}-{i}") i += 1 time.sleep(0.2) self.assertTrue( self.index_path.exists(), "the disk cache index file must be written to disk", ) def test_purge_on_encryption_flip(self): # 1. Populate the (unencrypted) cache and persist the index. self.populate_and_persist_index() self.assertTrue( self.entry_exists(URL), "entry should exist after being written" ) # 2. Control: restart without changing the pref. The index's stored # encryption flag still matches the pref, so the entry must survive. self.marionette.restart(in_app=True, clean=False) self.marionette.set_context("chrome") self.assertTrue( self.entry_exists(URL), "entry must survive a restart when the encryption pref is unchanged", ) # 3. Flip encryption on and restart. The index was written with the flag # off, so at startup it no longer matches the pref and the whole cache # must be purged. self.marionette.enforce_gecko_prefs({ "browser.cache.disk.encryption.enabled": True }) self.marionette.restart(in_app=True, clean=False) self.marionette.set_context("chrome") self.assertFalse( self.entry_exists(URL), "entry must be gone after flipping encryption purges the cache", )