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 26, 2023
1 parent 7b8db2b commit 6c7c299
Show file tree
Hide file tree
Showing 15 changed files with 493 additions and 98 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
95 changes: 51 additions & 44 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,59 @@ 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 {
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)

// Special case downgrade attempts as nothing to do
guard settingsVersion < SchemaVersion.current.rawValue else {
migrationCompleted(.nothing)
return
} catch {
throw ReadSettingsVersionError(underlyingError: error)
}

guard settingsVersion != SchemaVersion.current.rawValue else {
// Corrupted settings version (i.e. negative values) should fail
guard let savedSchema = SchemaVersion(rawValue: settingsVersion) else {
migrationCompleted(.failure(UnsupportedSettingsVersionError(
storedVersion: settingsVersion,
currentVersion: SchemaVersion.current
)))
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

public var underlyingError: Error? {
inner
}

public var errorDescription: String? {
"Failed to migrate settings from \(sourceVersion) to \(targetVersion)."
}

public init(sourceVersion: SchemaVersion, targetVersion: SchemaVersion, underlyingError: Error) {
self.sourceVersion = sourceVersion
self.targetVersion = targetVersion
inner = underlyingError
var versionTypeCopy = savedSchema
let savedSettings = try parser.parsePayload(as: versionTypeCopy.settingsType, from: settingsData)
var latestSettings = savedSettings

repeat {
let upgradedVersion = latestSettings.upgradeToNextVersion(
store: store,
proxyFactory: proxyFactory,
parser: parser
)
versionTypeCopy = versionTypeCopy.nextVersion
latestSettings = upgradedVersion
} while versionTypeCopy.rawValue < SchemaVersion.current.rawValue

// Write the latest settings back to the store
let latestVersionPayload = try parser.producePayload(latestSettings, 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
}
}
41 changes: 39 additions & 2 deletions ios/MullvadSettings/TunnelSettings.swift
Original file line number Diff line number Diff line change
Expand Up @@ -7,9 +7,19 @@
//

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(
store: SettingsStore,
proxyFactory: REST.ProxyFactory,
parser: SettingsParser
) -> any TunnelSettings
}

/// Settings and device state schema versions.
public enum SchemaVersion: Int, Equatable {
Expand All @@ -19,6 +29,33 @@ 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
}
}

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

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

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(
store: SettingsStore,
proxyFactory: REST.ProxyFactory,
parser: SettingsParser
) -> any TunnelSettings {
TunnelSettingsV2(relayConstraints: relayConstraints, dnsSettings: interface.dnsSettings)
}
}

/// A struct that holds a tun interface configuration.
Expand Down
42 changes: 12 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,16 @@ 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(
store: SettingsStore,
proxyFactory: REST.ProxyFactory,
parser: SettingsParser
) -> any TunnelSettings {
TunnelSettingsV3(
relayConstraints: relayConstraints,
dnsSettings: dnsSettings,
wireGuardObfuscation: WireGuardObfuscationSettings()
)
}
}
40 changes: 40 additions & 0 deletions ios/MullvadSettings/TunnelSettingsV3.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
//
// 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(
store: SettingsStore,
proxyFactory: REST.ProxyFactory,
parser: SettingsParser
) -> any TunnelSettings {
self
}
}
Loading

0 comments on commit 6c7c299

Please sign in to comment.