diff options
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 |
