diff options
| author | Andrej Mihajlov <and@mullvad.net> | 2022-05-30 15:00:28 +0200 |
|---|---|---|
| committer | Andrej Mihajlov <and@mullvad.net> | 2022-05-30 15:00:28 +0200 |
| commit | 5eb10755ed05d7d64e21978281976b74736574b5 (patch) | |
| tree | 6d4c83af1d0b559f8bb94b1b44cf8191a4e34bd7 | |
| parent | 07e672f7585e7f89ae1e833405c76ea16b82787f (diff) | |
| parent | 01290fbb01050b24e3b49fc7ecc6db4a2fd93a06 (diff) | |
| download | mullvadvpn-5eb10755ed05d7d64e21978281976b74736574b5.tar.xz mullvadvpn-5eb10755ed05d7d64e21978281976b74736574b5.zip | |
Merge branch 'integrate-device-api'
59 files changed, 2482 insertions, 2817 deletions
diff --git a/ios/MullvadVPN.xcodeproj/project.pbxproj b/ios/MullvadVPN.xcodeproj/project.pbxproj index bf644e9dc1..0e1af93ed2 100644 --- a/ios/MullvadVPN.xcodeproj/project.pbxproj +++ b/ios/MullvadVPN.xcodeproj/project.pbxproj @@ -8,13 +8,7 @@ /* Begin PBXBuildFile section */ 5801C9A527A14B2A0031566A /* TunnelManagerState.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5801C9A427A14B2A0031566A /* TunnelManagerState.swift */; }; - 5806766D27048E5500C858CB /* KeychainMatchLimit.swift in Sources */ = {isa = PBXBuildFile; fileRef = 58FAEDFC24533A5500CB0F5B /* KeychainMatchLimit.swift */; }; - 5806766E27048E5600C858CB /* KeychainMatchLimit.swift in Sources */ = {isa = PBXBuildFile; fileRef = 58FAEDFC24533A5500CB0F5B /* KeychainMatchLimit.swift */; }; - 5806767927048E8800C858CB /* Keychain.swift in Sources */ = {isa = PBXBuildFile; fileRef = 58FAEDF6245088E100CB0F5B /* Keychain.swift */; }; - 5806767A27048E8800C858CB /* Keychain.swift in Sources */ = {isa = PBXBuildFile; fileRef = 58FAEDF6245088E100CB0F5B /* Keychain.swift */; }; - 5806767B27048E8900C858CB /* Keychain.swift in Sources */ = {isa = PBXBuildFile; fileRef = 58FAEDF6245088E100CB0F5B /* Keychain.swift */; }; 5806767C27048E9B00C858CB /* PacketTunnelProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = 58CE5E7B224146470008646E /* PacketTunnelProvider.swift */; }; - 5806768127048EE000C858CB /* KeychainMatchLimit.swift in Sources */ = {isa = PBXBuildFile; fileRef = 58FAEDFC24533A5500CB0F5B /* KeychainMatchLimit.swift */; }; 5807483B27DB8A980020ECBF /* WireGuardKitTypes in Frameworks */ = {isa = PBXBuildFile; productRef = 5807483A27DB8A980020ECBF /* WireGuardKitTypes */; }; 5807E2C02432038B00F5FF30 /* String+Split.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5807E2BF2432038B00F5FF30 /* String+Split.swift */; }; 5807E2C2243203D000F5FF30 /* StringTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5807E2C1243203D000F5FF30 /* StringTests.swift */; }; @@ -28,6 +22,10 @@ 58095C592762155700890776 /* RESTRetryStrategy.swift in Sources */ = {isa = PBXBuildFile; fileRef = 58095C582762155700890776 /* RESTRetryStrategy.swift */; }; 580EE20624B3222200F9D8A1 /* ExclusivityController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 580EE20524B3222200F9D8A1 /* ExclusivityController.swift */; }; 580EE22424B3243100F9D8A1 /* AsyncBlockOperation.swift in Sources */ = {isa = PBXBuildFile; fileRef = 580EE22324B3243100F9D8A1 /* AsyncBlockOperation.swift */; }; + 580F8B8328197881002E0998 /* TunnelSettingsV2.swift in Sources */ = {isa = PBXBuildFile; fileRef = 580F8B8228197881002E0998 /* TunnelSettingsV2.swift */; }; + 580F8B8428197884002E0998 /* TunnelSettingsV2.swift in Sources */ = {isa = PBXBuildFile; fileRef = 580F8B8228197881002E0998 /* TunnelSettingsV2.swift */; }; + 580F8B8628197958002E0998 /* DNSSettings.swift in Sources */ = {isa = PBXBuildFile; fileRef = 580F8B8528197958002E0998 /* DNSSettings.swift */; }; + 580F8B872819795C002E0998 /* DNSSettings.swift in Sources */ = {isa = PBXBuildFile; fileRef = 580F8B8528197958002E0998 /* DNSSettings.swift */; }; 5811DE50239014550011EB53 /* NEVPNStatus+Debug.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5811DE4F239014550011EB53 /* NEVPNStatus+Debug.swift */; }; 5815039724D6ECAE00C9C50E /* CustomFormatLogHandler.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5815039624D6ECAE00C9C50E /* CustomFormatLogHandler.swift */; }; 5815039824D6ECAE00C9C50E /* CustomFormatLogHandler.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5815039624D6ECAE00C9C50E /* CustomFormatLogHandler.swift */; }; @@ -39,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 */; }; @@ -69,7 +68,6 @@ 582BB1AF229566420055B6EF /* SettingsCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = 582BB1AE229566420055B6EF /* SettingsCell.swift */; }; 582BB1B1229569620055B6EF /* CustomNavigationBar.swift in Sources */ = {isa = PBXBuildFile; fileRef = 582BB1B0229569620055B6EF /* CustomNavigationBar.swift */; }; 582BB1B3229574F40055B6EF /* SettingsAccountCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = 582BB1B2229574F40055B6EF /* SettingsAccountCell.swift */; }; - 582BB1B52295780F0055B6EF /* AccountExpiry.swift in Sources */ = {isa = PBXBuildFile; fileRef = 582BB1B42295780F0055B6EF /* AccountExpiry.swift */; }; 582CFEE726945FC30072883A /* AppStoreSubscriptions.strings in Resources */ = {isa = PBXBuildFile; fileRef = 582CFEE526945FC30072883A /* AppStoreSubscriptions.strings */; }; 582CFEEA269463B80072883A /* Settings.strings in Resources */ = {isa = PBXBuildFile; fileRef = 582CFEE8269463B80072883A /* Settings.strings */; }; 5835B7CC233B76CB0096D79F /* TunnelManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5835B7CB233B76CB0096D79F /* TunnelManager.swift */; }; @@ -81,6 +79,9 @@ 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 */; }; 5846227126E229F20035F7C2 /* AppStoreSubscription.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5846227026E229F20035F7C2 /* AppStoreSubscription.swift */; }; @@ -102,8 +103,6 @@ 584EBDBD2747C98F00A0C9FD /* NSAttributedString+Markdown.swift in Sources */ = {isa = PBXBuildFile; fileRef = 584EBDBC2747C98F00A0C9FD /* NSAttributedString+Markdown.swift */; }; 5850366825A47AC700A43E93 /* IPAddressRange+Codable.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5850366725A47AC700A43E93 /* IPAddressRange+Codable.swift */; }; 5850367F25A481D800A43E93 /* IPAddressRange+Codable.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5850366725A47AC700A43E93 /* IPAddressRange+Codable.swift */; }; - 5850368C25A49E2200A43E93 /* PrivateKeyWithMetadata.swift in Sources */ = {isa = PBXBuildFile; fileRef = 58C6B35322BB87C4003C19AD /* PrivateKeyWithMetadata.swift */; }; - 5850368D25A49E2200A43E93 /* PrivateKeyWithMetadata.swift in Sources */ = {isa = PBXBuildFile; fileRef = 58C6B35322BB87C4003C19AD /* PrivateKeyWithMetadata.swift */; }; 58554F73280AFA5A00013055 /* RESTAuthenticationProxy.swift in Sources */ = {isa = PBXBuildFile; fileRef = 58554F72280AFA5A00013055 /* RESTAuthenticationProxy.swift */; }; 58554F77280AFD5C00013055 /* RESTTaskIdentifier.swift in Sources */ = {isa = PBXBuildFile; fileRef = 58554F76280AFD5C00013055 /* RESTTaskIdentifier.swift */; }; 58554F79280B037400013055 /* RESTAccessTokenManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 58554F78280B037400013055 /* RESTAccessTokenManager.swift */; }; @@ -158,14 +157,14 @@ 5875960A26F371FC00BF6711 /* TunnelIPCSession.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5875960926F371FC00BF6711 /* TunnelIPCSession.swift */; }; 5875960B26F3723000BF6711 /* TunnelIPC.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5845F841236CBACD00B2D93C /* TunnelIPC.swift */; }; 5877153023981F7B001F8237 /* WireguardKeysViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5877152F23981F7B001F8237 /* WireguardKeysViewController.swift */; }; + 5877D70F282137E8002FCFC7 /* SettingsManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 58FF2C02281BDE02009EF542 /* SettingsManager.swift */; }; 58781CC922AE7CA8009B9D8E /* RelayConstraints.swift in Sources */ = {isa = PBXBuildFile; fileRef = 58781CC822AE7CA8009B9D8E /* RelayConstraints.swift */; }; 58781CCE22AE8918009B9D8E /* RelayConstraints.swift in Sources */ = {isa = PBXBuildFile; fileRef = 58781CC822AE7CA8009B9D8E /* RelayConstraints.swift */; }; 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 */; }; - 587AD7C623421D7000E93A53 /* TunnelSettings.swift in Sources */ = {isa = PBXBuildFile; fileRef = 587AD7C523421D7000E93A53 /* TunnelSettings.swift */; }; - 587AD7C723421D8600E93A53 /* TunnelSettings.swift in Sources */ = {isa = PBXBuildFile; fileRef = 587AD7C523421D7000E93A53 /* TunnelSettings.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 */; }; 587B753B2666467500DEF7E9 /* NotificationBannerView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 587B753A2666467500DEF7E9 /* NotificationBannerView.swift */; }; 587B753D2666468F00DEF7E9 /* NotificationController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 587B753C2666468F00DEF7E9 /* NotificationController.swift */; }; @@ -179,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 */; }; @@ -195,9 +193,6 @@ 5891BF1C25E3E3EB006D6FB0 /* Bundle+ProductVersion.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5891BF1B25E3E3EB006D6FB0 /* Bundle+ProductVersion.swift */; }; 5891BF5125E66B1E006D6FB0 /* UIBarButtonItem+KeyboardNavigation.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5891BF5025E66B1E006D6FB0 /* UIBarButtonItem+KeyboardNavigation.swift */; }; 5892A45E265FABFF00890742 /* EmptyTableViewHeaderFooterView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5892A45D265FABFF00890742 /* EmptyTableViewHeaderFooterView.swift */; }; - 5896AE7E246ACE65005B36CB /* KeychainAttributes.swift in Sources */ = {isa = PBXBuildFile; fileRef = 58FAEDEB245059F000CB0F5B /* KeychainAttributes.swift */; }; - 5896AE80246ACE79005B36CB /* KeychainClass.swift in Sources */ = {isa = PBXBuildFile; fileRef = 58FAEE0024533A9C00CB0F5B /* KeychainClass.swift */; }; - 5896AE82246ACE84005B36CB /* KeychainReturn.swift in Sources */ = {isa = PBXBuildFile; fileRef = 58FAEDFE24533A7000CB0F5B /* KeychainReturn.swift */; }; 5896AE84246D5889005B36CB /* CustomDateComponentsFormatting.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5896AE83246D5889005B36CB /* CustomDateComponentsFormatting.swift */; }; 5896AE86246D6AD8005B36CB /* CustomDateComponentsFormattingTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5896AE85246D6AD8005B36CB /* CustomDateComponentsFormattingTests.swift */; }; 5896AE88246D7FAF005B36CB /* CustomDateComponentsFormatting.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5896AE83246D5889005B36CB /* CustomDateComponentsFormatting.swift */; }; @@ -212,8 +207,6 @@ 58ACF64F26567A7100ACE4B7 /* CustomSwitchContainer.swift in Sources */ = {isa = PBXBuildFile; fileRef = 58ACF64E26567A7100ACE4B7 /* CustomSwitchContainer.swift */; }; 58AEEF652344A36000C9BBD5 /* KeychainError.swift in Sources */ = {isa = PBXBuildFile; fileRef = 58AEEF642344A36000C9BBD5 /* KeychainError.swift */; }; 58AEEF662344A37400C9BBD5 /* KeychainError.swift in Sources */ = {isa = PBXBuildFile; fileRef = 58AEEF642344A36000C9BBD5 /* KeychainError.swift */; }; - 58AEEF6B2344A46200C9BBD5 /* TunnelSettingsManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 58AEEF6A2344A46200C9BBD5 /* TunnelSettingsManager.swift */; }; - 58AEEF6C2344A49D00C9BBD5 /* TunnelSettingsManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 58AEEF6A2344A46200C9BBD5 /* TunnelSettingsManager.swift */; }; 58B0A2A8238EE68200BC001D /* RelaySelectorTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 584B26F3237434D00073B10E /* RelaySelectorTests.swift */; }; 58B0A2A9238EE6A100BC001D /* RelayConstraints.swift in Sources */ = {isa = PBXBuildFile; fileRef = 58781CC822AE7CA8009B9D8E /* RelayConstraints.swift */; }; 58B0A2AA238EE6A900BC001D /* RelaySelector.swift in Sources */ = {isa = PBXBuildFile; fileRef = 58781CD422AFBA39009B9D8E /* RelaySelector.swift */; }; @@ -263,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 */; }; @@ -291,18 +284,11 @@ 58F8AC0E25D3F8CE002BE0ED /* ProblemReportReviewViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 58F8AC0D25D3F8CE002BE0ED /* ProblemReportReviewViewController.swift */; }; 58F97A1B280EEBC00050C2FC /* RESTProxyFactory.swift in Sources */ = {isa = PBXBuildFile; fileRef = 58F97A1A280EEBC00050C2FC /* RESTProxyFactory.swift */; }; 58F97A1E280FDE230050C2FC /* RESTRequestHandler.swift in Sources */ = {isa = PBXBuildFile; fileRef = 58F97A1D280FDE230050C2FC /* RESTRequestHandler.swift */; }; - 58FAEDEF245069C700CB0F5B /* KeychainAttributes.swift in Sources */ = {isa = PBXBuildFile; fileRef = 58FAEDEB245059F000CB0F5B /* KeychainAttributes.swift */; }; - 58FAEDF1245069CA00CB0F5B /* KeychainAttributes.swift in Sources */ = {isa = PBXBuildFile; fileRef = 58FAEDEB245059F000CB0F5B /* KeychainAttributes.swift */; }; 58FAEDF4245088B300CB0F5B /* KeychainError.swift in Sources */ = {isa = PBXBuildFile; fileRef = 58AEEF642344A36000C9BBD5 /* KeychainError.swift */; }; - 58FAEDFF24533A7000CB0F5B /* KeychainReturn.swift in Sources */ = {isa = PBXBuildFile; fileRef = 58FAEDFE24533A7000CB0F5B /* KeychainReturn.swift */; }; - 58FAEE0124533A9C00CB0F5B /* KeychainClass.swift in Sources */ = {isa = PBXBuildFile; fileRef = 58FAEE0024533A9C00CB0F5B /* KeychainClass.swift */; }; - 58FAEE0324533ABE00CB0F5B /* KeychainReturn.swift in Sources */ = {isa = PBXBuildFile; fileRef = 58FAEDFE24533A7000CB0F5B /* KeychainReturn.swift */; }; - 58FAEE0424533AC000CB0F5B /* KeychainClass.swift in Sources */ = {isa = PBXBuildFile; fileRef = 58FAEE0024533A9C00CB0F5B /* KeychainClass.swift */; }; 58FB865526E8BF3100F188BC /* AppStorePaymentManagerError.swift in Sources */ = {isa = PBXBuildFile; fileRef = 58FB865426E8BF3100F188BC /* AppStorePaymentManagerError.swift */; }; 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 */; }; @@ -311,6 +297,7 @@ 58FEAFB92750DA2F003C1625 /* AddressCache.swift in Sources */ = {isa = PBXBuildFile; fileRef = 58FEAFB82750DA2F003C1625 /* AddressCache.swift */; }; 58FEEB46260A028D00A621A8 /* GeoJSON.swift in Sources */ = {isa = PBXBuildFile; fileRef = 58FEEB45260A028D00A621A8 /* GeoJSON.swift */; }; 58FEEB58260B662E00A621A8 /* AutomaticKeyboardResponder.swift in Sources */ = {isa = PBXBuildFile; fileRef = 58FEEB57260B662E00A621A8 /* AutomaticKeyboardResponder.swift */; }; + 58FF2C03281BDE02009EF542 /* SettingsManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 58FF2C02281BDE02009EF542 /* SettingsManager.swift */; }; /* End PBXBuildFile section */ /* Begin PBXContainerItemProxy section */ @@ -371,12 +358,15 @@ 58095C582762155700890776 /* RESTRetryStrategy.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RESTRetryStrategy.swift; sourceTree = "<group>"; }; 580EE20524B3222200F9D8A1 /* ExclusivityController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ExclusivityController.swift; sourceTree = "<group>"; }; 580EE22324B3243100F9D8A1 /* AsyncBlockOperation.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AsyncBlockOperation.swift; sourceTree = "<group>"; }; + 580F8B8228197881002E0998 /* TunnelSettingsV2.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TunnelSettingsV2.swift; sourceTree = "<group>"; }; + 580F8B8528197958002E0998 /* DNSSettings.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DNSSettings.swift; sourceTree = "<group>"; }; 5811DE4F239014550011EB53 /* NEVPNStatus+Debug.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "NEVPNStatus+Debug.swift"; sourceTree = "<group>"; }; 5815039324D6EB7200C9C50E /* LogRotation.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LogRotation.swift; sourceTree = "<group>"; }; 5815039624D6ECAE00C9C50E /* CustomFormatLogHandler.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CustomFormatLogHandler.swift; sourceTree = "<group>"; }; 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>"; }; @@ -398,7 +388,6 @@ 582BB1AE229566420055B6EF /* SettingsCell.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SettingsCell.swift; sourceTree = "<group>"; }; 582BB1B0229569620055B6EF /* CustomNavigationBar.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CustomNavigationBar.swift; sourceTree = "<group>"; }; 582BB1B2229574F40055B6EF /* SettingsAccountCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SettingsAccountCell.swift; sourceTree = "<group>"; }; - 582BB1B42295780F0055B6EF /* AccountExpiry.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AccountExpiry.swift; sourceTree = "<group>"; }; 582CFEE626945FC30072883A /* en */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = en; path = en.lproj/AppStoreSubscriptions.strings; sourceTree = "<group>"; }; 582CFEE9269463B80072883A /* en */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = en; path = en.lproj/Settings.strings; sourceTree = "<group>"; }; 5835B7CB233B76CB0096D79F /* TunnelManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TunnelManager.swift; sourceTree = "<group>"; }; @@ -408,6 +397,9 @@ 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>"; }; 5846226426E0D9630035F7C2 /* ProductsRequestOperation.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ProductsRequestOperation.swift; sourceTree = "<group>"; }; @@ -463,8 +455,7 @@ 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>"; }; - 587AD7C523421D7000E93A53 /* TunnelSettings.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = TunnelSettings.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>"; }; 587B753C2666468F00DEF7E9 /* NotificationController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NotificationController.swift; sourceTree = "<group>"; }; @@ -477,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>"; }; @@ -502,7 +492,6 @@ 58ACF64C26567A4F00ACE4B7 /* CustomSwitch.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CustomSwitch.swift; sourceTree = "<group>"; }; 58ACF64E26567A7100ACE4B7 /* CustomSwitchContainer.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CustomSwitchContainer.swift; sourceTree = "<group>"; }; 58AEEF642344A36000C9BBD5 /* KeychainError.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = KeychainError.swift; sourceTree = "<group>"; }; - 58AEEF6A2344A46200C9BBD5 /* TunnelSettingsManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TunnelSettingsManager.swift; sourceTree = "<group>"; }; 58B0A2A0238EE67E00BC001D /* MullvadVPNTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = MullvadVPNTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; }; 58B0A2A4238EE67E00BC001D /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = "<group>"; }; 58B3F30E2742708B00A2DD38 /* HeaderBarButton.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HeaderBarButton.swift; sourceTree = "<group>"; }; @@ -517,7 +506,6 @@ 58BFA5C522A7C97F00A6173D /* RelayCacheTracker.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RelayCacheTracker.swift; sourceTree = "<group>"; }; 58BFA5CB22A7CE1F00A6173D /* ApplicationConfiguration.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ApplicationConfiguration.swift; sourceTree = "<group>"; }; 58C3A4B122456F1A00340BDB /* AccountInputGroupView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AccountInputGroupView.swift; sourceTree = "<group>"; }; - 58C6B35322BB87C4003C19AD /* PrivateKeyWithMetadata.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PrivateKeyWithMetadata.swift; sourceTree = "<group>"; }; 58CB0EDF24B86751001EF0D8 /* RESTAPIProxy.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RESTAPIProxy.swift; sourceTree = "<group>"; }; 58CC40EE24A601900019D96E /* ObserverList.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ObserverList.swift; sourceTree = "<group>"; }; 58CCA00F224249A1004F3011 /* ConnectViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ConnectViewController.swift; sourceTree = "<group>"; }; @@ -554,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>"; }; @@ -580,15 +568,9 @@ 58F8AC0D25D3F8CE002BE0ED /* ProblemReportReviewViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ProblemReportReviewViewController.swift; sourceTree = "<group>"; }; 58F97A1A280EEBC00050C2FC /* RESTProxyFactory.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RESTProxyFactory.swift; sourceTree = "<group>"; }; 58F97A1D280FDE230050C2FC /* RESTRequestHandler.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RESTRequestHandler.swift; sourceTree = "<group>"; }; - 58FAEDEB245059F000CB0F5B /* KeychainAttributes.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = KeychainAttributes.swift; sourceTree = "<group>"; }; - 58FAEDF6245088E100CB0F5B /* Keychain.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Keychain.swift; sourceTree = "<group>"; }; - 58FAEDFC24533A5500CB0F5B /* KeychainMatchLimit.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = KeychainMatchLimit.swift; sourceTree = "<group>"; }; - 58FAEDFE24533A7000CB0F5B /* KeychainReturn.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = KeychainReturn.swift; sourceTree = "<group>"; }; - 58FAEE0024533A9C00CB0F5B /* KeychainClass.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = KeychainClass.swift; sourceTree = "<group>"; }; 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>"; }; @@ -597,6 +579,7 @@ 58FEAFB82750DA2F003C1625 /* AddressCache.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AddressCache.swift; sourceTree = "<group>"; }; 58FEEB45260A028D00A621A8 /* GeoJSON.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = GeoJSON.swift; sourceTree = "<group>"; }; 58FEEB57260B662E00A621A8 /* AutomaticKeyboardResponder.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AutomaticKeyboardResponder.swift; sourceTree = "<group>"; }; + 58FF2C02281BDE02009EF542 /* SettingsManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SettingsManager.swift; sourceTree = "<group>"; }; /* End PBXFileReference section */ /* Begin PBXFrameworksBuildPhase section */ @@ -671,6 +654,17 @@ path = Operations; sourceTree = "<group>"; }; + 580F8B88281A79A7002E0998 /* SettingsManager */ = { + isa = PBXGroup; + children = ( + 58FF2C02281BDE02009EF542 /* SettingsManager.swift */, + 587AD7C523421D7000E93A53 /* TunnelSettingsV1.swift */, + 580F8B8228197881002E0998 /* TunnelSettingsV2.swift */, + 58421033282E4B1500F24E46 /* TunnelSettingsV2+REST.swift */, + ); + path = SettingsManager; + sourceTree = "<group>"; + }; 5815039F24D6ECF200C9C50E /* Logging */ = { isa = PBXGroup; children = ( @@ -689,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>"; @@ -865,9 +860,7 @@ 58CE5E62224146200008646E /* MullvadVPN */ = { isa = PBXGroup; children = ( - 587AD7C92342283900E93A53 /* Account.swift */, 5896CEF126972DEB00B0FAE8 /* AccountContentView.swift */, - 582BB1B42295780F0055B6EF /* AccountExpiry.swift */, 58C3A4B122456F1A00340BDB /* AccountInputGroupView.swift */, 58CCA01D2242787B004F3011 /* AccountTextField.swift */, 582AE30F2440A6CA00E6733A /* AccountTokenInput.swift */, @@ -904,6 +897,7 @@ 587EB66F27143B6500123C75 /* DataSourceSnapshot.swift */, 58907D9424D17B4E00CFC3F5 /* DisconnectSplitButton.swift */, 58B9EB142489139B00095626 /* DisplayChainedError.swift */, + 580F8B8528197958002E0998 /* DNSSettings.swift */, 5892A45D265FABFF00890742 /* EmptyTableViewHeaderFooterView.swift */, 58FEEB45260A028D00A621A8 /* GeoJSON.swift */, 58B3F30E2742708B00A2DD38 /* HeaderBarButton.swift */, @@ -913,7 +907,7 @@ 5840250022B1124600E4CFEC /* IPAddress+Codable.swift */, 5850366725A47AC700A43E93 /* IPAddressRange+Codable.swift */, 58561C98239A5D1500BD6B5E /* IPEndpoint.swift */, - 58FB865626E8C06800F188BC /* Keychain */, + 58AEEF642344A36000C9BBD5 /* KeychainError.swift */, 58727282265D173C00F315B2 /* LaunchScreen.storyboard */, 58E20770274672CA00DE5D77 /* LaunchViewController.swift */, 58A1AA8623F43901009F7EA6 /* Location.swift */, @@ -938,7 +932,6 @@ 587EB6732714520600123C75 /* PreferencesDataSourceDelegate.swift */, 58ACF6482655365700ACE4B7 /* PreferencesViewController.swift */, 587EB671271451E300123C75 /* PreferencesViewModel.swift */, - 58C6B35322BB87C4003C19AD /* PrivateKeyWithMetadata.swift */, 58F8AC0D25D3F8CE002BE0ED /* ProblemReportReviewViewController.swift */, 58EF580A25D69D7A00AEBA94 /* ProblemReportSubmissionOverlayView.swift */, 58293FAC2510CA58005D0BB5 /* ProblemReportViewController.swift */, @@ -959,6 +952,7 @@ 58EE2E38272FF814003BFF93 /* SettingsDataSource.swift */, 58EE2E39272FF814003BFF93 /* SettingsDataSourceDelegate.swift */, 584D26C5270C8741004EA533 /* SettingsDNSTextCell.swift */, + 580F8B88281A79A7002E0998 /* SettingsManager */, 58E6771E24ADFE7800AA26E7 /* SettingsNavigationController.swift */, 584D26C1270C8542004EA533 /* SettingsStaticTextFooterView.swift */, 58ACF64A26553C3F00ACE4B7 /* SettingsSwitchCell.swift */, @@ -974,8 +968,6 @@ 58E0A98727C8F46300FE6BDD /* Tunnel.swift */, 585DA88D26B031D100B8C587 /* TunnelIPC */, 5823FA5726CE4A4100283BF8 /* TunnelManager */, - 587AD7C523421D7000E93A53 /* TunnelSettings.swift */, - 58AEEF6A2344A46200C9BBD5 /* TunnelSettingsManager.swift */, 5891BF5025E66B1E006D6FB0 /* UIBarButtonItem+KeyboardNavigation.swift */, 587CBFE222807F530028DED3 /* UIColor+Helpers.swift */, 58CCA0152242560B004F3011 /* UIColor+Palette.swift */, @@ -1029,19 +1021,6 @@ path = Assets; sourceTree = "<group>"; }; - 58FB865626E8C06800F188BC /* Keychain */ = { - isa = PBXGroup; - children = ( - 58FAEDF6245088E100CB0F5B /* Keychain.swift */, - 58FAEDEB245059F000CB0F5B /* KeychainAttributes.swift */, - 58FAEE0024533A9C00CB0F5B /* KeychainClass.swift */, - 58AEEF642344A36000C9BBD5 /* KeychainError.swift */, - 58FAEDFC24533A5500CB0F5B /* KeychainMatchLimit.swift */, - 58FAEDFE24533A7000CB0F5B /* KeychainReturn.swift */, - ); - path = Keychain; - sourceTree = "<group>"; - }; /* End PBXGroup section */ /* Begin PBXLegacyTarget section */ @@ -1280,15 +1259,12 @@ isa = PBXSourcesBuildPhase; buildActionMask = 2147483647; files = ( - 5896AE80246ACE79005B36CB /* KeychainClass.swift in Sources */, 582AE3132440CA2700E6733A /* AccountTokenInput.swift in Sources */, 58CAF4EF26025954007C5886 /* SimulatorTunnelProvider.swift in Sources */, 58B0A2AA238EE6A900BC001D /* RelaySelector.swift in Sources */, 5896AE86246D6AD8005B36CB /* CustomDateComponentsFormattingTests.swift in Sources */, 5807E2C3243203E700F5FF30 /* String+Split.swift in Sources */, - 5896AE82246ACE84005B36CB /* KeychainReturn.swift in Sources */, 58B0A2A8238EE68200BC001D /* RelaySelectorTests.swift in Sources */, - 5806766D27048E5500C858CB /* KeychainMatchLimit.swift in Sources */, 5819C2152726CC9400D6EC38 /* DataSourceSnapshot.swift in Sources */, 584E96BE240FD4DB00D3334F /* Location.swift in Sources */, 5857F23424C8443700CF6F47 /* AsyncOperation.swift in Sources */, @@ -1300,7 +1276,6 @@ 5857F23824C8446700CF6F47 /* AsyncBlockOperation.swift in Sources */, 582AE3122440CA0D00E6733A /* AccountTokenInputTests.swift in Sources */, 585DA8A526B14EE000B8C587 /* PacketTunnelStatus.swift in Sources */, - 5896AE7E246ACE65005B36CB /* KeychainAttributes.swift in Sources */, 58B0A2A9238EE6A100BC001D /* RelayConstraints.swift in Sources */, 5807E2C2243203D000F5FF30 /* StringTests.swift in Sources */, 5819C2142726CC8D00D6EC38 /* DataSourceSnapshotTests.swift in Sources */, @@ -1311,7 +1286,6 @@ 58A8BE81239FBE62006B74AC /* IPEndpoint.swift in Sources */, 5846227A26E24F1F0035F7C2 /* ExclusivityController.swift in Sources */, 58871D2325D535D2002297FA /* IPAddressRange+Codable.swift in Sources */, - 5806767B27048E8900C858CB /* Keychain.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -1328,8 +1302,10 @@ 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 */, 587EB672271451E300123C75 /* PreferencesViewModel.swift in Sources */, 58095C512760BBB500890776 /* AddressCacheTracker.swift in Sources */, 584D26C6270C8741004EA533 /* SettingsDNSTextCell.swift in Sources */, @@ -1344,9 +1320,8 @@ 58B993B12608A34500BA7811 /* LoginContentView.swift in Sources */, 58E6771F24ADFE7800AA26E7 /* SettingsNavigationController.swift in Sources */, 58A1AA8C23F5584C009F7EA6 /* ConnectionPanelView.swift in Sources */, - 582BB1B52295780F0055B6EF /* AccountExpiry.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 */, @@ -1358,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 */, @@ -1369,11 +1345,10 @@ 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 */, - 58FAEDEF245069C700CB0F5B /* KeychainAttributes.swift in Sources */, 58F2E146276A2C9900A79513 /* StopTunnelOperation.swift in Sources */, 585DA87A26B024F900B8C587 /* RelayCacheError.swift in Sources */, 5856D13727450A8A00DFD627 /* UIImage+TintColor.swift in Sources */, @@ -1392,12 +1367,9 @@ 58293FB3251241B4005D0BB5 /* CustomTextView.swift in Sources */, 58F19E35228C15BA00C7710B /* SpinnerActivityIndicatorView.swift in Sources */, 58A99ED3240014A0006599E9 /* ConsentViewController.swift in Sources */, - 58FAEE0124533A9C00CB0F5B /* KeychainClass.swift in Sources */, 58CCA0162242560B004F3011 /* UIColor+Palette.swift in Sources */, 58095C4F2760BA9100890776 /* AddressCacheStore.swift in Sources */, - 58AEEF6B2344A46200C9BBD5 /* TunnelSettingsManager.swift in Sources */, 587CBFE322807F530028DED3 /* UIColor+Helpers.swift in Sources */, - 5806767927048E8800C858CB /* Keychain.swift in Sources */, 588527B4276B4F2F00BAA373 /* SetAccountOperation.swift in Sources */, 585CA70F25F8C44600B47C62 /* UIMetrics.swift in Sources */, 58F97A1B280EEBC00050C2FC /* RESTProxyFactory.swift in Sources */, @@ -1420,7 +1392,6 @@ 584B17AB27637DE40057F3B8 /* ReloadTunnelOperation.swift in Sources */, 5820676426E771DB00655B05 /* TunnelManagerError.swift in Sources */, 585B4B8726D9098900555C4C /* TunnelErrorNotificationProvider.swift in Sources */, - 5850368C25A49E2200A43E93 /* PrivateKeyWithMetadata.swift in Sources */, 58FEAFB92750DA2F003C1625 /* AddressCache.swift in Sources */, 58B67B482602079E008EF58E /* RelaySelector.swift in Sources */, 58DF28A52417CB4B00E836B0 /* AppStorePaymentManager.swift in Sources */, @@ -1446,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 */, @@ -1460,20 +1430,21 @@ 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 */, + 580F8B8628197958002E0998 /* DNSSettings.swift in Sources */, 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 */, 58554F79280B037400013055 /* RESTAccessTokenManager.swift in Sources */, + 58421034282E4B1500F24E46 /* TunnelSettingsV2+REST.swift in Sources */, 58F2E144276A13F300A79513 /* StartTunnelOperation.swift in Sources */, 5868BD33261DCD2600E6027F /* CustomSplitViewController.swift in Sources */, - 5806766E27048E5600C858CB /* KeychainMatchLimit.swift in Sources */, 58CCA01E2242787B004F3011 /* AccountTextField.swift in Sources */, 586E54FB27A2DF6D0029B88B /* TunnelIPCRequestOperation.swift in Sources */, 584592612639B4A200EF967F /* ConsentContentView.swift in Sources */, @@ -1481,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 */, @@ -1489,7 +1459,7 @@ 585DA89B26B146B300B8C587 /* TunnelIPCCoding.swift in Sources */, 5896AE84246D5889005B36CB /* CustomDateComponentsFormatting.swift in Sources */, 585DA89626B0328000B8C587 /* TunnelIPCResponse.swift in Sources */, - 587AD7C623421D7000E93A53 /* TunnelSettings.swift in Sources */, + 587AD7C623421D7000E93A53 /* TunnelSettingsV1.swift in Sources */, 581503A324D6F1EC00C9C50E /* ChainedError+Logger.swift in Sources */, 58E20771274672CA00DE5D77 /* LaunchViewController.swift in Sources */, 584D26C4270C855B004EA533 /* PreferencesDataSource.swift in Sources */, @@ -1502,11 +1472,11 @@ 5811DE50239014550011EB53 /* NEVPNStatus+Debug.swift in Sources */, 58C3A4B222456F1B00340BDB /* AccountInputGroupView.swift in Sources */, 58F840B22464491D0044E708 /* ChainedError.swift in Sources */, - 58FAEDFF24533A7000CB0F5B /* KeychainReturn.swift in Sources */, 588BCF24280FE43D009ADCEC /* RESTDevicesProxy.swift in Sources */, 58ACF64B26553C3F00ACE4B7 /* SettingsSwitchCell.swift in Sources */, 587EB67027143B6500123C75 /* DataSourceSnapshot.swift in Sources */, 585DA88A26B027A300B8C587 /* RESTCoding.swift in Sources */, + 580F8B8328197881002E0998 /* TunnelSettingsV2.swift in Sources */, 587B753D2666468F00DEF7E9 /* NotificationController.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; @@ -1517,20 +1487,16 @@ files = ( 5850366825A47AC700A43E93 /* IPAddressRange+Codable.swift in Sources */, 58FB865F26EA2E6D00F188BC /* LogFormatting.swift in Sources */, - 5806767A27048E8800C858CB /* Keychain.swift in Sources */, 585DA89726B0328000B8C587 /* TunnelIPCResponse.swift in Sources */, - 5806768127048EE000C858CB /* KeychainMatchLimit.swift in Sources */, 587C575426D2615F005EF767 /* PacketTunnelOptions.swift in Sources */, - 58FAEE0324533ABE00CB0F5B /* KeychainReturn.swift in Sources */, 58BFA5CD22A7CE1F00A6173D /* ApplicationConfiguration.swift in Sources */, - 5850368D25A49E2200A43E93 /* PrivateKeyWithMetadata.swift in Sources */, 5820675826E652AF00655B05 /* RelayCacheIO.swift in Sources */, 584D26C0270C550E004EA533 /* AnyIPAddress.swift in Sources */, 5820675726E652A600655B05 /* REST.swift in Sources */, 585DA88F26B031E200B8C587 /* TunnelIPCCoding.swift in Sources */, 5806767C27048E9B00C858CB /* PacketTunnelProvider.swift in Sources */, 585DA89426B0323E00B8C587 /* TunnelIPCRequest.swift in Sources */, - 587AD7C723421D8600E93A53 /* TunnelSettings.swift in Sources */, + 587AD7C723421D8600E93A53 /* TunnelSettingsV1.swift in Sources */, 5875960B26F3723000BF6711 /* TunnelIPC.swift in Sources */, 58AEEF662344A37400C9BBD5 /* KeychainError.swift in Sources */, 582AD44127BE6178002A6BFC /* CodingErrors+ChainedError.swift in Sources */, @@ -1538,16 +1504,15 @@ 58FC040A27B3EE03001C21F0 /* TunnelMonitor.swift in Sources */, 5838318B27C40A3900000571 /* Pinger.swift in Sources */, 5820675C26E6576800655B05 /* RelayCache.swift in Sources */, - 58FAEDF1245069CA00CB0F5B /* KeychainAttributes.swift in Sources */, 585DA89A26B0329200B8C587 /* PacketTunnelStatus.swift in Sources */, 585DA88526B0270700B8C587 /* ServerRelaysResponse.swift in Sources */, 581503A724D6F4AE00C9C50E /* Logging.swift in Sources */, - 58FAEE0424533AC000CB0F5B /* KeychainClass.swift in Sources */, - 58AEEF6C2344A49D00C9BBD5 /* TunnelSettingsManager.swift in Sources */, + 580F8B8428197884002E0998 /* TunnelSettingsV2.swift in Sources */, 581503A424D6F1EC00C9C50E /* ChainedError+Logger.swift in Sources */, 5815039824D6ECAE00C9C50E /* CustomFormatLogHandler.swift in Sources */, 5840250522B11AB700E4CFEC /* MullvadEndpoint.swift in Sources */, 58906DE02445C7A5002F0673 /* NEProviderStopReason+Debug.swift in Sources */, + 580F8B872819795C002E0998 /* DNSSettings.swift in Sources */, 58655DCF27DA0A5D00911834 /* TunnelMonitorConfiguration.swift in Sources */, 5815039E24D6ECE600C9C50E /* TextFileOutputStream.swift in Sources */, 585DA87826B024A900B8C587 /* CachedRelays.swift in Sources */, @@ -1560,6 +1525,7 @@ 58781CCE22AE8918009B9D8E /* RelayConstraints.swift in Sources */, 581503A024D6F01E00C9C50E /* LogRotation.swift in Sources */, 58781CD522AFBA39009B9D8E /* RelaySelector.swift in Sources */, + 5877D70F282137E8002FCFC7 /* SettingsManager.swift in Sources */, 5820675926E652BE00655B05 /* RESTCoding.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; @@ -2165,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/AccountContentView.swift b/ios/MullvadVPN/AccountContentView.swift index 93fb6d3186..da3a8fa85f 100644 --- a/ios/MullvadVPN/AccountContentView.swift +++ b/ios/MullvadVPN/AccountContentView.swift @@ -172,9 +172,9 @@ class AccountExpiryRow: UIView { var value: Date? { didSet { - let expiry = value.flatMap { AccountExpiry(date: $0) } + let expiry = value - if let expiry = expiry, expiry.isExpired { + if let expiry = expiry, expiry <= Date() { let localizedString = NSLocalizedString( "ACCOUNT_OUT_OF_TIME_LABEL", tableName: "Account", @@ -187,7 +187,13 @@ class AccountExpiryRow: UIView { valueLabel.textColor = .dangerColor } else { - let formattedDate = expiry?.formattedDate + let formattedDate = expiry.map { date in + return DateFormatter.localizedString( + from: date, + dateStyle: .medium, + timeStyle: .short + ) + } valueLabel.text = formattedDate accessibilityValue = formattedDate diff --git a/ios/MullvadVPN/AccountExpiry.swift b/ios/MullvadVPN/AccountExpiry.swift deleted file mode 100644 index 4618e736ae..0000000000 --- a/ios/MullvadVPN/AccountExpiry.swift +++ /dev/null @@ -1,34 +0,0 @@ -// -// AccountExpiry.swift -// MullvadVPN -// -// Created by pronebird on 22/05/2019. -// Copyright © 2019 Mullvad VPN AB. All rights reserved. -// - -import Foundation - -class AccountExpiry { - let date: Date - - init(date: Date) { - self.date = date - } - - var isExpired: Bool { - return date <= Date() - } - - var formattedRemainingTime: String? { - return CustomDateComponentsFormatting.localizedString( - from: Date(), - to: date, - unitsStyle: .full - ) - } - - var formattedDate: String { - return DateFormatter.localizedString(from: date, dateStyle: .medium, timeStyle: .short) - } - -} 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 bba086e75d..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() } } @@ -263,18 +250,26 @@ class AppDelegate: UIResponder, UIApplicationDelegate { @available(iOS 13.0, *) private func scheduleBackgroundTasks() { - switch RelayCache.Tracker.shared.scheduleAppRefreshTask() { - case .success: - self.logger?.debug("Scheduled app refresh task.") - case .failure(let error): - self.logger?.error(chainedError: error, message: "Could not schedule app refresh task.") + do { + try RelayCache.Tracker.shared.scheduleAppRefreshTask() + + logger?.debug("Scheduled app refresh task.") + } catch { + logger?.error( + chainedError: AnyChainedError(error), + message: "Could not schedule app refresh task." + ) } - 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 { @@ -282,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() { @@ -317,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() } @@ -339,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() } @@ -357,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 { @@ -372,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 @@ -383,7 +379,7 @@ class AppDelegate: UIResponder, UIApplicationDelegate { } } - if Account.shared.isAgreedToTermsOfService { + if isAgreedToTermsOfService() { showNextController(false) } else { let consentViewController = self.makeConsentController { (consentController) in @@ -428,7 +424,7 @@ class AppDelegate: UIResponder, UIApplicationDelegate { } consentViewController.completionHandler = { (consentViewController) in - Account.shared.agreeToTermsOfService() + setAgreedToTermsOfService() completion(consentViewController) } @@ -478,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) } @@ -534,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: @@ -552,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) } } @@ -594,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 { @@ -788,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 } } @@ -823,7 +825,7 @@ extension AppDelegate: UISplitViewControllerDelegate { extension AppDelegate: UNUserNotificationCenterDelegate { func userNotificationCenter(_ center: UNUserNotificationCenter, didReceive response: UNNotificationResponse, withCompletionHandler completionHandler: @escaping () -> Void) { - if response.notification.request.identifier == kAccountExpiryNotificationIdentifier, + if response.notification.request.identifier == accountExpiryNotificationIdentifier, response.actionIdentifier == UNNotificationDefaultActionIdentifier { rootContainer?.showSettings(navigateTo: .account, animated: true) } @@ -840,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/AppStorePaymentManager/AppStorePaymentManager.swift b/ios/MullvadVPN/AppStorePaymentManager/AppStorePaymentManager.swift index 885ad3cbad..e94a6e906b 100644 --- a/ios/MullvadVPN/AppStorePaymentManager/AppStorePaymentManager.swift +++ b/ios/MullvadVPN/AppStorePaymentManager/AppStorePaymentManager.swift @@ -29,6 +29,7 @@ class AppStorePaymentManager: NSObject, SKPaymentTransactionObserver { }() private let apiProxy = REST.ProxyFactory.shared.createAPIProxy() + private let accountsProxy = REST.ProxyFactory.shared.createAccountsProxy() private let exclusivityController = ExclusivityController() @@ -111,16 +112,19 @@ class AppStorePaymentManager: NSObject, SKPaymentTransactionObserver { } func addPayment(_ payment: SKPayment, for accountToken: String) { - var cancellableTask: Cancellable? + var task: Cancellable? let backgroundTaskIdentifier = UIApplication.shared.beginBackgroundTask(withName: "Validate account token") { - cancellableTask?.cancel() + task?.cancel() } // Validate account token before adding new payment to the queue. - cancellableTask = apiProxy.getAccountExpiry(accountNumber: accountToken, retryStrategy: .default) { result in + task = accountsProxy.getAccountData( + accountNumber: accountToken, + retryStrategy: .default + ) { completion in dispatchPrecondition(condition: .onQueue(.main)) - switch result { + switch completion { case .success: self.associateAccountToken(accountToken, and: payment) self.paymentQueue.add(payment) 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/ConsolidatedApplicationLog.swift b/ios/MullvadVPN/ConsolidatedApplicationLog.swift index e31b6b6371..bd11fac1c9 100644 --- a/ios/MullvadVPN/ConsolidatedApplicationLog.swift +++ b/ios/MullvadVPN/ConsolidatedApplicationLog.swift @@ -15,7 +15,6 @@ private let kRedactedAccountPlaceholder = "[REDACTED ACCOUNT NUMBER]" private let kRedactedContainerPlaceholder = "[REDACTED CONTAINER PATH]" class ConsolidatedApplicationLog: TextOutputStreamable { - typealias Metadata = KeyValuePairs<MetadataKey, String> enum MetadataKey: String { @@ -108,13 +107,13 @@ class ConsolidatedApplicationLog: TextOutputStreamable { let path = fileURL.path let redactedPath = redact(string: path) - switch Self.readFileLossy(path: path, maxBytes: kLogMaxReadBytes) { - case .success(let lossyString): + do { + let lossyString = try Self.readFileLossy(path: path, maxBytes: kLogMaxReadBytes) let redactedString = redact(string: lossyString) - logs.append(LogAttachment(label: redactedPath, content: redactedString)) - case .failure(let error): - addError(message: redactedPath, error: error) + logs.append(LogAttachment(label: redactedPath, content: redactedString)) + } catch { + addError(message: redactedPath, error: AnyChainedError(error)) } } @@ -129,9 +128,9 @@ class ConsolidatedApplicationLog: TextOutputStreamable { ] } - private static func readFileLossy(path: String, maxBytes: UInt64) -> Result<String, Error> { + private static func readFileLossy(path: String, maxBytes: UInt64) throws -> String { guard let fileHandle = FileHandle(forReadingAtPath: path) else { - return .failure(.logFileDoesNotExist(path)) + throw Error.logFileDoesNotExist(path) } let endOfFileOffset = fileHandle.seekToEndOfFile() @@ -149,7 +148,7 @@ class ConsolidatedApplicationLog: TextOutputStreamable { return ch == replacementCharacter }) - return .success(lossyString) + return lossyString } private func redactCustomStrings(string: String) -> String { diff --git a/ios/MullvadVPN/TunnelSettings.swift b/ios/MullvadVPN/DNSSettings.swift index a9d8c5b063..bee5b1d7f1 100644 --- a/ios/MullvadVPN/TunnelSettings.swift +++ b/ios/MullvadVPN/DNSSettings.swift @@ -1,68 +1,13 @@ // -// TunnelSettings.swift +// DNSSettings.swift // MullvadVPN // -// Created by pronebird on 19/06/2019. -// Copyright © 2019 Mullvad VPN AB. All rights reserved. +// Created by pronebird on 27/04/2022. +// Copyright © 2022 Mullvad VPN AB. All rights reserved. // import Foundation import struct Network.IPv4Address -import class WireGuardKitTypes.PublicKey -import struct WireGuardKitTypes.IPAddressRange - -/// A struct that holds a tun interface configuration. -struct InterfaceSettings: Codable, Equatable { - var privateKey: PrivateKeyWithMetadata - var nextPrivateKey: PrivateKeyWithMetadata? - - var addresses: [IPAddressRange] - var dnsSettings: DNSSettings - - var publicKey: PublicKey { - return privateKey.publicKeyWithMetadata.publicKey - } - - private enum CodingKeys: String, CodingKey { - case privateKey, nextPrivateKey, addresses, dnsSettings - } - - init(privateKey: PrivateKeyWithMetadata = PrivateKeyWithMetadata(), nextPrivateKey: PrivateKeyWithMetadata? = nil, addresses: [IPAddressRange] = [], dnsSettings: DNSSettings = DNSSettings()) { - self.privateKey = privateKey - self.nextPrivateKey = nextPrivateKey - self.addresses = addresses - self.dnsSettings = dnsSettings - } - - init(from decoder: Decoder) throws { - let container = try decoder.container(keyedBy: CodingKeys.self) - - privateKey = try container.decode(PrivateKeyWithMetadata.self, forKey: .privateKey) - addresses = try container.decode([IPAddressRange].self, forKey: .addresses) - - // Added in 2022.1 - nextPrivateKey = try container.decodeIfPresent(PrivateKeyWithMetadata.self, forKey: .nextPrivateKey) - - // Provide default value, since `dnsSettings` key does not exist in <= 2021.2 - dnsSettings = try container.decodeIfPresent(DNSSettings.self, forKey: .dnsSettings) - ?? DNSSettings() - } - - func encode(to encoder: Encoder) throws { - var container = encoder.container(keyedBy: CodingKeys.self) - - try container.encode(privateKey, forKey: .privateKey) - try container.encode(nextPrivateKey, forKey: .nextPrivateKey) - try container.encode(addresses, forKey: .addresses) - try container.encode(dnsSettings, forKey: .dnsSettings) - } -} - -/// A struct that holds the configuration passed via `NETunnelProviderProtocol`. -struct TunnelSettings: Codable, Equatable { - var relayConstraints = RelayConstraints() - var interface = InterfaceSettings() -} /// A struct describing Mullvad DNS blocking options. struct DNSBlockingOptions: OptionSet, Codable { 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/Keychain/Keychain.swift b/ios/MullvadVPN/Keychain/Keychain.swift deleted file mode 100644 index 383219990a..0000000000 --- a/ios/MullvadVPN/Keychain/Keychain.swift +++ /dev/null @@ -1,154 +0,0 @@ -// -// Keychain.swift -// MullvadVPN -// -// Created by pronebird on 22/04/2020. -// Copyright © 2020 Mullvad VPN AB. All rights reserved. -// - -import Foundation -import Security - -protocol KeychainAttributeDecodable { - init?(attributes: [CFString: Any]) -} - -protocol KeychainAttributeEncodable { - func keychainRepresentation() -> [CFString: Any] - func updateKeychainAttributes(in attributes: inout [CFString: Any]) -} - -extension KeychainAttributeEncodable { - func keychainRepresentation() -> [CFString: Any] { - var attributes = [CFString: Any]() - updateKeychainAttributes(in: &attributes) - return attributes - } -} - -enum Keychain {} - -extension Keychain { - - /// A Keychain Result type - typealias Result<T> = Swift.Result<T, Keychain.Error> - - static func add(_ attributes: Keychain.Attributes) -> Result<Keychain.Attributes?> { - var result: CFTypeRef? - let status = SecItemAdd(attributes.keychainRepresentation() as CFDictionary, &result) - - return mapSecResultAndReturnValue( - status: status, - value: result, - returnSet: attributes.return ?? [], - limit: .one) - .map { $0.first } - } - - static func update(query: Keychain.Attributes, update: Keychain.Attributes) -> Result<()> { - let queryAttributes = query.keychainRepresentation() as CFDictionary - let updateAttributes = update.keychainRepresentation() as CFDictionary - - let status = SecItemUpdate(queryAttributes, updateAttributes) - - return mapSecResult(status: status) { - return () - } - } - - static func delete(query: Keychain.Attributes) -> Result<()> { - let status = SecItemDelete(query.keychainRepresentation() as CFDictionary) - - return mapSecResult(status: status) { - return () - } - } - - static func findFirst(query: Keychain.Attributes) -> Result<Keychain.Attributes?> { - return find(query: query).map { $0.first } - } - - static func find(query: Keychain.Attributes) -> Result<[Keychain.Attributes]> { - let attributes = query.keychainRepresentation() - - var result: CFTypeRef? - let status = SecItemCopyMatching(attributes as CFDictionary, &result) - - return mapSecResultAndReturnValue( - status: status, - value: result, - returnSet: query.return ?? [], - limit: query.matchLimit ?? .one - ) - } - - static private func mapSecResultAndReturnValue( - status: OSStatus, - value: CFTypeRef?, - returnSet: Set<Keychain.Return>, - limit: Keychain.MatchLimit) -> Result<[Keychain.Attributes]> - { - return mapSecResult(status: status) { () -> [Keychain.Attributes] in - return value.map { parseReturnValue(value: $0, returnSet: returnSet, limit: limit) } - ?? [] - } - } - - static private func parseReturnValue( - value: CFTypeRef, - returnSet: Set<Keychain.Return>, - limit: Keychain.MatchLimit) -> [Keychain.Attributes] - { - switch returnSet { - case []: - return [] - - case [.data]: - let values: [Data] = unsafelyCastReturnValue(value: value, limit: limit) - - return values.map { (data) -> Keychain.Attributes in - var attributes = Keychain.Attributes() - attributes.valueData = data - return attributes - } - - case [.persistentReference]: - let values: [Data] = unsafelyCastReturnValue(value: value, limit: limit) - - return values.map { (persistentReference) -> Keychain.Attributes in - var attributes = Keychain.Attributes() - attributes.valuePersistentReference = persistentReference - return attributes - } - - default: - let rawAttributeList: [[CFString: Any]] = - unsafelyCastReturnValue(value: value, limit: limit) - - return rawAttributeList.map { Keychain.Attributes(attributes: $0) } - } - } - - /// A private helper that casts and normalizes the return value from Keychain to produce - /// an array even when a single item is expected to be returned. - static private func unsafelyCastReturnValue<T>( - value: CFTypeRef, - limit: Keychain.MatchLimit) -> [T] - { - switch limit { - case .one: - return [value as! T] - case .all: - return value as! [T] - } - } - - /// A private helper that verifies the given `status` and executes `body` on success - static private func mapSecResult<T>(status: OSStatus, body: () -> T) -> Result<T> { - if status == errSecSuccess { - return .success(body()) - } else { - return .failure(Keychain.Error(code: status)) - } - } -} diff --git a/ios/MullvadVPN/Keychain/KeychainAttributes.swift b/ios/MullvadVPN/Keychain/KeychainAttributes.swift deleted file mode 100644 index 397620f37b..0000000000 --- a/ios/MullvadVPN/Keychain/KeychainAttributes.swift +++ /dev/null @@ -1,139 +0,0 @@ -// -// KeychainAttributes.swift -// MullvadVPN -// -// Created by pronebird on 22/04/2020. -// Copyright © 2020 Mullvad VPN AB. All rights reserved. -// - -import Foundation -import Security - -extension Keychain { - - enum Accessible: RawRepresentable, CaseIterable, KeychainAttributeDecodable, KeychainAttributeEncodable { - - case whenPasscodeSetThisDeviceOnly - case whenUnlocked - case whenUnlockedThisDeviceOnly - case afterFirstUnlock - case afterFirstUnlockThisDeviceOnly - - var rawValue: CFString { - switch self { - case .whenPasscodeSetThisDeviceOnly: - return kSecAttrAccessibleWhenPasscodeSetThisDeviceOnly - case .whenUnlocked: - return kSecAttrAccessibleWhenUnlocked - case .whenUnlockedThisDeviceOnly: - return kSecAttrAccessibleWhenUnlockedThisDeviceOnly - case .afterFirstUnlock: - return kSecAttrAccessibleAfterFirstUnlock - case .afterFirstUnlockThisDeviceOnly: - return kSecAttrAccessibleAfterFirstUnlockThisDeviceOnly - } - } - - init?(rawValue: CFString) { - let maybeCase = Self.allCases.first { $0.rawValue == rawValue } - - if let maybeCase = maybeCase { - self = maybeCase - } else { - return nil - } - } - - init?(attributes: [CFString: Any]) { - if let rawValue = attributes[kSecAttrAccessible] as? String { - self.init(rawValue: rawValue as CFString) - } else { - return nil - } - } - - func updateKeychainAttributes(in attributes: inout [CFString : Any]) { - attributes[kSecAttrAccessible] = rawValue - } - - } - - struct Attributes: KeychainAttributeEncodable, KeychainAttributeDecodable { - var `class`: KeychainClass? - var service: String? - var account: String? - var accessGroup: String? - var accessible: Accessible? - var creationDate: Date? - var modificationDate: Date? - var generic: Data? - - var valueData: Data? - var valuePersistentReference: Data? - - var `return`: Set<Keychain.Return>? - var matchLimit: Keychain.MatchLimit? - - init() {} - - init(attributes: [CFString: Any]) { - `class` = KeychainClass(attributes: attributes) - service = attributes[kSecAttrService] as? String - account = attributes[kSecAttrAccount] as? String - accessGroup = attributes[kSecAttrAccessGroup] as? String - accessible = Accessible(attributes: attributes) - creationDate = attributes[kSecAttrCreationDate] as? Date - modificationDate = attributes[kSecAttrModificationDate] as? Date - generic = attributes[kSecAttrGeneric] as? Data - - valueData = attributes[kSecValueData] as? Data - valuePersistentReference = attributes[kSecValuePersistentRef] as? Data - - `return` = Set(attributes: attributes) - matchLimit = Keychain.MatchLimit(attributes: attributes) - } - - func updateKeychainAttributes(in attributes: inout [CFString: Any]) { - `class`?.updateKeychainAttributes(in: &attributes) - - if let service = service { - attributes[kSecAttrService] = service - } - - if let account = account { - attributes[kSecAttrAccount] = account - } - - if let accessGroup = accessGroup { - attributes[kSecAttrAccessGroup] = accessGroup - } - - accessible?.updateKeychainAttributes(in: &attributes) - - if let creationDate = creationDate { - attributes[kSecAttrCreationDate] = creationDate - } - - if let modificationDate = modificationDate { - attributes[kSecAttrModificationDate] = modificationDate - } - - if let generic = generic { - attributes[kSecAttrGeneric] = generic - } - - if let valueData = valueData { - attributes[kSecValueData] = valueData - } - - if let valuePersistentReference = valuePersistentReference { - attributes[kSecValuePersistentRef] = valuePersistentReference - } - - `return`?.updateKeychainAttributes(in: &attributes) - matchLimit?.updateKeychainAttributes(in: &attributes) - } - - } - -} diff --git a/ios/MullvadVPN/Keychain/KeychainClass.swift b/ios/MullvadVPN/Keychain/KeychainClass.swift deleted file mode 100644 index 86e13f2bd6..0000000000 --- a/ios/MullvadVPN/Keychain/KeychainClass.swift +++ /dev/null @@ -1,50 +0,0 @@ -// -// KeychainClass.swift -// MullvadVPN -// -// Created by pronebird on 24/04/2020. -// Copyright © 2020 Mullvad VPN AB. All rights reserved. -// - -import Foundation -import Security - -extension Keychain { - - enum KeychainClass: RawRepresentable, CaseIterable, KeychainAttributeDecodable, KeychainAttributeEncodable { - case genericPassword - case internetPassword - - var rawValue: CFString { - switch self { - case .genericPassword: - return kSecClassGenericPassword - case .internetPassword: - return kSecClassInternetPassword - } - } - - init?(rawValue: CFString) { - let maybeCase = Self.allCases.first { $0.rawValue == rawValue } - - if let maybeCase = maybeCase { - self = maybeCase - } else { - return nil - } - } - - init?(attributes: [CFString: Any]) { - if let rawValue = attributes[kSecClass] as? String { - self.init(rawValue: rawValue as CFString) - } else { - return nil - } - } - - func updateKeychainAttributes(in attributes: inout [CFString : Any]) { - attributes[kSecClass] = rawValue - } - } - -} diff --git a/ios/MullvadVPN/Keychain/KeychainError.swift b/ios/MullvadVPN/Keychain/KeychainError.swift deleted file mode 100644 index 4758d08b66..0000000000 --- a/ios/MullvadVPN/Keychain/KeychainError.swift +++ /dev/null @@ -1,32 +0,0 @@ -// -// KeychainError.swift -// MullvadVPN -// -// Created by pronebird on 02/10/2019. -// Copyright © 2019 Mullvad VPN AB. All rights reserved. -// - -import Foundation -import Security - -extension Keychain { - struct Error: Swift.Error, LocalizedError { - let code: OSStatus - - var errorDescription: String? { - return SecCopyErrorMessageString(code, nil) as String? - } - } -} - - -extension Keychain.Error { - - static let duplicateItem = Keychain.Error(code: errSecDuplicateItem) - static let itemNotFound = Keychain.Error(code: errSecItemNotFound) - - static func ~= (lhs: Keychain.Error, rhs: Swift.Error) -> Bool { - guard let rhsError = rhs as? Keychain.Error else { return false } - return lhs.code == rhsError.code - } -} diff --git a/ios/MullvadVPN/Keychain/KeychainMatchLimit.swift b/ios/MullvadVPN/Keychain/KeychainMatchLimit.swift deleted file mode 100644 index f86b010532..0000000000 --- a/ios/MullvadVPN/Keychain/KeychainMatchLimit.swift +++ /dev/null @@ -1,48 +0,0 @@ -// -// KeychainMatchLimit.swift -// MullvadVPN -// -// Created by pronebird on 24/04/2020. -// Copyright © 2020 Mullvad VPN AB. All rights reserved. -// - -import Foundation -import Security - -extension Keychain { - enum MatchLimit: RawRepresentable, CaseIterable, KeychainAttributeDecodable, KeychainAttributeEncodable { - case one - case all - - var rawValue: CFString { - switch self { - case .one: - return kSecMatchLimitOne - case .all: - return kSecMatchLimitAll - } - } - - init?(rawValue: CFString) { - let maybeCase = Self.allCases.first { $0.rawValue == rawValue } - - if let maybeCase = maybeCase { - self = maybeCase - } else { - return nil - } - } - - init?(attributes: [CFString : Any]) { - if let rawValue = attributes[kSecMatchLimit] as? String { - self.init(rawValue: rawValue as CFString) - } else { - return nil - } - } - - func updateKeychainAttributes(in attributes: inout [CFString : Any]) { - attributes[kSecMatchLimit] = rawValue - } - } -} diff --git a/ios/MullvadVPN/Keychain/KeychainReturn.swift b/ios/MullvadVPN/Keychain/KeychainReturn.swift deleted file mode 100644 index 7216ffbd80..0000000000 --- a/ios/MullvadVPN/Keychain/KeychainReturn.swift +++ /dev/null @@ -1,57 +0,0 @@ -// -// KeychainReturn.swift -// MullvadVPN -// -// Created by pronebird on 24/04/2020. -// Copyright © 2020 Mullvad VPN AB. All rights reserved. -// - -import Foundation -import Security - -extension Keychain { - enum Return: KeychainAttributeEncodable, CaseIterable { - case data - case attributes - case persistentReference - - fileprivate var attributeKey: CFString { - switch self { - case .attributes: - return kSecReturnAttributes - case .data: - return kSecReturnData - case .persistentReference: - return kSecReturnPersistentRef - } - } - - func updateKeychainAttributes(in attributes: inout [CFString: Any]) { - attributes[attributeKey] = true - } - } -} - -extension Set: KeychainAttributeDecodable, KeychainAttributeEncodable - where Element == Keychain.Return -{ - init?(attributes: [CFString: Any]) { - let items = Keychain.Return.allCases.filter { (returnType) -> Bool in - return attributes[returnType.attributeKey] as? Bool == .some(true) - } - - if items.isEmpty { - return nil - } else { - self.init(items) - } - } - - func updateKeychainAttributes(in attributes: inout [CFString : Any]) { - Keychain.Return.allCases.forEach { (returnType) in - attributes.removeValue(forKey: returnType.attributeKey) - } - - forEach { $0.updateKeychainAttributes(in: &attributes) } - } -} diff --git a/ios/MullvadVPN/KeychainError.swift b/ios/MullvadVPN/KeychainError.swift new file mode 100644 index 0000000000..5ee831c83f --- /dev/null +++ b/ios/MullvadVPN/KeychainError.swift @@ -0,0 +1,25 @@ +// +// KeychainError.swift +// MullvadVPN +// +// Created by pronebird on 02/10/2019. +// Copyright © 2019 Mullvad VPN AB. All rights reserved. +// + +import Foundation +import Security + +struct KeychainError: LocalizedError, Equatable { + let code: OSStatus + + var errorDescription: String? { + return SecCopyErrorMessageString(code, nil) as String? + } + + static let duplicateItem = KeychainError(code: errSecDuplicateItem) + static let itemNotFound = KeychainError(code: errSecItemNotFound) + + static func == (lhs: KeychainError, rhs: KeychainError) -> Bool { + return lhs.code == rhs.code + } +} diff --git a/ios/MullvadVPN/Logging/ChainedError+Logger.swift b/ios/MullvadVPN/Logging/ChainedError+Logger.swift index a0f396767a..2d221c7132 100644 --- a/ios/MullvadVPN/Logging/ChainedError+Logger.swift +++ b/ios/MullvadVPN/Logging/ChainedError+Logger.swift @@ -10,15 +10,26 @@ import Foundation import Logging extension Logger { - - func error<T: ChainedError>(chainedError: T, - message: @autoclosure () -> String? = nil, - metadata: @autoclosure () -> Logger.Metadata? = nil, - source: @autoclosure () -> String? = nil, - file: String = #file, function: String = #function, line: UInt = #line) + func error<T: ChainedError>( + chainedError: T, + message: @autoclosure () -> String? = nil, + metadata: @autoclosure () -> Logger.Metadata? = nil, + source: @autoclosure () -> String? = nil, + file: String = #file, + function: String = #function, + line: UInt = #line + ) { - let s = Message(stringLiteral: chainedError.displayChain(message: message())) - log(level: .error, s, metadata: metadata(), source: source(), file: file, function: function, line: line) + log( + level: .error, + Message( + stringLiteral: chainedError.displayChain(message: message()) + ), + metadata: metadata(), + source: source(), + file: file, + function: function, + line: line + ) } - } diff --git a/ios/MullvadVPN/Logging/LogRotation.swift b/ios/MullvadVPN/Logging/LogRotation.swift index 696f7c4a41..16d48b84df 100644 --- a/ios/MullvadVPN/Logging/LogRotation.swift +++ b/ios/MullvadVPN/Logging/LogRotation.swift @@ -24,36 +24,37 @@ enum LogRotation { } } - static func rotateLog(logsDirectory: URL, logFileName: String) -> Result<(), Error> { + static func rotateLog(logsDirectory: URL, logFileName: String) throws { let fileManager = FileManager.default let source = logsDirectory.appendingPathComponent(logFileName) let backup = source.deletingPathExtension().appendingPathExtension("old.log") - return Result { _ = try fileManager.replaceItemAt(backup, withItemAt: source) } - .mapError { (error) -> Error in - // FileManager returns a very obscure error chain so we need to traverse it to find - // the root cause of the error. - var errorCursor: Swift.Error? = error - let cocoaErrorIterator = AnyIterator { () -> CocoaError? in - if let cocoaError = errorCursor as? CocoaError { - errorCursor = cocoaError.underlying - return cocoaError - } else { - errorCursor = nil - return nil - } + do { + _ = try fileManager.replaceItemAt(backup, withItemAt: source) + } catch { + // FileManager returns a very obscure error chain so we need to traverse it to find + // the root cause of the error. + var errorCursor: Swift.Error? = error + let cocoaErrorIterator = AnyIterator { () -> CocoaError? in + if let cocoaError = errorCursor as? CocoaError { + errorCursor = cocoaError.underlying + return cocoaError + } else { + errorCursor = nil + return nil } + } - while let fileError = cocoaErrorIterator.next() { - // .fileNoSuchFile is returned when both backup and source log files do not exist - // .fileReadNoSuchFile is returned when backup exists but source log file does not - if fileError.code == .fileNoSuchFile || fileError.code == .fileReadNoSuchFile, - fileError.url == source { - return .noSourceLogFile - } + while let fileError = cocoaErrorIterator.next() { + // .fileNoSuchFile is returned when both backup and source log files do not exist + // .fileReadNoSuchFile is returned when backup exists but source log file does not + if fileError.code == .fileNoSuchFile || fileError.code == .fileReadNoSuchFile, + fileError.url == source { + throw Error.noSourceLogFile } + } - return .moveSourceLogFile(error) + throw Error.moveSourceLogFile(error) } } } diff --git a/ios/MullvadVPN/Logging/Logging.swift b/ios/MullvadVPN/Logging/Logging.swift index fe5b65b433..55451b677b 100644 --- a/ios/MullvadVPN/Logging/Logging.swift +++ b/ios/MullvadVPN/Logging/Logging.swift @@ -19,7 +19,15 @@ func initLoggingSystem(bundleIdentifier: String, metadata: Logger.Metadata? = ni try? FileManager.default.createDirectory(at: logsDirectoryURL, withIntermediateDirectories: false, attributes: nil) // Rotate log - let logRotationResult = LogRotation.rotateLog(logsDirectory: logsDirectoryURL, logFileName: logFileName) + var logRotationError: Error? + do { + try LogRotation.rotateLog( + logsDirectory: logsDirectoryURL, + logFileName: logFileName + ) + } catch { + logRotationError = error + } // Create an array of log output streams var streams: [TextOutputStream] = [] @@ -52,8 +60,10 @@ func initLoggingSystem(bundleIdentifier: String, metadata: Logger.Metadata? = ni } } - if case .failure(let logRotationError) = logRotationResult { - Logger(label: "LogRotation") - .error(chainedError: logRotationError, message: "Failed to rotate log") + if let logRotationError = logRotationError { + Logger(label: "LogRotation").error( + chainedError: AnyChainedError(logRotationError), + message: "Failed to rotate log" + ) } } 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 c0e89ce1a1..4fd9aacdef 100644 --- a/ios/MullvadVPN/Notifications/AccountExpiryNotificationProvider.swift +++ b/ios/MullvadVPN/Notifications/AccountExpiryNotificationProvider.swift @@ -9,26 +9,26 @@ import Foundation import UserNotifications -let kAccountExpiryNotificationIdentifier = "net.mullvad.MullvadVPN.AccountExpiryNotification" -let kAccountExpiryDefaultTriggerInterval = 3 +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. private let triggerInterval: Int override var identifier: String { - return kAccountExpiryNotificationIdentifier + return accountExpiryNotificationIdentifier } - init(triggerInterval: Int = kAccountExpiryDefaultTriggerInterval) { + init(triggerInterval: Int = accountExpiryDefaultTriggerInterval) { self.triggerInterval = triggerInterval super.init() - accountExpiry = Account.shared.expiry - Account.shared.addObserver(self) + accountExpiry = TunnelManager.shared.accountExpiry + TunnelManager.shared.addObserver(self) } private var trigger: UNNotificationTrigger? { @@ -70,7 +70,7 @@ class AccountExpiryNotificationProvider: NotificationProvider, SystemNotificatio content.sound = UNNotificationSound.default return UNNotificationRequest( - identifier: kAccountExpiryNotificationIdentifier, + identifier: accountExpiryNotificationIdentifier, content: content, trigger: trigger ) @@ -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/PrivateKeyWithMetadata.swift b/ios/MullvadVPN/PrivateKeyWithMetadata.swift deleted file mode 100644 index 04e73f784c..0000000000 --- a/ios/MullvadVPN/PrivateKeyWithMetadata.swift +++ /dev/null @@ -1,100 +0,0 @@ -// -// PrivateKeyWithMetadata.swift -// MullvadVPN -// -// Created by pronebird on 20/06/2019. -// Copyright © 2019 Mullvad VPN AB. All rights reserved. -// - -import Foundation -import class WireGuardKitTypes.PrivateKey -import class WireGuardKitTypes.PublicKey - -/// A struct holding a private WireGuard key with associated metadata -struct PrivateKeyWithMetadata: Equatable { - - /// When the key was created - let creationDate: Date - - /// Private key - let privateKey: PrivateKey - - /// Public key metadata - var publicKeyWithMetadata: PublicKeyWithMetadata { - return PublicKeyWithMetadata(publicKey: privateKey.publicKey, createdAt: creationDate) - } - - /// Public key - var publicKey: PublicKey { - return privateKey.publicKey - } - - /// Initialize the new private key - init() { - privateKey = PrivateKey() - creationDate = Date() - } - - /// Initialize with the existing private key - init(privateKey: PrivateKey, createdAt: Date) { - self.privateKey = privateKey - creationDate = createdAt - } - -} - -/// A struct holding a public WireGuard key with associated metadata -struct PublicKeyWithMetadata: Equatable { - /// Refers to private key creation date - let creationDate: Date - - /// Public key - let publicKey: PublicKey - - init(publicKey: PublicKey, createdAt: Date) { - self.publicKey = publicKey - creationDate = createdAt - } - - /// Returns a base64 encoded string representation that can be used for displaying the key in - /// the user interface - func stringRepresentation(maxLength: Int? = nil) -> String { - let base64EncodedKey = publicKey.base64Key - - if let maxLength = maxLength, maxLength < base64EncodedKey.count { - return base64EncodedKey.prefix(maxLength) + "..." - } else { - return base64EncodedKey - } - } -} - -extension PrivateKeyWithMetadata: Codable { - - private enum CodingKeys: String, CodingKey { - case privateKeyData, creationDate - } - - func encode(to encoder: Encoder) throws { - var container = encoder.container(keyedBy: CodingKeys.self) - - try container.encode(privateKey.rawValue, forKey: .privateKeyData) - try container.encode(creationDate, forKey: .creationDate) - } - - init(from decoder: Decoder) throws { - let container = try decoder.container(keyedBy: CodingKeys.self) - let privateKeyBytes = try container.decode(Data.self, forKey: .privateKeyData) - - guard let privateKey = PrivateKey(rawValue: privateKeyBytes) else { - throw DecodingError.dataCorruptedError( - forKey: CodingKeys.privateKeyData, - in: container, - debugDescription: "Invalid key data" - ) - } - - self.privateKey = privateKey - self.creationDate = try container.decode(Date.self, forKey: .creationDate) - } -} 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/REST/RESTAPIProxy.swift b/ios/MullvadVPN/REST/RESTAPIProxy.swift index 1271422be1..1d7f259b4b 100644 --- a/ios/MullvadVPN/REST/RESTAPIProxy.swift +++ b/ios/MullvadVPN/REST/RESTAPIProxy.swift @@ -25,33 +25,6 @@ extension REST { ) } - func createAccount( - retryStrategy: REST.RetryStrategy, - completionHandler: @escaping CompletionHandler<AccountResponse> - ) -> Cancellable - { - let requestHandler = AnyRequestHandler { endpoint in - return try self.requestFactory.createRequest( - endpoint: endpoint, - method: .post, - pathTemplate: "accounts" - ) - } - - let responseHandler = REST.defaultResponseHandler( - decoding: AccountResponse.self, - with: responseDecoder - ) - - return addOperation( - name: "create-account", - retryStrategy: retryStrategy, - requestHandler: requestHandler, - responseHandler: responseHandler, - completionHandler: completionHandler - ) - } - func getAddressList( retryStrategy: REST.RetryStrategy, completionHandler: @escaping CompletionHandler<[AnyIPEndpoint]> @@ -136,205 +109,6 @@ extension REST { ) } - func getAccountExpiry( - accountNumber: String, - retryStrategy: REST.RetryStrategy, - completionHandler: @escaping CompletionHandler<AccountResponse> - ) -> Cancellable - { - let requestHandler = AnyRequestHandler { endpoint in - var requestBuilder = try self.requestFactory - .createRequestBuilder( - endpoint: endpoint, - method: .get, - pathTemplate: "me" - ) - requestBuilder.setAuthorization(.accountNumber(accountNumber)) - - return requestBuilder.getRequest() - } - - let responseHandler = REST.defaultResponseHandler( - decoding: AccountResponse.self, - with: responseDecoder - ) - - return addOperation( - name: "get-account-expiry", - retryStrategy: retryStrategy, - requestHandler: requestHandler, - responseHandler: responseHandler, - completionHandler: completionHandler - ) - } - - func getWireguardKey( - accountNumber: String, - publicKey: PublicKey, - retryStrategy: REST.RetryStrategy, - completionHandler: @escaping CompletionHandler<WireguardAddressesResponse> - ) -> Cancellable - { - let requestHandler = AnyRequestHandler { endpoint in - var path: URLPathTemplate = "wireguard-keys/{pubkey}" - try path.addPercentEncodedReplacement( - name: "pubkey", - value: publicKey.base64Key, - allowedCharacters: .alphanumerics - ) - - var requestBuilder = try self.requestFactory - .createRequestBuilder( - endpoint: endpoint, - method: .get, - pathTemplate: path - ) - requestBuilder.setAuthorization(.accountNumber(accountNumber)) - - return requestBuilder.getRequest() - } - - let responseHandler = REST.defaultResponseHandler( - decoding: WireguardAddressesResponse.self, - with: responseDecoder - ) - - return addOperation( - name: "get-wireguard-key", - retryStrategy: retryStrategy, - requestHandler: requestHandler, - responseHandler: responseHandler, - completionHandler: completionHandler - ) - } - - func pushWireguardKey( - accountNumber: String, - publicKey: PublicKey, - retryStrategy: REST.RetryStrategy, - completionHandler: @escaping CompletionHandler<WireguardAddressesResponse> - ) -> Cancellable - { - let requestHandler = AnyRequestHandler { endpoint in - var requestBuilder = try self.requestFactory.createRequestBuilder( - endpoint: endpoint, - method: .post, - pathTemplate: "wireguard-keys" - ) - requestBuilder.setAuthorization(.accountNumber(accountNumber)) - - let body = PushWireguardKeyRequest( - pubkey: publicKey.rawValue - ) - - try requestBuilder.setHTTPBody(value: body) - - return requestBuilder.getRequest() - } - - let responseHandler = REST.defaultResponseHandler( - decoding: WireguardAddressesResponse.self, - with: responseDecoder - ) - - return addOperation( - name: "push-wireguard-key", - retryStrategy: retryStrategy, - requestHandler: requestHandler, - responseHandler: responseHandler, - completionHandler: completionHandler - ) - } - - func replaceWireguardKey( - accountNumber: String, - oldPublicKey: PublicKey, - newPublicKey: PublicKey, - retryStrategy: REST.RetryStrategy, - completionHandler: @escaping CompletionHandler<WireguardAddressesResponse> - ) -> Cancellable - { - let requestHandler = AnyRequestHandler { endpoint in - var requestBuilder = try self.requestFactory.createRequestBuilder( - endpoint: endpoint, - method: .post, - pathTemplate: "replace-wireguard-key" - ) - requestBuilder.setAuthorization(.accountNumber(accountNumber)) - - let body = ReplaceWireguardKeyRequest( - old: oldPublicKey.rawValue, - new: newPublicKey.rawValue - ) - - try requestBuilder.setHTTPBody(value: body) - - return requestBuilder.getRequest() - } - - let responseHandler = REST.defaultResponseHandler( - decoding: WireguardAddressesResponse.self, - with: responseDecoder - ) - - return addOperation( - name: "replace-wireguard-key", - retryStrategy: retryStrategy, - requestHandler: requestHandler, - responseHandler: responseHandler, - completionHandler: completionHandler - ) - } - - func deleteWireguardKey( - accountNumber: String, - publicKey: PublicKey, - retryStrategy: REST.RetryStrategy, - completionHandler: @escaping CompletionHandler<Void> - ) -> Cancellable - { - let requestHandler = AnyRequestHandler { endpoint in - var path: URLPathTemplate = "wireguard-keys/{pubkey}" - - try path.addPercentEncodedReplacement( - name: "pubkey", - value: publicKey.base64Key, - allowedCharacters: .alphanumerics - ) - - var requestBuilder = try self.requestFactory - .createRequestBuilder( - endpoint: endpoint, - method: .delete, - pathTemplate: path - ) - requestBuilder.setAuthorization(.accountNumber(accountNumber)) - - return requestBuilder.getRequest() - } - - let responseHandler = AnyResponseHandler { response, data -> ResponseHandlerResult<Void> in - if HTTPStatus.isSuccess(response.statusCode) { - return .success(()) - } else { - return .unhandledResponse( - try? self.responseDecoder.decode( - ServerErrorResponse.self, - from: data - ) - ) - } - } - - return addOperation( - name: "delete-wireguard-key", - retryStrategy: retryStrategy, - requestHandler: requestHandler, - responseHandler: responseHandler, - completionHandler: completionHandler - ) - } - func createApplePayment( accountNumber: String, receiptString: Data, @@ -434,32 +208,11 @@ extension REST { // MARK: - Response types - struct AccountResponse: Decodable { - let token: String - let expires: Date - } - enum ServerRelaysCacheResponse { case notModified case newContent(_ etag: String?, _ value: ServerRelaysResponse) } - struct WireguardAddressesResponse: Decodable { - let id: String - let pubkey: Data - let ipv4Address: IPAddressRange - let ipv6Address: IPAddressRange - } - - fileprivate struct PushWireguardKeyRequest: Encodable { - let pubkey: Data - } - - fileprivate struct ReplaceWireguardKeyRequest: Encodable { - let old: Data - let new: Data - } - fileprivate struct CreateApplePaymentRequest: Encodable { let receiptString: Data } diff --git a/ios/MullvadVPN/REST/RESTError.swift b/ios/MullvadVPN/REST/RESTError.swift index 3b6f24c49a..e275a9ab34 100644 --- a/ios/MullvadVPN/REST/RESTError.swift +++ b/ios/MullvadVPN/REST/RESTError.swift @@ -73,6 +73,7 @@ extension REST { static let publicKeyInUse = ServerResponseCode(rawValue: "PUBKEY_IN_USE") static let maxDevicesReached = ServerResponseCode(rawValue: "MAX_DEVICES_REACHED") static let invalidAccessToken = ServerResponseCode(rawValue: "INVALID_ACCESS_TOKEN") + static let deviceNotFound = ServerResponseCode(rawValue: "DEVICE_NOT_FOUND") let rawValue: String init(rawValue: String) { diff --git a/ios/MullvadVPN/REST/RESTRetryStrategy.swift b/ios/MullvadVPN/REST/RESTRetryStrategy.swift index 29732ef8a8..c70e3bfc70 100644 --- a/ios/MullvadVPN/REST/RESTRetryStrategy.swift +++ b/ios/MullvadVPN/REST/RESTRetryStrategy.swift @@ -13,10 +13,13 @@ extension REST { var maxRetryCount: Int var retryDelay: DispatchTimeInterval - /// Strategy configured to never retry + /// Strategy configured to never retry. static var noRetry = RetryStrategy(maxRetryCount: 0, retryDelay: .never) - /// Startegy configured with 3 retry attempts with 2 seconds delay between + /// Startegy configured with 3 retry attempts with 2 seconds delay between. static var `default` = RetryStrategy(maxRetryCount: 3, retryDelay: .seconds(2)) + + /// Strategy configured with 10 retry attempts with 2 seconds delay between. + static var aggressive = RetryStrategy(maxRetryCount: 10, retryDelay: .seconds(2)) } } diff --git a/ios/MullvadVPN/RelayCache/RelayCacheError.swift b/ios/MullvadVPN/RelayCache/RelayCacheError.swift index 9e18643a21..fe75cf2d4e 100644 --- a/ios/MullvadVPN/RelayCache/RelayCacheError.swift +++ b/ios/MullvadVPN/RelayCache/RelayCacheError.swift @@ -19,7 +19,6 @@ extension RelayCache { case encodeCache(Swift.Error) case decodeCache(Swift.Error) case rest(REST.Error) - case backgroundTaskScheduler(Swift.Error) var errorDescription: String? { switch self { @@ -37,8 +36,6 @@ extension RelayCache { return "Write cache error." case .rest: return "REST error." - case .backgroundTaskScheduler: - return "Background task scheduler error." } } } diff --git a/ios/MullvadVPN/RelayCache/RelayCacheIO.swift b/ios/MullvadVPN/RelayCache/RelayCacheIO.swift index fc9a13dd41..ccd623d25a 100644 --- a/ios/MullvadVPN/RelayCache/RelayCacheIO.swift +++ b/ios/MullvadVPN/RelayCache/RelayCacheIO.swift @@ -26,17 +26,21 @@ extension RelayCache.IO { } /// Safely read the cache file from disk using file coordinator. - static func read(cacheFileURL: URL) -> Result<RelayCache.CachedRelays, RelayCache.Error> { + static func read(cacheFileURL: URL) throws -> RelayCache.CachedRelays { var result: Result<RelayCache.CachedRelays, RelayCache.Error>? let fileCoordinator = NSFileCoordinator(filePresenter: nil) let accessor = { (fileURLForReading: URL) -> Void in // Decode data from disk - result = Result { try Data(contentsOf: fileURLForReading) } - .mapError { RelayCache.Error.readCache($0) } - .flatMap { (data) in - Result { try JSONDecoder().decode(RelayCache.CachedRelays.self, from: data) } - .mapError { RelayCache.Error.decodeCache($0) } + do { + let data = try Data(contentsOf: fileURLForReading) + let relays = try JSONDecoder().decode(RelayCache.CachedRelays.self, from: data) + + result = .success(relays) + } catch let error as DecodingError { + result = .failure(.decodeCache(error)) + } catch { + result = .failure(.readCache(error)) } } @@ -50,50 +54,57 @@ extension RelayCache.IO { result = .failure(.readCache(error)) } - return result! + return try result!.get() } /// Safely read the cache file from disk using file coordinator and fallback to prebundled relays in case if the /// relay cache file is missing. - static func readWithFallback(cacheFileURL: URL, preBundledRelaysFileURL: URL) -> Result<RelayCache.CachedRelays, RelayCache.Error> { - return Self.read(cacheFileURL: cacheFileURL) - .flatMapError { (error) -> Result<RelayCache.CachedRelays, RelayCache.Error> in - switch error { - case .decodeCache, .readCache(CocoaError.fileReadNoSuchFile): - return RelayCache.IO.readPrebundledRelays(fileURL: preBundledRelaysFileURL) - default: - return .failure(error) - } + static func readWithFallback(cacheFileURL: URL, preBundledRelaysFileURL: URL) throws -> RelayCache.CachedRelays { + do { + return try Self.read(cacheFileURL: cacheFileURL) + } catch { + let error = error as! RelayCache.Error + + switch error { + case .decodeCache, .readCache(CocoaError.fileReadNoSuchFile): + return try RelayCache.IO.readPrebundledRelays(fileURL: preBundledRelaysFileURL) + default: + throw error } + } } /// Read pre-bundled relays file from disk. - static func readPrebundledRelays(fileURL: URL) -> Result<RelayCache.CachedRelays, RelayCache.Error> { - return Result { try Data(contentsOf: fileURL) } - .mapError { RelayCache.Error.readPrebundledRelays($0) } - .flatMap { (data) -> Result<RelayCache.CachedRelays, RelayCache.Error> in - return Result { try REST.Coding.makeJSONDecoder().decode(REST.ServerRelaysResponse.self, from: data) } - .mapError { RelayCache.Error.decodePrebundledRelays($0) } - .map { (relays) -> RelayCache.CachedRelays in - return RelayCache.CachedRelays( - relays: relays, - updatedAt: Date(timeIntervalSince1970: 0) - ) - } + static func readPrebundledRelays(fileURL: URL) throws -> RelayCache.CachedRelays { + do { + let data = try Data(contentsOf: fileURL) + let relays = try REST.Coding.makeJSONDecoder() + .decode(REST.ServerRelaysResponse.self, from: data) + + return RelayCache.CachedRelays( + relays: relays, + updatedAt: Date(timeIntervalSince1970: 0) + ) + } catch let error as DecodingError { + throw RelayCache.Error.decodePrebundledRelays(error) + } catch { + throw RelayCache.Error.readPrebundledRelays(error) } } /// Safely write the cache file on disk using file coordinator. - static func write(cacheFileURL: URL, record: RelayCache.CachedRelays) -> Result<(), RelayCache.Error> { - var result: Result<(), RelayCache.Error>? + static func write(cacheFileURL: URL, record: RelayCache.CachedRelays) throws { + var resultError: RelayCache.Error? let fileCoordinator = NSFileCoordinator(filePresenter: nil) let accessor = { (fileURLForWriting: URL) -> Void in - result = Result { try JSONEncoder().encode(record) } - .mapError { RelayCache.Error.encodeCache($0) } - .flatMap { (data) in - Result { try data.write(to: fileURLForWriting) } - .mapError { RelayCache.Error.writeCache($0) } + do { + let data = try JSONEncoder().encode(record) + try data.write(to: fileURLForWriting) + } catch let error as EncodingError { + resultError = .encodeCache(error) + } catch { + resultError = .writeCache(error) } } @@ -103,10 +114,8 @@ extension RelayCache.IO { error: &error, byAccessor: accessor) - if let error = error { - result = .failure(.writeCache(error)) + if let resultError = resultError { + throw resultError } - - return result! } } diff --git a/ios/MullvadVPN/RelayCache/RelayCacheTracker.swift b/ios/MullvadVPN/RelayCache/RelayCacheTracker.swift index a07ac38b65..025731fe23 100644 --- a/ios/MullvadVPN/RelayCache/RelayCacheTracker.swift +++ b/ios/MullvadVPN/RelayCache/RelayCacheTracker.swift @@ -70,15 +70,20 @@ extension RelayCache { self.isPeriodicUpdatesEnabled = true - switch RelayCache.IO.read(cacheFileURL: self.cacheFileURL) { - case .success(let cachedRelays): - let nextUpdate = cachedRelays.updatedAt.addingTimeInterval(Self.relayUpdateInterval) - self.scheduleRepeatingTimer(startTime: .now() + nextUpdate.timeIntervalSinceNow) + do { + let cachedRelays = try RelayCache.IO.read(cacheFileURL: self.cacheFileURL) + let nextUpdate = cachedRelays.updatedAt + .addingTimeInterval(Self.relayUpdateInterval) - case .failure(let readError): - self.logger.error(chainedError: readError, message: "Failed to read the relay cache.") + self.scheduleRepeatingTimer(startTime: .now() + nextUpdate.timeIntervalSinceNow) + } catch { + self.logger.error( + chainedError: AnyChainedError(error), + message: "Failed to read the relay cache." + ) - if Self.shouldDownloadRelaysOnReadFailure(readError) { + if let readError = error as? RelayCache.Error, + Self.shouldDownloadRelaysOnReadFailure(readError) { self.scheduleRepeatingTimer(startTime: .now()) } } @@ -98,7 +103,11 @@ extension RelayCache { } } - func updateRelays(completionHandler: @escaping (OperationCompletion<RelayCache.FetchResult, RelayCache.Error>) -> Void) -> Cancellable { + func updateRelays( + completionHandler: @escaping ( + OperationCompletion<RelayCache.FetchResult, RelayCache.Error> + ) -> Void + ) -> Cancellable { let operation = UpdateRelaysOperation( dispatchQueue: stateQueue, apiProxy: REST.ProxyFactory.shared.createAPIProxy(), @@ -131,20 +140,24 @@ extension RelayCache { func read(completionHandler: @escaping (Result<CachedRelays, RelayCache.Error>) -> Void) { stateQueue.async { - let result = RelayCache.IO.readWithFallback( - cacheFileURL: self.cacheFileURL, - preBundledRelaysFileURL: self.prebundledRelaysFileURL - ) + let result = Result { + try RelayCache.IO.readWithFallback( + cacheFileURL: self.cacheFileURL, + preBundledRelaysFileURL: self.prebundledRelaysFileURL + ) + }.mapError { error in + return error as! RelayCache.Error + } completionHandler(result) } } - func readAndWait() -> Result<CachedRelays, RelayCache.Error> { - return stateQueue.sync { - return RelayCache.IO.readWithFallback( - cacheFileURL: self.cacheFileURL, - preBundledRelaysFileURL: self.prebundledRelaysFileURL + func readAndWait() throws -> CachedRelays { + return try stateQueue.sync { + return try RelayCache.IO.readWithFallback( + cacheFileURL: cacheFileURL, + preBundledRelaysFileURL: prebundledRelaysFileURL ) } } @@ -241,38 +254,37 @@ extension RelayCache.Tracker { } /// Schedules app refresh task relative to the last relays update. - func scheduleAppRefreshTask() -> Result<(), RelayCache.Error> { - return readAndWait().flatMap { cachedRelays in - let beginDate = cachedRelays.updatedAt.addingTimeInterval(Self.relayUpdateInterval) + func scheduleAppRefreshTask() throws { + let cachedRelays = try readAndWait() + let beginDate = cachedRelays.updatedAt.addingTimeInterval(Self.relayUpdateInterval) - return self.submitAppRefreshTask(at: beginDate) - } + try submitAppRefreshTask(at: beginDate) } /// Create and submit task request to scheduler. - private func submitAppRefreshTask(at beginDate: Date) -> Result<(), RelayCache.Error> { + private func submitAppRefreshTask(at beginDate: Date) throws { let taskIdentifier = ApplicationConfiguration.appRefreshTaskIdentifier let request = BGAppRefreshTaskRequest(identifier: taskIdentifier) request.earliestBeginDate = beginDate - return Result { try BGTaskScheduler.shared.submit(request) } - .mapError { error in - return .backgroundTaskScheduler(error) - } + try BGTaskScheduler.shared.submit(request) } /// Background task handler private func handleAppRefreshTask(_ task: BGAppRefreshTask) { logger.debug("Start app refresh task.") - let cancellable = self.updateRelays { completion in + let cancellable = updateRelays { completion in switch completion { case .success(let fetchResult): self.logger.debug("Finished updating relays in app refresh task: \(fetchResult).") case .failure(let error): - self.logger.error(chainedError: error, message: "Failed to update relays in app refresh task.") + self.logger.error( + chainedError: error, + message: "Failed to update relays in app refresh task." + ) case .cancelled: self.logger.debug("App refresh task was cancelled.") @@ -287,13 +299,15 @@ extension RelayCache.Tracker { // Schedule next refresh let scheduleDate = Date(timeIntervalSinceNow: Self.relayUpdateInterval) + do { + try submitAppRefreshTask(at: scheduleDate) - switch self.submitAppRefreshTask(at: scheduleDate) { - case .success: logger.debug("Scheduled next app refresh task at \(scheduleDate.logFormatDate()).") - - case .failure(let error): - logger.error(chainedError: error, message: "Failed to schedule next app refresh task.") + } catch { + logger.error( + chainedError: AnyChainedError(error), + message: "Failed to schedule next app refresh task." + ) } } } @@ -308,7 +322,7 @@ fileprivate class UpdateRelaysOperation: ResultOperation<RelayCache.FetchResult, private let logger = Logger(label: "RelayCacheTracker.UpdateRelaysOperation") private let updateHandler: UpdateHandler - private var downloadCancellable: Cancellable? + private var downloadTask: Cancellable? init( dispatchQueue: DispatchQueue, @@ -332,32 +346,34 @@ fileprivate class UpdateRelaysOperation: ResultOperation<RelayCache.FetchResult, } override func main() { - let readResult = RelayCache.IO.read(cacheFileURL: self.cacheFileURL) - - switch readResult { - case .success(let cachedRelays): - let nextUpdate = cachedRelays.updatedAt.addingTimeInterval(self.relayUpdateInterval) + do { + let cachedRelays = try RelayCache.IO.read(cacheFileURL: cacheFileURL) + let nextUpdate = cachedRelays.updatedAt.addingTimeInterval(relayUpdateInterval) if nextUpdate <= Date() { - self.downloadRelays(previouslyCachedRelays: cachedRelays) + downloadRelays(previouslyCachedRelays: cachedRelays) } else { - self.finish(completion: .success(.throttled)) + finish(completion: .success(.throttled)) } + } catch { + let error = error as! RelayCache.Error - case .failure(let readError): - self.logger.error(chainedError: readError, message: "Failed to read the relay cache to determine if it needs to be updated.") + logger.error( + chainedError: error, + message: "Failed to read the relay cache to determine if it needs to be updated." + ) - if self.shouldDownloadRelaysOnReadFailure(readError) { - self.downloadRelays(previouslyCachedRelays: nil) + if shouldDownloadRelaysOnReadFailure(error) { + downloadRelays(previouslyCachedRelays: nil) } else { - self.finish(completion: .failure(readError)) + finish(completion: .failure(error)) } } } override func operationDidCancel() { - downloadCancellable?.cancel() - downloadCancellable = nil + downloadTask?.cancel() + downloadTask = nil } private func didReceiveNewRelays(etag: String?, relays: REST.ServerRelaysResponse) { @@ -365,38 +381,49 @@ fileprivate class UpdateRelaysOperation: ResultOperation<RelayCache.FetchResult, logger.info("Downloaded \(numRelays) relays.") - let cachedRelays = RelayCache.CachedRelays(etag: etag, relays: relays, updatedAt: Date()) - let writeResult = RelayCache.IO.write(cacheFileURL: cacheFileURL, record: cachedRelays) + let cachedRelays = RelayCache.CachedRelays( + etag: etag, + relays: relays, + updatedAt: Date() + ) + + do { + try RelayCache.IO.write(cacheFileURL: cacheFileURL, record: cachedRelays) - switch writeResult { - case .success: updateHandler(cachedRelays) finish(completion: .success(.newContent)) + } catch { + let error = error as! RelayCache.Error - case .failure(let error): - logger.error(chainedError: error, message: "Failed to store downloaded relays.") + logger.error( + chainedError: error, + message: "Failed to store downloaded relays." + ) - finish(completion: .failure(.writeCache(error))) + finish(completion: .failure(error)) } } private func didReceiveNotModified(previouslyCachedRelays: RelayCache.CachedRelays) { - logger.info("Relays haven't changed since last check.") - var cachedRelays = previouslyCachedRelays cachedRelays.updatedAt = Date() - let writeResult = RelayCache.IO.write(cacheFileURL: self.cacheFileURL, record: cachedRelays) + logger.info("Relays haven't changed since last check.") + + do { + try RelayCache.IO.write(cacheFileURL: cacheFileURL, record: cachedRelays) - switch writeResult { - case .success: finish(completion: .success(.sameContent)) + } catch { + let error = error as! RelayCache.Error - case .failure(let error): - logger.error(chainedError: error, message: "Failed to update cached relays timestamp.") + logger.error( + chainedError: error, + message: "Failed to update cached relays timestamp." + ) - finish(completion: .failure(.writeCache(error))) + finish(completion: .failure(error)) } } @@ -407,11 +434,11 @@ fileprivate class UpdateRelaysOperation: ResultOperation<RelayCache.FetchResult, } private func downloadRelays(previouslyCachedRelays: RelayCache.CachedRelays?) { - downloadCancellable = apiProxy.getRelays(etag: previouslyCachedRelays?.etag, retryStrategy: .noRetry) { [weak self] result in + downloadTask = apiProxy.getRelays(etag: previouslyCachedRelays?.etag, retryStrategy: .noRetry) { [weak self] completion in guard let self = self else { return } self.dispatchQueue.async { - switch result { + switch completion { case .success(.newContent(let etag, let relays)): self.didReceiveNewRelays(etag: etag, relays: relays) diff --git a/ios/MullvadVPN/SettingsAccountCell.swift b/ios/MullvadVPN/SettingsAccountCell.swift index 30411a1276..6021e4cca2 100644 --- a/ios/MullvadVPN/SettingsAccountCell.swift +++ b/ios/MullvadVPN/SettingsAccountCell.swift @@ -17,36 +17,39 @@ class SettingsAccountCell: SettingsCell { } private func didUpdateAccountExpiry() { - if let accountExpiryDate = accountExpiryDate { - let accountExpiry = AccountExpiry(date: accountExpiryDate) - - if accountExpiry.isExpired { - detailTitleLabel.text = NSLocalizedString( - "ACCOUNT_CELL_OUT_OF_TIME_LABEL", - tableName: "Settings", - comment: "Label displayed when user account ran out of time." - ) - detailTitleLabel.textColor = .dangerColor - } else { - if let remainingTime = accountExpiry.formattedRemainingTime { - let localizedString = NSLocalizedString( - "ACCOUNT_CELL_TIME_LEFT_LABEL_FORMAT", - tableName: "Settings", - value: "%@ left", - comment: "The amount of time left on user account. Use %@ placeholder to position the localized text with the time duration left (i.e 10 days)." - ) - let formattedString = String(format: localizedString, remainingTime) - - detailTitleLabel.text = formattedString.uppercased() - } else { - detailTitleLabel.text = "" - } - detailTitleLabel.textColor = UIColor.Cell.detailTextColor - } - } else { + guard let accountExpiryDate = accountExpiryDate else { detailTitleLabel.text = "" detailTitleLabel.textColor = UIColor.Cell.detailTextColor + return } - } + guard accountExpiryDate > Date() else { + detailTitleLabel.text = NSLocalizedString( + "ACCOUNT_CELL_OUT_OF_TIME_LABEL", + tableName: "Settings", + value: "OUT OF TIME", + comment: "" + ) + detailTitleLabel.textColor = .dangerColor + return + } + + let formattedTime = CustomDateComponentsFormatting.localizedString( + from: Date(), + to: accountExpiryDate, + unitsStyle: .full + ) + + detailTitleLabel.text = formattedTime.map { remainingTimeString in + let localizedString = NSLocalizedString( + "ACCOUNT_CELL_TIME_LEFT_LABEL_FORMAT", + tableName: "Settings", + value: "%@ left", + comment: "" + ) + + return String(format: localizedString, remainingTimeString).uppercased() + } ?? "" + detailTitleLabel.textColor = UIColor.Cell.detailTextColor + } } 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/SettingsManager/SettingsManager.swift b/ios/MullvadVPN/SettingsManager/SettingsManager.swift new file mode 100644 index 0000000000..41945b807d --- /dev/null +++ b/ios/MullvadVPN/SettingsManager/SettingsManager.swift @@ -0,0 +1,256 @@ +// +// SettingsManager.swift +// MullvadVPN +// +// Created by pronebird on 29/04/2022. +// Copyright © 2022 Mullvad VPN AB. All rights reserved. +// + +import Foundation +import Logging + +enum SettingsManager {} + +struct LegacyTunnelSettings { + let accountNumber: String + let tunnelSettings: TunnelSettingsV1 +} + +let keychainServiceName = "Mullvad VPN" + +enum KeychainAccountName: String, CaseIterable { + case settings = "Settings" + case lastUsedAccount = "LastUsedAccount" +} + +extension SettingsManager { + + // MARK: - + + static func getLastUsedAccount() throws -> String { + var query = createDefaultAttributes(accountName: .lastUsedAccount) + query[kSecReturnData] = true + + var result: CFTypeRef? + let status = SecItemCopyMatching(query as CFDictionary, &result) + + guard status == errSecSuccess else { + throw KeychainError(code: status) + } + + let data = result as! Data + + return String(data: data, encoding: .utf8)! + } + + static func setLastUsedAccount(_ string: String?) throws { + let query = createDefaultAttributes(accountName: .lastUsedAccount) + + guard let string = string else { + switch SecItemDelete(query as CFDictionary) { + case errSecSuccess, errSecItemNotFound: + return + case let status: + throw KeychainError(code: status) + } + } + + let data = string.data(using: .utf8)! + var status = SecItemUpdate( + query as CFDictionary, + [kSecValueData: data] as CFDictionary + ) + + switch status { + case errSecItemNotFound: + var insert = query + insert[kSecAttrAccessible] = kSecAttrAccessibleAfterFirstUnlock + insert[kSecValueData] = data + + status = SecItemAdd(insert as CFDictionary, nil) + if status != errSecSuccess { + throw KeychainError(code: status) + } + case errSecSuccess: + break + default: + throw KeychainError(code: status) + } + } + + // MARK: - + + static func readSettings() throws -> TunnelSettingsV2 { + var query = createDefaultAttributes(accountName: .settings) + query[kSecReturnData] = true + + var result: CFTypeRef? + let status = SecItemCopyMatching(query as CFDictionary, &result) + + guard status == errSecSuccess else { + throw KeychainError(code: status) + } + + let data = result as! Data + + let decoder = JSONDecoder() + return try decoder.decode(TunnelSettingsV2.self, from: data) + } + + static func writeSettings(_ settings: TunnelSettingsV2) throws { + let encoder = JSONEncoder() + let data = try encoder.encode(settings) + + let query = createDefaultAttributes(accountName: .settings) + var status = SecItemUpdate( + query as CFDictionary, + [kSecValueData: data] as CFDictionary + ) + + switch status { + case errSecItemNotFound: + var insert = query + insert[kSecAttrAccessGroup] = ApplicationConfiguration.securityGroupIdentifier + insert[kSecAttrAccessible] = kSecAttrAccessibleAfterFirstUnlock + insert[kSecValueData] = data + + status = SecItemAdd(insert as CFDictionary, nil) + if status != errSecSuccess { + throw KeychainError(code: status) + } + case errSecSuccess: + break + default: + throw KeychainError(code: status) + } + } + + static func deleteSettings() throws { + let query = createDefaultAttributes(accountName: .settings) + let status = SecItemDelete(query as CFDictionary) + if status != errSecSuccess { + throw KeychainError(code: status) + } + } + + private static func createDefaultAttributes(accountName: KeychainAccountName) -> [CFString: Any] { + return [ + kSecClass: kSecClassGenericPassword, + kSecAttrService: keychainServiceName, + kSecAttrAccount: accountName.rawValue + ] + } + + // MARK: - Legacy settings support + + private static let logger = Logger(label: "SettingsManager") + + static func readLegacySettings() throws -> [LegacyTunnelSettings] { + let query: [CFString: Any] = [ + kSecClass: kSecClassGenericPassword, + kSecAttrService: keychainServiceName, + kSecReturnAttributes: true, + kSecReturnData: true, + kSecMatchLimit: kSecMatchLimitAll + ] + + var result: CFTypeRef? + let status = SecItemCopyMatching(query as CFDictionary, &result) + + guard status == errSecSuccess else { + throw KeychainError(code: status) + } + + guard let items = result as? [[CFString: Any]] else { + return [] + } + + return items.filter(Self.filterLegacySettings) + .compactMap { item -> LegacyTunnelSettings? in + guard let accountNumber = item[kSecAttrAccount] as? String, + let data = item[kSecValueData] as? Data else { + return nil + } + do { + let tunnelSettings = try JSONDecoder().decode( + TunnelSettingsV1.self, + from: data + ) + + return LegacyTunnelSettings( + accountNumber: accountNumber, + tunnelSettings: tunnelSettings + ) + } catch { + logger.error( + chainedError: AnyChainedError(error), + message: "Failed to decode legacy settings." + ) + return nil + } + } + } + + static func deleteLegacySettings() { + let query: [CFString: Any] = [ + kSecClass: kSecClassGenericPassword, + kSecAttrService: keychainServiceName, + kSecReturnAttributes: true, + kSecMatchLimit: kSecMatchLimitAll + ] + + var result: CFTypeRef? + let status = SecItemCopyMatching(query as CFDictionary, &result) + + guard status == errSecSuccess else { + let error = KeychainError(code: status) + + if error != .itemNotFound { + logger.error( + chainedError: AnyChainedError(error), + message: "Failed to list legacy settings." + ) + } + + return + } + + guard let items = result as? [[CFString: Any]] else { + return + } + + items.filter(Self.filterLegacySettings) + .enumerated() + .forEach { (index, item) in + guard let account = item[kSecAttrAccount] else { + return + } + + let deleteQuery: [CFString: Any] = [ + kSecClass: kSecClassGenericPassword, + kSecAttrService: keychainServiceName, + kSecAttrAccount: account + ] + + let status = SecItemDelete(deleteQuery as CFDictionary) + if status == errSecSuccess { + logger.debug("Removed legacy settings entry \(index).") + } else { + let error = KeychainError(code: status) + + logger.error( + chainedError: AnyChainedError(error), + message: "Failed to remove legacy settings entry \(index)." + ) + } + } + } + + private static func filterLegacySettings(_ item: [CFString: Any]) -> Bool { + guard let accountNumber = item[kSecAttrAccount] as? String else { + return false + } + + return KeychainAccountName(rawValue: accountNumber) == nil + } +} diff --git a/ios/MullvadVPN/SettingsManager/TunnelSettingsV1.swift b/ios/MullvadVPN/SettingsManager/TunnelSettingsV1.swift new file mode 100644 index 0000000000..0d7facb554 --- /dev/null +++ b/ios/MullvadVPN/SettingsManager/TunnelSettingsV1.swift @@ -0,0 +1,98 @@ +// +// TunnelSettingsV1.swift +// MullvadVPN +// +// Created by pronebird on 19/06/2019. +// Copyright © 2019 Mullvad VPN AB. All rights reserved. +// + +import Foundation +import struct Network.IPv4Address +import class WireGuardKitTypes.PublicKey +import class WireGuardKitTypes.PrivateKey +import struct WireGuardKitTypes.IPAddressRange + +/// A struct that holds the configuration passed via `NETunnelProviderProtocol`. +struct TunnelSettingsV1: Codable, Equatable { + var relayConstraints = RelayConstraints() + var interface = InterfaceSettings() +} + +/// A struct that holds a tun interface configuration. +struct InterfaceSettings: Codable, Equatable { + var privateKey: PrivateKeyWithMetadata + var nextPrivateKey: PrivateKeyWithMetadata? + + var addresses: [IPAddressRange] + var dnsSettings: DNSSettings + + private enum CodingKeys: String, CodingKey { + case privateKey, nextPrivateKey, addresses, dnsSettings + } + + init( + privateKey: PrivateKeyWithMetadata = PrivateKeyWithMetadata(), + nextPrivateKey: PrivateKeyWithMetadata? = nil, + addresses: [IPAddressRange] = [], + dnsSettings: DNSSettings = DNSSettings() + ) + { + self.privateKey = privateKey + self.nextPrivateKey = nextPrivateKey + self.addresses = addresses + self.dnsSettings = dnsSettings + } + + init(from decoder: Decoder) throws { + let container = try decoder.container(keyedBy: CodingKeys.self) + + privateKey = try container.decode(PrivateKeyWithMetadata.self, forKey: .privateKey) + addresses = try container.decode([IPAddressRange].self, forKey: .addresses) + + // Added in 2022.1 + nextPrivateKey = try container.decodeIfPresent(PrivateKeyWithMetadata.self, forKey: .nextPrivateKey) + + // Provide default value, since `dnsSettings` key does not exist in <= 2021.2 + dnsSettings = try container.decodeIfPresent(DNSSettings.self, forKey: .dnsSettings) + ?? DNSSettings() + } + + func encode(to encoder: Encoder) throws { + var container = encoder.container(keyedBy: CodingKeys.self) + + try container.encode(privateKey, forKey: .privateKey) + try container.encode(nextPrivateKey, forKey: .nextPrivateKey) + try container.encode(addresses, forKey: .addresses) + try container.encode(dnsSettings, forKey: .dnsSettings) + } +} + +/// A struct holding a private WireGuard key with associated metadata +struct PrivateKeyWithMetadata: Equatable, Codable { + private enum CodingKeys: String, CodingKey { + case privateKey = "privateKeyData", creationDate + } + + /// When the key was created + let creationDate: Date + + /// Private key + let privateKey: PrivateKey + + /// Public key + var publicKey: PublicKey { + return privateKey.publicKey + } + + /// Initialize the new private key + init() { + privateKey = PrivateKey() + creationDate = Date() + } + + /// Initialize with the existing private key + init(privateKey: PrivateKey, createdAt: Date) { + self.privateKey = privateKey + creationDate = createdAt + } +} diff --git a/ios/MullvadVPN/SettingsManager/TunnelSettingsV2+REST.swift b/ios/MullvadVPN/SettingsManager/TunnelSettingsV2+REST.swift new file mode 100644 index 0000000000..438bd69481 --- /dev/null +++ b/ios/MullvadVPN/SettingsManager/TunnelSettingsV2+REST.swift @@ -0,0 +1,21 @@ +// +// TunnelSettingsV2+REST.swift +// MullvadVPN +// +// Created by pronebird on 13/05/2022. +// Copyright © 2022 Mullvad VPN AB. All rights reserved. +// + +import Foundation +import class WireGuardKitTypes.PrivateKey + +extension StoredDeviceData { + mutating func update(from device: REST.Device) { + identifier = device.id + name = device.name + creationDate = device.created + hijackDNS = device.hijackDNS + ipv4Address = device.ipv4Address + ipv6Address = device.ipv6Address + } +} diff --git a/ios/MullvadVPN/SettingsManager/TunnelSettingsV2.swift b/ios/MullvadVPN/SettingsManager/TunnelSettingsV2.swift new file mode 100644 index 0000000000..942e3a27fd --- /dev/null +++ b/ios/MullvadVPN/SettingsManager/TunnelSettingsV2.swift @@ -0,0 +1,69 @@ +// +// TunnelSettingsV2.swift +// MullvadVPN +// +// Created by pronebird on 27/04/2022. +// Copyright © 2022 Mullvad VPN AB. All rights reserved. +// + +import Foundation +import struct Network.IPv4Address +import class WireGuardKitTypes.PublicKey +import class WireGuardKitTypes.PrivateKey +import struct WireGuardKitTypes.IPAddressRange + +struct TunnelSettingsV2: Codable, Equatable { + /// Mullvad account data. + var account: StoredAccountData + + /// Device data. + var device: StoredDeviceData + + /// Relay constraints. + var relayConstraints: RelayConstraints + + /// DNS settings. + var dnsSettings: DNSSettings +} + +struct StoredAccountData: Codable, Equatable { + /// Account identifier. + var identifier: String + + /// Account number. + var number: String + + /// Account expiry. + var expiry: Date +} + +struct StoredDeviceData: Codable, Equatable { + /// Device creation date. + var creationDate: Date + + /// Device identifier. + var identifier: String + + /// Device name. + var name: String + + /// Whether relay hijacks DNS from this device. + var hijackDNS: Bool + + /// IPv4 address assigned to device. + var ipv4Address: IPAddressRange + + /// IPv6 address assignged to device. + var ipv6Address: IPAddressRange + + /// WireGuard key data. + var wgKeyData: StoredWgKeyData +} + +struct StoredWgKeyData: Codable, Equatable { + /// Private key creation date. + var creationDate: Date + + /// Private key. + var privateKey: PrivateKey +} 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/SimulatorTunnelProviderHost.swift b/ios/MullvadVPN/SimulatorTunnelProviderHost.swift index 9f6bf5fc78..7ad0d19a34 100644 --- a/ios/MullvadVPN/SimulatorTunnelProviderHost.swift +++ b/ios/MullvadVPN/SimulatorTunnelProviderHost.swift @@ -83,24 +83,29 @@ class SimulatorTunnelProviderHost: SimulatorTunnelProviderDelegate { } private func pickRelay() -> RelaySelectorResult? { - switch RelayCache.Tracker.shared.readAndWait() { - case .success(let cachedRelays): - let keychainReference = self.protocolConfiguration.passwordReference! - - switch TunnelSettingsManager.load(searchTerm: .persistentReference(keychainReference)) { - case .success(let entry): - return RelaySelector.evaluate( - relays: cachedRelays.relays, - constraints: entry.tunnelSettings.relayConstraints - ) - case .failure(let error): - self.providerLogger.error(chainedError: error, message: "Failed to load tunnel settings when picking relay.") + let cachedRelays: RelayCache.CachedRelays + do { + cachedRelays = try RelayCache.Tracker.shared.readAndWait() + } catch { + providerLogger.error( + chainedError: AnyChainedError(error), + message: "Failed to read relays when picking relay." + ) + return nil + } - return nil - } + do { + let tunnelSettings = try SettingsManager.readSettings() - case .failure(let error): - self.providerLogger.error(chainedError: error, message: "Failed to read relays when picking relay.") + return RelaySelector.evaluate( + relays: cachedRelays.relays, + constraints: tunnelSettings.relayConstraints + ) + } catch { + providerLogger.error( + chainedError: AnyChainedError(error), + message: "Failed to read settings when picking relay." + ) return nil } } 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..71a7c3a4a9 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,9 +953,10 @@ extension TunnelManager { // Do not retry if logged out. return nil - case .replaceWireguardKey(.unhandledResponse(_, let serverErrorResponse)) - where serverErrorResponse?.code == .invalidAccount: - // Do not retry if account was removed. + case .rotateKey(.unhandledResponse(_, let serverErrorResponse)) + where serverErrorResponse?.code == .invalidAccount || + serverErrorResponse?.code == .deviceNotFound: + // Do not retry if account or device were removed. return nil default: @@ -855,3 +964,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/TunnelSettingsManager.swift b/ios/MullvadVPN/TunnelSettingsManager.swift deleted file mode 100644 index b4aec2b8ba..0000000000 --- a/ios/MullvadVPN/TunnelSettingsManager.swift +++ /dev/null @@ -1,258 +0,0 @@ -// -// TunnelSettingsManager.swift -// MullvadVPN -// -// Created by pronebird on 02/10/2019. -// Copyright © 2019 Mullvad VPN AB. All rights reserved. -// - -import Foundation -import Security - -/// Service name used for keychain items -private let kServiceName = "Mullvad VPN" - -enum TunnelSettingsManager {} - -extension TunnelSettingsManager { - - enum Error: ChainedError { - /// A failure to encode the given tunnel settings - case encode(Swift.Error) - - /// A failure to decode the data stored in Keychain - case decode(Swift.Error) - - /// A failure to add a new entry to Keychain - case addEntry(Keychain.Error) - - /// A failure to update the existing entry in Keychain - case updateEntry(Keychain.Error) - - /// A failure to remove an entry in Keychain - case removeEntry(Keychain.Error) - - /// A failure to query the entry in Keychain - case lookupEntry(Keychain.Error) - - /// Missing attributes required to perform an operation. - case missingRequiredAttributes - - var errorDescription: String? { - switch self { - case .encode: - return "Failure to encode settings." - case .decode: - return "Failure to decode settings." - case .addEntry: - return "Failure to add keychain entry." - case .updateEntry: - return "Failure to update keychain entry." - case .removeEntry: - return "Failure to remove keychain entry." - case .lookupEntry: - return "Failure to lookup keychain entry." - case .missingRequiredAttributes: - return "Keychain entry is missing required set of attributes." - } - } - } - - typealias Result<T> = Swift.Result<T, Error> - - /// Keychain access level that should be used for all items containing tunnel settings - private static let keychainAccessibleLevel = Keychain.Accessible.afterFirstUnlock - - enum KeychainSearchTerm { - case accountToken(String) - case persistentReference(Data) - - /// Returns `Keychain.Attributes` appropriate for adding or querying the item - fileprivate func makeKeychainAttributes() -> Keychain.Attributes { - var attributes = Keychain.Attributes() - attributes.class = .genericPassword - - switch self { - case .accountToken(let accountToken): - attributes.account = accountToken - attributes.service = kServiceName - - case .persistentReference(let persistentReference): - attributes.valuePersistentReference = persistentReference - } - - return attributes - } - } - - struct KeychainEntry { - let accountToken: String - let tunnelSettings: TunnelSettings - } - - static func load(searchTerm: KeychainSearchTerm) -> Result<KeychainEntry> { - var query = searchTerm.makeKeychainAttributes() - query.return = [.data, .attributes] - - return Keychain.findFirst(query: query) - .mapError { .lookupEntry($0) } - .flatMap { (attributes) in - guard let account = attributes?.account, let data = attributes?.valueData else { - return .failure(.missingRequiredAttributes) - } - - return Self.decode(data: data) - .map { KeychainEntry(accountToken: account, tunnelSettings: $0) } - } - } - - static func add(configuration: TunnelSettings, account: String) -> Result<()> { - Self.encode(tunnelConfig: configuration) - .flatMap { (data) -> Result<()> in - var attributes = KeychainSearchTerm.accountToken(account) - .makeKeychainAttributes() - - // Share the item with the application group - attributes.accessGroup = ApplicationConfiguration.securityGroupIdentifier - - // Make sure the keychain item is available after the first unlock to enable - // automatic key rotation in background (from the packet tunnel process) - attributes.accessible = Self.keychainAccessibleLevel - - // Store value - attributes.valueData = data - - return Keychain.add(attributes) - .mapError { .addEntry($0) } - .map { _ in () } - } - } - - /// This is a migration path for the existing Keychain entries created by 2020.2 or before. - /// - /// - Set the appropriate `accessible` so that the Packet Tunnel can access the tunnel - /// configuration when the device is locked. - /// - Add revision field - /// - /// - Returns: A boolean that indicates whether the entry was up to date prior to the - /// migration request. - - static func migrateKeychainEntry(searchTerm: KeychainSearchTerm) -> Result<Bool> { - var queryAttributes = searchTerm.makeKeychainAttributes() - queryAttributes.return = [.attributes] - - return Keychain.findFirst(query: queryAttributes) - .mapError { .lookupEntry($0) } - .flatMap { itemAttributes -> Result<Bool> in - let searchAttributes = searchTerm.makeKeychainAttributes() - var updateAttributes = Keychain.Attributes() - - // Fix the accessibility permission for the Keychain entry - if itemAttributes?.accessible != Self.keychainAccessibleLevel { - updateAttributes.accessible = Self.keychainAccessibleLevel - } - - // Return immediately if nothing to update (i.e the keychain query is empty) - if updateAttributes.keychainRepresentation().isEmpty { - return .success(false) - } else { - return Keychain.update(query: searchAttributes, update: updateAttributes) - .mapError { .updateEntry($0) } - .map { true } - } - } - } - - /// Reads the tunnel settings from Keychain, then passes it to the given closure for - /// modifications, saves the result back to Keychain. - /// - /// The given block may run multiple times if Keychain entry was changed between read and write - /// operations. - static func update(searchTerm: KeychainSearchTerm, - using changeConfiguration: (inout TunnelSettings) -> Void) -> Result<TunnelSettings> - { - var searchQuery = searchTerm.makeKeychainAttributes() - searchQuery.return = [.attributes, .data] - - let result = Keychain.findFirst(query: searchQuery) - .mapError { .lookupEntry($0) } - .flatMap { itemAttributes -> Result<TunnelSettings> in - guard let serializedData = itemAttributes?.valueData, - let account = itemAttributes?.account else { return .failure(.missingRequiredAttributes) } - - return Self.decode(data: serializedData) - .flatMap { (tunnelConfig) -> Result<TunnelSettings> in - var tunnelConfig = tunnelConfig - changeConfiguration(&tunnelConfig) - - return Self.encode(tunnelConfig: tunnelConfig) - .flatMap { (newData) -> Result<TunnelSettings> in - // `SecItemUpdate` does not accept query parameters when using - // persistent reference, so constraint the query to account - // token instead now when we know it - let updateQuery = KeychainSearchTerm - .accountToken(account) - .makeKeychainAttributes() - - var updateAttributes = Keychain.Attributes() - updateAttributes.valueData = newData - - return Keychain.update(query: updateQuery, update: updateAttributes) - .mapError { .updateEntry($0) } - .map { tunnelConfig } - } - } - } - - return result - } - - static func remove(searchTerm: KeychainSearchTerm) -> Result<()> { - return Keychain.delete(query: searchTerm.makeKeychainAttributes()) - .mapError { .removeEntry($0) } - } - - /// Get a persistent reference to the Keychain item for the given account token - static func getPersistentKeychainReference(account: String) -> Result<Data> { - var query = KeychainSearchTerm.accountToken(account) - .makeKeychainAttributes() - query.return = [.persistentReference] - - return Keychain.findFirst(query: query) - .mapError { .lookupEntry($0) } - .flatMap { attributes -> Result<Data> in - guard let persistentReference = attributes?.valuePersistentReference else { - return .failure(.missingRequiredAttributes) - } - return .success(persistentReference) - } - } - - /// Verify that the keychain entry exists. - /// Returns an error in case of failure to access Keychain. - static func exists(searchTerm: KeychainSearchTerm) -> Result<Bool> { - let query = searchTerm.makeKeychainAttributes() - - return Keychain.findFirst(query: query) - .map({ (attributes) -> Bool in - return true - }) - .flatMapError({ (error) -> Result<Bool> in - if case .itemNotFound = error { - return .success(false) - } else { - return .failure(.lookupEntry(error)) - } - }) - } - - private static func encode(tunnelConfig: TunnelSettings) -> Result<Data> { - return Swift.Result { try JSONEncoder().encode(tunnelConfig) } - .mapError { .encode($0) } - } - - private static func decode(data: Data) -> Result<TunnelSettings> { - return Swift.Result { try JSONDecoder().decode(TunnelSettings.self, from: data) } - .mapError { .decode($0) } - } -} 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) diff --git a/ios/PacketTunnel/PacketTunnelProvider.swift b/ios/PacketTunnel/PacketTunnelProvider.swift index 2001f405fc..45052da9a2 100644 --- a/ios/PacketTunnel/PacketTunnelProvider.swift +++ b/ios/PacketTunnel/PacketTunnelProvider.swift @@ -32,11 +32,11 @@ class PacketTunnelProvider: NEPacketTunnelProvider, TunnelMonitorDelegate { /// A system completion handler passed from startTunnel and saved for later use once the /// connection is established. - private var startTunnelCompletionHandler: ((PacketTunnelProviderError?) -> Void)? + private var startTunnelCompletionHandler: ((Error?) -> Void)? /// A completion handler passed during reassertion and saved for later use once the connection /// is reestablished. - private var reassertTunnelCompletionHandler: ((PacketTunnelProviderError?) -> Void)? + private var reassertTunnelCompletionHandler: ((Error?) -> Void)? /// Tunnel monitor. private var tunnelMonitor: TunnelMonitor! @@ -96,12 +96,14 @@ class PacketTunnelProvider: NEPacketTunnelProvider, TunnelMonitorDelegate { // Read tunnel configuration. let tunnelConfiguration: PacketTunnelConfiguration - switch makeConfiguration(appSelectorResult) { - case .success(let configuration): - tunnelConfiguration = configuration + do { + tunnelConfiguration = try makeConfiguration(nil) + } catch { + providerLogger.error( + chainedError: AnyChainedError(error), + message: "Failed to start the tunnel." + ) - case .failure(let error): - providerLogger.error(chainedError: error, message: "Failed to start the tunnel.") completionHandler(error) return } @@ -190,7 +192,10 @@ class PacketTunnelProvider: NEPacketTunnelProvider, TunnelMonitorDelegate { guard let self = self else { return } if let error = error { - self.providerLogger.error(chainedError: error, message: "Failed to reload tunnel settings.") + self.providerLogger.error( + chainedError: AnyChainedError(error), + message: "Failed to reload tunnel settings." + ) } else { self.providerLogger.debug("Reloaded tunnel settings.") } @@ -241,7 +246,7 @@ class PacketTunnelProvider: NEPacketTunnelProvider, TunnelMonitorDelegate { providerLogger.debug("Recover connection. Picking next relay...") - let handleRecoveryFailure = { (_ error: PacketTunnelProviderError) in + let handleRecoveryFailure = { (_ error: Error) in // Stop tunnel monitor. tunnelMonitor.stop() @@ -260,11 +265,9 @@ class PacketTunnelProvider: NEPacketTunnelProvider, TunnelMonitorDelegate { // Read tunnel configuration. let tunnelConfiguration: PacketTunnelConfiguration - switch makeConfiguration(nil) { - case .success(let configuration): - tunnelConfiguration = configuration - - case .failure(let error): + do { + tunnelConfiguration = try makeConfiguration(nil) + } catch { handleRecoveryFailure(error) return } @@ -278,7 +281,10 @@ class PacketTunnelProvider: NEPacketTunnelProvider, TunnelMonitorDelegate { adapter.update(tunnelConfiguration: tunnelConfiguration.wgTunnelConfig) { error in self.dispatchQueue.async { if let error = error { - handleRecoveryFailure(.updateWireguardConfiguration(error)) + let providerError: PacketTunnelProviderError = + .updateWireGuardConfiguration(error) + + handleRecoveryFailure(providerError) } } } @@ -296,50 +302,35 @@ class PacketTunnelProvider: NEPacketTunnelProvider, TunnelMonitorDelegate { // MARK: - Private - private func makeConfiguration(_ appSelectorResult: RelaySelectorResult? = nil) -> Result<PacketTunnelConfiguration, PacketTunnelProviderError> { - guard let passwordRef = protocolConfiguration.passwordReference else { - return .failure(.missingKeychainConfigurationReference) - } - - let keychainEntry: TunnelSettingsManager.KeychainEntry - switch TunnelSettingsManager.load(searchTerm: .persistentReference(passwordRef)) { - case .success(let entry): - keychainEntry = entry - case .failure(let error): - return .failure(.cannotReadTunnelSettings(error)) + private func makeConfiguration(_ appSelectorResult: RelaySelectorResult? = nil) + throws -> PacketTunnelConfiguration + { + let tunnelSettings: TunnelSettingsV2 + do { + tunnelSettings = try SettingsManager.readSettings() + } catch { + throw PacketTunnelProviderError.readSettings(error) } - let selectorResult: RelaySelectorResult - if let appSelectorResult = appSelectorResult { - selectorResult = appSelectorResult - } else { - let relayConstraints = keychainEntry.tunnelSettings.relayConstraints - switch Self.selectRelayEndpoint(relayConstraints: relayConstraints) { - case .success(let value): - selectorResult = value - case .failure(let error): - return .failure(error) - } - } + let selectorResult = try appSelectorResult + ?? Self.selectRelayEndpoint( + relayConstraints: tunnelSettings.relayConstraints + ) - let tunnelConfiguration = PacketTunnelConfiguration( - tunnelSettings: keychainEntry.tunnelSettings, + return PacketTunnelConfiguration( + tunnelSettings: tunnelSettings, selectorResult: selectorResult ) - - return .success(tunnelConfiguration) } - private func reloadTunnelSettings(completionHandler: @escaping (PacketTunnelProviderError?) -> Void) { + private func reloadTunnelSettings(completionHandler: @escaping (Error?) -> Void) { dispatchPrecondition(condition: .onQueue(dispatchQueue)) // Read tunnel configuration. let tunnelConfiguration: PacketTunnelConfiguration - switch makeConfiguration(nil) { - case .success(let configuration): - tunnelConfiguration = configuration - - case .failure(let error): + do { + tunnelConfiguration = try makeConfiguration(nil) + } catch { completionHandler(error) return } @@ -379,7 +370,7 @@ class PacketTunnelProvider: NEPacketTunnelProvider, TunnelMonitorDelegate { } // Call completion handler immediately. - completionHandler(.updateWireguardConfiguration(error)) + completionHandler(PacketTunnelProviderError.updateWireGuardConfiguration(error)) } else { // Store completion handler and call it from TunnelMonitorDelegate once // the connection is established. @@ -412,45 +403,52 @@ class PacketTunnelProvider: NEPacketTunnelProvider, TunnelMonitorDelegate { /// Load relay cache with potential networking to refresh the cache and pick the relay for the /// given relay constraints. - private class func selectRelayEndpoint(relayConstraints: RelayConstraints) -> Result<RelaySelectorResult, PacketTunnelProviderError> { - let cacheFileURL = RelayCache.IO.defaultCacheFileURL(forSecurityApplicationGroupIdentifier: ApplicationConfiguration.securityGroupIdentifier)! + private class func selectRelayEndpoint(relayConstraints: RelayConstraints) + throws -> RelaySelectorResult + { + let cacheFileURL = RelayCache.IO.defaultCacheFileURL( + forSecurityApplicationGroupIdentifier: ApplicationConfiguration.securityGroupIdentifier + )! let prebundledRelaysURL = RelayCache.IO.preBundledRelaysFileURL! + let cachedRelayList: RelayCache.CachedRelays + do { + cachedRelayList = try RelayCache.IO.readWithFallback( + cacheFileURL: cacheFileURL, + preBundledRelaysFileURL: prebundledRelaysURL + ) + } catch { + throw PacketTunnelProviderError.readRelayCache(error) + } - return RelayCache.IO.readWithFallback(cacheFileURL: cacheFileURL, preBundledRelaysFileURL: prebundledRelaysURL) - .mapError { error in - return PacketTunnelProviderError.readRelayCache(error) - } - .flatMap { cachedRelayList in - if let selectorResult = RelaySelector.evaluate(relays: cachedRelayList.relays, constraints: relayConstraints) { - return .success(selectorResult) - } else { - return .failure(.noRelaySatisfyingConstraint) - } - } + if let selectorResult = RelaySelector.evaluate( + relays: cachedRelayList.relays, + constraints: relayConstraints + ) { + return selectorResult + } else { + throw PacketTunnelProviderError.noRelaySatisfyingConstraint + } } } enum PacketTunnelProviderError: ChainedError { - /// Failure to read the relay cache - case readRelayCache(RelayCache.Error) + /// Failure to read the relay cache. + case readRelayCache(Error) - /// Failure to satisfy the relay constraint + /// Failure to satisfy the relay constraint. case noRelaySatisfyingConstraint - /// Missing the persistent keychain reference to the tunnel settings - case missingKeychainConfigurationReference - - /// Failure to read the tunnel settings from Keychain - case cannotReadTunnelSettings(TunnelSettingsManager.Error) + /// Failure to read settings from keychain. + case readSettings(Error) - /// Failure to start the Wireguard backend + /// Failure to start the Wireguard backend. case startWireguardAdapter(WireGuardAdapterError) - /// Failure to stop the Wireguard backend + /// Failure to stop the Wireguard backend. case stopWireguardAdapter(WireGuardAdapterError) - /// Failure to update the Wireguard configuration - case updateWireguardConfiguration(WireGuardAdapterError) + /// Failure to update Wireguard configuration. + case updateWireGuardConfiguration(WireGuardAdapterError) var errorDescription: String? { switch self { @@ -460,11 +458,8 @@ enum PacketTunnelProviderError: ChainedError { case .noRelaySatisfyingConstraint: return "No relay satisfying the given constraint." - case .missingKeychainConfigurationReference: - return "Keychain configuration reference is not set on protocol configuration." - - case .cannotReadTunnelSettings: - return "Failure to read tunnel settings." + case .readSettings: + return "Failure to read settings." case .startWireguardAdapter: return "Failure to start the WireGuard adapter." @@ -472,14 +467,14 @@ enum PacketTunnelProviderError: ChainedError { case .stopWireguardAdapter: return "Failure to stop the WireGuard adapter." - case .updateWireguardConfiguration: - return "Failure to update the Wireguard configuration." + case .updateWireGuardConfiguration: + return "Failure to update the WireGuard configuration." } } } struct PacketTunnelConfiguration { - var tunnelSettings: TunnelSettings + var tunnelSettings: TunnelSettingsV2 var selectorResult: RelaySelectorResult } @@ -502,17 +497,22 @@ extension PacketTunnelConfiguration { return peerConfig } - var interfaceConfig = InterfaceConfiguration(privateKey: tunnelSettings.interface.privateKey.privateKey) + var interfaceConfig = InterfaceConfiguration( + privateKey: tunnelSettings.device.wgKeyData.privateKey + ) interfaceConfig.listenPort = 0 interfaceConfig.dns = dnsServers.map { DNSServer(address: $0) } - interfaceConfig.addresses = tunnelSettings.interface.addresses + interfaceConfig.addresses = [ + tunnelSettings.device.ipv4Address, + tunnelSettings.device.ipv6Address + ] return TunnelConfiguration(name: nil, interface: interfaceConfig, peers: peerConfigs) } var dnsServers: [IPAddress] { let mullvadEndpoint = selectorResult.endpoint - let dnsSettings = tunnelSettings.interface.dnsSettings + let dnsSettings = tunnelSettings.dnsSettings if dnsSettings.effectiveEnableCustomDNS { let dnsServers = dnsSettings.customDNSDomains diff --git a/ios/PacketTunnel/Pinger.swift b/ios/PacketTunnel/Pinger.swift index fd76fdc5ec..feb0fd678a 100644 --- a/ios/PacketTunnel/Pinger.swift +++ b/ios/PacketTunnel/Pinger.swift @@ -33,14 +33,14 @@ final class Pinger { stop() } - func start(delay: DispatchTimeInterval, repeating repeatInterval: DispatchTimeInterval) -> Result<(), Pinger.Error> { + func start(delay: DispatchTimeInterval, repeating repeatInterval: DispatchTimeInterval) throws { stateLock.lock() defer { stateLock.unlock() } stop() guard let newSocket = CFSocketCreate(kCFAllocatorDefault, AF_INET, SOCK_DGRAM, IPPROTO_ICMP, 0, nil, nil) else { - return .failure(.createSocket) + throw Error.createSocket } let flags = CFSocketGetSocketFlags(newSocket) @@ -48,12 +48,10 @@ final class Pinger { CFSocketSetSocketFlags(newSocket, flags | kCFSocketCloseOnInvalidate) } - if case .failure(let error) = bindSocket(newSocket) { - return .failure(error) - } + try bindSocket(newSocket) guard let runLoop = CFSocketCreateRunLoopSource(kCFAllocatorDefault, newSocket, 0) else { - return .failure(.createRunLoop) + throw Error.createRunLoop } CFRunLoopAddSource(CFRunLoopGetMain(), runLoop, .defaultMode) @@ -68,8 +66,6 @@ final class Pinger { newTimer.schedule(wallDeadline: .now() + delay, repeating: repeatInterval) newTimer.resume() - - return .success(()) } func stop() { @@ -135,15 +131,15 @@ final class Pinger { return nextSequenceNumber } - private func bindSocket(_ socket: CFSocket) -> Result<(), Pinger.Error> { + private func bindSocket(_ socket: CFSocket) throws { guard let interfaceName = interfaceName else { logger.debug("Interface is not specified.") - return .success(()) + return } var index = if_nametoindex(interfaceName) guard index > 0 else { - return .failure(.mapInterfaceNameToIndex(errno)) + throw Error.mapInterfaceNameToIndex(errno) } logger.debug("Bind socket to \"\(interfaceName)\" (index: \(index))...") @@ -159,9 +155,7 @@ final class Pinger { if result == -1 { logger.error("Failed to bind socket to \"\(interfaceName)\" (index: \(index), errno: \(errno)).") - return .failure(.bindSocket(errno)) - } else { - return .success(()) + throw Error.bindSocket(errno) } } diff --git a/ios/PacketTunnel/TunnelMonitor.swift b/ios/PacketTunnel/TunnelMonitor.swift index bec23409f0..b39a26d4d5 100644 --- a/ios/PacketTunnel/TunnelMonitor.swift +++ b/ios/PacketTunnel/TunnelMonitor.swift @@ -125,16 +125,16 @@ final class TunnelMonitor { stopPinging() } - private func startPinging(address: IPv4Address) -> Result<(), Pinger.Error> { + private func startPinging(address: IPv4Address) throws { let newPinger = Pinger(address: address, interfaceName: adapter.interfaceName) - let pingerResult = newPinger.start(delay: TunnelMonitorConfiguration.pingStartDelay, repeating: TunnelMonitorConfiguration.pingInterval) - if case .success = pingerResult { - pinger = newPinger - isPinging = true - } + try newPinger.start( + delay: TunnelMonitorConfiguration.pingStartDelay, + repeating: TunnelMonitorConfiguration.pingInterval + ) - return pingerResult + pinger = newPinger + isPinging = true } private func stopPinging() { @@ -230,8 +230,9 @@ final class TunnelMonitor { case (true, false): logger.debug("Network is reachable. Starting to ping.") - switch startPinging(address: address) { - case .success: + do { + try startPinging(address: address) + // Reset the last recovery attempt date. firstAttemptDate = Date() lastAttemptDate = firstAttemptDate @@ -242,10 +243,14 @@ final class TunnelMonitor { delegateQueue.async { self.delegate?.tunnelMonitor(self, networkReachabilityStatusDidChange: isNetworkReachable) } + } catch { + let error = error as! Pinger.Error - case .failure(let error): if error != lastError { - logger.error(chainedError: AnyChainedError(error), message: "Failed to start pinging.") + logger.error( + chainedError: AnyChainedError(error), + message: "Failed to start pinging." + ) lastError = error } } |
