summaryrefslogtreecommitdiffhomepage
diff options
context:
space:
mode:
authorAndrej Mihajlov <and@mullvad.net>2022-11-28 10:39:21 +0100
committerAndrej Mihajlov <and@mullvad.net>2022-11-28 10:39:21 +0100
commit5699d3f30333a7cc90eefb987b6c7e79ac14f423 (patch)
tree72726da701e7deccb68baef00f12087741b7839f
parent044b3868dfa1bd8c4573624aecd4c17b053d256e (diff)
parent15825b53b7d8f4688dbcd028ba3cf888a9d9ee16 (diff)
downloadmullvadvpn-5699d3f30333a7cc90eefb987b6c7e79ac14f423.tar.xz
mullvadvpn-5699d3f30333a7cc90eefb987b6c7e79ac14f423.zip
Merge branch 'reset-settings-on-migration-failure'
-rw-r--r--ios/MullvadVPN.xcodeproj/project.pbxproj12
-rw-r--r--ios/MullvadVPN/AppDelegate.swift50
-rw-r--r--ios/MullvadVPN/SceneDelegate.swift62
-rw-r--r--ios/MullvadVPN/SettingsManager/KeychainSettingsStore.swift12
-rw-r--r--ios/MullvadVPN/SettingsManager/Migration.swift (renamed from ios/MullvadVPN/SettingsManager/SettingsMigration/Migration.swift)0
-rw-r--r--ios/MullvadVPN/SettingsManager/Migrations/MigrationFromV1ToV2.swift (renamed from ios/MullvadVPN/SettingsManager/SettingsMigration/MigrationFromV1ToV2.swift)57
-rw-r--r--ios/MullvadVPN/SettingsManager/SettingsManager.swift203
-rw-r--r--ios/MullvadVPN/SettingsManager/TunnelSettingsV2.swift5
-rw-r--r--ios/MullvadVPN/SettingsMigrationUIHandler.swift13
-rw-r--r--ios/Operations/OperationCompletion.swift14
10 files changed, 304 insertions, 124 deletions
diff --git a/ios/MullvadVPN.xcodeproj/project.pbxproj b/ios/MullvadVPN.xcodeproj/project.pbxproj
index 4f7aaa8a3e..042c517930 100644
--- a/ios/MullvadVPN.xcodeproj/project.pbxproj
+++ b/ios/MullvadVPN.xcodeproj/project.pbxproj
@@ -293,6 +293,7 @@
58E0729F28814ACC008902F8 /* WireGuardLogLevel+Logging.swift in Sources */ = {isa = PBXBuildFile; fileRef = 58E0729E28814ACC008902F8 /* WireGuardLogLevel+Logging.swift */; };
58E072A128814B0E008902F8 /* MullvadEndpoint+WgEndpoint.swift in Sources */ = {isa = PBXBuildFile; fileRef = 58E072A028814B0E008902F8 /* MullvadEndpoint+WgEndpoint.swift */; };
58E0A98827C8F46300FE6BDD /* Tunnel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 58E0A98727C8F46300FE6BDD /* Tunnel.swift */; };
+ 58E11188292FA11F009FCA84 /* SettingsMigrationUIHandler.swift in Sources */ = {isa = PBXBuildFile; fileRef = 58E11187292FA11F009FCA84 /* SettingsMigrationUIHandler.swift */; };
58E20771274672CA00DE5D77 /* LaunchViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 58E20770274672CA00DE5D77 /* LaunchViewController.swift */; };
58E25F812837BBBB002CFB2C /* SceneDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 58E25F802837BBBB002CFB2C /* SceneDelegate.swift */; };
58E511E628DDDEAC00B0BCDE /* CodingErrors+CustomErrorDescription.swift in Sources */ = {isa = PBXBuildFile; fileRef = 58E511E528DDDEAC00B0BCDE /* CodingErrors+CustomErrorDescription.swift */; };
@@ -788,6 +789,7 @@
58E072A028814B0E008902F8 /* MullvadEndpoint+WgEndpoint.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "MullvadEndpoint+WgEndpoint.swift"; sourceTree = "<group>"; };
58E072A428814C28008902F8 /* TunnelMonitorDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TunnelMonitorDelegate.swift; sourceTree = "<group>"; };
58E0A98727C8F46300FE6BDD /* Tunnel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Tunnel.swift; sourceTree = "<group>"; };
+ 58E11187292FA11F009FCA84 /* SettingsMigrationUIHandler.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SettingsMigrationUIHandler.swift; sourceTree = "<group>"; };
58E20770274672CA00DE5D77 /* LaunchViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LaunchViewController.swift; sourceTree = "<group>"; };
58E25F802837BBBB002CFB2C /* SceneDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SceneDelegate.swift; sourceTree = "<group>"; };
58E511E028DDB7F100B0BCDE /* WrappingError.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WrappingError.swift; sourceTree = "<group>"; };
@@ -1026,20 +1028,20 @@
path = MullvadREST;
sourceTree = "<group>";
};
- 068CE57129278F5F00A068BB /* SettingsMigration */ = {
+ 068CE57129278F5F00A068BB /* Migrations */ = {
isa = PBXGroup;
children = (
- 068CE5732927B7A400A068BB /* Migration.swift */,
068CE56F29278F5300A068BB /* MigrationFromV1ToV2.swift */,
);
- path = SettingsMigration;
+ path = Migrations;
sourceTree = "<group>";
};
580F8B88281A79A7002E0998 /* SettingsManager */ = {
isa = PBXGroup;
children = (
- 068CE57129278F5F00A068BB /* SettingsMigration */,
+ 068CE57129278F5F00A068BB /* Migrations */,
06410DFD292CE18F00AFC18C /* KeychainSettingsStore.swift */,
+ 068CE5732927B7A400A068BB /* Migration.swift */,
58FF2C02281BDE02009EF542 /* SettingsManager.swift */,
06410E03292D0F7100AFC18C /* SettingsParser.swift */,
06410E06292D108E00AFC18C /* SettingsStore.swift */,
@@ -1410,6 +1412,7 @@
58CCA0152242560B004F3011 /* UIColor+Palette.swift */,
585CA70E25F8C44600B47C62 /* UIMetrics.swift */,
58F7CA872692E34000FC59FD /* WireguardKeysContentView.swift */,
+ 58E11187292FA11F009FCA84 /* SettingsMigrationUIHandler.swift */,
);
path = MullvadVPN;
sourceTree = "<group>";
@@ -2261,6 +2264,7 @@
5888AD83227B11080051EB06 /* SelectLocationCell.swift in Sources */,
5891BF1C25E3E3EB006D6FB0 /* Bundle+ProductVersion.swift in Sources */,
5878A26F2907E7E00096FC88 /* ProblemReportInteractor.swift in Sources */,
+ 58E11188292FA11F009FCA84 /* SettingsMigrationUIHandler.swift in Sources */,
5807E2C02432038B00F5FF30 /* String+Split.swift in Sources */,
58677712290976FB006F721F /* SettingsInteractor.swift in Sources */,
58CE5E66224146200008646E /* LoginViewController.swift in Sources */,
diff --git a/ios/MullvadVPN/AppDelegate.swift b/ios/MullvadVPN/AppDelegate.swift
index b11ada5f7c..1721f80261 100644
--- a/ios/MullvadVPN/AppDelegate.swift
+++ b/ios/MullvadVPN/AppDelegate.swift
@@ -110,28 +110,48 @@ class AppDelegate: UIResponder, UIApplicationDelegate, UNUserNotificationCenterD
setupNotificationHandler()
addApplicationNotifications(application: application)
- 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)
- }
+ let migrateSettingsOperation = AsyncBlockOperation(dispatchQueue: .main) { operation in
+ SettingsManager.migrateStore(with: self.proxyFactory) { error in
+ guard let error = error else {
+ operation.finish()
+ return
+ }
- self.logger.debug("Finished initialization.")
+ guard let migrationUIHandler = application.connectedScenes.compactMap({ scene in
+ return scene.delegate as? SettingsMigrationUIHandler
+ }).first else {
+ operation.finish()
+ return
+ }
- NotificationManager.shared.updateNotifications()
- self.storePaymentManager.startPaymentQueueMonitoring()
+ migrationUIHandler.showMigrationError(error) {
+ operation.finish()
+ }
+ }
+ }
- operation.finish()
+ let loadTunnelConfigurationOperation =
+ 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)
}
+
+ self.logger.debug("Finished initialization.")
+
+ NotificationManager.shared.updateNotifications()
+ self.storePaymentManager.startPaymentQueueMonitoring()
+
+ operation.finish()
}
}
+ loadTunnelConfigurationOperation.addDependency(migrateSettingsOperation)
- operationQueue.addOperation(setupTunnelManagerOperation)
+ operationQueue.addOperations(
+ [migrateSettingsOperation, loadTunnelConfigurationOperation],
+ waitUntilFinished: false
+ )
return true
}
diff --git a/ios/MullvadVPN/SceneDelegate.swift b/ios/MullvadVPN/SceneDelegate.swift
index 27fbfa7bf2..03f6bc3674 100644
--- a/ios/MullvadVPN/SceneDelegate.swift
+++ b/ios/MullvadVPN/SceneDelegate.swift
@@ -19,7 +19,7 @@ class SceneDelegate: UIResponder, UIWindowSceneDelegate, UISplitViewControllerDe
SettingsNavigationControllerDelegate, ConnectViewControllerDelegate,
OutOfTimeViewControllerDelegate, SelectLocationViewControllerDelegate,
RevokedDeviceViewControllerDelegate, NotificationManagerDelegate, TunnelObserver,
- RelayCacheTrackerObserver
+ RelayCacheTrackerObserver, SettingsMigrationUIHandler
{
private let logger = Logger(label: "SceneDelegate")
@@ -952,4 +952,64 @@ class SceneDelegate: UIResponder, UIWindowSceneDelegate, UISplitViewControllerDe
}
return nil
}
+
+ // MARK: - SettingsMigrationUIHandler
+
+ func showMigrationError(_ error: Error, completionHandler: @escaping () -> Void) {
+ let alertController = UIAlertController(
+ title: NSLocalizedString(
+ "ALERT_TITLE",
+ tableName: "SettingsMigrationUI",
+ value: "Settings migration error",
+ comment: ""
+ ),
+ message: Self.migrationErrorReason(error),
+ preferredStyle: .alert
+ )
+ alertController.addAction(
+ UIAlertAction(
+ title: NSLocalizedString("OK", tableName: "SettingsMigrationUI", comment: ""),
+ style: .default,
+ handler: { _ in
+ completionHandler()
+ }
+ )
+ )
+
+ if let rootViewController = window?.rootViewController {
+ rootViewController.present(alertController, animated: true)
+ } else {
+ completionHandler()
+ }
+ }
+
+ private static func migrationErrorReason(_ error: Error) -> String {
+ if error is UnsupportedSettingsVersionError {
+ return NSLocalizedString(
+ "NEWER_STORED_SETTINGS_ERROR",
+ tableName: "SettingsMigrationUI",
+ value: """
+ The version of settings stored on device is from a newer app than is currently \
+ running. Settings will be reset to defaults.
+ """,
+ comment: ""
+ )
+ } else if let error = error as? SettingsMigrationError,
+ error.underlyingError is REST.Error
+ {
+ return NSLocalizedString(
+ "NETWORK_ERROR",
+ tableName: "SettingsMigrationUI",
+ value: "Network error occurred. Settings will be reset to defaults.",
+ comment: ""
+ )
+ } else {
+ return NSLocalizedString(
+ "INTERNAL_ERROR",
+ tableName: "SettingsMigrationUI",
+ value: "Internal error occurred. Settings will be reset to defaults.",
+ comment: ""
+ )
+ }
+ }
}
diff --git a/ios/MullvadVPN/SettingsManager/KeychainSettingsStore.swift b/ios/MullvadVPN/SettingsManager/KeychainSettingsStore.swift
index f98ee06a6f..eb662b8d11 100644
--- a/ios/MullvadVPN/SettingsManager/KeychainSettingsStore.swift
+++ b/ios/MullvadVPN/SettingsManager/KeychainSettingsStore.swift
@@ -11,10 +11,12 @@ import MullvadTypes
import Security
class KeychainSettingsStore: SettingsStore {
- let keychainServiceName: String
+ let serviceName: String
+ let accessGroup: String
- init(keychainServiceName: String) {
- self.keychainServiceName = keychainServiceName
+ init(serviceName: String, accessGroup: String) {
+ self.serviceName = serviceName
+ self.accessGroup = accessGroup
}
func read(key: SettingsKey) throws -> Data {
@@ -89,14 +91,14 @@ class KeychainSettingsStore: SettingsStore {
private func createDefaultAttributes(item: SettingsKey) -> [CFString: Any] {
return [
kSecClass: kSecClassGenericPassword,
- kSecAttrService: keychainServiceName,
+ kSecAttrService: serviceName,
kSecAttrAccount: item.rawValue,
]
}
private func createAccessAttributes() -> [CFString: Any] {
return [
- kSecAttrAccessGroup: ApplicationConfiguration.securityGroupIdentifier,
+ kSecAttrAccessGroup: accessGroup,
kSecAttrAccessible: kSecAttrAccessibleAfterFirstUnlock,
]
}
diff --git a/ios/MullvadVPN/SettingsManager/SettingsMigration/Migration.swift b/ios/MullvadVPN/SettingsManager/Migration.swift
index 79704c7b9b..79704c7b9b 100644
--- a/ios/MullvadVPN/SettingsManager/SettingsMigration/Migration.swift
+++ b/ios/MullvadVPN/SettingsManager/Migration.swift
diff --git a/ios/MullvadVPN/SettingsManager/SettingsMigration/MigrationFromV1ToV2.swift b/ios/MullvadVPN/SettingsManager/Migrations/MigrationFromV1ToV2.swift
index ea95cb9632..34956cd1e0 100644
--- a/ios/MullvadVPN/SettingsManager/SettingsMigration/MigrationFromV1ToV2.swift
+++ b/ios/MullvadVPN/SettingsManager/Migrations/MigrationFromV1ToV2.swift
@@ -19,8 +19,8 @@ class MigrationFromV1ToV2: Migration {
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 var accountCompletion: OperationCompletion<REST.AccountData, REST.Error> = .cancelled
+ private var devicesCompletion: OperationCompletion<[REST.Device], REST.Error> = .cancelled
private let legacySettings: LegacyTunnelSettings
@@ -42,6 +42,18 @@ class MigrationFromV1ToV2: Migration {
) {
let storedAccountNumber = legacySettings.accountNumber
+ // Store last used account number.
+ logger.debug("Store legacy account number as last used account.")
+ do {
+ if let accountData = storedAccountNumber.data(using: .utf8) {
+ try store.write(accountData, for: .lastUsedAccount)
+ } else {
+ logger.error("Failed to encode account number into utf-8 data.")
+ }
+ } catch {
+ logger.error(error: error, message: "Failed to store last used account.")
+ }
+
// Fetch remote data concurrently.
logger.debug("Fetching account and device data...")
let dispatchGroup = DispatchGroup()
@@ -67,27 +79,16 @@ class MigrationFromV1ToV2: Migration {
}
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))
+ let result = Result {
+ try self.migrateSettings(
+ store: store,
+ parser: parser,
+ settings: self.legacySettings,
+ accountData: try self.accountCompletion.get(),
+ devices: try self.devicesCompletion.get()
+ )
}
+ completion(result.error)
}
}
@@ -163,15 +164,3 @@ class MigrationFromV1ToV2: Migration {
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/SettingsManager.swift b/ios/MullvadVPN/SettingsManager/SettingsManager.swift
index f0e88185b1..2940937ae4 100644
--- a/ios/MullvadVPN/SettingsManager/SettingsManager.swift
+++ b/ios/MullvadVPN/SettingsManager/SettingsManager.swift
@@ -16,8 +16,15 @@ private let accountTokenKey = "accountToken"
private let accountExpiryKey = "accountExpiry"
enum SettingsManager {
+ private static let logger = Logger(label: "SettingsManager")
+
+ private static let store: SettingsStore = KeychainSettingsStore(
+ serviceName: keychainServiceName,
+ accessGroup: ApplicationConfiguration.securityGroupIdentifier
+ )
+
private static func makeParser() -> SettingsParser {
- SettingsParser(decoder: JSONDecoder(), encoder: JSONEncoder())
+ return SettingsParser(decoder: JSONDecoder(), encoder: JSONEncoder())
}
// MARK: - Last used account
@@ -50,23 +57,29 @@ enum SettingsManager {
}
}
- private static let store: SettingsStore = KeychainSettingsStore(
- keychainServiceName: keychainServiceName
- )
-
// MARK: - Settings
static func readSettings() throws -> TunnelSettingsV2 {
- let data = try store.read(key: .settings)
+ let storedVersion: Int
+ let data: Data
let parser = makeParser()
- let version = try parser.parseVersion(data: data)
- let currentVersion = SchemaVersion.current.rawValue
+ do {
+ data = try store.read(key: .settings)
+ storedVersion = try parser.parseVersion(data: data)
+ } catch {
+ throw ReadSettingsVersionError(underlyingError: error)
+ }
+
+ let currentVersion = SchemaVersion.current
- if version == currentVersion {
+ if storedVersion == currentVersion.rawValue {
return try parser.parsePayload(as: TunnelSettingsV2.self, from: data)
} else {
- throw UnsupportedVersionSettings(storedVersion: version, currentVersion: currentVersion)
+ throw UnsupportedSettingsVersionError(
+ storedVersion: storedVersion,
+ currentVersion: currentVersion
+ )
}
}
@@ -95,21 +108,38 @@ enum SettingsManager {
// MARK: - Migration
+ /// Migrate settings store if needed.
+ ///
+ /// The error returned in `completion` handler is set to `nil` upon success or when no
+ /// migration is not 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 (Error?) -> Void
) {
+ let handleCompletion = { (error: Error?) in
+ // Reset store upon failure to migrate settings.
+ if error != nil {
+ self.resetStore()
+ }
+ completion(error)
+ }
+
if let legacySettings = readLegacySettings() {
migrateLegacySettings(
restFactory: restFactory,
legacySettings: legacySettings,
- completion: completion
+ completion: handleCompletion
)
} else {
- migrateModernSettings(completion: completion)
+ migrateModernSettings(completion: handleCompletion)
}
}
+ // MARK: - Private
+
private static func migrateLegacySettings(
restFactory: REST.ProxyFactory,
legacySettings: LegacyTunnelSettings,
@@ -124,22 +154,17 @@ enum SettingsManager {
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."
+ let migrationError = SettingsMigrationError(
+ sourceVersion: .v1,
+ targetVersion: .v2,
+ underlyingError: error
)
- completion(error)
- } else {
- let userDefaults = UserDefaults.standard
-
- logger.debug("Remove legacy settings from keychain.")
- Self.deleteLegacySettings()
+ logger.error(error: migrationError)
- logger.debug("Remove legacy settings from user defaults.")
-
- userDefaults.removeObject(forKey: accountTokenKey)
- userDefaults.removeObject(forKey: accountExpiryKey)
+ completion(migrationError)
+ } else {
+ Self.deleteAllLegacySettings()
completion(nil)
}
@@ -147,66 +172,68 @@ enum SettingsManager {
}
private static func migrateModernSettings(completion: @escaping (Error?) -> Void) {
- let parser = makeParser()
-
do {
+ let parser = makeParser()
let settingsData = try store.read(key: .settings)
let settingsVersion = try parser.parseVersion(data: settingsData)
if settingsVersion != SchemaVersion.current.rawValue {
- let error = UnsupportedVersionSettings(
+ let error = UnsupportedSettingsVersionError(
storedVersion: settingsVersion,
- currentVersion: SchemaVersion.current.rawValue
+ currentVersion: SchemaVersion.current
)
- logger.error(
- error: error,
- message: "Encountered an unknown version."
- )
+ logger.error(error: error, message: "Encountered an unknown version.")
completion(error)
-
} else {
completion(nil)
}
-
} catch .itemNotFound as KeychainError {
completion(nil)
} catch {
- completion(error)
+ completion(ReadSettingsVersionError(underlyingError: error))
}
}
- // MARK: - Legacy settings
+ /// Removes all legacy settings, device state and tunnel settings but keeps the last used
+ /// account number stored.
+ private static func resetStore() {
+ logger.debug("Reset store.")
- 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
+ do {
+ try store.delete(key: .deviceState)
+ } catch {
+ if (error as? KeychainError) != .itemNotFound {
+ logger.error(error: error, message: "Failed to delete device state.")
+ }
}
- // Set legacy account number as last used.
- logger.debug("Found legacy account number.")
- logger.debug("Store last used account.")
-
do {
- try Self.setLastUsedAccount(storedAccountNumber)
+ try store.delete(key: .settings)
} catch {
- logger.error(
- error: error,
- message: "Failed to store last used account."
- )
+ if (error as? KeychainError) != .itemNotFound {
+ logger.error(error: error, message: "Failed to delete settings.")
+ }
+ }
+
+ Self.deleteAllLegacySettings()
+ }
+
+ // MARK: - Legacy settings
+
+ private static func readLegacySettings() -> LegacyTunnelSettings? {
+ guard let storedAccountNumber = UserDefaults.standard.string(forKey: accountTokenKey) else {
+ logger.debug("Legacy account number is not found in user defaults. Nothing to migrate.")
+ return nil
}
// List legacy settings stored in keychain.
- logger.debug("Read legacy settings...")
+ logger.debug("List legacy settings in keychain...")
var storedSettings: [LegacyTunnelSettings] = []
do {
- storedSettings = try Self.readLegacySettings()
+ storedSettings = try findAllLegacySettingsInKeychain()
} catch .itemNotFound as KeychainError {
logger.debug("Legacy settings are not found in keychain.")
@@ -236,11 +263,7 @@ enum SettingsManager {
return matchingSettings
}
- // MARK: - Legacy settings support
-
- private static let logger = Logger(label: "SettingsManager")
-
- static func readLegacySettings() throws -> [LegacyTunnelSettings] {
+ private static func findAllLegacySettingsInKeychain() throws -> [LegacyTunnelSettings] {
let query: [CFString: Any] = [
kSecClass: kSecClassGenericPassword,
kSecAttrService: keychainServiceName,
@@ -287,7 +310,17 @@ enum SettingsManager {
}
}
- static func deleteLegacySettings() {
+ private static func deleteAllLegacySettings() {
+ logger.debug("Remove legacy settings from keychain.")
+ deleteLegacySettingsFromKeychain()
+
+ logger.debug("Remove legacy settings from user defaults.")
+ let userDefaults = UserDefaults.standard
+ userDefaults.removeObject(forKey: accountTokenKey)
+ userDefaults.removeObject(forKey: accountExpiryKey)
+ }
+
+ private static func deleteLegacySettingsFromKeychain() {
let query: [CFString: Any] = [
kSecClass: kSecClassGenericPassword,
kSecAttrService: keychainServiceName,
@@ -362,11 +395,53 @@ enum SettingsKey: String, CaseIterable {
case lastUsedAccount = "LastUsedAccount"
}
-struct UnsupportedVersionSettings: LocalizedError {
- let storedVersion, currentVersion: Int
+/// An error type describing a failure to read or parse settings version.
+struct ReadSettingsVersionError: LocalizedError, WrappingError {
+ private let inner: Error
+
+ var underlyingError: Error? {
+ return inner
+ }
+
+ var errorDescription: String? {
+ return "Failed to read settings version."
+ }
+
+ init(underlyingError: Error) {
+ inner = underlyingError
+ }
+}
+
+/// An error returned when stored settings version is unknown to the currently running app.
+struct UnsupportedSettingsVersionError: LocalizedError {
+ let storedVersion: Int
+ let currentVersion: SchemaVersion
var errorDescription: String? {
- return "Stored settings version was not the same as current version, stored version: \(storedVersion), current version: \(currentVersion)"
+ return """
+ Stored settings version was not the same as current version, \
+ stored version: \(storedVersion), current version: \(currentVersion)
+ """
+ }
+}
+
+/// A wrapper type for errors returned by concrete migrations.
+struct SettingsMigrationError: LocalizedError, WrappingError {
+ private let inner: Error
+ let sourceVersion, targetVersion: SchemaVersion
+
+ var underlyingError: Error? {
+ return inner
+ }
+
+ var errorDescription: String? {
+ return "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/TunnelSettingsV2.swift b/ios/MullvadVPN/SettingsManager/TunnelSettingsV2.swift
index 6c0ada1415..76b794eba9 100644
--- a/ios/MullvadVPN/SettingsManager/TunnelSettingsV2.swift
+++ b/ios/MullvadVPN/SettingsManager/TunnelSettingsV2.swift
@@ -14,7 +14,10 @@ import class WireGuardKitTypes.PrivateKey
import class WireGuardKitTypes.PublicKey
/// Settings and device state schema versions.
-enum SchemaVersion: Int {
+enum SchemaVersion: Int, Equatable {
+ /// Legacy settings format, stored as `TunnelSettingsV1`.
+ case v1 = 1
+
/// New settings format, stored as `TunnelSettingsV2`.
case v2 = 2
diff --git a/ios/MullvadVPN/SettingsMigrationUIHandler.swift b/ios/MullvadVPN/SettingsMigrationUIHandler.swift
new file mode 100644
index 0000000000..949458e394
--- /dev/null
+++ b/ios/MullvadVPN/SettingsMigrationUIHandler.swift
@@ -0,0 +1,13 @@
+//
+// SettingsMigrationUIHandler.swift
+// MullvadVPN
+//
+// Created by pronebird on 24/11/2022.
+// Copyright © 2022 Mullvad VPN AB. All rights reserved.
+//
+
+import Foundation
+
+protocol SettingsMigrationUIHandler {
+ func showMigrationError(_ error: Error, completionHandler: @escaping () -> Void)
+}
diff --git a/ios/Operations/OperationCompletion.swift b/ios/Operations/OperationCompletion.swift
index 0e004bfe50..49a34e0884 100644
--- a/ios/Operations/OperationCompletion.swift
+++ b/ios/Operations/OperationCompletion.swift
@@ -65,6 +65,14 @@ public enum OperationCompletion<Success, Failure: Error> {
}
}
+ public func get() throws -> Success {
+ if let result = result {
+ return try result.get()
+ } else {
+ throw OperationCancellationError()
+ }
+ }
+
public func map<NewSuccess>(_ block: (Success) -> NewSuccess)
-> OperationCompletion<NewSuccess, Failure>
{
@@ -143,3 +151,9 @@ public enum OperationCompletion<Success, Failure: Error> {
return mapError { $0 }
}
}
+
+public struct OperationCancellationError: LocalizedError {
+ public var errorDescription: String? {
+ return "Operation was cancelled."
+ }
+}