diff options
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() { |
