diff options
27 files changed, 1565 insertions, 1241 deletions
diff --git a/ios/MullvadVPN.xcodeproj/project.pbxproj b/ios/MullvadVPN.xcodeproj/project.pbxproj index 8e5de87959..0e1af93ed2 100644 --- a/ios/MullvadVPN.xcodeproj/project.pbxproj +++ b/ios/MullvadVPN.xcodeproj/project.pbxproj @@ -37,6 +37,7 @@ 581503A424D6F1EC00C9C50E /* ChainedError+Logger.swift in Sources */ = {isa = PBXBuildFile; fileRef = 581503A224D6F1EC00C9C50E /* ChainedError+Logger.swift */; }; 581503A624D6F4AE00C9C50E /* Logging.swift in Sources */ = {isa = PBXBuildFile; fileRef = 581503A524D6F4AE00C9C50E /* Logging.swift */; }; 581503A724D6F4AE00C9C50E /* Logging.swift in Sources */ = {isa = PBXBuildFile; fileRef = 581503A524D6F4AE00C9C50E /* Logging.swift */; }; + 58161C9C28352F850028ECFD /* MigrateSettingsOperation.swift in Sources */ = {isa = PBXBuildFile; fileRef = 58161C9B28352F850028ECFD /* MigrateSettingsOperation.swift */; }; 5819C2142726CC8D00D6EC38 /* DataSourceSnapshotTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5819C2132726CC8D00D6EC38 /* DataSourceSnapshotTests.swift */; }; 5819C2152726CC9400D6EC38 /* DataSourceSnapshot.swift in Sources */ = {isa = PBXBuildFile; fileRef = 587EB66F27143B6500123C75 /* DataSourceSnapshot.swift */; }; 5819C2172729595500D6EC38 /* SettingsAddDNSEntryCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5819C2162729595500D6EC38 /* SettingsAddDNSEntryCell.swift */; }; @@ -78,6 +79,8 @@ 5840250522B11AB700E4CFEC /* MullvadEndpoint.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5840250322B11AB700E4CFEC /* MullvadEndpoint.swift */; }; 5840BE35279EDB16002836BA /* OperationCompletion.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5840BE34279EDB16002836BA /* OperationCompletion.swift */; }; 5842102E282D3FC200F24E46 /* ResultBlockOperation.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5842102D282D3FC200F24E46 /* ResultBlockOperation.swift */; }; + 58421030282D8A3C00F24E46 /* UpdateAccountDataOperation.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5842102F282D8A3C00F24E46 /* UpdateAccountDataOperation.swift */; }; + 58421032282E42B000F24E46 /* UpdateDeviceDataOperation.swift in Sources */ = {isa = PBXBuildFile; fileRef = 58421031282E42B000F24E46 /* UpdateDeviceDataOperation.swift */; }; 58421034282E4B1500F24E46 /* TunnelSettingsV2+REST.swift in Sources */ = {isa = PBXBuildFile; fileRef = 58421033282E4B1500F24E46 /* TunnelSettingsV2+REST.swift */; }; 584592612639B4A200EF967F /* ConsentContentView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 584592602639B4A200EF967F /* ConsentContentView.swift */; }; 5846226526E0D9630035F7C2 /* ProductsRequestOperation.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5846226426E0D9630035F7C2 /* ProductsRequestOperation.swift */; }; @@ -160,7 +163,6 @@ 58781CD522AFBA39009B9D8E /* RelaySelector.swift in Sources */ = {isa = PBXBuildFile; fileRef = 58781CD422AFBA39009B9D8E /* RelaySelector.swift */; }; 5878BA1426DD0B01004147D7 /* OSLogHandler.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5823FA4F26CA690600283BF8 /* OSLogHandler.swift */; }; 587A01FC23F1F0BE00B68763 /* SimulatorTunnelProviderHost.swift in Sources */ = {isa = PBXBuildFile; fileRef = 587A01FB23F1F0BE00B68763 /* SimulatorTunnelProviderHost.swift */; }; - 587AD7CA2342283900E93A53 /* Account.swift in Sources */ = {isa = PBXBuildFile; fileRef = 587AD7C92342283900E93A53 /* Account.swift */; }; 587AD7C623421D7000E93A53 /* TunnelSettingsV1.swift in Sources */ = {isa = PBXBuildFile; fileRef = 587AD7C523421D7000E93A53 /* TunnelSettingsV1.swift */; }; 587AD7C723421D8600E93A53 /* TunnelSettingsV1.swift in Sources */ = {isa = PBXBuildFile; fileRef = 587AD7C523421D7000E93A53 /* TunnelSettingsV1.swift */; }; 587B7536266528A200DEF7E9 /* NotificationManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 587B7535266528A200DEF7E9 /* NotificationManager.swift */; }; @@ -176,9 +178,8 @@ 587EB672271451E300123C75 /* PreferencesViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 587EB671271451E300123C75 /* PreferencesViewModel.swift */; }; 587EB6742714520600123C75 /* PreferencesDataSourceDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 587EB6732714520600123C75 /* PreferencesDataSourceDelegate.swift */; }; 5883A09E266A5AF7003EFFCB /* Localizable.strings in Resources */ = {isa = PBXBuildFile; fileRef = 587B7543266922BF00DEF7E9 /* Localizable.strings */; }; - 588527B2276B3F0700BAA373 /* LoadTunnelOperation.swift in Sources */ = {isa = PBXBuildFile; fileRef = 588527B1276B3F0700BAA373 /* LoadTunnelOperation.swift */; }; + 588527B2276B3F0700BAA373 /* LoadTunnelConfigurationOperation.swift in Sources */ = {isa = PBXBuildFile; fileRef = 588527B1276B3F0700BAA373 /* LoadTunnelConfigurationOperation.swift */; }; 588527B4276B4F2F00BAA373 /* SetAccountOperation.swift in Sources */ = {isa = PBXBuildFile; fileRef = 588527B3276B4F2F00BAA373 /* SetAccountOperation.swift */; }; - 588527B6276B58B300BAA373 /* SetTunnelSettingsOperation.swift in Sources */ = {isa = PBXBuildFile; fileRef = 588527B5276B58B300BAA373 /* SetTunnelSettingsOperation.swift */; }; 58871D1E25D535A3002297FA /* WireGuardKit in Frameworks */ = {isa = PBXBuildFile; productRef = 58871D1D25D535A3002297FA /* WireGuardKit */; }; 58871D2325D535D2002297FA /* IPAddressRange+Codable.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5850366725A47AC700A43E93 /* IPAddressRange+Codable.swift */; }; 5888AD83227B11080051EB06 /* SelectLocationCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5888AD82227B11080051EB06 /* SelectLocationCell.swift */; }; @@ -255,7 +256,7 @@ 58F2E144276A13F300A79513 /* StartTunnelOperation.swift in Sources */ = {isa = PBXBuildFile; fileRef = 58F2E143276A13F300A79513 /* StartTunnelOperation.swift */; }; 58F2E146276A2C9900A79513 /* StopTunnelOperation.swift in Sources */ = {isa = PBXBuildFile; fileRef = 58F2E145276A2C9900A79513 /* StopTunnelOperation.swift */; }; 58F2E148276A307400A79513 /* MapConnectionStatusOperation.swift in Sources */ = {isa = PBXBuildFile; fileRef = 58F2E147276A307400A79513 /* MapConnectionStatusOperation.swift */; }; - 58F2E14C276A61C000A79513 /* ReplaceKeyOperation.swift in Sources */ = {isa = PBXBuildFile; fileRef = 58F2E14B276A61C000A79513 /* ReplaceKeyOperation.swift */; }; + 58F2E14C276A61C000A79513 /* RotateKeyOperation.swift in Sources */ = {isa = PBXBuildFile; fileRef = 58F2E14B276A61C000A79513 /* RotateKeyOperation.swift */; }; 58F3C0A4249CB069003E76BE /* HeaderBarView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 58F3C0A3249CB069003E76BE /* HeaderBarView.swift */; }; 58F3C0A624A50157003E76BE /* relays.json in Resources */ = {isa = PBXBuildFile; fileRef = 58F3C0A524A50155003E76BE /* relays.json */; }; 58F3C0A724A50C02003E76BE /* relays.json in Resources */ = {isa = PBXBuildFile; fileRef = 58F3C0A524A50155003E76BE /* relays.json */; }; @@ -288,7 +289,6 @@ 58FB865A26EA214400F188BC /* RelayCacheObserver.swift in Sources */ = {isa = PBXBuildFile; fileRef = 58FB865926EA214400F188BC /* RelayCacheObserver.swift */; }; 58FB865E26EA284E00F188BC /* LogFormatting.swift in Sources */ = {isa = PBXBuildFile; fileRef = 58FB865D26EA284E00F188BC /* LogFormatting.swift */; }; 58FB865F26EA2E6D00F188BC /* LogFormatting.swift in Sources */ = {isa = PBXBuildFile; fileRef = 58FB865D26EA284E00F188BC /* LogFormatting.swift */; }; - 58FB866126EB678000F188BC /* TunnelInfo.swift in Sources */ = {isa = PBXBuildFile; fileRef = 58FB866026EB677F00F188BC /* TunnelInfo.swift */; }; 58FC040A27B3EE03001C21F0 /* TunnelMonitor.swift in Sources */ = {isa = PBXBuildFile; fileRef = 58FC040927B3EE03001C21F0 /* TunnelMonitor.swift */; }; 58FD5BE724192A2C00112C88 /* AppStoreReceipt.swift in Sources */ = {isa = PBXBuildFile; fileRef = 58FD5BE624192A2B00112C88 /* AppStoreReceipt.swift */; }; 58FD5BF024238EB300112C88 /* SKProduct+Formatting.swift in Sources */ = {isa = PBXBuildFile; fileRef = 58FD5BEF24238EB300112C88 /* SKProduct+Formatting.swift */; }; @@ -366,6 +366,7 @@ 5815039C24D6ECE600C9C50E /* TextFileOutputStream.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TextFileOutputStream.swift; sourceTree = "<group>"; }; 581503A224D6F1EC00C9C50E /* ChainedError+Logger.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "ChainedError+Logger.swift"; sourceTree = "<group>"; }; 581503A524D6F4AE00C9C50E /* Logging.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Logging.swift; sourceTree = "<group>"; }; + 58161C9B28352F850028ECFD /* MigrateSettingsOperation.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MigrateSettingsOperation.swift; sourceTree = "<group>"; }; 5819C2132726CC8D00D6EC38 /* DataSourceSnapshotTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DataSourceSnapshotTests.swift; sourceTree = "<group>"; }; 5819C2162729595500D6EC38 /* SettingsAddDNSEntryCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SettingsAddDNSEntryCell.swift; sourceTree = "<group>"; }; 581FC4F92695ACE100AA97BA /* en */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = en; path = en.lproj/Account.strings; sourceTree = "<group>"; }; @@ -396,6 +397,8 @@ 5840250322B11AB700E4CFEC /* MullvadEndpoint.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MullvadEndpoint.swift; sourceTree = "<group>"; }; 5840BE34279EDB16002836BA /* OperationCompletion.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OperationCompletion.swift; sourceTree = "<group>"; }; 5842102D282D3FC200F24E46 /* ResultBlockOperation.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ResultBlockOperation.swift; sourceTree = "<group>"; }; + 5842102F282D8A3C00F24E46 /* UpdateAccountDataOperation.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UpdateAccountDataOperation.swift; sourceTree = "<group>"; }; + 58421031282E42B000F24E46 /* UpdateDeviceDataOperation.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UpdateDeviceDataOperation.swift; sourceTree = "<group>"; }; 58421033282E4B1500F24E46 /* TunnelSettingsV2+REST.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "TunnelSettingsV2+REST.swift"; sourceTree = "<group>"; }; 584592602639B4A200EF967F /* ConsentContentView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ConsentContentView.swift; sourceTree = "<group>"; }; 5845F841236CBACD00B2D93C /* TunnelIPC.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TunnelIPC.swift; sourceTree = "<group>"; }; @@ -452,7 +455,6 @@ 58781CC822AE7CA8009B9D8E /* RelayConstraints.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RelayConstraints.swift; sourceTree = "<group>"; }; 58781CD422AFBA39009B9D8E /* RelaySelector.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RelaySelector.swift; sourceTree = "<group>"; }; 587A01FB23F1F0BE00B68763 /* SimulatorTunnelProviderHost.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SimulatorTunnelProviderHost.swift; sourceTree = "<group>"; }; - 587AD7C92342283900E93A53 /* Account.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Account.swift; sourceTree = "<group>"; }; 587AD7C523421D7000E93A53 /* TunnelSettingsV1.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = TunnelSettingsV1.swift; sourceTree = "<group>"; }; 587B7535266528A200DEF7E9 /* NotificationManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NotificationManager.swift; sourceTree = "<group>"; }; 587B753A2666467500DEF7E9 /* NotificationBannerView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NotificationBannerView.swift; sourceTree = "<group>"; }; @@ -466,9 +468,8 @@ 587EB66F27143B6500123C75 /* DataSourceSnapshot.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DataSourceSnapshot.swift; sourceTree = "<group>"; }; 587EB671271451E300123C75 /* PreferencesViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PreferencesViewModel.swift; sourceTree = "<group>"; }; 587EB6732714520600123C75 /* PreferencesDataSourceDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PreferencesDataSourceDelegate.swift; sourceTree = "<group>"; }; - 588527B1276B3F0700BAA373 /* LoadTunnelOperation.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LoadTunnelOperation.swift; sourceTree = "<group>"; }; + 588527B1276B3F0700BAA373 /* LoadTunnelConfigurationOperation.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LoadTunnelConfigurationOperation.swift; sourceTree = "<group>"; }; 588527B3276B4F2F00BAA373 /* SetAccountOperation.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SetAccountOperation.swift; sourceTree = "<group>"; }; - 588527B5276B58B300BAA373 /* SetTunnelSettingsOperation.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SetTunnelSettingsOperation.swift; sourceTree = "<group>"; }; 5888AD82227B11080051EB06 /* SelectLocationCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SelectLocationCell.swift; sourceTree = "<group>"; }; 5888AD86227B17950051EB06 /* SelectLocationViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SelectLocationViewController.swift; sourceTree = "<group>"; }; 588BCF23280FE43D009ADCEC /* RESTDevicesProxy.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RESTDevicesProxy.swift; sourceTree = "<group>"; }; @@ -541,7 +542,7 @@ 58F2E143276A13F300A79513 /* StartTunnelOperation.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StartTunnelOperation.swift; sourceTree = "<group>"; }; 58F2E145276A2C9900A79513 /* StopTunnelOperation.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StopTunnelOperation.swift; sourceTree = "<group>"; }; 58F2E147276A307400A79513 /* MapConnectionStatusOperation.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MapConnectionStatusOperation.swift; sourceTree = "<group>"; }; - 58F2E14B276A61C000A79513 /* ReplaceKeyOperation.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ReplaceKeyOperation.swift; sourceTree = "<group>"; }; + 58F2E14B276A61C000A79513 /* RotateKeyOperation.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RotateKeyOperation.swift; sourceTree = "<group>"; }; 58F3C0A3249CB069003E76BE /* HeaderBarView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HeaderBarView.swift; sourceTree = "<group>"; }; 58F3C0A524A50155003E76BE /* relays.json */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.json; path = relays.json; sourceTree = "<group>"; }; 58F558DC2695B85E00F630D0 /* en */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = en; path = en.lproj/Consent.strings; sourceTree = "<group>"; }; @@ -570,7 +571,6 @@ 58FB865426E8BF3100F188BC /* AppStorePaymentManagerError.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppStorePaymentManagerError.swift; sourceTree = "<group>"; }; 58FB865926EA214400F188BC /* RelayCacheObserver.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RelayCacheObserver.swift; sourceTree = "<group>"; }; 58FB865D26EA284E00F188BC /* LogFormatting.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LogFormatting.swift; sourceTree = "<group>"; }; - 58FB866026EB677F00F188BC /* TunnelInfo.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TunnelInfo.swift; sourceTree = "<group>"; }; 58FC040927B3EE03001C21F0 /* TunnelMonitor.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TunnelMonitor.swift; sourceTree = "<group>"; }; 58FD5BE624192A2B00112C88 /* AppStoreReceipt.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppStoreReceipt.swift; sourceTree = "<group>"; }; 58FD5BEF24238EB300112C88 /* SKProduct+Formatting.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "SKProduct+Formatting.swift"; sourceTree = "<group>"; }; @@ -683,20 +683,21 @@ isa = PBXGroup; children = ( 587C575226D2615F005EF767 /* PacketTunnelOptions.swift */, - 58FB866026EB677F00F188BC /* TunnelInfo.swift */, 5835B7CB233B76CB0096D79F /* TunnelManager.swift */, 5801C9A427A14B2A0031566A /* TunnelManagerState.swift */, 5820676326E771DB00655B05 /* TunnelManagerError.swift */, 5823FA5326CE49F600283BF8 /* TunnelObserver.swift */, 58B93A1226C3F13600A55733 /* TunnelState.swift */, - 588527B1276B3F0700BAA373 /* LoadTunnelOperation.swift */, + 588527B1276B3F0700BAA373 /* LoadTunnelConfigurationOperation.swift */, 584B17AA27637DE40057F3B8 /* ReloadTunnelOperation.swift */, 588527B3276B4F2F00BAA373 /* SetAccountOperation.swift */, 58F2E143276A13F300A79513 /* StartTunnelOperation.swift */, 58F2E145276A2C9900A79513 /* StopTunnelOperation.swift */, 58F2E147276A307400A79513 /* MapConnectionStatusOperation.swift */, - 58F2E14B276A61C000A79513 /* ReplaceKeyOperation.swift */, - 588527B5276B58B300BAA373 /* SetTunnelSettingsOperation.swift */, + 58F2E14B276A61C000A79513 /* RotateKeyOperation.swift */, + 5842102F282D8A3C00F24E46 /* UpdateAccountDataOperation.swift */, + 58421031282E42B000F24E46 /* UpdateDeviceDataOperation.swift */, + 58161C9B28352F850028ECFD /* MigrateSettingsOperation.swift */, ); path = TunnelManager; sourceTree = "<group>"; @@ -859,7 +860,6 @@ 58CE5E62224146200008646E /* MullvadVPN */ = { isa = PBXGroup; children = ( - 587AD7C92342283900E93A53 /* Account.swift */, 5896CEF126972DEB00B0FAE8 /* AccountContentView.swift */, 58C3A4B122456F1A00340BDB /* AccountInputGroupView.swift */, 58CCA01D2242787B004F3011 /* AccountTextField.swift */, @@ -1302,6 +1302,7 @@ 5842102E282D3FC200F24E46 /* ResultBlockOperation.swift in Sources */, 5857F24724C882D700CF6F47 /* SelectLocationNavigationController.swift in Sources */, 5846227126E229F20035F7C2 /* AppStoreSubscription.swift in Sources */, + 58421030282D8A3C00F24E46 /* UpdateAccountDataOperation.swift in Sources */, 5820675B26E6576800655B05 /* RelayCache.swift in Sources */, 5846226526E0D9630035F7C2 /* ProductsRequestOperation.swift in Sources */, 58FF2C03281BDE02009EF542 /* SettingsManager.swift in Sources */, @@ -1320,7 +1321,7 @@ 58E6771F24ADFE7800AA26E7 /* SettingsNavigationController.swift in Sources */, 58A1AA8C23F5584C009F7EA6 /* ConnectionPanelView.swift in Sources */, 582BB1B3229574F40055B6EF /* SettingsAccountCell.swift in Sources */, - 588527B2276B3F0700BAA373 /* LoadTunnelOperation.swift in Sources */, + 588527B2276B3F0700BAA373 /* LoadTunnelConfigurationOperation.swift in Sources */, 58F1311527E0B2AB007AC5BC /* Result+Extensions.swift in Sources */, 585DA88426B0270700B8C587 /* ServerRelaysResponse.swift in Sources */, 5875960726F36B3A00BF6711 /* TunnelIPCError.swift in Sources */, @@ -1332,6 +1333,7 @@ 582BB1B1229569620055B6EF /* CustomNavigationBar.swift in Sources */, 58B3F30F2742708B00A2DD38 /* HeaderBarButton.swift in Sources */, 584789E026529D72000E45FB /* SSLPinningURLSessionDelegate.swift in Sources */, + 58161C9C28352F850028ECFD /* MigrateSettingsOperation.swift in Sources */, 58ACF6492655365700ACE4B7 /* PreferencesViewController.swift in Sources */, 588D2FE3248AC27F00E313F7 /* AsyncOperation.swift in Sources */, 5820675026E6514100655B05 /* HTTP.swift in Sources */, @@ -1343,7 +1345,7 @@ 58ACF64D26567A5000ACE4B7 /* CustomSwitch.swift in Sources */, 58655DCE27DA0A5D00911834 /* TunnelMonitorConfiguration.swift in Sources */, 5850367F25A481D800A43E93 /* IPAddressRange+Codable.swift in Sources */, - 58F2E14C276A61C000A79513 /* ReplaceKeyOperation.swift in Sources */, + 58F2E14C276A61C000A79513 /* RotateKeyOperation.swift in Sources */, 5871FB96254ADE4E0051A0A4 /* ConsolidatedApplicationLog.swift in Sources */, 58FEEB58260B662E00A621A8 /* AutomaticKeyboardResponder.swift in Sources */, 5846227326E22A160035F7C2 /* AppStorePaymentObserver.swift in Sources */, @@ -1415,7 +1417,6 @@ 58EF580B25D69D7A00AEBA94 /* ProblemReportSubmissionOverlayView.swift in Sources */, 58FD5BE724192A2C00112C88 /* AppStoreReceipt.swift in Sources */, 5892A45E265FABFF00890742 /* EmptyTableViewHeaderFooterView.swift in Sources */, - 58FB866126EB678000F188BC /* TunnelInfo.swift in Sources */, 5835B7CC233B76CB0096D79F /* TunnelManager.swift in Sources */, 58B93A1326C3F13600A55733 /* TunnelState.swift in Sources */, 58FEEB46260A028D00A621A8 /* GeoJSON.swift in Sources */, @@ -1429,6 +1430,7 @@ 5857F24324C8662600CF6F47 /* SelectLocationHeaderView.swift in Sources */, 5840BE35279EDB16002836BA /* OperationCompletion.swift in Sources */, 58EE2E3A272FF814003BFF93 /* SettingsDataSource.swift in Sources */, + 58421032282E42B000F24E46 /* UpdateDeviceDataOperation.swift in Sources */, 58AEEF652344A36000C9BBD5 /* KeychainError.swift in Sources */, 581503A624D6F4AE00C9C50E /* Logging.swift in Sources */, 58CCA01222424D11004F3011 /* SettingsViewController.swift in Sources */, @@ -1436,7 +1438,6 @@ 58FB865526E8BF3100F188BC /* AppStorePaymentManagerError.swift in Sources */, 58FD5BF42428C67600112C88 /* InAppPurchaseButton.swift in Sources */, 580EE22424B3243100F9D8A1 /* AsyncBlockOperation.swift in Sources */, - 588527B6276B58B300BAA373 /* SetTunnelSettingsOperation.swift in Sources */, 58293FAE2510CA58005D0BB5 /* ProblemReportViewController.swift in Sources */, 58B9EB152489139B00095626 /* DisplayChainedError.swift in Sources */, 587B753F2668E5A700DEF7E9 /* NotificationContainerView.swift in Sources */, @@ -1451,7 +1452,6 @@ 584EBDBD2747C98F00A0C9FD /* NSAttributedString+Markdown.swift in Sources */, 5875960A26F371FC00BF6711 /* TunnelIPCSession.swift in Sources */, 58FB865E26EA284E00F188BC /* LogFormatting.swift in Sources */, - 587AD7CA2342283900E93A53 /* Account.swift in Sources */, 585DA88726B0277200B8C587 /* RESTError.swift in Sources */, 58293FB725138B88005D0BB5 /* CustomNavigationController.swift in Sources */, 587425C12299833500CA2045 /* RootContainerViewController.swift in Sources */, @@ -2131,8 +2131,8 @@ isa = XCRemoteSwiftPackageReference; repositoryURL = "https://github.com/mullvad/wireguard-apple.git"; requirement = { - branch = "mullvad-master"; - kind = branch; + kind = revision; + revision = eeb980058f5bff593868e34b718b4519a3236dcc; }; }; /* End XCRemoteSwiftPackageReference section */ diff --git a/ios/MullvadVPN/Account.swift b/ios/MullvadVPN/Account.swift deleted file mode 100644 index c3ba572a5b..0000000000 --- a/ios/MullvadVPN/Account.swift +++ /dev/null @@ -1,292 +0,0 @@ -// -// Account.swift -// MullvadVPN -// -// Created by pronebird on 16/05/2019. -// Copyright © 2019 Mullvad VPN AB. All rights reserved. -// - -import Foundation -import StoreKit -import Logging - -/// A enum holding the `UserDefaults` string keys -private enum UserDefaultsKeys: String { - case isAgreedToTermsOfService = "isAgreedToTermsOfService" - case accountToken = "accountToken" - case accountExpiry = "accountExpiry" -} - -protocol AccountObserver: AnyObject { - func account(_ account: Account, didUpdateExpiry expiry: Date) - func account(_ account: Account, didLoginWithToken token: String, expiry: Date) - func accountDidLogout(_ account: Account) -} - -/// A class that groups the account related operations -class Account { - - enum Error: ChainedError { - /// A failure to create the new account token - case createAccount(REST.Error) - - /// A failure to verify the account token - case verifyAccount(REST.Error) - - /// A failure to configure a tunnel - case tunnelConfiguration(TunnelManager.Error) - - var errorDescription: String? { - switch self { - case .createAccount: - return "Failure to create new account." - case .verifyAccount: - return "Failure to verify account." - case .tunnelConfiguration: - return "Failure to configure the tunnel." - } - } - } - - /// A shared instance of `Account` - static let shared = Account() - - private let logger = Logger(label: "Account") - private var observerList = ObserverList<AccountObserver>() - - private let operationQueue: OperationQueue = { - let operationQueue = OperationQueue() - operationQueue.maxConcurrentOperationCount = 1 - return operationQueue - }() - - private let apiProxy = REST.ProxyFactory.shared.createAPIProxy() - - /// Returns true if user agreed to terms of service, otherwise false - var isAgreedToTermsOfService: Bool { - return UserDefaults.standard.bool(forKey: UserDefaultsKeys.isAgreedToTermsOfService.rawValue) - } - - /// Returns the currently used account token - private(set) var token: String? { - set { - UserDefaults.standard.set(newValue, forKey: UserDefaultsKeys.accountToken.rawValue) - } - get { - return UserDefaults.standard.string(forKey: UserDefaultsKeys.accountToken.rawValue) - } - } - - var formattedToken: String? { - return token?.split(every: 4).joined(separator: " ") - } - - /// Returns the account expiry for the currently used account token - private(set) var expiry: Date? { - set { - UserDefaults.standard.set(newValue, forKey: UserDefaultsKeys.accountExpiry.rawValue) - } - get { - return UserDefaults.standard.object(forKey: UserDefaultsKeys.accountExpiry.rawValue) as? Date - } - } - - var isLoggedIn: Bool { - return token != nil - } - - /// Save the boolean flag in preferences indicating that the user agreed to terms of service. - func agreeToTermsOfService() { - UserDefaults.standard.set(true, forKey: UserDefaultsKeys.isAgreedToTermsOfService.rawValue) - } - - func loginWithNewAccount(completionHandler: @escaping (Result<REST.AccountResponse, Account.Error>) -> Void) { - let operation = AsyncBlockOperation(dispatchQueue: .main) { operation in - _ = self.apiProxy.createAccount(retryStrategy: .noRetry) { result in - switch result { - case .success(let response): - self.setupTunnel(accountToken: response.token, expiry: response.expires) { error in - if let error = error { - completionHandler(.failure(error)) - } else { - self.observerList.forEach { observer in - observer.account(self, didLoginWithToken: response.token, expiry: response.expires) - } - - completionHandler(.success(response)) - } - - operation.finish() - } - - case .failure(let error): - completionHandler(.failure(.createAccount(error))) - - operation.finish() - - case .cancelled: - operation.finish() - } - } - } - - operationQueue.addOperation(operation) - } - - /// Perform the login and save the account token along with expiry (if available) to the - /// application preferences. - func login(accountToken: String, completionHandler: @escaping (Result<REST.AccountResponse, Account.Error>) -> Void) { - let operation = AsyncBlockOperation(dispatchQueue: .main) { operation in - _ = self.apiProxy.getAccountExpiry(accountNumber: accountToken, retryStrategy: .default) { result in - switch result { - case .success(let response): - self.setupTunnel(accountToken: response.token, expiry: response.expires) { error in - if let error = error { - completionHandler(.failure(error)) - } else { - self.observerList.forEach { observer in - observer.account(self, didLoginWithToken: response.token, expiry: response.expires) - } - completionHandler(.success(response)) - } - operation.finish() - } - - case .failure(let error): - completionHandler(.failure(.verifyAccount(error))) - operation.finish() - - case .cancelled: - operation.finish() - } - } - } - - operationQueue.addOperation(operation) - } - - /// Perform the logout by erasing the account token and expiry from the application preferences. - func logout(completionHandler: @escaping () -> Void) { - let operation = AsyncBlockOperation(dispatchQueue: .main) { operation in - TunnelManager.shared.unsetAccount { - self.removeFromPreferences() - self.observerList.forEach { (observer) in - observer.accountDidLogout(self) - } - - completionHandler() - operation.finish() - } - } - - operationQueue.addOperation(operation) - } - - /// Forget that user was logged in, but do not attempt to unset account in `TunnelManager`. - /// This function is used in cases where the tunnel or tunnel settings are corrupt. - func forget(completionHandler: @escaping () -> Void) { - let operation = AsyncBlockOperation(dispatchQueue: .main) { operation in - self.removeFromPreferences() - self.observerList.forEach { (observer) in - observer.accountDidLogout(self) - } - - completionHandler() - operation.finish() - } - - operationQueue.addOperation(operation) - } - - func updateAccountExpiry() { - let operation = AsyncBlockOperation(dispatchQueue: .main) { operation in - guard let token = self.token else { - operation.finish() - return - } - - _ = self.apiProxy.getAccountExpiry(accountNumber: token, retryStrategy: .default) { completion in - switch completion { - case .success(let response): - if self.expiry != response.expires { - self.expiry = response.expires - self.observerList.forEach { (observer) in - observer.account(self, didUpdateExpiry: response.expires) - } - } - - case .failure(let error): - self.logger.error(chainedError: error, message: "Failed to update account expiry.") - - case .cancelled: - self.logger.debug("Account expiry update was cancelled.") - } - - operation.finish() - } - } - - operationQueue.addOperation(operation) - } - - private func setupTunnel(accountToken: String, expiry: Date, completionHandler: @escaping (Account.Error?) -> Void) { - TunnelManager.shared.setAccount(accountToken: accountToken) { error in - dispatchPrecondition(condition: .onQueue(.main)) - - if let error = error { - completionHandler(.tunnelConfiguration(error)) - } else { - self.token = accountToken - self.expiry = expiry - - completionHandler(nil) - } - } - } - - private func removeFromPreferences() { - let preferences = UserDefaults.standard - - preferences.removeObject(forKey: UserDefaultsKeys.accountToken.rawValue) - preferences.removeObject(forKey: UserDefaultsKeys.accountExpiry.rawValue) - } - - // MARK: - Account observation - - func addObserver(_ observer: AccountObserver) { - observerList.append(observer) - } - - func removeObserver(_ observer: AccountObserver) { - observerList.remove(observer) - } -} - -extension Account: AppStorePaymentObserver { - - func startPaymentMonitoring(with paymentManager: AppStorePaymentManager) { - paymentManager.addPaymentObserver(self) - } - - func appStorePaymentManager(_ manager: AppStorePaymentManager, transaction: SKPaymentTransaction?, payment: SKPayment, accountToken: String?, didFailWithError error: AppStorePaymentManager.Error) { - // no-op - } - - func appStorePaymentManager(_ manager: AppStorePaymentManager, transaction: SKPaymentTransaction, accountToken: String, didFinishWithResponse response: REST.CreateApplePaymentResponse) { - let operation = AsyncBlockOperation(dispatchQueue: .main) { operation in - let newExpiry = response.newExpiry - - // Make sure that payment corresponds to the active account token - if self.token == accountToken, self.expiry != newExpiry { - self.expiry = newExpiry - self.observerList.forEach { (observer) in - observer.account(self, didUpdateExpiry: newExpiry) - } - } - - operation.finish() - } - - operationQueue.addOperation(operation) - } -} diff --git a/ios/MullvadVPN/AccountViewController.swift b/ios/MullvadVPN/AccountViewController.swift index 88a34167f5..164985e316 100644 --- a/ios/MullvadVPN/AccountViewController.swift +++ b/ios/MullvadVPN/AccountViewController.swift @@ -14,8 +14,7 @@ protocol AccountViewControllerDelegate: AnyObject { func accountViewControllerDidLogout(_ controller: AccountViewController) } -class AccountViewController: UIViewController, AppStorePaymentObserver, AccountObserver { - +class AccountViewController: UIViewController, AppStorePaymentObserver, TunnelObserver { private let contentView: AccountContentView = { let contentView = AccountContentView() contentView.translatesAutoresizingMaskIntoConstraints = false @@ -84,7 +83,9 @@ class AccountViewController: UIViewController, AppStorePaymentObserver, AccountO comment: "Navigation title" ) - contentView.accountTokenRowView.value = Account.shared.formattedToken + contentView.accountTokenRowView.value = TunnelManager.shared.accountNumber.map { string in + return formatAccountNumber(string) + } contentView.accountTokenRowView.actionHandler = { [weak self] in self?.copyAccountToken() } @@ -94,9 +95,9 @@ class AccountViewController: UIViewController, AppStorePaymentObserver, AccountO contentView.logoutButton.addTarget(self, action: #selector(doLogout), for: .touchUpInside) AppStorePaymentManager.shared.addPaymentObserver(self) - Account.shared.addObserver(self) + TunnelManager.shared.addObserver(self) - updateAccountExpiry(expiryDate: Account.shared.expiry) + updateAccountExpiry(expiryDate: TunnelManager.shared.accountExpiry) // Make sure to disable IAPs when payments are restricted if AppStorePaymentManager.canMakePayments { @@ -293,7 +294,7 @@ class AccountViewController: UIViewController, AppStorePaymentObserver, AccountO ) alertPresenter.enqueue(alertController, presentingController: self) { - Account.shared.logout { + TunnelManager.shared.unsetAccount { DispatchQueue.main.asyncAfter(deadline: .now() + .seconds(1)) { alertController.dismiss(animated: true) { self.delegate?.accountViewControllerDidLogout(self) @@ -303,18 +304,18 @@ class AccountViewController: UIViewController, AppStorePaymentObserver, AccountO } } - // MARK: - AccountObserver + // MARK: - TunnelObserver - func account(_ account: Account, didUpdateExpiry expiry: Date) { - updateAccountExpiry(expiryDate: expiry) + func tunnelManager(_ manager: TunnelManager, didUpdateTunnelState tunnelState: TunnelState) { + // no-op } - func account(_ account: Account, didLoginWithToken token: String, expiry: Date) { + func tunnelManager(_ manager: TunnelManager, didFailWithError error: TunnelManager.Error) { // no-op } - func accountDidLogout(_ account: Account) { - // no-op + func tunnelManager(_ manager: TunnelManager, didUpdateTunnelSettings tunnelSettings: TunnelSettingsV2?) { + updateAccountExpiry(expiryDate: tunnelSettings?.account.expiry) } // MARK: - AppStorePaymentObserver @@ -351,7 +352,7 @@ class AccountViewController: UIViewController, AppStorePaymentObserver, AccountO func appStorePaymentManager(_ manager: AppStorePaymentManager, transaction: SKPaymentTransaction, accountToken: String, didFinishWithResponse response: REST.CreateApplePaymentResponse) { showTimeAddedConfirmationAlert(with: response, context: .purchase) - if transaction.payment == self.pendingPayment { + if transaction.payment == pendingPayment { compoundInteractionRestriction.decrease(animated: true) } } @@ -368,7 +369,7 @@ class AccountViewController: UIViewController, AppStorePaymentObserver, AccountO } private func copyAccountToken() { - UIPasteboard.general.string = Account.shared.token + UIPasteboard.general.string = TunnelManager.shared.accountNumber contentView.accountTokenRowView.value = NSLocalizedString( "COPIED_TO_PASTEBOARD_LABEL", @@ -377,9 +378,9 @@ class AccountViewController: UIViewController, AppStorePaymentObserver, AccountO ) let workItem = DispatchWorkItem { [weak self] in - guard let formattedToken = Account.shared.formattedToken else { return } + guard let accountNumber = TunnelManager.shared.accountNumber else { return } - self?.contentView.accountTokenRowView.value = formattedToken + self?.contentView.accountTokenRowView.value = self?.formatAccountNumber(accountNumber) } copyToPasteboardWork?.cancel() @@ -389,22 +390,22 @@ class AccountViewController: UIViewController, AppStorePaymentObserver, AccountO } @objc private func doPurchase() { - guard let product = product, let accountToken = Account.shared.token else { return } + guard let product = product, let accountNumber = TunnelManager.shared.accountNumber else { return } let payment = SKPayment(product: product) pendingPayment = payment compoundInteractionRestriction.increase(animated: true) - AppStorePaymentManager.shared.addPayment(payment, for: accountToken) + AppStorePaymentManager.shared.addPayment(payment, for: accountNumber) } @objc private func restorePurchases() { - guard let accountToken = Account.shared.token else { return } + guard let accountNumber = TunnelManager.shared.accountNumber else { return } compoundInteractionRestriction.increase(animated: true) - _ = AppStorePaymentManager.shared.restorePurchases(for: accountToken) { completion in + _ = AppStorePaymentManager.shared.restorePurchases(for: accountNumber) { completion in switch completion { case .success(let response): self.showTimeAddedConfirmationAlert(with: response, context: .restoration) @@ -438,6 +439,10 @@ class AccountViewController: UIViewController, AppStorePaymentObserver, AccountO } } + private func formatAccountNumber(_ string: String) -> String { + return string.split(every: 4).joined(separator: " ") + } + } private extension REST.CreateApplePaymentResponse { diff --git a/ios/MullvadVPN/AppDelegate.swift b/ios/MullvadVPN/AppDelegate.swift index 32b2b93681..c5d4d9e9e4 100644 --- a/ios/MullvadVPN/AppDelegate.swift +++ b/ios/MullvadVPN/AppDelegate.swift @@ -103,29 +103,16 @@ class AppDelegate: UIResponder, UIApplicationDelegate { } // Load tunnels - TunnelManager.shared.loadTunnel(accountToken: Account.shared.token) { error in + TunnelManager.shared.loadConfiguration { error in dispatchPrecondition(condition: .onQueue(.main)) if let error = error { self.logger?.error(chainedError: error, message: "Failed to load tunnels") - switch error { - case .loadAllVPNConfigurations(_), .removeInconsistentVPNConfiguration(_): - // TODO: avoid throwing fatal error and show the problem report UI instead. - fatalError(error.displayChain(message: "Failed to load tunnels")) - - case .migrateTunnelSettings(_), .readTunnelSettings(_): - // Forget that user was logged in since tunnel settings are likely corrupt - // or missing. - Account.shared.forget { - self.didFinishInitialization() - } - - default: - fatalError("Unexpected error coming from loadTunnel()") - } + // TODO: avoid throwing fatal error and show the problem report UI instead. + fatalError(error.displayChain(message: "Failed to load VPN tunnel configuration")) } else { - self.relayConstraints = TunnelManager.shared.tunnelInfo?.tunnelSettings.relayConstraints + self.relayConstraints = TunnelManager.shared.tunnelSettings?.relayConstraints self.didFinishInitialization() } } @@ -274,11 +261,15 @@ class AppDelegate: UIResponder, UIApplicationDelegate { ) } - switch TunnelManager.shared.scheduleBackgroundTask() { - case .success: - self.logger?.debug("Scheduled private key rotation task") - case .failure(let error): - self.logger?.error(chainedError: error, message: "Could not schedule private key rotation task.") + do { + try TunnelManager.shared.scheduleBackgroundTask() + + logger?.debug("Scheduled private key rotation task.") + } catch { + logger?.error( + chainedError: AnyChainedError(error), + message: "Could not schedule private key rotation task." + ) } do { @@ -286,9 +277,11 @@ class AppDelegate: UIResponder, UIApplicationDelegate { self.logger?.debug("Scheduled address cache update task.") } catch { - self.logger?.error(chainedError: AnyChainedError(error), message: "Could not schedule address cache update task.") + self.logger?.error( + chainedError: AnyChainedError(error), + message: "Could not schedule address cache update task." + ) } - } private func didFinishInitialization() { @@ -321,8 +314,7 @@ class AppDelegate: UIResponder, UIApplicationDelegate { private func startPaymentQueueHandling() { let paymentManager = AppStorePaymentManager.shared paymentManager.delegate = self - - Account.shared.startPaymentMonitoring(with: paymentManager) + paymentManager.addPaymentObserver(TunnelManager.shared) paymentManager.startPaymentQueueMonitoring() } @@ -343,15 +335,15 @@ class AppDelegate: UIResponder, UIApplicationDelegate { self.connectController = connectController self.rootContainer?.setViewControllers([splitViewController], animated: false) - showSplitViewMaster(Account.shared.isLoggedIn, animated: false) + showSplitViewMaster(TunnelManager.shared.isAccountSet, animated: false) let rootContainerWrapper = makeLoginContainerController() - if !Account.shared.isAgreedToTermsOfService { + if !isAgreedToTermsOfService() { let consentViewController = self.makeConsentController { [weak self] (viewController) in guard let self = self else { return } - if Account.shared.isLoggedIn { + if TunnelManager.shared.isAccountSet { rootContainerWrapper.dismiss(animated: true) { self.showAccountSettingsControllerIfAccountExpired() } @@ -361,7 +353,7 @@ class AppDelegate: UIResponder, UIApplicationDelegate { } rootContainerWrapper.setViewControllers([consentViewController], animated: false) self.rootContainer?.present(rootContainerWrapper, animated: false) - } else if !Account.shared.isLoggedIn { + } else if !TunnelManager.shared.isAccountSet { rootContainerWrapper.setViewControllers([makeLoginController()], animated: false) self.rootContainer?.present(rootContainerWrapper, animated: false) } else { @@ -376,7 +368,7 @@ class AppDelegate: UIResponder, UIApplicationDelegate { let loginViewController = self.makeLoginController() var viewControllers: [UIViewController] = [loginViewController] - if Account.shared.isLoggedIn { + if TunnelManager.shared.isAccountSet { let connectController = self.makeConnectViewController() viewControllers.append(connectController) self.connectController = connectController @@ -387,7 +379,7 @@ class AppDelegate: UIResponder, UIApplicationDelegate { } } - if Account.shared.isAgreedToTermsOfService { + if isAgreedToTermsOfService() { showNextController(false) } else { let consentViewController = self.makeConsentController { (consentController) in @@ -432,7 +424,7 @@ class AppDelegate: UIResponder, UIApplicationDelegate { } consentViewController.completionHandler = { (consentViewController) in - Account.shared.agreeToTermsOfService() + setAgreedToTermsOfService() completion(consentViewController) } @@ -482,7 +474,7 @@ class AppDelegate: UIResponder, UIApplicationDelegate { } private func showAccountSettingsControllerIfAccountExpired() { - guard let accountExpiry = Account.shared.expiry, AccountExpiry(date: accountExpiry).isExpired else { return } + guard let accountExpiry = TunnelManager.shared.accountExpiry, accountExpiry <= Date() else { return } rootContainer?.showSettings(navigateTo: .account, animated: true) } @@ -538,7 +530,7 @@ extension AppDelegate: RootContainerViewControllerDelegate { } func rootContainerViewAccessibilityPerformMagicTap(_ controller: RootContainerViewController) -> Bool { - guard Account.shared.isLoggedIn else { return false } + guard TunnelManager.shared.isAccountSet else { return false } switch TunnelManager.shared.tunnelState { case .connected, .connecting, .reconnecting: @@ -556,39 +548,45 @@ extension AppDelegate: RootContainerViewControllerDelegate { extension AppDelegate: LoginViewControllerDelegate { - func loginViewController(_ controller: LoginViewController, loginWithAccountToken accountToken: String, completion: @escaping (Result<REST.AccountResponse, Account.Error>) -> Void) { + func loginViewController(_ controller: LoginViewController, loginWithAccountToken accountNumber: String, completion: @escaping (OperationCompletion<StoredAccountData?, TunnelManager.Error>) -> Void) { self.rootContainer?.setEnableSettingsButton(false) - Account.shared.login(accountToken: accountToken) { result in - switch result { + TunnelManager.shared.setAccount(action: .existing(accountNumber)) { operationCompletion in + switch operationCompletion { case .success: - self.logger?.debug("Logged in with existing token") + self.logger?.debug("Logged in with existing account.") // RootContainer's settings button will be re-enabled in `loginViewControllerDidLogin` case .failure(let error): - self.logger?.error(chainedError: error, message: "Failed to log in with existing account") + self.logger?.error(chainedError: error, message: "Failed to log in with existing account.") + fallthrough + + case .cancelled: self.rootContainer?.setEnableSettingsButton(true) } - completion(result) + completion(operationCompletion) } } - func loginViewControllerLoginWithNewAccount(_ controller: LoginViewController, completion: @escaping (Result<REST.AccountResponse, Account.Error>) -> Void) { + func loginViewControllerLoginWithNewAccount(_ controller: LoginViewController, completion: @escaping (OperationCompletion<StoredAccountData?, TunnelManager.Error>) -> Void) { self.rootContainer?.setEnableSettingsButton(false) - Account.shared.loginWithNewAccount { result in - switch result { + TunnelManager.shared.setAccount(action: .new) { operationCompletion in + switch operationCompletion { case .success: - self.logger?.debug("Logged in with new account token") + self.logger?.debug("Logged in with new account number.") // RootContainer's settings button will be re-enabled in `loginViewControllerDidLogin` case .failure(let error): - self.logger?.error(chainedError: error, message: "Failed to log in with new account") + self.logger?.error(chainedError: error, message: "Failed to log in with new account.") + fallthrough + + case .cancelled: self.rootContainer?.setEnableSettingsButton(true) } - completion(result) + completion(operationCompletion) } } @@ -598,7 +596,7 @@ extension AppDelegate: LoginViewControllerDelegate { // Move the settings button back into header bar self.rootContainer?.removeSettingsButtonFromPresentationContainer() - self.relayConstraints = TunnelManager.shared.tunnelInfo?.tunnelSettings.relayConstraints + self.relayConstraints = TunnelManager.shared.tunnelSettings?.relayConstraints self.selectLocationViewController?.setSelectedRelayLocation(relayConstraints?.location.value, animated: false, scrollPosition: .middle) switch UIDevice.current.userInterfaceIdiom { @@ -792,7 +790,7 @@ extension AppDelegate: AppStorePaymentManagerDelegate { { // Since we do not persist the relation between the payment and account token between the // app launches, we assume that all successful purchases belong to the active account token. - return Account.shared.token + return TunnelManager.shared.tunnelSettings?.account.number } } @@ -844,3 +842,18 @@ extension AppDelegate: UNUserNotificationCenterDelegate { } } + +// MARK: - + +/// A enum holding the `UserDefaults` string keys +private let isAgreedToTermsOfServiceKey = "isAgreedToTermsOfService" + +/// Returns true if user agreed to terms of service, otherwise false. +func isAgreedToTermsOfService() -> Bool { + return UserDefaults.standard.bool(forKey: isAgreedToTermsOfServiceKey) +} + +/// Save the boolean flag in preferences indicating that the user agreed to terms of service. +func setAgreedToTermsOfService() { + UserDefaults.standard.set(true, forKey: isAgreedToTermsOfServiceKey) +} diff --git a/ios/MullvadVPN/ConnectViewController.swift b/ios/MullvadVPN/ConnectViewController.swift index 1c6401bf63..3e2f6926ba 100644 --- a/ios/MullvadVPN/ConnectViewController.swift +++ b/ios/MullvadVPN/ConnectViewController.swift @@ -22,7 +22,7 @@ protocol ConnectViewControllerDelegate: AnyObject { func connectViewControllerShouldShowSelectLocationPicker(_ controller: ConnectViewController) } -class ConnectViewController: UIViewController, MKMapViewDelegate, RootContainment, TunnelObserver, AccountObserver { +class ConnectViewController: UIViewController, MKMapViewDelegate, RootContainment, TunnelObserver { weak var delegate: ConnectViewControllerDelegate? @@ -46,9 +46,10 @@ class ConnectViewController: UIViewController, MKMapViewDelegate, RootContainmen } var preferredHeaderBarPresentation: HeaderBarPresentation { - if !Account.shared.isLoggedIn { + guard TunnelManager.shared.isAccountSet else { return HeaderBarPresentation(style: .default, showsDivider: true) } + switch tunnelState { case .connecting, .reconnecting, .connected: return HeaderBarPresentation(style: .secured, showsDivider: false) @@ -94,7 +95,7 @@ class ConnectViewController: UIViewController, MKMapViewDelegate, RootContainmen updateLocation(animated: false) addNotificationController() - Account.shared.addObserver(self) + TunnelManager.shared.addObserver(self) } override func traitCollectionDidChange(_ previousTraitCollection: UITraitCollection?) { @@ -128,24 +129,10 @@ class ConnectViewController: UIViewController, MKMapViewDelegate, RootContainmen ]) } - // MARK: - AccountObserver - - func account(_ account: Account, didLoginWithToken token: String, expiry: Date) { - setNeedsHeaderBarStyleAppearanceUpdate() - } - - func account(_ account: Account, didUpdateExpiry expiry: Date) { - // no-op - } - - func accountDidLogout(_ account: Account) { - setNeedsHeaderBarStyleAppearanceUpdate() - } - // MARK: - TunnelObserver - func tunnelManager(_ manager: TunnelManager, didUpdateTunnelSettings tunnelInfo: TunnelInfo?) { - // no-op + func tunnelManager(_ manager: TunnelManager, didUpdateTunnelSettings tunnelSettings: TunnelSettingsV2?) { + setNeedsHeaderBarStyleAppearanceUpdate() } func tunnelManager(_ manager: TunnelManager, didUpdateTunnelState tunnelState: TunnelState) { diff --git a/ios/MullvadVPN/DisplayChainedError.swift b/ios/MullvadVPN/DisplayChainedError.swift index aa7ec26b2d..d5955e3aae 100644 --- a/ios/MullvadVPN/DisplayChainedError.swift +++ b/ios/MullvadVPN/DisplayChainedError.swift @@ -68,7 +68,6 @@ extension TunnelManager.Error: DisplayChainedError { ), systemError.localizedDescription ) - case .reloadVPNConfiguration(let systemError): return String( format: NSLocalizedString( @@ -79,7 +78,6 @@ extension TunnelManager.Error: DisplayChainedError { ), systemError.localizedDescription ) - case .saveVPNConfiguration(let systemError): return String( format: NSLocalizedString( @@ -90,15 +88,6 @@ extension TunnelManager.Error: DisplayChainedError { ), systemError.localizedDescription ) - - case .obtainPersistentKeychainReference(_): - return NSLocalizedString( - "OBTAIN_PERSISTENT_KEYCHAIN_REFERENCE_ERROR", - tableName: "TunnelManager", - value: "Failed to obtain the persistent keychain reference for the VPN configuration", - comment: "" - ) - case .startVPNTunnel(let systemError): return String( format: NSLocalizedString( @@ -109,7 +98,6 @@ extension TunnelManager.Error: DisplayChainedError { ), systemError.localizedDescription ) - case .removeVPNConfiguration(let systemError): return String( format: NSLocalizedString( @@ -120,121 +108,79 @@ extension TunnelManager.Error: DisplayChainedError { ), systemError.localizedDescription ) - - case .removeInconsistentVPNConfiguration(let systemError): - return String( - format: NSLocalizedString( - "REMOVE_INCONSISTENT_VPN_CONFIGURATION", - tableName: "TunnelManager", - value: "Failed to remove the outdated system VPN configuration: %@", - comment: "" - ), - systemError.localizedDescription - ) - - case .readTunnelSettings(_): + case .readSettings: return NSLocalizedString( "READ_TUNNEL_SETTINGS_ERROR", tableName: "TunnelManager", - value: "Failed to read tunnel settings", + value: "Failed to read settings", comment: "" ) - - case .addTunnelSettings(_): + case .writeSettings: return NSLocalizedString( - "ADD_TUNNEL_SETTINGS_ERROR", + "WRITE_TUNNEL_SETTINGS_ERROR", tableName: "TunnelManager", - value: "Failed to add tunnel settings", + value: "Failed to write settings", comment: "" ) - - case .updateTunnelSettings(_): + case .deleteSettings: return NSLocalizedString( - "UPDATE_TUNNEL_SETTINGS_ERROR", + "DELETE_TUNNEL_SETTINGS_ERROR", tableName: "TunnelManager", - value: "Failed to update tunnel settings", + value: "Failed to delete settings", comment: "" ) - - case .removeTunnelSettings(_): - return NSLocalizedString( - "REMOVE_TUNNEL_SETTINGS_ERROR", - tableName: "TunnelManager", - value: "Failed to remove tunnel settings", - comment: "" + case .deleteDevice(let restError): + return String( + format: NSLocalizedString( + "DELETE_DEVICE_ERROR", + tableName: "TunnelManager", + value: "Failed to create a device: %@", + comment: "" + ), + restError.errorChainDescription ?? "" ) - - case .migrateTunnelSettings(_): + case .getDevice(let restError): + return String( + format: NSLocalizedString( + "CREATE_DEVICE_ERROR", + tableName: "TunnelManager", + value: "Failed to obtain device data: %@", + comment: "" + ), + restError.errorChainDescription ?? "" + ) + case .deviceRevoked: return NSLocalizedString( - "MIGRATE_TUNNEL_SETTINGS_ERROR", + "DEVICE_REVOKED_ERROR", tableName: "TunnelManager", - value: "Failed to migrate tunnel settings", + value: "Device is revoked.", comment: "" ) - - case .pushWireguardKey(let restError): - let reason = restError.errorChainDescription ?? "" - var message = String( + case .createDevice(let restError): + return String( format: NSLocalizedString( - "PUSH_WIREGUARD_KEY_ERROR", + "CREATE_DEVICE_ERROR", tableName: "TunnelManager", - value: "Failed to send the WireGuard key to server: %@", + value: "Failed to create a device: %@", comment: "" ), - reason + restError.errorChainDescription ?? "" ) - - if case .unhandledResponse(_, let serverErrorResponse) = restError, - serverErrorResponse?.code == .keyLimitReached - { - // TODO: maybe use `restError.recoverySuggestion` instead? - message.append("\n\n") - message.append(NSLocalizedString( - "PUSH_WIREGUARD_KEY_RECOVERY_SUGGESTION", - tableName: "TunnelManager", - value: "Remove unused WireGuard keys and try again", - comment: "" - )) - } - - return message - - case .replaceWireguardKey(let restError): - let reason = restError.errorChainDescription ?? "" - var message = String( + case .rotateKey(let restError): + return String( format: NSLocalizedString( - "REPLACE_WIREGUARD_KEY_ERROR", + "ROTATE_KY_ERROR", tableName: "TunnelManager", - value: "Failed to replace the WireGuard key on server: %@", + value: "Failed to rotate WireGuard key: %@", comment: "" ), - reason + restError.errorChainDescription ?? "" ) - - if case .unhandledResponse(_, let serverErrorResponse) = restError, - serverErrorResponse?.code == .keyLimitReached - { - // TODO: maybe use `restError.recoverySuggestion` instead? - message.append("\n\n") - message.append(NSLocalizedString( - "REPLACE_WIREGUARD_KEY_RECOVERY_SUGGESTION", - tableName: "TunnelManager", - value: "Remove unused WireGuard keys and try again", - comment: "") - ) - } - - return message - - case .removeWireguardKey: - // This error is never displayed anywhere - return nil - case .unsetAccount: return NSLocalizedString( - "MISSING_ACCOUNT_INTERNAL_ERROR", + "UNSET_ACCOUNT_ERROR", tableName: "TunnelManager", - value: "Internal error: missing account", + value: "Internal error: account is unset", comment: "" ) case .readRelays: @@ -251,11 +197,6 @@ extension TunnelManager.Error: DisplayChainedError { value: "Failed to satisfy relay constraints.", comment: "" ) - - case .backgroundTaskScheduler: - // This error is never displayed anywhere - return nil - case .reloadTunnel(let error): return String( format: NSLocalizedString( @@ -266,18 +207,26 @@ extension TunnelManager.Error: DisplayChainedError { ), error.localizedDescription ) - } - } -} - -extension Account.Error: DisplayChainedError { - var errorChainDescription: String? { - switch self { - case .createAccount(let restError), .verifyAccount(let restError): - return restError.errorChainDescription - - case .tunnelConfiguration(let tunnelError): - return tunnelError.errorChainDescription + case .getAccountData(let restError): + return String( + format: NSLocalizedString( + "GET_ACCOUNT_DATA_ERROR", + tableName: "TunnelManager", + value: "Failed to obtain account data: %@", + comment: "" + ), + restError.errorChainDescription ?? "" + ) + case .createAccount(let restError): + return String( + format: NSLocalizedString( + "CREATE_ACCOUNT_ERROR", + tableName: "TunnelManager", + value: "Failed to create new account: %@", + comment: "" + ), + restError.errorChainDescription ?? "" + ) } } } diff --git a/ios/MullvadVPN/LoginViewController.swift b/ios/MullvadVPN/LoginViewController.swift index f345798941..cba1f78992 100644 --- a/ios/MullvadVPN/LoginViewController.swift +++ b/ios/MullvadVPN/LoginViewController.swift @@ -16,13 +16,22 @@ enum AuthenticationMethod { enum LoginState { case `default` case authenticating(AuthenticationMethod) - case failure(Account.Error) + case failure(TunnelManager.Error) case success(AuthenticationMethod) } protocol LoginViewControllerDelegate: AnyObject { - func loginViewController(_ controller: LoginViewController, loginWithAccountToken accountToken: String, completion: @escaping (Result<REST.AccountResponse, Account.Error>) -> Void) - func loginViewControllerLoginWithNewAccount(_ controller: LoginViewController, completion: @escaping (Result<REST.AccountResponse, Account.Error>) -> Void) + func loginViewController( + _ controller: LoginViewController, + loginWithAccountToken accountToken: String, + completion: @escaping (OperationCompletion<StoredAccountData?, TunnelManager.Error>) -> Void + ) + + func loginViewControllerLoginWithNewAccount( + _ controller: LoginViewController, + completion: @escaping (OperationCompletion<StoredAccountData?, TunnelManager.Error>) -> Void + ) + func loginViewControllerDidLogin(_ controller: LoginViewController) } @@ -183,12 +192,14 @@ class LoginViewController: UIViewController, RootContainment { let accountToken = contentView.accountInputGroup.parsedToken beginLogin(method: .existingAccount) - self.delegate?.loginViewController(self, loginWithAccountToken: accountToken, completion: { [weak self] (result) in - switch result { + self.delegate?.loginViewController(self, loginWithAccountToken: accountToken, completion: { [weak self] completion in + switch completion { case .success: self?.endLogin(.success(.existingAccount)) case .failure(let error): self?.endLogin(.failure(error)) + case .cancelled: + self?.endLogin(.default) } }) } @@ -199,13 +210,15 @@ class LoginViewController: UIViewController, RootContainment { contentView.accountInputGroup.clearToken() updateKeyboardToolbar() - self.delegate?.loginViewControllerLoginWithNewAccount(self, completion: { [weak self] (result) in - switch result { - case .success(let response): - self?.contentView.accountInputGroup.setToken(response.token) + self.delegate?.loginViewControllerLoginWithNewAccount(self, completion: { [weak self] completion in + switch completion { + case .success(let accountData): + self?.contentView.accountInputGroup.setToken(accountData?.number ?? "") self?.endLogin(.success(.newAccount)) case .failure(let error): self?.endLogin(.failure(error)) + case .cancelled: + self?.endLogin(.default) } }) } @@ -341,43 +354,7 @@ private extension LoginState { } case .failure(let error): - let localizedUnknownInternalError = NSLocalizedString( - "SUBHEAD_TITLE_INTERNAL_ERROR", - tableName: "Login", - comment: "Subhead displayed in the event of internal error." - ) - - switch error { - case .createAccount(let rpcError), .verifyAccount(let rpcError): - return rpcError.errorChainDescription ?? "" - case .tunnelConfiguration(let error): - if case .pushWireguardKey(let pushError) = error { - switch pushError { - case .network(let urlError): - return String( - format: NSLocalizedString( - "SUBHEAD_TITLE_NETWORK_ERROR_FORMAT", - tableName: "Login", - value: "Network error: %@", - comment: "Subhead displayed in the event of network error. Use %@ placeholder to place localized text describing network failure." - ), - urlError.localizedDescription - ) - - case .unhandledResponse(_, let serverError): - return serverError?.detail ?? NSLocalizedString( - "SUBHEAD_TITLE_UNKNOWN_SERVER_ERROR", - tableName: "Login", - comment: "Subhead displayed in the event of unknown server error." - ) - - case .createURLRequest, .decodeResponse: - return localizedUnknownInternalError - } - } else { - return localizedUnknownInternalError - } - } + return error.errorChainDescription ?? "" case .success(let method): switch method { diff --git a/ios/MullvadVPN/Notifications/AccountExpiryNotificationProvider.swift b/ios/MullvadVPN/Notifications/AccountExpiryNotificationProvider.swift index 294028f8c1..4fd9aacdef 100644 --- a/ios/MullvadVPN/Notifications/AccountExpiryNotificationProvider.swift +++ b/ios/MullvadVPN/Notifications/AccountExpiryNotificationProvider.swift @@ -12,7 +12,7 @@ import UserNotifications let accountExpiryNotificationIdentifier = "net.mullvad.MullvadVPN.AccountExpiryNotification" let accountExpiryDefaultTriggerInterval = 3 -class AccountExpiryNotificationProvider: NotificationProvider, SystemNotificationProvider, InAppNotificationProvider, AccountObserver { +class AccountExpiryNotificationProvider: NotificationProvider, SystemNotificationProvider, InAppNotificationProvider, TunnelObserver { private var accountExpiry: Date? /// Interval prior to expiry used to calculate when to trigger notifications. @@ -27,8 +27,8 @@ class AccountExpiryNotificationProvider: NotificationProvider, SystemNotificatio super.init() - accountExpiry = Account.shared.expiry - Account.shared.addObserver(self) + accountExpiry = TunnelManager.shared.accountExpiry + TunnelManager.shared.addObserver(self) } private var trigger: UNNotificationTrigger? { @@ -122,18 +122,18 @@ class AccountExpiryNotificationProvider: NotificationProvider, SystemNotificatio ) } - func account(_ account: Account, didUpdateExpiry expiry: Date) { - self.accountExpiry = expiry - invalidate() + // MARK: - TunnelObserver + + func tunnelManager(_ manager: TunnelManager, didUpdateTunnelState tunnelState: TunnelState) { + // no-op } - func account(_ account: Account, didLoginWithToken token: String, expiry: Date) { - self.accountExpiry = expiry - invalidate() + func tunnelManager(_ manager: TunnelManager, didFailWithError error: TunnelManager.Error) { + // no-op } - func accountDidLogout(_ account: Account) { - self.accountExpiry = nil + func tunnelManager(_ manager: TunnelManager, didUpdateTunnelSettings tunnelSettings: TunnelSettingsV2?) { + accountExpiry = tunnelSettings?.account.expiry invalidate() } diff --git a/ios/MullvadVPN/Notifications/TunnelErrorNotificationProvider.swift b/ios/MullvadVPN/Notifications/TunnelErrorNotificationProvider.swift index 85f0de05ad..afc6e5aa72 100644 --- a/ios/MullvadVPN/Notifications/TunnelErrorNotificationProvider.swift +++ b/ios/MullvadVPN/Notifications/TunnelErrorNotificationProvider.swift @@ -42,7 +42,7 @@ class TunnelErrorNotificationProvider: NotificationProvider, InAppNotificationPr invalidate() } - func tunnelManager(_ manager: TunnelManager, didUpdateTunnelSettings tunnelInfo: TunnelInfo?) { + func tunnelManager(_ manager: TunnelManager, didUpdateTunnelSettings tunnelSettings: TunnelSettingsV2?) { // no-op } diff --git a/ios/MullvadVPN/PreferencesViewController.swift b/ios/MullvadVPN/PreferencesViewController.swift index a5cc0dc06c..6d21afeea8 100644 --- a/ios/MullvadVPN/PreferencesViewController.swift +++ b/ios/MullvadVPN/PreferencesViewController.swift @@ -43,7 +43,7 @@ class PreferencesViewController: UITableViewController, PreferencesDataSourceDel TunnelManager.shared.addObserver(self) - if let dnsSettings = TunnelManager.shared.tunnelInfo?.tunnelSettings.interface.dnsSettings { + if let dnsSettings = TunnelManager.shared.tunnelSettings?.dnsSettings { dataSource.update(from: dnsSettings) } } @@ -85,8 +85,8 @@ class PreferencesViewController: UITableViewController, PreferencesDataSourceDel // no-op } - func tunnelManager(_ manager: TunnelManager, didUpdateTunnelSettings tunnelInfo: TunnelInfo?) { - guard let dnsSettings = tunnelInfo?.tunnelSettings.interface.dnsSettings else { return } + func tunnelManager(_ manager: TunnelManager, didUpdateTunnelSettings tunnelSettings: TunnelSettingsV2?) { + guard let dnsSettings = tunnelSettings?.dnsSettings else { return } dataSource.update(from: dnsSettings) } diff --git a/ios/MullvadVPN/ProblemReportViewController.swift b/ios/MullvadVPN/ProblemReportViewController.swift index b9311174e7..47addb7589 100644 --- a/ios/MullvadVPN/ProblemReportViewController.swift +++ b/ios/MullvadVPN/ProblemReportViewController.swift @@ -19,7 +19,7 @@ class ProblemReportViewController: UIViewController, UITextFieldDelegate, Condit let securityGroupIdentifier = ApplicationConfiguration.securityGroupIdentifier // TODO: make sure we redact old tokens - let redactStrings = Account.shared.token.flatMap { [$0] } ?? [] + let redactStrings = TunnelManager.shared.accountNumber.flatMap { [$0] } ?? [] let report = ConsolidatedApplicationLog( redactCustomStrings: redactStrings, diff --git a/ios/MullvadVPN/SettingsDataSource.swift b/ios/MullvadVPN/SettingsDataSource.swift index 830e0ec2af..5912240f3b 100644 --- a/ios/MullvadVPN/SettingsDataSource.swift +++ b/ios/MullvadVPN/SettingsDataSource.swift @@ -8,7 +8,7 @@ import UIKit -class SettingsDataSource: NSObject, AccountObserver, UITableViewDataSource, UITableViewDelegate { +class SettingsDataSource: NSObject, TunnelObserver, UITableViewDataSource, UITableViewDelegate { private enum CellReuseIdentifiers: String, CaseIterable { case accountCell case basicCell @@ -50,6 +50,7 @@ class SettingsDataSource: NSObject, AccountObserver, UITableViewDataSource, UITa } private var snapshot = DataSourceSnapshot<Section, Item>() + private var storedAccountData: StoredAccountData? weak var delegate: SettingsDataSourceDelegate? @@ -65,7 +66,9 @@ class SettingsDataSource: NSObject, AccountObserver, UITableViewDataSource, UITa override init() { super.init() - Account.shared.addObserver(self) + TunnelManager.shared.addObserver(self) + storedAccountData = TunnelManager.shared.tunnelSettings?.account + updateDataSnapshot() } @@ -82,7 +85,7 @@ class SettingsDataSource: NSObject, AccountObserver, UITableViewDataSource, UITa private func updateDataSnapshot() { var newSnapshot = DataSourceSnapshot<Section, Item>() - if Account.shared.isLoggedIn { + if TunnelManager.shared.isAccountSet { newSnapshot.appendSections([.main]) newSnapshot.appendItems([.account, .preferences, .wireguardKey], in: .main) } @@ -113,7 +116,7 @@ class SettingsDataSource: NSObject, AccountObserver, UITableViewDataSource, UITa case .account: let cell = tableView.dequeueReusableCell(withIdentifier: CellReuseIdentifiers.accountCell.rawValue, for: indexPath) as! SettingsAccountCell cell.titleLabel.text = NSLocalizedString("ACCOUNT_CELL_LABEL", tableName: "Settings", value: "Account", comment: "") - cell.accountExpiryDate = Account.shared.expiry + cell.accountExpiryDate = TunnelManager.shared.accountExpiry cell.accessibilityIdentifier = "AccountCell" cell.disclosureType = .chevron @@ -199,22 +202,34 @@ class SettingsDataSource: NSObject, AccountObserver, UITableViewDataSource, UITa return 0 } - // MARK: - AccountObserver + // MARK: - TunnelObserver - func account(_ account: Account, didUpdateExpiry expiry: Date) { - tableView?.performBatchUpdates { - if let indexPath = snapshot.indexPathForItem(.account) { - tableView?.reloadRows(at: [indexPath], with: .none) - } - } + func tunnelManager(_ manager: TunnelManager, didFailWithError error: TunnelManager.Error) { + // no-op } - func account(_ account: Account, didLoginWithToken token: String, expiry: Date) { - updateDataSnapshot() - tableView?.reloadData() + func tunnelManager(_ manager: TunnelManager, didUpdateTunnelState tunnelState: TunnelState) { + // no-op } - func accountDidLogout(_ account: Account) { + func tunnelManager(_ manager: TunnelManager, didUpdateTunnelSettings tunnelSettings: TunnelSettingsV2?) { + let newAccountData = tunnelSettings?.account + let oldAccountData = storedAccountData + + storedAccountData = newAccountData + + // Refresh individual row if expiry changed. + if let newAccountData = newAccountData, let oldAccountData = oldAccountData, + oldAccountData.number == newAccountData.number, + oldAccountData.expiry != newAccountData.expiry { + tableView?.performBatchUpdates { + if let indexPath = snapshot.indexPathForItem(.account) { + tableView?.reloadRows(at: [indexPath], with: .none) + } + } + return + } + updateDataSnapshot() tableView?.reloadData() } diff --git a/ios/MullvadVPN/SettingsNavigationController.swift b/ios/MullvadVPN/SettingsNavigationController.swift index b594492cff..b916561373 100644 --- a/ios/MullvadVPN/SettingsNavigationController.swift +++ b/ios/MullvadVPN/SettingsNavigationController.swift @@ -62,7 +62,7 @@ class SettingsNavigationController: CustomNavigationController, SettingsViewCont navigationBar.prefersLargeTitles = true // Update account expiry - Account.shared.updateAccountExpiry() + TunnelManager.shared.updateAccountData() } // MARK: - SettingsViewControllerDelegate diff --git a/ios/MullvadVPN/TunnelManager/LoadTunnelConfigurationOperation.swift b/ios/MullvadVPN/TunnelManager/LoadTunnelConfigurationOperation.swift new file mode 100644 index 0000000000..50fad00647 --- /dev/null +++ b/ios/MullvadVPN/TunnelManager/LoadTunnelConfigurationOperation.swift @@ -0,0 +1,128 @@ +// +// LoadTunnelConfigurationOperation.swift +// MullvadVPN +// +// Created by pronebird on 16/12/2021. +// Copyright © 2021 Mullvad VPN AB. All rights reserved. +// + +import Foundation +import Logging + +class LoadTunnelConfigurationOperation: ResultOperation<(), TunnelManager.Error> { + private let logger = Logger(label: "LoadTunnelConfigurationOperation") + private let state: TunnelManager.State + + init( + dispatchQueue: DispatchQueue, + state: TunnelManager.State, + completionHandler: @escaping CompletionHandler + ) { + self.state = state + + super.init( + dispatchQueue: dispatchQueue, + completionQueue: dispatchQueue, + completionHandler: completionHandler + ) + } + + override func main() { + TunnelProviderManagerType.loadAllFromPreferences { tunnels, error in + self.dispatchQueue.async { + if let error = error { + self.finish(completion: .failure(.loadAllVPNConfigurations(error))) + } else { + self.didLoadVPNConfigurations(tunnels: tunnels) + } + } + } + } + + private func didLoadVPNConfigurations(tunnels: [TunnelProviderManagerType]?) { + let tunnelProvider = tunnels?.first + + do { + let tunnelSettings = try SettingsManager.readSettings() + let tunnel = tunnelProvider.map { tunnelProvider in + return Tunnel(tunnelProvider: tunnelProvider) + } + + state.tunnelSettings = tunnelSettings + state.setTunnel(tunnel, shouldRefreshTunnelState: true) + + finish(completion: .success(())) + } catch .itemNotFound as KeychainError { + logger.debug("Settings not found in keychain.") + + state.tunnelSettings = nil + state.setTunnel(nil, shouldRefreshTunnelState: true) + + if let tunnelProvider = tunnelProvider { + removeOrphanedTunnel(tunnelProvider: tunnelProvider) { error in + self.finish(completion: error.map { .failure($0) } ?? .success(())) + } + } else { + finish(completion: .success(())) + } + } catch let error as DecodingError { + state.tunnelSettings = nil + state.setTunnel(nil, shouldRefreshTunnelState: true) + + do { + logger.error( + chainedError: AnyChainedError(error), + message: "Cannot decode settings. Will attempt to delete them from keychain." + ) + + try SettingsManager.deleteSettings() + } catch { + logger.error( + chainedError: AnyChainedError(error), + message: "Failed to delete settings from keychain." + ) + } + + let returnError: TunnelManager.Error = .readSettings(error) + + if let tunnelProvider = tunnelProvider { + removeOrphanedTunnel(tunnelProvider: tunnelProvider) { _ in + self.finish(completion: .failure(returnError)) + } + } else { + finish(completion: .failure(returnError)) + } + } catch { + state.tunnelSettings = nil + state.setTunnel(nil, shouldRefreshTunnelState: true) + + let returnError: TunnelManager.Error = .readSettings(error) + + if let tunnelProvider = tunnelProvider { + removeOrphanedTunnel(tunnelProvider: tunnelProvider) { _ in + self.finish(completion: .failure(returnError)) + } + } else { + finish(completion: .failure(returnError)) + } + } + } + + private func removeOrphanedTunnel(tunnelProvider: TunnelProviderManagerType, completion: @escaping (TunnelManager.Error?) -> Void) { + logger.debug("Remove orphaned VPN configuration.") + + tunnelProvider.removeFromPreferences { error in + self.dispatchQueue.async { + if let error = error { + self.logger.error( + chainedError: AnyChainedError(error), + message: "Failed to remove VPN configuration." + ) + completion(.removeVPNConfiguration(error)) + } else { + completion(nil) + } + } + } + } +} diff --git a/ios/MullvadVPN/TunnelManager/MigrateSettingsOperation.swift b/ios/MullvadVPN/TunnelManager/MigrateSettingsOperation.swift new file mode 100644 index 0000000000..61327ec409 --- /dev/null +++ b/ios/MullvadVPN/TunnelManager/MigrateSettingsOperation.swift @@ -0,0 +1,249 @@ +// +// MigrateSettingsOperation.swift +// MullvadVPN +// +// Created by pronebird on 18/05/2022. +// Copyright © 2022 Mullvad VPN AB. All rights reserved. +// + +import Foundation +import Logging +import class WireGuardKitTypes.PrivateKey + +class MigrateSettingsOperation: AsyncOperation { + private let accountTokenKey = "accountToken" + private let accountExpiryKey = "accountExpiry" + + private let accountsProxy: REST.AccountsProxy + private let devicesProxy: REST.DevicesProxy + + private let logger = Logger(label: "MigrateSettingsOperation") + + private var accountTask: Cancellable? + private var deviceTask: Cancellable? + + private var accountData: REST.AccountData? + private var devices: [REST.Device]? + + init( + dispatchQueue: DispatchQueue, + accountsProxy: REST.AccountsProxy, + devicesProxy: REST.DevicesProxy + ) + { + self.accountsProxy = accountsProxy + self.devicesProxy = devicesProxy + + super.init(dispatchQueue: dispatchQueue) + } + + override func main() { + // Read legacy account number from user defaults. + let storedAccountNumber = UserDefaults.standard.string(forKey: accountTokenKey) + + guard let storedAccountNumber = storedAccountNumber else { + logger.debug("Account number is not found in user defaults. Nothing to migrate.") + + finishMigration() + return + } + + // Set legacy account number as last used. + logger.debug("Found legacy account number.") + logger.debug("Store last used account.") + + do { + try SettingsManager.setLastUsedAccount(storedAccountNumber) + } catch { + logger.error( + chainedError: AnyChainedError(error), + message: "Failed to store last used account." + ) + } + + // List legacy settings stored in keychain. + logger.debug("Read legacy settings...") + + var storedSettings: [LegacyTunnelSettings] = [] + do { + storedSettings = try SettingsManager.readLegacySettings() + } catch .itemNotFound as KeychainError { + logger.debug("Legacy settings are not found in keychain.") + + finishMigration() + return + } catch { + logger.error( + chainedError: AnyChainedError(error), + message: "Failed to read legacy settings from keychain." + ) + finishMigration() + return + } + + // Find settings matching the account number stored in user defaults. + let matchingSettings = storedSettings.first { settings in + return settings.accountNumber == storedAccountNumber + } + + guard let matchingSettings = matchingSettings else { + logger.debug( + "Could not find legacy settings matching the legacy account number." + ) + + finishMigration() + return + } + + // Fetch remote data concurrently. + logger.debug("Fetching account and device data...") + + let dispatchGroup = DispatchGroup() + + dispatchGroup.enter() + accountTask = accountsProxy.getAccountData( + accountNumber: storedAccountNumber, + retryStrategy: .aggressive + ) { completion in + self.dispatchQueue.async { + self.didFinishAccountRequest(completion) + + dispatchGroup.leave() + } + } + + dispatchGroup.enter() + deviceTask = devicesProxy.getDevices( + accountNumber: storedAccountNumber, + retryStrategy: .aggressive + ) { completion in + self.dispatchQueue.async { + self.didFinishDeviceRequest(completion) + + dispatchGroup.leave() + } + } + + dispatchGroup.notify(queue: dispatchQueue) { + // Migrate settings if all data is available. + if let accountData = self.accountData, let devices = self.devices { + self.migrateSettings( + settings: matchingSettings, + accountData: accountData, + devices: devices + ) + } + + // Finish migration. + self.finishMigration() + } + } + + private func didFinishAccountRequest(_ completion: OperationCompletion<REST.AccountData, REST.Error>) { + switch completion { + case .success(let accountData): + self.accountData = accountData + + case .failure(let error): + logger.error(chainedError: error, message: "Failed to fetch accound data.") + + case .cancelled: + logger.debug("Account data request was cancelled.") + } + } + + private func didFinishDeviceRequest(_ completion: OperationCompletion<[REST.Device], REST.Error>) { + switch completion { + case .success(let devices): + self.devices = devices + + case .failure(let error): + logger.error(chainedError: error, message: "Failed to fetch devices.") + + case .cancelled: + logger.debug("Device request was cancelled.") + } + } + + private func migrateSettings( + settings: LegacyTunnelSettings, + accountData: REST.AccountData, + devices: [REST.Device] + ) { + let tunnelSettings = settings.tunnelSettings + let interfaceData = settings.tunnelSettings.interface + + // Find device that matches the public key stored in legacy settings. + let device = devices.first { device in + return device.pubkey == interfaceData.privateKey.publicKey || + device.pubkey == interfaceData.nextPrivateKey?.publicKey + } + + guard let device = device else { + logger.debug( + "Failed to match legacy settings against available devices." + ) + return + } + + logger.debug("Found device matching public key stored in legacy settings.") + + // Match private key. + let privateKeyWithMetadata: PrivateKeyWithMetadata + if let nextKey = interfaceData.nextPrivateKey, nextKey.publicKey == device.pubkey + { + privateKeyWithMetadata = nextKey + } else { + privateKeyWithMetadata = interfaceData.privateKey + } + + logger.debug("Store new settings...") + + // Create new settings. + let newSettings = TunnelSettingsV2( + account: StoredAccountData( + identifier: accountData.id, + number: settings.accountNumber, + expiry: accountData.expiry + ), + device: StoredDeviceData( + creationDate: device.created, + identifier: device.id, + name: device.name, + hijackDNS: device.hijackDNS, + ipv4Address: device.ipv4Address, + ipv6Address: device.ipv6Address, + wgKeyData: StoredWgKeyData( + creationDate: privateKeyWithMetadata.creationDate, + privateKey: privateKeyWithMetadata.privateKey + ) + ), + relayConstraints: tunnelSettings.relayConstraints, + dnsSettings: interfaceData.dnsSettings + ) + + // Save settings. + do { + try SettingsManager.writeSettings(newSettings) + } catch { + logger.error( + chainedError: AnyChainedError(error), + message: "Failed to write migrated settings." + ) + } + } + + private func finishMigration() { + let userDefaults = UserDefaults.standard + + logger.debug("Remove legacy settings from keychain.") + SettingsManager.deleteLegacySettings() + + logger.debug("Remove legacy settings from user defaults.") + userDefaults.removeObject(forKey: accountTokenKey) + userDefaults.removeObject(forKey: accountExpiryKey) + + finish() + } + +} diff --git a/ios/MullvadVPN/TunnelManager/ReplaceKeyOperation.swift b/ios/MullvadVPN/TunnelManager/ReplaceKeyOperation.swift deleted file mode 100644 index 6a02ad09db..0000000000 --- a/ios/MullvadVPN/TunnelManager/ReplaceKeyOperation.swift +++ /dev/null @@ -1,204 +0,0 @@ -// -// ReplaceKeyOperation.swift -// MullvadVPN -// -// Created by pronebird on 15/12/2021. -// Copyright © 2021 Mullvad VPN AB. All rights reserved. -// - -import Foundation -import Logging - -class ReplaceKeyOperation: ResultOperation<TunnelManager.KeyRotationResult, TunnelManager.Error> { - private let state: TunnelManager.State - - private let apiProxy: REST.APIProxy - private var restRequest: Cancellable? - - private let rotationInterval: TimeInterval? - - private let logger = Logger(label: "TunnelManager.ReplaceKeyOperation") - - class func operationForKeyRotation( - dispatchQueue: DispatchQueue, - state: TunnelManager.State, - apiProxy: REST.APIProxy, - rotationInterval: TimeInterval, - completionHandler: @escaping CompletionHandler - ) -> ReplaceKeyOperation { - return ReplaceKeyOperation( - dispatchQueue: dispatchQueue, - state: state, - apiProxy: apiProxy, - rotationInterval: rotationInterval, - completionHandler: completionHandler - ) - } - - class func operationForKeyRegeneration( - dispatchQueue: DispatchQueue, - state: TunnelManager.State, - apiProxy: REST.APIProxy, - completionHandler: @escaping (OperationCompletion<(), TunnelManager.Error>) -> Void - ) -> ReplaceKeyOperation { - return ReplaceKeyOperation( - dispatchQueue: dispatchQueue, - state: state, - apiProxy: apiProxy, - rotationInterval: nil - ) { completion in - let mappedCompletion = completion.map { keyRotationResult -> () in - switch keyRotationResult { - case .finished: - return () - case .throttled: - fatalError("ReplaceKeyOperation.operationForKeyRegeneration() must never recieve throttled!") - } - } - - completionHandler(mappedCompletion) - } - } - - private init( - dispatchQueue: DispatchQueue, - state: TunnelManager.State, - apiProxy: REST.APIProxy, - rotationInterval: TimeInterval?, - completionHandler: @escaping CompletionHandler - ) { - self.state = state - - self.apiProxy = apiProxy - self.rotationInterval = rotationInterval - - super.init( - dispatchQueue: dispatchQueue, - completionQueue: dispatchQueue, - completionHandler: completionHandler - ) - } - - override func main() { - self.execute { completion in - self.finish(completion: completion) - } - } - - override func operationDidCancel() { - restRequest?.cancel() - restRequest = nil - } - - private func execute(completionHandler: @escaping CompletionHandler) { - guard let tunnelInfo = state.tunnelInfo else { - completionHandler(.failure(.unsetAccount)) - return - } - - if let rotationInterval = rotationInterval { - let creationDate = tunnelInfo.tunnelSettings.interface.privateKey.creationDate - let nextRotationDate = creationDate.addingTimeInterval(rotationInterval) - - if nextRotationDate > Date() { - logger.debug("Throttle private key rotation.") - - completionHandler(.success(.throttled(creationDate))) - return - } else { - logger.debug("Private key is old enough, rotate right away.") - } - } else { - logger.debug("Rotate private key right away.") - } - - let newPrivateKey: PrivateKeyWithMetadata - let oldPublicKey = tunnelInfo.tunnelSettings.interface.publicKey - - if let nextPrivateKey = tunnelInfo.tunnelSettings.interface.nextPrivateKey { - newPrivateKey = nextPrivateKey - - logger.debug("Next private key is already created.") - } else { - newPrivateKey = PrivateKeyWithMetadata() - - logger.debug("Create next private key.") - - let saveResult = TunnelSettingsManager.update(searchTerm: .accountToken(tunnelInfo.token)) { newTunnelSettings in - newTunnelSettings.interface.nextPrivateKey = newPrivateKey - } - - switch saveResult { - case .success(let newTunnelSettings): - logger.debug("Saved next private key.") - - state.tunnelInfo?.tunnelSettings = newTunnelSettings - - case .failure(let error): - logger.error(chainedError: error, message: "Failed to save next private key.") - - completionHandler(.failure(.updateTunnelSettings(error))) - return - } - } - - logger.debug("Replacing old key with new key on server...") - - restRequest = self.apiProxy.replaceWireguardKey( - accountNumber: tunnelInfo.token, - oldPublicKey: oldPublicKey, - newPublicKey: newPrivateKey.publicKey, - retryStrategy: .default - ) { completion in - self.dispatchQueue.async { - self.didReceiveResponse( - completion: completion, - accountToken: tunnelInfo.token, - newPrivateKey: newPrivateKey, - completionHandler: completionHandler - ) - } - } - } - - private func didReceiveResponse(completion: OperationCompletion<REST.WireguardAddressesResponse, REST.Error>, accountToken: String, newPrivateKey: PrivateKeyWithMetadata, completionHandler: @escaping CompletionHandler) { - switch completion { - case .success(let associatedAddresses): - logger.debug("Replaced old key with new key on server.") - - let saveResult = TunnelSettingsManager.update(searchTerm: .accountToken(accountToken)) { newTunnelSettings in - newTunnelSettings.interface.privateKey = newPrivateKey - newTunnelSettings.interface.nextPrivateKey = nil - - newTunnelSettings.interface.addresses = [ - associatedAddresses.ipv4Address, - associatedAddresses.ipv6Address - ] - } - - switch saveResult { - case .success(let newTunnelSettings): - logger.debug("Saved associated addresses.") - - state.tunnelInfo?.tunnelSettings = newTunnelSettings - - completionHandler(.success(.finished)) - - case .failure(let error): - logger.error(chainedError: error, message: "Failed to save associated addresses.") - - completionHandler(.failure(.updateTunnelSettings(error))) - } - - case .failure(let restError): - logger.error(chainedError: restError, message: "Failed to replace old key with new key on server.") - - completionHandler(.failure(.replaceWireguardKey(restError))) - - case .cancelled: - logger.debug("Cancelled replace key request.") - - completionHandler(.cancelled) - } - } -} diff --git a/ios/MullvadVPN/TunnelManager/RotateKeyOperation.swift b/ios/MullvadVPN/TunnelManager/RotateKeyOperation.swift new file mode 100644 index 0000000000..bde9f12811 --- /dev/null +++ b/ios/MullvadVPN/TunnelManager/RotateKeyOperation.swift @@ -0,0 +1,132 @@ +// +// RotateKeyOperation.swift +// MullvadVPN +// +// Created by pronebird on 15/12/2021. +// Copyright © 2021 Mullvad VPN AB. All rights reserved. +// + +import Foundation +import Logging +import class WireGuardKitTypes.PrivateKey + +class RotateKeyOperation: ResultOperation<TunnelManager.KeyRotationResult, TunnelManager.Error> { + private let state: TunnelManager.State + + private let devicesProxy: REST.DevicesProxy + private var task: Cancellable? + + private let rotationInterval: TimeInterval? + private let logger = Logger(label: "ReplaceKeyOperation") + + init( + dispatchQueue: DispatchQueue, + state: TunnelManager.State, + devicesProxy: REST.DevicesProxy, + rotationInterval: TimeInterval?, + completionHandler: @escaping CompletionHandler + ) { + self.state = state + + self.devicesProxy = devicesProxy + self.rotationInterval = rotationInterval + + super.init( + dispatchQueue: dispatchQueue, + completionQueue: dispatchQueue, + completionHandler: completionHandler + ) + } + + override func main() { + guard let tunnelSettings = state.tunnelSettings else { + finish(completion: .failure(.unsetAccount)) + return + } + + if let rotationInterval = rotationInterval { + let creationDate = tunnelSettings.device.wgKeyData.creationDate + let nextRotationDate = creationDate.addingTimeInterval(rotationInterval) + + if nextRotationDate > Date() { + logger.debug("Throttle private key rotation.") + + finish(completion: .success(.throttled(creationDate))) + return + } else { + logger.debug("Private key is old enough, rotate right away.") + } + } else { + logger.debug("Rotate private key right away.") + } + + logger.debug("Replacing old key with new key on server...") + + let newPrivateKey = PrivateKey() + + task = devicesProxy.rotateDeviceKey( + accountNumber: tunnelSettings.account.number, + identifier: tunnelSettings.device.identifier, + publicKey: newPrivateKey.publicKey, + retryStrategy: .default + ) { completion in + self.dispatchQueue.async { + self.didRotateKey( + tunnelSettings: tunnelSettings, + newPrivateKey: newPrivateKey, + completion: completion + ) + } + } + } + + override func operationDidCancel() { + task?.cancel() + task = nil + } + + private func didRotateKey( + tunnelSettings: TunnelSettingsV2, + newPrivateKey: PrivateKey, + completion: OperationCompletion<REST.Device, REST.Error> + ) + { + let mappedCompletion = completion.mapError { error -> TunnelManager.Error in + logger.error( + chainedError: error, + message: "Failed to rotate device key." + ) + + return .rotateKey(error) + } + + guard let device = mappedCompletion.value else { + finish(completion: mappedCompletion.assertNoSuccess()) + return + } + + logger.debug("Successfully rotated device key. Persisting settings...") + + do { + var newTunnelSettings = tunnelSettings + newTunnelSettings.device.update(from: device) + newTunnelSettings.device.wgKeyData = StoredWgKeyData( + creationDate: Date(), + privateKey: newPrivateKey + ) + + try SettingsManager.writeSettings(newTunnelSettings) + + state.tunnelSettings = newTunnelSettings + + finish(completion: .success(.finished)) + } catch { + logger.error( + chainedError: AnyChainedError(error), + message: "Failed to write settings." + ) + + finish(completion: .failure(.writeSettings(error))) + } + } +} diff --git a/ios/MullvadVPN/TunnelManager/SetAccountOperation.swift b/ios/MullvadVPN/TunnelManager/SetAccountOperation.swift index 92f1d8df14..769b3c1510 100644 --- a/ios/MullvadVPN/TunnelManager/SetAccountOperation.swift +++ b/ios/MullvadVPN/TunnelManager/SetAccountOperation.swift @@ -8,14 +8,39 @@ import Foundation import class WireGuardKitTypes.PublicKey +import class WireGuardKitTypes.PrivateKey import Logging -class SetAccountOperation: ResultOperation<(), TunnelManager.Error> { +enum SetAccountAction { + /// Set new account. + case new + + /// Set existing account. + case existing(String) + + /// Unset account. + case unset + + var taskName: String { + switch self { + case .new: + return "Set new account" + case .existing: + return "Set existing account" + case .unset: + return "Unset account" + } + } +} + +class SetAccountOperation: ResultOperation<StoredAccountData?, TunnelManager.Error> { typealias WillDeleteVPNConfigurationHandler = () -> Void private let state: TunnelManager.State - private let apiProxy: REST.APIProxy - private let accountToken: String? + private let accountsProxy: REST.AccountsProxy + private let devicesProxy: REST.DevicesProxy + private let action: SetAccountAction + private var task: Cancellable? private var willDeleteVPNConfigurationHandler: WillDeleteVPNConfigurationHandler? private let logger = Logger(label: "TunnelManager.SetAccountOperation") @@ -23,15 +48,17 @@ class SetAccountOperation: ResultOperation<(), TunnelManager.Error> { init( dispatchQueue: DispatchQueue, state: TunnelManager.State, - apiProxy: REST.APIProxy, - accountToken: String?, + accountsProxy: REST.AccountsProxy, + devicesProxy: REST.DevicesProxy, + action: SetAccountAction, willDeleteVPNConfigurationHandler: @escaping WillDeleteVPNConfigurationHandler, completionHandler: @escaping CompletionHandler ) { self.state = state - self.apiProxy = apiProxy - self.accountToken = accountToken + self.accountsProxy = accountsProxy + self.devicesProxy = devicesProxy + self.action = action self.willDeleteVPNConfigurationHandler = willDeleteVPNConfigurationHandler super.init( @@ -42,139 +69,245 @@ class SetAccountOperation: ResultOperation<(), TunnelManager.Error> { } override func main() { - execute { completion in - self.finish(completion: completion) + if let tunnelSettings = self.state.tunnelSettings { + self.deleteDevice( + accountNumber: tunnelSettings.account.number, + deviceIdentifier: tunnelSettings.device.identifier + ) + } else { + self.fetchAccountData() } } - private func execute(completionHandler: @escaping CompletionHandler) { - // Delete current account key and configuration if set. - if let tunnelInfo = state.tunnelInfo, tunnelInfo.token != accountToken { - let currentAccountToken = tunnelInfo.token - let currentPublicKey = tunnelInfo.tunnelSettings.interface.publicKey - let nextPublicKey = tunnelInfo.tunnelSettings.interface.nextPrivateKey?.publicKey + private func deleteDevice(accountNumber: String, deviceIdentifier: String) { + logger.debug("Delete current device...") - logger.debug("Unset current account token.") + task = devicesProxy.deleteDevice( + accountNumber: accountNumber, + identifier: deviceIdentifier, + retryStrategy: .default, + completion: { [weak self] completion in + self?.dispatchQueue.async { + self?.didDeleteDevice(completion) + } + }) + } - let publicKeys = [currentPublicKey, nextPublicKey].compactMap { $0 } + private func didDeleteDevice(_ completion: OperationCompletion<Bool, REST.Error>) { + let mappedCompletion = completion.mapError { error -> TunnelManager.Error in + logger.error(chainedError: error, message: "Failed to delete device.") - deletePublicKeys(publicKeys, accountToken: currentAccountToken) { - self.deleteKeychainEntryAndVPNConfiguration(accountToken: currentAccountToken) { - self.setNewAccount(completionHandler: completionHandler) - } - } - } else { - setNewAccount(completionHandler: completionHandler) + return .deleteDevice(error) } - } - private func setNewAccount(completionHandler: @escaping CompletionHandler) { - guard let accountToken = accountToken else { - logger.debug("Account token is unset.") - completionHandler(.success(())) + guard let isDeleted = mappedCompletion.value else { + finish(completion: mappedCompletion.assertNoSuccess()) return } - logger.debug("Set new account token.") + if isDeleted { + logger.debug("Deleted device.") + } else { + logger.debug("Device is already deleted.") + } - // Check Keychain for leftover settings from previous installation and attempt to remove - // previous WirGuard keys before proceeding. - switch TunnelSettingsManager.load(searchTerm: .accountToken(accountToken)) { - case .success(let keychainEntry): - let interfaceSettings = keychainEntry.tunnelSettings.interface + state.tunnelSettings = nil - logger.debug("Found leftover tunnel settings in Keychain.") + deleteSettingsAndVPNConfiguration { error in + if let error = error { + self.finish(completion: .failure(error)) + } else { + self.fetchAccountData() + } + } + } - let publicKeys = [interfaceSettings.publicKey, interfaceSettings.nextPrivateKey?.publicKey] - .compactMap { $0 } + private func fetchAccountData() { + switch action { + case .unset: + logger.debug("Account number is unset.") - deletePublicKeys(publicKeys, accountToken: accountToken) { - self.addTunnelSettingsAndPushKey(accountToken: accountToken, completionHandler: completionHandler) + finish(completion: .success(nil)) + + case .new: + logger.debug("Create new account...") + + task = accountsProxy.createAccount(retryStrategy: .default) { [weak self] completion in + self?.dispatchQueue.async { + self?.didCreateAccount(completion: completion) + } } - // Explicit return. - return + case .existing(let accountNumber): + logger.debug("Request account data...") - case .failure(.lookupEntry(.itemNotFound)): - break + task = accountsProxy.getAccountData( + accountNumber: accountNumber, + retryStrategy: .default, + completion: { [weak self] completion in + self?.dispatchQueue.async { + self?.didReceiveAccountData(accountNumber: accountNumber, completion: completion) + } + }) + } + } + + private func didCreateAccount(completion: OperationCompletion<REST.NewAccountData, REST.Error>) { + let mappedCompletion = completion.mapError { error -> TunnelManager.Error in + logger.error( + chainedError: AnyChainedError(error), + message: "Failed to create new account." + ) - case .failure(let error): - logger.error(chainedError: error, message: "Failed to read leftover tunnel settings.") + return .createAccount(error) + } + + guard let newAccountData = mappedCompletion.value else { + finish(completion: mappedCompletion.assertNoSuccess()) + return } - addTunnelSettingsAndPushKey(accountToken: accountToken, completionHandler: completionHandler) + logger.debug("Created new account. Updating settings...") + + createDevice( + storedAccountData: StoredAccountData( + identifier: newAccountData.id, + number: newAccountData.number, + expiry: newAccountData.expiry + ) + ) } - private func addTunnelSettingsAndPushKey(accountToken: String, completionHandler: @escaping CompletionHandler) { - switch addTunnelSettings(accountToken: accountToken) { - case .success(let tunnelSettings): - self.pushNewAccountKey( - accountToken: accountToken, - publicKey: tunnelSettings.interface.publicKey, - completionHandler: completionHandler + private func didReceiveAccountData(accountNumber: String, completion: OperationCompletion<REST.AccountData, REST.Error>) { + let mappedCompletion = completion.mapError { error -> TunnelManager.Error in + logger.error( + chainedError: AnyChainedError(error), + message: "Failed to receive account data." ) - case .failure(let error): - logger.error(chainedError: error, message: "Failed to add tunnel settings for new account.") - completionHandler(.failure(error)) + return .getAccountData(error) } - } - private func addTunnelSettings(accountToken: String) -> Result<TunnelSettings, TunnelManager.Error> { - return TunnelSettingsManager.remove(searchTerm: .accountToken(accountToken)) - .flatMapError { error in - if case .removeEntry(.itemNotFound) = error { - return .success(()) - } else { - return .failure(.removeTunnelSettings(error)) - } - } - .flatMap { _ in - let defaultSettings = TunnelSettings() + guard let accountData = mappedCompletion.value else { + finish(completion: mappedCompletion.assertNoSuccess()) + return + } - return TunnelSettingsManager.add(configuration: defaultSettings, account: accountToken) - .map { _ in - return defaultSettings - } - .mapError { error in - return .addTunnelSettings(error) - } - } + logger.debug("Received account data.") + + createDevice( + storedAccountData: StoredAccountData( + identifier: accountData.id, + number: accountNumber, + expiry: accountData.expiry + ) + ) } - private func deletePublicKeys(_ publicKeys: [PublicKey], accountToken: String, completionHandler: @escaping () -> Void) { - let dispatchGroup = DispatchGroup() + private func createDevice(storedAccountData: StoredAccountData) { + logger.debug("Store last used account.") - for (index, publicKey) in publicKeys.enumerated() { - dispatchGroup.enter() - _ = apiProxy.deleteWireguardKey(accountNumber: accountToken, publicKey: publicKey, retryStrategy: .default) { result in - self.dispatchQueue.async { - switch result { - case .success: - self.logger.info("Removed key (\(index)) from server.") + do { + try SettingsManager.setLastUsedAccount(storedAccountData.number) + } catch { + logger.error( + chainedError: AnyChainedError(error), + message: "Failed to store last used account number." + ) + } - case .failure(.unhandledResponse(_, let serverErrorResponse)) - where serverErrorResponse?.code == .publicKeyNotFound: - self.logger.debug("Key (\(index)) was not found on server.") + logger.debug("Create device...") - case .failure(let error): - self.logger.error(chainedError: error, message: "Failed to delete key (\(index)) on server.") + let privateKey = PrivateKey() - case .cancelled: - self.logger.debug("Cancelled public key deletion.") - } + let request = REST.CreateDeviceRequest( + publicKey: privateKey.publicKey, + hijackDNS: false + ) - dispatchGroup.leave() + task = devicesProxy.createDevice( + accountNumber: storedAccountData.number, + request: request, + retryStrategy: .default, + completion: { [weak self] completion in + self?.dispatchQueue.async { + self?.didCreateDevice( + storedAccountData: storedAccountData, + privateKey: privateKey, + completion: completion + ) } - } + }) + } + + private func didCreateDevice( + storedAccountData: StoredAccountData, + privateKey: PrivateKey, + completion: OperationCompletion<REST.Device, REST.Error> + ) + { + let mappedCompletion = completion.mapError { error -> TunnelManager.Error in + logger.error(chainedError: error, message: "Failed to create device.") + return .createDevice(error) } - dispatchGroup.notify(queue: dispatchQueue) { - completionHandler() + guard let device = mappedCompletion.value else { + finish(completion: mappedCompletion.assertNoSuccess()) + return + } + + logger.debug("Created device. Saving settings...") + + let tunnelSettings = TunnelSettingsV2( + account: storedAccountData, + device: StoredDeviceData( + creationDate: device.created, + identifier: device.id, + name: device.name, + hijackDNS: device.hijackDNS, + ipv4Address: device.ipv4Address, + ipv6Address: device.ipv6Address, + wgKeyData: StoredWgKeyData( + creationDate: Date(), + privateKey: privateKey + ) + ), + relayConstraints: RelayConstraints(), + dnsSettings: DNSSettings() + ) + + do { + try SettingsManager.writeSettings(tunnelSettings) + + state.tunnelSettings = tunnelSettings + + finish(completion: .success(storedAccountData)) + } catch { + logger.error( + chainedError: AnyChainedError(error), + message: "Failed to write settings." + ) + finish(completion: .failure(.writeSettings(error))) } } - private func deleteKeychainEntryAndVPNConfiguration(accountToken: String, completionHandler: @escaping () -> Void) { + private func deleteSettingsAndVPNConfiguration( + completionHandler: @escaping (TunnelManager.Error?) -> Void + ) { + // Delete keychain entry. + do { + try SettingsManager.deleteSettings() + } catch .itemNotFound as KeychainError { + logger.debug("Settings are already deleted.") + } catch { + logger.error( + chainedError: AnyChainedError(error), + message: "Failed to delete settings." + ) + completionHandler(.deleteSettings(error)) + return + } + // Tell the caller to unsubscribe from VPN status notifications. willDeleteVPNConfigurationHandler?() willDeleteVPNConfigurationHandler = nil @@ -182,23 +315,12 @@ class SetAccountOperation: ResultOperation<(), TunnelManager.Error> { // Reset tunnel state to disconnected state.tunnelStatus.reset(to: .disconnected) - // Remove tunnel info - state.tunnelInfo = nil - - // Remove settings from Keychain - if case .failure(let error) = TunnelSettingsManager.remove(searchTerm: .accountToken(accountToken)) { - // Ignore Keychain errors because that normally means that the Keychain - // configuration was already removed and we shouldn't be blocking the - // user from logging out - logger.error( - chainedError: error, - message: "Failed to delete old account settings." - ) - } + // Remove tunnel settins + state.tunnelSettings = nil // Finish immediately if tunnel provider is not set. guard let tunnel = state.tunnel else { - completionHandler() + completionHandler(nil) return } @@ -215,62 +337,8 @@ class SetAccountOperation: ResultOperation<(), TunnelManager.Error> { self.state.setTunnel(nil, shouldRefreshTunnelState: false) - completionHandler() - } - } - } - - private func pushNewAccountKey(accountToken: String, publicKey: PublicKey, completionHandler: @escaping CompletionHandler) { - _ = apiProxy.pushWireguardKey(accountNumber: accountToken, publicKey: publicKey, retryStrategy: .default) { result in - self.dispatchQueue.async { - switch result { - case .success(let associatedAddresses): - self.logger.debug("Pushed new key to server.") - - self.saveAssociatedAddresses(associatedAddresses, accountToken: accountToken, newPrivateKey: nil, completionHandler: completionHandler) - - case .failure(let error): - self.logger.error(chainedError: error, message: "Failed to push new key to server.") - - completionHandler(.failure(.pushWireguardKey(error))) - - case .cancelled: - self.logger.debug("Cancelled new key push to server.") - - completionHandler(.cancelled) - } + completionHandler(nil) } } } - - private func saveAssociatedAddresses(_ associatedAddresses: REST.WireguardAddressesResponse, accountToken: String, newPrivateKey: PrivateKeyWithMetadata?, completionHandler: @escaping (OperationCompletion<(), TunnelManager.Error>) -> Void) { - let saveResult = TunnelSettingsManager.update(searchTerm: .accountToken(accountToken)) { tunnelSettings in - tunnelSettings.interface.addresses = [ - associatedAddresses.ipv4Address, - associatedAddresses.ipv6Address - ] - - if let newPrivateKey = newPrivateKey { - tunnelSettings.interface.privateKey = newPrivateKey - tunnelSettings.interface.nextPrivateKey = nil - } - } - - switch saveResult { - case .success(let newTunnelSettings): - logger.debug("Saved associated addresses.") - - state.tunnelInfo = TunnelInfo( - token: accountToken, - tunnelSettings: newTunnelSettings - ) - - completionHandler(.success(())) - - case .failure(let error): - logger.error(chainedError: error, message: "Failed to save associated addresses.") - - completionHandler(.failure(.updateTunnelSettings(error))) - } - } } diff --git a/ios/MullvadVPN/TunnelManager/StartTunnelOperation.swift b/ios/MullvadVPN/TunnelManager/StartTunnelOperation.swift index 8b72dbcea7..0bcfa6458b 100644 --- a/ios/MullvadVPN/TunnelManager/StartTunnelOperation.swift +++ b/ios/MullvadVPN/TunnelManager/StartTunnelOperation.swift @@ -33,8 +33,8 @@ class StartTunnelOperation: ResultOperation<(), TunnelManager.Error> { } override func main() { - guard let tunnelInfo = state.tunnelInfo else { - self.finish(completion: .failure(.unsetAccount)) + guard let tunnelSettings = state.tunnelSettings else { + finish(completion: .failure(.unsetAccount)) return } @@ -50,7 +50,7 @@ class StartTunnelOperation: ResultOperation<(), TunnelManager.Error> { switch readResult { case .success(let cachedRelays): self.didReceiveRelays( - tunnelInfo: tunnelInfo, + tunnelSettings: tunnelSettings, cachedRelays: cachedRelays ) @@ -66,10 +66,10 @@ class StartTunnelOperation: ResultOperation<(), TunnelManager.Error> { } } - private func didReceiveRelays(tunnelInfo: TunnelInfo, cachedRelays: RelayCache.CachedRelays) { + private func didReceiveRelays(tunnelSettings: TunnelSettingsV2, cachedRelays: RelayCache.CachedRelays) { let selectorResult = RelaySelector.evaluate( relays: cachedRelays.relays, - constraints: tunnelInfo.tunnelSettings.relayConstraints + constraints: tunnelSettings.relayConstraints ) guard let selectorResult = selectorResult else { @@ -77,13 +77,16 @@ class StartTunnelOperation: ResultOperation<(), TunnelManager.Error> { return } - Self.makeTunnelProvider(accountToken: tunnelInfo.token) { makeTunnelProviderResult in + Self.makeTunnelProvider { makeTunnelProviderResult in self.dispatchQueue.async { switch makeTunnelProviderResult { case .success(let tunnelProvider): let startTunnelResult = Result { try self.startTunnel(tunnelProvider: tunnelProvider, selectorResult: selectorResult) } + .mapError { error -> TunnelManager.Error in + return .startVPNTunnel(error) + } - self.finish(completion: OperationCompletion(result: startTunnelResult.mapError { .startVPNTunnel($0) })) + self.finish(completion: OperationCompletion(result: startTunnelResult)) case .failure(let error): self.finish(completion: .failure(error)) @@ -109,22 +112,27 @@ class StartTunnelOperation: ResultOperation<(), TunnelManager.Error> { try tunnelProvider.connection.startVPNTunnel(options: tunnelOptions.rawOptions()) } - private class func makeTunnelProvider(accountToken: String, completionHandler: @escaping (Result<TunnelProviderManagerType, TunnelManager.Error>) -> Void) { + private class func makeTunnelProvider(completionHandler: @escaping (Result<TunnelProviderManagerType, TunnelManager.Error>) -> Void) { TunnelProviderManagerType.loadAllFromPreferences { tunnelProviders, error in if let error = error { completionHandler(.failure(.loadAllVPNConfigurations(error))) return } - let result = Self.setupTunnelProvider( - accountToken: accountToken, - tunnels: tunnelProviders - ) + let protocolConfig = NETunnelProviderProtocol() + protocolConfig.providerBundleIdentifier = ApplicationConfiguration.packetTunnelExtensionIdentifier + protocolConfig.serverAddress = "" - guard case .success(let tunnelProvider) = result else { - completionHandler(result) - return - } + let tunnelProvider = tunnelProviders?.first ?? TunnelProviderManagerType() + tunnelProvider.isEnabled = true + tunnelProvider.localizedDescription = "WireGuard" + tunnelProvider.protocolConfiguration = protocolConfig + + // Enable on-demand VPN, always connect the tunnel when on Wi-Fi or cellular. + let alwaysOnRule = NEOnDemandRuleConnect() + alwaysOnRule.interfaceTypeMatch = .any + tunnelProvider.onDemandRules = [alwaysOnRule] + tunnelProvider.isOnDemandEnabled = true tunnelProvider.saveToPreferences { error in if let error = error { @@ -146,34 +154,4 @@ class StartTunnelOperation: ResultOperation<(), TunnelManager.Error> { } } } - - private class func setupTunnelProvider(accountToken: String, tunnels: [TunnelProviderManagerType]?) -> Result<TunnelProviderManagerType, TunnelManager.Error> { - // Request persistent keychain reference to tunnel settings - return TunnelSettingsManager.getPersistentKeychainReference(account: accountToken) - .mapError { error in - return .obtainPersistentKeychainReference(error) - } - .map { passwordReference in - // Get the first available tunnel or make a new one - let tunnelProvider = tunnels?.first ?? TunnelProviderManagerType() - - let protocolConfig = NETunnelProviderProtocol() - protocolConfig.providerBundleIdentifier = ApplicationConfiguration.packetTunnelExtensionIdentifier - protocolConfig.serverAddress = "" - protocolConfig.username = accountToken - protocolConfig.passwordReference = passwordReference - - tunnelProvider.isEnabled = true - tunnelProvider.localizedDescription = "WireGuard" - tunnelProvider.protocolConfiguration = protocolConfig - - // Enable on-demand VPN, always connect the tunnel when on Wi-Fi or cellular - let alwaysOnRule = NEOnDemandRuleConnect() - alwaysOnRule.interfaceTypeMatch = .any - tunnelProvider.onDemandRules = [alwaysOnRule] - tunnelProvider.isOnDemandEnabled = true - - return tunnelProvider - } - } } diff --git a/ios/MullvadVPN/TunnelManager/TunnelInfo.swift b/ios/MullvadVPN/TunnelManager/TunnelInfo.swift deleted file mode 100644 index e16ab22632..0000000000 --- a/ios/MullvadVPN/TunnelManager/TunnelInfo.swift +++ /dev/null @@ -1,18 +0,0 @@ -// -// TunnelInfo.swift -// TunnelInfo -// -// Created by pronebird on 10/09/2021. -// Copyright © 2021 Mullvad VPN AB. All rights reserved. -// - -import Foundation - -/// Struct that holds current account token and tunnel settings. -struct TunnelInfo: Equatable { - /// Mullvad account token - var token: String - - /// Tunnel settings - var tunnelSettings: TunnelSettings -} diff --git a/ios/MullvadVPN/TunnelManager/TunnelManager.swift b/ios/MullvadVPN/TunnelManager/TunnelManager.swift index 2b216b409e..66e26f0488 100644 --- a/ios/MullvadVPN/TunnelManager/TunnelManager.swift +++ b/ios/MullvadVPN/TunnelManager/TunnelManager.swift @@ -10,6 +10,7 @@ import BackgroundTasks import Foundation import NetworkExtension import UIKit +import StoreKit import Logging import class WireGuardKitTypes.PublicKey @@ -47,12 +48,16 @@ final class TunnelManager: TunnelManagerStateDelegate { } static let shared: TunnelManager = { - return TunnelManager(apiProxy: REST.ProxyFactory.shared.createAPIProxy()) + return TunnelManager( + accountsProxy: REST.ProxyFactory.shared.createAccountsProxy(), + devicesProxy: REST.ProxyFactory.shared.createDevicesProxy() + ) }() // MARK: - Internal variables - private let apiProxy: REST.APIProxy + private let accountsProxy: REST.AccountsProxy + private let devicesProxy: REST.DevicesProxy private let logger = Logger(label: "TunnelManager") private let stateQueue = DispatchQueue(label: "TunnelManager.stateQueue") @@ -72,18 +77,37 @@ final class TunnelManager: TunnelManagerStateDelegate { private var isPolling = false private var lastConnectingDate: Date? - var tunnelInfo: TunnelInfo? { - return state.tunnelInfo + var accountNumber: String? { + return state.tunnelSettings?.account.number + } + + var accountExpiry: Date? { + return state.tunnelSettings?.account.expiry + } + + var isAccountSet: Bool { + return state.tunnelSettings != nil + } + + var device: StoredDeviceData? { + return state.tunnelSettings?.device + } + + var tunnelSettings: TunnelSettingsV2? { + return state.tunnelSettings } var tunnelState: TunnelState { return state.tunnelStatus.state } - private init(apiProxy: REST.APIProxy) { - self.apiProxy = apiProxy + private init(accountsProxy: REST.AccountsProxy, devicesProxy: REST.DevicesProxy) { + self.accountsProxy = accountsProxy + self.devicesProxy = devicesProxy self.state = TunnelManager.State(queue: stateQueue) self.state.delegate = self + self.operationQueue.name = "TunnelManager.operationQueue" + self.operationQueue.underlyingQueue = stateQueue NotificationCenter.default.addObserver( self, @@ -125,9 +149,12 @@ final class TunnelManager: TunnelManagerStateDelegate { guard self.isRunningPeriodicPrivateKeyRotation else { return } - if let tunnelInfo = self.state.tunnelInfo { - let creationDate = tunnelInfo.tunnelSettings.interface.privateKey.creationDate - let scheduleDate = Date(timeInterval: TunnelManagerConfiguration.privateKeyRotationInterval, since: creationDate) + if let tunnelSettings = state.tunnelSettings { + let creationDate = tunnelSettings.device.wgKeyData.creationDate + let scheduleDate = Date( + timeInterval: TunnelManagerConfiguration.privateKeyRotationInterval, + since: creationDate + ) schedulePrivateKeyRotationTimer(scheduleDate) } else { @@ -171,12 +198,17 @@ final class TunnelManager: TunnelManagerStateDelegate { // MARK: - Public methods - /// Initialize the TunnelManager with the tunnel from the system. - /// - /// The given account token is used to ensure that the system tunnel was configured for the same - /// account. The system tunnel is removed in case of inconsistency. - func loadTunnel(accountToken: String?, completionHandler: @escaping (TunnelManager.Error?) -> Void) { - let operation = LoadTunnelOperation(dispatchQueue: stateQueue, state: state, accountToken: accountToken) { [weak self] completion in + func loadConfiguration(completionHandler: @escaping (TunnelManager.Error?) -> Void) { + let migrateSettingsOperation = MigrateSettingsOperation( + dispatchQueue: stateQueue, + accountsProxy: accountsProxy, + devicesProxy: devicesProxy + ) + + let loadTunnelOperation = LoadTunnelConfigurationOperation( + dispatchQueue: stateQueue, + state: state + ) { [weak self] completion in guard let self = self else { return } dispatchPrecondition(condition: .onQueue(self.stateQueue)) @@ -192,17 +224,31 @@ final class TunnelManager: TunnelManagerStateDelegate { } } - let backgroundTaskIdentifier = UIApplication.shared.beginBackgroundTask(withName: "Load tunnel") { - operation.cancel() + let backgroundTaskIdentifier = UIApplication.shared.beginBackgroundTask( + withName: "Load tunnel configuration" + ) { + // no-op } - operation.completionBlock = { + loadTunnelOperation.completionBlock = { UIApplication.shared.endBackgroundTask(backgroundTaskIdentifier) } - exclusivityController.addOperation(operation, categories: [OperationCategory.manageTunnelProvider, OperationCategory.changeTunnelSettings]) + exclusivityController.addOperation(migrateSettingsOperation, categories: [ + OperationCategory.changeTunnelSettings + ]) - operationQueue.addOperation(operation) + exclusivityController.addOperation(loadTunnelOperation, categories: [ + OperationCategory.manageTunnelProvider, + OperationCategory.changeTunnelSettings + ]) + + loadTunnelOperation.addDependency(migrateSettingsOperation) + + operationQueue.addOperations([ + migrateSettingsOperation, + loadTunnelOperation + ], waitUntilFinished: false) } func startTunnel() { @@ -241,7 +287,10 @@ final class TunnelManager: TunnelManagerStateDelegate { } func stopTunnel() { - let operation = StopTunnelOperation(dispatchQueue: stateQueue, state: state) { [weak self] completion in + let operation = StopTunnelOperation( + dispatchQueue: stateQueue, + state: state + ) { [weak self] completion in guard let self = self else { return } dispatchPrecondition(condition: .onQueue(self.stateQueue)) @@ -309,14 +358,38 @@ final class TunnelManager: TunnelManagerStateDelegate { operationQueue.addOperation(operation) } - func setAccount(accountToken: String, completionHandler: @escaping (TunnelManager.Error?) -> Void) { - let operation = makeSetAccountOperation(accountToken: accountToken) { completion in - DispatchQueue.main.async { - completionHandler(completion.error) - } - } + func setAccount(action: SetAccountAction, completionHandler: @escaping (OperationCompletion<StoredAccountData?, TunnelManager.Error>) -> Void) { + let operation = SetAccountOperation( + dispatchQueue: stateQueue, + state: state, + accountsProxy: accountsProxy, + devicesProxy: devicesProxy, + action: action, + willDeleteVPNConfigurationHandler: { [weak self] in + guard let self = self else { return } + + dispatchPrecondition(condition: .onQueue(self.stateQueue)) - let backgroundTaskIdentifier = UIApplication.shared.beginBackgroundTask(withName: "Set tunnel account") { + // Unregister from receiving VPN connection status changes + self.unsubscribeVPNStatusObserver() + + // Cancel last VPN status mapping operation + self.lastMapConnectionStatusOperation?.cancel() + self.lastMapConnectionStatusOperation = nil + }, + completionHandler: { [weak self] completion in + guard let self = self else { return } + + dispatchPrecondition(condition: .onQueue(self.stateQueue)) + + self.updatePrivateKeyRotationTimer() + + DispatchQueue.main.async { + completionHandler(completion) + } + }) + + let backgroundTaskIdentifier = UIApplication.shared.beginBackgroundTask(withName: action.taskName) { operation.cancel() } @@ -324,19 +397,33 @@ final class TunnelManager: TunnelManagerStateDelegate { UIApplication.shared.endBackgroundTask(backgroundTaskIdentifier) } - exclusivityController.addOperation(operation, categories: [OperationCategory.manageTunnelProvider, OperationCategory.changeTunnelSettings]) + exclusivityController.addOperation(operation, categories: [ + OperationCategory.manageTunnelProvider, + OperationCategory.changeTunnelSettings + ]) operationQueue.addOperation(operation) } func unsetAccount(completionHandler: @escaping () -> Void) { - let operation = makeSetAccountOperation(accountToken: nil) { _ in - DispatchQueue.main.async { - completionHandler() - } + setAccount(action: .unset) { _ in + completionHandler() } + } + + func updateAccountData(_ completionHandler: ((TunnelManager.Error?) -> Void)? = nil) { + let operation = UpdateAccountDataOperation( + dispatchQueue: stateQueue, + state: state, + accountsProxy: accountsProxy + ) - let backgroundTaskIdentifier = UIApplication.shared.beginBackgroundTask(withName: "Unset tunnel account") { + operation.completionQueue = .main + operation.completionHandler = { completion in + completionHandler?(completion.error) + } + + let backgroundTaskIdentifier = UIApplication.shared.beginBackgroundTask(withName: "Update account data") { operation.cancel() } @@ -344,13 +431,47 @@ final class TunnelManager: TunnelManagerStateDelegate { UIApplication.shared.endBackgroundTask(backgroundTaskIdentifier) } - exclusivityController.addOperation(operation, categories: [OperationCategory.manageTunnelProvider, OperationCategory.changeTunnelSettings]) + exclusivityController.addOperation(operation, categories: [ + OperationCategory.changeTunnelSettings + ]) operationQueue.addOperation(operation) } + func updateDeviceData(_ completionHandler: @escaping (OperationCompletion<StoredDeviceData, TunnelManager.Error>) -> Void) -> Cancellable { + let operation = UpdateDeviceDataOperation( + dispatchQueue: stateQueue, + state: state, + devicesProxy: devicesProxy + ) + + operation.completionQueue = .main + operation.completionHandler = completionHandler + + let backgroundTaskIdentifier = UIApplication.shared.beginBackgroundTask(withName: "Update device data") { + operation.cancel() + } + + operation.completionBlock = { + UIApplication.shared.endBackgroundTask(backgroundTaskIdentifier) + } + + exclusivityController.addOperation(operation, categories: [ + OperationCategory.changeTunnelSettings + ]) + + operationQueue.addOperation(operation) + + return operation + } + func regeneratePrivateKey(completionHandler: ((TunnelManager.Error?) -> Void)? = nil) { - let operation = ReplaceKeyOperation.operationForKeyRegeneration(dispatchQueue: stateQueue, state: state, apiProxy: apiProxy) { [weak self] completion in + let operation = RotateKeyOperation( + dispatchQueue: stateQueue, + state: state, + devicesProxy: devicesProxy, + rotationInterval: nil + ) { [weak self] completion in guard let self = self else { return } dispatchPrecondition(condition: .onQueue(self.stateQueue)) @@ -386,10 +507,10 @@ final class TunnelManager: TunnelManagerStateDelegate { } func rotatePrivateKey(completionHandler: @escaping (OperationCompletion<KeyRotationResult, TunnelManager.Error>) -> Void) -> Cancellable { - let operation = ReplaceKeyOperation.operationForKeyRotation( + let operation = RotateKeyOperation( dispatchQueue: stateQueue, state: state, - apiProxy: apiProxy, + devicesProxy: devicesProxy, rotationInterval: TunnelManagerConfiguration.privateKeyRotationInterval ) { [weak self] completion in guard let self = self else { return } @@ -445,7 +566,7 @@ final class TunnelManager: TunnelManagerStateDelegate { scheduleTunnelSettingsUpdate( taskName: "Set DNS settings", modificationBlock: { tunnelSettings in - tunnelSettings.interface.dnsSettings = newDNSSettings + tunnelSettings.dnsSettings = newDNSSettings }, completionHandler: completionHandler ) @@ -467,10 +588,10 @@ final class TunnelManager: TunnelManagerStateDelegate { // MARK: - TunnelManagerStateDelegate - func tunnelManagerState(_ state: TunnelManager.State, didChangeTunnelInfo newTunnelInfo: TunnelInfo?) { + func tunnelManagerState(_ state: TunnelManager.State, didChangeTunnelSettings newTunnelSettinggs: TunnelSettingsV2?) { DispatchQueue.main.async { - self.observerList.forEach { (observer) in - observer.tunnelManager(self, didUpdateTunnelSettings: newTunnelInfo) + self.observerList.forEach { observer in + observer.tunnelManager(self, didUpdateTunnelSettings: newTunnelSettinggs) } } } @@ -545,7 +666,11 @@ final class TunnelManager: TunnelManagerStateDelegate { private func updateTunnelStatus(_ connectionStatus: NEVPNStatus) { dispatchPrecondition(condition: .onQueue(stateQueue)) - let operation = MapConnectionStatusOperation(queue: stateQueue, state: state, connectionStatus: connectionStatus) { [weak self] in + let operation = MapConnectionStatusOperation( + queue: stateQueue, + state: state, + connectionStatus: connectionStatus + ) { [weak self] in guard let self = self else { return } dispatchPrecondition(condition: .onQueue(self.stateQueue)) @@ -569,60 +694,38 @@ final class TunnelManager: TunnelManagerStateDelegate { } } - private func makeSetAccountOperation(accountToken: String?, completionHandler: @escaping (OperationCompletion<(), TunnelManager.Error>) -> Void) -> Operation { - return SetAccountOperation( - dispatchQueue: stateQueue, - state: state, - apiProxy: apiProxy, - accountToken: accountToken, - willDeleteVPNConfigurationHandler: { [weak self] in - guard let self = self else { return } - - dispatchPrecondition(condition: .onQueue(self.stateQueue)) - - // Unregister from receiving VPN connection status changes - self.unsubscribeVPNStatusObserver() - - // Cancel last VPN status mapping operation - self.lastMapConnectionStatusOperation?.cancel() - self.lastMapConnectionStatusOperation = nil - }, - completionHandler: { [weak self] completion in - guard let self = self else { return } - - dispatchPrecondition(condition: .onQueue(self.stateQueue)) - - self.updatePrivateKeyRotationTimer() - - completionHandler(completion) - }) - } + fileprivate func scheduleTunnelSettingsUpdate(taskName: String, modificationBlock: @escaping (inout TunnelSettingsV2) -> Void, completionHandler: @escaping (TunnelManager.Error?) -> Void) { + let operation = ResultBlockOperation<Void, TunnelManager.Error>( + dispatchQueue: stateQueue + ) { operation in + guard var tunnelSettings = self.tunnelSettings else { + operation.finish(completion: .failure(.unsetAccount)) + return + } - private func scheduleTunnelSettingsUpdate(taskName: String, modificationBlock: @escaping (inout TunnelSettings) -> Void, completionHandler: @escaping (TunnelManager.Error?) -> Void) { - let operation = SetTunnelSettingsOperation( - dispatchQueue: stateQueue, - state: state, - modificationBlock: modificationBlock, - completionHandler: { [weak self] completion in - guard let self = self else { return } + do { + modificationBlock(&tunnelSettings) - dispatchPrecondition(condition: .onQueue(self.stateQueue)) + try SettingsManager.writeSettings(tunnelSettings) - switch completion { - case .success: - self.reconnectTunnel(completionHandler: nil) + self.state.tunnelSettings = tunnelSettings + self.reconnectTunnel(completionHandler: nil) - case .failure(let error): - self.logger.error(chainedError: error, message: "Failed to set tunnel settings.") + operation.finish(completion: .success(())) + } catch { + self.logger.error( + chainedError: AnyChainedError(error), + message: "Failed to write settings." + ) - case .cancelled: - break - } + operation.finish(completion: .failure(.writeSettings(error))) + } + } - DispatchQueue.main.async { - completionHandler(completion.error) - } - }) + operation.completionQueue = .main + operation.completionHandler = { completion in + completionHandler(completion.error) + } let backgroundTaskIdentifier = UIApplication.shared.beginBackgroundTask(withName: taskName) { operation.cancel() @@ -753,29 +856,29 @@ extension TunnelManager { } /// Schedule background task relative to the private key creation date. - func scheduleBackgroundTask() -> Result<(), TunnelManager.Error> { - if let tunnelInfo = self.state.tunnelInfo { - let creationDate = tunnelInfo.tunnelSettings.interface.privateKey.creationDate - let beginDate = Date(timeInterval: TunnelManagerConfiguration.privateKeyRotationInterval, since: creationDate) - - return submitBackgroundTask(at: beginDate) - } else { - return .failure(.unsetAccount) + func scheduleBackgroundTask() throws { + guard let tunnelSettings = state.tunnelSettings else { + throw Error.unsetAccount } + + let creationDate = tunnelSettings.device.wgKeyData.creationDate + let beginDate = Date( + timeInterval: TunnelManagerConfiguration.privateKeyRotationInterval, + since: creationDate + ) + + return try submitBackgroundTask(at: beginDate) } /// Create and submit task request to scheduler. - private func submitBackgroundTask(at beginDate: Date) -> Result<(), TunnelManager.Error> { + private func submitBackgroundTask(at beginDate: Date) throws { let taskIdentifier = ApplicationConfiguration.privateKeyRotationTaskIdentifier let request = BGProcessingTaskRequest(identifier: taskIdentifier) request.earliestBeginDate = beginDate request.requiresNetworkConnectivity = true - return Result { try BGTaskScheduler.shared.submit(request) } - .mapError { error in - return .backgroundTaskScheduler(error) - } + try BGTaskScheduler.shared.submit(request) } /// Background task handler. @@ -785,12 +888,17 @@ extension TunnelManager { let cancellableTask = rotatePrivateKey { completion in if let scheduleDate = self.handlePrivateKeyRotationCompletion(completion) { // Schedule next background task - switch self.submitBackgroundTask(at: scheduleDate) { - case .success: - self.logger.debug("Scheduled next private key rotation task at \(scheduleDate.logFormatDate())") + do { + try self.submitBackgroundTask(at: scheduleDate) - case .failure(let error): - self.logger.error(chainedError: error, message: "Failed to schedule next private key rotation task.") + self.logger.debug( + "Scheduled next private key rotation task at \(scheduleDate.logFormatDate())" + ) + } catch { + self.logger.error( + chainedError: AnyChainedError(error), + message: "Failed to schedule next private key rotation task." + ) } } @@ -845,7 +953,7 @@ extension TunnelManager { // Do not retry if logged out. return nil - case .replaceWireguardKey(.unhandledResponse(_, let serverErrorResponse)) + case .rotateKey(.unhandledResponse(_, let serverErrorResponse)) where serverErrorResponse?.code == .invalidAccount: // Do not retry if account was removed. return nil @@ -855,3 +963,39 @@ extension TunnelManager { } } } + +extension TunnelManager: AppStorePaymentObserver { + func appStorePaymentManager(_ manager: AppStorePaymentManager, + transaction: SKPaymentTransaction?, + payment: SKPayment, + accountToken: String?, + didFailWithError error: AppStorePaymentManager.Error + ) + { + // no-op + } + + func appStorePaymentManager(_ manager: AppStorePaymentManager, + transaction: SKPaymentTransaction, + accountToken: String, + didFinishWithResponse response: REST.CreateApplePaymentResponse + ) + { + scheduleTunnelSettingsUpdate( + taskName: "Update account expiry after in-app purchase", + modificationBlock: { tunnelSettings in + if tunnelSettings.account.number == accountToken { + tunnelSettings.account.expiry = response.newExpiry + } + }, + completionHandler: { error in + guard let error = error else { return } + + self.logger.error( + chainedError: error, + message: "Failed to update account expiry after in-app purchase" + ) + } + ) + } +} diff --git a/ios/MullvadVPN/TunnelManager/TunnelManagerError.swift b/ios/MullvadVPN/TunnelManager/TunnelManagerError.swift index 4f0b346417..fb1fd03c7f 100644 --- a/ios/MullvadVPN/TunnelManager/TunnelManagerError.swift +++ b/ios/MullvadVPN/TunnelManager/TunnelManagerError.swift @@ -14,62 +14,58 @@ extension TunnelManager { /// Account is unset. case unsetAccount - /// A failure to start the VPN tunnel via system call. + /// Failure to start the VPN tunnel via system call. case startVPNTunnel(Swift.Error) - /// A failure to load the system VPN configurations created by the app. + /// Failure to load the system VPN configurations created by the app. case loadAllVPNConfigurations(Swift.Error) - /// A failure to save the system VPN configuration. + /// Failure to save the system VPN configuration. case saveVPNConfiguration(Swift.Error) - /// A failure to reload the system VPN configuration. + /// Failure to reload the system VPN configuration. case reloadVPNConfiguration(Swift.Error) - /// A failure to remove the system VPN configuration. + /// Failure to remove the system VPN configuration. case removeVPNConfiguration(Swift.Error) - /// A failure to perform a recovery (by removing the VPN configuration) when a corrupt - /// VPN configuration is detected. - case removeInconsistentVPNConfiguration(Swift.Error) + /// Failure to read settings. + case readSettings(Swift.Error) - /// A failure to read tunnel settings. - case readTunnelSettings(TunnelSettingsManager.Error) + /// Failure to write settings. + case writeSettings(Swift.Error) - /// A failure to read relays cache. + /// Failure to delete settings. + case deleteSettings(Swift.Error) + + /// Failure to read relays cache. case readRelays(RelayCache.Error) - /// A failure to find a relay satisfying the given constraints. + /// Failure to find a relay satisfying the given constraints. case cannotSatisfyRelayConstraints - /// A failure to add the tunnel settings. - case addTunnelSettings(TunnelSettingsManager.Error) - - /// A failure to update the tunnel settings. - case updateTunnelSettings(TunnelSettingsManager.Error) - - /// A failure to remove the tunnel settings from Keychain. - case removeTunnelSettings(TunnelSettingsManager.Error) + /// Failure to create device. + case createDevice(REST.Error) - /// A failure to migrate tunnel settings. - case migrateTunnelSettings(TunnelSettingsManager.Error) + /// Failure to delete device. + case deleteDevice(REST.Error) - /// Unable to obtain the persistent keychain reference for the tunnel settings. - case obtainPersistentKeychainReference(TunnelSettingsManager.Error) + /// Failure to obtain device data. + case getDevice(REST.Error) - /// A failure to push the public WireGuard key. - case pushWireguardKey(REST.Error) + /// Requested device is already revoked. + case deviceRevoked - /// A failure to replace the public WireGuard key. - case replaceWireguardKey(REST.Error) + /// Failure to obtain account data. + case getAccountData(REST.Error) - /// A failure to remove the public WireGuard key. - case removeWireguardKey(REST.Error) + /// Failure to create account. + case createAccount(REST.Error) - /// A failure to schedule background task. - case backgroundTaskScheduler(Swift.Error) + /// Failure to rotate WireGuard key. + case rotateKey(REST.Error) - /// A failure to reload tunnel. + /// Failure to reload tunnel. case reloadTunnel(TunnelIPC.Error) var errorDescription: String? { @@ -86,32 +82,30 @@ extension TunnelManager { return "Failed to reload the system VPN configuration." case .removeVPNConfiguration: return "Failed to remove the system VPN configuration." - case .removeInconsistentVPNConfiguration: - return "Failed to remove the inconsistent VPN tunnel." - case .readTunnelSettings: - return "Failed to read the tunnel settings." + case .readSettings: + return "Failed to read settings." case .readRelays: return "Failed to read relays." case .cannotSatisfyRelayConstraints: return "Failed to satisfy the relay constraints." - case .addTunnelSettings: - return "Failed to add the tunnel settings." - case .updateTunnelSettings: - return "Failed to update the tunnel settings." - case .removeTunnelSettings: - return "Failed to remove the tunnel settings." - case .migrateTunnelSettings: - return "Failed to migrate the tunnel settings." - case .obtainPersistentKeychainReference: - return "Failed to obtain the persistent keychain reference." - case .pushWireguardKey: - return "Failed to push the WireGuard key to server." - case .replaceWireguardKey: - return "Failed to replace the WireGuard key on server." - case .removeWireguardKey: - return "Failed to remove the WireGuard key from server." - case .backgroundTaskScheduler: - return "Failed to schedule background task." + case .writeSettings: + return "Failed to write settings." + case .deleteSettings: + return "Failed to delete settings." + case .createDevice: + return "Failed to create a device." + case .deleteDevice: + return "Failed to delete a device." + case .getDevice: + return "Failed to obtain device data." + case .deviceRevoked: + return "Requested device is already revoked." + case .getAccountData: + return "Failed to obtain account data." + case .createAccount: + return "Failed to create new account." + case .rotateKey: + return "Failed to rotate WireGuard key." case .reloadTunnel: return "Failed to reload tunnel." } diff --git a/ios/MullvadVPN/TunnelManager/TunnelManagerState.swift b/ios/MullvadVPN/TunnelManager/TunnelManagerState.swift index 3802390376..990e8e482f 100644 --- a/ios/MullvadVPN/TunnelManager/TunnelManagerState.swift +++ b/ios/MullvadVPN/TunnelManager/TunnelManagerState.swift @@ -10,7 +10,7 @@ import Foundation import NetworkExtension protocol TunnelManagerStateDelegate: AnyObject { - func tunnelManagerState(_ state: TunnelManager.State, didChangeTunnelInfo newTunnelInfo: TunnelInfo?) + func tunnelManagerState(_ state: TunnelManager.State, didChangeTunnelSettings newTunnelSettings: TunnelSettingsV2?) func tunnelManagerState(_ state: TunnelManager.State, didChangeTunnelStatus newTunnelStatus: TunnelStatus) func tunnelManagerState(_ state: TunnelManager.State, didChangeTunnelProvider newTunnelObject: Tunnel?, shouldRefreshTunnelState: Bool) } @@ -23,7 +23,7 @@ extension TunnelManager { private let queueMarkerKey = DispatchSpecificKey<Bool>() - private var _tunnelInfo: TunnelInfo? + private var _tunnelSettings: TunnelSettingsV2? private var _tunnelObject: Tunnel? private var _tunnelStatus = TunnelStatus( isNetworkReachable: false, @@ -31,18 +31,18 @@ extension TunnelManager { state: .disconnected ) - var tunnelInfo: TunnelInfo? { + var tunnelSettings: TunnelSettingsV2? { get { return performBlock { - return _tunnelInfo + return _tunnelSettings } } set { performBlock { - if _tunnelInfo != newValue { - _tunnelInfo = newValue + if _tunnelSettings != newValue { + _tunnelSettings = newValue - delegate?.tunnelManagerState(self, didChangeTunnelInfo: newValue) + delegate?.tunnelManagerState(self, didChangeTunnelSettings: newValue) } } } diff --git a/ios/MullvadVPN/TunnelManager/TunnelObserver.swift b/ios/MullvadVPN/TunnelManager/TunnelObserver.swift index 5fefb4a89e..6e1934e30e 100644 --- a/ios/MullvadVPN/TunnelManager/TunnelObserver.swift +++ b/ios/MullvadVPN/TunnelManager/TunnelObserver.swift @@ -10,6 +10,6 @@ import Foundation protocol TunnelObserver: AnyObject { func tunnelManager(_ manager: TunnelManager, didUpdateTunnelState tunnelState: TunnelState) - func tunnelManager(_ manager: TunnelManager, didUpdateTunnelSettings tunnelInfo: TunnelInfo?) + func tunnelManager(_ manager: TunnelManager, didUpdateTunnelSettings tunnelSettings: TunnelSettingsV2?) func tunnelManager(_ manager: TunnelManager, didFailWithError error: TunnelManager.Error) } diff --git a/ios/MullvadVPN/TunnelManager/UpdateAccountDataOperation.swift b/ios/MullvadVPN/TunnelManager/UpdateAccountDataOperation.swift new file mode 100644 index 0000000000..ae0f0dea7a --- /dev/null +++ b/ios/MullvadVPN/TunnelManager/UpdateAccountDataOperation.swift @@ -0,0 +1,87 @@ +// +// UpdateAccountDataOperation.swift +// MullvadVPN +// +// Created by pronebird on 12/05/2022. +// Copyright © 2022 Mullvad VPN AB. All rights reserved. +// + +import Foundation +import Logging + +class UpdateAccountDataOperation: ResultOperation<Void, TunnelManager.Error> { + private let logger = Logger(label: "UpdateAccountDataOperation") + private let state: TunnelManager.State + private let accountsProxy: REST.AccountsProxy + private var task: Cancellable? + + init( + dispatchQueue: DispatchQueue, + state: TunnelManager.State, + accountsProxy: REST.AccountsProxy + ) + { + self.state = state + self.accountsProxy = accountsProxy + + super.init(dispatchQueue: dispatchQueue) + } + + override func main() { + guard let tunnelSettings = state.tunnelSettings else { + finish(completion: .failure(.unsetAccount)) + return + } + + task = accountsProxy.getAccountData( + accountNumber: tunnelSettings.account.number, + retryStrategy: .default + ) { completion in + self.dispatchQueue.async { + self.didReceiveAccountData( + tunnelSettings: tunnelSettings, + completion: completion + ) + } + } + } + + override func operationDidCancel() { + task?.cancel() + task = nil + } + + private func didReceiveAccountData( + tunnelSettings: TunnelSettingsV2, + completion: OperationCompletion<REST.AccountData, REST.Error> + ) + { + let mappedCompletion = completion.mapError { error -> TunnelManager.Error in + self.logger.error( + chainedError: error, + message: "Failed to fetch account expiry." + ) + return .getAccountData(error) + } + + guard let accountData = mappedCompletion.value else { + finish(completion: mappedCompletion.assertNoSuccess()) + return + } + + do { + var newTunnelSettings = tunnelSettings + newTunnelSettings.account.expiry = accountData.expiry + try SettingsManager.writeSettings(newTunnelSettings) + + finish(completion: .success(())) + } catch { + self.logger.error( + chainedError: AnyChainedError(error), + message: "Failed to save account data." + ) + + finish(completion: .failure(.writeSettings(error))) + } + } +} diff --git a/ios/MullvadVPN/TunnelManager/UpdateDeviceDataOperation.swift b/ios/MullvadVPN/TunnelManager/UpdateDeviceDataOperation.swift new file mode 100644 index 0000000000..6c0fd08823 --- /dev/null +++ b/ios/MullvadVPN/TunnelManager/UpdateDeviceDataOperation.swift @@ -0,0 +1,96 @@ +// +// UpdateDeviceDataOperation.swift +// MullvadVPN +// +// Created by pronebird on 13/05/2022. +// Copyright © 2022 Mullvad VPN AB. All rights reserved. +// + +import Foundation +import Logging +import class WireGuardKitTypes.PublicKey + +class UpdateDeviceDataOperation: ResultOperation<StoredDeviceData, TunnelManager.Error> { + private let logger = Logger(label: "UpdateDeviceDataOperation") + + private let state: TunnelManager.State + private let devicesProxy: REST.DevicesProxy + + private var task: Cancellable? + + init( + dispatchQueue: DispatchQueue, + state: TunnelManager.State, + devicesProxy: REST.DevicesProxy + ) + { + self.state = state + self.devicesProxy = devicesProxy + + super.init(dispatchQueue: dispatchQueue) + } + + override func main() { + guard let tunnelSettings = state.tunnelSettings else { + finish(completion: .failure(.unsetAccount)) + return + } + + task = devicesProxy.getDevice( + accountNumber: tunnelSettings.account.number, + identifier: tunnelSettings.device.identifier, + retryStrategy: .default, + completion: { [weak self] completion in + self?.dispatchQueue.async { + self?.didReceiveDeviceResponse( + tunnelSettings: tunnelSettings, + completion: completion + ) + } + }) + } + + override func operationDidCancel() { + task?.cancel() + task = nil + } + + private func didReceiveDeviceResponse( + tunnelSettings: TunnelSettingsV2, + completion: OperationCompletion<REST.Device?, REST.Error> + ) { + let mappedCompletion = completion + .mapError { error -> TunnelManager.Error in + return .getDevice(error) + } + .flatMap { device -> OperationCompletion<REST.Device, TunnelManager.Error> in + if let device = device { + return .success(device) + } else { + return .failure(.deviceRevoked) + } + } + + guard let device = mappedCompletion.value else { + finish(completion: mappedCompletion.assertNoSuccess()) + return + } + + do { + var newTunnelSettings = tunnelSettings + newTunnelSettings.device.update(from: device) + + try SettingsManager.writeSettings(newTunnelSettings) + + finish(completion: .success(newTunnelSettings.device)) + } catch { + logger.error( + chainedError: AnyChainedError(error), + message: "Failed to write settings." + ) + + finish(completion: .failure(.writeSettings(error))) + } + } + +} diff --git a/ios/MullvadVPN/WireguardKeysViewController.swift b/ios/MullvadVPN/WireguardKeysViewController.swift index f3fad9cbc6..71b5f9e5b9 100644 --- a/ios/MullvadVPN/WireguardKeysViewController.swift +++ b/ios/MullvadVPN/WireguardKeysViewController.swift @@ -11,10 +11,10 @@ import UIKit import Logging /// A UI refresh interval for the public key creation date (in seconds) -private let kCreationDateRefreshInterval = Int(60) +private let creationDateRefreshInterval = Int(60) /// A maximum number of characters to display out of the entire public key representation -private let kDisplayPublicKeyMaxLength = 20 +private let displayPublicKeyMaxLength = 20 private enum WireguardKeysViewState { case `default` @@ -34,7 +34,7 @@ class WireguardKeysViewController: UIViewController, TunnelObserver { private var publicKeyPeriodicUpdateTimer: DispatchSourceTimer? private var copyToPasteboardWork: DispatchWorkItem? - private var verifyKeyCancellable: Cancellable? + private var updateDeviceTask: Cancellable? private let alertPresenter = AlertPresenter() private var state: WireguardKeysViewState = .default { @@ -47,8 +47,6 @@ class WireguardKeysViewController: UIViewController, TunnelObserver { return .lightContent } - private let apiProxy = REST.ProxyFactory.shared.createAPIProxy() - override func viewDidLoad() { super.viewDidLoad() @@ -82,16 +80,16 @@ class WireguardKeysViewController: UIViewController, TunnelObserver { contentView.verifyKeyButton.addTarget(self, action: #selector(handleVerifyKey(_:)), for: .touchUpInside) TunnelManager.shared.addObserver(self) - updatePublicKey(tunnelSettings: TunnelManager.shared.tunnelInfo?.tunnelSettings, animated: false) + updatePublicKey(deviceData: TunnelManager.shared.device, animated: false) startPublicKeyPeriodicUpdate() } private func startPublicKeyPeriodicUpdate() { - let interval = DispatchTimeInterval.seconds(kCreationDateRefreshInterval) + let interval = DispatchTimeInterval.seconds(creationDateRefreshInterval) let timerSource = DispatchSource.makeTimerSource(queue: .main) timerSource.setEventHandler { [weak self] () -> Void in - self?.updatePublicKey(tunnelSettings: TunnelManager.shared.tunnelInfo?.tunnelSettings, animated: true) + self?.updatePublicKey(deviceData: TunnelManager.shared.device, animated: true) } timerSource.schedule(deadline: .now() + interval, repeating: interval) timerSource.activate() @@ -105,8 +103,8 @@ class WireguardKeysViewController: UIViewController, TunnelObserver { // no-op } - func tunnelManager(_ manager: TunnelManager, didUpdateTunnelSettings tunnelInfo: TunnelInfo?) { - self.updatePublicKey(tunnelSettings: tunnelInfo?.tunnelSettings, animated: true) + func tunnelManager(_ manager: TunnelManager, didUpdateTunnelSettings tunnelSettings: TunnelSettingsV2?) { + updatePublicKey(deviceData: tunnelSettings?.device, animated: true) } func tunnelManager(_ manager: TunnelManager, didFailWithError error: TunnelManager.Error) { @@ -116,20 +114,16 @@ class WireguardKeysViewController: UIViewController, TunnelObserver { // MARK: - Actions private func copyPublicKey() { - guard let tunnelInfo = TunnelManager.shared.tunnelInfo else { return } - - let metadata = tunnelInfo.tunnelSettings.interface.privateKey.publicKeyWithMetadata + guard let tunnelSettings = TunnelManager.shared.tunnelSettings else { return } - UIPasteboard.general.string = metadata.stringRepresentation() + UIPasteboard.general.string = tunnelSettings.device.wgKeyData.privateKey.publicKey.base64Key setPublicKeyTitle( string: NSLocalizedString("COPIED_TO_PASTEBOARD_LABEL", tableName: "WireguardKeys", comment: ""), animated: true) let workItem = DispatchWorkItem { [weak self] in - let tunnelSettings = TunnelManager.shared.tunnelInfo?.tunnelSettings - - self?.updatePublicKey(tunnelSettings: tunnelSettings, animated: true) + self?.updatePublicKey(deviceData: TunnelManager.shared.device, animated: true) } DispatchQueue.main.asyncAfter(wallDeadline: .now() + .seconds(3), execute: workItem) @@ -161,13 +155,16 @@ class WireguardKeysViewController: UIViewController, TunnelObserver { contentView.creationRowView.value = formatKeyGenerationElapsedTime(with: creationDate) ?? "-" } - private func updatePublicKey(tunnelSettings: TunnelSettings?, animated: Bool) { - if let publicKey = tunnelSettings?.interface.privateKey.publicKeyWithMetadata { - let displayKey = publicKey - .stringRepresentation(maxLength: kDisplayPublicKeyMaxLength) + private func updatePublicKey(deviceData: StoredDeviceData?, animated: Bool) { + if let wgKeyData = deviceData?.wgKeyData { + let displayKey = wgKeyData.privateKey + .publicKey + .base64Key + .prefix(displayPublicKeyMaxLength) + .appending("...") setPublicKeyTitle(string: displayKey, animated: animated) - updateCreationDateLabel(with: publicKey.creationDate) + updateCreationDateLabel(with: wgKeyData.creationDate) } else { setPublicKeyTitle(string: "-", animated: animated) contentView.creationRowView.value = "-" @@ -209,26 +206,19 @@ class WireguardKeysViewController: UIViewController, TunnelObserver { } private func verifyKey() { - guard let tunnelInfo = TunnelManager.shared.tunnelInfo else { return } - updateViewState(.verifyingKey) - verifyKeyCancellable?.cancel() + updateDeviceTask?.cancel() - verifyKeyCancellable = apiProxy.getWireguardKey( - accountNumber: tunnelInfo.token, - publicKey: tunnelInfo.tunnelSettings.interface.publicKey, - retryStrategy: .default - ) { [weak self] result in + updateDeviceTask = TunnelManager.shared.updateDeviceData { [weak self] completion in guard let self = self else { return } - switch result { + switch completion { case .success: self.updateViewState(.verifiedKey(true)) case .failure(let error): - if case .unhandledResponse(_, let serverErrorResponse) = error, - serverErrorResponse?.code == .publicKeyNotFound { + if case .deviceRevoked = error { self.updateViewState(.verifiedKey(false)) } else { self.showKeyVerificationFailureAlert(error) @@ -254,26 +244,39 @@ class WireguardKeysViewController: UIViewController, TunnelObserver { } } - private func showKeyVerificationFailureAlert(_ error: REST.Error) { + private func showKeyVerificationFailureAlert(_ error: TunnelManager.Error) { let reason = error.errorChainDescription ?? "" let errorDescription = String( format: NSLocalizedString( "VERIFY_KEY_FAILURE_ALERT_MESSAGE", tableName: "WireguardKeys", - value: "Failed to verify the WireGuard key on server: %@", + value: "Failed to verify the WireGuard key: %@", comment: "" ), reason ) let alertController = UIAlertController( - title: NSLocalizedString("VERIFY_KEY_FAILURE_ALERT_TITLE", tableName: "WireguardKeys", comment: ""), + title: NSLocalizedString( + "VERIFY_KEY_FAILURE_ALERT_TITLE", + tableName: "WireguardKeys", + value: "Cannot verify the key", + comment: "" + ), message: errorDescription, preferredStyle: .alert ) alertController.addAction( - UIAlertAction(title: NSLocalizedString("VERIFY_KEY_FAILURE_ALERT_OK_ACTION", tableName: "WireguardKeys", comment: ""), style: .cancel) + UIAlertAction( + title: NSLocalizedString( + "VERIFY_KEY_FAILURE_ALERT_OK_ACTION", + tableName: "WireguardKeys", + value: "OK", + comment: "" + ), + style: .cancel + ) ) alertPresenter.enqueue(alertController, presentingController: self) @@ -281,12 +284,25 @@ class WireguardKeysViewController: UIViewController, TunnelObserver { private func showKeyRegenerationFailureAlert(_ error: TunnelManager.Error) { let alertController = UIAlertController( - title: NSLocalizedString("REGENERATE_KEY_FAILURE_ALERT_TITLE", tableName: "WireguardKeys", comment: ""), + title: NSLocalizedString( + "REGENERATE_KEY_FAILURE_ALERT_TITLE", + tableName: "WireguardKeys", + value: "Cannot regenerate the key", + comment: "" + ), message: error.errorChainDescription, preferredStyle: .alert ) alertController.addAction( - UIAlertAction(title: NSLocalizedString("REGENERATE_KEY_FAILURE_ALERT_OK_ACTION", tableName: "WireguardKeys", comment: ""), style: .cancel) + UIAlertAction( + title: NSLocalizedString( + "REGENERATE_KEY_FAILURE_ALERT_OK_ACTION", + tableName: "WireguardKeys", + value: "OK", + comment: "" + ), + style: .cancel + ) ) alertPresenter.enqueue(alertController, presentingController: self) |
