Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Migrate settings to Version3 and introduce incremental migration scheme. #5367

Merged
merged 1 commit into from
Oct 30, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions ios/MullvadREST/RESTAuthorization.swift
Original file line number Diff line number Diff line change
Expand Up @@ -19,10 +19,10 @@ extension REST {
typealias Authorization = String

struct AccessTokenProvider: RESTAuthorizationProvider {
private let accessTokenManager: AccessTokenManager
private let accessTokenManager: RESTAccessTokenManagement
private let accountNumber: String

init(accessTokenManager: AccessTokenManager, accountNumber: String) {
init(accessTokenManager: RESTAccessTokenManagement, accountNumber: String) {
self.accessTokenManager = accessTokenManager
self.accountNumber = accountNumber
}
Expand Down
4 changes: 2 additions & 2 deletions ios/MullvadREST/RESTProxy.swift
Original file line number Diff line number Diff line change
Expand Up @@ -160,11 +160,11 @@ extension REST {
}

public class AuthProxyConfiguration: ProxyConfiguration {
public let accessTokenManager: AccessTokenManager
public let accessTokenManager: RESTAccessTokenManagement

public init(
proxyConfiguration: ProxyConfiguration,
accessTokenManager: AccessTokenManager
accessTokenManager: RESTAccessTokenManagement
) {
self.accessTokenManager = accessTokenManager

Expand Down
84 changes: 42 additions & 42 deletions ios/MullvadSettings/MigrationManager.swift
Original file line number Diff line number Diff line change
Expand Up @@ -29,14 +29,18 @@ public struct MigrationManager {

/// Migrate settings store if needed.
///
/// The following types of error are expected to be returned by this method:
/// `SettingsMigrationError`, `UnsupportedSettingsVersionError`, `ReadSettingsVersionError`.
/// Reads the current settings, upgrades them to the latest version if needed
/// and writes back to `store` when settings are updated.
/// - Parameters:
/// - store: The store to from which settings are read and written to.
/// - proxyFactory: Factory used for migrations that involve API calls.
/// - migrationCompleted: Completion handler called with a migration result.
public func migrateSettings(
store: SettingsStore,
proxyFactory: REST.ProxyFactory,
migrationCompleted: @escaping (SettingsMigrationResult) -> Void
) {
let handleCompletion = { (result: SettingsMigrationResult) in
let resetStoreHandler = { (result: SettingsMigrationResult) in
// Reset store upon failure to migrate settings.
if case .failure = result {
SettingsManager.resetStore()
Expand All @@ -45,56 +49,52 @@ public struct MigrationManager {
}

do {
try checkLatestSettingsVersion(in: store)
handleCompletion(.nothing)
try upgradeSettingsToLatestVersion(
store: store,
proxyFactory: proxyFactory,
migrationCompleted: migrationCompleted
)
} catch .itemNotFound as KeychainError {
migrationCompleted(.nothing)
} catch {
handleCompletion(.failure(error))
resetStoreHandler(.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)
}
private func upgradeSettingsToLatestVersion(
store: SettingsStore,
proxyFactory: REST.ProxyFactory,
migrationCompleted: @escaping (SettingsMigrationResult) -> Void
) throws {
let parser = SettingsParser(decoder: JSONDecoder(), encoder: JSONEncoder())
let settingsData = try store.read(key: SettingsKey.settings)
let settingsVersion = try parser.parseVersion(data: settingsData)

guard settingsVersion != SchemaVersion.current.rawValue else {
migrationCompleted(.nothing)
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.
public struct SettingsMigrationError: LocalizedError, WrappingError {
private let inner: Error
public let sourceVersion, targetVersion: SchemaVersion
// Corrupted settings version (i.e. negative values, or downgrade from a future version) should fail
guard var savedSchema = SchemaVersion(rawValue: settingsVersion) else {
migrationCompleted(.failure(UnsupportedSettingsVersionError(
storedVersion: settingsVersion,
currentVersion: SchemaVersion.current
)))
return
}

public var underlyingError: Error? {
inner
}
var savedSettings = try parser.parsePayload(as: savedSchema.settingsType, from: settingsData)

public var errorDescription: String? {
"Failed to migrate settings from \(sourceVersion) to \(targetVersion)."
}
repeat {
let upgradedVersion = savedSettings.upgradeToNextVersion()
savedSchema = savedSchema.nextVersion
savedSettings = upgradedVersion
} while savedSchema.rawValue < SchemaVersion.current.rawValue

public init(sourceVersion: SchemaVersion, targetVersion: SchemaVersion, underlyingError: Error) {
self.sourceVersion = sourceVersion
self.targetVersion = targetVersion
inner = underlyingError
// Write the latest settings back to the store
let latestVersionPayload = try parser.producePayload(savedSettings, version: SchemaVersion.current.rawValue)
try store.write(latestVersionPayload, for: .settings)
migrationCompleted(.success)
}
}
17 changes: 17 additions & 0 deletions ios/MullvadSettings/SettingsManager.swift
Original file line number Diff line number Diff line change
Expand Up @@ -18,11 +18,28 @@ private let accountExpiryKey = "accountExpiry"
public enum SettingsManager {
private static let logger = Logger(label: "SettingsManager")

#if DEBUG
private static var _store = KeychainSettingsStore(
serviceName: keychainServiceName,
accessGroup: ApplicationConfiguration.securityGroupIdentifier
)

/// Alternative store used for tests.
internal static var unitTestStore: SettingsStore?

public static var store: SettingsStore {
if let unitTestStore { return unitTestStore }
return _store
}

#else
public static let store: SettingsStore = KeychainSettingsStore(
serviceName: keychainServiceName,
accessGroup: ApplicationConfiguration.securityGroupIdentifier
)

#endif

private static func makeParser() -> SettingsParser {
SettingsParser(decoder: JSONDecoder(), encoder: JSONEncoder())
}
Expand Down
37 changes: 37 additions & 0 deletions ios/MullvadSettings/StoredWgKeyData.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
//
// StoredWgKeyData.swift
// MullvadSettings
//
// Created by Marco Nikic on 2023-10-23.
// Copyright © 2023 Mullvad VPN AB. All rights reserved.
//

import Foundation
import WireGuardKitTypes

public struct StoredWgKeyData: Codable, Equatable {
/// Private key creation date.
public var creationDate: Date

/// Last date a rotation was attempted. Nil if last attempt was successful.
public var lastRotationAttemptDate: Date?

/// Private key.
public var privateKey: PrivateKey

/// Next private key we're trying to rotate to.
/// Added in 2023.3
public var nextPrivateKey: PrivateKey?

public init(
creationDate: Date,
lastRotationAttemptDate: Date? = nil,
privateKey: PrivateKey,
nextPrivateKey: PrivateKey? = nil
) {
self.creationDate = creationDate
self.lastRotationAttemptDate = lastRotationAttemptDate
self.privateKey = privateKey
self.nextPrivateKey = nextPrivateKey
}
}
29 changes: 27 additions & 2 deletions ios/MullvadSettings/TunnelSettings.swift
Original file line number Diff line number Diff line change
Expand Up @@ -7,9 +7,15 @@
//

import Foundation
import MullvadREST

/// Alias to the latest version of the `TunnelSettings`.
public typealias LatestTunnelSettings = TunnelSettingsV2
public typealias LatestTunnelSettings = TunnelSettingsV3

/// Protocol all TunnelSettings must adhere to, for upgrade purposes.
public protocol TunnelSettings: Codable {
func upgradeToNextVersion() -> any TunnelSettings
}

/// Settings and device state schema versions.
public enum SchemaVersion: Int, Equatable {
Expand All @@ -19,6 +25,25 @@ public enum SchemaVersion: Int, Equatable {
/// New settings format, stored as `TunnelSettingsV2`.
case v2 = 2

/// V2 format with WireGuard obfuscation options, stored as `TunnelSettingsV3`.
case v3 = 3

var settingsType: any TunnelSettings.Type {
switch self {
case .v1: return TunnelSettingsV1.self
case .v2: return TunnelSettingsV2.self
case .v3: return TunnelSettingsV3.self
}
}

var nextVersion: Self {
switch self {
case .v1: return .v2
case .v2: return .v3
case .v3: return .v3
}
}

/// Current schema version.
public static let current = SchemaVersion.v2
public static let current = SchemaVersion.v3
}
7 changes: 6 additions & 1 deletion ios/MullvadSettings/TunnelSettingsV1.swift
Original file line number Diff line number Diff line change
Expand Up @@ -7,16 +7,21 @@
//

import Foundation
import MullvadREST
import MullvadTypes
import struct Network.IPv4Address
import struct WireGuardKitTypes.IPAddressRange
import class WireGuardKitTypes.PrivateKey
import class WireGuardKitTypes.PublicKey

/// A struct that holds the configuration passed via `NETunnelProviderProtocol`.
public struct TunnelSettingsV1: Codable, Equatable {
public struct TunnelSettingsV1: Codable, Equatable, TunnelSettings {
public var relayConstraints = RelayConstraints()
public var interface = InterfaceSettings()

public func upgradeToNextVersion() -> any TunnelSettings {
TunnelSettingsV2(relayConstraints: relayConstraints, dnsSettings: interface.dnsSettings)
}
}

/// A struct that holds a tun interface configuration.
Expand Down
38 changes: 8 additions & 30 deletions ios/MullvadSettings/TunnelSettingsV2.swift
Original file line number Diff line number Diff line change
Expand Up @@ -7,13 +7,10 @@
//

import Foundation
import MullvadREST
import MullvadTypes
import struct Network.IPv4Address
import struct WireGuardKitTypes.IPAddressRange
import class WireGuardKitTypes.PrivateKey
import class WireGuardKitTypes.PublicKey

public struct TunnelSettingsV2: Codable, Equatable {
public struct TunnelSettingsV2: Codable, Equatable, TunnelSettings {
/// Relay constraints.
public var relayConstraints: RelayConstraints

Expand All @@ -27,31 +24,12 @@ public struct TunnelSettingsV2: Codable, Equatable {
self.relayConstraints = relayConstraints
self.dnsSettings = dnsSettings
}
}

public struct StoredWgKeyData: Codable, Equatable {
/// Private key creation date.
public var creationDate: Date

/// Last date a rotation was attempted. Nil if last attempt was successful.
public var lastRotationAttemptDate: Date?

/// Private key.
public var privateKey: PrivateKey

/// Next private key we're trying to rotate to.
/// Added in 2023.3
public var nextPrivateKey: PrivateKey?

public init(
creationDate: Date,
lastRotationAttemptDate: Date? = nil,
privateKey: PrivateKey,
nextPrivateKey: PrivateKey? = nil
) {
self.creationDate = creationDate
self.lastRotationAttemptDate = lastRotationAttemptDate
self.privateKey = privateKey
self.nextPrivateKey = nextPrivateKey
public func upgradeToNextVersion() -> any TunnelSettings {
TunnelSettingsV3(
relayConstraints: relayConstraints,
dnsSettings: dnsSettings,
wireGuardObfuscation: WireGuardObfuscationSettings()
)
}
}
36 changes: 36 additions & 0 deletions ios/MullvadSettings/TunnelSettingsV3.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
//
// TunnelSettingsV3.swift
// MullvadVPN
//
// Created by Marco Nikic on 2023-10-17.
// Copyright © 2023 Mullvad VPN AB. All rights reserved.
//

import Foundation
import MullvadREST
import MullvadTypes

public struct TunnelSettingsV3: Codable, Equatable, TunnelSettings {
/// Relay constraints.
public var relayConstraints: RelayConstraints

/// DNS settings.
public var dnsSettings: DNSSettings

/// WireGuard obfuscation settings
public var wireGuardObfuscation: WireGuardObfuscationSettings

public init(
relayConstraints: RelayConstraints = RelayConstraints(),
dnsSettings: DNSSettings = DNSSettings(),
wireGuardObfuscation: WireGuardObfuscationSettings = WireGuardObfuscationSettings()
) {
self.relayConstraints = relayConstraints
self.dnsSettings = dnsSettings
self.wireGuardObfuscation = wireGuardObfuscation
}

public func upgradeToNextVersion() -> any TunnelSettings {
self
}
}
31 changes: 31 additions & 0 deletions ios/MullvadSettings/WireGuardObfuscationSettings.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
//
// WireGuardObfuscationSettings.swift
// MullvadVPN
//
// Created by Marco Nikic on 2023-10-17.
// Copyright © 2023 Mullvad VPN AB. All rights reserved.
//

import Foundation

public enum WireGuardObfuscationState: Codable {
case automatic
case on
case off
}

public enum WireGuardObfuscationPort: Codable {
case automatic
case port80
case port5001
}

public struct WireGuardObfuscationSettings: Codable, Equatable {
let state: WireGuardObfuscationState
let port: WireGuardObfuscationPort

public init(state: WireGuardObfuscationState = .automatic, port: WireGuardObfuscationPort = .automatic) {
self.state = state
self.port = port
}
}
Loading
Loading