summaryrefslogtreecommitdiffhomepage
diff options
context:
space:
mode:
-rw-r--r--ios/MullvadVPN.xcodeproj/project.pbxproj44
-rw-r--r--ios/MullvadVPN/Account.swift292
-rw-r--r--ios/MullvadVPN/AccountViewController.swift45
-rw-r--r--ios/MullvadVPN/AppDelegate.swift111
-rw-r--r--ios/MullvadVPN/ConnectViewController.swift25
-rw-r--r--ios/MullvadVPN/DisplayChainedError.swift175
-rw-r--r--ios/MullvadVPN/LoginViewController.swift69
-rw-r--r--ios/MullvadVPN/Notifications/AccountExpiryNotificationProvider.swift22
-rw-r--r--ios/MullvadVPN/Notifications/TunnelErrorNotificationProvider.swift2
-rw-r--r--ios/MullvadVPN/PreferencesViewController.swift6
-rw-r--r--ios/MullvadVPN/ProblemReportViewController.swift2
-rw-r--r--ios/MullvadVPN/SettingsDataSource.swift45
-rw-r--r--ios/MullvadVPN/SettingsNavigationController.swift2
-rw-r--r--ios/MullvadVPN/TunnelManager/LoadTunnelConfigurationOperation.swift128
-rw-r--r--ios/MullvadVPN/TunnelManager/MigrateSettingsOperation.swift249
-rw-r--r--ios/MullvadVPN/TunnelManager/ReplaceKeyOperation.swift204
-rw-r--r--ios/MullvadVPN/TunnelManager/RotateKeyOperation.swift132
-rw-r--r--ios/MullvadVPN/TunnelManager/SetAccountOperation.swift404
-rw-r--r--ios/MullvadVPN/TunnelManager/StartTunnelOperation.swift70
-rw-r--r--ios/MullvadVPN/TunnelManager/TunnelInfo.swift18
-rw-r--r--ios/MullvadVPN/TunnelManager/TunnelManager.swift364
-rw-r--r--ios/MullvadVPN/TunnelManager/TunnelManagerError.swift104
-rw-r--r--ios/MullvadVPN/TunnelManager/TunnelManagerState.swift14
-rw-r--r--ios/MullvadVPN/TunnelManager/TunnelObserver.swift2
-rw-r--r--ios/MullvadVPN/TunnelManager/UpdateAccountDataOperation.swift87
-rw-r--r--ios/MullvadVPN/TunnelManager/UpdateDeviceDataOperation.swift96
-rw-r--r--ios/MullvadVPN/WireguardKeysViewController.swift94
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)