diff options
| author | Bug Magnet <marco.nikic@mullvad.net> | 2023-08-08 14:23:25 +0200 |
|---|---|---|
| committer | Bug Magnet <marco.nikic@mullvad.net> | 2023-08-08 14:23:25 +0200 |
| commit | 1ac353b00318bc8ec8787c08120144255adb77de (patch) | |
| tree | 2717d36ef0af30b2e7644e89933c917072f0ff0d | |
| parent | 652d163d575bab039a57c156b89628de66e472bd (diff) | |
| parent | 6b3825130405e748db3313f082eee6266996b557 (diff) | |
| download | mullvadvpn-1ac353b00318bc8ec8787c08120144255adb77de.tar.xz mullvadvpn-1ac353b00318bc8ec8787c08120144255adb77de.zip | |
Merge branch 'introduce-a-migration-manager-ios-242'
| -rw-r--r-- | ios/MullvadVPN.xcodeproj/project.pbxproj | 14 | ||||
| -rw-r--r-- | ios/MullvadVPN/AppDelegate.swift | 37 | ||||
| -rw-r--r-- | ios/MullvadVPN/MigrationManager/MigrationManager.swift | 98 | ||||
| -rw-r--r-- | ios/MullvadVPN/SettingsManager/SettingsManager.swift | 66 | ||||
| -rw-r--r-- | ios/MullvadVPN/SettingsManager/SettingsStore.swift | 7 | ||||
| -rw-r--r-- | ios/MullvadVPN/SettingsManager/TunnelSettings.swift | 12 | ||||
| -rw-r--r-- | ios/MullvadVPN/SettingsManager/TunnelSettingsV2.swift | 12 |
7 files changed, 153 insertions, 93 deletions
diff --git a/ios/MullvadVPN.xcodeproj/project.pbxproj b/ios/MullvadVPN.xcodeproj/project.pbxproj index cfddd66410..464240306d 100644 --- a/ios/MullvadVPN.xcodeproj/project.pbxproj +++ b/ios/MullvadVPN.xcodeproj/project.pbxproj @@ -421,6 +421,8 @@ A9A8A8EB2A262AB30086D569 /* FileCache.swift in Sources */ = {isa = PBXBuildFile; fileRef = A9A8A8EA2A262AB30086D569 /* FileCache.swift */; }; A9AD31D72A6AB68B00141BE8 /* InputTextFormatter.swift in Sources */ = {isa = PBXBuildFile; fileRef = 582AE30F2440A6CA00E6733A /* InputTextFormatter.swift */; }; A9B2CF722A1F64CD0013CC6C /* MullvadREST.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 06799ABC28F98E1D00ACD94E /* MullvadREST.framework */; }; + A9D96B1A2A8247C100A5C673 /* MigrationManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = A9D96B192A8247C100A5C673 /* MigrationManager.swift */; }; + A9D96B1B2A8248F200A5C673 /* MigrationManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = A9D96B192A8247C100A5C673 /* MigrationManager.swift */; }; A9D99B9A2A1F7C3200DE27D3 /* RESTTransport.swift in Sources */ = {isa = PBXBuildFile; fileRef = 06FAE67D28F83CA50033DD93 /* RESTTransport.swift */; }; A9D99BA02A1F7F3A00DE27D3 /* TransportProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = A9D99B9F2A1F7F3A00DE27D3 /* TransportProvider.swift */; }; A9D99BA52A1F808900DE27D3 /* RelayCache.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 063F02732902B63F001FA09F /* RelayCache.framework */; }; @@ -1210,6 +1212,7 @@ A97FF54F2A0D2FFC00900996 /* NSFileCoordinator+Extensions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "NSFileCoordinator+Extensions.swift"; sourceTree = "<group>"; }; A9A8A8EA2A262AB30086D569 /* FileCache.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FileCache.swift; sourceTree = "<group>"; }; A9CF11FC2A0518E7001D9565 /* AddressCacheTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AddressCacheTests.swift; sourceTree = "<group>"; }; + A9D96B192A8247C100A5C673 /* MigrationManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MigrationManager.swift; sourceTree = "<group>"; }; A9D99B9F2A1F7F3A00DE27D3 /* TransportProvider.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TransportProvider.swift; sourceTree = "<group>"; }; A9EC20E52A5C488D0040D56E /* Haversine.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Haversine.swift; sourceTree = "<group>"; }; A9EC20E72A5D3A8C0040D56E /* CoordinatesTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CoordinatesTests.swift; sourceTree = "<group>"; }; @@ -2154,6 +2157,7 @@ 58C774C929AB543C003A1A56 /* Containers */, 58CAF9F22983D32200BE19F7 /* Coordinators */, 583FE02329C1AC9F006E85F9 /* Extensions */, + A9D96B182A82479700A5C673 /* MigrationManager */, 58B26E1F2943516500D5980C /* Notifications */, 586A950B2901250A007BAF2B /* Operations */, 5864859729A0D012006C5743 /* Presentation controllers */, @@ -2326,6 +2330,14 @@ path = MullvadTransport; sourceTree = "<group>"; }; + A9D96B182A82479700A5C673 /* MigrationManager */ = { + isa = PBXGroup; + children = ( + A9D96B192A8247C100A5C673 /* MigrationManager.swift */, + ); + path = MigrationManager; + sourceTree = "<group>"; + }; F028A5472A336E1900C0CAA3 /* RedeemVoucher */ = { isa = PBXGroup; children = ( @@ -3272,6 +3284,7 @@ E158B360285381C60002F069 /* String+AccountFormatting.swift in Sources */, 582BB1B1229569620055B6EF /* UINavigationBar+Appearance.swift in Sources */, 58ACF6492655365700ACE4B7 /* PreferencesViewController.swift in Sources */, + A9D96B1A2A8247C100A5C673 /* MigrationManager.swift in Sources */, 7ABE318D2A1CDD4500DF4963 /* UIFont+Weight.swift in Sources */, 58C774BE29A7A249003A1A56 /* CustomNavigationController.swift in Sources */, E1FD0DF528AA7CE400299DB4 /* StatusActivityView.swift in Sources */, @@ -3461,6 +3474,7 @@ 5838318B27C40A3900000571 /* Pinger.swift in Sources */, 06410E05292D0FC000AFC18C /* SettingsParser.swift in Sources */, A92ECC292A7802AB0052F1B1 /* StoredDeviceData.swift in Sources */, + A9D96B1B2A8248F200A5C673 /* MigrationManager.swift in Sources */, 58E0729D28814AAE008902F8 /* PacketTunnelConfiguration.swift in Sources */, 58E0729F28814ACC008902F8 /* WireGuardLogLevel+Logging.swift in Sources */, 580F8B8428197884002E0998 /* TunnelSettingsV2.swift in Sources */, diff --git a/ios/MullvadVPN/AppDelegate.swift b/ios/MullvadVPN/AppDelegate.swift index c8003c068e..f1da222e3d 100644 --- a/ios/MullvadVPN/AppDelegate.swift +++ b/ios/MullvadVPN/AppDelegate.swift @@ -41,6 +41,7 @@ class AppDelegate: UIResponder, UIApplicationDelegate, UNUserNotificationCenterD private(set) var storePaymentManager: StorePaymentManager! private var transportMonitor: TransportMonitor! private var relayConstraintsObserver: TunnelBlockObserver! + private let migrationManager = MigrationManager() // MARK: - Application lifecycle @@ -368,31 +369,33 @@ class AppDelegate: UIResponder, UIApplicationDelegate, UNUserNotificationCenterD } let migrateSettingsOperation = AsyncBlockOperation(dispatchQueue: .main) { [self] finish in - SettingsManager.migrateStore(with: proxyFactory) { [self] migrationResult in - switch migrationResult { - case .success: - // Tell the tunnel to re-read tunnel configuration after migration. - logger.debug("Reconnect the tunnel after settings migration.") - tunnelManager.reconnectTunnel(selectNewRelay: true) - fallthrough + migrationManager + .migrateSettings(store: SettingsManager.store, proxyFactory: proxyFactory) { [self] migrationResult in + switch migrationResult { + case .success: + // Tell the tunnel to re-read tunnel configuration after migration. + logger.debug("Reconnect the tunnel after settings migration.") + tunnelManager.reconnectTunnel(selectNewRelay: true) + fallthrough - case .nothing: - finish(nil) + case .nothing: + finish(nil) - case let .failure(error): - let migrationUIHandler = application.connectedScenes.first { $0 is SettingsMigrationUIHandler } - as? SettingsMigrationUIHandler + case let .failure(error): + let migrationUIHandler = application.connectedScenes.first { $0 is SettingsMigrationUIHandler } + as? SettingsMigrationUIHandler - if let migrationUIHandler { - migrationUIHandler.showMigrationError(error) { + if let migrationUIHandler { + migrationUIHandler.showMigrationError(error) { + finish(error) + } + } else { finish(error) } - } else { - finish(error) } } - } } + migrateSettingsOperation.addDependencies([wipeSettingsOperation, loadTunnelStoreOperation]) let initTunnelManagerOperation = AsyncBlockOperation(dispatchQueue: .main) { finish in diff --git a/ios/MullvadVPN/MigrationManager/MigrationManager.swift b/ios/MullvadVPN/MigrationManager/MigrationManager.swift new file mode 100644 index 0000000000..f2357bcb83 --- /dev/null +++ b/ios/MullvadVPN/MigrationManager/MigrationManager.swift @@ -0,0 +1,98 @@ +// +// MigrationManager.swift +// MullvadVPN +// +// Created by Marco Nikic on 2023-08-08. +// Copyright © 2023 Mullvad VPN AB. All rights reserved. +// + +import Foundation +import MullvadLogging +import MullvadREST +import MullvadTypes + +enum SettingsMigrationResult { + /// Nothing to migrate. + case nothing + + /// Successfully performed migration. + case success + + /// Failure when migrating store. + case failure(Error) +} + +struct MigrationManager { + private let logger = Logger(label: "MigrationManager") + + /// Migrate settings store if needed. + /// + /// The following types of error are expected to be returned by this method: + /// `SettingsMigrationError`, `UnsupportedSettingsVersionError`, `ReadSettingsVersionError`. + func migrateSettings( + store: SettingsStore, + proxyFactory: REST.ProxyFactory, + migrationCompleted: @escaping (SettingsMigrationResult) -> Void + ) { + let handleCompletion = { (result: SettingsMigrationResult) in + // Reset store upon failure to migrate settings. + if case .failure = result { + SettingsManager.resetStore() + } + migrationCompleted(result) + } + + do { + try checkLatestSettingsVersion(in: store) + handleCompletion(.nothing) + } catch { + handleCompletion(.failure(error)) + } + } + + private func checkLatestSettingsVersion(in store: SettingsStore) throws { + let settingsVersion: Int + do { + let parser = SettingsParser(decoder: JSONDecoder(), encoder: JSONEncoder()) + let settingsData = try store.read(key: SettingsKey.settings) + settingsVersion = try parser.parseVersion(data: settingsData) + } catch .itemNotFound as KeychainError { + return + } catch { + throw ReadSettingsVersionError(underlyingError: error) + } + + guard settingsVersion != SchemaVersion.current.rawValue else { + return + } + + let error = UnsupportedSettingsVersionError( + storedVersion: settingsVersion, + currentVersion: SchemaVersion.current + ) + + logger.error(error: error, message: "Encountered an unknown version.") + + throw error + } +} + +/// A wrapper type for errors returned by concrete migrations. +struct SettingsMigrationError: LocalizedError, WrappingError { + private let inner: Error + let sourceVersion, targetVersion: SchemaVersion + + var underlyingError: Error? { + inner + } + + var errorDescription: String? { + "Failed to migrate settings from \(sourceVersion) to \(targetVersion)." + } + + init(sourceVersion: SchemaVersion, targetVersion: SchemaVersion, underlyingError: Error) { + self.sourceVersion = sourceVersion + self.targetVersion = targetVersion + inner = underlyingError + } +} diff --git a/ios/MullvadVPN/SettingsManager/SettingsManager.swift b/ios/MullvadVPN/SettingsManager/SettingsManager.swift index 5ee563c475..91a73b8b07 100644 --- a/ios/MullvadVPN/SettingsManager/SettingsManager.swift +++ b/ios/MullvadVPN/SettingsManager/SettingsManager.swift @@ -15,21 +15,10 @@ private let keychainServiceName = "Mullvad VPN" private let accountTokenKey = "accountToken" private let accountExpiryKey = "accountExpiry" -enum SettingsMigrationResult { - /// Nothing to migrate. - case nothing - - /// Successfully performed migration. - case success - - /// Failure when migrating store. - case failure(Error) -} - enum SettingsManager { private static let logger = Logger(label: "SettingsManager") - private static let store: SettingsStore = KeychainSettingsStore( + static let store: SettingsStore = KeychainSettingsStore( serviceName: keychainServiceName, accessGroup: ApplicationConfiguration.securityGroupIdentifier ) @@ -134,32 +123,6 @@ enum SettingsManager { try store.write(data, for: .deviceState) } - // MARK: - Migration - - /// Migrate settings store if needed. - /// - /// The following types of error are expected to be returned by this method: - /// `SettingsMigrationError`, `UnsupportedSettingsVersionError`, `ReadSettingsVersionError`. - static func migrateStore( - with restFactory: REST.ProxyFactory, - completion: @escaping (SettingsMigrationResult) -> Void - ) { - let handleCompletion = { (result: SettingsMigrationResult) in - // Reset store upon failure to migrate settings. - if case .failure = result { - resetStore() - } - completion(result) - } - - do { - try checkLatestSettingsVersion() - handleCompletion(.nothing) - } catch { - handleCompletion(.failure(error)) - } - } - /// Removes all legacy settings, device state and tunnel settings but keeps the last used /// account number stored. static func resetStore(completely: Bool = false) { @@ -229,12 +192,7 @@ enum SettingsManager { } } -enum SettingsKey: String, CaseIterable { - case settings = "Settings" - case deviceState = "DeviceState" - case lastUsedAccount = "LastUsedAccount" - case shouldWipeSettings = "ShouldWipeSettings" -} +// MARK: - Supporting types /// An error type describing a failure to read or parse settings version. struct ReadSettingsVersionError: LocalizedError, WrappingError { @@ -266,26 +224,6 @@ struct UnsupportedSettingsVersionError: LocalizedError { } } -/// A wrapper type for errors returned by concrete migrations. -struct SettingsMigrationError: LocalizedError, WrappingError { - private let inner: Error - let sourceVersion, targetVersion: SchemaVersion - - var underlyingError: Error? { - inner - } - - var errorDescription: String? { - "Failed to migrate settings from \(sourceVersion) to \(targetVersion)." - } - - init(sourceVersion: SchemaVersion, targetVersion: SchemaVersion, underlyingError: Error) { - self.sourceVersion = sourceVersion - self.targetVersion = targetVersion - inner = underlyingError - } -} - struct StringDecodingError: LocalizedError { let data: Data diff --git a/ios/MullvadVPN/SettingsManager/SettingsStore.swift b/ios/MullvadVPN/SettingsManager/SettingsStore.swift index 03b587433d..55b565390f 100644 --- a/ios/MullvadVPN/SettingsManager/SettingsStore.swift +++ b/ios/MullvadVPN/SettingsManager/SettingsStore.swift @@ -8,6 +8,13 @@ import Foundation +enum SettingsKey: String, CaseIterable { + case settings = "Settings" + case deviceState = "DeviceState" + case lastUsedAccount = "LastUsedAccount" + case shouldWipeSettings = "ShouldWipeSettings" +} + protocol SettingsStore { func read(key: SettingsKey) throws -> Data func write(_ data: Data, for key: SettingsKey) throws diff --git a/ios/MullvadVPN/SettingsManager/TunnelSettings.swift b/ios/MullvadVPN/SettingsManager/TunnelSettings.swift index a95eaf5802..91108d08be 100644 --- a/ios/MullvadVPN/SettingsManager/TunnelSettings.swift +++ b/ios/MullvadVPN/SettingsManager/TunnelSettings.swift @@ -10,3 +10,15 @@ import Foundation /// Alias to the latest version of the `TunnelSettings`. typealias LatestTunnelSettings = TunnelSettingsV2 + +/// Settings and device state schema versions. +enum SchemaVersion: Int, Equatable { + /// Legacy settings format, stored as `TunnelSettingsV1`. + case v1 = 1 + + /// New settings format, stored as `TunnelSettingsV2`. + case v2 = 2 + + /// Current schema version. + static let current = SchemaVersion.v2 +} diff --git a/ios/MullvadVPN/SettingsManager/TunnelSettingsV2.swift b/ios/MullvadVPN/SettingsManager/TunnelSettingsV2.swift index a530cfb03f..821a848878 100644 --- a/ios/MullvadVPN/SettingsManager/TunnelSettingsV2.swift +++ b/ios/MullvadVPN/SettingsManager/TunnelSettingsV2.swift @@ -13,18 +13,6 @@ import struct WireGuardKitTypes.IPAddressRange import class WireGuardKitTypes.PrivateKey import class WireGuardKitTypes.PublicKey -/// Settings and device state schema versions. -enum SchemaVersion: Int, Equatable { - /// Legacy settings format, stored as `TunnelSettingsV1`. - case v1 = 1 - - /// 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() |
