summaryrefslogtreecommitdiffhomepage
diff options
context:
space:
mode:
authorsajacl <sajaclvishkai@gmail.com>2022-11-13 12:27:29 +0100
committersajacl <sajaclvishkai@gmail.com>2022-11-23 17:17:08 +0100
commitd90d269564a682ef32731f720a8ae8bfab5b3990 (patch)
treec8d99b4cd2bff7bc92da74f46bb2db7faaf26b5a
parent8c51336473062557817f8421755bb14a8a282a80 (diff)
downloadmullvadvpn-d90d269564a682ef32731f720a8ae8bfab5b3990.tar.xz
mullvadvpn-d90d269564a682ef32731f720a8ae8bfab5b3990.zip
Add version and migration to settings.
Introduced `KeychainFacade` as store reader/writer. Introduced `SettingsStorageMiddleware` for version handler and serialization wrapper. Introduced `SettingsManager.migrateStore` method for migration logic handler. Introduced migration protocol. Introduced `MigrationFromV1ToV2` for handling migration from legacy settings (v1) to v2. Introduced `MigrationFromUnversionedToV2` for handling migration from unversioned settings, to storing version (v2) alongside data.
-rw-r--r--ios/MullvadVPN.xcodeproj/project.pbxproj57
-rw-r--r--ios/MullvadVPN/AppDelegate.swift27
-rw-r--r--ios/MullvadVPN/SettingsManager/KeychainSettingsStore.swift103
-rw-r--r--ios/MullvadVPN/SettingsManager/SettingsManager.swift287
-rw-r--r--ios/MullvadVPN/SettingsManager/SettingsMigration/Migration.swift17
-rw-r--r--ios/MullvadVPN/SettingsManager/SettingsMigration/MigrationFromV1ToV2.swift177
-rw-r--r--ios/MullvadVPN/SettingsManager/SettingsParser.swift68
-rw-r--r--ios/MullvadVPN/SettingsManager/SettingsStore.swift15
-rw-r--r--ios/MullvadVPN/SettingsManager/TunnelSettingsV2.swift9
-rw-r--r--ios/MullvadVPN/TunnelManager/LoadTunnelConfigurationOperation.swift34
-rw-r--r--ios/MullvadVPN/TunnelManager/MigrateSettingsOperation.swift259
-rw-r--r--ios/MullvadVPN/TunnelManager/TunnelManager.swift23
12 files changed, 653 insertions, 423 deletions
diff --git a/ios/MullvadVPN.xcodeproj/project.pbxproj b/ios/MullvadVPN.xcodeproj/project.pbxproj
index cd0e4f5d84..4f7aaa8a3e 100644
--- a/ios/MullvadVPN.xcodeproj/project.pbxproj
+++ b/ios/MullvadVPN.xcodeproj/project.pbxproj
@@ -24,6 +24,13 @@
063F027F2902B6EB001FA09F /* CachedRelays.swift in Sources */ = {isa = PBXBuildFile; fileRef = 585DA87626B024A600B8C587 /* CachedRelays.swift */; };
063F028A2902B7B2001FA09F /* WireGuardKitTypes in Frameworks */ = {isa = PBXBuildFile; productRef = 063F02892902B7B2001FA09F /* WireGuardKitTypes */; };
063F028F2902BD8C001FA09F /* relays.json in Resources */ = {isa = PBXBuildFile; fileRef = 58F3C0A524A50155003E76BE /* relays.json */; };
+ 06410DFE292CE18F00AFC18C /* KeychainSettingsStore.swift in Sources */ = {isa = PBXBuildFile; fileRef = 06410DFD292CE18F00AFC18C /* KeychainSettingsStore.swift */; };
+ 06410DFF292CF16C00AFC18C /* KeychainSettingsStore.swift in Sources */ = {isa = PBXBuildFile; fileRef = 06410DFD292CE18F00AFC18C /* KeychainSettingsStore.swift */; };
+ 06410E04292D0F7100AFC18C /* SettingsParser.swift in Sources */ = {isa = PBXBuildFile; fileRef = 06410E03292D0F7100AFC18C /* SettingsParser.swift */; };
+ 06410E05292D0FC000AFC18C /* SettingsParser.swift in Sources */ = {isa = PBXBuildFile; fileRef = 06410E03292D0F7100AFC18C /* SettingsParser.swift */; };
+ 06410E07292D108E00AFC18C /* SettingsStore.swift in Sources */ = {isa = PBXBuildFile; fileRef = 06410E06292D108E00AFC18C /* SettingsStore.swift */; };
+ 06410E08292D117800AFC18C /* SettingsStore.swift in Sources */ = {isa = PBXBuildFile; fileRef = 06410E06292D108E00AFC18C /* SettingsStore.swift */; };
+ 06410E09292D990C00AFC18C /* Result+Extensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = 58F1311427E0B2AB007AC5BC /* Result+Extensions.swift */; };
06799ACE28F98E1D00ACD94E /* MullvadREST.h in Headers */ = {isa = PBXBuildFile; fileRef = 06799ABE28F98E1D00ACD94E /* MullvadREST.h */; settings = {ATTRIBUTES = (Public, ); }; };
06799AD128F98E1D00ACD94E /* MullvadREST.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 06799ABC28F98E1D00ACD94E /* MullvadREST.framework */; };
06799AD228F98E1D00ACD94E /* MullvadREST.framework in Embed Frameworks */ = {isa = PBXBuildFile; fileRef = 06799ABC28F98E1D00ACD94E /* MullvadREST.framework */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; };
@@ -51,6 +58,10 @@
06799AF328F98E4800ACD94E /* RESTAuthenticationProxy.swift in Sources */ = {isa = PBXBuildFile; fileRef = 06FAE67028F83CA40033DD93 /* RESTAuthenticationProxy.swift */; };
06799AF428F98E4800ACD94E /* RESTAuthorization.swift in Sources */ = {isa = PBXBuildFile; fileRef = 06FAE67928F83CA50033DD93 /* RESTAuthorization.swift */; };
06799AFC28F98EE300ACD94E /* AddressCache.swift in Sources */ = {isa = PBXBuildFile; fileRef = 06AC114128F8413A0037AF9A /* AddressCache.swift */; };
+ 068CE57029278F5300A068BB /* MigrationFromV1ToV2.swift in Sources */ = {isa = PBXBuildFile; fileRef = 068CE56F29278F5300A068BB /* MigrationFromV1ToV2.swift */; };
+ 068CE57229278F6D00A068BB /* MigrationFromV1ToV2.swift in Sources */ = {isa = PBXBuildFile; fileRef = 068CE56F29278F5300A068BB /* MigrationFromV1ToV2.swift */; };
+ 068CE5742927B7A400A068BB /* Migration.swift in Sources */ = {isa = PBXBuildFile; fileRef = 068CE5732927B7A400A068BB /* Migration.swift */; };
+ 068CE5782927BE4800A068BB /* Migration.swift in Sources */ = {isa = PBXBuildFile; fileRef = 068CE5732927B7A400A068BB /* Migration.swift */; };
0697D6E728F01513007A9E99 /* TransportMonitor.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0697D6E628F01513007A9E99 /* TransportMonitor.swift */; };
06AC116228F94C450037AF9A /* ApplicationConfiguration.swift in Sources */ = {isa = PBXBuildFile; fileRef = 58BFA5CB22A7CE1F00A6173D /* ApplicationConfiguration.swift */; };
06D47B7B28F98F53008E762C /* libMullvadTypes.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 581943F128F8014500B0CB5E /* libMullvadTypes.a */; };
@@ -71,7 +82,6 @@
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 */; };
- 58161C9C28352F850028ECFD /* MigrateSettingsOperation.swift in Sources */ = {isa = PBXBuildFile; fileRef = 58161C9B28352F850028ECFD /* MigrateSettingsOperation.swift */; };
5818139F28E09BD8002817DE /* libOperations.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 58E5126528DDF04200B0BCDE /* libOperations.a */; };
581813A128E09DBB002817DE /* NoCancelledDependenciesCondition.swift in Sources */ = {isa = PBXBuildFile; fileRef = 581813A028E09DBB002817DE /* NoCancelledDependenciesCondition.swift */; };
581813A328E09DCD002817DE /* NoFailedDependenciesCondition.swift in Sources */ = {isa = PBXBuildFile; fileRef = 581813A228E09DCD002817DE /* NoFailedDependenciesCondition.swift */; };
@@ -365,6 +375,13 @@
remoteGlobalIDString = 063F02722902B63F001FA09F;
remoteInfo = RelayCache;
};
+ 06410DF9292C4ABC00AFC18C /* PBXContainerItemProxy */ = {
+ isa = PBXContainerItemProxy;
+ containerPortal = 58CE5E58224146200008646E /* Project object */;
+ proxyType = 1;
+ remoteGlobalIDString = 58CE5E5F224146200008646E;
+ remoteInfo = MullvadVPN;
+ };
06799ACF28F98E1D00ACD94E /* PBXContainerItemProxy */ = {
isa = PBXContainerItemProxy;
containerPortal = 58CE5E58224146200008646E /* Project object */;
@@ -522,9 +539,14 @@
063687B928EB234F00BE7161 /* PacketTunnelTransport.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PacketTunnelTransport.swift; sourceTree = "<group>"; };
063F02732902B63F001FA09F /* RelayCache.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = RelayCache.framework; sourceTree = BUILT_PRODUCTS_DIR; };
063F02752902B63F001FA09F /* RelayCache.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = RelayCache.h; sourceTree = "<group>"; };
+ 06410DFD292CE18F00AFC18C /* KeychainSettingsStore.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = KeychainSettingsStore.swift; sourceTree = "<group>"; };
+ 06410E03292D0F7100AFC18C /* SettingsParser.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SettingsParser.swift; sourceTree = "<group>"; };
+ 06410E06292D108E00AFC18C /* SettingsStore.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SettingsStore.swift; sourceTree = "<group>"; };
06799AB428F98CE700ACD94E /* le_root_cert.cer */ = {isa = PBXFileReference; lastKnownFileType = file; path = le_root_cert.cer; sourceTree = "<group>"; };
06799ABC28F98E1D00ACD94E /* MullvadREST.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = MullvadREST.framework; sourceTree = BUILT_PRODUCTS_DIR; };
06799ABE28F98E1D00ACD94E /* MullvadREST.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = MullvadREST.h; sourceTree = "<group>"; };
+ 068CE56F29278F5300A068BB /* MigrationFromV1ToV2.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MigrationFromV1ToV2.swift; sourceTree = "<group>"; };
+ 068CE5732927B7A400A068BB /* Migration.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Migration.swift; sourceTree = "<group>"; };
0697D6E628F01513007A9E99 /* TransportMonitor.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TransportMonitor.swift; sourceTree = "<group>"; };
06AC113628F83FD70037AF9A /* Cancellable.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Cancellable.swift; sourceTree = "<group>"; };
06AC114028F841390037AF9A /* AddressCacheTracker.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AddressCacheTracker.swift; sourceTree = "<group>"; };
@@ -568,7 +590,6 @@
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>"; };
- 58161C9B28352F850028ECFD /* MigrateSettingsOperation.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MigrateSettingsOperation.swift; sourceTree = "<group>"; };
581813A028E09DBB002817DE /* NoCancelledDependenciesCondition.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NoCancelledDependenciesCondition.swift; sourceTree = "<group>"; };
581813A228E09DCD002817DE /* NoFailedDependenciesCondition.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NoFailedDependenciesCondition.swift; sourceTree = "<group>"; };
581813A428E09DE2002817DE /* BlockCondition.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BlockCondition.swift; sourceTree = "<group>"; };
@@ -1005,10 +1026,23 @@
path = MullvadREST;
sourceTree = "<group>";
};
+ 068CE57129278F5F00A068BB /* SettingsMigration */ = {
+ isa = PBXGroup;
+ children = (
+ 068CE5732927B7A400A068BB /* Migration.swift */,
+ 068CE56F29278F5300A068BB /* MigrationFromV1ToV2.swift */,
+ );
+ path = SettingsMigration;
+ sourceTree = "<group>";
+ };
580F8B88281A79A7002E0998 /* SettingsManager */ = {
isa = PBXGroup;
children = (
+ 068CE57129278F5F00A068BB /* SettingsMigration */,
+ 06410DFD292CE18F00AFC18C /* KeychainSettingsStore.swift */,
58FF2C02281BDE02009EF542 /* SettingsManager.swift */,
+ 06410E03292D0F7100AFC18C /* SettingsParser.swift */,
+ 06410E06292D108E00AFC18C /* SettingsStore.swift */,
587AD7C523421D7000E93A53 /* TunnelSettingsV1.swift */,
580F8B8228197881002E0998 /* TunnelSettingsV2.swift */,
58421033282E4B1500F24E46 /* TunnelSettingsV2+REST.swift */,
@@ -1061,7 +1095,6 @@
children = (
588527B1276B3F0700BAA373 /* LoadTunnelConfigurationOperation.swift */,
58F2E147276A307400A79513 /* MapConnectionStatusOperation.swift */,
- 58161C9B28352F850028ECFD /* MigrateSettingsOperation.swift */,
584B17AA27637DE40057F3B8 /* ReconnectTunnelOperation.swift */,
58F2E14B276A61C000A79513 /* RotateKeyOperation.swift */,
586E54FA27A2DF6D0029B88B /* SendTunnelProviderMessageOperation.swift */,
@@ -1664,6 +1697,7 @@
dependencies = (
062B45BF28FDA85D00746E77 /* PBXTargetDependency */,
58D889B528DDF4DD00583FA8 /* PBXTargetDependency */,
+ 06410DFA292C4ABC00AFC18C /* PBXTargetDependency */,
);
name = MullvadVPNTests;
packageProductDependencies = (
@@ -2158,13 +2192,13 @@
5872631B283F6EAB00E14ADF /* Intents.intentdefinition in Sources */,
58F8AC0E25D3F8CE002BE0ED /* ProblemReportReviewViewController.swift in Sources */,
5878A27129091CF20096FC88 /* AccountInteractor.swift in Sources */,
+ 068CE5742927B7A400A068BB /* Migration.swift in Sources */,
58CCA010224249A1004F3011 /* ConnectViewController.swift in Sources */,
5893716A28817A45004EE76C /* DeviceManagementViewController.swift in Sources */,
58BFA5C622A7C97F00A6173D /* RelayCacheTracker.swift in Sources */,
E158B360285381C60002F069 /* StringFormatter.swift in Sources */,
582BB1B1229569620055B6EF /* CustomNavigationBar.swift in Sources */,
58B3F30F2742708B00A2DD38 /* HeaderBarButton.swift in Sources */,
- 58161C9C28352F850028ECFD /* MigrateSettingsOperation.swift in Sources */,
58ACF6492655365700ACE4B7 /* PreferencesViewController.swift in Sources */,
E1FD0DF528AA7CE400299DB4 /* StatusActivityView.swift in Sources */,
0697D6E728F01513007A9E99 /* TransportMonitor.swift in Sources */,
@@ -2176,6 +2210,7 @@
58ACF64D26567A5000ACE4B7 /* CustomSwitch.swift in Sources */,
58F2E14C276A61C000A79513 /* RotateKeyOperation.swift in Sources */,
5871FB96254ADE4E0051A0A4 /* ConsolidatedApplicationLog.swift in Sources */,
+ 068CE57029278F5300A068BB /* MigrationFromV1ToV2.swift in Sources */,
58FEEB58260B662E00A621A8 /* AutomaticKeyboardResponder.swift in Sources */,
5846227326E22A160035F7C2 /* StorePaymentObserver.swift in Sources */,
58F2E146276A2C9900A79513 /* StopTunnelOperation.swift in Sources */,
@@ -2233,6 +2268,7 @@
5892A45E265FABFF00890742 /* EmptyTableViewHeaderFooterView.swift in Sources */,
580909D32876D09A0078138D /* RevokedDeviceViewController.swift in Sources */,
5835B7CC233B76CB0096D79F /* TunnelManager.swift in Sources */,
+ 06410E07292D108E00AFC18C /* SettingsStore.swift in Sources */,
586A950D290125F0007BAF2B /* PresentAlertOperation.swift in Sources */,
58B93A1326C3F13600A55733 /* TunnelState.swift in Sources */,
58FEEB46260A028D00A621A8 /* GeoJSON.swift in Sources */,
@@ -2245,10 +2281,12 @@
5857F24324C8662600CF6F47 /* SelectLocationHeaderView.swift in Sources */,
58EE2E3A272FF814003BFF93 /* SettingsDataSource.swift in Sources */,
58421032282E42B000F24E46 /* UpdateDeviceDataOperation.swift in Sources */,
+ 06410E04292D0F7100AFC18C /* SettingsParser.swift in Sources */,
5878A27D2909657C0096FC88 /* RevokedDeviceInteractor.swift in Sources */,
58677710290975E9006F721F /* SettingsInteractorFactory.swift in Sources */,
5872631D283F755900E14ADF /* IntentHandlers.swift in Sources */,
58CCA01222424D11004F3011 /* SettingsViewController.swift in Sources */,
+ 06410DFE292CE18F00AFC18C /* KeychainSettingsStore.swift in Sources */,
580F8B8628197958002E0998 /* DNSSettings.swift in Sources */,
58FB865526E8BF3100F188BC /* StorePaymentManagerError.swift in Sources */,
58FD5BF42428C67600112C88 /* InAppPurchaseButton.swift in Sources */,
@@ -2291,13 +2329,19 @@
files = (
5806767C27048E9B00C858CB /* PacketTunnelProvider.swift in Sources */,
587AD7C723421D8600E93A53 /* TunnelSettingsV1.swift in Sources */,
+ 06410E09292D990C00AFC18C /* Result+Extensions.swift in Sources */,
58CE38C828992C9200A6D6E5 /* TunnelMonitorDelegate.swift in Sources */,
+ 068CE5782927BE4800A068BB /* Migration.swift in Sources */,
58FC040A27B3EE03001C21F0 /* TunnelMonitor.swift in Sources */,
5838318B27C40A3900000571 /* Pinger.swift in Sources */,
+ 06410E05292D0FC000AFC18C /* SettingsParser.swift in Sources */,
58E0729D28814AAE008902F8 /* PacketTunnelConfiguration.swift in Sources */,
58E0729F28814ACC008902F8 /* WireGuardLogLevel+Logging.swift in Sources */,
580F8B8428197884002E0998 /* TunnelSettingsV2.swift in Sources */,
+ 06410E08292D117800AFC18C /* SettingsStore.swift in Sources */,
58906DE02445C7A5002F0673 /* NEProviderStopReason+Debug.swift in Sources */,
+ 068CE57229278F6D00A068BB /* MigrationFromV1ToV2.swift in Sources */,
+ 06410DFF292CF16C00AFC18C /* KeychainSettingsStore.swift in Sources */,
580F8B872819795C002E0998 /* DNSSettings.swift in Sources */,
58E072A128814B0E008902F8 /* MullvadEndpoint+WgEndpoint.swift in Sources */,
06AC116228F94C450037AF9A /* ApplicationConfiguration.swift in Sources */,
@@ -2385,6 +2429,11 @@
target = 063F02722902B63F001FA09F /* RelayCache */;
targetProxy = 063F028B2902B83C001FA09F /* PBXContainerItemProxy */;
};
+ 06410DFA292C4ABC00AFC18C /* PBXTargetDependency */ = {
+ isa = PBXTargetDependency;
+ target = 58CE5E5F224146200008646E /* MullvadVPN */;
+ targetProxy = 06410DF9292C4ABC00AFC18C /* PBXContainerItemProxy */;
+ };
06799AD028F98E1D00ACD94E /* PBXTargetDependency */ = {
isa = PBXTargetDependency;
target = 06799ABB28F98E1D00ACD94E /* MullvadREST */;
diff --git a/ios/MullvadVPN/AppDelegate.swift b/ios/MullvadVPN/AppDelegate.swift
index 00165f9021..b11ada5f7c 100644
--- a/ios/MullvadVPN/AppDelegate.swift
+++ b/ios/MullvadVPN/AppDelegate.swift
@@ -110,21 +110,26 @@ class AppDelegate: UIResponder, UIApplicationDelegate, UNUserNotificationCenterD
setupNotificationHandler()
addApplicationNotifications(application: application)
- let setupTunnelManagerOperation = AsyncBlockOperation(dispatchQueue: .main) { operation in
- self.tunnelManager.loadConfiguration { error in
- // TODO: avoid throwing fatal error and show the problem report UI instead.
- if let error = error {
- fatalError(error.localizedDescription)
- }
+ let setupTunnelManagerOperation =
+ AsyncBlockOperation(dispatchQueue: .main) { operation in
+ SettingsManager.migrateStore(with: self.proxyFactory) { error in
+ precondition(error == nil)
+
+ self.tunnelManager.loadConfiguration { error in
+ // TODO: avoid throwing fatal error and show the problem report UI instead.
+ if let error = error {
+ fatalError(error.localizedDescription)
+ }
- self.logger.debug("Finished initialization.")
+ self.logger.debug("Finished initialization.")
- NotificationManager.shared.updateNotifications()
- self.storePaymentManager.startPaymentQueueMonitoring()
+ NotificationManager.shared.updateNotifications()
+ self.storePaymentManager.startPaymentQueueMonitoring()
- operation.finish()
+ operation.finish()
+ }
+ }
}
- }
operationQueue.addOperation(setupTunnelManagerOperation)
diff --git a/ios/MullvadVPN/SettingsManager/KeychainSettingsStore.swift b/ios/MullvadVPN/SettingsManager/KeychainSettingsStore.swift
new file mode 100644
index 0000000000..f98ee06a6f
--- /dev/null
+++ b/ios/MullvadVPN/SettingsManager/KeychainSettingsStore.swift
@@ -0,0 +1,103 @@
+//
+// KeychainSettingsStore.swift
+// MullvadVPN
+//
+// Created by Sajad Vishkai on 2022-11-22.
+// Copyright © 2022 Mullvad VPN AB. All rights reserved.
+//
+
+import Foundation
+import MullvadTypes
+import Security
+
+class KeychainSettingsStore: SettingsStore {
+ let keychainServiceName: String
+
+ init(keychainServiceName: String) {
+ self.keychainServiceName = keychainServiceName
+ }
+
+ func read(key: SettingsKey) throws -> Data {
+ try readItemData(key)
+ }
+
+ func write(_ data: Data, for key: SettingsKey) throws {
+ try addOrUpdateItem(key, data: data)
+ }
+
+ func delete(key: SettingsKey) throws {
+ try deleteItem(key)
+ }
+
+ private func addItem(_ item: SettingsKey, data: Data) throws {
+ var query = createDefaultAttributes(item: item)
+ query.merge(createAccessAttributes()) { current, _ in
+ return current
+ }
+ query[kSecValueData] = data
+
+ let status = SecItemAdd(query as CFDictionary, nil)
+ if status != errSecSuccess {
+ throw KeychainError(code: status)
+ }
+ }
+
+ private func updateItem(_ item: SettingsKey, data: Data) throws {
+ let query = createDefaultAttributes(item: item)
+ let status = SecItemUpdate(
+ query as CFDictionary,
+ [kSecValueData: data] as CFDictionary
+ )
+
+ if status != errSecSuccess {
+ throw KeychainError(code: status)
+ }
+ }
+
+ private func addOrUpdateItem(_ item: SettingsKey, data: Data) throws {
+ do {
+ try updateItem(item, data: data)
+ } catch let error as KeychainError where error == .itemNotFound {
+ try addItem(item, data: data)
+ } catch {
+ throw error
+ }
+ }
+
+ private func readItemData(_ item: SettingsKey) throws -> Data {
+ var query = createDefaultAttributes(item: item)
+ query[kSecReturnData] = true
+
+ var result: CFTypeRef?
+ let status = SecItemCopyMatching(query as CFDictionary, &result)
+
+ if status == errSecSuccess {
+ return result as? Data ?? Data()
+ } else {
+ throw KeychainError(code: status)
+ }
+ }
+
+ private func deleteItem(_ item: SettingsKey) throws {
+ let query = createDefaultAttributes(item: item)
+ let status = SecItemDelete(query as CFDictionary)
+ if status != errSecSuccess {
+ throw KeychainError(code: status)
+ }
+ }
+
+ private func createDefaultAttributes(item: SettingsKey) -> [CFString: Any] {
+ return [
+ kSecClass: kSecClassGenericPassword,
+ kSecAttrService: keychainServiceName,
+ kSecAttrAccount: item.rawValue,
+ ]
+ }
+
+ private func createAccessAttributes() -> [CFString: Any] {
+ return [
+ kSecAttrAccessGroup: ApplicationConfiguration.securityGroupIdentifier,
+ kSecAttrAccessible: kSecAttrAccessibleAfterFirstUnlock,
+ ]
+ }
+}
diff --git a/ios/MullvadVPN/SettingsManager/SettingsManager.swift b/ios/MullvadVPN/SettingsManager/SettingsManager.swift
index aef9867b10..f0e88185b1 100644
--- a/ios/MullvadVPN/SettingsManager/SettingsManager.swift
+++ b/ios/MullvadVPN/SettingsManager/SettingsManager.swift
@@ -8,44 +8,22 @@
import Foundation
import MullvadLogging
+import MullvadREST
import MullvadTypes
-enum SettingsManager {}
-
-struct LegacyTunnelSettings {
- let accountNumber: String
- let tunnelSettings: TunnelSettingsV1
-}
-
private let keychainServiceName = "Mullvad VPN"
+private let accountTokenKey = "accountToken"
+private let accountExpiryKey = "accountExpiry"
-private enum Item: String, CaseIterable {
- case settings = "Settings"
- case deviceState = "DeviceState"
- case lastUsedAccount = "LastUsedAccount"
-}
-
-struct StringDecodingError: LocalizedError {
- let data: Data
-
- var errorDescription: String? {
- return "Failed to decode string from data."
- }
-}
-
-struct StringEncodingError: LocalizedError {
- let string: String
-
- var errorDescription: String? {
- return "Failed to encode string into data."
+enum SettingsManager {
+ private static func makeParser() -> SettingsParser {
+ SettingsParser(decoder: JSONDecoder(), encoder: JSONEncoder())
}
-}
-extension SettingsManager {
- // MARK: - Lsat used account
+ // MARK: - Last used account
static func getLastUsedAccount() throws -> String {
- let data = try readItemData(.lastUsedAccount)
+ let data = try store.read(key: .lastUsedAccount)
if let string = String(data: data, encoding: .utf8) {
return string
@@ -60,10 +38,10 @@ extension SettingsManager {
throw StringEncodingError(string: string)
}
- try addOrUpdateItem(.lastUsedAccount, data: data)
+ try store.write(data, for: .lastUsedAccount)
} else {
do {
- try deleteItem(.lastUsedAccount)
+ try store.delete(key: .lastUsedAccount)
} catch let error as KeychainError where error == .itemNotFound {
return
} catch {
@@ -72,114 +50,190 @@ extension SettingsManager {
}
}
+ private static let store: SettingsStore = KeychainSettingsStore(
+ keychainServiceName: keychainServiceName
+ )
+
// MARK: - Settings
static func readSettings() throws -> TunnelSettingsV2 {
- let data = try readItemData(.settings)
+ let data = try store.read(key: .settings)
+ let parser = makeParser()
- return try JSONDecoder().decode(TunnelSettingsV2.self, from: data)
+ let version = try parser.parseVersion(data: data)
+ let currentVersion = SchemaVersion.current.rawValue
+
+ if version == currentVersion {
+ return try parser.parsePayload(as: TunnelSettingsV2.self, from: data)
+ } else {
+ throw UnsupportedVersionSettings(storedVersion: version, currentVersion: currentVersion)
+ }
}
static func writeSettings(_ settings: TunnelSettingsV2) throws {
- let data = try JSONEncoder().encode(settings)
+ let parser = makeParser()
+ let data = try parser.producePayload(settings, version: SchemaVersion.current.rawValue)
- try addOrUpdateItem(.settings, data: data)
- }
-
- static func deleteSettings() throws {
- try deleteItem(.settings)
+ try store.write(data, for: .settings)
}
// MARK: - Device state
static func readDeviceState() throws -> DeviceState {
- let data = try readItemData(.deviceState)
+ let data = try store.read(key: .deviceState)
+ let parser = makeParser()
- return try JSONDecoder().decode(DeviceState.self, from: data)
+ return try parser.parseUnversionedPayload(as: DeviceState.self, from: data)
}
static func writeDeviceState(_ deviceState: DeviceState) throws {
- let data = try JSONEncoder().encode(deviceState)
+ let parser = makeParser()
+ let data = try parser.produceUnversionedPayload(deviceState)
- try addOrUpdateItem(.deviceState, data: data)
+ try store.write(data, for: .deviceState)
}
- static func deleteDeviceState() throws {
- try deleteItem(.deviceState)
+ // MARK: - Migration
+
+ static func migrateStore(
+ with restFactory: REST.ProxyFactory,
+ completion: @escaping (Error?) -> Void
+ ) {
+ if let legacySettings = readLegacySettings() {
+ migrateLegacySettings(
+ restFactory: restFactory,
+ legacySettings: legacySettings,
+ completion: completion
+ )
+ } else {
+ migrateModernSettings(completion: completion)
+ }
}
- // MARK: - Keychain helpers
+ private static func migrateLegacySettings(
+ restFactory: REST.ProxyFactory,
+ legacySettings: LegacyTunnelSettings,
+ completion: @escaping (Error?) -> Void
+ ) {
+ let parser = makeParser()
- private static func addItem(_ item: Item, data: Data) throws {
- var query = createDefaultAttributes(item: item)
- query.merge(createAccessAttributes()) { current, _ in
- return current
- }
- query[kSecValueData] = data
+ let migration = MigrationFromV1ToV2(
+ restFactory: restFactory,
+ legacySettings: legacySettings
+ )
- let status = SecItemAdd(query as CFDictionary, nil)
- if status != errSecSuccess {
- throw KeychainError(code: status)
+ migration.migrate(with: store, parser: parser) { error in
+ if let error = error {
+ logger.error(
+ error: error,
+ message: "Failed to migrate from legacy settings to v2."
+ )
+
+ completion(error)
+ } else {
+ let userDefaults = UserDefaults.standard
+
+ logger.debug("Remove legacy settings from keychain.")
+ Self.deleteLegacySettings()
+
+ logger.debug("Remove legacy settings from user defaults.")
+
+ userDefaults.removeObject(forKey: accountTokenKey)
+ userDefaults.removeObject(forKey: accountExpiryKey)
+
+ completion(nil)
+ }
}
}
- private static func updateItem(_ item: Item, data: Data) throws {
- let query = createDefaultAttributes(item: item)
- let status = SecItemUpdate(
- query as CFDictionary,
- [kSecValueData: data] as CFDictionary
- )
+ private static func migrateModernSettings(completion: @escaping (Error?) -> Void) {
+ let parser = makeParser()
- if status != errSecSuccess {
- throw KeychainError(code: status)
+ do {
+ let settingsData = try store.read(key: .settings)
+ let settingsVersion = try parser.parseVersion(data: settingsData)
+
+ if settingsVersion != SchemaVersion.current.rawValue {
+ let error = UnsupportedVersionSettings(
+ storedVersion: settingsVersion,
+ currentVersion: SchemaVersion.current.rawValue
+ )
+
+ logger.error(
+ error: error,
+ message: "Encountered an unknown version."
+ )
+
+ completion(error)
+
+ } else {
+ completion(nil)
+ }
+
+ } catch .itemNotFound as KeychainError {
+ completion(nil)
+ } catch {
+ completion(error)
}
}
- private static func addOrUpdateItem(_ item: Item, data: Data) throws {
+ // MARK: - Legacy settings
+
+ private static func readLegacySettings() -> LegacyTunnelSettings? {
+ 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.")
+
+ return nil
+ }
+
+ // Set legacy account number as last used.
+ logger.debug("Found legacy account number.")
+ logger.debug("Store last used account.")
+
do {
- try updateItem(item, data: data)
- } catch let error as KeychainError where error == .itemNotFound {
- try addItem(item, data: data)
+ try Self.setLastUsedAccount(storedAccountNumber)
} catch {
- throw error
+ logger.error(
+ error: error,
+ message: "Failed to store last used account."
+ )
}
- }
- private static func readItemData(_ item: Item) throws -> Data {
- var query = createDefaultAttributes(item: item)
- query[kSecReturnData] = true
+ // List legacy settings stored in keychain.
+ logger.debug("Read legacy settings...")
- var result: CFTypeRef?
- let status = SecItemCopyMatching(query as CFDictionary, &result)
+ var storedSettings: [LegacyTunnelSettings] = []
+ do {
+ storedSettings = try Self.readLegacySettings()
+ } catch .itemNotFound as KeychainError {
+ logger.debug("Legacy settings are not found in keychain.")
- if status == errSecSuccess {
- return result as? Data ?? Data()
- } else {
- throw KeychainError(code: status)
+ return nil
+ } catch {
+ logger.error(
+ error: error,
+ message: "Failed to read legacy settings from keychain."
+ )
+
+ return nil
}
- }
- private static func deleteItem(_ item: Item) throws {
- let query = createDefaultAttributes(item: item)
- let status = SecItemDelete(query as CFDictionary)
- if status != errSecSuccess {
- throw KeychainError(code: status)
+ // Find settings matching the account number stored in user defaults.
+ let matchingSettings = storedSettings.first { settings in
+ return settings.accountNumber == storedAccountNumber
}
- }
- private static func createDefaultAttributes(item: Item) -> [CFString: Any] {
- return [
- kSecClass: kSecClassGenericPassword,
- kSecAttrService: keychainServiceName,
- kSecAttrAccount: item.rawValue,
- ]
- }
+ guard let matchingSettings = matchingSettings else {
+ logger.debug(
+ "Could not find legacy settings matching the legacy account number."
+ )
- private static func createAccessAttributes() -> [CFString: Any] {
- return [
- kSecAttrAccessGroup: ApplicationConfiguration.securityGroupIdentifier,
- kSecAttrAccessible: kSecAttrAccessibleAfterFirstUnlock,
- ]
+ return nil
+ }
+
+ return matchingSettings
}
// MARK: - Legacy settings support
@@ -293,6 +347,41 @@ extension SettingsManager {
return false
}
- return Item(rawValue: accountNumber) == nil
+ return SettingsKey(rawValue: accountNumber) == nil
+ }
+}
+
+struct LegacyTunnelSettings {
+ let accountNumber: String
+ let tunnelSettings: TunnelSettingsV1
+}
+
+enum SettingsKey: String, CaseIterable {
+ case settings = "Settings"
+ case deviceState = "DeviceState"
+ case lastUsedAccount = "LastUsedAccount"
+}
+
+struct UnsupportedVersionSettings: LocalizedError {
+ let storedVersion, currentVersion: Int
+
+ var errorDescription: String? {
+ return "Stored settings version was not the same as current version, stored version: \(storedVersion), current version: \(currentVersion)"
+ }
+}
+
+struct StringDecodingError: LocalizedError {
+ let data: Data
+
+ var errorDescription: String? {
+ return "Failed to decode string from data."
+ }
+}
+
+struct StringEncodingError: LocalizedError {
+ let string: String
+
+ var errorDescription: String? {
+ return "Failed to encode string into data."
}
}
diff --git a/ios/MullvadVPN/SettingsManager/SettingsMigration/Migration.swift b/ios/MullvadVPN/SettingsManager/SettingsMigration/Migration.swift
new file mode 100644
index 0000000000..79704c7b9b
--- /dev/null
+++ b/ios/MullvadVPN/SettingsManager/SettingsMigration/Migration.swift
@@ -0,0 +1,17 @@
+//
+// Migration.swift
+// MullvadVPN
+//
+// Created by Sajad Vishkai on 2022-11-18.
+// Copyright © 2022 Mullvad VPN AB. All rights reserved.
+//
+
+import Foundation
+
+protocol Migration {
+ func migrate(
+ with store: SettingsStore,
+ parser: SettingsParser,
+ completion: @escaping (Error?) -> Void
+ )
+}
diff --git a/ios/MullvadVPN/SettingsManager/SettingsMigration/MigrationFromV1ToV2.swift b/ios/MullvadVPN/SettingsManager/SettingsMigration/MigrationFromV1ToV2.swift
new file mode 100644
index 0000000000..ea95cb9632
--- /dev/null
+++ b/ios/MullvadVPN/SettingsManager/SettingsMigration/MigrationFromV1ToV2.swift
@@ -0,0 +1,177 @@
+//
+// MigrationFromV1ToV2.swift
+// MullvadVPN
+//
+// Created by Sajad Vishkai on 2022-11-18.
+// Copyright © 2022 Mullvad VPN AB. All rights reserved.
+//
+
+import Foundation
+import MullvadLogging
+import MullvadREST
+import MullvadTypes
+import Operations
+
+class MigrationFromV1ToV2: Migration {
+ private let accountsProxy: REST.AccountsProxy
+ private let devicesProxy: REST.DevicesProxy
+
+ private var accountTask: Cancellable?
+ private var deviceTask: Cancellable?
+
+ private var accountCompletion: OperationCompletion<REST.AccountData, REST.Error>?
+ private var devicesCompletion: OperationCompletion<[REST.Device], REST.Error>?
+
+ private let legacySettings: LegacyTunnelSettings
+
+ private let logger = Logger(label: "Migration.V1ToV2")
+
+ init(
+ restFactory: REST.ProxyFactory,
+ legacySettings: LegacyTunnelSettings
+ ) {
+ accountsProxy = restFactory.createAccountsProxy()
+ devicesProxy = restFactory.createDevicesProxy()
+ self.legacySettings = legacySettings
+ }
+
+ func migrate(
+ with store: SettingsStore,
+ parser: SettingsParser,
+ completion: @escaping (Error?) -> Void
+ ) {
+ let storedAccountNumber = legacySettings.accountNumber
+
+ // 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.accountCompletion = completion
+
+ dispatchGroup.leave()
+ }
+
+ dispatchGroup.enter()
+ deviceTask = devicesProxy.getDevices(
+ accountNumber: storedAccountNumber,
+ retryStrategy: .aggressive
+ ) { completion in
+ self.devicesCompletion = completion
+
+ dispatchGroup.leave()
+ }
+
+ dispatchGroup.notify(queue: .main) {
+ switch (self.accountCompletion, self.devicesCompletion) {
+ case let (.success(accountData), .success(deviceData)):
+ // Migrate settings if all data is available.
+
+ let result = Result {
+ try self.migrateSettings(
+ store: store,
+ parser: parser,
+ settings: self.legacySettings,
+ accountData: accountData,
+ devices: deviceData
+ )
+ }
+
+ completion(result.error)
+
+ default:
+ let errors = [self.accountCompletion?.error, self.devicesCompletion?.error]
+ .compactMap { $0 }
+ completion(MigrateLegacySettingsError(underlyingErrors: errors))
+ }
+ }
+ }
+
+ private func migrateSettings(
+ store: SettingsStore,
+ parser: SettingsParser,
+ settings: LegacyTunnelSettings,
+ accountData: REST.AccountData,
+ devices: [REST.Device]
+ ) throws {
+ 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 newDeviceState = DeviceState.loggedIn(
+ StoredAccountData(
+ identifier: accountData.id,
+ number: settings.accountNumber,
+ expiry: accountData.expiry
+ ),
+ 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
+ )
+ )
+ )
+
+ let newSettings = TunnelSettingsV2(
+ relayConstraints: tunnelSettings.relayConstraints,
+ dnsSettings: interfaceData.dnsSettings
+ )
+
+ // Save settings.
+ let settingsData = try parser.producePayload(
+ newSettings,
+ version: SchemaVersion.v2.rawValue
+ )
+ let deviceData = try parser.produceUnversionedPayload(newDeviceState)
+
+ try store.write(settingsData, for: .settings)
+ try store.write(deviceData, for: .deviceState)
+ }
+}
+
+struct MigrateLegacySettingsError: WrappingError {
+ let underlyingErrors: [REST.Error]
+
+ var underlyingError: Error? {
+ return underlyingErrors.first
+ }
+
+ var errorDescription: String? {
+ return "Failed to migrate legacy settings to v2"
+ }
+}
diff --git a/ios/MullvadVPN/SettingsManager/SettingsParser.swift b/ios/MullvadVPN/SettingsManager/SettingsParser.swift
new file mode 100644
index 0000000000..b57dda6fbe
--- /dev/null
+++ b/ios/MullvadVPN/SettingsManager/SettingsParser.swift
@@ -0,0 +1,68 @@
+//
+// SettingsParser.swift
+// MullvadVPN
+//
+// Created by Sajad Vishkai on 2022-11-22.
+// Copyright © 2022 Mullvad VPN AB. All rights reserved.
+//
+
+import Foundation
+
+private struct VersionHeader: Codable {
+ var version: Int
+}
+
+private struct Payload<T: Codable>: Codable {
+ var data: T
+}
+
+private struct VersionedPayload<T: Codable>: Codable {
+ var version: Int
+ var data: T
+}
+
+struct SettingsParser {
+ /// The decoder used to decode values.
+ private let decoder: JSONDecoder
+
+ /// The encoder used to encode values.
+ private let encoder: JSONEncoder
+
+ init(decoder: JSONDecoder, encoder: JSONEncoder) {
+ self.decoder = decoder
+ self.encoder = encoder
+ }
+
+ /// Produces versioned data encoded as the given type
+ func producePayload<T: Codable>(_ payload: T, version: Int) throws -> Data {
+ return try encoder.encode(VersionedPayload(version: version, data: payload))
+ }
+
+ /// Produces unversioned data encoded as the given type
+ func produceUnversionedPayload<T: Codable>(_ payload: T) throws -> Data {
+ return try encoder.encode(payload)
+ }
+
+ /// Returns settings version if found inside the stored data.
+ func parseVersion(data: Data) throws -> Int {
+ let header = try decoder.decode(VersionHeader.self, from: data)
+
+ return header.version
+ }
+
+ /// Returns unversioned payload parsed as the given type.
+ func parseUnversionedPayload<T: Codable>(
+ as type: T.Type,
+ from data: Data
+ ) throws -> T {
+ return try decoder.decode(T.self, from: data)
+ }
+
+ /// Returns data from versioned payload parsed as the given type.
+ func parsePayload<T: Codable>(
+ as type: T.Type,
+ from data: Data
+ ) throws -> T {
+ return try decoder.decode(Payload<T>.self, from: data).data
+ }
+}
diff --git a/ios/MullvadVPN/SettingsManager/SettingsStore.swift b/ios/MullvadVPN/SettingsManager/SettingsStore.swift
new file mode 100644
index 0000000000..03b587433d
--- /dev/null
+++ b/ios/MullvadVPN/SettingsManager/SettingsStore.swift
@@ -0,0 +1,15 @@
+//
+// SettingsStore.swift
+// MullvadVPN
+//
+// Created by Sajad Vishkai on 2022-11-22.
+// Copyright © 2022 Mullvad VPN AB. All rights reserved.
+//
+
+import Foundation
+
+protocol SettingsStore {
+ func read(key: SettingsKey) throws -> Data
+ func write(_ data: Data, for key: SettingsKey) throws
+ func delete(key: SettingsKey) throws
+}
diff --git a/ios/MullvadVPN/SettingsManager/TunnelSettingsV2.swift b/ios/MullvadVPN/SettingsManager/TunnelSettingsV2.swift
index d382bf238b..6c0ada1415 100644
--- a/ios/MullvadVPN/SettingsManager/TunnelSettingsV2.swift
+++ b/ios/MullvadVPN/SettingsManager/TunnelSettingsV2.swift
@@ -13,6 +13,15 @@ import struct WireGuardKitTypes.IPAddressRange
import class WireGuardKitTypes.PrivateKey
import class WireGuardKitTypes.PublicKey
+/// Settings and device state schema versions.
+enum SchemaVersion: Int {
+ /// New settings format, stored as `TunnelSettingsV2`.
+ case v2 = 2
+
+ /// Current schema version.
+ static let current = SchemaVersion.v2
+}
+
struct TunnelSettingsV2: Codable, Equatable {
/// Relay constraints.
var relayConstraints = RelayConstraints()
diff --git a/ios/MullvadVPN/TunnelManager/LoadTunnelConfigurationOperation.swift b/ios/MullvadVPN/TunnelManager/LoadTunnelConfigurationOperation.swift
index 7bb6575d6a..715ca595b2 100644
--- a/ios/MullvadVPN/TunnelManager/LoadTunnelConfigurationOperation.swift
+++ b/ios/MullvadVPN/TunnelManager/LoadTunnelConfigurationOperation.swift
@@ -78,25 +78,12 @@ class LoadTunnelConfigurationOperation: ResultOperation<Void, Error> {
logger.debug("Settings not found in keychain.")
return .success(nil)
- } else if let error = error as? DecodingError {
+ } else {
logger.error(
error: error,
- message: "Cannot decode settings. Will attempt to delete them from keychain."
+ message: "Cannot read settings."
)
- return Result { try SettingsManager.deleteSettings() }
- .mapError { error in
- logger.error(
- error: error,
- message: "Failed to delete settings from keychain."
- )
-
- return error
- }
- .map { _ in
- return nil
- }
- } else {
return .failure(error)
}
}
@@ -109,25 +96,12 @@ class LoadTunnelConfigurationOperation: ResultOperation<Void, Error> {
logger.debug("Device state not found in keychain.")
return .success(nil)
- } else if let error = error as? DecodingError {
+ } else {
logger.error(
error: error,
- message: "Cannot decode device state. Will attempt to delete it from keychain."
+ message: "Cannot read device state."
)
- return Result { try SettingsManager.deleteDeviceState() }
- .mapError { error in
- logger.error(
- error: error,
- message: "Failed to delete device state from keychain."
- )
-
- return error
- }
- .map { _ in
- return nil
- }
- } else {
return .failure(error)
}
}
diff --git a/ios/MullvadVPN/TunnelManager/MigrateSettingsOperation.swift b/ios/MullvadVPN/TunnelManager/MigrateSettingsOperation.swift
deleted file mode 100644
index 1256c8148e..0000000000
--- a/ios/MullvadVPN/TunnelManager/MigrateSettingsOperation.swift
+++ /dev/null
@@ -1,259 +0,0 @@
-//
-// MigrateSettingsOperation.swift
-// MullvadVPN
-//
-// Created by pronebird on 18/05/2022.
-// Copyright © 2022 Mullvad VPN AB. All rights reserved.
-//
-
-import Foundation
-import MullvadLogging
-import MullvadREST
-import MullvadTypes
-import Operations
-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(
- error: 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(
- error: 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 let .success(accountData):
- self.accountData = accountData
-
- case let .failure(error):
- logger.error(error: 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 let .success(devices):
- self.devices = devices
-
- case let .failure(error):
- logger.error(error: 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 newDeviceState = DeviceState.loggedIn(
- StoredAccountData(
- identifier: accountData.id,
- number: settings.accountNumber,
- expiry: accountData.expiry
- ),
- 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
- )
- )
- )
-
- let newSettings = TunnelSettingsV2(
- relayConstraints: tunnelSettings.relayConstraints,
- dnsSettings: interfaceData.dnsSettings
- )
-
- // Save settings.
- do {
- try SettingsManager.writeSettings(newSettings)
- try SettingsManager.writeDeviceState(newDeviceState)
- } catch {
- logger.error(
- error: 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/TunnelManager.swift b/ios/MullvadVPN/TunnelManager/TunnelManager.swift
index 9bdc4ec8e9..c163f992f2 100644
--- a/ios/MullvadVPN/TunnelManager/TunnelManager.swift
+++ b/ios/MullvadVPN/TunnelManager/TunnelManager.swift
@@ -207,12 +207,6 @@ final class TunnelManager: StorePaymentObserver {
// MARK: - Public methods
func loadConfiguration(completionHandler: @escaping (Error?) -> Void) {
- let migrateSettingsOperation = MigrateSettingsOperation(
- dispatchQueue: internalQueue,
- accountsProxy: accountsProxy,
- devicesProxy: devicesProxy
- )
-
let loadTunnelOperation = LoadTunnelConfigurationOperation(
dispatchQueue: internalQueue,
interactor: TunnelInteractorProxy(self)
@@ -232,13 +226,8 @@ final class TunnelManager: StorePaymentObserver {
completionHandler(completion.error)
}
- loadTunnelOperation.addDependency(migrateSettingsOperation)
- let groupOperation = GroupOperation(operations: [
- migrateSettingsOperation, loadTunnelOperation,
- ])
-
- groupOperation.addObserver(
+ loadTunnelOperation.addObserver(
BackgroundObserver(
application: application,
name: "Load tunnel configuration",
@@ -246,17 +235,11 @@ final class TunnelManager: StorePaymentObserver {
)
)
- groupOperation.addCondition(
+ loadTunnelOperation.addCondition(
MutuallyExclusive(category: OperationCategory.manageTunnel.category)
)
- groupOperation.addCondition(
- MutuallyExclusive(category: OperationCategory.deviceStateUpdate.category)
- )
- groupOperation.addCondition(
- MutuallyExclusive(category: OperationCategory.settingsUpdate.category)
- )
- operationQueue.addOperation(groupOperation)
+ operationQueue.addOperation(loadTunnelOperation)
}
func refreshTunnelStatus() {