Skip to content

Commit

Permalink
Migrate settings to Version3 and introduce incremental migration scheme.
Browse files Browse the repository at this point in the history
  • Loading branch information
buggmagnet committed Oct 30, 2023
1 parent 4f40522 commit c7eecb7
Show file tree
Hide file tree
Showing 16 changed files with 460 additions and 97 deletions.
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

0 comments on commit c7eecb7

Please sign in to comment.