summaryrefslogtreecommitdiffhomepage
diff options
context:
space:
mode:
authorAndrej Mihajlov <and@mullvad.net>2023-06-08 14:02:34 +0200
committerAndrej Mihajlov <and@mullvad.net>2023-06-08 14:02:34 +0200
commitac8deecc4ebd4a71810006d221292c03e36ad120 (patch)
treec76f15cc8d0819f3eef59e766235bfd7049a5503
parent0ca8f82db446df494b400f0b467de1bef9e051e4 (diff)
parentdab9467547a35369aafdda7174287921dc7accc8 (diff)
downloadmullvadvpn-ac8deecc4ebd4a71810006d221292c03e36ad120.tar.xz
mullvadvpn-ac8deecc4ebd4a71810006d221292c03e36ad120.zip
Merge remote-tracking branch 'origin/packet-tunnel-should-rotate-the-key-if-ios-109'
-rw-r--r--ios/CHANGELOG.md2
-rw-r--r--ios/MullvadREST/RESTAccountsProxy.swift13
-rw-r--r--ios/MullvadREST/RESTDevicesProxy.swift20
-rw-r--r--ios/MullvadREST/RESTError.swift5
-rw-r--r--ios/MullvadTypes/PacketTunnelStatus.swift76
-rw-r--r--ios/MullvadTypes/RESTTypes.swift73
-rw-r--r--ios/MullvadVPN.xcodeproj/project.pbxproj211
-rw-r--r--ios/MullvadVPN.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved2
-rw-r--r--ios/MullvadVPN/AppDelegate.swift22
-rw-r--r--ios/MullvadVPN/Notifications/Notification Providers/TunnelStatusNotificationProvider.swift4
-rw-r--r--ios/MullvadVPN/SettingsManager/Migrations/MigrationFromV1ToV2.swift10
-rw-r--r--ios/MullvadVPN/SettingsManager/TunnelSettingsV2+REST.swift22
-rw-r--r--ios/MullvadVPN/SettingsManager/TunnelSettingsV2.swift45
-rw-r--r--ios/MullvadVPN/SimulatorTunnelProvider/SimulatorTunnelProvider.swift24
-rw-r--r--ios/MullvadVPN/SimulatorTunnelProvider/SimulatorTunnelProviderHost.swift10
-rw-r--r--ios/MullvadVPN/StorePaymentManager/StorePaymentManager.swift2
-rw-r--r--ios/MullvadVPN/TunnelManager/RotateKeyOperation.swift126
-rw-r--r--ios/MullvadVPN/TunnelManager/SetAccountOperation.swift14
-rw-r--r--ios/MullvadVPN/TunnelManager/TunnelInteractor.swift3
-rw-r--r--ios/MullvadVPN/TunnelManager/TunnelManager.swift140
-rw-r--r--ios/MullvadVPN/TunnelManager/UpdateAccountDataOperation.swift2
-rw-r--r--ios/MullvadVPN/TunnelManager/UpdateDeviceDataOperation.swift2
-rw-r--r--ios/MullvadVPN/TunnelManager/WgKeyRotation.swift138
-rw-r--r--ios/MullvadVPN/View controllers/DeviceList/DeviceManagementInteractor.swift10
-rw-r--r--ios/MullvadVPN/View controllers/DeviceList/DeviceManagementViewController.swift3
-rw-r--r--ios/MullvadVPNTests/DeviceCheckOperationTests.swift573
-rw-r--r--ios/MullvadVPNTests/WgKeyRotationTests.swift117
-rw-r--r--ios/PacketTunnel/DeviceCheck/DeviceCheckOperation.swift295
-rw-r--r--ios/PacketTunnel/DeviceCheck/DeviceCheckRemoteService.swift58
-rw-r--r--ios/PacketTunnel/DeviceCheck/DeviceCheckRemoteServiceProtocol.swift25
-rw-r--r--ios/PacketTunnel/DeviceCheck/DeviceStateAccessor.swift21
-rw-r--r--ios/PacketTunnel/DeviceCheck/DeviceStateAccessorProtocol.swift15
-rw-r--r--ios/PacketTunnel/PacketTunnelProvider.swift196
-rw-r--r--ios/TunnelProviderMessaging/PacketTunnelOptions.swift2
34 files changed, 1797 insertions, 484 deletions
diff --git a/ios/CHANGELOG.md b/ios/CHANGELOG.md
index 422f9b8d48..90ec221650 100644
--- a/ios/CHANGELOG.md
+++ b/ios/CHANGELOG.md
@@ -27,6 +27,8 @@ Line wrap the file at 100 chars. Th
- Add search functionality to location selection view.
- Wipe all settings on app reinstall.
- Add a dedicated account button on the main view and remove it from settings.
+- Rotate public key from within packet tunnel when it detects that the key stored on backend does
+ not match the one stored on device.
## [2023.2 - 2023-04-03]
diff --git a/ios/MullvadREST/RESTAccountsProxy.swift b/ios/MullvadREST/RESTAccountsProxy.swift
index cb50b66faa..11426bb415 100644
--- a/ios/MullvadREST/RESTAccountsProxy.swift
+++ b/ios/MullvadREST/RESTAccountsProxy.swift
@@ -52,7 +52,7 @@ extension REST {
public func getAccountData(
accountNumber: String,
retryStrategy: REST.RetryStrategy,
- completion: @escaping CompletionHandler<AccountData>
+ completion: @escaping CompletionHandler<Account>
) -> Cancellable {
let requestHandler = AnyRequestHandler(
createURLRequest: { endpoint, authorization in
@@ -70,7 +70,7 @@ extension REST {
)
let responseHandler = REST.defaultResponseHandler(
- decoding: AccountData.self,
+ decoding: Account.self,
with: responseDecoder
)
@@ -84,15 +84,6 @@ extension REST {
}
}
- public struct AccountData: Decodable {
- public let id: String
- public let expiry: Date
- public let maxPorts: Int
- public let canAddPorts: Bool
- public let maxDevices: Int
- public let canAddDevices: Bool
- }
-
public struct NewAccountData: Decodable {
public let id: String
public let expiry: Date
diff --git a/ios/MullvadREST/RESTDevicesProxy.swift b/ios/MullvadREST/RESTDevicesProxy.swift
index 14648ca19b..37cfd809d7 100644
--- a/ios/MullvadREST/RESTDevicesProxy.swift
+++ b/ios/MullvadREST/RESTDevicesProxy.swift
@@ -309,24 +309,4 @@ extension REST {
try container.encode(publicKey.base64Key, forKey: .publicKey)
}
}
-
- public struct Device: Decodable {
- public let id: String
- public let name: String
- public let pubkey: PublicKey
- public let hijackDNS: Bool
- public let created: Date
- public let ipv4Address: IPAddressRange
- public let ipv6Address: IPAddressRange
- public let ports: [Port]
-
- private enum CodingKeys: String, CodingKey {
- case hijackDNS = "hijackDns"
- case id, name, pubkey, created, ipv4Address, ipv6Address, ports
- }
- }
-
- public struct Port: Decodable {
- public let id: String
- }
}
diff --git a/ios/MullvadREST/RESTError.swift b/ios/MullvadREST/RESTError.swift
index 08e562c866..90a5cb6be1 100644
--- a/ios/MullvadREST/RESTError.swift
+++ b/ios/MullvadREST/RESTError.swift
@@ -96,6 +96,11 @@ extension REST {
detail = try container.decodeIfPresent(String.self, forKey: .detail)
?? container.decodeIfPresent(String.self, forKey: .error)
}
+
+ public init(code: REST.ServerResponseCode, detail: String? = nil) {
+ self.code = code
+ self.detail = detail
+ }
}
public struct ServerResponseCode: RawRepresentable, Equatable {
diff --git a/ios/MullvadTypes/PacketTunnelStatus.swift b/ios/MullvadTypes/PacketTunnelStatus.swift
index bceed0c2fe..8a1161b354 100644
--- a/ios/MullvadTypes/PacketTunnelStatus.swift
+++ b/ios/MullvadTypes/PacketTunnelStatus.swift
@@ -8,27 +8,73 @@
import Foundation
+/// The verdict of an account status check.
+public enum AccountVerdict: Equatable, Codable {
+ /// Account is no longer valid.
+ case invalid
+
+ /// Account is expired.
+ case expired(Account)
+
+ /// Account exists and has enough time left.
+ case active(Account)
+}
+
+/// The verdict of a device status check.
+public enum DeviceVerdict: Equatable, Codable {
+ /// Device is revoked.
+ case revoked
+
+ /// Device exists but the public key registered on server does not match any longer.
+ case keyMismatch
+
+ /// Device is in good standing and should work as normal.
+ case active
+}
+
+/// Type describing whether key rotation took place and the outcome of it.
+public enum KeyRotationStatus: Equatable, Codable {
+ /// No rotation took place yet.
+ case noAction
+
+ /// Rotation attempt took place but without success.
+ case attempted(Date)
+
+ /// Rotation attempt took place and succeeded.
+ case succeeded(Date)
+
+ /// Returns `true` if the status is `.succeeded`.
+ public var isSucceeded: Bool {
+ if case .succeeded = self {
+ return true
+ } else {
+ return false
+ }
+ }
+}
+
+/**
+ Struct holding data associated with account and device diagnostics and also device key recovery performed by packet
+ tunnel process.
+ */
public struct DeviceCheck: Codable, Equatable {
- /// Unique identifier for the device check.
- /// Should only change when other fields in the struct are being changed.
- public var identifier: UUID
+ /// The verdict of account status check.
+ public var accountVerdict: AccountVerdict
- /// Flag indicating whether device is revoked.
- /// Set to `nil` when the device status is unknown yet.
- public var isDeviceRevoked: Bool?
+ /// The verdict of device status check.
+ public var deviceVerdict: DeviceVerdict
- /// Last known account expiry.
- /// Set to `nil` when account expiry is unknown yet.
- public var accountExpiry: Date?
+ // The status of the last performed key rotation.
+ public var keyRotationStatus: KeyRotationStatus
public init(
- identifier: UUID = UUID(),
- isDeviceRevoked: Bool? = nil,
- accountExpiry: Date? = nil
+ accountVerdict: AccountVerdict,
+ deviceVerdict: DeviceVerdict,
+ keyRotationStatus: KeyRotationStatus
) {
- self.identifier = identifier
- self.isDeviceRevoked = isDeviceRevoked
- self.accountExpiry = accountExpiry
+ self.accountVerdict = accountVerdict
+ self.deviceVerdict = deviceVerdict
+ self.keyRotationStatus = keyRotationStatus
}
}
diff --git a/ios/MullvadTypes/RESTTypes.swift b/ios/MullvadTypes/RESTTypes.swift
new file mode 100644
index 0000000000..a39d8df87a
--- /dev/null
+++ b/ios/MullvadTypes/RESTTypes.swift
@@ -0,0 +1,73 @@
+//
+// RESTTypes.swift
+// MullvadTypes
+//
+// Created by pronebird on 24/05/2023.
+// Copyright © 2023 Mullvad VPN AB. All rights reserved.
+//
+
+import Foundation
+import struct WireGuardKitTypes.IPAddressRange
+import class WireGuardKitTypes.PublicKey
+
+public struct Account: Codable, Equatable {
+ public let id: String
+ public let expiry: Date
+ public let maxPorts: Int
+ public let canAddPorts: Bool
+ public let maxDevices: Int
+ public let canAddDevices: Bool
+
+ public init(id: String, expiry: Date, maxPorts: Int, canAddPorts: Bool, maxDevices: Int, canAddDevices: Bool) {
+ self.id = id
+ self.expiry = expiry
+ self.maxPorts = maxPorts
+ self.canAddPorts = canAddPorts
+ self.maxDevices = maxDevices
+ self.canAddDevices = canAddDevices
+ }
+}
+
+public struct Device: Codable, Equatable {
+ public let id: String
+ public let name: String
+ public let pubkey: PublicKey
+ public let hijackDNS: Bool
+ public let created: Date
+ public let ipv4Address: IPAddressRange
+ public let ipv6Address: IPAddressRange
+ public let ports: [Port]
+
+ private enum CodingKeys: String, CodingKey {
+ case hijackDNS = "hijackDns"
+ case id, name, pubkey, created, ipv4Address, ipv6Address, ports
+ }
+
+ public init(
+ id: String,
+ name: String,
+ pubkey: PublicKey,
+ hijackDNS: Bool,
+ created: Date,
+ ipv4Address: IPAddressRange,
+ ipv6Address: IPAddressRange,
+ ports: [Port]
+ ) {
+ self.id = id
+ self.name = name
+ self.pubkey = pubkey
+ self.hijackDNS = hijackDNS
+ self.created = created
+ self.ipv4Address = ipv4Address
+ self.ipv6Address = ipv6Address
+ self.ports = ports
+ }
+}
+
+public struct Port: Codable, Equatable {
+ public let id: String
+
+ public init(id: String) {
+ self.id = id
+ }
+}
diff --git a/ios/MullvadVPN.xcodeproj/project.pbxproj b/ios/MullvadVPN.xcodeproj/project.pbxproj
index e6389fd0ab..ad13c84ad4 100644
--- a/ios/MullvadVPN.xcodeproj/project.pbxproj
+++ b/ios/MullvadVPN.xcodeproj/project.pbxproj
@@ -9,7 +9,6 @@
/* Begin PBXBuildFile section */
01F1FF1E29F0627D007083C3 /* libshadowsocks_proxy.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 01F1FF1D29F0627D007083C3 /* libshadowsocks_proxy.a */; };
062B45A328FD4CA700746E77 /* le_root_cert.cer in Resources */ = {isa = PBXBuildFile; fileRef = 06799AB428F98CE700ACD94E /* le_root_cert.cer */; };
- 062B45AE28FD503000746E77 /* WireGuardKit in Frameworks */ = {isa = PBXBuildFile; productRef = 062B45AD28FD503000746E77 /* WireGuardKit */; };
062B45BC28FD8C3B00746E77 /* RESTDefaults.swift in Sources */ = {isa = PBXBuildFile; fileRef = 062B45BB28FD8C3B00746E77 /* RESTDefaults.swift */; };
063687BA28EB234F00BE7161 /* PacketTunnelTransport.swift in Sources */ = {isa = PBXBuildFile; fileRef = 063687B928EB234F00BE7161 /* PacketTunnelTransport.swift */; };
063F026628FFE11C001FA09F /* RESTCreateApplePaymentResponse+Localization.swift in Sources */ = {isa = PBXBuildFile; fileRef = 06FAE67828F83CA50033DD93 /* RESTCreateApplePaymentResponse+Localization.swift */; };
@@ -18,7 +17,6 @@
063F027A2902B63F001FA09F /* RelayCache.framework in Embed Frameworks */ = {isa = PBXBuildFile; fileRef = 063F02732902B63F001FA09F /* RelayCache.framework */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; };
063F027E2902B6EB001FA09F /* RelayCache.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5820675A26E6576800655B05 /* RelayCache.swift */; };
063F027F2902B6EB001FA09F /* CachedRelays.swift in Sources */ = {isa = PBXBuildFile; fileRef = 585DA87626B024A600B8C587 /* CachedRelays.swift */; };
- 063F028A2902B7B2001FA09F /* WireGuardKitTypes in Frameworks */ = {isa = PBXBuildFile; productRef = 063F02892902B7B2001FA09F /* WireGuardKitTypes */; };
063F028F2902BD8C001FA09F /* relays.json in Resources */ = {isa = PBXBuildFile; fileRef = 58F3C0A524A50155003E76BE /* relays.json */; };
06410DFE292CE18F00AFC18C /* KeychainSettingsStore.swift in Sources */ = {isa = PBXBuildFile; fileRef = 06410DFD292CE18F00AFC18C /* KeychainSettingsStore.swift */; };
06410DFF292CF16C00AFC18C /* KeychainSettingsStore.swift in Sources */ = {isa = PBXBuildFile; fileRef = 06410DFD292CE18F00AFC18C /* KeychainSettingsStore.swift */; };
@@ -57,21 +55,27 @@
068CE5782927BE4800A068BB /* Migration.swift in Sources */ = {isa = PBXBuildFile; fileRef = 068CE5732927B7A400A068BB /* Migration.swift */; };
0697D6E728F01513007A9E99 /* TransportMonitor.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0697D6E628F01513007A9E99 /* TransportMonitor.swift */; };
06AC116228F94C450037AF9A /* ApplicationConfiguration.swift in Sources */ = {isa = PBXBuildFile; fileRef = 58BFA5CB22A7CE1F00A6173D /* ApplicationConfiguration.swift */; };
- 06D9844C28F990AB003AABE9 /* WireGuardKitTypes in Frameworks */ = {isa = PBXBuildFile; productRef = 06D9844B28F990AB003AABE9 /* WireGuardKitTypes */; };
5803B4B02940A47300C23744 /* TunnelConfiguration.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5803B4AF2940A47300C23744 /* TunnelConfiguration.swift */; };
5803B4B22940A48700C23744 /* TunnelStore.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5803B4B12940A48700C23744 /* TunnelStore.swift */; };
5806767C27048E9B00C858CB /* PacketTunnelProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = 58CE5E7B224146470008646E /* PacketTunnelProvider.swift */; };
- 5807483B27DB8A980020ECBF /* WireGuardKitTypes in Frameworks */ = {isa = PBXBuildFile; productRef = 5807483A27DB8A980020ECBF /* WireGuardKitTypes */; };
5807E2C02432038B00F5FF30 /* String+Split.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5807E2BF2432038B00F5FF30 /* String+Split.swift */; };
5807E2C2243203D000F5FF30 /* StringTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5807E2C1243203D000F5FF30 /* StringTests.swift */; };
5807E2C3243203E700F5FF30 /* String+Split.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5807E2BF2432038B00F5FF30 /* String+Split.swift */; };
+ 580810E52A30E13A00B74552 /* DeviceStateAccessorProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = 580810E42A30E13A00B74552 /* DeviceStateAccessorProtocol.swift */; };
+ 580810E62A30E13D00B74552 /* DeviceStateAccessorProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = 580810E42A30E13A00B74552 /* DeviceStateAccessorProtocol.swift */; };
+ 580810E82A30E15500B74552 /* DeviceCheckRemoteServiceProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = 580810E72A30E15500B74552 /* DeviceCheckRemoteServiceProtocol.swift */; };
+ 580810E92A30E17300B74552 /* DeviceCheckRemoteServiceProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = 580810E72A30E15500B74552 /* DeviceCheckRemoteServiceProtocol.swift */; };
580909D32876D09A0078138D /* RevokedDeviceViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 580909D22876D09A0078138D /* RevokedDeviceViewController.swift */; };
580F8B8328197881002E0998 /* TunnelSettingsV2.swift in Sources */ = {isa = PBXBuildFile; fileRef = 580F8B8228197881002E0998 /* TunnelSettingsV2.swift */; };
580F8B8428197884002E0998 /* TunnelSettingsV2.swift in Sources */ = {isa = PBXBuildFile; fileRef = 580F8B8228197881002E0998 /* TunnelSettingsV2.swift */; };
5811DE50239014550011EB53 /* NEVPNStatus+Debug.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5811DE4F239014550011EB53 /* NEVPNStatus+Debug.swift */; };
58138E61294871C600684F0C /* DeviceDataThrottling.swift in Sources */ = {isa = PBXBuildFile; fileRef = 58138E60294871C600684F0C /* DeviceDataThrottling.swift */; };
58153071294CBE8B00D1702E /* MullvadREST.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 06799ABC28F98E1D00ACD94E /* MullvadREST.framework */; };
+ 58165EBE2A262CBB00688EAD /* WgKeyRotationTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 58165EBD2A262CBB00688EAD /* WgKeyRotationTests.swift */; };
5819C2172729595500D6EC38 /* SettingsAddDNSEntryCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5819C2162729595500D6EC38 /* SettingsAddDNSEntryCell.swift */; };
+ 581DA2732A1E227D0046ED47 /* RESTTypes.swift in Sources */ = {isa = PBXBuildFile; fileRef = 581DA2722A1E227D0046ED47 /* RESTTypes.swift */; };
+ 581DA2752A1E283E0046ED47 /* WgKeyRotation.swift in Sources */ = {isa = PBXBuildFile; fileRef = 581DA2742A1E283E0046ED47 /* WgKeyRotation.swift */; };
+ 581DA2762A1E2FD10046ED47 /* WgKeyRotation.swift in Sources */ = {isa = PBXBuildFile; fileRef = 581DA2742A1E283E0046ED47 /* WgKeyRotation.swift */; };
5820676426E771DB00655B05 /* TunnelManagerErrors.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5820676326E771DB00655B05 /* TunnelManagerErrors.swift */; };
5820EDA9288FE064006BF4E4 /* DeviceManagementInteractor.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5820EDA8288FE064006BF4E4 /* DeviceManagementInteractor.swift */; };
5820EDAB288FF0D2006BF4E4 /* DeviceRowView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5820EDAA288FF0D2006BF4E4 /* DeviceRowView.swift */; };
@@ -86,8 +90,8 @@
582BB1B1229569620055B6EF /* UINavigationBar+Appearance.swift in Sources */ = {isa = PBXBuildFile; fileRef = 582BB1B0229569620055B6EF /* UINavigationBar+Appearance.swift */; };
5835B7CC233B76CB0096D79F /* TunnelManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5835B7CB233B76CB0096D79F /* TunnelManager.swift */; };
5838318B27C40A3900000571 /* Pinger.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5838318A27C40A3900000571 /* Pinger.swift */; };
+ 583D86482A2678DC0060D63B /* DeviceStateAccessor.swift in Sources */ = {isa = PBXBuildFile; fileRef = 583D86472A2678DC0060D63B /* DeviceStateAccessor.swift */; };
583DA21425FA4B5C00318683 /* LocationDataSource.swift in Sources */ = {isa = PBXBuildFile; fileRef = 583DA21325FA4B5C00318683 /* LocationDataSource.swift */; };
- 583E1E2C2848E1A1004838B3 /* WireGuardKitTypes in Frameworks */ = {isa = PBXBuildFile; productRef = 583E1E2B2848E1A1004838B3 /* WireGuardKitTypes */; };
583FE00C29C0C7FD006E85F9 /* ModalPresentationConfiguration.swift in Sources */ = {isa = PBXBuildFile; fileRef = 583FE00B29C0C7FD006E85F9 /* ModalPresentationConfiguration.swift */; };
583FE00E29C0D586006E85F9 /* OutOfTimeCoordinator.swift in Sources */ = {isa = PBXBuildFile; fileRef = 583FE00D29C0D586006E85F9 /* OutOfTimeCoordinator.swift */; };
583FE01029C0F532006E85F9 /* CustomSplitViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 583FE00F29C0F532006E85F9 /* CustomSplitViewController.swift */; };
@@ -95,7 +99,6 @@
583FE02429C1ACB3006E85F9 /* RESTCreateApplePaymentResponse+Localization.swift in Sources */ = {isa = PBXBuildFile; fileRef = 06FAE67828F83CA50033DD93 /* RESTCreateApplePaymentResponse+Localization.swift */; };
58421030282D8A3C00F24E46 /* UpdateAccountDataOperation.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5842102F282D8A3C00F24E46 /* UpdateAccountDataOperation.swift */; };
58421032282E42B000F24E46 /* UpdateDeviceDataOperation.swift in Sources */ = {isa = PBXBuildFile; fileRef = 58421031282E42B000F24E46 /* UpdateDeviceDataOperation.swift */; };
- 58421034282E4B1500F24E46 /* TunnelSettingsV2+REST.swift in Sources */ = {isa = PBXBuildFile; fileRef = 58421033282E4B1500F24E46 /* TunnelSettingsV2+REST.swift */; };
58435AC229CB2A350099C71B /* LocationCellFactory.swift in Sources */ = {isa = PBXBuildFile; fileRef = 58435AC129CB2A350099C71B /* LocationCellFactory.swift */; };
584592612639B4A200EF967F /* TermsOfServiceContentView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 584592602639B4A200EF967F /* TermsOfServiceContentView.swift */; };
5846227126E229F20035F7C2 /* StoreSubscription.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5846227026E229F20035F7C2 /* StoreSubscription.swift */; };
@@ -128,6 +131,9 @@
5867771629097C5B006F721F /* ProductState.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5867771529097C5B006F721F /* ProductState.swift */; };
5868585524054096000B8131 /* AppButton.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5868585424054096000B8131 /* AppButton.swift */; };
586891CD29D452E4002A8278 /* SafariCoordinator.swift in Sources */ = {isa = PBXBuildFile; fileRef = 586891CC29D452E4002A8278 /* SafariCoordinator.swift */; };
+ 586A0DCB2A20E359006C731C /* MullvadTypes.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 58D223D5294C8E5E0029F5F8 /* MullvadTypes.framework */; };
+ 586A0DD12A20E371006C731C /* WireGuardKitTypes in Frameworks */ = {isa = PBXBuildFile; productRef = 586A0DD02A20E371006C731C /* WireGuardKitTypes */; };
+ 586A0DD42A20E4A9006C731C /* MullvadREST.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 06799ABC28F98E1D00ACD94E /* MullvadREST.framework */; };
586A950C290125EE007BAF2B /* AlertPresenter.swift in Sources */ = {isa = PBXBuildFile; fileRef = 58B9EB122488ED2100095626 /* AlertPresenter.swift */; };
586A950D290125F0007BAF2B /* PresentAlertOperation.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5820675D26E6839900655B05 /* PresentAlertOperation.swift */; };
586A950E290125F3007BAF2B /* ProductsRequestOperation.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5846226426E0D9630035F7C2 /* ProductsRequestOperation.swift */; };
@@ -180,6 +186,13 @@
588E4EAE28FEEDD8008046E3 /* MullvadREST.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 06799ABC28F98E1D00ACD94E /* MullvadREST.framework */; };
58906DE02445C7A5002F0673 /* NEProviderStopReason+Debug.swift in Sources */ = {isa = PBXBuildFile; fileRef = 58906DDF2445C7A5002F0673 /* NEProviderStopReason+Debug.swift */; };
58907D9524D17B4E00CFC3F5 /* DisconnectSplitButton.swift in Sources */ = {isa = PBXBuildFile; fileRef = 58907D9424D17B4E00CFC3F5 /* DisconnectSplitButton.swift */; };
+ 58915D632A25F8400066445B /* DeviceCheckOperationTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 58915D622A25F8400066445B /* DeviceCheckOperationTests.swift */; };
+ 58915D642A25F8B30066445B /* DeviceCheckOperation.swift in Sources */ = {isa = PBXBuildFile; fileRef = 58FDF2D82A0BA11900C2B061 /* DeviceCheckOperation.swift */; };
+ 58915D652A25F9E20066445B /* TunnelSettingsV2.swift in Sources */ = {isa = PBXBuildFile; fileRef = 580F8B8228197881002E0998 /* TunnelSettingsV2.swift */; };
+ 58915D682A25FA080066445B /* DeviceCheckRemoteService.swift in Sources */ = {isa = PBXBuildFile; fileRef = 58915D672A25FA080066445B /* DeviceCheckRemoteService.swift */; };
+ 58915D692A2601FB0066445B /* WgKeyRotation.swift in Sources */ = {isa = PBXBuildFile; fileRef = 581DA2742A1E283E0046ED47 /* WgKeyRotation.swift */; };
+ 58915D6A2A26031B0066445B /* DNSSettings.swift in Sources */ = {isa = PBXBuildFile; fileRef = 580F8B8528197958002E0998 /* DNSSettings.swift */; };
+ 58915D6E2A26037A0066445B /* WireGuardKitTypes in Frameworks */ = {isa = PBXBuildFile; productRef = 58915D6D2A26037A0066445B /* WireGuardKitTypes */; };
5891BF1C25E3E3EB006D6FB0 /* Bundle+ProductVersion.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5891BF1B25E3E3EB006D6FB0 /* Bundle+ProductVersion.swift */; };
5891BF5125E66B1E006D6FB0 /* UIBarButtonItem+KeyboardNavigation.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5891BF5025E66B1E006D6FB0 /* UIBarButtonItem+KeyboardNavigation.swift */; };
5892A45E265FABFF00890742 /* EmptyTableViewHeaderFooterView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5892A45D265FABFF00890742 /* EmptyTableViewHeaderFooterView.swift */; };
@@ -341,6 +354,10 @@
58EE2E3B272FF814003BFF93 /* SettingsDataSourceDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 58EE2E39272FF814003BFF93 /* SettingsDataSourceDelegate.swift */; };
58EF580B25D69D7A00AEBA94 /* ProblemReportSubmissionOverlayView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 58EF580A25D69D7A00AEBA94 /* ProblemReportSubmissionOverlayView.swift */; };
58EF581125D69DB400AEBA94 /* StatusImageView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 58EF581025D69DB400AEBA94 /* StatusImageView.swift */; };
+ 58F0974E2A20C31100DA2DAD /* WireGuardKitTypes in Frameworks */ = {isa = PBXBuildFile; productRef = 58F0974D2A20C31100DA2DAD /* WireGuardKitTypes */; };
+ 58F0974F2A20C31100DA2DAD /* WireGuardKitTypes in Embed Frameworks */ = {isa = PBXBuildFile; productRef = 58F0974D2A20C31100DA2DAD /* WireGuardKitTypes */; settings = {ATTRIBUTES = (CodeSignOnCopy, ); }; };
+ 58F097512A20C35000DA2DAD /* WireGuardKitTypes in Frameworks */ = {isa = PBXBuildFile; productRef = 58F097502A20C35000DA2DAD /* WireGuardKitTypes */; };
+ 58F097542A20C36000DA2DAD /* WireGuardKit in Frameworks */ = {isa = PBXBuildFile; productRef = 58F097532A20C36000DA2DAD /* WireGuardKit */; };
58F185AA298A3E3E00075977 /* TunnelCoordinator.swift in Sources */ = {isa = PBXBuildFile; fileRef = 58F185A9298A3E3E00075977 /* TunnelCoordinator.swift */; };
58F19E35228C15BA00C7710B /* SpinnerActivityIndicatorView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 58F19E34228C15BA00C7710B /* SpinnerActivityIndicatorView.swift */; };
58F2E144276A13F300A79513 /* StartTunnelOperation.swift in Sources */ = {isa = PBXBuildFile; fileRef = 58F2E143276A13F300A79513 /* StartTunnelOperation.swift */; };
@@ -357,6 +374,7 @@
58FC040A27B3EE03001C21F0 /* TunnelMonitor.swift in Sources */ = {isa = PBXBuildFile; fileRef = 58FC040927B3EE03001C21F0 /* TunnelMonitor.swift */; };
58FD5BF024238EB300112C88 /* SKProduct+Formatting.swift in Sources */ = {isa = PBXBuildFile; fileRef = 58FD5BEF24238EB300112C88 /* SKProduct+Formatting.swift */; };
58FD5BF42428C67600112C88 /* InAppPurchaseButton.swift in Sources */ = {isa = PBXBuildFile; fileRef = 58FD5BF32428C67600112C88 /* InAppPurchaseButton.swift */; };
+ 58FDF2D92A0BA11A00C2B061 /* DeviceCheckOperation.swift in Sources */ = {isa = PBXBuildFile; fileRef = 58FDF2D82A0BA11900C2B061 /* DeviceCheckOperation.swift */; };
58FEEB58260B662E00A621A8 /* AutomaticKeyboardResponder.swift in Sources */ = {isa = PBXBuildFile; fileRef = 58FEEB57260B662E00A621A8 /* AutomaticKeyboardResponder.swift */; };
58FF2C03281BDE02009EF542 /* SettingsManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 58FF2C02281BDE02009EF542 /* SettingsManager.swift */; };
7A09C98129D99215000C2CAC /* String+FuzzyMatch.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7A09C98029D99215000C2CAC /* String+FuzzyMatch.swift */; };
@@ -436,6 +454,20 @@
remoteGlobalIDString = 06799ABB28F98E1D00ACD94E;
remoteInfo = MullvadREST;
};
+ 586A0DCD2A20E359006C731C /* PBXContainerItemProxy */ = {
+ isa = PBXContainerItemProxy;
+ containerPortal = 58CE5E58224146200008646E /* Project object */;
+ proxyType = 1;
+ remoteGlobalIDString = 58D223D4294C8E5E0029F5F8;
+ remoteInfo = MullvadTypes;
+ };
+ 586A0DD62A20E4A9006C731C /* PBXContainerItemProxy */ = {
+ isa = PBXContainerItemProxy;
+ containerPortal = 58CE5E58224146200008646E /* Project object */;
+ proxyType = 1;
+ remoteGlobalIDString = 06799ABB28F98E1D00ACD94E;
+ remoteInfo = MullvadREST;
+ };
58CE5E7F224146470008646E /* PBXContainerItemProxy */ = {
isa = PBXContainerItemProxy;
containerPortal = 58CE5E58224146200008646E /* Project object */;
@@ -660,11 +692,32 @@
06799AD228F98E1D00ACD94E /* MullvadREST.framework in Embed Frameworks */,
58D223CD294C8BCB0029F5F8 /* Operations.framework in Embed Frameworks */,
A97F1F482A1F4E1A00ECEFDE /* MullvadTransport.framework in Embed Frameworks */,
+ 58F0974F2A20C31100DA2DAD /* WireGuardKitTypes in Embed Frameworks */,
063F027A2902B63F001FA09F /* RelayCache.framework in Embed Frameworks */,
);
name = "Embed Frameworks";
runOnlyForDeploymentPostprocessing = 0;
};
+ 586A0DD32A20E371006C731C /* Embed Frameworks */ = {
+ isa = PBXCopyFilesBuildPhase;
+ buildActionMask = 2147483647;
+ dstPath = "";
+ dstSubfolderSpec = 10;
+ files = (
+ );
+ name = "Embed Frameworks";
+ runOnlyForDeploymentPostprocessing = 0;
+ };
+ 58915D6F2A26037A0066445B /* Embed Frameworks */ = {
+ isa = PBXCopyFilesBuildPhase;
+ buildActionMask = 2147483647;
+ dstPath = "";
+ dstSubfolderSpec = 10;
+ files = (
+ );
+ name = "Embed Frameworks";
+ runOnlyForDeploymentPostprocessing = 0;
+ };
5898D28729017BD300EB5EBA /* CopyFiles */ = {
isa = PBXCopyFilesBuildPhase;
buildActionMask = 2147483647;
@@ -694,6 +747,16 @@
name = "Embed Foundation Extensions";
runOnlyForDeploymentPostprocessing = 0;
};
+ 58F097442A20C24C00DA2DAD /* Embed Frameworks */ = {
+ isa = PBXCopyFilesBuildPhase;
+ buildActionMask = 2147483647;
+ dstPath = "";
+ dstSubfolderSpec = 10;
+ files = (
+ );
+ name = "Embed Frameworks";
+ runOnlyForDeploymentPostprocessing = 0;
+ };
/* End PBXCopyFilesBuildPhase section */
/* Begin PBXFileReference section */
@@ -748,6 +811,8 @@
58059DDD28468158002B1049 /* OutputOperation.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OutputOperation.swift; sourceTree = "<group>"; };
5807E2BF2432038B00F5FF30 /* String+Split.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "String+Split.swift"; sourceTree = "<group>"; };
5807E2C1243203D000F5FF30 /* StringTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = StringTests.swift; sourceTree = "<group>"; };
+ 580810E42A30E13A00B74552 /* DeviceStateAccessorProtocol.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DeviceStateAccessorProtocol.swift; sourceTree = "<group>"; };
+ 580810E72A30E15500B74552 /* DeviceCheckRemoteServiceProtocol.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DeviceCheckRemoteServiceProtocol.swift; sourceTree = "<group>"; };
5808273928487E3E006B77A4 /* Base.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = Base.xcconfig; sourceTree = "<group>"; };
5808273B284888BC006B77A4 /* App.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = App.xcconfig; sourceTree = "<group>"; };
5808273C284888E5006B77A4 /* PacketTunnel.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = PacketTunnel.xcconfig; sourceTree = "<group>"; };
@@ -758,6 +823,7 @@
580F8B8528197958002E0998 /* DNSSettings.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DNSSettings.swift; sourceTree = "<group>"; };
5811DE4F239014550011EB53 /* NEVPNStatus+Debug.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "NEVPNStatus+Debug.swift"; sourceTree = "<group>"; };
58138E60294871C600684F0C /* DeviceDataThrottling.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DeviceDataThrottling.swift; sourceTree = "<group>"; };
+ 58165EBD2A262CBB00688EAD /* WgKeyRotationTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WgKeyRotationTests.swift; sourceTree = "<group>"; };
581813A028E09DBB002817DE /* NoCancelledDependenciesCondition.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NoCancelledDependenciesCondition.swift; sourceTree = "<group>"; };
581813A228E09DCD002817DE /* NoFailedDependenciesCondition.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NoFailedDependenciesCondition.swift; sourceTree = "<group>"; };
581813A428E09DE2002817DE /* BlockCondition.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BlockCondition.swift; sourceTree = "<group>"; };
@@ -771,6 +837,8 @@
581943E328F8010400B0CB5E /* CustomFormatLogHandler.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = CustomFormatLogHandler.swift; sourceTree = "<group>"; };
581943E428F8010400B0CB5E /* OSLogHandler.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = OSLogHandler.swift; sourceTree = "<group>"; };
5819C2162729595500D6EC38 /* SettingsAddDNSEntryCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SettingsAddDNSEntryCell.swift; sourceTree = "<group>"; };
+ 581DA2722A1E227D0046ED47 /* RESTTypes.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RESTTypes.swift; sourceTree = "<group>"; };
+ 581DA2742A1E283E0046ED47 /* WgKeyRotation.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WgKeyRotation.swift; sourceTree = "<group>"; };
5820675A26E6576800655B05 /* RelayCache.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RelayCache.swift; sourceTree = "<group>"; };
5820675D26E6839900655B05 /* PresentAlertOperation.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PresentAlertOperation.swift; sourceTree = "<group>"; };
5820676326E771DB00655B05 /* TunnelManagerErrors.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TunnelManagerErrors.swift; sourceTree = "<group>"; };
@@ -791,6 +859,7 @@
582FFA82290A84E700895745 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist; path = Info.plist; sourceTree = "<group>"; };
5835B7CB233B76CB0096D79F /* TunnelManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TunnelManager.swift; sourceTree = "<group>"; };
5838318A27C40A3900000571 /* Pinger.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Pinger.swift; sourceTree = "<group>"; };
+ 583D86472A2678DC0060D63B /* DeviceStateAccessor.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DeviceStateAccessor.swift; sourceTree = "<group>"; };
583DA21325FA4B5C00318683 /* LocationDataSource.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LocationDataSource.swift; sourceTree = "<group>"; };
583E1E292848DF67004838B3 /* OperationObserverTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OperationObserverTests.swift; sourceTree = "<group>"; };
583FE00B29C0C7FD006E85F9 /* ModalPresentationConfiguration.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ModalPresentationConfiguration.swift; sourceTree = "<group>"; };
@@ -802,7 +871,6 @@
5842102D282D3FC200F24E46 /* ResultBlockOperation.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ResultBlockOperation.swift; sourceTree = "<group>"; };
5842102F282D8A3C00F24E46 /* UpdateAccountDataOperation.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UpdateAccountDataOperation.swift; sourceTree = "<group>"; };
58421031282E42B000F24E46 /* UpdateDeviceDataOperation.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UpdateDeviceDataOperation.swift; sourceTree = "<group>"; };
- 58421033282E4B1500F24E46 /* TunnelSettingsV2+REST.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "TunnelSettingsV2+REST.swift"; sourceTree = "<group>"; };
58435AC129CB2A350099C71B /* LocationCellFactory.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = LocationCellFactory.swift; sourceTree = "<group>"; };
584592602639B4A200EF967F /* TermsOfServiceContentView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TermsOfServiceContentView.swift; sourceTree = "<group>"; };
5846226426E0D9630035F7C2 /* ProductsRequestOperation.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ProductsRequestOperation.swift; sourceTree = "<group>"; };
@@ -894,6 +962,8 @@
58900D0228BBDCC70094E4F0 /* FixedWidthInteger+Arithmetics.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "FixedWidthInteger+Arithmetics.swift"; sourceTree = "<group>"; };
58906DDF2445C7A5002F0673 /* NEProviderStopReason+Debug.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "NEProviderStopReason+Debug.swift"; sourceTree = "<group>"; };
58907D9424D17B4E00CFC3F5 /* DisconnectSplitButton.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DisconnectSplitButton.swift; sourceTree = "<group>"; };
+ 58915D622A25F8400066445B /* DeviceCheckOperationTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DeviceCheckOperationTests.swift; sourceTree = "<group>"; };
+ 58915D672A25FA080066445B /* DeviceCheckRemoteService.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DeviceCheckRemoteService.swift; sourceTree = "<group>"; };
5891BF1B25E3E3EB006D6FB0 /* Bundle+ProductVersion.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Bundle+ProductVersion.swift"; sourceTree = "<group>"; };
5891BF5025E66B1E006D6FB0 /* UIBarButtonItem+KeyboardNavigation.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "UIBarButtonItem+KeyboardNavigation.swift"; sourceTree = "<group>"; };
5892A45D265FABFF00890742 /* EmptyTableViewHeaderFooterView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EmptyTableViewHeaderFooterView.swift; sourceTree = "<group>"; };
@@ -1029,6 +1099,7 @@
58FC040927B3EE03001C21F0 /* TunnelMonitor.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TunnelMonitor.swift; sourceTree = "<group>"; };
58FD5BEF24238EB300112C88 /* SKProduct+Formatting.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "SKProduct+Formatting.swift"; sourceTree = "<group>"; };
58FD5BF32428C67600112C88 /* InAppPurchaseButton.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = InAppPurchaseButton.swift; sourceTree = "<group>"; };
+ 58FDF2D82A0BA11900C2B061 /* DeviceCheckOperation.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = DeviceCheckOperation.swift; sourceTree = "<group>"; };
58FEEB57260B662E00A621A8 /* AutomaticKeyboardResponder.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AutomaticKeyboardResponder.swift; sourceTree = "<group>"; };
58FF2C02281BDE02009EF542 /* SettingsManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SettingsManager.swift; sourceTree = "<group>"; };
7A09C98029D99215000C2CAC /* String+FuzzyMatch.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "String+FuzzyMatch.swift"; sourceTree = "<group>"; };
@@ -1061,7 +1132,7 @@
isa = PBXFrameworksBuildPhase;
buildActionMask = 2147483647;
files = (
- 063F028A2902B7B2001FA09F /* WireGuardKitTypes in Frameworks */,
+ 586A0DD42A20E4A9006C731C /* MullvadREST.framework in Frameworks */,
);
runOnlyForDeploymentPostprocessing = 0;
};
@@ -1070,9 +1141,9 @@
buildActionMask = 2147483647;
files = (
58D223BF294C8AE90029F5F8 /* Operations.framework in Frameworks */,
+ 586A0DD12A20E371006C731C /* WireGuardKitTypes in Frameworks */,
58D2241D294C91D20029F5F8 /* MullvadLogging.framework in Frameworks */,
58D223DC294C8EB90029F5F8 /* MullvadTypes.framework in Frameworks */,
- 06D9844C28F990AB003AABE9 /* WireGuardKitTypes in Frameworks */,
);
runOnlyForDeploymentPostprocessing = 0;
};
@@ -1105,8 +1176,8 @@
buildActionMask = 2147483647;
files = (
584F99202902CBDD001F858D /* libRelaySelector.a in Frameworks */,
+ 58915D6E2A26037A0066445B /* WireGuardKitTypes in Frameworks */,
588E4EAE28FEEDD8008046E3 /* MullvadREST.framework in Frameworks */,
- 583E1E2C2848E1A1004838B3 /* WireGuardKitTypes in Frameworks */,
);
runOnlyForDeploymentPostprocessing = 0;
};
@@ -1114,6 +1185,7 @@
isa = PBXFrameworksBuildPhase;
buildActionMask = 2147483647;
files = (
+ 58F0974E2A20C31100DA2DAD /* WireGuardKitTypes in Frameworks */,
5898D2A92901844E00EB5EBA /* libRelaySelector.a in Frameworks */,
58D223F9294C8FF00029F5F8 /* MullvadLogging.framework in Frameworks */,
58D223E6294C8F120029F5F8 /* MullvadTypes.framework in Frameworks */,
@@ -1122,7 +1194,6 @@
06799AD128F98E1D00ACD94E /* MullvadREST.framework in Frameworks */,
063F02792902B63F001FA09F /* RelayCache.framework in Frameworks */,
A97F1F472A1F4E1A00ECEFDE /* MullvadTransport.framework in Frameworks */,
- 5807483B27DB8A980020ECBF /* WireGuardKitTypes in Frameworks */,
);
runOnlyForDeploymentPostprocessing = 0;
};
@@ -1136,8 +1207,8 @@
58D223EA294C8F3C0029F5F8 /* MullvadTypes.framework in Frameworks */,
58D223C6294C8B970029F5F8 /* Operations.framework in Frameworks */,
58153071294CBE8B00D1702E /* MullvadREST.framework in Frameworks */,
+ 58F097542A20C36000DA2DAD /* WireGuardKit in Frameworks */,
58D22422294C921B0029F5F8 /* MullvadLogging.framework in Frameworks */,
- 062B45AE28FD503000746E77 /* WireGuardKit in Frameworks */,
);
runOnlyForDeploymentPostprocessing = 0;
};
@@ -1152,6 +1223,7 @@
isa = PBXFrameworksBuildPhase;
buildActionMask = 2147483647;
files = (
+ 586A0DCB2A20E359006C731C /* MullvadTypes.framework in Frameworks */,
);
runOnlyForDeploymentPostprocessing = 0;
};
@@ -1159,6 +1231,7 @@
isa = PBXFrameworksBuildPhase;
buildActionMask = 2147483647;
files = (
+ 58F097512A20C35000DA2DAD /* WireGuardKitTypes in Frameworks */,
);
runOnlyForDeploymentPostprocessing = 0;
};
@@ -1253,8 +1326,6 @@
06FAE66528F83CA30033DD93 /* RESTURLSession.swift */,
06FAE67728F83CA40033DD93 /* ServerRelaysResponse.swift */,
06FAE66B28F83CA30033DD93 /* SSLPinningURLSessionDelegate.swift */,
- 01F1FF1B29F06124007083C3 /* ShadowsocksProxy.swift */,
- 06FAE67C28F83CA50033DD93 /* URLSessionTransport.swift */,
);
path = MullvadREST;
sourceTree = "<group>";
@@ -1279,7 +1350,6 @@
580F8B8528197958002E0998 /* DNSSettings.swift */,
587AD7C523421D7000E93A53 /* TunnelSettingsV1.swift */,
580F8B8228197881002E0998 /* TunnelSettingsV2.swift */,
- 58421033282E4B1500F24E46 /* TunnelSettingsV2+REST.swift */,
);
path = SettingsManager;
sourceTree = "<group>";
@@ -1309,6 +1379,7 @@
5898D2B12902A6DE00EB5EBA /* RelayConstraint.swift */,
58781CC822AE7CA8009B9D8E /* RelayConstraints.swift */,
5898D2AF2902A67C00EB5EBA /* RelayLocation.swift */,
+ 581DA2722A1E227D0046ED47 /* RESTTypes.swift */,
58F1311427E0B2AB007AC5BC /* Result+Extensions.swift */,
58E511E028DDB7F100B0BCDE /* WrappingError.swift */,
);
@@ -1337,6 +1408,7 @@
5803B4B12940A48700C23744 /* TunnelStore.swift */,
5842102F282D8A3C00F24E46 /* UpdateAccountDataOperation.swift */,
58421031282E42B000F24E46 /* UpdateDeviceDataOperation.swift */,
+ 581DA2742A1E283E0046ED47 /* WgKeyRotation.swift */,
);
path = TunnelManager;
sourceTree = "<group>";
@@ -1705,6 +1777,18 @@
path = "Notification Providers";
sourceTree = "<group>";
};
+ 58915D662A25F9F20066445B /* DeviceCheck */ = {
+ isa = PBXGroup;
+ children = (
+ 58FDF2D82A0BA11900C2B061 /* DeviceCheckOperation.swift */,
+ 58915D672A25FA080066445B /* DeviceCheckRemoteService.swift */,
+ 580810E72A30E15500B74552 /* DeviceCheckRemoteServiceProtocol.swift */,
+ 583D86472A2678DC0060D63B /* DeviceStateAccessor.swift */,
+ 580810E42A30E13A00B74552 /* DeviceStateAccessorProtocol.swift */,
+ );
+ path = DeviceCheck;
+ sourceTree = "<group>";
+ };
589453E0297807DB0015DA3B /* App */ = {
isa = PBXGroup;
children = (
@@ -1781,10 +1865,12 @@
children = (
582AE3112440CA0D00E6733A /* AccountTokenInputTests.swift */,
5896AE85246D6AD8005B36CB /* CustomDateComponentsFormattingTests.swift */,
+ 58915D622A25F8400066445B /* DeviceCheckOperationTests.swift */,
+ 582A8A3928BCE19B00D0F9FB /* FixedWidthIntegerArithmeticsTests.swift */,
58B0A2A4238EE67E00BC001D /* Info.plist */,
584B26F3237434D00073B10E /* RelaySelectorTests.swift */,
5807E2C1243203D000F5FF30 /* StringTests.swift */,
- 582A8A3928BCE19B00D0F9FB /* FixedWidthIntegerArithmeticsTests.swift */,
+ 58165EBD2A262CBB00688EAD /* WgKeyRotationTests.swift */,
);
path = MullvadVPNTests;
sourceTree = "<group>";
@@ -1924,6 +2010,7 @@
58E07298288031D5008902F8 /* WireGuardAdapterError+Localization.swift */,
58E0729E28814ACC008902F8 /* WireGuardLogLevel+Logging.swift */,
58906DDF2445C7A5002F0673 /* NEProviderStopReason+Debug.swift */,
+ 58915D662A25F9F20066445B /* DeviceCheck */,
);
path = PacketTunnel;
sourceTree = "<group>";
@@ -2141,10 +2228,10 @@
);
dependencies = (
063F02822902B6F8001FA09F /* PBXTargetDependency */,
+ 586A0DD72A20E4A9006C731C /* PBXTargetDependency */,
);
name = RelayCache;
packageProductDependencies = (
- 063F02892902B7B2001FA09F /* WireGuardKitTypes */,
);
productName = RelayCache;
productReference = 063F02732902B63F001FA09F /* RelayCache.framework */;
@@ -2158,6 +2245,7 @@
06799AB828F98E1D00ACD94E /* Sources */,
06799AB928F98E1D00ACD94E /* Frameworks */,
06799ABA28F98E1D00ACD94E /* Resources */,
+ 586A0DD32A20E371006C731C /* Embed Frameworks */,
);
buildRules = (
);
@@ -2169,7 +2257,7 @@
);
name = MullvadREST;
packageProductDependencies = (
- 06D9844B28F990AB003AABE9 /* WireGuardKitTypes */,
+ 586A0DD02A20E371006C731C /* WireGuardKitTypes */,
);
productName = MullvadREST;
productReference = 06799ABC28F98E1D00ACD94E /* MullvadREST.framework */;
@@ -2236,16 +2324,18 @@
58B0A29C238EE67E00BC001D /* Sources */,
58B0A29D238EE67E00BC001D /* Frameworks */,
58B0A29E238EE67E00BC001D /* Resources */,
+ 58915D6F2A26037A0066445B /* Embed Frameworks */,
);
buildRules = (
);
dependencies = (
+ 58915D6C2A2603700066445B /* PBXTargetDependency */,
062B45BF28FDA85D00746E77 /* PBXTargetDependency */,
06410DFA292C4ABC00AFC18C /* PBXTargetDependency */,
);
name = MullvadVPNTests;
packageProductDependencies = (
- 583E1E2B2848E1A1004838B3 /* WireGuardKitTypes */,
+ 58915D6D2A26037A0066445B /* WireGuardKitTypes */,
);
productName = MullvadVPNTests;
productReference = 58B0A2A0238EE67E00BC001D /* MullvadVPNTests.xctest */;
@@ -2276,7 +2366,7 @@
);
name = MullvadVPN;
packageProductDependencies = (
- 5807483A27DB8A980020ECBF /* WireGuardKitTypes */,
+ 58F0974D2A20C31100DA2DAD /* WireGuardKitTypes */,
);
productName = MullvadVPN;
productReference = 58CE5E60224146200008646E /* MullvadVPN.app */;
@@ -2306,7 +2396,7 @@
);
name = PacketTunnel;
packageProductDependencies = (
- 062B45AD28FD503000746E77 /* WireGuardKit */,
+ 58F097532A20C36000DA2DAD /* WireGuardKit */,
);
productName = PacketTunnel;
productReference = 58CE5E79224146470008646E /* PacketTunnel.appex */;
@@ -2343,6 +2433,7 @@
);
dependencies = (
58EED36E29FBEF040000CBAF /* PBXTargetDependency */,
+ 586A0DCE2A20E359006C731C /* PBXTargetDependency */,
);
name = Operations;
productName = Operations;
@@ -2357,12 +2448,16 @@
58D223D1294C8E5E0029F5F8 /* Sources */,
58D223D2294C8E5E0029F5F8 /* Frameworks */,
58D223D3294C8E5E0029F5F8 /* Resources */,
+ 58F097442A20C24C00DA2DAD /* Embed Frameworks */,
);
buildRules = (
);
dependencies = (
);
name = MullvadTypes;
+ packageProductDependencies = (
+ 58F097502A20C35000DA2DAD /* WireGuardKitTypes */,
+ );
productName = MullvadTypes;
productReference = 58D223D5294C8E5E0029F5F8 /* MullvadTypes.framework */;
productType = "com.apple.product-type.framework";
@@ -2514,7 +2609,7 @@
mainGroup = 58CE5E57224146200008646E;
packageReferences = (
585834F624D2BC1F00A8AF56 /* XCRemoteSwiftPackageReference "swift-log" */,
- 58BA79192578F092006FAEA0 /* XCRemoteSwiftPackageReference "wireguard-apple" */,
+ 58F097482A20C30000DA2DAD /* XCRemoteSwiftPackageReference "wireguard-apple" */,
);
productRefGroup = 58CE5E61224146200008646E /* Products */;
projectDirPath = "";
@@ -2756,13 +2851,21 @@
isa = PBXSourcesBuildPhase;
buildActionMask = 2147483647;
files = (
+ 58915D6A2A26031B0066445B /* DNSSettings.swift in Sources */,
58B8644529C7971B005E107C /* AccountTokenInput.swift in Sources */,
+ 58915D692A2601FB0066445B /* WgKeyRotation.swift in Sources */,
582AE3122440CA0D00E6733A /* AccountTokenInputTests.swift in Sources */,
+ 580810E62A30E13D00B74552 /* DeviceStateAccessorProtocol.swift in Sources */,
+ 58915D642A25F8B30066445B /* DeviceCheckOperation.swift in Sources */,
+ 58915D652A25F9E20066445B /* TunnelSettingsV2.swift in Sources */,
582A8A3A28BCE19B00D0F9FB /* FixedWidthIntegerArithmeticsTests.swift in Sources */,
+ 58915D632A25F8400066445B /* DeviceCheckOperationTests.swift in Sources */,
5896AE86246D6AD8005B36CB /* CustomDateComponentsFormattingTests.swift in Sources */,
58B8644629C7972F005E107C /* CustomDateComponentsFormatting.swift in Sources */,
5807E2C2243203D000F5FF30 /* StringTests.swift in Sources */,
+ 58165EBE2A262CBB00688EAD /* WgKeyRotationTests.swift in Sources */,
5807E2C3243203E700F5FF30 /* String+Split.swift in Sources */,
+ 580810E92A30E17300B74552 /* DeviceCheckRemoteServiceProtocol.swift in Sources */,
58B0A2A8238EE68200BC001D /* RelaySelectorTests.swift in Sources */,
);
runOnlyForDeploymentPostprocessing = 0;
@@ -2929,6 +3032,7 @@
58677710290975E9006F721F /* SettingsInteractorFactory.swift in Sources */,
58B26E282943527300D5980C /* SystemNotificationProvider.swift in Sources */,
58CCA01222424D11004F3011 /* SettingsViewController.swift in Sources */,
+ 581DA2752A1E283E0046ED47 /* WgKeyRotation.swift in Sources */,
06410DFE292CE18F00AFC18C /* KeychainSettingsStore.swift in Sources */,
58FB865526E8BF3100F188BC /* StorePaymentManagerError.swift in Sources */,
58FD5BF42428C67600112C88 /* InAppPurchaseButton.swift in Sources */,
@@ -2937,7 +3041,6 @@
5878F50429CDC547003D4BE2 /* ChangeLogContentView.swift in Sources */,
58B9EB152489139B00095626 /* RESTError+Display.swift in Sources */,
587B753F2668E5A700DEF7E9 /* NotificationContainerView.swift in Sources */,
- 58421034282E4B1500F24E46 /* TunnelSettingsV2+REST.swift in Sources */,
58F2E144276A13F300A79513 /* StartTunnelOperation.swift in Sources */,
58CCA01E2242787B004F3011 /* AccountTextField.swift in Sources */,
586E54FB27A2DF6D0029B88B /* SendTunnelProviderMessageOperation.swift in Sources */,
@@ -2976,10 +3079,14 @@
buildActionMask = 2147483647;
files = (
5806767C27048E9B00C858CB /* PacketTunnelProvider.swift in Sources */,
+ 581DA2762A1E2FD10046ED47 /* WgKeyRotation.swift in Sources */,
587AD7C723421D8600E93A53 /* TunnelSettingsV1.swift in Sources */,
5893C6FA29C1B481009090D1 /* DNSSettings.swift in Sources */,
+ 580810E52A30E13A00B74552 /* DeviceStateAccessorProtocol.swift in Sources */,
58CE38C828992C9200A6D6E5 /* TunnelMonitorDelegate.swift in Sources */,
+ 580810E82A30E15500B74552 /* DeviceCheckRemoteServiceProtocol.swift in Sources */,
068CE5782927BE4800A068BB /* Migration.swift in Sources */,
+ 58915D682A25FA080066445B /* DeviceCheckRemoteService.swift in Sources */,
58FC040A27B3EE03001C21F0 /* TunnelMonitor.swift in Sources */,
5838318B27C40A3900000571 /* Pinger.swift in Sources */,
06410E05292D0FC000AFC18C /* SettingsParser.swift in Sources */,
@@ -2987,6 +3094,7 @@
58E0729F28814ACC008902F8 /* WireGuardLogLevel+Logging.swift in Sources */,
580F8B8428197884002E0998 /* TunnelSettingsV2.swift in Sources */,
06410E08292D117800AFC18C /* SettingsStore.swift in Sources */,
+ 583D86482A2678DC0060D63B /* DeviceStateAccessor.swift in Sources */,
58906DE02445C7A5002F0673 /* NEProviderStopReason+Debug.swift in Sources */,
068CE57229278F6D00A068BB /* MigrationFromV1ToV2.swift in Sources */,
06410DFF292CF16C00AFC18C /* KeychainSettingsStore.swift in Sources */,
@@ -2997,6 +3105,7 @@
5877D70F282137E8002FCFC7 /* SettingsManager.swift in Sources */,
58CE38C728992C8700A6D6E5 /* WireGuardAdapterError+Localization.swift in Sources */,
58E511E828DDDF2400B0BCDE /* CodingErrors+CustomErrorDescription.swift in Sources */,
+ 58FDF2D92A0BA11A00C2B061 /* DeviceCheckOperation.swift in Sources */,
);
runOnlyForDeploymentPostprocessing = 0;
};
@@ -3057,6 +3166,7 @@
58D22412294C90210029F5F8 /* RelayConstraint.swift in Sources */,
58D22413294C90210029F5F8 /* RelayConstraints.swift in Sources */,
58D22414294C90210029F5F8 /* RelayLocation.swift in Sources */,
+ 581DA2732A1E227D0046ED47 /* RESTTypes.swift in Sources */,
58D22415294C90210029F5F8 /* PacketTunnelStatus.swift in Sources */,
58D22416294C90210029F5F8 /* PacketTunnelRelay.swift in Sources */,
58D22417294C90210029F5F8 /* FixedWidthInteger+Arithmetics.swift in Sources */,
@@ -3148,6 +3258,20 @@
target = 06799ABB28F98E1D00ACD94E /* MullvadREST */;
targetProxy = 58153073294CBE8B00D1702E /* PBXContainerItemProxy */;
};
+ 586A0DCE2A20E359006C731C /* PBXTargetDependency */ = {
+ isa = PBXTargetDependency;
+ target = 58D223D4294C8E5E0029F5F8 /* MullvadTypes */;
+ targetProxy = 586A0DCD2A20E359006C731C /* PBXContainerItemProxy */;
+ };
+ 586A0DD72A20E4A9006C731C /* PBXTargetDependency */ = {
+ isa = PBXTargetDependency;
+ target = 06799ABB28F98E1D00ACD94E /* MullvadREST */;
+ targetProxy = 586A0DD62A20E4A9006C731C /* PBXContainerItemProxy */;
+ };
+ 58915D6C2A2603700066445B /* PBXTargetDependency */ = {
+ isa = PBXTargetDependency;
+ productRef = 58915D6B2A2603700066445B /* WireGuardKitTypes */;
+ };
58CE5E80224146470008646E /* PBXTargetDependency */ = {
isa = PBXTargetDependency;
target = 58CE5E78224146470008646E /* PacketTunnel */;
@@ -4287,40 +4411,30 @@
version = 1.4.0;
};
};
- 58BA79192578F092006FAEA0 /* XCRemoteSwiftPackageReference "wireguard-apple" */ = {
+ 58F097482A20C30000DA2DAD /* XCRemoteSwiftPackageReference "wireguard-apple" */ = {
isa = XCRemoteSwiftPackageReference;
repositoryURL = "https://github.com/mullvad/wireguard-apple.git";
requirement = {
kind = revision;
- revision = 6baeac49a14313a7b8b7a956f67f4a47a6ae7a41;
+ revision = 11a00c20dc03f2751db47e94f585c0778c7bde82;
};
};
/* End XCRemoteSwiftPackageReference section */
/* Begin XCSwiftPackageProductDependency section */
- 062B45AD28FD503000746E77 /* WireGuardKit */ = {
- isa = XCSwiftPackageProductDependency;
- package = 58BA79192578F092006FAEA0 /* XCRemoteSwiftPackageReference "wireguard-apple" */;
- productName = WireGuardKit;
- };
- 063F02892902B7B2001FA09F /* WireGuardKitTypes */ = {
- isa = XCSwiftPackageProductDependency;
- package = 58BA79192578F092006FAEA0 /* XCRemoteSwiftPackageReference "wireguard-apple" */;
- productName = WireGuardKitTypes;
- };
- 06D9844B28F990AB003AABE9 /* WireGuardKitTypes */ = {
+ 586A0DD02A20E371006C731C /* WireGuardKitTypes */ = {
isa = XCSwiftPackageProductDependency;
- package = 58BA79192578F092006FAEA0 /* XCRemoteSwiftPackageReference "wireguard-apple" */;
+ package = 58F097482A20C30000DA2DAD /* XCRemoteSwiftPackageReference "wireguard-apple" */;
productName = WireGuardKitTypes;
};
- 5807483A27DB8A980020ECBF /* WireGuardKitTypes */ = {
+ 58915D6B2A2603700066445B /* WireGuardKitTypes */ = {
isa = XCSwiftPackageProductDependency;
- package = 58BA79192578F092006FAEA0 /* XCRemoteSwiftPackageReference "wireguard-apple" */;
+ package = 58F097482A20C30000DA2DAD /* XCRemoteSwiftPackageReference "wireguard-apple" */;
productName = WireGuardKitTypes;
};
- 583E1E2B2848E1A1004838B3 /* WireGuardKitTypes */ = {
+ 58915D6D2A26037A0066445B /* WireGuardKitTypes */ = {
isa = XCSwiftPackageProductDependency;
- package = 58BA79192578F092006FAEA0 /* XCRemoteSwiftPackageReference "wireguard-apple" */;
+ package = 58F097482A20C30000DA2DAD /* XCRemoteSwiftPackageReference "wireguard-apple" */;
productName = WireGuardKitTypes;
};
58D22419294C90380029F5F8 /* Logging */ = {
@@ -4333,6 +4447,21 @@
package = 585834F624D2BC1F00A8AF56 /* XCRemoteSwiftPackageReference "swift-log" */;
productName = Logging;
};
+ 58F0974D2A20C31100DA2DAD /* WireGuardKitTypes */ = {
+ isa = XCSwiftPackageProductDependency;
+ package = 58F097482A20C30000DA2DAD /* XCRemoteSwiftPackageReference "wireguard-apple" */;
+ productName = WireGuardKitTypes;
+ };
+ 58F097502A20C35000DA2DAD /* WireGuardKitTypes */ = {
+ isa = XCSwiftPackageProductDependency;
+ package = 58F097482A20C30000DA2DAD /* XCRemoteSwiftPackageReference "wireguard-apple" */;
+ productName = WireGuardKitTypes;
+ };
+ 58F097532A20C36000DA2DAD /* WireGuardKit */ = {
+ isa = XCSwiftPackageProductDependency;
+ package = 58F097482A20C30000DA2DAD /* XCRemoteSwiftPackageReference "wireguard-apple" */;
+ productName = WireGuardKit;
+ };
/* End XCSwiftPackageProductDependency section */
};
rootObject = 58CE5E58224146200008646E /* Project object */;
diff --git a/ios/MullvadVPN.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved b/ios/MullvadVPN.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved
index 3b4fa835a2..02691892fe 100644
--- a/ios/MullvadVPN.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved
+++ b/ios/MullvadVPN.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved
@@ -14,7 +14,7 @@
"kind" : "remoteSourceControl",
"location" : "https://github.com/mullvad/wireguard-apple.git",
"state" : {
- "revision" : "6baeac49a14313a7b8b7a956f67f4a47a6ae7a41"
+ "revision" : "11a00c20dc03f2751db47e94f585c0778c7bde82"
}
}
],
diff --git a/ios/MullvadVPN/AppDelegate.swift b/ios/MullvadVPN/AppDelegate.swift
index 43a3483309..6090ccdd82 100644
--- a/ios/MullvadVPN/AppDelegate.swift
+++ b/ios/MullvadVPN/AppDelegate.swift
@@ -179,16 +179,16 @@ class AppDelegate: UIResponder, UIApplicationDelegate, UNUserNotificationCenterD
let isRegistered = BGTaskScheduler.shared.register(
forTaskWithIdentifier: ApplicationConfiguration.appRefreshTaskIdentifier,
using: nil
- ) { task in
- let handle = self.relayCacheTracker.updateRelays { completion in
- task.setTaskCompleted(success: completion.isSuccess)
+ ) { [self] task in
+ let handle = relayCacheTracker.updateRelays { result in
+ task.setTaskCompleted(success: result.isSuccess)
}
task.expirationHandler = {
handle.cancel()
}
- self.scheduleAppRefreshTask()
+ scheduleAppRefreshTask()
}
if isRegistered {
@@ -202,11 +202,11 @@ class AppDelegate: UIResponder, UIApplicationDelegate, UNUserNotificationCenterD
let isRegistered = BGTaskScheduler.shared.register(
forTaskWithIdentifier: ApplicationConfiguration.privateKeyRotationTaskIdentifier,
using: nil
- ) { task in
- let handle = self.tunnelManager.rotatePrivateKey { completion in
- self.scheduleKeyRotationTask()
+ ) { [self] task in
+ let handle = tunnelManager.rotatePrivateKey { [self] error in
+ scheduleKeyRotationTask()
- task.setTaskCompleted(success: completion.isSuccess)
+ task.setTaskCompleted(success: error == nil)
}
task.expirationHandler = {
@@ -225,9 +225,9 @@ class AppDelegate: UIResponder, UIApplicationDelegate, UNUserNotificationCenterD
let isRegistered = BGTaskScheduler.shared.register(
forTaskWithIdentifier: ApplicationConfiguration.addressCacheUpdateTaskIdentifier,
using: nil
- ) { task in
- let handle = self.addressCacheTracker.updateEndpoints { result in
- self.scheduleAddressCacheUpdateTask()
+ ) { [self] task in
+ let handle = addressCacheTracker.updateEndpoints { [self] result in
+ scheduleAddressCacheUpdateTask()
task.setTaskCompleted(success: result.isSuccess)
}
diff --git a/ios/MullvadVPN/Notifications/Notification Providers/TunnelStatusNotificationProvider.swift b/ios/MullvadVPN/Notifications/Notification Providers/TunnelStatusNotificationProvider.swift
index b961e1e134..9378e264bb 100644
--- a/ios/MullvadVPN/Notifications/Notification Providers/TunnelStatusNotificationProvider.swift
+++ b/ios/MullvadVPN/Notifications/Notification Providers/TunnelStatusNotificationProvider.swift
@@ -116,9 +116,7 @@ final class TunnelStatusNotificationProvider: NotificationProvider, InAppNotific
return false
}
- private func notificationDescription(for packetTunnelError: String)
- -> InAppNotificationDescriptor
- {
+ private func notificationDescription(for packetTunnelError: String) -> InAppNotificationDescriptor {
return InAppNotificationDescriptor(
identifier: identifier,
style: .error,
diff --git a/ios/MullvadVPN/SettingsManager/Migrations/MigrationFromV1ToV2.swift b/ios/MullvadVPN/SettingsManager/Migrations/MigrationFromV1ToV2.swift
index c7df558a7a..9404febc9a 100644
--- a/ios/MullvadVPN/SettingsManager/Migrations/MigrationFromV1ToV2.swift
+++ b/ios/MullvadVPN/SettingsManager/Migrations/MigrationFromV1ToV2.swift
@@ -19,10 +19,8 @@ final class MigrationFromV1ToV2: Migration {
private var accountTask: Cancellable?
private var deviceTask: Cancellable?
- private var accountCompletion: Result<REST.AccountData, Error> = .failure(
- OperationError.cancelled
- )
- private var devicesCompletion: Result<[REST.Device], Error> = .failure(OperationError.cancelled)
+ private var accountCompletion: Result<Account, Error> = .failure(OperationError.cancelled)
+ private var devicesCompletion: Result<[Device], Error> = .failure(OperationError.cancelled)
private let legacySettings: LegacyTunnelSettings
@@ -98,8 +96,8 @@ final class MigrationFromV1ToV2: Migration {
store: SettingsStore,
parser: SettingsParser,
settings: LegacyTunnelSettings,
- accountData: REST.AccountData,
- devices: [REST.Device]
+ accountData: Account,
+ devices: [Device]
) throws {
let tunnelSettings = settings.tunnelSettings
let interfaceData = settings.tunnelSettings.interface
diff --git a/ios/MullvadVPN/SettingsManager/TunnelSettingsV2+REST.swift b/ios/MullvadVPN/SettingsManager/TunnelSettingsV2+REST.swift
deleted file mode 100644
index 7cbe953ba9..0000000000
--- a/ios/MullvadVPN/SettingsManager/TunnelSettingsV2+REST.swift
+++ /dev/null
@@ -1,22 +0,0 @@
-//
-// TunnelSettingsV2+REST.swift
-// MullvadVPN
-//
-// Created by pronebird on 13/05/2022.
-// Copyright © 2022 Mullvad VPN AB. All rights reserved.
-//
-
-import Foundation
-import MullvadREST
-import class WireGuardKitTypes.PrivateKey
-
-extension StoredDeviceData {
- mutating func update(from device: REST.Device) {
- identifier = device.id
- name = device.name
- creationDate = device.created
- hijackDNS = device.hijackDNS
- ipv4Address = device.ipv4Address
- ipv6Address = device.ipv6Address
- }
-}
diff --git a/ios/MullvadVPN/SettingsManager/TunnelSettingsV2.swift b/ios/MullvadVPN/SettingsManager/TunnelSettingsV2.swift
index 7ee39c6934..a86c1ee199 100644
--- a/ios/MullvadVPN/SettingsManager/TunnelSettingsV2.swift
+++ b/ios/MullvadVPN/SettingsManager/TunnelSettingsV2.swift
@@ -85,19 +85,6 @@ enum DeviceState: Codable, Equatable {
return nil
}
}
-
- /**
- Mutates account and device data when in logged in state, otherwise does nothing.
- */
- mutating func updateData(_ body: (inout StoredAccountData, inout StoredDeviceData) -> Void) {
- switch self {
- case var .loggedIn(accountData, deviceData):
- body(&accountData, &deviceData)
- self = .loggedIn(accountData, deviceData)
- case .revoked, .loggedOut:
- break
- }
- }
}
struct StoredDeviceData: Codable, Equatable {
@@ -113,17 +100,28 @@ struct StoredDeviceData: Codable, Equatable {
/// Whether relay hijacks DNS from this device.
var hijackDNS: Bool
- /// IPv4 address assigned to device.
+ /// IPv4 address + mask assigned to device.
var ipv4Address: IPAddressRange
- /// IPv6 address assignged to device.
+ /// IPv6 address + mask assigned to device.
var ipv6Address: IPAddressRange
/// WireGuard key data.
var wgKeyData: StoredWgKeyData
+ /// Returns capitalized device name.
var capitalizedName: String {
- name.capitalized
+ return name.capitalized
+ }
+
+ /// Fill in part of the structure that contains device related properties from `Device` struct.
+ mutating func update(from device: Device) {
+ identifier = device.id
+ name = device.name
+ creationDate = device.created
+ hijackDNS = device.hijackDNS
+ ipv4Address = device.ipv4Address
+ ipv6Address = device.ipv6Address
}
}
@@ -141,18 +139,3 @@ struct StoredWgKeyData: Codable, Equatable {
/// Added in 2023.3
var nextPrivateKey: PrivateKey?
}
-
-extension StoredWgKeyData {
- struct KeyRotationConfiguration {
- let rotationInterval: TimeInterval = 60 * 60 * 24 * 14
- let retryInterval: TimeInterval = 60 * 60 * 24
- }
-
- func getNextRotationDate(for configuration: KeyRotationConfiguration) -> Date {
- return max(
- Date(),
- lastRotationAttemptDate?.addingTimeInterval(configuration.retryInterval) ?? creationDate
- .addingTimeInterval(configuration.rotationInterval)
- )
- }
-}
diff --git a/ios/MullvadVPN/SimulatorTunnelProvider/SimulatorTunnelProvider.swift b/ios/MullvadVPN/SimulatorTunnelProvider/SimulatorTunnelProvider.swift
index bf30237905..cf0ee46968 100644
--- a/ios/MullvadVPN/SimulatorTunnelProvider/SimulatorTunnelProvider.swift
+++ b/ios/MullvadVPN/SimulatorTunnelProvider/SimulatorTunnelProvider.swift
@@ -66,17 +66,11 @@ class SimulatorTunnelProviderDelegate {
}
}
- func startTunnel(
- options: [String: NSObject]?,
- completionHandler: @escaping (Error?) -> Void
- ) {
+ func startTunnel(options: [String: NSObject]?, completionHandler: @escaping (Error?) -> Void) {
completionHandler(nil)
}
- func stopTunnel(
- with reason: NEProviderStopReason,
- completionHandler: @escaping () -> Void
- ) {
+ func stopTunnel(with reason: NEProviderStopReason, completionHandler: @escaping () -> Void) {
completionHandler()
}
@@ -107,10 +101,7 @@ final class SimulatorTunnelProvider {
private init() {}
- fileprivate func handleAppMessage(
- _ messageData: Data,
- completionHandler: ((Data?) -> Void)? = nil
- ) {
+ fileprivate func handleAppMessage(_ messageData: Data, completionHandler: ((Data?) -> Void)? = nil) {
delegate.handleAppMessage(messageData, completionHandler: completionHandler)
}
}
@@ -223,9 +214,7 @@ class SimulatorVPNConnection: NSObject, VPNConnectionProtocol {
// MARK: - NETunnelProviderSession stubs
-final class SimulatorTunnelProviderSession: SimulatorVPNConnection,
- VPNTunnelProviderSessionProtocol
-{
+final class SimulatorTunnelProviderSession: SimulatorVPNConnection, VPNTunnelProviderSessionProtocol {
func sendProviderMessage(_ messageData: Data, responseHandler: ((Data?) -> Void)?) throws {
SimulatorTunnelProvider.shared.handleAppMessage(
messageData,
@@ -428,10 +417,7 @@ final class SimulatorTunnelProviderManager: NSObject, VPNTunnelProviderManagerPr
completionHandler?(error)
}
- static func == (
- lhs: SimulatorTunnelProviderManager,
- rhs: SimulatorTunnelProviderManager
- ) -> Bool {
+ static func == (lhs: SimulatorTunnelProviderManager, rhs: SimulatorTunnelProviderManager) -> Bool {
lhs.identifier == rhs.identifier
}
}
diff --git a/ios/MullvadVPN/SimulatorTunnelProvider/SimulatorTunnelProviderHost.swift b/ios/MullvadVPN/SimulatorTunnelProvider/SimulatorTunnelProviderHost.swift
index 304d9be8e2..bc2772ee83 100644
--- a/ios/MullvadVPN/SimulatorTunnelProvider/SimulatorTunnelProviderHost.swift
+++ b/ios/MullvadVPN/SimulatorTunnelProvider/SimulatorTunnelProviderHost.swift
@@ -76,10 +76,7 @@ final class SimulatorTunnelProviderHost: SimulatorTunnelProviderDelegate {
}
}
- override func stopTunnel(
- with reason: NEProviderStopReason,
- completionHandler: @escaping () -> Void
- ) {
+ override func stopTunnel(with reason: NEProviderStopReason, completionHandler: @escaping () -> Void) {
dispatchQueue.async {
self.selectorResult = nil
@@ -104,10 +101,7 @@ final class SimulatorTunnelProviderHost: SimulatorTunnelProviderDelegate {
}
}
- private func handleProviderMessage(
- _ message: TunnelProviderMessage,
- completionHandler: ((Data?) -> Void)?
- ) {
+ private func handleProviderMessage(_ message: TunnelProviderMessage, completionHandler: ((Data?) -> Void)?) {
switch message {
case .getTunnelStatus:
var tunnelStatus = PacketTunnelStatus()
diff --git a/ios/MullvadVPN/StorePaymentManager/StorePaymentManager.swift b/ios/MullvadVPN/StorePaymentManager/StorePaymentManager.swift
index 22362df97d..2ec6331fe8 100644
--- a/ios/MullvadVPN/StorePaymentManager/StorePaymentManager.swift
+++ b/ios/MullvadVPN/StorePaymentManager/StorePaymentManager.swift
@@ -181,7 +181,7 @@ final class StorePaymentManager: NSObject, SKPaymentTransactionObserver {
accountNumber: String,
completionHandler: @escaping (StorePaymentManagerError?) -> Void
) {
- let accountOperation = ResultBlockOperation<REST.AccountData>(dispatchQueue: .main) { finish in
+ let accountOperation = ResultBlockOperation<Account>(dispatchQueue: .main) { finish in
return self.accountsProxy.getAccountData(
accountNumber: accountNumber,
retryStrategy: .default,
diff --git a/ios/MullvadVPN/TunnelManager/RotateKeyOperation.swift b/ios/MullvadVPN/TunnelManager/RotateKeyOperation.swift
index 555c3d9ea0..5f3c448160 100644
--- a/ios/MullvadVPN/TunnelManager/RotateKeyOperation.swift
+++ b/ios/MullvadVPN/TunnelManager/RotateKeyOperation.swift
@@ -13,70 +13,60 @@ import MullvadTypes
import Operations
import class WireGuardKitTypes.PrivateKey
-class RotateKeyOperation: ResultOperation<Bool> {
+class RotateKeyOperation: ResultOperation<Void> {
+ private let logger = Logger(label: "RotateKeyOperation")
private let interactor: TunnelInteractor
-
private let devicesProxy: REST.DevicesProxy
private var task: Cancellable?
- private let keyRotationConfiguration: StoredWgKeyData.KeyRotationConfiguration
- private let logger = Logger(label: "ReplaceKeyOperation")
-
- init(
- dispatchQueue: DispatchQueue,
- interactor: TunnelInteractor,
- devicesProxy: REST.DevicesProxy,
- keyRotationConfiguration: StoredWgKeyData.KeyRotationConfiguration
- ) {
+ init(dispatchQueue: DispatchQueue, interactor: TunnelInteractor, devicesProxy: REST.DevicesProxy) {
self.interactor = interactor
self.devicesProxy = devicesProxy
- self.keyRotationConfiguration = keyRotationConfiguration
super.init(dispatchQueue: dispatchQueue, completionQueue: nil, completionHandler: nil)
}
override func main() {
- guard case .loggedIn(let accountData, var deviceData) = interactor.deviceState else {
+ // Extract login metadata.
+ guard case let .loggedIn(accountData, deviceData) = interactor.deviceState else {
finish(result: .failure(InvalidDeviceStateError()))
return
}
- let nextRotationDate = deviceData.wgKeyData.getNextRotationDate(for: keyRotationConfiguration)
- if nextRotationDate > Date() {
- logger.debug("Throttle private key rotation.")
+ // Create key rotation.
+ var keyRotation = WgKeyRotation(data: deviceData)
- finish(result: .success(false))
+ // Check if key rotation can take place.
+ guard keyRotation.shouldRotate else {
+ logger.debug("Throttle private key rotation.")
+ finish(result: .success(()))
return
- } else {
- logger.debug("Private key is old enough, rotate right away.")
}
- deviceData.wgKeyData.lastRotationAttemptDate = Date()
- interactor.setDeviceState(.loggedIn(accountData, deviceData), persist: true)
+ logger.debug("Private key is old enough, rotate right away.")
- logger.debug("Replacing old key with new key on server...")
+ // Mark the beginning of key rotation and receive the public key to push to backend.
+ let publicKey = keyRotation.beginAttempt()
- let newPrivateKey: PrivateKey
- if let nextPrivateKey = deviceData.wgKeyData.nextPrivateKey {
- logger.debug("Next private key is already stored in Keychain. Using it.")
+ // Persist mutated device data.
+ interactor.setDeviceState(.loggedIn(accountData, keyRotation.data), persist: true)
- newPrivateKey = nextPrivateKey
- } else {
- logger.debug("Create next private key and store it in Keychain.")
-
- newPrivateKey = PrivateKey()
- deviceData.wgKeyData.nextPrivateKey = newPrivateKey
- interactor.setDeviceState(.loggedIn(accountData, deviceData), persist: true)
- }
+ // Send REST request to rotate the device key.
+ logger.debug("Replacing old key with new key on server...")
task = devicesProxy.rotateDeviceKey(
accountNumber: accountData.number,
identifier: deviceData.identifier,
- publicKey: newPrivateKey.publicKey,
+ publicKey: publicKey,
retryStrategy: .default
- ) { result in
- self.dispatchQueue.async {
- self.didRotateKey(newPrivateKey: newPrivateKey, result: result)
+ ) { [self] result in
+ dispatchQueue.async { [self] in
+ switch result {
+ case let .success(device):
+ handleSuccess(accountData: accountData, fetchedDevice: device, keyRotation: keyRotation)
+ case let .failure(error):
+ handleError(error)
+ }
}
}
}
@@ -86,53 +76,33 @@ class RotateKeyOperation: ResultOperation<Bool> {
task = nil
}
- private func didRotateKey(newPrivateKey: PrivateKey, result: Result<REST.Device, Error>) {
- switch result {
- case let .success(device):
- logger.debug("Successfully rotated device key. Persisting settings...")
+ private func handleSuccess(accountData: StoredAccountData, fetchedDevice: Device, keyRotation: WgKeyRotation) {
+ logger.debug("Successfully rotated device key. Persisting device state...")
- switch interactor.deviceState {
- case .loggedIn(let accountData, var deviceData):
- deviceData.update(from: device)
+ var keyRotation = keyRotation
- deviceData.wgKeyData = StoredWgKeyData(
- creationDate: Date(),
- lastRotationAttemptDate: nil,
- privateKey: newPrivateKey
- )
+ // Mark key rotation completed.
+ _ = keyRotation.setCompleted(with: fetchedDevice)
- interactor.setDeviceState(.loggedIn(accountData, deviceData), persist: true)
+ // Persist changes.
+ interactor.setDeviceState(.loggedIn(accountData, keyRotation.data), persist: true)
- if let tunnel = interactor.tunnel {
- _ = tunnel.notifyKeyRotation { [weak self] _ in
- self?.finish(result: .success(true))
- }
- } else {
- finish(result: .success(true))
- }
- default:
- finish(result: .failure(InvalidDeviceStateError()))
- }
-
- case let .failure(error):
- if !error.isOperationCancellationError {
- logger.error(
- error: error,
- message: "Failed to rotate device key."
- )
- }
-
- switch interactor.deviceState {
- case .loggedIn(let accountData, var deviceData):
- deviceData.wgKeyData.lastRotationAttemptDate = Date()
- interactor.setDeviceState(.loggedIn(accountData, deviceData), persist: true)
-
- default:
- finish(result: .failure(InvalidDeviceStateError()))
+ // Notify the tunnel that key rotation took place and that it should reload VPN configuration.
+ if let tunnel = interactor.tunnel {
+ _ = tunnel.notifyKeyRotation { [weak self] _ in
+ self?.finish(result: .success(()))
}
+ } else {
+ finish(result: .success(()))
+ }
+ }
- interactor.handleRestError(error)
- finish(result: .failure(error))
+ private func handleError(_ error: Error) {
+ if !error.isOperationCancellationError {
+ logger.error(error: error, message: "Failed to rotate device key.")
}
+
+ interactor.handleRestError(error)
+ finish(result: .failure(error))
}
}
diff --git a/ios/MullvadVPN/TunnelManager/SetAccountOperation.swift b/ios/MullvadVPN/TunnelManager/SetAccountOperation.swift
index 4107001d35..1b85516871 100644
--- a/ios/MullvadVPN/TunnelManager/SetAccountOperation.swift
+++ b/ios/MullvadVPN/TunnelManager/SetAccountOperation.swift
@@ -39,13 +39,13 @@ enum SetAccountAction {
private struct SetAccountResult {
let accountData: StoredAccountData
let privateKey: PrivateKey
- let device: REST.Device
+ let device: Device
}
private struct SetAccountContext: OperationInputContext {
var accountData: StoredAccountData?
var privateKey: PrivateKey?
- var device: REST.Device?
+ var device: Device?
func reduce() -> SetAccountResult? {
guard let accountData,
@@ -309,11 +309,11 @@ class SetAccountOperation: ResultOperation<StoredAccountData?> {
}
}
- private func getCreateDeviceOperation() -> TransformOperation<StoredAccountData, (PrivateKey, REST.Device)> {
- return TransformOperation<StoredAccountData, (
- PrivateKey,
- REST.Device
- )>(dispatchQueue: dispatchQueue) { storedAccountData, finish -> Cancellable in
+ private func getCreateDeviceOperation() -> TransformOperation<StoredAccountData, (PrivateKey, Device)> {
+ return TransformOperation<
+ StoredAccountData,
+ (PrivateKey, Device)
+ >(dispatchQueue: dispatchQueue) { storedAccountData, finish -> Cancellable in
self.logger.debug("Store last used account.")
do {
diff --git a/ios/MullvadVPN/TunnelManager/TunnelInteractor.swift b/ios/MullvadVPN/TunnelManager/TunnelInteractor.swift
index e22e33cf16..5a387b369f 100644
--- a/ios/MullvadVPN/TunnelManager/TunnelInteractor.swift
+++ b/ios/MullvadVPN/TunnelManager/TunnelInteractor.swift
@@ -22,8 +22,7 @@ protocol TunnelInteractor {
// MARK: - Tunnel status
var tunnelStatus: TunnelStatus { get }
- @discardableResult func updateTunnelStatus(_ block: (inout TunnelStatus) -> Void)
- -> TunnelStatus
+ @discardableResult func updateTunnelStatus(_ block: (inout TunnelStatus) -> Void) -> TunnelStatus
// MARK: - Configuration
diff --git a/ios/MullvadVPN/TunnelManager/TunnelManager.swift b/ios/MullvadVPN/TunnelManager/TunnelManager.swift
index 7f7b215eb3..3cbe936a41 100644
--- a/ios/MullvadVPN/TunnelManager/TunnelManager.swift
+++ b/ios/MullvadVPN/TunnelManager/TunnelManager.swift
@@ -72,8 +72,8 @@ final class TunnelManager: StorePaymentObserver {
private var _tunnel: Tunnel?
private var _tunnelStatus = TunnelStatus()
- /// Last processed device check identifier.
- private var lastDeviceCheckIdentifier: UUID?
+ /// Last processed device check.
+ private var lastDeviceCheck: DeviceCheck?
// MARK: - Initialization
@@ -130,7 +130,7 @@ final class TunnelManager: StorePaymentObserver {
nslock.lock()
defer { nslock.unlock() }
- return deviceState.deviceData?.wgKeyData.getNextRotationDate(for: StoredWgKeyData.KeyRotationConfiguration())
+ return deviceState.deviceData.flatMap { WgKeyRotation(data: $0).nextRotationDate }
}
private func updatePrivateKeyRotationTimer() {
@@ -427,31 +427,25 @@ final class TunnelManager: StorePaymentObserver {
operationQueue.addOperation(operation)
}
- func rotatePrivateKey(completionHandler: @escaping (Result<Bool, Error>) -> Void) -> Cancellable {
+ func rotatePrivateKey(completionHandler: @escaping (Error?) -> Void) -> Cancellable {
let operation = RotateKeyOperation(
dispatchQueue: internalQueue,
interactor: TunnelInteractorProxy(self),
- devicesProxy: devicesProxy,
- keyRotationConfiguration: StoredWgKeyData.KeyRotationConfiguration()
+ devicesProxy: devicesProxy
)
operation.completionQueue = .main
operation.completionHandler = { [weak self] result in
guard let self else { return }
- self.updatePrivateKeyRotationTimer()
-
- switch result {
- case .success:
- completionHandler(result)
+ updatePrivateKeyRotationTimer()
- case let .failure(error):
- if !error.isOperationCancellationError {
- self.handleRestError(error)
- }
-
- completionHandler(result)
+ let error = result.error
+ if let error {
+ handleRestError(error)
}
+
+ completionHandler(error)
}
operation.addObserver(
@@ -624,30 +618,8 @@ final class TunnelManager: StorePaymentObserver {
_tunnelStatus = newTunnelStatus
- if let deviceCheck = newTunnelStatus.packetTunnelStatus.deviceCheck,
- deviceCheck.identifier != lastDeviceCheckIdentifier
- {
- if deviceCheck.isDeviceRevoked ?? false {
- scheduleDeviceStateUpdate(
- taskName: "Set device revoked",
- modificationBlock: { deviceState in
- deviceState = .revoked
- },
- completionHandler: nil
- )
- } else if let accountExpiry = deviceCheck.accountExpiry {
- scheduleDeviceStateUpdate(
- taskName: "Update account expiry",
- reconnectTunnel: false
- ) { deviceState in
- if case .loggedIn(var accountData, let deviceData) = deviceState {
- accountData.expiry = accountExpiry
- deviceState = .loggedIn(accountData, deviceData)
- }
- }
- }
-
- lastDeviceCheckIdentifier = deviceCheck.identifier
+ if let deviceCheck = newTunnelStatus.packetTunnelStatus.deviceCheck {
+ handleDeviceCheck(deviceCheck)
}
switch newTunnelStatus.state {
@@ -674,6 +646,54 @@ final class TunnelManager: StorePaymentObserver {
return newTunnelStatus
}
+ private func handleDeviceCheck(_ deviceCheck: DeviceCheck) {
+ // Bail immediately when last device check is identical.
+ guard lastDeviceCheck != deviceCheck else { return }
+
+ // Packet tunnel may have attempted or rotated the key.
+ // In that case we have to reload device state from Keychain as it's likely was modified by packet tunnel.
+ if lastDeviceCheck?.keyRotationStatus != deviceCheck.keyRotationStatus {
+ switch deviceCheck.keyRotationStatus {
+ case .attempted, .succeeded:
+ refreshDeviceState()
+ case .noAction:
+ break
+ }
+ }
+
+ // Packet tunnel detected that device is revoked.
+ if lastDeviceCheck?.deviceVerdict != deviceCheck.deviceVerdict, deviceCheck.deviceVerdict == .revoked {
+ scheduleDeviceStateUpdate(taskName: "Set device revoked", reconnectTunnel: false) { deviceState in
+ deviceState = .revoked
+ }
+ }
+
+ // Packet tunnel received new account expiry.
+ if lastDeviceCheck?.accountVerdict != deviceCheck.accountVerdict {
+ switch deviceCheck.accountVerdict {
+ case let .expired(accountData), let .active(accountData):
+ scheduleDeviceStateUpdate(taskName: "Update account expiry", reconnectTunnel: false) { deviceState in
+ guard case .loggedIn(var storedAccountData, let storedDeviceData) = deviceState else {
+ return
+ }
+
+ if storedAccountData.identifier == accountData.id {
+ storedAccountData.expiry = accountData.expiry
+ }
+
+ deviceState = .loggedIn(storedAccountData, storedDeviceData)
+ }
+
+ case .invalid:
+ // TODO: handle invalid account in some way?
+ break
+ }
+ }
+
+ // Save last device check.
+ lastDeviceCheck = deviceCheck
+ }
+
fileprivate func setSettings(_ settings: TunnelSettingsV2, persist: Bool) {
nslock.lock()
defer { nslock.unlock() }
@@ -739,9 +759,10 @@ final class TunnelManager: StorePaymentObserver {
@objc private func applicationDidBecomeActive(_ notification: Notification) {
#if DEBUG
- logger.debug("Refresh tunnel status due to application becoming active.")
+ logger.debug("Refresh device state and tunnel status due to application becoming active.")
#endif
refreshTunnelStatus()
+ refreshDeviceState()
}
private func didUpdateNetworkPath(_ path: Network.NWPath) {
@@ -846,6 +867,33 @@ final class TunnelManager: StorePaymentObserver {
}
}
+ /// Refresh device state from settings and update the in-memory value.
+ /// Used to refresh device state when it's modified by packet tunnel during key rotation.
+ private func refreshDeviceState() {
+ let operation = AsyncBlockOperation(dispatchQueue: internalQueue) {
+ do {
+ let newDeviceState = try SettingsManager.readDeviceState()
+
+ self.setDeviceState(newDeviceState, persist: false)
+ } catch {
+ if let error = error as? KeychainError, error == .itemNotFound {
+ return
+ }
+
+ self.logger.error(error: error, message: "Failed to refresh device state")
+ }
+ }
+
+ operation.addCondition(MutuallyExclusive(category: OperationCategory.deviceStateUpdate.category))
+ operation.addObserver(BackgroundObserver(
+ application: application,
+ name: "Refresh device state",
+ cancelUponExpiration: true
+ ))
+
+ operationQueue.addOperation(operation)
+ }
+
/// Update `TunnelStatus` from `NEVPNStatus`.
/// Collects the `PacketTunnelStatus` from the tunnel via IPC if needed before assigning
/// the `tunnelStatus`.
@@ -1064,11 +1112,11 @@ extension TunnelManager {
*/
func simulateAccountExpiration(option: AccountExpirySimulationOption) {
scheduleDeviceStateUpdate(taskName: "Simulating account expiry", reconnectTunnel: false) { deviceState in
- deviceState.updateData { accountData, deviceData in
- guard let date = option.date else { return }
+ guard case .loggedIn(var accountData, let deviceData) = deviceState, let date = option.date else { return }
- accountData.expiry = date
- }
+ accountData.expiry = date
+
+ deviceState = .loggedIn(accountData, deviceData)
}
}
}
diff --git a/ios/MullvadVPN/TunnelManager/UpdateAccountDataOperation.swift b/ios/MullvadVPN/TunnelManager/UpdateAccountDataOperation.swift
index 00531bc2e0..1f52991650 100644
--- a/ios/MullvadVPN/TunnelManager/UpdateAccountDataOperation.swift
+++ b/ios/MullvadVPN/TunnelManager/UpdateAccountDataOperation.swift
@@ -50,7 +50,7 @@ class UpdateAccountDataOperation: ResultOperation<Void> {
task = nil
}
- private func didReceiveAccountData(result: Result<REST.AccountData, Error>) {
+ private func didReceiveAccountData(result: Result<Account, Error>) {
let result = result.inspectError { error in
guard !error.isOperationCancellationError else { return }
diff --git a/ios/MullvadVPN/TunnelManager/UpdateDeviceDataOperation.swift b/ios/MullvadVPN/TunnelManager/UpdateDeviceDataOperation.swift
index c58e32eca9..6d3b33ffd3 100644
--- a/ios/MullvadVPN/TunnelManager/UpdateDeviceDataOperation.swift
+++ b/ios/MullvadVPN/TunnelManager/UpdateDeviceDataOperation.swift
@@ -53,7 +53,7 @@ class UpdateDeviceDataOperation: ResultOperation<StoredDeviceData> {
task = nil
}
- private func didReceiveDeviceResponse(result: Result<REST.Device, Error>) {
+ private func didReceiveDeviceResponse(result: Result<Device, Error>) {
let result = result.tryMap { device -> StoredDeviceData in
switch interactor.deviceState {
case .loggedIn(let storedAccount, var storedDevice):
diff --git a/ios/MullvadVPN/TunnelManager/WgKeyRotation.swift b/ios/MullvadVPN/TunnelManager/WgKeyRotation.swift
new file mode 100644
index 0000000000..e6191af64f
--- /dev/null
+++ b/ios/MullvadVPN/TunnelManager/WgKeyRotation.swift
@@ -0,0 +1,138 @@
+//
+// WgKeyRotation.swift
+// MullvadVPN
+//
+// Created by pronebird on 24/05/2023.
+// Copyright © 2023 Mullvad VPN AB. All rights reserved.
+//
+
+import Foundation
+import struct MullvadTypes.Device
+import class WireGuardKitTypes.PrivateKey
+import class WireGuardKitTypes.PublicKey
+
+/**
+ Implements manipulations related to marking the beginning and the completion of key rotation, private key creation and other tasks relevant to handling the state of
+ key rotation.
+ */
+struct WgKeyRotation {
+ /// Private key rotation interval measured in seconds and counted from the time when the key was successfully pushed
+ /// to the backend.
+ public static let rotationInterval: TimeInterval = 60 * 60 * 24 * 14
+
+ /// Private key rotation retry interval measured in seconds and counted from the time when the last rotation
+ /// attempt took place.
+ public static let retryInterval: TimeInterval = 60 * 60 * 24
+
+ /// Cooldown interval measured in seconds used to prevent packet tunnel from forcefully pushing the key to our
+ /// backend in the event of restart loop.
+ public static let packetTunnelCooldownInterval: TimeInterval = 15
+
+ /// Mutated device data value.
+ private(set) var data: StoredDeviceData
+
+ /// Initialize object with `StoredDeviceData` that the struct is going to manipulate.
+ init(data: StoredDeviceData) {
+ self.data = data
+ }
+
+ /**
+ Begin key rotation attempt by marking last rotation attempt and creating next private key if needed.
+ If the next private key was created during the preivous rotation attempt then continue using the same key.
+
+ Returns the public key that should be pushed to the backend.
+ */
+ mutating func beginAttempt() -> PublicKey {
+ // Mark the rotation attempt.
+ data.wgKeyData.lastRotationAttemptDate = Date()
+
+ // Fetch the next private key we're attempting to rotate to.
+ if let nextPrivateKey = data.wgKeyData.nextPrivateKey {
+ return nextPrivateKey.publicKey
+ } else {
+ // If not found then create a new one and store it.
+ let newKey = PrivateKey()
+ data.wgKeyData.nextPrivateKey = newKey
+ return newKey.publicKey
+ }
+ }
+
+ /**
+ Successfuly finish key rotation by swapping the current key with the next one, marking key creation date and
+ removing the date of last rotation attempt which indicates that the last rotation had succedeed and no new
+ rotation attempts were made.
+
+ Device related properties are refreshed from `Device` struct that the caller should have received from the API. This function does nothing if the next private
+ key is unset.
+
+ Returns `false` if next private key is unset. Otherwise `true`.
+ */
+ mutating func setCompleted(with updatedDevice: Device) -> Bool {
+ guard let nextKey = data.wgKeyData.nextPrivateKey else { return false }
+
+ // Update stored device data with properties from updated `Device` struct.
+ data.update(from: updatedDevice)
+
+ // Reset creation date so that next period key rotation could happen relative to this date.
+ data.wgKeyData.creationDate = Date()
+
+ // Swap old and new keys.
+ data.wgKeyData.privateKey = nextKey
+ data.wgKeyData.nextPrivateKey = nil
+
+ // Unset the date of last rotation attempt to mark the end of key rotation sequence.
+ data.wgKeyData.lastRotationAttemptDate = nil
+
+ return true
+ }
+
+ /**
+ Returns the date of next key rotation, as it normally occurs in the app process using the following rules:
+
+ 1. Returns the date relative to key creation date + 14 days, if last rotation attempt was successful.
+ 2. Returns the date relative to last rotation attempt date + 24 hours, if last rotation attempt was unsuccessful.
+
+ If the date produced is in the past then `Date()` is returned instead.
+ */
+ var nextRotationDate: Date {
+ let nextRotationDate = data.wgKeyData.lastRotationAttemptDate?.addingTimeInterval(Self.retryInterval)
+ ?? data.wgKeyData.creationDate.addingTimeInterval(Self.rotationInterval)
+
+ return max(nextRotationDate, Date())
+ }
+
+ /// Returns `true` if the app should rotate the private key.
+ var shouldRotate: Bool {
+ return nextRotationDate <= Date()
+ }
+
+ /**
+ Returns `true` if packet tunnel should perform key rotation.
+
+ During the startup packet tunnel rotates the key immediately if it detected that the key stored on server does not
+ match the key stored on device. In that case it passes `rotateImmediately = true`.
+
+ To dampen the effect of packet tunnel entering into a restart cycle and going on a key rotation rampage,
+ this function adds a 15 seconds cooldown interval to prevent it from pushing keys too often.
+
+ After performing the initial key rotation on startup, packet tunnel will keep a 24 hour interval between the
+ subsequent key rotation attempts.
+ */
+ func shouldRotateFromPacketTunnel(rotateImmediately: Bool) -> Bool {
+ guard let lastRotationAttemptDate = data.wgKeyData.lastRotationAttemptDate else { return true }
+
+ let now = Date()
+
+ // Add cooldown interval when requested to rotate the key immediately.
+ if rotateImmediately, lastRotationAttemptDate.distance(to: now) > Self.packetTunnelCooldownInterval {
+ return true
+ }
+
+ let nextRotationAttempt = max(now, lastRotationAttemptDate.addingTimeInterval(Self.retryInterval))
+ if nextRotationAttempt <= now {
+ return true
+ }
+
+ return false
+ }
+}
diff --git a/ios/MullvadVPN/View controllers/DeviceList/DeviceManagementInteractor.swift b/ios/MullvadVPN/View controllers/DeviceList/DeviceManagementInteractor.swift
index 7aa202a2cd..501bc2bc6e 100644
--- a/ios/MullvadVPN/View controllers/DeviceList/DeviceManagementInteractor.swift
+++ b/ios/MullvadVPN/View controllers/DeviceList/DeviceManagementInteractor.swift
@@ -21,10 +21,7 @@ class DeviceManagementInteractor {
}
@discardableResult
- func getDevices(
- _ completionHandler: @escaping (Result<[REST.Device], Error>)
- -> Void
- ) -> Cancellable {
+ func getDevices(_ completionHandler: @escaping (Result<[Device], Error>) -> Void) -> Cancellable {
return devicesProxy.getDevices(
accountNumber: accountNumber,
retryStrategy: .default,
@@ -33,10 +30,7 @@ class DeviceManagementInteractor {
}
@discardableResult
- func deleteDevice(
- _ identifier: String,
- completionHandler: @escaping (Result<Bool, Error>) -> Void
- ) -> Cancellable {
+ func deleteDevice(_ identifier: String, completionHandler: @escaping (Result<Bool, Error>) -> Void) -> Cancellable {
return devicesProxy.deleteDevice(
accountNumber: accountNumber,
identifier: identifier,
diff --git a/ios/MullvadVPN/View controllers/DeviceList/DeviceManagementViewController.swift b/ios/MullvadVPN/View controllers/DeviceList/DeviceManagementViewController.swift
index 4536aa1cf5..3a29d0fe34 100644
--- a/ios/MullvadVPN/View controllers/DeviceList/DeviceManagementViewController.swift
+++ b/ios/MullvadVPN/View controllers/DeviceList/DeviceManagementViewController.swift
@@ -8,6 +8,7 @@
import MullvadLogging
import MullvadREST
+import MullvadTypes
import Operations
import UIKit
@@ -100,7 +101,7 @@ class DeviceManagementViewController: UIViewController, RootContainment {
// MARK: - Private
- private func setDevices(_ devices: [REST.Device], animated: Bool) {
+ private func setDevices(_ devices: [Device], animated: Bool) {
let viewModels = devices.map { restDevice -> DeviceViewModel in
return DeviceViewModel(
id: restDevice.id,
diff --git a/ios/MullvadVPNTests/DeviceCheckOperationTests.swift b/ios/MullvadVPNTests/DeviceCheckOperationTests.swift
new file mode 100644
index 0000000000..c744323662
--- /dev/null
+++ b/ios/MullvadVPNTests/DeviceCheckOperationTests.swift
@@ -0,0 +1,573 @@
+//
+// DeviceCheckOperationTests.swift
+// MullvadVPNTests
+//
+// Created by pronebird on 30/05/2023.
+// Copyright © 2023 Mullvad VPN AB. All rights reserved.
+//
+
+import MullvadREST
+import MullvadTypes
+import Operations
+import WireGuardKitTypes
+import XCTest
+
+class DeviceCheckOperationTests: XCTestCase {
+ private let operationQueue = AsyncOperationQueue()
+ private let dispatchQueue = DispatchQueue(label: "TestQueue")
+
+ func testShouldReportExpiredAccount() {
+ let expect = expectation(description: "Wait for operation to complete")
+
+ let currentKey = PrivateKey()
+ let remoteService = MockRemoteService(
+ initialKey: currentKey.publicKey,
+ getAccount: { accountNumber in
+ return Account.mock(expiry: .distantPast)
+ }
+ )
+ let deviceStateAccessor = MockDeviceStateAccessor.mockLoggedIn(
+ currentKey: currentKey,
+ rotationState: .succeeded
+ )
+
+ startDeviceCheck(remoteService: remoteService, deviceStateAccessor: deviceStateAccessor) { result in
+ let deviceCheck = result.value
+
+ XCTAssertNotNil(deviceCheck)
+ XCTAssertTrue(deviceCheck?.accountVerdict.isExpired ?? false)
+ XCTAssert(deviceCheck?.keyRotationStatus == .noAction)
+ XCTAssertEqual(try? deviceStateAccessor.read().deviceData?.wgKeyData.privateKey, currentKey)
+
+ expect.fulfill()
+ }
+
+ waitForExpectations(timeout: 1)
+ }
+
+ func testShouldNotRotateKeyForInvalidAccount() {
+ let expect = expectation(description: "Wait for operation to complete")
+
+ let currentKey = PrivateKey()
+ let nextKey = PrivateKey()
+
+ let remoteService = MockRemoteService(
+ initialKey: currentKey.publicKey,
+ getAccount: { accountNumber in
+ throw REST.Error.unhandledResponse(404, REST.ServerErrorResponse(code: .invalidAccount))
+ }
+ )
+ let deviceStateAccessor = MockDeviceStateAccessor.mockLoggedIn(
+ currentKey: currentKey,
+ rotationState: .failed(when: .retryInterval, nextKey: nextKey)
+ )
+
+ startDeviceCheck(remoteService: remoteService, deviceStateAccessor: deviceStateAccessor) { result in
+ let deviceCheck = result.value
+
+ XCTAssertNotNil(deviceCheck)
+ XCTAssert(deviceCheck?.accountVerdict == .invalid)
+ XCTAssert(deviceCheck?.keyRotationStatus == .noAction)
+ XCTAssertEqual(try? deviceStateAccessor.read().deviceData?.wgKeyData.privateKey, currentKey)
+
+ expect.fulfill()
+ }
+
+ waitForExpectations(timeout: 1)
+ }
+
+ func testShouldNotRotateKeyForRevokedDevice() {
+ let expect = expectation(description: "Wait for operation to complete")
+
+ let currentKey = PrivateKey()
+ let nextKey = PrivateKey()
+
+ let remoteService = MockRemoteService(
+ initialKey: currentKey.publicKey,
+ getDevice: { accountNumber, deviceIdentifier in
+ throw REST.Error.unhandledResponse(404, REST.ServerErrorResponse(code: .deviceNotFound))
+ }
+ )
+ let deviceStateAccessor = MockDeviceStateAccessor.mockLoggedIn(
+ currentKey: currentKey,
+ rotationState: .failed(when: .retryInterval, nextKey: nextKey)
+ )
+
+ startDeviceCheck(remoteService: remoteService, deviceStateAccessor: deviceStateAccessor) { result in
+ let deviceCheck = result.value
+
+ XCTAssertNotNil(deviceCheck)
+ XCTAssert(deviceCheck?.deviceVerdict == .revoked)
+ XCTAssert(deviceCheck?.keyRotationStatus == .noAction)
+ XCTAssertEqual(try? deviceStateAccessor.read().deviceData?.wgKeyData.privateKey, currentKey)
+
+ expect.fulfill()
+ }
+
+ waitForExpectations(timeout: 1)
+ }
+
+ func testShouldRotateKeyOnMismatchImmediately() {
+ let expect = expectation(description: "Wait for operation to complete")
+
+ let currentKey = PrivateKey()
+ let nextKey = PrivateKey()
+
+ let remoteService = MockRemoteService()
+ let deviceStateAccessor = MockDeviceStateAccessor.mockLoggedIn(
+ currentKey: currentKey,
+ rotationState: .failed(when: .packetTunnelCooldownInterval, nextKey: nextKey)
+ )
+
+ startDeviceCheck(
+ remoteService: remoteService,
+ deviceStateAccessor: deviceStateAccessor,
+ rotateImmediatelyOnKeyMismatch: true
+ ) { result in
+ let deviceCheck = result.value
+
+ XCTAssertNotNil(deviceCheck)
+ XCTAssertTrue(deviceCheck?.keyRotationStatus.isSucceeded ?? false)
+ XCTAssertEqual(try? deviceStateAccessor.read().deviceData?.wgKeyData.privateKey, nextKey)
+
+ expect.fulfill()
+ }
+
+ waitForExpectations(timeout: 1)
+ }
+
+ func testShouldRespectCooldownWhenAttemptingToRotateImmediately() {
+ let expect = expectation(description: "Wait for operation to complete")
+
+ let currentKey = PrivateKey()
+ let nextKey = PrivateKey()
+
+ let remoteService = MockRemoteService()
+ let deviceStateAccessor = MockDeviceStateAccessor.mockLoggedIn(
+ currentKey: currentKey,
+ rotationState: .failed(when: .zero, nextKey: nextKey)
+ )
+
+ startDeviceCheck(
+ remoteService: remoteService,
+ deviceStateAccessor: deviceStateAccessor,
+ rotateImmediatelyOnKeyMismatch: true
+ ) { result in
+ let deviceCheck = result.value
+
+ XCTAssertNotNil(deviceCheck)
+ XCTAssertEqual(deviceCheck?.keyRotationStatus, KeyRotationStatus.noAction)
+ XCTAssertEqual(try? deviceStateAccessor.read().deviceData?.wgKeyData.privateKey, currentKey)
+
+ expect.fulfill()
+ }
+
+ waitForExpectations(timeout: 1)
+ }
+
+ func testShouldNotRotateDeviceKeyWhenServerKeyIsIdentical() {
+ let expect = expectation(description: "Wait for operation to complete")
+
+ let currentKey = PrivateKey()
+ let remoteService = MockRemoteService(initialKey: currentKey.publicKey)
+ let deviceStateAccessor = MockDeviceStateAccessor.mockLoggedIn(
+ currentKey: currentKey,
+ rotationState: .succeeded
+ )
+
+ startDeviceCheck(remoteService: remoteService, deviceStateAccessor: deviceStateAccessor) { result in
+ let deviceCheck = result.value
+
+ XCTAssertNotNil(deviceCheck)
+ XCTAssertEqual(deviceCheck?.keyRotationStatus, KeyRotationStatus.noAction)
+ XCTAssertEqual(try? deviceStateAccessor.read().deviceData?.wgKeyData.privateKey, currentKey)
+
+ expect.fulfill()
+ }
+
+ waitForExpectations(timeout: 1)
+ }
+
+ func testShouldNotRotateKeyBeforeRetryIntervalPassed() {
+ let expect = expectation(description: "Wait for operation to complete")
+
+ let currentKey = PrivateKey()
+ let nextKey = PrivateKey()
+
+ let remoteService = MockRemoteService(initialKey: currentKey.publicKey)
+ let deviceStateAccessor = MockDeviceStateAccessor.mockLoggedIn(
+ currentKey: currentKey,
+ rotationState: .failed(when: .closeToRetryInterval, nextKey: nextKey)
+ )
+
+ startDeviceCheck(remoteService: remoteService, deviceStateAccessor: deviceStateAccessor) { result in
+ let deviceCheck = result.value
+
+ XCTAssertNotNil(deviceCheck)
+ XCTAssertEqual(deviceCheck?.keyRotationStatus, KeyRotationStatus.noAction)
+ XCTAssertEqual(try? deviceStateAccessor.read().deviceData?.wgKeyData.privateKey, currentKey)
+
+ expect.fulfill()
+ }
+
+ waitForExpectations(timeout: 1)
+ }
+
+ func testShouldRotateKeyOnceInTwentyFourHours() {
+ let expect = expectation(description: "Wait for operation to complete")
+
+ let currentKey = PrivateKey()
+ let nextKey = PrivateKey()
+
+ let remoteService = MockRemoteService()
+ let deviceStateAccessor = MockDeviceStateAccessor.mockLoggedIn(
+ currentKey: currentKey,
+ rotationState: .failed(when: .retryInterval, nextKey: nextKey)
+ )
+
+ startDeviceCheck(remoteService: remoteService, deviceStateAccessor: deviceStateAccessor) { result in
+ let deviceCheck = result.value
+
+ XCTAssertNotNil(deviceCheck)
+ XCTAssertTrue(deviceCheck?.keyRotationStatus.isSucceeded ?? false)
+ XCTAssertEqual(try? deviceStateAccessor.read().deviceData?.wgKeyData.privateKey, nextKey)
+
+ expect.fulfill()
+ }
+
+ waitForExpectations(timeout: 1)
+ }
+
+ func testShouldReportFailedKeyRotataionAttempt() {
+ let expect = expectation(description: "Wait for operation to complete")
+
+ let currentKey = PrivateKey()
+ let nextKey = PrivateKey()
+
+ let remoteService = MockRemoteService(
+ rotateDeviceKey: { accountNumber, identifier, publicKey in
+ throw URLError(.badURL)
+ }
+ )
+
+ let deviceStateAccessor = MockDeviceStateAccessor.mockLoggedIn(
+ currentKey: currentKey,
+ rotationState: .failed(when: .retryInterval, nextKey: nextKey)
+ )
+
+ startDeviceCheck(remoteService: remoteService, deviceStateAccessor: deviceStateAccessor) { result in
+ let deviceCheck = result.value
+
+ XCTAssertNotNil(deviceCheck)
+ XCTAssertTrue(deviceCheck?.keyRotationStatus.isAttempted ?? false)
+ XCTAssertEqual(try? deviceStateAccessor.read().deviceData?.wgKeyData.privateKey, currentKey)
+
+ expect.fulfill()
+ }
+
+ waitForExpectations(timeout: 1)
+ }
+
+ func testShouldFailOnKeyRotationRace() {
+ let expect = expectation(description: "Wait for operation to complete")
+
+ let currentKey = PrivateKey()
+ let nextKey = PrivateKey()
+
+ let deviceStateAccessor = MockDeviceStateAccessor.mockLoggedIn(
+ currentKey: currentKey,
+ rotationState: .failed(when: .retryInterval, nextKey: nextKey)
+ )
+
+ let remoteService = MockRemoteService(
+ rotateDeviceKey: { accountNumber, identifier, publicKey in
+ // Overwrite device state before returning the result from key rotation to simulate the race condition
+ // in the underlying storage.
+ try deviceStateAccessor.write(
+ .loggedIn(
+ StoredAccountData.mock(),
+ StoredDeviceData.mock(wgKeyData: StoredWgKeyData(creationDate: Date(), privateKey: currentKey))
+ )
+ )
+ }
+ )
+
+ startDeviceCheck(remoteService: remoteService, deviceStateAccessor: deviceStateAccessor) { result in
+ let deviceCheck = result.value
+
+ XCTAssertNotNil(deviceCheck)
+ XCTAssertTrue(deviceCheck?.keyRotationStatus.isAttempted ?? false)
+ XCTAssertEqual(try? deviceStateAccessor.read().deviceData?.wgKeyData.privateKey, currentKey)
+
+ expect.fulfill()
+ }
+
+ waitForExpectations(timeout: 1)
+ }
+
+ private func startDeviceCheck(
+ remoteService: DeviceCheckRemoteServiceProtocol,
+ deviceStateAccessor: DeviceStateAccessorProtocol,
+ rotateImmediatelyOnKeyMismatch: Bool = false,
+ completion: @escaping (Result<DeviceCheck, Error>) -> Void
+ ) {
+ let operation = DeviceCheckOperation(
+ dispatchQueue: dispatchQueue,
+ remoteSevice: remoteService,
+ deviceStateAccessor: deviceStateAccessor,
+ rotateImmediatelyOnKeyMismatch: rotateImmediatelyOnKeyMismatch,
+ completionHandler: completion
+ )
+
+ operationQueue.addOperation(operation)
+ }
+}
+
+/// Mock implementation of a remote service used by `DeviceCheckOperation` to reach the API.
+private class MockRemoteService: DeviceCheckRemoteServiceProtocol {
+ typealias AccountDataHandler = (_ accountNumber: String) throws -> Account
+ typealias DeviceDataHandler = (_ accountNumber: String, _ deviceIdentifier: String) throws -> Device
+ typealias RotateDeviceKeyHandler = (
+ _ accountNumber: String,
+ _ deviceIdentifier: String,
+ _ publicKey: PublicKey
+ ) throws -> Void
+
+ private let getAccountDataHandler: AccountDataHandler?
+ private let getDeviceDataHandler: DeviceDataHandler?
+ private let rotateDeviceKeyHandler: RotateDeviceKeyHandler?
+
+ private var currentKey: PublicKey
+
+ init(
+ initialKey: PublicKey = PrivateKey().publicKey,
+ getAccount: AccountDataHandler? = nil,
+ getDevice: DeviceDataHandler? = nil,
+ rotateDeviceKey: RotateDeviceKeyHandler? = nil
+ ) {
+ currentKey = initialKey
+ getAccountDataHandler = getAccount
+ getDeviceDataHandler = getDevice
+ rotateDeviceKeyHandler = rotateDeviceKey
+ }
+
+ func getAccountData(
+ accountNumber: String,
+ completion: @escaping (Result<Account, Error>) -> Void
+ ) -> Cancellable {
+ DispatchQueue.main.async { [self] in
+ let result: Result<Account, Error> = Result {
+ if let getAccountDataHandler {
+ return try getAccountDataHandler(accountNumber)
+ } else {
+ return Account.mock()
+ }
+ }
+ completion(result)
+ }
+ return AnyCancellable()
+ }
+
+ func getDevice(
+ accountNumber: String,
+ identifier: String,
+ completion: @escaping (Result<Device, Error>) -> Void
+ ) -> Cancellable {
+ DispatchQueue.main.async { [self] in
+ let result: Result<Device, Error> = Result {
+ if let getDeviceDataHandler {
+ return try getDeviceDataHandler(accountNumber, identifier)
+ } else {
+ return Device.mock(publicKey: currentKey)
+ }
+ }
+
+ completion(result)
+ }
+
+ return AnyCancellable()
+ }
+
+ func rotateDeviceKey(
+ accountNumber: String,
+ identifier: String,
+ publicKey: PublicKey,
+ completion: @escaping (Result<Device, Error>) -> Void
+ ) -> Cancellable {
+ DispatchQueue.main.async { [self] in
+ let result: Result<Device, Error> = Result {
+ try rotateDeviceKeyHandler?(accountNumber, identifier, publicKey)
+
+ currentKey = publicKey
+
+ return Device.mock(publicKey: currentKey)
+ }
+
+ completion(result)
+ }
+ return AnyCancellable()
+ }
+}
+
+/// Mock implementation of device state accessor used by `CheckDeviceOperation` to access the storage holding device
+/// state.
+private class MockDeviceStateAccessor: DeviceStateAccessorProtocol {
+ private var state: DeviceState
+ private let stateLock = NSLock()
+
+ init(initialState: DeviceState) {
+ state = initialState
+ }
+
+ func read() throws -> DeviceState {
+ stateLock.lock()
+ defer { stateLock.unlock() }
+ return state
+ }
+
+ func write(_ deviceState: DeviceState) throws {
+ stateLock.lock()
+ defer { stateLock.unlock() }
+ state = deviceState
+ }
+}
+
+/// Time interval since last key rotation used for mocking `StoredWgKeyData`.
+private enum TimeSinceLastKeyRotation {
+ /// No time passed since last key rotation.
+ case zero
+
+ /// Equal to key rotation retry interval.
+ case retryInterval
+
+ /// Equal to key rotation retry interval minus 1 second.
+ case closeToRetryInterval
+
+ /// Equal to cooldown interval used for packet tunnel based rotation.
+ case packetTunnelCooldownInterval
+
+ /// Returns negative time offset that can be used to compute the date in the past that can be used to simulate last
+ /// attempt date when simulating key rotation.
+ var timeOffset: TimeInterval {
+ switch self {
+ case .zero:
+ return .zero
+ case .retryInterval:
+ return -WgKeyRotation.retryInterval
+ case .closeToRetryInterval:
+ return -WgKeyRotation.retryInterval + 1
+ case .packetTunnelCooldownInterval:
+ return -WgKeyRotation.packetTunnelCooldownInterval
+ }
+ }
+}
+
+/// State of last key rotation used for mocking `StoredWgKeyData`.
+private enum LastKeyRotationState {
+ case succeeded
+ case failed(when: TimeSinceLastKeyRotation, nextKey: PrivateKey)
+}
+
+extension MockDeviceStateAccessor {
+ static func mockLoggedIn(currentKey: PrivateKey, rotationState: LastKeyRotationState) -> MockDeviceStateAccessor {
+ return MockDeviceStateAccessor(initialState: .loggedIn(
+ StoredAccountData.mock(),
+ StoredDeviceData.mock(wgKeyData: StoredWgKeyData.mock(currentKey: currentKey, rotationState: rotationState))
+ ))
+ }
+}
+
+private extension StoredWgKeyData {
+ static func mock(currentKey: PrivateKey, rotationState: LastKeyRotationState) -> StoredWgKeyData {
+ var keyData = StoredWgKeyData(creationDate: Date(), privateKey: currentKey)
+ keyData.apply(rotationState)
+ return keyData
+ }
+
+ private mutating func apply(_ rotationState: LastKeyRotationState) {
+ switch rotationState {
+ case .succeeded:
+ lastRotationAttemptDate = nil
+ nextPrivateKey = nil
+
+ case let .failed(recency, nextKey):
+ let attemptDate = creationDate.addingTimeInterval(recency.timeOffset)
+
+ creationDate = min(creationDate, attemptDate)
+ lastRotationAttemptDate = attemptDate
+ nextPrivateKey = nextKey
+ }
+ }
+}
+
+private extension StoredAccountData {
+ static func mock() -> StoredAccountData {
+ return StoredAccountData(
+ identifier: "account-id",
+ number: "account-number",
+ expiry: .distantFuture
+ )
+ }
+}
+
+private extension StoredDeviceData {
+ static func mock(wgKeyData: StoredWgKeyData) -> StoredDeviceData {
+ return StoredDeviceData(
+ creationDate: Date(),
+ identifier: "device-id",
+ name: "device-name",
+ hijackDNS: false,
+ ipv4Address: IPAddressRange(from: "127.0.0.1/32")!,
+ ipv6Address: IPAddressRange(from: "::ff/64")!,
+ wgKeyData: wgKeyData
+ )
+ }
+}
+
+private extension Device {
+ static func mock(publicKey: PublicKey) -> Device {
+ return Device(
+ id: "device-id",
+ name: "device-name",
+ pubkey: publicKey,
+ hijackDNS: false,
+ created: Date(),
+ ipv4Address: IPAddressRange(from: "127.0.0.1/32")!,
+ ipv6Address: IPAddressRange(from: "::ff/64")!,
+ ports: []
+ )
+ }
+}
+
+private extension Account {
+ static func mock(expiry: Date = .distantFuture) -> Account {
+ return Account(
+ id: "account-id",
+ expiry: expiry,
+ maxPorts: 5,
+ canAddPorts: true,
+ maxDevices: 5,
+ canAddDevices: true
+ )
+ }
+}
+
+private extension KeyRotationStatus {
+ /// Returns `true` if key rotation status is `.attempted`.
+ var isAttempted: Bool {
+ if case .attempted = self {
+ return true
+ }
+ return false
+ }
+}
+
+private extension AccountVerdict {
+ /// Returns `true` if account verdict is `.expired`.
+ var isExpired: Bool {
+ if case .expired = self {
+ return true
+ }
+ return false
+ }
+}
diff --git a/ios/MullvadVPNTests/WgKeyRotationTests.swift b/ios/MullvadVPNTests/WgKeyRotationTests.swift
new file mode 100644
index 0000000000..0b18d2df0c
--- /dev/null
+++ b/ios/MullvadVPNTests/WgKeyRotationTests.swift
@@ -0,0 +1,117 @@
+//
+// WgKeyRotationTests.swift
+// MullvadVPNTests
+//
+// Created by pronebird on 30/05/2023.
+// Copyright © 2023 Mullvad VPN AB. All rights reserved.
+//
+
+import MullvadTypes
+import struct WireGuardKitTypes.IPAddressRange
+import class WireGuardKitTypes.PrivateKey
+import XCTest
+
+final class WgKeyRotationTests: XCTestCase {
+ func testKeyRotationLifecycle() {
+ let data = StoredDeviceData.mock(
+ keyData: StoredWgKeyData(
+ creationDate: Date(),
+ privateKey: PrivateKey()
+ )
+ )
+
+ var keyRotation = WgKeyRotation(data: data)
+ let nextPubKey = keyRotation.beginAttempt()
+
+ let nextKey = keyRotation.data.wgKeyData.nextPrivateKey
+ let lastRotationDate = keyRotation.data.wgKeyData.lastRotationAttemptDate
+
+ XCTAssertNotNil(nextKey)
+ XCTAssertNotNil(lastRotationDate)
+ XCTAssertEqual(nextPubKey, nextKey?.publicKey)
+
+ XCTAssertTrue(keyRotation.setCompleted(with: Device.mock(privateKey: nextKey!)))
+
+ XCTAssertNil(keyRotation.data.wgKeyData.lastRotationAttemptDate)
+ XCTAssertNil(keyRotation.data.wgKeyData.nextPrivateKey)
+ XCTAssertEqual(keyRotation.data.wgKeyData.privateKey, nextKey)
+ }
+
+ func testHandlesMultipleKeyRotationAttempts() {
+ let currentKey = PrivateKey()
+ let nextKey = PrivateKey()
+ let data = StoredDeviceData.mock(
+ keyData: StoredWgKeyData(
+ creationDate: Date(),
+ lastRotationAttemptDate: Date(),
+ privateKey: currentKey,
+ nextPrivateKey: nextKey
+ )
+ )
+
+ var keyRotation = WgKeyRotation(data: data)
+ let pubKey = keyRotation.beginAttempt()
+ let lastAttemptDate = keyRotation.data.wgKeyData.lastRotationAttemptDate
+
+ let samePubKey = keyRotation.beginAttempt()
+ let anotherAttemptDate = keyRotation.data.wgKeyData.lastRotationAttemptDate
+
+ XCTAssertEqual(pubKey, nextKey.publicKey)
+ XCTAssertEqual(pubKey, samePubKey)
+ XCTAssertNotEqual(lastAttemptDate, anotherAttemptDate)
+
+ XCTAssertEqual(keyRotation.data.wgKeyData.privateKey, currentKey)
+ XCTAssertEqual(keyRotation.data.wgKeyData.nextPrivateKey, nextKey)
+ }
+
+ func testHandlesMultipleKeyRotationCompletions() {
+ let currentKey = PrivateKey()
+ let nextKey = PrivateKey()
+ let data = StoredDeviceData.mock(
+ keyData: StoredWgKeyData(
+ creationDate: Date(),
+ lastRotationAttemptDate: Date(),
+ privateKey: currentKey,
+ nextPrivateKey: nextKey
+ )
+ )
+
+ var keyRotation = WgKeyRotation(data: data)
+
+ XCTAssertTrue(keyRotation.setCompleted(with: Device.mock(privateKey: nextKey)))
+ XCTAssertFalse(keyRotation.setCompleted(with: Device.mock(privateKey: nextKey)))
+
+ XCTAssertEqual(keyRotation.data.wgKeyData.privateKey, nextKey)
+ XCTAssertNil(keyRotation.data.wgKeyData.nextPrivateKey)
+ XCTAssertNil(keyRotation.data.wgKeyData.lastRotationAttemptDate)
+ }
+}
+
+private extension StoredDeviceData {
+ static func mock(keyData: StoredWgKeyData) -> StoredDeviceData {
+ return StoredDeviceData(
+ creationDate: Date(),
+ identifier: "device-id",
+ name: "device-name",
+ hijackDNS: false,
+ ipv4Address: IPAddressRange(from: "127.0.0.1/32")!,
+ ipv6Address: IPAddressRange(from: "::ff/64")!,
+ wgKeyData: keyData
+ )
+ }
+}
+
+private extension Device {
+ static func mock(privateKey: PrivateKey) -> Device {
+ return Device(
+ id: "device-id",
+ name: "device-name",
+ pubkey: privateKey.publicKey,
+ hijackDNS: false,
+ created: Date(),
+ ipv4Address: IPAddressRange(from: "127.0.0.1/32")!,
+ ipv6Address: IPAddressRange(from: "::ff/64")!,
+ ports: []
+ )
+ }
+}
diff --git a/ios/PacketTunnel/DeviceCheck/DeviceCheckOperation.swift b/ios/PacketTunnel/DeviceCheck/DeviceCheckOperation.swift
new file mode 100644
index 0000000000..3e30b80f6f
--- /dev/null
+++ b/ios/PacketTunnel/DeviceCheck/DeviceCheckOperation.swift
@@ -0,0 +1,295 @@
+//
+// DeviceCheckOperation.swift
+// PacketTunnel
+//
+// Created by pronebird on 20/04/2023.
+// Copyright © 2023 Mullvad VPN AB. All rights reserved.
+//
+
+import Foundation
+import MullvadLogging
+import MullvadREST
+import MullvadTypes
+import Operations
+import class WireGuardKitTypes.PrivateKey
+import class WireGuardKitTypes.PublicKey
+
+/**
+ An operation that is responsible for performing account and device diagnostics and key rotation from within packet
+ tunnel process.
+
+ Packet tunnel runs this operation immediately as it starts, with `rotateImmediatelyOnKeyMismatch` flag set to
+ `true` which forces key rotation to happpen immediately given that the key stored on server does not match the key
+ stored on device. Unless the last rotation attempt took place less than 15 seconds ago in which case the key rotation
+ is not performed.
+
+ Other times, packet tunnel runs this operation with `rotateImmediatelyOnKeyMismatch` set to `false`, in which
+ case it respects the 24 hour interval between key rotation retry attempts.
+ */
+final class DeviceCheckOperation: ResultOperation<DeviceCheck> {
+ private let logger = Logger(label: "DeviceCheckOperation")
+
+ private let remoteService: DeviceCheckRemoteServiceProtocol
+ private let deviceStateAccessor: DeviceStateAccessorProtocol
+ private let rotateImmediatelyOnKeyMismatch: Bool
+
+ private var tasks: [Cancellable] = []
+
+ init(
+ dispatchQueue: DispatchQueue,
+ remoteSevice: DeviceCheckRemoteServiceProtocol,
+ deviceStateAccessor: DeviceStateAccessorProtocol,
+ rotateImmediatelyOnKeyMismatch: Bool,
+ completionHandler: @escaping CompletionHandler
+ ) {
+ self.remoteService = remoteSevice
+ self.deviceStateAccessor = deviceStateAccessor
+ self.rotateImmediatelyOnKeyMismatch = rotateImmediatelyOnKeyMismatch
+
+ super.init(dispatchQueue: dispatchQueue, completionQueue: dispatchQueue, completionHandler: completionHandler)
+ }
+
+ override func main() {
+ startFlow { result in
+ self.finish(result: result)
+ }
+ }
+
+ override func operationDidCancel() {
+ tasks.forEach { $0.cancel() }
+ }
+
+ // MARK: - Flow
+
+ /**
+ Begins the flow by fetching device state and then fetching account and device data. Calls `didReceiveData()` with
+ the received data when done.
+ */
+ private func startFlow(completion: @escaping (Result<DeviceCheck, Error>) -> Void) {
+ do {
+ guard case let .loggedIn(accountData, deviceData) = try deviceStateAccessor.read() else {
+ throw DeviceCheckError.invalidDeviceState
+ }
+
+ fetchData(
+ accountNumber: accountData.number,
+ deviceIdentifier: deviceData.identifier
+ ) { [self] accountResult, deviceResult in
+ didReceiveData(accountResult: accountResult, deviceResult: deviceResult, completion: completion)
+ }
+ } catch {
+ completion(.failure(error))
+ }
+ }
+
+ /**
+ Handles received data results and initiates key rotation when the key stored on server does not match the key
+ stored on device.
+ */
+ private func didReceiveData(
+ accountResult: Result<Account, Error>,
+ deviceResult: Result<Device, Error>,
+ completion: @escaping (Result<DeviceCheck, Error>) -> Void
+ ) {
+ do {
+ let accountVerdict = try accountVerdict(from: accountResult)
+ let deviceVerdict = try deviceVerdict(from: deviceResult)
+
+ // Do not rotate the key if account is invalid even if the API successfully returns a device.
+ if accountVerdict != .invalid, deviceVerdict == .keyMismatch {
+ rotateKeyIfNeeded { rotationResult in
+ completion(rotationResult.map { rotationStatus in
+ return DeviceCheck(
+ accountVerdict: accountVerdict,
+ deviceVerdict: rotationStatus.isSucceeded ? .active : .keyMismatch,
+ keyRotationStatus: rotationStatus
+ )
+ })
+ }
+ } else {
+ completion(.success(DeviceCheck(
+ accountVerdict: accountVerdict,
+ deviceVerdict: deviceVerdict,
+ keyRotationStatus: .noAction
+ )))
+ }
+ } catch {
+ completion(.failure(error))
+ }
+ }
+
+ // MARK: - Data fetch
+
+ /// Fetch account and device data simultaneously, upon completion calls completion handler passing the results to
+ /// it.
+ private func fetchData(
+ accountNumber: String, deviceIdentifier: String,
+ completion: @escaping (Result<Account, Error>, Result<Device, Error>) -> Void
+ ) {
+ var accountResult: Result<Account, Error> = .failure(OperationError.cancelled)
+ var deviceResult: Result<Device, Error> = .failure(OperationError.cancelled)
+
+ let dispatchGroup = DispatchGroup()
+
+ dispatchGroup.enter()
+ let accountTask = remoteService.getAccountData(accountNumber: accountNumber) { result in
+ accountResult = result
+ dispatchGroup.leave()
+ }
+
+ dispatchGroup.enter()
+ let deviceTask = remoteService.getDevice(accountNumber: accountNumber, identifier: deviceIdentifier) { result in
+ deviceResult = result
+ dispatchGroup.leave()
+ }
+
+ tasks.append(contentsOf: [accountTask, deviceTask])
+
+ dispatchGroup.notify(queue: dispatchQueue) {
+ completion(accountResult, deviceResult)
+ }
+ }
+
+ // MARK: - Key rotation
+
+ /**
+ Checks if the key should be rotated by checking when the last rotation took place. If conditions are satisfied,
+ then it rotate device key by marking the beginning of key rotation, updating device state and persisting before
+ proceeding to rotate the key.
+ */
+ private func rotateKeyIfNeeded(completion: @escaping (Result<KeyRotationStatus, Error>) -> Void) {
+ let deviceState: DeviceState
+ do {
+ deviceState = try deviceStateAccessor.read()
+ } catch {
+ logger.error(error: error, message: "Failed to read device state before rotating the key.")
+ completion(.failure(error))
+ return
+ }
+
+ guard case let .loggedIn(accountData, deviceData) = deviceState else {
+ logger.debug("Will not attempt to rotate the key as device is no longer logged in.")
+ completion(.failure(DeviceCheckError.invalidDeviceState))
+ return
+ }
+
+ var keyRotation = WgKeyRotation(data: deviceData)
+ guard keyRotation.shouldRotateFromPacketTunnel(rotateImmediately: rotateImmediatelyOnKeyMismatch) else {
+ completion(.success(.noAction))
+ return
+ }
+
+ let publicKey = keyRotation.beginAttempt()
+
+ do {
+ try deviceStateAccessor.write(.loggedIn(accountData, keyRotation.data))
+ } catch {
+ logger.error(error: error, message: "Failed to persist updated device state before rotating the key.")
+ completion(.failure(error))
+ return
+ }
+
+ logger.debug("Rotate private key from packet tunnel.")
+
+ let task = remoteService.rotateDeviceKey(
+ accountNumber: accountData.number,
+ identifier: deviceData.identifier,
+ publicKey: publicKey
+ ) { result in
+ self.dispatchQueue.async {
+ let returnResult = result.tryMap { device -> KeyRotationStatus in
+ try self.completeKeyRotation(device)
+ return .succeeded(Date())
+ }
+ .flatMapError { error in
+ self.logger.error(error: error, message: "Failed to rotate device key.")
+
+ if error.isOperationCancellationError {
+ return .failure(error)
+ } else {
+ return .success(.attempted(Date()))
+ }
+ }
+
+ completion(returnResult)
+ }
+ }
+
+ tasks.append(task)
+ }
+
+ /**
+ Updates device state with the new data received from `Device` and marks key rotation as completed by swapping the
+ current private key and erasing information about the last key rotation attempt.
+ */
+ private func completeKeyRotation(_ device: Device) throws {
+ logger.debug("Successfully rotated device key. Persisting device state...")
+
+ let deviceState = try deviceStateAccessor.read()
+ guard case let .loggedIn(accountData, deviceData) = deviceState else {
+ logger.debug("Will not persist device state after rotating the key because device is no longer logged in.")
+ throw DeviceCheckError.invalidDeviceState
+ }
+
+ var keyRotation = WgKeyRotation(data: deviceData)
+ let isCompleted = keyRotation.setCompleted(with: device)
+
+ if isCompleted {
+ do {
+ try deviceStateAccessor.write(.loggedIn(accountData, keyRotation.data))
+ } catch {
+ logger.error(error: error, message: "Failed to persist device state after rotating the key.")
+ throw error
+ }
+ } else {
+ logger.debug("Cannot complete key rotation due to rotation race.")
+
+ throw DeviceCheckError.keyRotationRace
+ }
+ }
+
+ // MARK: - Private helpers
+
+ /// Converts account data result type into `AccountVerdict`.
+ private func accountVerdict(from accountResult: Result<Account, Error>) throws -> AccountVerdict {
+ do {
+ let account = try accountResult.get()
+
+ return account.expiry > Date() ? .active(account) : .expired(account)
+ } catch let error as REST.Error where error.compareErrorCode(.invalidAccount) {
+ return .invalid
+ }
+ }
+
+ /// Converts device result type into `DeviceVerdict`.
+ private func deviceVerdict(from deviceResult: Result<Device, Error>) throws -> DeviceVerdict {
+ do {
+ let deviceState = try deviceStateAccessor.read()
+ guard let deviceData = deviceState.deviceData else { throw DeviceCheckError.invalidDeviceState }
+
+ let device = try deviceResult.get()
+
+ return deviceData.wgKeyData.privateKey.publicKey == device.pubkey ? .active : .keyMismatch
+ } catch let error as REST.Error where error.compareErrorCode(.deviceNotFound) {
+ return .revoked
+ }
+ }
+}
+
+/// An error used internally by `DeviceCheckOperation`.
+private enum DeviceCheckError: LocalizedError, Equatable {
+ /// Device is no longer logged in.
+ case invalidDeviceState
+
+ /// Main process has likely performed key rotation at the same time when packet tunnel was doing so.
+ case keyRotationRace
+
+ var errorDescription: String? {
+ switch self {
+ case .invalidDeviceState:
+ return "Cannot complete device check because device is no longer logged in."
+ case .keyRotationRace:
+ return "Detected key rotation race condition."
+ }
+ }
+}
diff --git a/ios/PacketTunnel/DeviceCheck/DeviceCheckRemoteService.swift b/ios/PacketTunnel/DeviceCheck/DeviceCheckRemoteService.swift
new file mode 100644
index 0000000000..da8c11726d
--- /dev/null
+++ b/ios/PacketTunnel/DeviceCheck/DeviceCheckRemoteService.swift
@@ -0,0 +1,58 @@
+//
+// DeviceCheckRemoteService.swift
+// PacketTunnel
+//
+// Created by pronebird on 30/05/2023.
+// Copyright © 2023 Mullvad VPN AB. All rights reserved.
+//
+
+import Foundation
+import MullvadREST
+import MullvadTypes
+import class WireGuardKitTypes.PublicKey
+
+/// An object that implements remote service used by `DeviceCheckOperation`.
+struct DeviceCheckRemoteService: DeviceCheckRemoteServiceProtocol {
+ private let accountsProxy: REST.AccountsProxy
+ private let devicesProxy: REST.DevicesProxy
+
+ init(accountsProxy: REST.AccountsProxy, devicesProxy: REST.DevicesProxy) {
+ self.accountsProxy = accountsProxy
+ self.devicesProxy = devicesProxy
+ }
+
+ func getAccountData(
+ accountNumber: String,
+ completion: @escaping (Result<Account, Error>) -> Void
+ ) -> Cancellable {
+ accountsProxy.getAccountData(accountNumber: accountNumber, retryStrategy: .noRetry, completion: completion)
+ }
+
+ func getDevice(
+ accountNumber: String,
+ identifier: String,
+ completion: @escaping (Result<Device, Error>) -> Void
+ ) -> Cancellable {
+ devicesProxy.getDevice(
+ accountNumber: accountNumber,
+ identifier: identifier,
+ retryStrategy: .noRetry,
+ completion: completion
+ )
+ }
+
+ func rotateDeviceKey(
+ accountNumber: String,
+ identifier: String,
+ publicKey: PublicKey,
+ completion: @escaping (Result<Device, Error>) -> Void
+ ) -> Cancellable {
+ devicesProxy.rotateDeviceKey(
+ accountNumber: accountNumber,
+ identifier: identifier,
+ publicKey: publicKey,
+ retryStrategy: .default,
+ completion: completion
+ )
+ }
+}
diff --git a/ios/PacketTunnel/DeviceCheck/DeviceCheckRemoteServiceProtocol.swift b/ios/PacketTunnel/DeviceCheck/DeviceCheckRemoteServiceProtocol.swift
new file mode 100644
index 0000000000..faf22e3680
--- /dev/null
+++ b/ios/PacketTunnel/DeviceCheck/DeviceCheckRemoteServiceProtocol.swift
@@ -0,0 +1,25 @@
+//
+// DeviceCheckRemoteServiceProtocol.swift
+// PacketTunnel
+//
+// Created by pronebird on 07/06/2023.
+// Copyright © 2023 Mullvad VPN AB. All rights reserved.
+//
+
+import Foundation
+import MullvadTypes
+import class WireGuardKitTypes.PublicKey
+
+/// A protocol that formalizes remote service dependency used by `DeviceCheckOperation`.
+protocol DeviceCheckRemoteServiceProtocol {
+ func getAccountData(accountNumber: String, completion: @escaping (Result<Account, Error>) -> Void)
+ -> Cancellable
+ func getDevice(accountNumber: String, identifier: String, completion: @escaping (Result<Device, Error>) -> Void)
+ -> Cancellable
+ func rotateDeviceKey(
+ accountNumber: String,
+ identifier: String,
+ publicKey: PublicKey,
+ completion: @escaping (Result<Device, Error>) -> Void
+ ) -> Cancellable
+}
diff --git a/ios/PacketTunnel/DeviceCheck/DeviceStateAccessor.swift b/ios/PacketTunnel/DeviceCheck/DeviceStateAccessor.swift
new file mode 100644
index 0000000000..cbca39a278
--- /dev/null
+++ b/ios/PacketTunnel/DeviceCheck/DeviceStateAccessor.swift
@@ -0,0 +1,21 @@
+//
+// DeviceStateAccessor.swift
+// PacketTunnel
+//
+// Created by pronebird on 30/05/2023.
+// Copyright © 2023 Mullvad VPN AB. All rights reserved.
+//
+
+import Foundation
+import MullvadTypes
+
+/// An object that provides access to `DeviceState` used by `DeviceCheckOperation`.
+struct DeviceStateAccessor: DeviceStateAccessorProtocol {
+ func read() throws -> DeviceState {
+ return try SettingsManager.readDeviceState()
+ }
+
+ func write(_ deviceState: DeviceState) throws {
+ try SettingsManager.writeDeviceState(deviceState)
+ }
+}
diff --git a/ios/PacketTunnel/DeviceCheck/DeviceStateAccessorProtocol.swift b/ios/PacketTunnel/DeviceCheck/DeviceStateAccessorProtocol.swift
new file mode 100644
index 0000000000..44f568e959
--- /dev/null
+++ b/ios/PacketTunnel/DeviceCheck/DeviceStateAccessorProtocol.swift
@@ -0,0 +1,15 @@
+//
+// DeviceStateAccessorProtocol.swift
+// PacketTunnel
+//
+// Created by pronebird on 07/06/2023.
+// Copyright © 2023 Mullvad VPN AB. All rights reserved.
+//
+
+import Foundation
+
+/// A protocol that formalizes device state accessor dependency used by `DeviceCheckOperation`.
+protocol DeviceStateAccessorProtocol {
+ func read() throws -> DeviceState
+ func write(_ deviceState: DeviceState) throws
+}
diff --git a/ios/PacketTunnel/PacketTunnelProvider.swift b/ios/PacketTunnel/PacketTunnelProvider.swift
index e58429838d..92855e7444 100644
--- a/ios/PacketTunnel/PacketTunnelProvider.swift
+++ b/ios/PacketTunnel/PacketTunnelProvider.swift
@@ -191,10 +191,7 @@ class PacketTunnelProvider: NEPacketTunnelProvider, TunnelMonitorDelegate {
tunnelMonitor.delegate = self
}
- override func startTunnel(
- options: [String: NSObject]?,
- completionHandler: @escaping (Error?) -> Void
- ) {
+ override func startTunnel(options: [String: NSObject]?, completionHandler: @escaping (Error?) -> Void) {
dispatchQueue.async {
let tunnelOptions = PacketTunnelOptions(rawOptions: options ?? [:])
var appSelectorResult: RelaySelectorResult?
@@ -264,6 +261,8 @@ class PacketTunnelProvider: NEPacketTunnelProvider, TunnelMonitorDelegate {
} else {
self.providerLogger.debug("Started the tunnel.")
+ self.tunnelAdapterDidStart()
+
self.startTunnelCompletionHandler = { [weak self] in
self?.isConnected = true
completionHandler(nil)
@@ -278,10 +277,7 @@ class PacketTunnelProvider: NEPacketTunnelProvider, TunnelMonitorDelegate {
}
}
- override func stopTunnel(
- with reason: NEProviderStopReason,
- completionHandler: @escaping () -> Void
- ) {
+ override func stopTunnel(with reason: NEProviderStopReason, completionHandler: @escaping () -> Void) {
dispatchQueue.async {
self.providerLogger.debug("Stop the tunnel: \(reason)")
@@ -407,8 +403,6 @@ class PacketTunnelProvider: NEPacketTunnelProvider, TunnelMonitorDelegate {
checkDeviceStateTask?.cancel()
checkDeviceStateTask = nil
- deviceCheck = nil
-
setReconnecting(false)
}
@@ -489,10 +483,7 @@ class PacketTunnelProvider: NEPacketTunnelProvider, TunnelMonitorDelegate {
guard let self else { return }
providerLogger.debug("Restart the tunnel that had startup failure.")
- reconnectTunnel(
- to: .automatic,
- shouldStopTunnelMonitor: false
- ) { [weak self] error in
+ reconnectTunnel(to: .automatic, shouldStopTunnelMonitor: false) { [weak self] error in
if error == nil {
self?.cancelTunnelStartupFailureRecovery()
}
@@ -516,6 +507,15 @@ class PacketTunnelProvider: NEPacketTunnelProvider, TunnelMonitorDelegate {
tunnelStartupFailureRecoveryTimer = nil
}
+ /**
+ Called once the tunnel was able to read configuration and start WireGuard adapter.
+ */
+ private func tunnelAdapterDidStart() {
+ dispatchPrecondition(condition: .onQueue(dispatchQueue))
+
+ startDeviceCheck(shouldImmediatelyRotateKeyOnMismatch: true)
+ }
+
private func startEmptyTunnel(completionHandler: @escaping (Error?) -> Void) {
dispatchPrecondition(condition: .onQueue(dispatchQueue))
@@ -537,6 +537,8 @@ class PacketTunnelProvider: NEPacketTunnelProvider, TunnelMonitorDelegate {
} else {
self.providerLogger.debug("Started an empty tunnel.")
+ self.tunnelAdapterDidStart()
+
self.startTunnelCompletionHandler = { [weak self] in
self?.isConnected = true
completionHandler(nil)
@@ -616,10 +618,7 @@ class PacketTunnelProvider: NEPacketTunnelProvider, TunnelMonitorDelegate {
operationQueue.addOperation(blockOperation)
}
- private func reconnectTunnelInner(
- to nextRelay: NextRelay,
- completionHandler: ((Error?) -> Void)? = nil
- ) {
+ private func reconnectTunnelInner(to nextRelay: NextRelay, completionHandler: ((Error?) -> Void)? = nil) {
dispatchPrecondition(condition: .onQueue(dispatchQueue))
// Read tunnel configuration.
@@ -691,127 +690,44 @@ class PacketTunnelProvider: NEPacketTunnelProvider, TunnelMonitorDelegate {
// MARK: - Device check
- /// Fetch account and device data to verify account expiry and device status.
- /// Saves the result into deviceCheck
- private func startDeviceCheck() {
- let deviceState: DeviceState
- do {
- deviceState = try SettingsManager.readDeviceState()
- } catch {
- providerLogger.error(
- error: error,
- message: "Failed to read device state."
- )
- return
- }
-
- guard case let .loggedIn(storedAccountData, storedDeviceData) = deviceState else {
- return
- }
-
- providerLogger.debug("Start device check.")
-
- let accountOperation = createGetAccountDataOperation(
- accountNumber: storedAccountData.number
- )
- let deviceOperation = createGetDeviceDataOperation(
- accountNumber: storedAccountData.number,
- identifier: storedDeviceData.identifier
- )
-
- let completionOperation = AsyncBlockOperation(dispatchQueue: dispatchQueue) { [weak self] in
- guard let self else { return }
-
- var newAccountExpiry: Date?
- var newDeviceRevoked: Bool?
-
- switch accountOperation.result {
- case let .failure(error):
- if !error.isOperationCancellationError {
- providerLogger.error(
- error: error,
- message: "Failed to fetch account data."
- )
- }
-
- case let .success(accountData):
- newAccountExpiry = accountData.expiry
+ /**
+ Start device diagnostics to determine the reason why the tunnel is not functional.
- case .none:
- break
- }
-
- switch deviceOperation.result {
- case let .failure(error):
- if let restError = error as? REST.Error,
- restError.compareErrorCode(.deviceNotFound)
- {
- newDeviceRevoked = true
- } else if !error.isOperationCancellationError {
- providerLogger.error(
- error: error,
- message: "Failed to fetch device data."
- )
- }
-
- case .none, .success:
- break
- }
+ This involves the following steps:
- if var deviceCheck {
- deviceCheck.update(
- accountExpiry: newAccountExpiry,
- isDeviceRevoked: newDeviceRevoked
- )
-
- self.deviceCheck = deviceCheck
- } else {
- deviceCheck = DeviceCheck(
- identifier: UUID(),
- isDeviceRevoked: newDeviceRevoked,
- accountExpiry: newAccountExpiry
- )
- }
+ 1. Fetch account and device data.
+ 2. Check account validity and whether it has enough time left.
+ 3. Verify that current device is registered with backend and that both device and backend point to the same public
+ key.
+ 4. Rotate WireGuard key on key mismatch.
+ */
+ private func startDeviceCheck(shouldImmediatelyRotateKeyOnMismatch: Bool = false) {
+ let checkOperation = DeviceCheckOperation(
+ dispatchQueue: dispatchQueue,
+ remoteSevice: DeviceCheckRemoteService(accountsProxy: accountsProxy, devicesProxy: devicesProxy),
+ deviceStateAccessor: DeviceStateAccessor(),
+ rotateImmediatelyOnKeyMismatch: shouldImmediatelyRotateKeyOnMismatch
+ ) { [self] result in
+ guard var newDeviceCheck = result.value else { return }
- if newDeviceRevoked ?? false {
+ if newDeviceCheck.accountVerdict == .invalid || newDeviceCheck.deviceVerdict == .revoked {
+ // Stop tunnel monitor when device is revoked or account is invalid.
tunnelMonitor.stop()
+ } else if case .succeeded = newDeviceCheck.keyRotationStatus {
+ // Tell the tunnel to reconnect using new private key if key was rotated dring device check.
+ reconnectTunnel(to: .automatic, shouldStopTunnelMonitor: false)
}
- }
-
- completionOperation.addDependencies([accountOperation, deviceOperation])
- let groupOperation = GroupOperation(operations: [
- accountOperation,
- deviceOperation,
- completionOperation,
- ])
- checkDeviceStateTask = groupOperation
-
- operationQueue.addOperation(groupOperation)
- }
+ // Retain the last key rotation status that isn't `.noAction` so that UI could keep track of when rotation
+ // attempts take place which should give it a hint when to refresh device state from settings.
+ if let deviceCheck, newDeviceCheck.keyRotationStatus == .noAction {
+ newDeviceCheck.keyRotationStatus = deviceCheck.keyRotationStatus
+ }
- private func createGetAccountDataOperation(accountNumber: String) -> ResultOperation<REST.AccountData> {
- return ResultBlockOperation<REST.AccountData>(dispatchQueue: dispatchQueue) { finish -> Cancellable in
- return self.accountsProxy.getAccountData(
- accountNumber: accountNumber,
- retryStrategy: .noRetry,
- completion: finish
- )
+ deviceCheck = newDeviceCheck
}
- }
- private func createGetDeviceDataOperation(
- accountNumber: String,
- identifier: String
- ) -> ResultOperation<REST.Device> {
- return ResultBlockOperation<REST.Device>(dispatchQueue: dispatchQueue) { finish -> Cancellable in
- return self.devicesProxy.getDevice(
- accountNumber: accountNumber,
- identifier: identifier,
- retryStrategy: .noRetry,
- completion: finish
- )
- }
+ operationQueue.addOperation(checkOperation)
}
}
@@ -824,26 +740,6 @@ private enum NextRelay {
case automatic
}
-extension DeviceCheck {
- mutating func update(accountExpiry: Date?, isDeviceRevoked: Bool?) {
- var shouldChangeIdentifier = false
-
- if let accountExpiry, self.accountExpiry != accountExpiry {
- shouldChangeIdentifier = true
- self.accountExpiry = accountExpiry
- }
-
- if let isDeviceRevoked, self.isDeviceRevoked != isDeviceRevoked {
- shouldChangeIdentifier = true
- self.isDeviceRevoked = isDeviceRevoked
- }
-
- if shouldChangeIdentifier {
- identifier = UUID()
- }
- }
-}
-
extension PacketTunnelErrorWrapper {
init?(error: Error) {
switch error {
diff --git a/ios/TunnelProviderMessaging/PacketTunnelOptions.swift b/ios/TunnelProviderMessaging/PacketTunnelOptions.swift
index 6598f24b58..623b67631a 100644
--- a/ios/TunnelProviderMessaging/PacketTunnelOptions.swift
+++ b/ios/TunnelProviderMessaging/PacketTunnelOptions.swift
@@ -48,7 +48,7 @@ public struct PacketTunnelOptions {
}
public func isOnDemand() -> Bool {
- return _rawOptions[Keys.isOnDemand.rawValue] as? Int == .some(1)
+ return _rawOptions[Keys.isOnDemand.rawValue] as? Int == 1
}
/// Encode custom parameter value