diff options
| author | Bug Magnet <marco.nikic@mullvad.net> | 2023-09-20 14:17:42 +0200 |
|---|---|---|
| committer | Bug Magnet <marco.nikic@mullvad.net> | 2023-09-20 15:29:59 +0200 |
| commit | da4e628b79fda9a2fa2426c1aa0e1f3cfcbc7ce8 (patch) | |
| tree | 54db6508c9d716d1c7be533d39ff4c847ddffd5a /ios/MullvadSettings | |
| parent | 1e17c07490ea3c66430559794778b343e21d83eb (diff) | |
| download | mullvadvpn-da4e628b79fda9a2fa2426c1aa0e1f3cfcbc7ce8.tar.xz mullvadvpn-da4e628b79fda9a2fa2426c1aa0e1f3cfcbc7ce8.zip | |
Disable Module verifier, rename Settings to MullvadSettings to avoid clash with Apple private Framework of the same name
Diffstat (limited to 'ios/MullvadSettings')
| -rw-r--r-- | ios/MullvadSettings/DNSSettings.swift | 127 | ||||
| -rw-r--r-- | ios/MullvadSettings/DeviceState.swift | 47 | ||||
| -rw-r--r-- | ios/MullvadSettings/KeychainSettingsStore.swift | 105 | ||||
| -rw-r--r-- | ios/MullvadSettings/Migration.swift | 17 | ||||
| -rw-r--r-- | ios/MullvadSettings/MigrationManager.swift | 100 | ||||
| -rw-r--r-- | ios/MullvadSettings/MullvadSettings.h | 19 | ||||
| -rw-r--r-- | ios/MullvadSettings/SettingsManager.swift | 241 | ||||
| -rw-r--r-- | ios/MullvadSettings/SettingsParser.swift | 68 | ||||
| -rw-r--r-- | ios/MullvadSettings/SettingsStore.swift | 22 | ||||
| -rw-r--r-- | ios/MullvadSettings/StoredAccountData.swift | 40 | ||||
| -rw-r--r-- | ios/MullvadSettings/StoredDeviceData.swift | 67 | ||||
| -rw-r--r-- | ios/MullvadSettings/TunnelSettings.swift | 24 | ||||
| -rw-r--r-- | ios/MullvadSettings/TunnelSettingsV1.swift | 101 | ||||
| -rw-r--r-- | ios/MullvadSettings/TunnelSettingsV2.swift | 57 |
14 files changed, 1035 insertions, 0 deletions
diff --git a/ios/MullvadSettings/DNSSettings.swift b/ios/MullvadSettings/DNSSettings.swift new file mode 100644 index 0000000000..cae9de2928 --- /dev/null +++ b/ios/MullvadSettings/DNSSettings.swift @@ -0,0 +1,127 @@ +// +// DNSSettings.swift +// MullvadVPN +// +// Created by pronebird on 27/04/2022. +// Copyright © 2022 Mullvad VPN AB. All rights reserved. +// + +import Foundation +import MullvadTypes +import struct Network.IPv4Address + +/// A struct describing Mullvad DNS blocking options. +public struct DNSBlockingOptions: OptionSet, Codable { + public let rawValue: UInt32 + + public static let blockAdvertising = DNSBlockingOptions(rawValue: 1 << 0) + public static let blockTracking = DNSBlockingOptions(rawValue: 1 << 1) + public static let blockMalware = DNSBlockingOptions(rawValue: 1 << 2) + public static let blockAdultContent = DNSBlockingOptions(rawValue: 1 << 3) + public static let blockGambling = DNSBlockingOptions(rawValue: 1 << 4) + + public var serverAddress: IPv4Address? { + if isEmpty { + return nil + } else { + return IPv4Address("100.64.0.\(rawValue)") + } + } + + public init(rawValue: UInt32) { + self.rawValue = rawValue + } + + public init(from decoder: Decoder) throws { + let container = try decoder.singleValueContainer() + let rawValue = try container.decode(RawValue.self) + + self.init(rawValue: rawValue) + } + + public func encode(to encoder: Encoder) throws { + var container = encoder.singleValueContainer() + + try container.encode(rawValue) + } +} + +/// A struct that holds DNS settings. +public struct DNSSettings: Codable, Equatable { + /// Maximum number of allowed DNS domains. + public static let maxAllowedCustomDNSDomains = 3 + + /// DNS blocking options. + public var blockingOptions: DNSBlockingOptions = [] + + /// Enable custom DNS. + public var enableCustomDNS = false + + /// Custom DNS domains. + public var customDNSDomains: [AnyIPAddress] = [] + + /// Effective state of the custom DNS setting. + public var effectiveEnableCustomDNS: Bool { + blockingOptions.isEmpty && enableCustomDNS && !customDNSDomains.isEmpty + } + + private enum CodingKeys: String, CodingKey { + // Removed in 2022.1 in favor of `blockingOptions` + case blockAdvertising, blockTracking + + // Added in 2022.1 + case blockingOptions + + case enableCustomDNS, customDNSDomains + } + + public init() {} + + public init(from decoder: Decoder) throws { + let container = try decoder.container(keyedBy: CodingKeys.self) + + // Added in 2022.1 + if let storedBlockingOptions = try container.decodeIfPresent( + DNSBlockingOptions.self, + forKey: .blockingOptions + ) { + blockingOptions = storedBlockingOptions + } + + if let storedBlockAdvertising = try container.decodeIfPresent( + Bool.self, + forKey: .blockAdvertising + ), storedBlockAdvertising { + blockingOptions.insert(.blockAdvertising) + } + + if let storedBlockTracking = try container.decodeIfPresent( + Bool.self, + forKey: .blockTracking + ), storedBlockTracking { + blockingOptions.insert(.blockTracking) + } + + if let storedEnableCustomDNS = try container.decodeIfPresent( + Bool.self, + forKey: .enableCustomDNS + ) { + enableCustomDNS = storedEnableCustomDNS + } + + if let storedCustomDNSDomains = try container.decodeIfPresent( + [AnyIPAddress].self, + forKey: .customDNSDomains + ) { + customDNSDomains = storedCustomDNSDomains + } + } + + public func encode(to encoder: Encoder) throws { + var container = encoder.container(keyedBy: CodingKeys.self) + + try container.encode(blockingOptions, forKey: .blockingOptions) + try container.encode(enableCustomDNS, forKey: .enableCustomDNS) + try container.encode(customDNSDomains, forKey: .customDNSDomains) + } +} diff --git a/ios/MullvadSettings/DeviceState.swift b/ios/MullvadSettings/DeviceState.swift new file mode 100644 index 0000000000..052511d328 --- /dev/null +++ b/ios/MullvadSettings/DeviceState.swift @@ -0,0 +1,47 @@ +// +// DeviceState.swift +// MullvadVPN +// +// Created by Marco Nikic on 2023-07-31. +// Copyright © 2023 Mullvad VPN AB. All rights reserved. +// + +import Foundation + +public enum DeviceState: Codable, Equatable { + case loggedIn(StoredAccountData, StoredDeviceData) + case loggedOut + case revoked + + private enum LoggedInCodableKeys: String, CodingKey { + case _0 = "account" + case _1 = "device" + } + + public var isLoggedIn: Bool { + switch self { + case .loggedIn: + return true + case .loggedOut, .revoked: + return false + } + } + + public var accountData: StoredAccountData? { + switch self { + case let .loggedIn(accountData, _): + return accountData + case .loggedOut, .revoked: + return nil + } + } + + public var deviceData: StoredDeviceData? { + switch self { + case let .loggedIn(_, deviceData): + return deviceData + case .loggedOut, .revoked: + return nil + } + } +} diff --git a/ios/MullvadSettings/KeychainSettingsStore.swift b/ios/MullvadSettings/KeychainSettingsStore.swift new file mode 100644 index 0000000000..ba0612de0d --- /dev/null +++ b/ios/MullvadSettings/KeychainSettingsStore.swift @@ -0,0 +1,105 @@ +// +// KeychainSettingsStore.swift +// MullvadVPN +// +// Created by Sajad Vishkai on 2022-11-22. +// Copyright © 2022 Mullvad VPN AB. All rights reserved. +// + +import Foundation +import MullvadTypes +import Security + +public class KeychainSettingsStore: SettingsStore { + public let serviceName: String + public let accessGroup: String + + public init(serviceName: String, accessGroup: String) { + self.serviceName = serviceName + self.accessGroup = accessGroup + } + + public func read(key: SettingsKey) throws -> Data { + try readItemData(key) + } + + public func write(_ data: Data, for key: SettingsKey) throws { + try addOrUpdateItem(key, data: data) + } + + public func delete(key: SettingsKey) throws { + try deleteItem(key) + } + + private func addItem(_ item: SettingsKey, data: Data) throws { + var query = createDefaultAttributes(item: item) + query.merge(createAccessAttributes()) { current, _ in + current + } + query[kSecValueData] = data + + let status = SecItemAdd(query as CFDictionary, nil) + if status != errSecSuccess { + throw KeychainError(code: status) + } + } + + private func updateItem(_ item: SettingsKey, data: Data) throws { + let query = createDefaultAttributes(item: item) + let status = SecItemUpdate( + query as CFDictionary, + [kSecValueData: data] as CFDictionary + ) + + if status != errSecSuccess { + throw KeychainError(code: status) + } + } + + private func addOrUpdateItem(_ item: SettingsKey, data: Data) throws { + do { + try updateItem(item, data: data) + } catch let error as KeychainError where error == .itemNotFound { + try addItem(item, data: data) + } catch { + throw error + } + } + + private func readItemData(_ item: SettingsKey) throws -> Data { + var query = createDefaultAttributes(item: item) + query[kSecReturnData] = true + + var result: CFTypeRef? + let status = SecItemCopyMatching(query as CFDictionary, &result) + + if status == errSecSuccess { + return result as? Data ?? Data() + } else { + throw KeychainError(code: status) + } + } + + private func deleteItem(_ item: SettingsKey) throws { + let query = createDefaultAttributes(item: item) + let status = SecItemDelete(query as CFDictionary) + if status != errSecSuccess { + throw KeychainError(code: status) + } + } + + private func createDefaultAttributes(item: SettingsKey) -> [CFString: Any] { + [ + kSecClass: kSecClassGenericPassword, + kSecAttrService: serviceName, + kSecAttrAccount: item.rawValue, + ] + } + + private func createAccessAttributes() -> [CFString: Any] { + [ + kSecAttrAccessGroup: accessGroup, + kSecAttrAccessible: kSecAttrAccessibleAfterFirstUnlock, + ] + } +} diff --git a/ios/MullvadSettings/Migration.swift b/ios/MullvadSettings/Migration.swift new file mode 100644 index 0000000000..0d9678deb5 --- /dev/null +++ b/ios/MullvadSettings/Migration.swift @@ -0,0 +1,17 @@ +// +// Migration.swift +// MullvadVPN +// +// Created by Sajad Vishkai on 2022-11-18. +// Copyright © 2022 Mullvad VPN AB. All rights reserved. +// + +import Foundation + +public protocol Migration { + func migrate( + with store: SettingsStore, + parser: SettingsParser, + completion: @escaping (Error?) -> Void + ) +} diff --git a/ios/MullvadSettings/MigrationManager.swift b/ios/MullvadSettings/MigrationManager.swift new file mode 100644 index 0000000000..6348cf9471 --- /dev/null +++ b/ios/MullvadSettings/MigrationManager.swift @@ -0,0 +1,100 @@ +// +// 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 + +public enum SettingsMigrationResult { + /// Nothing to migrate. + case nothing + + /// Successfully performed migration. + case success + + /// Failure when migrating store. + case failure(Error) +} + +public struct MigrationManager { + private let logger = Logger(label: "MigrationManager") + + public init() {} + + /// Migrate settings store if needed. + /// + /// The following types of error are expected to be returned by this method: + /// `SettingsMigrationError`, `UnsupportedSettingsVersionError`, `ReadSettingsVersionError`. + public 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. +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 + } +} diff --git a/ios/MullvadSettings/MullvadSettings.h b/ios/MullvadSettings/MullvadSettings.h new file mode 100644 index 0000000000..598468f6ea --- /dev/null +++ b/ios/MullvadSettings/MullvadSettings.h @@ -0,0 +1,19 @@ +// +// Settings.h +// Settings +// +// Created by pronebird on 05/09/2023. +// Copyright © 2023 Mullvad VPN AB. All rights reserved. +// + +#import <Foundation/Foundation.h> + +//! Project version number for Settings. +FOUNDATION_EXPORT double SettingsVersionNumber; + +//! Project version string for Settings. +FOUNDATION_EXPORT const unsigned char SettingsVersionString[]; + +// In this header, you should import all the public headers of your framework using statements like #import <Settings/PublicHeader.h> + + diff --git a/ios/MullvadSettings/SettingsManager.swift b/ios/MullvadSettings/SettingsManager.swift new file mode 100644 index 0000000000..953c052beb --- /dev/null +++ b/ios/MullvadSettings/SettingsManager.swift @@ -0,0 +1,241 @@ +// +// SettingsManager.swift +// MullvadVPN +// +// Created by pronebird on 29/04/2022. +// Copyright © 2022 Mullvad VPN AB. All rights reserved. +// + +import Foundation +import MullvadLogging +import MullvadREST +import MullvadTypes + +private let keychainServiceName = "Mullvad VPN" +private let accountTokenKey = "accountToken" +private let accountExpiryKey = "accountExpiry" + +public enum SettingsManager { + private static let logger = Logger(label: "SettingsManager") + + public static let store: SettingsStore = KeychainSettingsStore( + serviceName: keychainServiceName, + accessGroup: ApplicationConfiguration.securityGroupIdentifier + ) + + private static func makeParser() -> SettingsParser { + SettingsParser(decoder: JSONDecoder(), encoder: JSONEncoder()) + } + + // MARK: - Last used account + + public static func getLastUsedAccount() throws -> String { + let data = try store.read(key: .lastUsedAccount) + + if let string = String(data: data, encoding: .utf8) { + return string + } else { + throw StringDecodingError(data: data) + } + } + + public static func setLastUsedAccount(_ string: String?) throws { + if let string { + guard let data = string.data(using: .utf8) else { + throw StringEncodingError(string: string) + } + + try store.write(data, for: .lastUsedAccount) + } else { + do { + try store.delete(key: .lastUsedAccount) + } catch let error as KeychainError where error == .itemNotFound { + return + } catch { + throw error + } + } + } + + // MARK: - Should wipe settings + + public static func getShouldWipeSettings() -> Bool { + (try? store.read(key: .shouldWipeSettings)) != nil + } + + public static func setShouldWipeSettings() { + do { + try store.write(Data(), for: .shouldWipeSettings) + } catch { + logger.error( + error: error, + message: "Failed to set should wipe settings." + ) + } + } + + // MARK: - Settings + + public static func readSettings() throws -> LatestTunnelSettings { + let storedVersion: Int + let data: Data + let parser = makeParser() + + do { + data = try store.read(key: .settings) + storedVersion = try parser.parseVersion(data: data) + } catch { + throw ReadSettingsVersionError(underlyingError: error) + } + + let currentVersion = SchemaVersion.current + + if storedVersion == currentVersion.rawValue { + return try parser.parsePayload(as: LatestTunnelSettings.self, from: data) + } else { + throw UnsupportedSettingsVersionError( + storedVersion: storedVersion, + currentVersion: currentVersion + ) + } + } + + public static func writeSettings(_ settings: LatestTunnelSettings) throws { + let parser = makeParser() + let data = try parser.producePayload(settings, version: SchemaVersion.current.rawValue) + + try store.write(data, for: .settings) + } + + // MARK: - Device state + + public static func readDeviceState() throws -> DeviceState { + let data = try store.read(key: .deviceState) + let parser = makeParser() + + return try parser.parseUnversionedPayload(as: DeviceState.self, from: data) + } + + public static func writeDeviceState(_ deviceState: DeviceState) throws { + let parser = makeParser() + let data = try parser.produceUnversionedPayload(deviceState) + + try store.write(data, for: .deviceState) + } + + /// Removes all legacy settings, device state and tunnel settings but keeps the last used + /// account number stored. + public static func resetStore(completely: Bool = false) { + logger.debug("Reset store.") + + do { + try store.delete(key: .deviceState) + } catch { + if (error as? KeychainError) != .itemNotFound { + logger.error(error: error, message: "Failed to delete device state.") + } + } + + do { + try store.delete(key: .settings) + } catch { + if (error as? KeychainError) != .itemNotFound { + logger.error(error: error, message: "Failed to delete settings.") + } + } + + if completely { + do { + try store.delete(key: .lastUsedAccount) + } catch { + if (error as? KeychainError) != .itemNotFound { + logger.error(error: error, message: "Failed to delete last used account.") + } + } + + do { + try store.delete(key: .shouldWipeSettings) + } catch { + if (error as? KeychainError) != .itemNotFound { + logger.error(error: error, message: "Failed to delete should wipe settings.") + } + } + } + } + + // MARK: - Private + + private static func checkLatestSettingsVersion() throws { + let settingsVersion: Int + do { + let parser = makeParser() + let settingsData = try store.read(key: .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 + } +} + +// MARK: - Supporting types + +/// An error type describing a failure to read or parse settings version. +public struct ReadSettingsVersionError: LocalizedError, WrappingError { + private let inner: Error + + public var underlyingError: Error? { + inner + } + + public var errorDescription: String? { + "Failed to read settings version." + } + + public init(underlyingError: Error) { + inner = underlyingError + } +} + +/// An error returned when stored settings version is unknown to the currently running app. +public struct UnsupportedSettingsVersionError: LocalizedError { + public let storedVersion: Int + public let currentVersion: SchemaVersion + + public var errorDescription: String? { + """ + Stored settings version was not the same as current version, \ + stored version: \(storedVersion), current version: \(currentVersion) + """ + } +} + +public struct StringDecodingError: LocalizedError { + public let data: Data + + public var errorDescription: String? { + "Failed to decode string from data." + } +} + +public struct StringEncodingError: LocalizedError { + public let string: String + + public var errorDescription: String? { + "Failed to encode string into data." + } +} diff --git a/ios/MullvadSettings/SettingsParser.swift b/ios/MullvadSettings/SettingsParser.swift new file mode 100644 index 0000000000..16bd1ba7ff --- /dev/null +++ b/ios/MullvadSettings/SettingsParser.swift @@ -0,0 +1,68 @@ +// +// SettingsParser.swift +// MullvadVPN +// +// Created by Sajad Vishkai on 2022-11-22. +// Copyright © 2022 Mullvad VPN AB. All rights reserved. +// + +import Foundation + +private struct VersionHeader: Codable { + var version: Int +} + +private struct Payload<T: Codable>: Codable { + var data: T +} + +private struct VersionedPayload<T: Codable>: Codable { + var version: Int + var data: T +} + +public struct SettingsParser { + /// The decoder used to decode values. + private let decoder: JSONDecoder + + /// The encoder used to encode values. + private let encoder: JSONEncoder + + public init(decoder: JSONDecoder, encoder: JSONEncoder) { + self.decoder = decoder + self.encoder = encoder + } + + /// Produces versioned data encoded as the given type + public func producePayload(_ payload: some Codable, version: Int) throws -> Data { + try encoder.encode(VersionedPayload(version: version, data: payload)) + } + + /// Produces unversioned data encoded as the given type + public func produceUnversionedPayload(_ payload: some Codable) throws -> Data { + try encoder.encode(payload) + } + + /// Returns settings version if found inside the stored data. + public func parseVersion(data: Data) throws -> Int { + let header = try decoder.decode(VersionHeader.self, from: data) + + return header.version + } + + /// Returns unversioned payload parsed as the given type. + public func parseUnversionedPayload<T: Codable>( + as type: T.Type, + from data: Data + ) throws -> T { + try decoder.decode(T.self, from: data) + } + + /// Returns data from versioned payload parsed as the given type. + public func parsePayload<T: Codable>( + as type: T.Type, + from data: Data + ) throws -> T { + try decoder.decode(Payload<T>.self, from: data).data + } +} diff --git a/ios/MullvadSettings/SettingsStore.swift b/ios/MullvadSettings/SettingsStore.swift new file mode 100644 index 0000000000..4609ce8d39 --- /dev/null +++ b/ios/MullvadSettings/SettingsStore.swift @@ -0,0 +1,22 @@ +// +// SettingsStore.swift +// MullvadVPN +// +// Created by Sajad Vishkai on 2022-11-22. +// Copyright © 2022 Mullvad VPN AB. All rights reserved. +// + +import Foundation + +public enum SettingsKey: String, CaseIterable { + case settings = "Settings" + case deviceState = "DeviceState" + case lastUsedAccount = "LastUsedAccount" + case shouldWipeSettings = "ShouldWipeSettings" +} + +public protocol SettingsStore { + func read(key: SettingsKey) throws -> Data + func write(_ data: Data, for key: SettingsKey) throws + func delete(key: SettingsKey) throws +} diff --git a/ios/MullvadSettings/StoredAccountData.swift b/ios/MullvadSettings/StoredAccountData.swift new file mode 100644 index 0000000000..276982805c --- /dev/null +++ b/ios/MullvadSettings/StoredAccountData.swift @@ -0,0 +1,40 @@ +// +// StoredAccountData.swift +// MullvadVPN +// +// Created by Marco Nikic on 2023-07-31. +// Copyright © 2023 Mullvad VPN AB. All rights reserved. +// + +import Foundation + +public struct StoredAccountData: Codable, Equatable { + /// Account identifier. + public var identifier: String + + /// Account number. + public var number: String + + /// Account expiry. + public var expiry: Date + + /// Returns `true` if account has expired. + public var isExpired: Bool { + expiry <= Date() + } + + public init(identifier: String, number: String, expiry: Date) { + self.identifier = identifier + self.number = number + self.expiry = expiry + } +} + +extension StoredAccountData { + public init(from decoder: Decoder) throws { + let container = try decoder.container(keyedBy: CodingKeys.self) + self.identifier = try container.decode(String.self, forKey: .identifier) + self.number = try container.decode(String.self, forKey: .number) + self.expiry = try container.decode(Date.self, forKey: .expiry) + } +} diff --git a/ios/MullvadSettings/StoredDeviceData.swift b/ios/MullvadSettings/StoredDeviceData.swift new file mode 100644 index 0000000000..a0d784af61 --- /dev/null +++ b/ios/MullvadSettings/StoredDeviceData.swift @@ -0,0 +1,67 @@ +// +// StoredDeviceData.swift +// MullvadVPN +// +// Created by Marco Nikic on 2023-07-31. +// Copyright © 2023 Mullvad VPN AB. All rights reserved. +// + +import Foundation +import MullvadTypes +import WireGuardKitTypes + +public struct StoredDeviceData: Codable, Equatable { + /// Device creation date. + public var creationDate: Date + + /// Device identifier. + public var identifier: String + + /// Device name. + public var name: String + + /// Whether relay hijacks DNS from this device. + public var hijackDNS: Bool + + /// IPv4 address + mask assigned to device. + public var ipv4Address: IPAddressRange + + /// IPv6 address + mask assigned to device. + public var ipv6Address: IPAddressRange + + /// WireGuard key data. + public var wgKeyData: StoredWgKeyData + + /// Returns capitalized device name. + public var capitalizedName: String { + name.capitalized + } + + public init( + creationDate: Date, + identifier: String, + name: String, + hijackDNS: Bool, + ipv4Address: IPAddressRange, + ipv6Address: IPAddressRange, + wgKeyData: StoredWgKeyData + ) { + self.creationDate = creationDate + self.identifier = identifier + self.name = name + self.hijackDNS = hijackDNS + self.ipv4Address = ipv4Address + self.ipv6Address = ipv6Address + self.wgKeyData = wgKeyData + } + + /// Fill in part of the structure that contains device related properties from `Device` struct. + public mutating func update(from device: Device) { + identifier = device.id + name = device.name + creationDate = device.created + hijackDNS = device.hijackDNS + ipv4Address = device.ipv4Address + ipv6Address = device.ipv6Address + } +} diff --git a/ios/MullvadSettings/TunnelSettings.swift b/ios/MullvadSettings/TunnelSettings.swift new file mode 100644 index 0000000000..f58c88c47c --- /dev/null +++ b/ios/MullvadSettings/TunnelSettings.swift @@ -0,0 +1,24 @@ +// +// TunnelSettings.swift +// MullvadVPN +// +// Created by Marco Nikic on 2023-07-31. +// Copyright © 2023 Mullvad VPN AB. All rights reserved. +// + +import Foundation + +/// Alias to the latest version of the `TunnelSettings`. +public typealias LatestTunnelSettings = TunnelSettingsV2 + +/// Settings and device state schema versions. +public 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. + public static let current = SchemaVersion.v2 +} diff --git a/ios/MullvadSettings/TunnelSettingsV1.swift b/ios/MullvadSettings/TunnelSettingsV1.swift new file mode 100644 index 0000000000..5148266e21 --- /dev/null +++ b/ios/MullvadSettings/TunnelSettingsV1.swift @@ -0,0 +1,101 @@ +// +// TunnelSettingsV1.swift +// MullvadVPN +// +// Created by pronebird on 19/06/2019. +// Copyright © 2019 Mullvad VPN AB. All rights reserved. +// + +import Foundation +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 var relayConstraints = RelayConstraints() + public var interface = InterfaceSettings() +} + +/// A struct that holds a tun interface configuration. +public struct InterfaceSettings: Codable, Equatable { + public var privateKey: PrivateKeyWithMetadata + public var nextPrivateKey: PrivateKeyWithMetadata? + + public var addresses: [IPAddressRange] + public var dnsSettings: DNSSettings + + private enum CodingKeys: String, CodingKey { + case privateKey, nextPrivateKey, addresses, dnsSettings + } + + public init( + privateKey: PrivateKeyWithMetadata = PrivateKeyWithMetadata(), + nextPrivateKey: PrivateKeyWithMetadata? = nil, + addresses: [IPAddressRange] = [], + dnsSettings: DNSSettings = DNSSettings() + ) { + self.privateKey = privateKey + self.nextPrivateKey = nextPrivateKey + self.addresses = addresses + self.dnsSettings = dnsSettings + } + + public init(from decoder: Decoder) throws { + let container = try decoder.container(keyedBy: CodingKeys.self) + + privateKey = try container.decode(PrivateKeyWithMetadata.self, forKey: .privateKey) + addresses = try container.decode([IPAddressRange].self, forKey: .addresses) + + // Added in 2022.1 + nextPrivateKey = try container.decodeIfPresent( + PrivateKeyWithMetadata.self, + forKey: .nextPrivateKey + ) + + // Provide default value, since `dnsSettings` key does not exist in <= 2021.2 + dnsSettings = try container.decodeIfPresent(DNSSettings.self, forKey: .dnsSettings) + ?? DNSSettings() + } + + public func encode(to encoder: Encoder) throws { + var container = encoder.container(keyedBy: CodingKeys.self) + + try container.encode(privateKey, forKey: .privateKey) + try container.encode(nextPrivateKey, forKey: .nextPrivateKey) + try container.encode(addresses, forKey: .addresses) + try container.encode(dnsSettings, forKey: .dnsSettings) + } +} + +/// A struct holding a private WireGuard key with associated metadata +public struct PrivateKeyWithMetadata: Equatable, Codable { + private enum CodingKeys: String, CodingKey { + case privateKey = "privateKeyData", creationDate + } + + /// When the key was created + public let creationDate: Date + + /// Private key + public let privateKey: PrivateKey + + /// Public key + public var publicKey: PublicKey { + privateKey.publicKey + } + + /// Initialize the new private key + public init() { + privateKey = PrivateKey() + creationDate = Date() + } + + /// Initialize with the existing private key + public init(privateKey: PrivateKey, createdAt: Date) { + self.privateKey = privateKey + creationDate = createdAt + } +} diff --git a/ios/MullvadSettings/TunnelSettingsV2.swift b/ios/MullvadSettings/TunnelSettingsV2.swift new file mode 100644 index 0000000000..001cc3adb6 --- /dev/null +++ b/ios/MullvadSettings/TunnelSettingsV2.swift @@ -0,0 +1,57 @@ +// +// TunnelSettingsV2.swift +// MullvadVPN +// +// Created by pronebird on 27/04/2022. +// Copyright © 2022 Mullvad VPN AB. All rights reserved. +// + +import Foundation +import MullvadTypes +import struct Network.IPv4Address +import struct WireGuardKitTypes.IPAddressRange +import class WireGuardKitTypes.PrivateKey +import class WireGuardKitTypes.PublicKey + +public struct TunnelSettingsV2: Codable, Equatable { + /// Relay constraints. + public var relayConstraints: RelayConstraints + + /// DNS settings. + public var dnsSettings: DNSSettings + + public init( + relayConstraints: RelayConstraints = RelayConstraints(), + dnsSettings: DNSSettings = DNSSettings() + ) { + 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 + } +} |
