diff --git a/Client.xcodeproj/project.pbxproj b/Client.xcodeproj/project.pbxproj index 4c58f5730e0..529dddb96ac 100644 --- a/Client.xcodeproj/project.pbxproj +++ b/Client.xcodeproj/project.pbxproj @@ -405,8 +405,6 @@ 27953F7423F5B04500B4B595 /* ViewLabel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 27953F7323F5B04500B4B595 /* ViewLabel.swift */; }; 27953F7723F5DF5D00B4B595 /* AboutShieldsViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 27953F7623F5DF5D00B4B595 /* AboutShieldsViewController.swift */; }; 2798E01D213EFC3F003EDBB1 /* FavoriteTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2798E015213EFC3F003EDBB1 /* FavoriteTests.swift */; }; - 279C756B219DDE3B001CD1CB /* FingerprintingProtection.swift in Sources */ = {isa = PBXBuildFile; fileRef = 279C756A219DDE3B001CD1CB /* FingerprintingProtection.swift */; }; - 279C75C821A5B37D001CD1CB /* FingerprintingProtection.js in Resources */ = {isa = PBXBuildFile; fileRef = 279C75C021A5B37D001CD1CB /* FingerprintingProtection.js */; }; 279CC2B0260CEB5D009B3895 /* ActivityIndicatorView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 279CC2AF260CEB5D009B3895 /* ActivityIndicatorView.swift */; }; 279CC2C4260CEFE7009B3895 /* TableCellButtonStyle.swift in Sources */ = {isa = PBXBuildFile; fileRef = 279CC2C3260CEFE7009B3895 /* TableCellButtonStyle.swift */; }; 279CC2D8260CF37C009B3895 /* BrowserViewController+Menu.swift in Sources */ = {isa = PBXBuildFile; fileRef = 279CC2D7260CF37C009B3895 /* BrowserViewController+Menu.swift */; }; @@ -954,6 +952,18 @@ 7DF9113D275AC86100EF0D8B /* WalletLoadingButton.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7DF9113C275AC86100EF0D8B /* WalletLoadingButton.swift */; }; 7DF911C0276A8D1600EF0D8B /* SwapTokenStoreTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7DF911BF276A8D1600EF0D8B /* SwapTokenStoreTests.swift */; }; 7DF911DF27726E5E00EF0D8B /* BuyTokenStoreTest.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7DF911DE27726E5E00EF0D8B /* BuyTokenStoreTest.swift */; }; + 8CB0FD9A27DAB06E00707490 /* FarblingProtection.js in Resources */ = {isa = PBXBuildFile; fileRef = 8CB0FD9927DAB06E00707490 /* FarblingProtection.js */; }; + 8CB0FDA827DB9D5400707490 /* ScriptFactory.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8CB0FDA727DB9D5300707490 /* ScriptFactory.swift */; }; + 8CB0FDBA27E0EC1C00707490 /* UserScriptHelper.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8CB0FDB927E0EC1B00707490 /* UserScriptHelper.swift */; }; + 8CB0FDD427E12D7700707490 /* UserScriptHelperTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8CB0FDD327E12D7700707490 /* UserScriptHelperTests.swift */; }; + 8CB0FDD627E12DA100707490 /* ScriptFactoryTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8CB0FDD527E12DA100707490 /* ScriptFactoryTests.swift */; }; + 8CB0FDD827E652B700707490 /* nacl.min.js in Resources */ = {isa = PBXBuildFile; fileRef = 8CB0FDD727E652B500707490 /* nacl.min.js */; }; + 8CB0FE4427EA69E200707490 /* FarblingProtectionHelper.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8CB0FE4327EA69E100707490 /* FarblingProtectionHelper.swift */; }; + 8CB0FE5027EB528200707490 /* RandomConfiguration.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8CB0FE4F27EB528200707490 /* RandomConfiguration.swift */; }; + 8CB0FE5627EB596B00707490 /* RandomConfigurationTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8CB0FE5527EB596B00707490 /* RandomConfigurationTests.swift */; }; + 8CB0FE5827EB5F5100707490 /* FarblingProtectionHelperTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8CB0FE5727EB5F5000707490 /* FarblingProtectionHelperTests.swift */; }; + 8CFEF62427F220E700CF3235 /* ScriptSourceType.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8CFEF62327F220E700CF3235 /* ScriptSourceType.swift */; }; + 8CFEF62627F2213A00CF3235 /* UserScriptType.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8CFEF62527F2213900CF3235 /* UserScriptType.swift */; }; A104E199210A384400D2323E /* ShieldsViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = A104E198210A384400D2323E /* ShieldsViewController.swift */; }; A134B88A20DA98BB00A581D0 /* ClientPreferences.swift in Sources */ = {isa = PBXBuildFile; fileRef = A134B88920DA98BB00A581D0 /* ClientPreferences.swift */; }; A13AC72520EC12360040D4BB /* Migration.swift in Sources */ = {isa = PBXBuildFile; fileRef = A13AC72420EC12360040D4BB /* Migration.swift */; }; @@ -2165,8 +2175,6 @@ 27953F7323F5B04500B4B595 /* ViewLabel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ViewLabel.swift; sourceTree = ""; }; 27953F7623F5DF5D00B4B595 /* AboutShieldsViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AboutShieldsViewController.swift; sourceTree = ""; }; 2798E015213EFC3F003EDBB1 /* FavoriteTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = FavoriteTests.swift; sourceTree = ""; }; - 279C756A219DDE3B001CD1CB /* FingerprintingProtection.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FingerprintingProtection.swift; sourceTree = ""; }; - 279C75C021A5B37D001CD1CB /* FingerprintingProtection.js */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.javascript; path = FingerprintingProtection.js; sourceTree = ""; }; 279CC2AF260CEB5D009B3895 /* ActivityIndicatorView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ActivityIndicatorView.swift; sourceTree = ""; }; 279CC2C3260CEFE7009B3895 /* TableCellButtonStyle.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TableCellButtonStyle.swift; sourceTree = ""; }; 279CC2D7260CF37C009B3895 /* BrowserViewController+Menu.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "BrowserViewController+Menu.swift"; sourceTree = ""; }; @@ -2926,6 +2934,18 @@ 7DF9113C275AC86100EF0D8B /* WalletLoadingButton.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WalletLoadingButton.swift; sourceTree = ""; }; 7DF911BF276A8D1600EF0D8B /* SwapTokenStoreTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SwapTokenStoreTests.swift; sourceTree = ""; }; 7DF911DE27726E5E00EF0D8B /* BuyTokenStoreTest.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BuyTokenStoreTest.swift; sourceTree = ""; }; + 8CB0FD9927DAB06E00707490 /* FarblingProtection.js */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.javascript; path = FarblingProtection.js; sourceTree = ""; tabWidth = 2; }; + 8CB0FDA727DB9D5300707490 /* ScriptFactory.swift */ = {isa = PBXFileReference; indentWidth = 2; lastKnownFileType = sourcecode.swift; path = ScriptFactory.swift; sourceTree = ""; tabWidth = 2; }; + 8CB0FDB927E0EC1B00707490 /* UserScriptHelper.swift */ = {isa = PBXFileReference; indentWidth = 2; lastKnownFileType = sourcecode.swift; path = UserScriptHelper.swift; sourceTree = ""; tabWidth = 2; }; + 8CB0FDD327E12D7700707490 /* UserScriptHelperTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UserScriptHelperTests.swift; sourceTree = ""; }; + 8CB0FDD527E12DA100707490 /* ScriptFactoryTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ScriptFactoryTests.swift; sourceTree = ""; }; + 8CB0FDD727E652B500707490 /* nacl.min.js */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.javascript; path = nacl.min.js; sourceTree = ""; }; + 8CB0FE4327EA69E100707490 /* FarblingProtectionHelper.swift */ = {isa = PBXFileReference; indentWidth = 2; lastKnownFileType = sourcecode.swift; path = FarblingProtectionHelper.swift; sourceTree = ""; tabWidth = 2; }; + 8CB0FE4F27EB528200707490 /* RandomConfiguration.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RandomConfiguration.swift; sourceTree = ""; }; + 8CB0FE5527EB596B00707490 /* RandomConfigurationTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RandomConfigurationTests.swift; sourceTree = ""; }; + 8CB0FE5727EB5F5000707490 /* FarblingProtectionHelperTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FarblingProtectionHelperTests.swift; sourceTree = ""; }; + 8CFEF62327F220E700CF3235 /* ScriptSourceType.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ScriptSourceType.swift; sourceTree = ""; }; + 8CFEF62527F2213900CF3235 /* UserScriptType.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UserScriptType.swift; sourceTree = ""; }; A104E198210A384400D2323E /* ShieldsViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ShieldsViewController.swift; sourceTree = ""; }; A134B88920DA98BB00A581D0 /* ClientPreferences.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ClientPreferences.swift; sourceTree = ""; }; A13AC72420EC12360040D4BB /* Migration.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Migration.swift; sourceTree = ""; }; @@ -5780,6 +5800,30 @@ path = "Brave Rewards"; sourceTree = ""; }; + 8CB0FD8C27DAAE9400707490 /* User Scripts */ = { + isa = PBXGroup; + children = ( + 8CB0FDA727DB9D5300707490 /* ScriptFactory.swift */, + 8CB0FDB927E0EC1B00707490 /* UserScriptHelper.swift */, + 8CB0FE4327EA69E100707490 /* FarblingProtectionHelper.swift */, + 8CB0FE4F27EB528200707490 /* RandomConfiguration.swift */, + 8CFEF62327F220E700CF3235 /* ScriptSourceType.swift */, + 8CFEF62527F2213900CF3235 /* UserScriptType.swift */, + ); + path = "User Scripts"; + sourceTree = ""; + }; + 8CB0FDD227E12D4600707490 /* User Scripts */ = { + isa = PBXGroup; + children = ( + 8CB0FDD327E12D7700707490 /* UserScriptHelperTests.swift */, + 8CB0FDD527E12DA100707490 /* ScriptFactoryTests.swift */, + 8CB0FE5527EB596B00707490 /* RandomConfigurationTests.swift */, + 8CB0FE5727EB5F5000707490 /* FarblingProtectionHelperTests.swift */, + ); + path = "User Scripts"; + sourceTree = ""; + }; A104E190210A380E00D2323E /* Shields */ = { isa = PBXGroup; children = ( @@ -6087,7 +6131,6 @@ 595E0EDA21CAD97C00813D49 /* CookieControl.js */, 0AE50853261C63D70099C6A3 /* BraveSearchHelper.js */, 449AC16E26D9008D00A53F33 /* BraveTalkHelper.js */, - 279C75C021A5B37D001CD1CB /* FingerprintingProtection.js */, 5EB57D0122FDC0CB00A07325 /* FullscreenHelper.js */, F9B23EE123F61173000EB3D8 /* PaymentRequest.js */, 5E4E078224A0E4D700B01720 /* YoutubeAdblock.js */, @@ -6097,6 +6140,8 @@ 5EC0017D260129AC005DDE4A /* PlaylistSwizzler.js */, CA55048F269DED8F00C19917 /* MediaBackgrounding.js */, CA2CA1B027F4B50300B25646 /* ReadyState.js */, + 8CB0FDD727E652B500707490 /* nacl.min.js */, + 8CB0FD9927DAB06E00707490 /* FarblingProtection.js */, ); path = UserScripts; sourceTree = ""; @@ -6212,6 +6257,7 @@ 0AADC4BB20D2A4F700FDE368 /* Favorites */, 2772236F2469B4AE0059A7EB /* Favicons */, 0A4B011620D02DAC004D4011 /* TabsBar */, + 8CB0FD8C27DAAE9400707490 /* User Scripts */, 27DCB9EA263764D50067EF4A /* Tab Tray */, 44331DC722551871007E3E93 /* Toolbars */, 0A583F752142BC9B0086B3E7 /* Search */, @@ -6250,7 +6296,6 @@ D04D1B91209790B60074B35F /* Toast.swift */, D0FCF7F41FE45842004A7995 /* UserScriptManager.swift */, 0A64384A24FD3F0F000E80A3 /* DomainUserScript.swift */, - 279C756A219DDE3B001CD1CB /* FingerprintingProtection.swift */, 5E34780F22D7A1D200B0D5F8 /* ResourceDownloadManager.swift */, 0A19365323508756002E2B81 /* LinkPreviewViewController.swift */, 0A66550923E9D9750047EF2A /* UserAgent.swift */, @@ -6517,6 +6562,7 @@ F84B21D61A090F8100AAB793 /* ClientTests */ = { isa = PBXGroup; children = ( + 8CB0FDD227E12D4600707490 /* User Scripts */, 0AD9F88D22F05376008B4D95 /* AdblockRustTests.swift */, 0A4BEFD5221E13830005551A /* ContentBlockerTests.swift */, E696FE501C47F86E00EC007C /* AuthenticatorTests.swift */, @@ -7738,6 +7784,7 @@ 4422D56E21BFFB7F00BF1855 /* make_perl_groups.pl in Resources */, 0A2FF33F24A4B75100F88F43 /* vpncheckmark.json in Resources */, F9F859702533CE52000CEB24 /* LoginsHelper.js in Resources */, + 8CB0FDD827E652B700707490 /* nacl.min.js in Resources */, 0AB22A95257EADA900126ADC /* corwin-prescott_beach.jpg in Resources */, CA2202B227199123002D3582 /* DarkWarning.svg in Resources */, F9488EB32589899800A72C84 /* RewardsReporting.js in Resources */, @@ -7747,6 +7794,7 @@ E4B7B7861A793CF20022C5E0 /* FiraSans-UltraLight.ttf in Resources */, 4481F23F26CBD3DC00658EAC /* ISRGRootCA_X1.cer in Resources */, 4481F22D26CBD1C600658EAC /* AmazonRootCA3.cer in Resources */, + 8CB0FD9A27DAB06E00707490 /* FarblingProtection.js in Resources */, E4B7B77D1A793CF20022C5E0 /* FiraSans-Regular.ttf in Resources */, 4481F23A26CBD30900658EAC /* AmazonRootCA1.cer in Resources */, CA225DF22795CECE0024C104 /* MainFrame.js in Resources */, @@ -7803,7 +7851,6 @@ 2F44FB2D1A9D5D8500FD20CC /* FiraSans-BoldItalic.ttf in Resources */, 5EE4CF5B2540868E00BC4509 /* Bookmarks.html in Resources */, F9BD7D9926C6E77600344FE7 /* upgrade-http.json in Resources */, - 279C75C821A5B37D001CD1CB /* FingerprintingProtection.js in Resources */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -8373,6 +8420,7 @@ 4422D4B921BFFB7600BF1855 /* crc32c.cc in Sources */, 0A39FE932486604D00290ABC /* GRDVPNHelper.m in Sources */, 592F521E2217327C0078395E /* HttpCookieExtension.swift in Sources */, + 8CFEF62427F220E700CF3235 /* ScriptSourceType.swift in Sources */, 4422D4D721BFFB7600BF1855 /* table.cc in Sources */, 0A4BEF62221B26610005551A /* LocalAdblockResourceProtocol.swift in Sources */, 27C626CB25BA198700418F40 /* WalletTransferExpiredViewController.swift in Sources */, @@ -8444,6 +8492,7 @@ 442477FA268CB86F008A04D7 /* BraveSearchDebugMenuDetail.swift in Sources */, 27AD20CC26851DD100889AA7 /* BraveRewards.swift in Sources */, 0A8ABE19247435E30062DA81 /* BraveVPNCommonUI.swift in Sources */, + 8CB0FDA827DB9D5400707490 /* ScriptFactory.swift in Sources */, A13AC72520EC12360040D4BB /* Migration.swift in Sources */, 27201EFC24535C2F00C19DD1 /* NewTabPageFlowLayout.swift in Sources */, 27FD2CAB2146C31C00A5A779 /* RequestDesktopSiteActivity.swift in Sources */, @@ -8687,6 +8736,7 @@ 2FD1C61C2639AE9100E3C25F /* BrowserViewController+Onboarding.swift in Sources */, 39455F771FC83F430088A22C /* TabEventHandler.swift in Sources */, 4422D4B721BFFB7600BF1855 /* filter_policy.cc in Sources */, + 8CB0FE5027EB528200707490 /* RandomConfiguration.swift in Sources */, 2F12744027DA56C7007EE7B7 /* LoginInfoViewController.swift in Sources */, CA0EE175273C248C00F269DA /* OnboardingPulseAnimationView.swift in Sources */, 27036EA325671817004EF6B6 /* FeedCardFooterButton.swift in Sources */, @@ -8742,6 +8792,8 @@ 4422D50121BFFB7600BF1855 /* repair.cc in Sources */, E660BDD91BB06521009AC090 /* TabsButton.swift in Sources */, 2FA01E5D25F2C93800103D67 /* ShieldsActivityItemSourceProvider.swift in Sources */, + 8CB0FE4427EA69E200707490 /* FarblingProtectionHelper.swift in Sources */, + 8CB0FDBA27E0EC1C00707490 /* UserScriptHelper.swift in Sources */, 4422D55721BFFB7F00BF1855 /* unicode_groups.cc in Sources */, 4422D55421BFFB7E00BF1855 /* re2_fuzzer.cc in Sources */, 2F44FCCB1A9E972E00FD20CC /* SearchEnginePicker.swift in Sources */, @@ -8788,7 +8840,6 @@ 274398F524E71D8700E79605 /* FailableDecodable.swift in Sources */, CA752EA526CEABF8009356EF /* PlaylistToast.swift in Sources */, 0A918DCD252C81FA00496088 /* BraveVPNRegionPickerViewController.swift in Sources */, - 279C756B219DDE3B001CD1CB /* FingerprintingProtection.swift in Sources */, E650755F1E37F756006961AC /* Try.m in Sources */, 2746D27324A2A5DA00E38852 /* RewardsInternalsContributionListController.swift in Sources */, 3B6889C51D66950E002AC85E /* UIImageColors.swift in Sources */, @@ -8881,6 +8932,7 @@ 0A1DF48424487AA000541FE4 /* GRDGatewayAPIResponse.m in Sources */, 4422D4B421BFFB7600BF1855 /* hash.cc in Sources */, 2F1A835A274C35340089A8A9 /* RetentionPreferencesDebugMenuViewController.swift in Sources */, + 8CFEF62627F2213A00CF3235 /* UserScriptType.swift in Sources */, 0A5CBD3823D4905D00362CC8 /* NTPNotificationView.swift in Sources */, 27D114D62358FCA400166534 /* SettingsRowViews.swift in Sources */, 4422D56621BFFB7F00BF1855 /* tostring.cc in Sources */, @@ -8914,6 +8966,7 @@ 5953AAEF2226E9D800A92DE1 /* HttpCookieExtensionTest.swift in Sources */, F84B21DA1A090F8100AAB793 /* ClientTests.swift in Sources */, 281B2BEA1ADF4D90002917DC /* MockProfile.swift in Sources */, + 8CB0FE5627EB596B00707490 /* RandomConfigurationTests.swift in Sources */, 2F697F7E1A9FD22D009E03AE /* SearchEnginesTests.swift in Sources */, 27C46201211CD8D20088A441 /* DeferredTestUtils.swift in Sources */, 2F44FA1B1A9D426A00FD20CC /* TestHashExtensions.swift in Sources */, @@ -8926,15 +8979,18 @@ 2FD0E3AF2576C48A000C773B /* SchemePermissionTests.swift in Sources */, 27756ED825AF701C00C129AF /* SearchTests.swift in Sources */, 0BF42D4F1A7CD09600889E28 /* TestFavicons.swift in Sources */, + 8CB0FE5827EB5F5100707490 /* FarblingProtectionHelperTests.swift in Sources */, 5E9288CA22DF864C007BE7A6 /* TabSessionTests.swift in Sources */, 0A4214E921A6EBCF006B8E39 /* SafeBrowsingTests.swift in Sources */, CA04E8A627022D9D00BFBB4D /* PlaylistTests.swift in Sources */, + 8CB0FDD627E12DA100707490 /* ScriptFactoryTests.swift in Sources */, 0A8A623025790D0900B035F4 /* UniversalLinkManagerTests.swift in Sources */, 2F26C438267B9F0D00E5AD60 /* BlockSummaryTests.swift in Sources */, 7BBFEE741BB405D900A305AA /* TabManagerTests.swift in Sources */, 0A4BEFDE221F03C80005551A /* NetworkManagerTests.swift in Sources */, 39236E721FCC600200A38F1B /* TabEventHandlerTests.swift in Sources */, 2F13E79B1AC0C02700D75081 /* StringExtensionsTests.swift in Sources */, + 8CB0FDD427E12D7700707490 /* UserScriptHelperTests.swift in Sources */, 59350AFB22612D95004D7445 /* DisplayTextFieldTest.swift in Sources */, 0A66550C23E9E04F0047EF2A /* UserAgentTests.swift in Sources */, 2FDB10931A9FBEC5006CF312 /* PrefsTests.swift in Sources */, diff --git a/Client/Frontend/Browser/BrowserViewController.swift b/Client/Frontend/Browser/BrowserViewController.swift index f4a2f167462..2a10f66befe 100644 --- a/Client/Frontend/Browser/BrowserViewController.swift +++ b/Client/Frontend/Browser/BrowserViewController.swift @@ -375,6 +375,7 @@ class BrowserViewController: UIViewController, BrowserViewControllerDelegate { override func didReceiveMemoryWarning() { super.didReceiveMemoryWarning() + ScriptFactory.shared.clearCaches() for tab in tabManager.tabsForCurrentMode where tab.id != tabManager.selectedTab?.id { tab.newTabPageViewController = nil @@ -2047,8 +2048,6 @@ extension BrowserViewController: TabDelegate { tab.addContentScript(FocusHelper(tab: tab), name: FocusHelper.name(), contentWorld: .defaultClient) - tab.addContentScript(FingerprintingProtection(tab: tab), name: FingerprintingProtection.name(), contentWorld: .page) - tab.addContentScript(BraveGetUA(tab: tab), name: BraveGetUA.name(), contentWorld: .page) tab.addContentScript( BraveSearchScriptHandler( diff --git a/Client/Frontend/Browser/BrowserViewController/BrowserViewController+WKNavigationDelegate.swift b/Client/Frontend/Browser/BrowserViewController/BrowserViewController+WKNavigationDelegate.swift index 17334d300eb..e7f2f41ae42 100644 --- a/Client/Frontend/Browser/BrowserViewController/BrowserViewController+WKNavigationDelegate.swift +++ b/Client/Frontend/Browser/BrowserViewController/BrowserViewController+WKNavigationDelegate.swift @@ -183,10 +183,12 @@ extension BrowserViewController: WKNavigationDelegate { } let isPrivateBrowsing = PrivateBrowsingManager.shared.isPrivateBrowsing - - // Check if custom user scripts must be added to the web view. let tab = tabManager[webView] - tab?.userScriptManager?.handleDomainUserScript(for: url) + + // Check if custom user scripts must be added to or removed from the web view. + tab?.userScriptManager?.userScriptTypes = UserScriptHelper.getUserScriptTypes( + for: navigationAction, options: isPrivateBrowsing ? .privateBrowsing : .default + ) // Brave Search logic. @@ -268,17 +270,8 @@ extension BrowserViewController: WKNavigationDelegate { on.compactMap { $0.rule }.forEach(controller.add) off.compactMap { $0.rule }.forEach(controller.remove) - if let tab = tabManager[webView] { - tab.userScriptManager?.isFingerprintingProtectionEnabled = - domainForShields.isShieldExpected(.FpProtection, considerAllShieldsOption: true) - } - let isScriptsEnabled = !domainForShields.isShieldExpected(.NoScript, considerAllShieldsOption: true) - if #available(iOS 14.0, *) { preferences.allowsContentJavaScript = isScriptsEnabled - } else { - webView.configuration.preferences.javaScriptEnabled = isScriptsEnabled - } } // Cookie Blocking code below @@ -518,3 +511,18 @@ extension BrowserViewController: WKNavigationDelegate { tab.redirectURLs.append(url) } } + +extension WKNavigationType: CustomDebugStringConvertible { + public var debugDescription: String { + switch self { + case .linkActivated: return "linkActivated" + case .formResubmitted: return "formResubmitted" + case .backForward: return "backForward" + case .formSubmitted: return "formSubmitted" + case .other: return "other" + case .reload: return "reload" + @unknown default: + return "Unknown(\(rawValue))" + } + } +} diff --git a/Client/Frontend/Browser/DomainUserScript.swift b/Client/Frontend/Browser/DomainUserScript.swift index becafb90243..e6b1f20db03 100644 --- a/Client/Frontend/Browser/DomainUserScript.swift +++ b/Client/Frontend/Browser/DomainUserScript.swift @@ -11,139 +11,53 @@ import WebKit private let log = Logger.browserLogger enum DomainUserScript: CaseIterable { - case youtube + case youtubeAdBlock case archive - case braveSearch - case braveTalk - - static func get(for url: URL) -> Self? { - var found: DomainUserScript? - - // First we look for exact domain match, if no matches we look for base domain matches. - guard let host = url.host else { return nil } - allCases.forEach { - if $0.associatedDomains.contains(host) { - found = $0 - return - } + case braveSearchHelper + case braveTalkHelper + + /// Initialize this script with a URL + init?(for url: URL) { + // First we look for an exact domain match + if let host = url.host, let found = Self.allCases.first(where: { $0.associatedDomains.contains(host) }) { + self = found + return } - if found != nil { return found } - - guard let baseDomain = url.baseDomain else { return nil } - allCases.forEach { - if $0.associatedDomains.contains(baseDomain) { - found = $0 - return - } + // If no matches, we look for a baseDomain (eTLD+1) match. + if let baseDomain = url.baseDomain, let found = Self.allCases.first(where: { $0.associatedDomains.contains(baseDomain) }) { + self = found + return } - return found + return nil } /// Returns a shield type for a given user script domain. - /// Returns nil if the domain's user script can't be turned off via a shield toggle. + /// Returns nil if the domain's user script can't be turned off via a shield toggle. (i.e. it's always enabled) var shieldType: BraveShield? { switch self { - case .youtube: + case .youtubeAdBlock: return .AdblockAndTp - case .archive, .braveSearch, .braveTalk: + case .archive, .braveSearchHelper, .braveTalkHelper: return nil } } + /// The domains associated with this script. var associatedDomains: Set { switch self { - case .youtube: - return .init(arrayLiteral: "youtube.com") - case .archive: - return .init(arrayLiteral: "archive.is", "archive.today", "archive.vn", "archive.fo") - case .braveSearch: - return .init(arrayLiteral: "search.brave.com", "search-dev.brave.com") - case .braveTalk: - return .init( - arrayLiteral: "talk.brave.com", "beta.talk.brave.com", - "talk.bravesoftware.com", "beta.talk.bravesoftware.com", - "dev.talk.brave.software", "beta.talk.brave.software", - "talk.brave.software") - } - } - - private var scriptName: String { - switch self { - case .youtube: - return "YoutubeAdblock" - case .archive: - return "ArchiveIsCompat" - case .braveSearch: - return "BraveSearchHelper" - case .braveTalk: - return "BraveTalkHelper" - } - } - - var script: WKUserScript? { - guard let source = sourceFile else { return nil } - - switch self { - case .youtube: - // Verify that the application itself is making a call to the JS script instead of other scripts on the page. - // This variable will be unique amongst scripts loaded in the page. - // When the script is called, the token is provided in order to access the script variable. - var alteredSource = source - let token = UserScriptManager.securityTokenString - alteredSource = alteredSource.replacingOccurrences( - of: "$", with: "ABSPP\(token)", - options: .literal) - alteredSource = alteredSource.replacingOccurrences( - of: "$", with: "ABSFO\(token)", - options: .literal) - alteredSource = alteredSource.replacingOccurrences( - of: "$", with: "ABSSJ\(token)", - options: .literal) - - return WKUserScript.create(source: alteredSource, injectionTime: .atDocumentStart, forMainFrameOnly: false, in: .page) + case .youtubeAdBlock: + return Set(arrayLiteral: "youtube.com") case .archive: - return WKUserScript.create(source: source, injectionTime: .atDocumentStart, forMainFrameOnly: false, in: .page) - case .braveSearch: - var alteredSource = source - - let securityToken = UserScriptManager.securityTokenString - alteredSource = - alteredSource - .replacingOccurrences( - of: "$", - with: "BSH\(UserScriptManager.messageHandlerTokenString)", - options: .literal - ) - .replacingOccurrences(of: "$", with: securityToken) - - return WKUserScript.create(source: alteredSource, injectionTime: .atDocumentStart, forMainFrameOnly: false, in: .page) - case .braveTalk: - var alteredSource = source - - let securityToken = UserScriptManager.securityTokenString - alteredSource = - alteredSource - .replacingOccurrences( - of: "$", - with: "BT\(UserScriptManager.messageHandlerTokenString)", - options: .literal - ) - .replacingOccurrences(of: "$", with: securityToken) - - return WKUserScript.create(source: alteredSource, injectionTime: .atDocumentStart, forMainFrameOnly: false, in: .page) - } - } - - private var sourceFile: String? { - guard let path = Bundle.main.path(forResource: scriptName, ofType: "js"), - let source = try? String(contentsOfFile: path) - else { - log.error("Failed to load \(scriptName).js") - return nil + return Set(arrayLiteral: "archive.is", "archive.today", "archive.vn", "archive.fo") + case .braveSearchHelper: + return Set(arrayLiteral: "search.brave.com", "search-dev.brave.com") + case .braveTalkHelper: + return Set(arrayLiteral: "talk.brave.com", "beta.talk.brave.com", + "talk.bravesoftware.com", "beta.talk.bravesoftware.com", + "dev.talk.brave.software", "beta.talk.brave.software", + "talk.brave.software") } - - return source } } diff --git a/Client/Frontend/Browser/Handlers/BraveSearchScriptHandler.swift b/Client/Frontend/Browser/Handlers/BraveSearchScriptHandler.swift index 21eeff0048d..4b65e935391 100644 --- a/Client/Frontend/Browser/Handlers/BraveSearchScriptHandler.swift +++ b/Client/Frontend/Browser/Handlers/BraveSearchScriptHandler.swift @@ -53,7 +53,7 @@ class BraveSearchScriptHandler: TabContentScript { replyHandler: (Any?, String?) -> Void ) { defer { replyHandler(nil, nil) } - let allowedHosts = DomainUserScript.braveSearch.associatedDomains + let allowedHosts = DomainUserScript.braveSearchHelper.associatedDomains guard let requestHost = message.frameInfo.request.url?.host, allowedHosts.contains(requestHost), diff --git a/Client/Frontend/Browser/Handlers/BraveTalkScriptHandler.swift b/Client/Frontend/Browser/Handlers/BraveTalkScriptHandler.swift index 8f89bb3081f..f868fb301f2 100644 --- a/Client/Frontend/Browser/Handlers/BraveTalkScriptHandler.swift +++ b/Client/Frontend/Browser/Handlers/BraveTalkScriptHandler.swift @@ -34,7 +34,7 @@ class BraveTalkScriptHandler: TabContentScript { replyHandler: (Any?, String?) -> Void ) { defer { replyHandler(nil, nil) } - let allowedHosts = DomainUserScript.braveTalk.associatedDomains + let allowedHosts = DomainUserScript.braveTalkHelper.associatedDomains guard let requestHost = message.frameInfo.request.url?.host, allowedHosts.contains(requestHost), diff --git a/Client/Frontend/Browser/Playlist/Managers & Cache/PlaylistCacheLoader.swift b/Client/Frontend/Browser/Playlist/Managers & Cache/PlaylistCacheLoader.swift index c19dc85c5bf..ed73f388e4a 100644 --- a/Client/Frontend/Browser/Playlist/Managers & Cache/PlaylistCacheLoader.swift +++ b/Client/Frontend/Browser/Playlist/Managers & Cache/PlaylistCacheLoader.swift @@ -623,7 +623,9 @@ extension PlaylistWebLoader: WKNavigationDelegate { return } - tab.userScriptManager?.handleDomainUserScript(for: url) + tab.userScriptManager?.userScriptTypes = UserScriptHelper.getUserScriptTypes( + for: navigationAction, options: .playlistCacheLoader + ) // For Playlist automatic detection since the above `handleDomainUserScript` removes ALL scripts! if let script = playlistDetectorScript { @@ -675,9 +677,6 @@ extension PlaylistWebLoader: WKNavigationDelegate { on.compactMap { $0.rule }.forEach(controller.add) off.compactMap { $0.rule }.forEach(controller.remove) - tab.userScriptManager?.isFingerprintingProtectionEnabled = - domainForShields.isShieldExpected(.FpProtection, considerAllShieldsOption: true) - webView.configuration.preferences.javaScriptEnabled = !domainForShields.isShieldExpected(.NoScript, considerAllShieldsOption: true) } diff --git a/Client/Frontend/Browser/Tab.swift b/Client/Frontend/Browser/Tab.swift index cab03ddceeb..196ef88465f 100644 --- a/Client/Frontend/Browser/Tab.swift +++ b/Client/Frontend/Browser/Tab.swift @@ -295,7 +295,6 @@ class Tab: NSObject { self.webView?.addObserver(self, forKeyPath: KVOConstants.URL.rawValue, options: .new, context: nil) self.userScriptManager = UserScriptManager( tab: self, - isFingerprintingProtectionEnabled: Preferences.Shields.fingerprintingProtection.value, isCookieBlockingEnabled: Preferences.Privacy.blockAllCookies.value, isPaymentRequestEnabled: webView.hasOnlySecureContent, isWebCompatibilityMediaSourceAPIEnabled: Preferences.Playlist.webMediaSourceCompatibility.value, diff --git a/Client/Frontend/Browser/User Scripts/FarblingProtectionHelper.swift b/Client/Frontend/Browser/User Scripts/FarblingProtectionHelper.swift new file mode 100644 index 00000000000..4f70226f39e --- /dev/null +++ b/Client/Frontend/Browser/User Scripts/FarblingProtectionHelper.swift @@ -0,0 +1,158 @@ +// Copyright 2022 The Brave Authors. All rights reserved. +// 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 Foundation + +/// A class that helps in creating farbling data +class FarblingProtectionHelper { + /// Represents `JSON` data that needs to be passed to `FarblingProtection.js` + struct FarblingData: Encodable { + /// Represents the `JSON` data that is needed to construct a fake WebKit `Plugin` + struct FakePluginData: Encodable { + let name: String + let filename: String + let description: String + let mimeTypes: [FakeMimeTypeData] + } + + /// Represents the `JSON` data that is needed to construct a fake WebKit `MimeType` + struct FakeMimeTypeData: Encodable { + let suffixes: String + let type: String + let description: String + } + + /// A value between 0.99 and 1 to fudge audio data + /// + /// A value between 0.99 to 1 means the values in the destination will + /// always be within the expected range of -1 and 1. + /// This small decrease should not affect affect legitimite users of this api. + /// But will affect fingerprinters by introducing a small random change. + let fudgeFactor: Float + /// A value representing a fake voice name that will be used to add a fake voice + let fakeVoiceName: String + /// Fake data that is to be used to construct fake plugins + let fakePluginData: [FakePluginData] + /// This value is used to get a random index between 0 and an unknown count + /// + /// It's important to have a value between 0 - 1 in order to be within the array bounds + let randomVoiceIndexScale: Float + } + + /// Variables representing the prefix of a randomly generated strings used as the plugin name + private static let pluginNameFirstParts: [String?] = [ + "Chrome", "Chromium", "Brave", "Web", "Browser", + "OpenSource", "Online", "JavaScript", "WebKit", + "Web-Kit", "WK", nil + ] + + /// Variables representing the middle of a randomly generated strings used as the plugin name + private static let pluginNameSecondParts: [String?] = [ + "PDF", "Portable Document Format", + "portable-document-format", "document", "doc", + "PDF and PS", "com.adobe.pdf", nil + ] + + /// Variables representing the suffix of a randomly generated strings used as the plugin name + private static let pluginNameThirdParts: [String?] = [ + "Viewer", "Renderer", "Display", "Plugin", + "plug-in", "plug in", "extension", nil + ] + + /// A list of fake voice names to be used to generate a fake `SpeechSynthesizer` voice + private static let fakeVoiceNames: [String] = [ + "Hubert", "Vernon", "Rudolph", "Clayton", "Irving", + "Wilson", "Alva", "Harley", "Beauregard", "Cleveland", + "Cecil", "Reuben", "Sylvester", "Jasper" + ] + + static func makeFarblingParams(from randomConfiguration: RandomConfiguration) throws -> String { + srand48(randomConfiguration.domainKeyHEX.hashValue) + + let farblingData = FarblingData( + fudgeFactor: Float.seededRandom(in: 0.99...1), + fakeVoiceName: fakeVoiceNames.seededRandom() ?? "", + fakePluginData: FarblingProtectionHelper.makeFakePluginData(), + randomVoiceIndexScale: Float(drand48()) + ) + + let encoder = JSONEncoder() + let data = try encoder.encode(farblingData) + return String(data: data, encoding: .utf8)! + } + + /// Generate fake plugin data to be injected into the farbling protection script + private static func makeFakePluginData() -> [FarblingData.FakePluginData] { + let pluginCount = UInt8.seededRandom(in: 1...3) + + // Generate 1 to 3 fake plugins + return (0.. FarblingData.FakePluginData in + let mimeTypesCount = UInt8.seededRandom(in: 1...3) + + // Generate 1 to 3 fake mime types + let mimeTypes = (0.. FarblingData.FakeMimeTypeData in + return FarblingData.FakeMimeTypeData( + suffixes: "pdf", + type: "application/pdf", + description: randomPluginName() + ) + } + + return FarblingData.FakePluginData( + name: randomPluginName(), + filename: "", + description: randomPluginName(), + mimeTypes: mimeTypes + ) + } + } + + /// Generate a random string using a prefix, middle and suffix where any of those may be empty. + /// - Note: May result in an empty string. + private static func randomPluginName() -> String { + return [ + pluginNameFirstParts.seededRandom(), + pluginNameSecondParts.seededRandom(), + pluginNameThirdParts.seededRandom() + ].compactMap({ $0 ?? nil }).joined(separator: " ") + } +} + +private extension FixedWidthInteger { + /// Return a random value in the given range. + /// + /// Uses `drand48`, hence you need to seed it before using this function using `srand48` + static func seededRandom(in range: ClosedRange) -> Self { + let size = Double(range.upperBound - range.lowerBound) + let offset = drand48() * size + let value = Self(Double(range.lowerBound) + offset) + return value + } +} + +private extension Float { + /// Return a random float in the given range. + /// + /// Uses `drand48`, hence you need to seed it before using this function using `srand48` + static func seededRandom(in range: ClosedRange) -> Self { + let size = Double(range.upperBound - range.lowerBound) + let offset = drand48() * size + let value = Self(Double(range.lowerBound) + offset) + return value + } +} + +private extension Array { + /// Return a random value in the given range. + /// + /// Uses `drand48`, hence you need to seed it before using this function using `srand48` + /// - Note: Will return a `nil` value only if the array is empty. You can safely force unswrap the result + /// in cases where the array is not empty + func seededRandom() -> Element? { + guard !isEmpty else { return nil } + let randomIndex = Int(UInt.seededRandom(in: 0...UInt(count - 1))) + return self[randomIndex] + } +} diff --git a/Client/Frontend/Browser/User Scripts/RandomConfiguration.swift b/Client/Frontend/Browser/User Scripts/RandomConfiguration.swift new file mode 100644 index 00000000000..7f8826ebddd --- /dev/null +++ b/Client/Frontend/Browser/User Scripts/RandomConfiguration.swift @@ -0,0 +1,65 @@ +// Copyright 2022 The Brave Authors. All rights reserved. +// 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 Foundation +import CryptoKit + +/// Class that aids creating the same random values for any given eTLD. +/// +/// Constructing this class the same eTLD+1 will result in the same random values +/// (provided the same `sessionKey` is used) +/// - Note: Any string can actually be used and this class can be used for more general purpose applications +class RandomConfiguration { + /// This is used to encode the domain key (i.e. the eTLD+1). + /// + /// For farbling, this key should be the same for the lifecycle of the app. Hence we use a static variable. + private static let sessionKey = SymmetricKey(size: .bits256) + + /// The eTLD+1 this random manager was created with + private let etld: String + + /// The session key this random manager was created with + private let sessionKey: SymmetricKey + + /// The 64-character hex domain key for the provided eTLD+1 + /// + /// - Note: A 64 character hex is represented by 256 bits. This means we can use it directly in our `ARC4RandomNumberGenerator`. + private(set) lazy var domainKeyData: Data = { + let signature = HMAC.authenticationCode(for: Data(etld.utf8), using: sessionKey) + return Data(signature) + }() + + /// The domain key as hex `String` + private(set) lazy var domainKeyHEX: String = { + domainKeyData.hexString + }() + + /// The domain key as a `SymmetricKey` + private(set) lazy var domainKey: SymmetricKey = { + return SymmetricKey(data: domainKeyData) + }() + + /// Initialize this class with an eTLD+1 and a sessionKey. + /// + /// If no sessionKey is provided, a shared session key will be used. + /// The shared session key is lost when the application terminates. + /// - Note: This is what we want for farbling. + init(etld: String, sessionKey: SymmetricKey = RandomConfiguration.sessionKey) { + self.etld = etld + self.sessionKey = sessionKey + } + + /// Signs this given value using the `domainKey` + func domainSignedKey(for value: String) -> String { + let signature = HMAC.authenticationCode(for: Data(value.utf8), using: domainKey) + return Data(signature).hexString + } +} + +private extension Data { + var hexString: String { + map({ String(format: "%02hhx", $0) }).joined() + } +} diff --git a/Client/Frontend/Browser/User Scripts/ScriptFactory.swift b/Client/Frontend/Browser/User Scripts/ScriptFactory.swift new file mode 100644 index 00000000000..a814356cce7 --- /dev/null +++ b/Client/Frontend/Browser/User Scripts/ScriptFactory.swift @@ -0,0 +1,118 @@ +// Copyright 2022 The Brave Authors. All rights reserved. +// 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 Foundation +import WebKit + +/// An error representing failures in loading scripts +enum ScriptLoadFailure: Error { + case notFound +} + +/// A class that helps in the aid of creating and caching scripts. +class ScriptFactory { + /// A shared instance to be shared throughout the app. + /// + /// - Note: Perhaps we will move this to the tab to be managed on a tab basis. + static let shared = ScriptFactory() + + /// Ensures that the message handlers cannot be invoked by the page scripts + public static let messageHandlerTokenString: String = { + return UUID().uuidString.replacingOccurrences(of: "-", with: "", options: .literal) + }() + + /// This contains cahced script sources for a script type. Avoids reading from disk. + private var cachedScriptSources: [ScriptSourceType: String] + + /// This contains cached altered scripts that are ready to be inected. Avoids replacing strings. + private var cachedDomainScriptsSources: [UserScriptType: WKUserScript] + + init() { + cachedScriptSources = [:] + cachedDomainScriptsSources = [:] + } + + /// Clear some caches in case we need to. + /// + /// Should only really be called in a memory warning scenario. + func clearCaches() { + cachedScriptSources = [:] + cachedDomainScriptsSources = [:] + } + + /// Returns a script source by loading a file or returning cached data + private func makeScriptSource(of type: ScriptSourceType) throws -> String { + if let source = cachedScriptSources[type] { + return source + } else { + let source = try type.loadScript() + cachedScriptSources[type] = source + return source + } + } + + /// Get a script for the `UserScriptType`. + /// + /// Scripts can be cached on two levels: + /// - On the unmodified source file (per `ScriptSourceType`) + /// - On the modfied source file (per `UserScriptType`) + func makeScript(for domainType: UserScriptType) throws -> WKUserScript { + var source = try makeScriptSource(of: domainType.sourceType) + + // First check for and return cached value + if let script = cachedDomainScriptsSources[domainType] { + return script + } + + switch domainType { + case .farblingProtection(let etld): + let randomConfiguration = RandomConfiguration(etld: etld) + let fakeParams = try FarblingProtectionHelper.makeFarblingParams(from: randomConfiguration) + source = "\(source)\nwindow.braveFarble(\(fakeParams))\ndelete window.braveFarble" + + case .nacl: + // No modifications needed + break + + case .domainUserScript(let domainUserScript): + switch domainUserScript { + case .youtubeAdBlock: + // Verify that the application itself is making a call to the JS script instead of other scripts on the page. + // This variable will be unique amongst scripts loaded in the page. + // When the script is called, the token is provided in order to access the script variable. + let securityToken = UserScriptManager.securityTokenString + + source = source + .replacingOccurrences(of: "$", with: "ABSPP\(securityToken)", options: .literal) + .replacingOccurrences(of: "$", with: "ABSFO\(securityToken)", options: .literal) + .replacingOccurrences(of: "$", with: "ABSSJ\(securityToken)", options: .literal) + + case .archive: + // No modifications needed + break + + case .braveSearchHelper: + let securityToken = UserScriptManager.securityTokenString + let messageToken = "BSH\(UserScriptManager.messageHandlerTokenString)" + + source = source + .replacingOccurrences(of: "$", with: messageToken, options: .literal) + .replacingOccurrences(of: "$", with: securityToken) + + case .braveTalkHelper: + let securityToken = UserScriptManager.securityTokenString + let messageToken = "BT\(UserScriptManager.messageHandlerTokenString)" + + source = source + .replacingOccurrences(of: "$", with: messageToken, options: .literal) + .replacingOccurrences(of: "$", with: securityToken) + } + } + + let userScript = WKUserScript.create(source: source, injectionTime: domainType.injectionTime, forMainFrameOnly: domainType.forMainFrameOnly, in: domainType.contentWorld) + cachedDomainScriptsSources[domainType] = userScript + return userScript + } +} diff --git a/Client/Frontend/Browser/User Scripts/ScriptSourceType.swift b/Client/Frontend/Browser/User Scripts/ScriptSourceType.swift new file mode 100644 index 00000000000..fb2c5b7df6b --- /dev/null +++ b/Client/Frontend/Browser/User Scripts/ScriptSourceType.swift @@ -0,0 +1,43 @@ +// Copyright 2022 The Brave Authors. All rights reserved. +// 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 Foundation + +/// An enum representing the unmodified local scripts stored in the application. +/// +/// - Warning: Some of these scripts are not usable "as-is". Rather, you should be using `UserScriptType`. +enum ScriptSourceType { + /// A simple encryption library found here: + /// https://www.npmjs.com/package/tweetnacl + case nacl + /// This script farbles certian system methods to output slightly randomized output. + /// This script has a dependency on `nacl`. + case farblingProtection + /// A YouTube ad blocking script + case youtubeAdBlock + case archive + case braveSearchHelper + case braveTalkHelper + + var fileName: String { + switch self { + case .nacl: return "nacl.min" + case .farblingProtection: return "FarblingProtection" + case .youtubeAdBlock: return "YoutubeAdblock" + case .archive: return "ArchiveIsCompat" + case .braveSearchHelper: return "BraveSearchHelper" + case .braveTalkHelper: return "BraveTalkHelper" + } + } + + func loadScript() throws -> String { + guard let path = Bundle.main.path(forResource: fileName, ofType: "js") else { + assertionFailure("Cannot load script. This should not happen as it's part of the codebase") + throw ScriptLoadFailure.notFound + } + + return try String(contentsOfFile: path) + } +} diff --git a/Client/Frontend/Browser/User Scripts/UserScriptHelper.swift b/Client/Frontend/Browser/User Scripts/UserScriptHelper.swift new file mode 100644 index 00000000000..96e49487538 --- /dev/null +++ b/Client/Frontend/Browser/User Scripts/UserScriptHelper.swift @@ -0,0 +1,72 @@ +// Copyright 2022 The Brave Authors. All rights reserved. +// 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 Foundation +import WebKit +import Data + +/// A class that helps in handling user scripts. +class UserScriptHelper { + /// Object that represent the settings when returning deomian script types. + struct DomainScriptOptions: OptionSet { + let rawValue: Int + + /// Wether or not we should persist shield settings + static let persistShieldSettings = DomainScriptOptions(rawValue: 1 << 0) + + /// These settings are tuned for a regular browsing session: + /// - Persist shield settings + static let `default`: DomainScriptOptions = [persistShieldSettings] + + /// These settings are tuned for a private browsing session: + /// - Does not persist shield settings + static let privateBrowsing: DomainScriptOptions = [] + + /// These options are tuned for the `PlaylistCacheLoader`: + /// - Does not persist shield settings + static let playlistCacheLoader: DomainScriptOptions = [] + } + + /// Return the set of DomainScryptTypes for this navigation action. + static func getUserScriptTypes(for navigationAction: WKNavigationAction, options: DomainScriptOptions) -> Set { + var userScriptTypes: Set = [] + + // Handle dynamic domain level scripts on the request + if let url = navigationAction.request.url { + // Add the old domain user scripts + if let domainUserScript = DomainUserScript(for: url) { + if let shieldType = domainUserScript.shieldType { + // We need to check the shield settings for this domain. + let domain = Domain.getOrCreate(forUrl: url, persistent: options.contains(.persistShieldSettings)) + + if domain.isShieldExpected(shieldType, considerAllShieldsOption: true) { + // Add the shield only if its enabled + userScriptTypes.insert(.domainUserScript(domainUserScript)) + } + } else { + // It means the script is always on. No shield setting is required. + userScriptTypes.insert(.domainUserScript(domainUserScript)) + } + } + } + + // Handle dynamic domain level scripts on the main document. + // These are scripts that change depending on the domain and the main document + if let mainDocumentURL = navigationAction.request.mainDocumentURL { + let domainForShields = Domain.getOrCreate(forUrl: mainDocumentURL, persistent: options.contains(.persistShieldSettings)) + let isFPProtectionOn = domainForShields.isShieldExpected(.FpProtection, considerAllShieldsOption: true) + + // Add the `farblingProtection` script if needed + // Note: The added farbling protection script based on the document url, not the frame's url. + // It is also added for every frame, including subframes. + if let etldP1 = mainDocumentURL.baseDomain, isFPProtectionOn { + userScriptTypes.insert(.nacl) // dependency for `farblingProtection` + userScriptTypes.insert(.farblingProtection(etld: etldP1)) + } + } + + return userScriptTypes + } +} diff --git a/Client/Frontend/Browser/User Scripts/UserScriptType.swift b/Client/Frontend/Browser/User Scripts/UserScriptType.swift new file mode 100644 index 00000000000..0321580df37 --- /dev/null +++ b/Client/Frontend/Browser/User Scripts/UserScriptType.swift @@ -0,0 +1,71 @@ +// Copyright 2022 The Brave Authors. All rights reserved. +// 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 Foundation +import WebKit + +/// An enum representing a specific (modified) variation of a local script replacing any dynamic variables. +enum UserScriptType: Hashable { + /// This type does farbling protection and is customized for the provided eTLD+1 + /// Has a dependency on `nacl` + case farblingProtection(etld: String) + /// Scripts specific to certain domains + case domainUserScript(DomainUserScript) + /// A symple encryption library to be used by other scripts + case nacl + + /// Return a source typ for this script type + var sourceType: ScriptSourceType { + switch self { + case .farblingProtection: + return .farblingProtection + case .domainUserScript(let domainUserScript): + switch domainUserScript { + case .youtubeAdBlock: + return .youtubeAdBlock + case .archive: + return .archive + case .braveSearchHelper: + return .braveSearchHelper + case .braveTalkHelper: + return .braveTalkHelper + } + case .nacl: + return .nacl + } + } + + /// The order in which we want to inject the scripts + var order: Int { + switch self { + case .nacl: return 0 + case .farblingProtection: return 1 + case .domainUserScript: return 2 + } + } + + var injectionTime: WKUserScriptInjectionTime { + switch self { + case .farblingProtection, .domainUserScript, .nacl: + return .atDocumentStart + } + } + + var forMainFrameOnly: Bool { + switch self { + case .farblingProtection, .domainUserScript, .nacl: + return false + } + } + + var contentWorld: WKContentWorld { + switch self { + case .farblingProtection, .domainUserScript: + return .page + case .nacl: + return .page + } + } +} diff --git a/Client/Frontend/Browser/UserScriptManager.swift b/Client/Frontend/Browser/UserScriptManager.swift index 9685dbf9728..ff0658b3340 100644 --- a/Client/Frontend/Browser/UserScriptManager.swift +++ b/Client/Frontend/Browser/UserScriptManager.swift @@ -24,14 +24,6 @@ class UserScriptManager { private weak var tab: Tab? - /// Whether or not the fingerprinting protection - var isFingerprintingProtectionEnabled: Bool { - didSet { - if oldValue == isFingerprintingProtectionEnabled { return } - reloadUserScripts() - } - } - /// Whether cookie blocking is enabled var isCookieBlockingEnabled: Bool { didSet { @@ -80,38 +72,19 @@ class UserScriptManager { } } - /// Stores domain specific scriplet, usually used for webcompat workarounds. - var domainUserScript: DomainUserScript? { + // TODO: @JS Add other scripts to this list to avoid uneccesary calls to `reloadUserScripts()` + /// Domain script types that are currently injected into the web-view. Will reloaded scripts if this set changes. + /// + /// We only `reloadUserScripts()` if any of these have changed. A set is used to ignore order and ensure uniqueness. + /// This way we don't necessarily invoke `reloadUserScripts()` too often but only when necessary. + /// + var userScriptTypes: Set { didSet { - if oldValue == domainUserScript { return } + guard oldValue != userScriptTypes else { return } reloadUserScripts() } } - func handleDomainUserScript(for url: URL) { - guard let customDomainUserScript = DomainUserScript.get(for: url) else { - // No custom script for this domain, clearing existing user script - // in case the previous domain had one. - domainUserScript = nil - return - } - - if let shieldType = customDomainUserScript.shieldType { - let domain = Domain.getOrCreate( - forUrl: url, - persistent: !PrivateBrowsingManager.shared.isPrivateBrowsing) - - if domain.isShieldExpected(shieldType, considerAllShieldsOption: true) { - domainUserScript = customDomainUserScript - } else { - // Remove old user script. - domainUserScript = nil - } - } else { - domainUserScript = customDomainUserScript - } - } - public static func isMessageHandlerTokenMissing(in body: [String: Any]) -> Bool { guard let token = body["securitytoken"] as? String, token == UserScriptManager.messageHandlerTokenString else { return true @@ -121,7 +94,6 @@ class UserScriptManager { init( tab: Tab, - isFingerprintingProtectionEnabled: Bool, isCookieBlockingEnabled: Bool, isPaymentRequestEnabled: Bool, isWebCompatibilityMediaSourceAPIEnabled: Bool, @@ -129,13 +101,13 @@ class UserScriptManager { isNightModeEnabled: Bool ) { self.tab = tab - self.isFingerprintingProtectionEnabled = isFingerprintingProtectionEnabled self.isCookieBlockingEnabled = isCookieBlockingEnabled self.isPaymentRequestEnabled = isPaymentRequestEnabled self.isWebCompatibilityMediaSourceAPIEnabled = isWebCompatibilityMediaSourceAPIEnabled self.isPlaylistEnabled = true self.isMediaBackgroundPlaybackEnabled = isMediaBackgroundPlaybackEnabled self.isNightModeEnabled = isNightModeEnabled + self.userScriptTypes = [] reloadUserScripts() } @@ -169,20 +141,6 @@ class UserScriptManager { } }() - private let fingerprintingProtectionUserScript: WKUserScript? = { - guard let path = Bundle.main.path(forResource: "FingerprintingProtection", ofType: "js"), let source = try? String(contentsOfFile: path) else { - log.error("Failed to load fingerprinting protection user script") - return nil - } - var alteredSource = source - alteredSource = alteredSource.replacingOccurrences(of: "$", with: "FingerprintingProtection\(messageHandlerTokenString)", options: .literal) - return WKUserScript.create( - source: alteredSource, - injectionTime: .atDocumentStart, - forMainFrameOnly: false, - in: .page) - }() - private let cookieControlUserScript: WKUserScript? = { guard let path = Bundle.main.path(forResource: "CookieControl", ofType: "js"), let source: String = try? String(contentsOfFile: path) else { log.error("Failed to load cookie control user script") @@ -393,10 +351,7 @@ class UserScriptManager { tab?.webView?.configuration.userContentController.do { $0.removeAllUserScripts() self.packedUserScripts.forEach($0.addUserScript) - - if isFingerprintingProtectionEnabled, let script = fingerprintingProtectionUserScript { - $0.addUserScript(script) - } + if isCookieBlockingEnabled, let script = cookieControlUserScript { $0.addUserScript(script) } @@ -433,8 +388,14 @@ class UserScriptManager { $0.addUserScript(script) } - if let domainUserScript = domainUserScript, let script = domainUserScript.script { - $0.addUserScript(script) + for userScriptType in userScriptTypes.sorted(by: { $0.order < $1.order }) { + do { + let script = try ScriptFactory.shared.makeScript(for: userScriptType) + $0.addUserScript(script) + } catch { + assertionFailure("Should never happen. The scripts are packed in the project and loading/modifying should always be possible.") + log.error(error) + } } } } diff --git a/Client/Frontend/UserContent/UserScripts/FarblingProtection.js b/Client/Frontend/UserContent/UserScripts/FarblingProtection.js new file mode 100644 index 00000000000..0fcb9be7737 --- /dev/null +++ b/Client/Frontend/UserContent/UserScripts/FarblingProtection.js @@ -0,0 +1,272 @@ +// Copyright 2022 The Brave Authors. All rights reserved. +// 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/. + +"use strict"; + +window.braveFarble = (args) => { + // 1. Farble audio + // Adds slight randization when reading data for audio files + // Randomization is determined by the fudge factor + const farbleAudio = (fudgeFactor) => { + const farbledChannels = new WeakMap() + const braveNacl = window.nacl + delete window.nacl + + const farbleArrayData = (destination) => { + // Let's fudge the data by our fudge factor. + for (const index in destination) { + destination[index] = destination[index] * fudgeFactor + } + } + + // Convert an unsinged byte (Uint8) to a hex character + // Unsigned bytes must be between 0 and 255 + const byteToHex = (unsignedByte) => { + // convert the possibly signed byte (-128 to 127) to an unsigned byte (0 to 255). + // if you know, that you only deal with unsigned bytes (Uint8Array), you can omit this line + // const unsignedByte = byte & 0xff + + // If the number can be represented with only 4 bits (0-15), + // the hexadecimal representation of this number is only one char (0-9, a-f). + if (unsignedByte < 16) { + return '0' + unsignedByte.toString(16) + } else { + return unsignedByte.toString(16) + } + } + + // Convert an array of unsigned bytes (Uint8Array) to a hex string. + // Each value in the array must be between 0 and 255, + // resulting in hex values between 0 to f (i.e. 0 to 15) + const toHexString = (unsignedBytes) => { + return Array.from(unsignedBytes) + .map(byte => byteToHex(byte)) + .join('') + } + + // Hash an array + const hashArray = (a) => { + const byteArray = new Uint8Array(a.buffer) + const hexArray = braveNacl.hash(byteArray) + return toHexString(hexArray) + } + + // 1. Farble `getChannelData` + // This will also result in a farbled `copyFromChannel` + const getChannelData = window.AudioBuffer.prototype.getChannelData + window.AudioBuffer.prototype.getChannelData = function () { + const channelData = Reflect.apply(getChannelData, this, arguments) + let hashes = farbledChannels.get(channelData) + + // First let's check if we already farbled this set + if (hashes !== undefined) { + // We had this data set farbled already. + // Lets see if it changed it's shape since then. + const hash = hashArray(channelData) + + if (hashes.has(hash)) { + // We already farbled this version of the channel data + // Let's not farble it again + return channelData + } + } else { + // If we dont have any hashes of farbled data at all for this + // channel, then we trivially haven't hashed this channel data yet. + hashes = new Set() + farbledChannels.set(channelData, hashes) + } + + // Farble the array and store the hash of this + // so that we don't farble the same data again. + farbleArrayData(channelData) + const hash = hashArray(channelData) + hashes.add(hash) + + return channelData + } + + // 2. Farble "destination" methods + const structuresToFarble = [ + [window.AnalyserNode, 'getFloatFrequencyData'], + [window.AnalyserNode, 'getByteFrequencyData'], + [window.AnalyserNode, 'getByteTimeDomainData'], + [window.AnalyserNode, 'getFloatTimeDomainData'] + ] + + for (const [structure, methodName] of structuresToFarble) { + const origImplementation = structure.prototype[methodName] + structure.prototype[methodName] = function () { + Reflect.apply(origImplementation, this, arguments) + farbleArrayData(arguments[0]) + } + } + } + + // 2. Farble plugin data + // Injects fake plugins with fake mime-types + // Random plugins are determined by the plugin data + const farblePlugins = (pluginData) => { + // Function that create a fake mime-type based on the given fake data + const makeFakeMimeType = (fakeData) => { + return Object.create(window.MimeType.prototype, { + suffixes: { value: fakeData.suffixes }, + type: { value: fakeData.type }, + description: { value: fakeData.description } + }) + } + + // Create a fake plugin given the plugin data + const makeFakePlugin = (pluginData) => { + const newPlugin = Object.create(window.Plugin.prototype, { + description: { value: pluginData.description }, + name: { value: pluginData.name }, + filename: { value: pluginData.filename }, + length: { value: pluginData.mimeTypes.length } + }) + + // Create mime-types and link them to the new plugin + for (const [index, mimeType] of pluginData.mimeTypes.entries()) { + const newMimeType = makeFakeMimeType(mimeType) + + newPlugin[index] = newMimeType + newPlugin[newMimeType.type] = newMimeType + + Reflect.defineProperty(newMimeType, 'enabledPlugin', { + value: newPlugin + }) + } + + // Patch `Plugin.item(index)` function to return the correct item otherwise it + // throws a `TypeError: Can only call Plugin.item on instances of Plugin` + newPlugin.item = function (index) { + return newPlugin[index] + } + + return newPlugin + } + + if (window.navigator.plugins !== undefined) { + // We need the original length so we can reference it (as we will change it) + const plugins = window.navigator.plugins + const originalPluginsLength = plugins.length + + // Adds a fake plugin for the given index on fakePluginData + const addPluginAtIndex = (newPlugin, index) => { + const pluginPosition = originalPluginsLength + index + window.navigator.plugins[pluginPosition] = newPlugin + window.navigator.plugins[newPlugin.name] = newPlugin + } + + for (const [index, pluginData] of fakePluginData.entries()) { + const newPlugin = makeFakePlugin(pluginData) + addPluginAtIndex(newPlugin, index) + } + + // Adjust the length of the original plugin array + Reflect.defineProperty(window.navigator.plugins, 'length', { + value: originalPluginsLength + fakePluginData.length + }) + + // Patch `PluginArray.item(index)` function to return the correct item + // otherwise it returns `undefined` + const originalItemFunction = plugins.item + window.PluginArray.prototype.item = function (index) { + if (index < originalPluginsLength) { + return Reflect.apply(originalItemFunction, plugins, arguments) + } else { + return plugins[index] + } + } + } + } + + // 3. Farble speech synthesizer + // Adds a vake voice determined by the fakeVoiceName and randomVoiceIndexScale. + const farbleVoices = (fakeVoiceName, randomVoiceIndexScale) => { + const makeFakeVoiceFromVoice = (voice) => { + const newVoice = Object.create(Object.getPrototypeOf(voice), { + name: { value: fakeVoiceName }, + voiceURI: { value: voice.voiceURI }, + lang: { value: voice.lang }, + localService: { value: voice.localService }, + default: { value: false } + }) + + return newVoice + } + + let originalVoice + let fakeVoice + let passedFakeVoice + + // We need to override the voice property to allow our fake voice to work + const descriptor = Reflect.getOwnPropertyDescriptor(SpeechSynthesisUtterance.prototype, 'voice') + Reflect.defineProperty(SpeechSynthesisUtterance.prototype, 'voice', { + get () { + if (!passedFakeVoice) { + // We didn't set a fake voice + return Reflect.apply(descriptor.get, this, arguments) + } else { + // We set a fake voice, return that instead + return passedFakeVoice + } + }, + set (passedVoice) { + if (passedVoice === fakeVoice && originalVoice !== undefined) { + // If we passed a fake voice, ignore it. We need to use the real voice + // The fake voice will not work. + passedFakeVoice = passedVoice + Reflect.apply(descriptor.set, this, [originalVoice]) + } else { + // Otherwise, if we set a real voice, use a real voice instead. + passedFakeVoice = undefined + Reflect.apply(descriptor.set, this, arguments) + } + } + }) + + // Patch get voices to return an extra fake voice + const getVoices = window.speechSynthesis.getVoices + const getVoicesPrototype = Object.getPrototypeOf(window.speechSynthesis) + getVoicesPrototype.getVoices = function () { + const voices = Reflect.apply(getVoices, this, arguments) + + if (fakeVoice === undefined) { + const randomVoiceIndex = Math.round(randomVoiceIndexScale * voices.length) + originalVoice = voices[randomVoiceIndex] + fakeVoice = makeFakeVoiceFromVoice(originalVoice) + + if (fakeVoice !== undefined) { + voices.push(fakeVoice) + } + } else { + voices.push(fakeVoice) + } + return voices + } + } + + // A value between 0.99 and 1 to fudge the audio data + // A value between 0.99 to 1 means the values in the destination will + // always be within the expected range of -1 and 1. + // This small decrease should not affect affect legitimite users of this api. + // But will affect fingerprinters by introducing a small random change. + const fudgeFactor = args['fudgeFactor'] + farbleAudio(fudgeFactor) + + // Fake data that is to be used to construct fake plugins + const fakePluginData = args['fakePluginData'] + farblePlugins(fakePluginData) + + // A value representing a fake voice name that will be used to add a fake voice + const fakeVoiceName = args['fakeVoiceName'] + // This value is used to get a random index between 0 and voices.length + // It's important to have a value between 0 - 1 in order to be within the + // array bounds + const randomVoiceIndexScale = args['randomVoiceIndexScale'] + farbleVoices(fakeVoiceName, randomVoiceIndexScale) +} + +// Invoke window.braveFarble then delete the function diff --git a/Client/Frontend/UserContent/UserScripts/FingerprintingProtection.js b/Client/Frontend/UserContent/UserScripts/FingerprintingProtection.js deleted file mode 100644 index 657d4719f7f..00000000000 --- a/Client/Frontend/UserContent/UserScripts/FingerprintingProtection.js +++ /dev/null @@ -1,42 +0,0 @@ -/* 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/. */ - -"use strict"; - -if (webkit.messageHandlers.$) { - (function () { - function sendMessage(msg) { - if (msg) { - webkit.messageHandlers.$.postMessage(msg); - } - } - - let hooks = [ - { - obj: window.CanvasRenderingContext2D.prototype, - methods: ['getImageData', 'getLineDash', 'measureText'] - }, - { - obj: window.WebGLRenderingContext.prototype, - methods: ['getSupportedExtensions', 'getParameter', 'getContextAttributes', 'getShaderPrecisionFormat', 'getExtension'] - }, - { - obj: window.AudioBuffer.prototype, - methods: ['copyFromChannel', 'getChannelData'] - }, - { - obj: window.AnalyserNode.prototype, - methods: ['getFloatFrequencyData', 'getByteFrequencyData', 'getFloatTimeDomainData', 'getByteTimeDomainData'] - } - ]; - - // Install Method Hooks - hooks.forEach(function (hook) { - hook.methods.forEach(function (method) { - hook.obj[method] = function () { - sendMessage({ obj: `${hook.obj}`, method: method }); - } - }); - }); - - })(); -} diff --git a/Client/Frontend/UserContent/UserScripts/nacl.min.js b/Client/Frontend/UserContent/UserScripts/nacl.min.js new file mode 100644 index 00000000000..65340cca9ce --- /dev/null +++ b/Client/Frontend/UserContent/UserScripts/nacl.min.js @@ -0,0 +1 @@ +!function(i){"use strict";var m=function(r,n){this.hi=0|r,this.lo=0|n},v=function(r){var n,e=new Float64Array(16);if(r)for(n=0;n>>32-n}function b(r,n){var e=255&r[n+3];return(e=(e=e<<8|255&r[n+2])<<8|255&r[n+1])<<8|255&r[n+0]}function B(r,n){var e=r[n]<<24|r[n+1]<<16|r[n+2]<<8|r[n+3],t=r[n+4]<<24|r[n+5]<<16|r[n+6]<<8|r[n+7];return new m(e,t)}function p(r,n,e){var t;for(t=0;t<4;t++)r[n+t]=255&e,e>>>=8}function S(r,n,e){r[n]=e.hi>>24&255,r[n+1]=e.hi>>16&255,r[n+2]=e.hi>>8&255,r[n+3]=255&e.hi,r[n+4]=e.lo>>24&255,r[n+5]=e.lo>>16&255,r[n+6]=e.lo>>8&255,r[n+7]=255&e.lo}function u(r,n,e,t,o){var i,a=0;for(i=0;i>>8)-1}function A(r,n,e,t){return u(r,n,e,t,16)}function _(r,n,e,t){return u(r,n,e,t,32)}function U(r,n,e,t,o){var i,a,f,u=new Uint32Array(16),c=new Uint32Array(16),w=new Uint32Array(16),y=new Uint32Array(4);for(i=0;i<4;i++)c[5*i]=b(t,4*i),c[1+i]=b(e,4*i),c[6+i]=b(n,4*i),c[11+i]=b(e,16+4*i);for(i=0;i<16;i++)w[i]=c[i];for(i=0;i<20;i++){for(a=0;a<4;a++){for(f=0;f<4;f++)y[f]=c[(5*a+4*f)%16];for(y[1]^=h(y[0]+y[3]|0,7),y[2]^=h(y[1]+y[0]|0,9),y[3]^=h(y[2]+y[1]|0,13),y[0]^=h(y[3]+y[2]|0,18),f=0;f<4;f++)u[4*a+(a+f)%4]=y[f]}for(f=0;f<16;f++)c[f]=u[f]}if(o){for(i=0;i<16;i++)c[i]=c[i]+w[i]|0;for(i=0;i<4;i++)c[5*i]=c[5*i]-b(t,4*i)|0,c[6+i]=c[6+i]-b(n,4*i)|0;for(i=0;i<4;i++)p(r,4*i,c[5*i]),p(r,16+4*i,c[6+i])}else for(i=0;i<16;i++)p(r,4*i,c[i]+w[i]|0)}function E(r,n,e,t){U(r,n,e,t,!1)}function x(r,n,e,t){return U(r,n,e,t,!0),0}var d=new Uint8Array([101,120,112,97,110,100,32,51,50,45,98,121,116,101,32,107]);function K(r,n,e,t,o,i,a){var f,u,c=new Uint8Array(16),w=new Uint8Array(64);if(!o)return 0;for(u=0;u<16;u++)c[u]=0;for(u=0;u<8;u++)c[u]=i[u];for(;64<=o;){for(E(w,c,a,d),u=0;u<64;u++)r[n+u]=(e?e[t+u]:0)^w[u];for(f=1,u=8;u<16;u++)f=f+(255&c[u])|0,c[u]=255&f,f>>>=8;o-=64,n+=64,e&&(t+=64)}if(0>>=8}var z=new Uint32Array([5,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,252]);function R(r,n,e,t,o,i){var a,f,u,c,w=new Uint32Array(17),y=new Uint32Array(17),l=new Uint32Array(17),s=new Uint32Array(17),h=new Uint32Array(17);for(u=0;u<17;u++)y[u]=l[u]=0;for(u=0;u<16;u++)y[u]=i[u];for(y[3]&=15,y[4]&=252,y[7]&=15,y[8]&=252,y[11]&=15,y[12]&=252,y[15]&=15;0>>=8;for(c=c+l[16]|0,l[16]=3&c,c=5*(c>>>2)|0,u=0;u<16;u++)c=c+l[u]|0,l[u]=255&c,c>>>=8;c=c+l[16]|0,l[16]=c}for(u=0;u<17;u++)h[u]=l[u];for(k(l,z),a=0|-(l[16]>>>7),u=0;u<17;u++)l[u]^=a&(h[u]^l[u]);for(u=0;u<16;u++)s[u]=i[u+16];for(s[16]=0,k(l,s),u=0;u<16;u++)r[n+u]=l[u];return 0}function P(r,n,e,t,o,i){var a=new Uint8Array(16);return R(a,0,e,t,o,i),A(r,n,a,0)}function M(r,n,e,t,o){var i;if(e<32)return-1;for(T(r,0,n,0,e,t,o),R(r,16,r,32,e-32,r),i=0;i<16;i++)r[i]=0;return 0}function N(r,n,e,t,o){var i,a=new Uint8Array(32);if(e<32)return-1;if(L(a,0,32,t,o),0!==P(n,16,n,32,e-32,a))return-1;for(T(r,0,n,0,e,t,o),i=0;i<32;i++)r[i]=0;return 0}function O(r,n){var e;for(e=0;e<16;e++)r[e]=0|n[e]}function C(r){var n,e;for(e=0;e<16;e++)r[e]+=65536,n=Math.floor(r[e]/65536),r[(e+1)*(e<15?1:0)]+=n-1+37*(n-1)*(15===e?1:0),r[e]-=65536*n}function F(r,n,e){for(var t,o=~(e-1),i=0;i<16;i++)t=o&(r[i]^n[i]),r[i]^=t,n[i]^=t}function Z(r,n){var e,t,o,i=v(),a=v();for(e=0;e<16;e++)a[e]=n[e];for(C(a),C(a),C(a),t=0;t<2;t++){for(i[0]=a[0]-65517,e=1;e<15;e++)i[e]=a[e]-65535-(i[e-1]>>16&1),i[e-1]&=65535;i[15]=a[15]-32767-(i[14]>>16&1),o=i[15]>>16&1,i[14]&=65535,F(a,i,1-o)}for(e=0;e<16;e++)r[2*e]=255&a[e],r[2*e+1]=a[e]>>8}function G(r,n){var e=new Uint8Array(32),t=new Uint8Array(32);return Z(e,r),Z(t,n),_(e,0,t,0)}function q(r){var n=new Uint8Array(32);return Z(n,r),1&n[0]}function D(r,n){var e;for(e=0;e<16;e++)r[e]=n[2*e]+(n[2*e+1]<<8);r[15]&=32767}function I(r,n,e){var t;for(t=0;t<16;t++)r[t]=n[t]+e[t]|0}function V(r,n,e){var t;for(t=0;t<16;t++)r[t]=n[t]-e[t]|0}function X(r,n,e){var t,o,i=new Float64Array(31);for(t=0;t<31;t++)i[t]=0;for(t=0;t<16;t++)for(o=0;o<16;o++)i[t+o]+=n[t]*e[o];for(t=0;t<15;t++)i[t]+=38*i[t+16];for(t=0;t<16;t++)r[t]=i[t];C(r),C(r)}function j(r,n){X(r,n,n)}function H(r,n){var e,t=v();for(e=0;e<16;e++)t[e]=n[e];for(e=253;0<=e;e--)j(t,t),2!==e&&4!==e&&X(t,t,n);for(e=0;e<16;e++)r[e]=t[e]}function J(r,n){var e,t=v();for(e=0;e<16;e++)t[e]=n[e];for(e=250;0<=e;e--)j(t,t),1!==e&&X(t,t,n);for(e=0;e<16;e++)r[e]=t[e]}function Q(r,n,e){var t,o,i=new Uint8Array(32),a=new Float64Array(80),f=v(),u=v(),c=v(),w=v(),y=v(),l=v();for(o=0;o<31;o++)i[o]=n[o];for(i[31]=127&n[31]|64,i[0]&=248,D(a,e),o=0;o<16;o++)u[o]=a[o],w[o]=f[o]=c[o]=0;for(f[0]=w[0]=1,o=254;0<=o;--o)F(f,u,t=i[o>>>3]>>>(7&o)&1),F(c,w,t),I(y,f,c),V(f,f,c),I(c,u,w),V(u,u,w),j(w,y),j(l,f),X(f,c,f),X(c,u,y),I(y,f,c),V(f,f,c),j(u,f),V(c,w,l),X(f,c,g),I(f,f,w),X(c,c,f),X(f,w,l),X(w,u,a),j(u,y),F(f,u,t),F(c,w,t);for(o=0;o<16;o++)a[o+16]=f[o],a[o+32]=c[o],a[o+48]=u[o],a[o+64]=w[o];var s=a.subarray(32),h=a.subarray(16);return H(s,s),X(h,h,s),Z(r,h),0}function W(r,n){return Q(r,n,e)}function $(r,n){return a(n,32),W(r,n)}function rr(r,n,e){var t=new Uint8Array(32);return Q(t,e,n),x(r,o,t,d)}var nr=M,er=N;function tr(){var r,n,e,t=0,o=0,i=0,a=0,f=65535;for(e=0;e>>16,i+=(n=arguments[e].hi)&f,a+=n>>>16;return new m((i+=(o+=t>>>16)>>>16)&f|(a+=i>>>16)<<16,t&f|o<<16)}function or(r,n){return new m(r.hi>>>n,r.lo>>>n|r.hi<<32-n)}function ir(){var r,n=0,e=0;for(r=0;r>>n|r.lo<>>n|r.hi<>>n|r.hi<>>n|r.lo<>(7&o)&1),yr(n,r),yr(r,r),lr(r,n,t)}function vr(r,n){var e=[v(),v(),v(),v()];O(e[0],t),O(e[1],f),O(e[2],w),X(e[3],t,f),hr(r,e,n)}function gr(r,n,e){var t,o=new Uint8Array(64),i=[v(),v(),v(),v()];for(e||a(n,32),wr(o,n,32),o[0]&=248,o[31]&=127,o[31]|=64,vr(i,o),sr(r,i),t=0;t<32;t++)n[t+32]=r[t];return 0}var br=new Float64Array([237,211,245,92,26,99,18,88,214,156,247,162,222,249,222,20,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,16]);function pr(r,n){var e,t,o,i;for(t=63;32<=t;--t){for(e=0,o=t-32,i=t-12;o>4)*br[o],e=n[o]>>8,n[o]&=255;for(o=0;o<32;o++)n[o]-=e*br[o];for(t=0;t<32;t++)n[t+1]+=n[t]>>8,r[t]=255&n[t]}function Ar(r){var n,e=new Float64Array(64);for(n=0;n<64;n++)e[n]=r[n];for(n=0;n<64;n++)r[n]=0;pr(r,e)}function _r(r,n,e,t){var o,i,a=new Uint8Array(64),f=new Uint8Array(64),u=new Uint8Array(64),c=new Float64Array(64),w=[v(),v(),v(),v()];wr(a,t,32),a[0]&=248,a[31]&=127,a[31]|=64;var y=e+64;for(o=0;o>7&&V(r[0],c,r[0]),X(r[3],r[0],r[1])}(u,t))return-1;for(o=0;o