# 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/. import os import shutil import tempfile import mozfile from marionette_harness import MarionetteTestCase class BackupTest(MarionetteTestCase): # This is the DB key that will be computed for the http2-ca.pem certificate # that's included in a support-file for this test. _cert_db_key = "AAAAAAAAAAAAAAAUAAAAG0Wbze8lahTcE4RhwEqMtTpThrzjMBkxFzAVBgNVBAMMDiBIVFRQMiBUZXN0IENB" def setUp(self): MarionetteTestCase.setUp(self) # We need to force the "browser.backup.log" pref already set to true # before Firefox starts in order for it to be displayed. self.marionette.enforce_gecko_prefs({"browser.backup.log": True}) self.marionette.set_context("chrome") def tearDown(self): # Restart Firefox with a new profile to get rid from all modifications. self.marionette.quit() self.marionette.instance.switch_profile() self.marionette.start_session() MarionetteTestCase.tearDown(self) def test_backup(self): self.add_test_cookie() self.add_test_login() self.add_test_certificate() self.add_test_saved_address() self.add_test_identity_credential() self.add_test_form_history() self.add_test_asrouter_snippets_data() self.add_test_protections_data() self.add_test_bookmarks() self.add_test_history() self.add_test_preferences() self.add_test_permissions() # We want to make sure that any payment methods in this testing profile # are properly encrypted using OSKeyStore, and that the encrypted # backup will properly extract and recover from the original OSKeyStore # secret. # # What we _don't_ want to do is encrypt or extract the OSKeyStore secret # used by this machine's _actual_ Firefox instance, if one exists # (since they're all shared). We also definitely do not want to # accidentally overwrite that secret. # # We solve this by poking a new STORE_LABEL value into the OSKeyStore # module before we do the following: # # 1. Store payment methods # 2. Enable encryption # # Once that'd one, we delete the temporary OSKeyStore row that we # created. This technique is similar to the one used in # OSKeyStoreTestUtils, which is unfortunately not a module that is # available to Marionette tests. backupOSKeyStoreLabel = self.marionette.execute_script( """ const { OSKeyStore } = ChromeUtils.importESModule( "resource://gre/modules/OSKeyStore.sys.mjs" ); const BACKUP_OSKEYSTORE_LABEL = "test-" + Math.random().toString(36).substr(2); OSKeyStore.STORE_LABEL = BACKUP_OSKEYSTORE_LABEL; return BACKUP_OSKEYSTORE_LABEL; """ ) # Now that we've got the fake OSKeyStore set up, we can insert our # testing payment methods. self.add_test_payment_methods() # Restart the browser to force all of the test data we just added # to be flushed to disk and to be made ready for backup self.marionette.restart() # Put the OSKeyStore label back, since it would have been cleared # from memory during the restart. self.marionette.execute_script( """ const { OSKeyStore } = ChromeUtils.importESModule( "resource://gre/modules/OSKeyStore.sys.mjs" ); const BACKUP_OSKEYSTORE_LABEL = arguments[0]; OSKeyStore.STORE_LABEL = BACKUP_OSKEYSTORE_LABEL; """, script_args=[backupOSKeyStoreLabel], ) archiveDestPath = os.path.join(tempfile.gettempdir(), "backup-dest") recoveryCode = "This is a test password" archivePath = self.marionette.execute_async_script( """ const { BackupService } = ChromeUtils.importESModule("resource:///modules/backup/BackupService.sys.mjs"); let bs = BackupService.init(); if (!bs) { throw new Error("Could not get initialized BackupService."); } let [archiveDestPath, recoveryCode, outerResolve] = arguments; bs.setParentDirPath(archiveDestPath); (async () => { await bs.enableEncryption(recoveryCode); let { archivePath } = await bs.createBackup(); if (!archivePath) { throw new Error("Could not create backup."); } return archivePath; })().then(outerResolve); """, script_args=[archiveDestPath, recoveryCode], ) # Now we clean up our temporary OSKeyStore from the OS's secure storage. # We won't need it anymore. self.marionette.execute_async_script( """ const { OSKeyStore } = ChromeUtils.importESModule( "resource://gre/modules/OSKeyStore.sys.mjs" ); let [outerResolve] = arguments; (async () => { await OSKeyStore.cleanup(); })().then(outerResolve); """ ) recoveryPath = os.path.join(tempfile.gettempdir(), "recovery") shutil.rmtree(recoveryPath, ignore_errors=True) # Start a brand new profile, one without any of the data we created or # backed up. This is the one that we'll be starting recovery from. self.marionette.quit() self.marionette.instance.switch_profile() self.marionette.start_session() self.marionette.set_context("chrome") # Recover the created backup into a new profile directory. Also get out # the client ID of this profile, because we're going to want to make # sure that this client ID is inherited by the recovered profile. [ newProfileName, newProfilePath, expectedClientID, osKeyStoreLabel, ] = self.marionette.execute_async_script( """ const { OSKeyStore } = ChromeUtils.importESModule("resource://gre/modules/OSKeyStore.sys.mjs"); const { ClientID } = ChromeUtils.importESModule("resource://gre/modules/ClientID.sys.mjs"); const { BackupService } = ChromeUtils.importESModule("resource:///modules/backup/BackupService.sys.mjs"); let bs = BackupService.get(); if (!bs) { throw new Error("Could not get initialized BackupService."); } let [archivePath, recoveryCode, recoveryPath, outerResolve] = arguments; (async () => { let newProfileRootPath = await IOUtils.createUniqueDirectory( PathUtils.tempDir, "recoverFromBackupArchiveTest-newProfileRoot" ); // This is some hackery to make it so that OSKeyStore doesn't kick // off an OS authentication dialog in our test, and also to make // sure we don't blow away the _real_ OSKeyStore key for the browser // on the system that this test is running on. Normally, I'd use // OSKeyStoreTestUtils.setup to do this, but apparently the // testing-common modules aren't available in Marionette tests. const ORIGINAL_STORE_LABEL = OSKeyStore.STORE_LABEL; OSKeyStore.STORE_LABEL = "test-" + Math.random().toString(36).substr(2); let newProfile = await bs.recoverFromBackupArchive(archivePath, recoveryCode, false, recoveryPath, newProfileRootPath); if (!newProfile) { throw new Error("Could not create recovery profile."); } let expectedClientID = await ClientID.getClientID(); return [newProfile.name, newProfile.rootDir.path, expectedClientID, OSKeyStore.STORE_LABEL]; })().then(outerResolve); """, script_args=[archivePath, recoveryCode, recoveryPath], ) print("Recovery name: %s" % newProfileName) print("Recovery path: %s" % newProfilePath) print("Expected clientID: %s" % expectedClientID) print("Persisting fake OSKeyStore label: %s" % osKeyStoreLabel) self.marionette.quit() originalProfile = self.marionette.instance.profile self.marionette.instance.profile = newProfilePath self.marionette.start_session() self.marionette.set_context("chrome") # Ensure that all postRecovery actions have completed, and that # encryption is enabled. encryptionEnabled = self.marionette.execute_async_script( """ const { BackupService } = ChromeUtils.importESModule("resource:///modules/backup/BackupService.sys.mjs"); let bs = BackupService.get(); if (!bs) { throw new Error("Could not get initialized BackupService."); } let [outerResolve] = arguments; (async () => { await bs.postRecoveryComplete; await bs.loadEncryptionState(); return bs.state.encryptionEnabled; })().then(outerResolve); """ ) self.assertTrue(encryptionEnabled) self.verify_recovered_test_cookie() self.verify_recovered_test_login() self.verify_recovered_test_certificate() self.verify_recovered_saved_address() self.verify_recovered_identity_credential() self.verify_recovered_form_history() self.verify_recovered_asrouter_snippets_data() self.verify_recovered_protections_data() self.verify_recovered_bookmarks() self.verify_recovered_history() self.verify_recovered_preferences() self.verify_recovered_permissions() self.verify_recovered_payment_methods(osKeyStoreLabel) # Clean up the temporary OSKeyStore label self.marionette.execute_async_script( """ const { OSKeyStore } = ChromeUtils.importESModule("resource://gre/modules/OSKeyStore.sys.mjs"); let [osKeyStoreLabel, outerResolve] = arguments; OSKeyStore.STORE_LABEL = osKeyStoreLabel; (async () => { await OSKeyStore.cleanup(); })().then(outerResolve); """, script_args=[osKeyStoreLabel], ) # Now also ensure that the recovered profile inherited the client ID # from the profile that initiated recovery. recoveredClientID = self.marionette.execute_async_script( """ const { ClientID } = ChromeUtils.importESModule("resource://gre/modules/ClientID.sys.mjs"); let [outerResolve] = arguments; (async () => { return ClientID.getClientID(); })().then(outerResolve); """ ) self.assertEqual(recoveredClientID, expectedClientID) self.marionette.quit() self.marionette.instance.profile = originalProfile self.marionette.start_session() self.marionette.set_context("chrome") # Don't pollute the profile list by getting rid of the one we just created. self.marionette.execute_async_script( """ let [newProfileName, outerResolve] = arguments; let profileSvc = Cc["@mozilla.org/toolkit/profile-service;1"].getService( Ci.nsIToolkitProfileService ); let profile = profileSvc.getProfileByName(newProfileName); profile.remove(true); profileSvc.asyncFlush().then(outerResolve); """, script_args=[newProfileName], ) # Cleanup the archive we moved, and the recovery folder we decompressed to. mozfile.remove(archivePath) mozfile.remove(recoveryPath) def add_test_cookie(self): self.marionette.execute_async_script( """ let [outerResolve] = arguments; (async () => { // We'll just add a single cookie, and then make sure that it shows // up on the other side. Services.cookies.removeAll(); Services.cookies.add( ".example.com", "/", "first", "one", false, false, false, Date.now() / 1000 + 1, {}, Ci.nsICookie.SAMESITE_NONE, Ci.nsICookie.SCHEME_HTTP ); })().then(outerResolve); """ ) def verify_recovered_test_cookie(self): cookiesLength = self.marionette.execute_async_script( """ let [outerResolve] = arguments; (async () => { let cookies = Services.cookies.getCookiesFromHost("example.com", {}); return cookies.length; })().then(outerResolve); """ ) self.assertEqual(cookiesLength, 1) def add_test_login(self): self.marionette.execute_async_script( """ let [outerResolve] = arguments; (async () => { // Let's start with adding a single password Services.logins.removeAllLogins(); const nsLoginInfo = new Components.Constructor( "@mozilla.org/login-manager/loginInfo;1", Ci.nsILoginInfo, "init" ); const login1 = new nsLoginInfo( "https://example.com", "https://example.com", null, "notifyu1", "notifyp1", "user", "pass" ); await Services.logins.addLoginAsync(login1); })().then(outerResolve); """ ) def verify_recovered_test_login(self): loginsLength = self.marionette.execute_async_script( """ let [outerResolve] = arguments; (async () => { let logins = await Services.logins.searchLoginsAsync({ origin: "https://example.com", }); return logins.length; })().then(outerResolve); """ ) self.assertEqual(loginsLength, 1) def add_test_certificate(self): certPath = os.path.join(os.path.dirname(__file__), "http2-ca.pem") self.marionette.execute_async_script( """ let [certPath, certDbKey, outerResolve] = arguments; (async () => { const { NetUtil } = ChromeUtils.importESModule( "resource://gre/modules/NetUtil.sys.mjs" ); let certDb = Cc["@mozilla.org/security/x509certdb;1"].getService( Ci.nsIX509CertDB ); if (certDb.findCertByDBKey(certDbKey)) { throw new Error("Should not have this certificate yet!"); } let certFile = await IOUtils.getFile(certPath); let fstream = Cc["@mozilla.org/network/file-input-stream;1"].createInstance( Ci.nsIFileInputStream ); fstream.init(certFile, -1, 0, 0); let data = NetUtil.readInputStreamToString(fstream, fstream.available()); fstream.close(); let pem = data.replace(/-----BEGIN CERTIFICATE-----/, "") .replace(/-----END CERTIFICATE-----/, "") .replace(/[\\r\\n]/g, ""); let cert = certDb.addCertFromBase64(pem, "CTu,u,u"); if (cert.dbKey != certDbKey) { throw new Error("The inserted certificate DB key is unexpected."); } })().then(outerResolve); """, script_args=[certPath, self._cert_db_key], ) def verify_recovered_test_certificate(self): certExists = self.marionette.execute_async_script( """ let [certDbKey, outerResolve] = arguments; (async () => { let certDb = Cc["@mozilla.org/security/x509certdb;1"].getService( Ci.nsIX509CertDB ); return certDb.findCertByDBKey(certDbKey) != null; })().then(outerResolve); """, script_args=[self._cert_db_key], ) self.assertTrue(certExists) def add_test_saved_address(self): self.marionette.execute_async_script( """ const { formAutofillStorage } = ChromeUtils.importESModule( "resource://autofill/FormAutofillStorage.sys.mjs" ); let [outerResolve] = arguments; (async () => { const TEST_ADDRESS_1 = { "given-name": "John", "additional-name": "R.", "family-name": "Smith", organization: "World Wide Web Consortium", "street-address": "32 Vassar Street\\\nMIT Room 32-G524", "address-level2": "Cambridge", "address-level1": "MA", "postal-code": "02139", country: "US", tel: "+15195555555", email: "user@example.com", }; await formAutofillStorage.initialize(); formAutofillStorage.addresses.removeAll(); await formAutofillStorage.addresses.add(TEST_ADDRESS_1); })().then(outerResolve); """ ) def verify_recovered_saved_address(self): addressesLength = self.marionette.execute_async_script( """ const { formAutofillStorage } = ChromeUtils.importESModule( "resource://autofill/FormAutofillStorage.sys.mjs" ); let [outerResolve] = arguments; (async () => { await formAutofillStorage.initialize(); let addresses = await formAutofillStorage.addresses.getAll(); return addresses.length; })().then(outerResolve); """ ) self.assertEqual(addressesLength, 1) def add_test_identity_credential(self): self.marionette.execute_async_script( """ let [outerResolve] = arguments; (async () => { let service = Cc["@mozilla.org/browser/identity-credential-storage-service;1"] .getService(Ci.nsIIdentityCredentialStorageService); service.clear(); let testPrincipal = Services.scriptSecurityManager.createContentPrincipal( Services.io.newURI("https://test.com/"), {} ); let idpPrincipal = Services.scriptSecurityManager.createContentPrincipal( Services.io.newURI("https://idp-test.com/"), {} ); service.setState( testPrincipal, idpPrincipal, "ID", true, true ); })().then(outerResolve); """ ) def verify_recovered_identity_credential(self): [registered, allowLogout] = self.marionette.execute_async_script( """ let [outerResolve] = arguments; (async () => { let service = Cc["@mozilla.org/browser/identity-credential-storage-service;1"] .getService(Ci.nsIIdentityCredentialStorageService); let testPrincipal = Services.scriptSecurityManager.createContentPrincipal( Services.io.newURI("https://test.com/"), {} ); let idpPrincipal = Services.scriptSecurityManager.createContentPrincipal( Services.io.newURI("https://idp-test.com/"), {} ); let registered = {}; let allowLogout = {}; service.getState( testPrincipal, idpPrincipal, "ID", registered, allowLogout ); return [registered.value, allowLogout.value]; })().then(outerResolve); """ ) self.assertTrue(registered) self.assertTrue(allowLogout) def add_test_form_history(self): self.marionette.execute_async_script( """ const { FormHistory } = ChromeUtils.importESModule( "resource://gre/modules/FormHistory.sys.mjs" ); let [outerResolve] = arguments; (async () => { await FormHistory.update({ op: "add", fieldname: "some-test-field", value: "I was recovered!", timesUsed: 1, firstUsed: 0, lastUsed: 0, }); })().then(outerResolve); """ ) def verify_recovered_form_history(self): formHistoryResultsLength = self.marionette.execute_async_script( """ const { FormHistory } = ChromeUtils.importESModule( "resource://gre/modules/FormHistory.sys.mjs" ); let [outerResolve] = arguments; (async () => { let results = await FormHistory.search( ["guid"], { fieldname: "some-test-field" } ); return results.length; })().then(outerResolve); """ ) self.assertEqual(formHistoryResultsLength, 1) def add_test_asrouter_snippets_data(self): self.marionette.execute_async_script( """ const { ASRouterStorage } = ChromeUtils.importESModule( "resource:///modules/asrouter/ASRouterStorage.sys.mjs", ); const SNIPPETS_TABLE_NAME = "snippets"; let [outerResolve] = arguments; (async () => { let storage = new ASRouterStorage({ storeNames: [SNIPPETS_TABLE_NAME], }); let snippetsTable = await storage.getDbTable(SNIPPETS_TABLE_NAME); await snippetsTable.set("backup-test", "some-test-value"); })().then(outerResolve); """ ) def verify_recovered_asrouter_snippets_data(self): snippetsResult = self.marionette.execute_async_script( """ const { ASRouterStorage } = ChromeUtils.importESModule( "resource:///modules/asrouter/ASRouterStorage.sys.mjs", ); const SNIPPETS_TABLE_NAME = "snippets"; let [outerResolve] = arguments; (async () => { let storage = new ASRouterStorage({ storeNames: [SNIPPETS_TABLE_NAME], }); let snippetsTable = await storage.getDbTable(SNIPPETS_TABLE_NAME); return await snippetsTable.get("backup-test"); })().then(outerResolve); """ ) self.assertEqual(snippetsResult, "some-test-value") def add_test_protections_data(self): self.marionette.execute_async_script( """ const TrackingDBService = Cc["@mozilla.org/tracking-db-service;1"] .getService(Ci.nsITrackingDBService); let [outerResolve] = arguments; (async () => { let entry = { "https://test.com": [ [Ci.nsIWebProgressListener.STATE_BLOCKED_TRACKING_CONTENT, true, 1], ], }; await TrackingDBService.clearAll(); await TrackingDBService.saveEvents(JSON.stringify(entry)); })().then(outerResolve); """ ) def verify_recovered_protections_data(self): eventsSum = self.marionette.execute_async_script( """ const TrackingDBService = Cc["@mozilla.org/tracking-db-service;1"] .getService(Ci.nsITrackingDBService); let [outerResolve] = arguments; (async () => { return TrackingDBService.sumAllEvents(); })().then(outerResolve); """ ) self.assertEqual(eventsSum, 1) def add_test_bookmarks(self): self.marionette.execute_async_script( """ const { PlacesUtils } = ChromeUtils.importESModule( "resource://gre/modules/PlacesUtils.sys.mjs" ); let [outerResolve] = arguments; (async () => { await PlacesUtils.bookmarks.eraseEverything(); await PlacesUtils.bookmarks.insert({ parentGuid: PlacesUtils.bookmarks.toolbarGuid, title: "Some test page", url: Services.io.newURI("https://www.backup.test/"), }); })().then(outerResolve); """ ) def verify_recovered_bookmarks(self): bookmarkExists = self.marionette.execute_async_script( """ const { PlacesUtils } = ChromeUtils.importESModule( "resource://gre/modules/PlacesUtils.sys.mjs" ); let [outerResolve] = arguments; (async () => { let url = Services.io.newURI("https://www.backup.test/"); let bookmark = await PlacesUtils.bookmarks.fetch({ url }); return bookmark != null; })().then(outerResolve); """ ) self.assertTrue(bookmarkExists) def add_test_history(self): self.marionette.execute_async_script( """ const { PlacesUtils } = ChromeUtils.importESModule( "resource://gre/modules/PlacesUtils.sys.mjs" ); let [outerResolve] = arguments; (async () => { await PlacesUtils.history.clear(); let entry = { url: "http://my-restored-history.com", visits: [{ transition: PlacesUtils.history.TRANSITION_LINK }], }; await PlacesUtils.history.insertMany([entry]); })().then(outerResolve); """ ) def verify_recovered_history(self): historyExists = self.marionette.execute_async_script( """ const { PlacesUtils } = ChromeUtils.importESModule( "resource://gre/modules/PlacesUtils.sys.mjs" ); let [outerResolve] = arguments; (async () => { let entry = await PlacesUtils.history.fetch("http://my-restored-history.com"); return entry != null; })().then(outerResolve); """ ) self.assertTrue(historyExists) def add_test_preferences(self): self.marionette.execute_script( """ Services.prefs.setBoolPref("test-pref-for-backup", true) """ ) def verify_recovered_preferences(self): prefExists = self.marionette.execute_script( """ return Services.prefs.getBoolPref("test-pref-for-backup", false); """ ) self.assertTrue(prefExists) def add_test_permissions(self): self.marionette.execute_script( """ let principal = Services.scriptSecurityManager.createContentPrincipalFromOrigin( "https://test-permission-site.com" ); Services.perms.addFromPrincipal( principal, "desktop-notification", Services.perms.ALLOW_ACTION ); """ ) def verify_recovered_permissions(self): permissionExists = self.marionette.execute_script( """ let principal = Services.scriptSecurityManager.createContentPrincipalFromOrigin( "https://test-permission-site.com" ); let perms = Services.perms.getAllForPrincipal(principal); if (perms.length != 1) { throw new Error("Got an unexpected number of permissions"); } return perms[0].type == "desktop-notification" """ ) self.assertTrue(permissionExists) def add_test_payment_methods(self): self.marionette.execute_async_script( """ const { formAutofillStorage } = ChromeUtils.importESModule( "resource://autofill/FormAutofillStorage.sys.mjs" ); let [outerResolve] = arguments; (async () => { await formAutofillStorage.initialize(); await formAutofillStorage.creditCards.add({ "cc-name": "Foxy the Firefox", "cc-number": "5555555555554444", "cc-exp-month": 5, "cc-exp-year": 2099, }); })().then(outerResolve); """ ) def verify_recovered_payment_methods(self, osKeyStoreLabel): cardExists = self.marionette.execute_async_script( """ const { formAutofillStorage } = ChromeUtils.importESModule( "resource://autofill/FormAutofillStorage.sys.mjs" ); let nativeOSKeyStore = Cc["@mozilla.org/security/oskeystore;1"].getService( Ci.nsIOSKeyStore ); let [osKeyStoreLabel, outerResolve] = arguments; (async () => { await formAutofillStorage.initialize(); let cards = await formAutofillStorage.creditCards.getAll(); if (cards.length != 1) { return false; } let card = cards[0]; if (card["cc-name"] != "Foxy the Firefox") { return false; } if (card["cc-exp-month"] != "5") { return false; } if (card["cc-exp-year"] != "2099") { return false; } if (!card["cc-number-encrypted"]) { return false; } // Hack around OSKeyStore's insistence on asking for // reauthentication by using the underlying nativeOSKeyStore // to decrypt the credit card number to check it. let plaintextCardBytes = await nativeOSKeyStore.asyncDecryptBytes( osKeyStoreLabel, card["cc-number-encrypted"] ); let plaintextCard = String.fromCharCode.apply( String, plaintextCardBytes ); if (plaintextCard != "5555555555554444") { return false; } return true; })().then(outerResolve); """, script_args=[osKeyStoreLabel], ) self.assertTrue(cardExists)