diff --git a/ios/MullvadSettings/QuantumResistanceSettings.swift b/ios/MullvadSettings/QuantumResistanceSettings.swift index b5c12ae703b5..956b2fd0ded2 100644 --- a/ios/MullvadSettings/QuantumResistanceSettings.swift +++ b/ios/MullvadSettings/QuantumResistanceSettings.swift @@ -13,3 +13,10 @@ public enum TunnelQuantumResistance: Codable { case on case off } + +public extension TunnelQuantumResistance { + /// A single source of truth for whether the current state counts as on + var isEnabled: Bool { + self == .on + } +} diff --git a/ios/MullvadVPN.xcodeproj/project.pbxproj b/ios/MullvadVPN.xcodeproj/project.pbxproj index c8fb16cb0135..01a4df4ae586 100644 --- a/ios/MullvadVPN.xcodeproj/project.pbxproj +++ b/ios/MullvadVPN.xcodeproj/project.pbxproj @@ -47,6 +47,9 @@ 449EBA262B975B9700DFA4EB /* PostQuantumKeyReceiving.swift in Sources */ = {isa = PBXBuildFile; fileRef = 449EBA252B975B9700DFA4EB /* PostQuantumKeyReceiving.swift */; }; 44B02E3B2BC5732D008EDF34 /* LoggingTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 44B02E3A2BC5732D008EDF34 /* LoggingTests.swift */; }; 44B02E3C2BC5B8A5008EDF34 /* Bundle+ProductVersion.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5891BF1B25E3E3EB006D6FB0 /* Bundle+ProductVersion.swift */; }; + 44BB5F972BE527F4002520EB /* TunnelState+UI.swift in Sources */ = {isa = PBXBuildFile; fileRef = 44BB5F962BE527F4002520EB /* TunnelState+UI.swift */; }; + 44BB5F982BE527F4002520EB /* TunnelState+UI.swift in Sources */ = {isa = PBXBuildFile; fileRef = 44BB5F962BE527F4002520EB /* TunnelState+UI.swift */; }; + 44BB5F9A2BE529FF002520EB /* TunnelStateTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 44BB5F992BE529FE002520EB /* TunnelStateTests.swift */; }; 44DD7D242B6CFFD70005F67F /* StartTunnelOperationTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 44DD7D232B6CFFD70005F67F /* StartTunnelOperationTests.swift */; }; 44DD7D272B6D18FB0005F67F /* MockTunnelInteractor.swift in Sources */ = {isa = PBXBuildFile; fileRef = 44DD7D262B6D18FB0005F67F /* MockTunnelInteractor.swift */; }; 44DD7D292B7113CA0005F67F /* MockTunnel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 44DD7D282B7113CA0005F67F /* MockTunnel.swift */; }; @@ -1379,6 +1382,8 @@ 449EB9FE2B95FF2500DFA4EB /* AccountMock.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AccountMock.swift; sourceTree = ""; }; 449EBA252B975B9700DFA4EB /* PostQuantumKeyReceiving.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PostQuantumKeyReceiving.swift; sourceTree = ""; }; 44B02E3A2BC5732D008EDF34 /* LoggingTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = LoggingTests.swift; sourceTree = ""; }; + 44BB5F962BE527F4002520EB /* TunnelState+UI.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "TunnelState+UI.swift"; sourceTree = ""; }; + 44BB5F992BE529FE002520EB /* TunnelStateTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = TunnelStateTests.swift; sourceTree = ""; }; 44DD7D232B6CFFD70005F67F /* StartTunnelOperationTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StartTunnelOperationTests.swift; sourceTree = ""; }; 44DD7D262B6D18FB0005F67F /* MockTunnelInteractor.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MockTunnelInteractor.swift; sourceTree = ""; }; 44DD7D282B7113CA0005F67F /* MockTunnel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MockTunnel.swift; sourceTree = ""; }; @@ -2483,6 +2488,7 @@ 44DD7D262B6D18FB0005F67F /* MockTunnelInteractor.swift */, 44DD7D232B6CFFD70005F67F /* StartTunnelOperationTests.swift */, A9A5F9A12ACB003D0083449F /* TunnelManagerTests.swift */, + 44BB5F992BE529FE002520EB /* TunnelStateTests.swift */, A9E0317B2ACBFC7E0095D843 /* TunnelStore+Stubs.swift */, A9E031792ACB0AE70095D843 /* UIApplication+Stubs.swift */, 58165EBD2A262CBB00688EAD /* WgKeyRotationTests.swift */, @@ -2596,6 +2602,7 @@ 5820676326E771DB00655B05 /* TunnelManagerErrors.swift */, 5823FA5326CE49F600283BF8 /* TunnelObserver.swift */, 58B93A1226C3F13600A55733 /* TunnelState.swift */, + 44BB5F962BE527F4002520EB /* TunnelState+UI.swift */, 5803B4B12940A48700C23744 /* TunnelStore.swift */, 5842102F282D8A3C00F24E46 /* UpdateAccountDataOperation.swift */, 58421031282E42B000F24E46 /* UpdateDeviceDataOperation.swift */, @@ -5199,6 +5206,7 @@ A9A5F9FE2ACB05160083449F /* NotificationManager.swift in Sources */, A9A5F9FF2ACB05160083449F /* NotificationManagerDelegate.swift in Sources */, 7A9BE5AD2B90DF2D00E2A7D0 /* AllLocationsDataSourceTests.swift in Sources */, + 44BB5F9A2BE529FF002520EB /* TunnelStateTests.swift in Sources */, A900E9BE2ACC654100C95F67 /* APIProxy+Stubs.swift in Sources */, A900E9BA2ACC5D0600C95F67 /* RESTRequestExecutor+Stubs.swift in Sources */, A9A5FA002ACB05160083449F /* ProductsRequestOperation.swift in Sources */, @@ -5267,6 +5275,7 @@ 7A9BE5AB2B909A1700E2A7D0 /* LocationDataSourceProtocol.swift in Sources */, A9A5FA2A2ACB05160083449F /* CoordinatesTests.swift in Sources */, 44DD7D242B6CFFD70005F67F /* StartTunnelOperationTests.swift in Sources */, + 44BB5F982BE527F4002520EB /* TunnelState+UI.swift in Sources */, A9A5FA2B2ACB05160083449F /* CustomDateComponentsFormattingTests.swift in Sources */, A9A5FA2C2ACB05160083449F /* DeviceCheckOperationTests.swift in Sources */, A9A5FA2D2ACB05160083449F /* DurationTests.swift in Sources */, @@ -5444,6 +5453,7 @@ 7A9CCCB72A96302800DD6A34 /* RevokedCoordinator.swift in Sources */, 7A6389F82B864CDF008E77E1 /* LocationNode.swift in Sources */, 587D96742886D87C00CD8F1C /* DeviceManagementContentView.swift in Sources */, + 44BB5F972BE527F4002520EB /* TunnelState+UI.swift in Sources */, 7A11DD0B2A9495D400098CD8 /* AppRoutes.swift in Sources */, 5827B0902B0CAA0500CCBBA1 /* EditAccessMethodCoordinator.swift in Sources */, 5846227126E229F20035F7C2 /* StoreSubscription.swift in Sources */, diff --git a/ios/MullvadVPN/SimulatorTunnelProvider/SimulatorTunnelProviderHost.swift b/ios/MullvadVPN/SimulatorTunnelProvider/SimulatorTunnelProviderHost.swift index e7bf690f872d..ea5260b8af2d 100644 --- a/ios/MullvadVPN/SimulatorTunnelProvider/SimulatorTunnelProviderHost.swift +++ b/ios/MullvadVPN/SimulatorTunnelProvider/SimulatorTunnelProviderHost.swift @@ -176,14 +176,16 @@ final class SimulatorTunnelProviderHost: SimulatorTunnelProviderDelegate { guard let selectedRelay = selectedRelay else { return } do { + let settings = try SettingsManager.readSettings() observedState = .connected( ObservedConnectionState( selectedRelay: selectedRelay, - relayConstraints: try SettingsManager.readSettings().relayConstraints, + relayConstraints: settings.relayConstraints, networkReachability: .reachable, connectionAttemptCount: 0, transportLayer: .udp, - remotePort: selectedRelay.endpoint.ipv4Relay.port + remotePort: selectedRelay.endpoint.ipv4Relay.port, + isPostQuantum: settings.tunnelQuantumResistance.isEnabled ) ) } catch { diff --git a/ios/MullvadVPN/TunnelManager/MapConnectionStatusOperation.swift b/ios/MullvadVPN/TunnelManager/MapConnectionStatusOperation.swift index f92fd37e077b..4957af7c52ec 100644 --- a/ios/MullvadVPN/TunnelManager/MapConnectionStatusOperation.swift +++ b/ios/MullvadVPN/TunnelManager/MapConnectionStatusOperation.swift @@ -51,11 +51,11 @@ class MapConnectionStatusOperation: AsyncOperation { switch observedState { case let .connected(connectionState): return connectionState.isNetworkReachable - ? .connected(connectionState.selectedRelay) + ? .connected(connectionState.selectedRelay, isPostQuantum: connectionState.isPostQuantum) : .waitingForConnectivity(.noConnection) case let .connecting(connectionState): return connectionState.isNetworkReachable - ? .connecting(connectionState.selectedRelay) + ? .connecting(connectionState.selectedRelay, isPostQuantum: connectionState.isPostQuantum) : .waitingForConnectivity(.noConnection) case let .negotiatingPostQuantumKey(connectionState, privateKey): return connectionState.isNetworkReachable @@ -63,7 +63,7 @@ class MapConnectionStatusOperation: AsyncOperation { : .waitingForConnectivity(.noConnection) case let .reconnecting(connectionState): return connectionState.isNetworkReachable - ? .reconnecting(connectionState.selectedRelay) + ? .reconnecting(connectionState.selectedRelay, isPostQuantum: connectionState.isPostQuantum) : .waitingForConnectivity(.noConnection) case let .error(blockedState): return .error(blockedState.reason) diff --git a/ios/MullvadVPN/TunnelManager/StartTunnelOperation.swift b/ios/MullvadVPN/TunnelManager/StartTunnelOperation.swift index 9474a0a4813f..cd9e8b7a88c5 100644 --- a/ios/MullvadVPN/TunnelManager/StartTunnelOperation.swift +++ b/ios/MullvadVPN/TunnelManager/StartTunnelOperation.swift @@ -90,7 +90,10 @@ class StartTunnelOperation: ResultOperation { interactor.updateTunnelStatus { tunnelStatus in tunnelStatus = TunnelStatus() - tunnelStatus.state = .connecting(selectedRelay) + tunnelStatus.state = .connecting( + selectedRelay, + isPostQuantum: interactor.settings.tunnelQuantumResistance.isEnabled + ) } try tunnel.start(options: tunnelOptions.rawOptions()) diff --git a/ios/MullvadVPN/TunnelManager/TunnelState+UI.swift b/ios/MullvadVPN/TunnelManager/TunnelState+UI.swift new file mode 100644 index 000000000000..090fdbfe2256 --- /dev/null +++ b/ios/MullvadVPN/TunnelManager/TunnelState+UI.swift @@ -0,0 +1,260 @@ +// +// TunnelState+UI.swift +// MullvadVPN +// +// Created by Andrew Bulhak on 2024-05-03. +// Copyright © 2024 Mullvad VPN AB. All rights reserved. +// + +import UIKit + +extension TunnelState { + var textColorForSecureLabel: UIColor { + switch self { + case .connecting, .reconnecting, .waitingForConnectivity(.noConnection), .negotiatingPostQuantumKey: + .white + + case .connected: + .successColor + + case .disconnecting, .disconnected, .pendingReconnect, .waitingForConnectivity(.noNetwork), .error: + .dangerColor + } + } + + var shouldEnableButtons: Bool { + if case .waitingForConnectivity(.noNetwork) = self { + return false + } + + return true + } + + var localizedTitleForSecureLabel: String { + switch self { + case let .connecting(_, isPostQuantum), let .reconnecting(_, isPostQuantum): + if isPostQuantum { + NSLocalizedString( + "TUNNEL_STATE_PQ_CONNECTING", + tableName: "Main", + value: "Creating quantum secure connection", + comment: "" + ) + } else { + NSLocalizedString( + "TUNNEL_STATE_CONNECTING", + tableName: "Main", + value: "Creating secure connection", + comment: "" + ) + } + + case .negotiatingPostQuantumKey: + NSLocalizedString( + "TUNNEL_STATE_NEGOTIATING_KEY", + tableName: "Main", + value: "Creating quantum secure connection", + comment: "" + ) + + case let .connected(_, isPostQuantum): + if isPostQuantum { + NSLocalizedString( + "TUNNEL_STATE_PQ_CONNECTED", + tableName: "Main", + value: "Quantum secure connection", + comment: "" + ) + } else { + NSLocalizedString( + "TUNNEL_STATE_CONNECTED", + tableName: "Main", + value: "Secure connection", + comment: "" + ) + } + + case .disconnecting(.nothing): + NSLocalizedString( + "TUNNEL_STATE_DISCONNECTING", + tableName: "Main", + value: "Disconnecting", + comment: "" + ) + case .disconnecting(.reconnect), .pendingReconnect: + NSLocalizedString( + "TUNNEL_STATE_PENDING_RECONNECT", + tableName: "Main", + value: "Reconnecting", + comment: "" + ) + + case .disconnected: + NSLocalizedString( + "TUNNEL_STATE_DISCONNECTED", + tableName: "Main", + value: "Unsecured connection", + comment: "" + ) + + case .waitingForConnectivity(.noConnection), .error: + NSLocalizedString( + "TUNNEL_STATE_WAITING_FOR_CONNECTIVITY", + tableName: "Main", + value: "Blocked connection", + comment: "" + ) + + case .waitingForConnectivity(.noNetwork): + NSLocalizedString( + "TUNNEL_STATE_NO_NETWORK", + tableName: "Main", + value: "No network", + comment: "" + ) + } + } + + var localizedTitleForSelectLocationButton: String? { + switch self { + case .disconnecting(.reconnect), .pendingReconnect: + NSLocalizedString( + "SWITCH_LOCATION_BUTTON_TITLE", + tableName: "Main", + value: "Select location", + comment: "" + ) + + case .disconnected, .disconnecting(.nothing): + NSLocalizedString( + "SELECT_LOCATION_BUTTON_TITLE", + tableName: "Main", + value: "Select location", + comment: "" + ) + + case .connecting, .connected, .reconnecting, .waitingForConnectivity, .error: + NSLocalizedString( + "SWITCH_LOCATION_BUTTON_TITLE", + tableName: "Main", + value: "Switch location", + comment: "" + ) + + case .negotiatingPostQuantumKey: + NSLocalizedString( + "SWITCH_LOCATION_BUTTON_TITLE", + tableName: "Main", + value: "Switch location", + comment: "" + ) + } + } + + var localizedAccessibilityLabel: String { + switch self { + case let .connecting(_, isPostQuantum): + if isPostQuantum { + NSLocalizedString( + "TUNNEL_STATE_PQ_CONNECTING_ACCESSIBILITY_LABEL", + tableName: "Main", + value: "Creating quantum secure connection", + comment: "" + ) + } else { + NSLocalizedString( + "TUNNEL_STATE_CONNECTING_ACCESSIBILITY_LABEL", + tableName: "Main", + value: "Creating secure connection", + comment: "" + ) + } + + // TODO: Is this correct ? + case .negotiatingPostQuantumKey: + NSLocalizedString( + "TUNNEL_STATE_CONNECTING_ACCESSIBILITY_LABEL", + tableName: "Main", + value: "Creating quantum secure connection", + comment: "" + ) + + case let .connected(tunnelInfo, isPostQuantum): + if isPostQuantum { + String( + format: NSLocalizedString( + "TUNNEL_STATE_PQ_CONNECTED_ACCESSIBILITY_LABEL", + tableName: "Main", + value: "Quantum secure connection. Connected to %@, %@", + comment: "" + ), + tunnelInfo.location.city, + tunnelInfo.location.country + ) + } else { + String( + format: NSLocalizedString( + "TUNNEL_STATE_CONNECTED_ACCESSIBILITY_LABEL", + tableName: "Main", + value: "Secure connection. Connected to %@, %@", + comment: "" + ), + tunnelInfo.location.city, + tunnelInfo.location.country + ) + } + + case .disconnected: + NSLocalizedString( + "TUNNEL_STATE_DISCONNECTED_ACCESSIBILITY_LABEL", + tableName: "Main", + value: "Unsecured connection", + comment: "" + ) + + case let .reconnecting(tunnelInfo, _): + String( + format: NSLocalizedString( + "TUNNEL_STATE_RECONNECTING_ACCESSIBILITY_LABEL", + tableName: "Main", + value: "Reconnecting to %@, %@", + comment: "" + ), + tunnelInfo.location.city, + tunnelInfo.location.country + ) + + case .waitingForConnectivity(.noConnection), .error: + NSLocalizedString( + "TUNNEL_STATE_WAITING_FOR_CONNECTIVITY_ACCESSIBILITY_LABEL", + tableName: "Main", + value: "Blocked connection", + comment: "" + ) + + case .waitingForConnectivity(.noNetwork): + NSLocalizedString( + "TUNNEL_STATE_NO_NETWORK_ACCESSIBILITY_LABEL", + tableName: "Main", + value: "No network", + comment: "" + ) + + case .disconnecting(.nothing): + NSLocalizedString( + "TUNNEL_STATE_DISCONNECTING_ACCESSIBILITY_LABEL", + tableName: "Main", + value: "Disconnecting", + comment: "" + ) + + case .disconnecting(.reconnect), .pendingReconnect: + NSLocalizedString( + "TUNNEL_STATE_PENDING_RECONNECT_ACCESSIBILITY_LABEL", + tableName: "Main", + value: "Reconnecting", + comment: "" + ) + } + } +} diff --git a/ios/MullvadVPN/TunnelManager/TunnelState.swift b/ios/MullvadVPN/TunnelManager/TunnelState.swift index 1d480fe3a53f..76148bdbb889 100644 --- a/ios/MullvadVPN/TunnelManager/TunnelState.swift +++ b/ios/MullvadVPN/TunnelManager/TunnelState.swift @@ -49,13 +49,13 @@ enum TunnelState: Equatable, CustomStringConvertible { case pendingReconnect /// Connecting the tunnel. - case connecting(SelectedRelay?) + case connecting(SelectedRelay?, isPostQuantum: Bool) /// Negotiating a key for post-quantum resistance case negotiatingPostQuantumKey(SelectedRelay, PrivateKey) /// Connected the tunnel - case connected(SelectedRelay) + case connected(SelectedRelay, isPostQuantum: Bool) /// Disconnecting the tunnel case disconnecting(ActionAfterDisconnect) @@ -68,7 +68,7 @@ enum TunnelState: Equatable, CustomStringConvertible { /// 1. Asking the running tunnel to reconnect to new relay via IPC. /// 2. Tunnel attempts to reconnect to new relay as the current relay appears to be /// dysfunctional. - case reconnecting(SelectedRelay) + case reconnecting(SelectedRelay, isPostQuantum: Bool) /// Waiting for connectivity to come back up. case waitingForConnectivity(WaitingForConnectionReason) @@ -80,20 +80,20 @@ enum TunnelState: Equatable, CustomStringConvertible { switch self { case .pendingReconnect: "pending reconnect after disconnect" - case let .connecting(tunnelRelay): + case let .connecting(tunnelRelay, isPostQuantum): if let tunnelRelay { - "connecting to \(tunnelRelay.hostname)" + "connecting \(isPostQuantum ? "(PQ) " : "")to \(tunnelRelay.hostname)" } else { - "connecting, fetching relay" + "connecting\(isPostQuantum ? " (PQ)" : ""), fetching relay" } - case let .connected(tunnelRelay): - "connected to \(tunnelRelay.hostname)" + case let .connected(tunnelRelay, isPostQuantum): + "connected \(isPostQuantum ? "(PQ) " : "")to \(tunnelRelay.hostname)" case let .disconnecting(actionAfterDisconnect): "disconnecting and then \(actionAfterDisconnect)" case .disconnected: "disconnected" - case let .reconnecting(tunnelRelay): - "reconnecting to \(tunnelRelay.hostname)" + case let .reconnecting(tunnelRelay, isPostQuantum): + "reconnecting \(isPostQuantum ? "(PQ) " : "")to \(tunnelRelay.hostname)" case .waitingForConnectivity: "waiting for connectivity" case let .error(blockedStateReason): @@ -106,20 +106,18 @@ enum TunnelState: Equatable, CustomStringConvertible { var isSecured: Bool { switch self { case .reconnecting, .connecting, .connected, .waitingForConnectivity(.noConnection), .error(.accountExpired), - .error(.deviceRevoked): + .error(.deviceRevoked), .negotiatingPostQuantumKey: true case .pendingReconnect, .disconnecting, .disconnected, .waitingForConnectivity(.noNetwork), .error: false - case .negotiatingPostQuantumKey: - false } } var relay: SelectedRelay? { switch self { - case let .connected(relay), let .reconnecting(relay), let .negotiatingPostQuantumKey(relay, _): + case let .connected(relay, _), let .reconnecting(relay, _), let .negotiatingPostQuantumKey(relay, _): relay - case let .connecting(relay): + case let .connecting(relay, _): relay case .disconnecting, .disconnected, .waitingForConnectivity, .pendingReconnect, .error: nil diff --git a/ios/MullvadVPN/View controllers/Tunnel/TunnelControlView.swift b/ios/MullvadVPN/View controllers/Tunnel/TunnelControlView.swift index 2d348b252835..92acdb7a5542 100644 --- a/ios/MullvadVPN/View controllers/Tunnel/TunnelControlView.swift +++ b/ios/MullvadVPN/View controllers/Tunnel/TunnelControlView.swift @@ -27,7 +27,7 @@ private enum TunnelControlActionButton { } final class TunnelControlView: UIView { - private let secureLabel = makeBoldTextLabel(ofSize: 20) + private let secureLabel = makeBoldTextLabel(ofSize: 20, numberOfLines: 0) private let cityLabel = makeBoldTextLabel(ofSize: 34) private let countryLabel = makeBoldTextLabel(ofSize: 34) @@ -420,11 +420,12 @@ final class TunnelControlView: UIView { ) } - private class func makeBoldTextLabel(ofSize fontSize: CGFloat) -> UILabel { + private class func makeBoldTextLabel(ofSize fontSize: CGFloat, numberOfLines: Int = 1) -> UILabel { let textLabel = UILabel() textLabel.translatesAutoresizingMaskIntoConstraints = false textLabel.font = UIFont.boldSystemFont(ofSize: fontSize) textLabel.textColor = .white + textLabel.numberOfLines = numberOfLines return textLabel } @@ -452,217 +453,6 @@ final class TunnelControlView: UIView { } private extension TunnelState { - var textColorForSecureLabel: UIColor { - switch self { - case .connecting, .reconnecting, .waitingForConnectivity(.noConnection), .negotiatingPostQuantumKey: - .white - - case .connected: - .successColor - - case .disconnecting, .disconnected, .pendingReconnect, .waitingForConnectivity(.noNetwork), .error: - .dangerColor - } - } - - var shouldEnableButtons: Bool { - if case .waitingForConnectivity(.noNetwork) = self { - return false - } - - return true - } - - var localizedTitleForSecureLabel: String { - switch self { - case .connecting, .reconnecting: - NSLocalizedString( - "TUNNEL_STATE_CONNECTING", - tableName: "Main", - value: "Creating secure connection", - comment: "" - ) - - // TODO: Is this the correct message here ? - case .negotiatingPostQuantumKey: - NSLocalizedString( - "TUNNEL_STATE_NEGOTIATING_KEY", - tableName: "Main", - value: "Creating quantum secure connection", - comment: "" - ) - - case .connected: - NSLocalizedString( - "TUNNEL_STATE_CONNECTED", - tableName: "Main", - value: "Secure connection", - comment: "" - ) - - case .disconnecting(.nothing): - NSLocalizedString( - "TUNNEL_STATE_DISCONNECTING", - tableName: "Main", - value: "Disconnecting", - comment: "" - ) - case .disconnecting(.reconnect), .pendingReconnect: - NSLocalizedString( - "TUNNEL_STATE_PENDING_RECONNECT", - tableName: "Main", - value: "Reconnecting", - comment: "" - ) - - case .disconnected: - NSLocalizedString( - "TUNNEL_STATE_DISCONNECTED", - tableName: "Main", - value: "Unsecured connection", - comment: "" - ) - - case .waitingForConnectivity(.noConnection), .error: - NSLocalizedString( - "TUNNEL_STATE_WAITING_FOR_CONNECTIVITY", - tableName: "Main", - value: "Blocked connection", - comment: "" - ) - - case .waitingForConnectivity(.noNetwork): - NSLocalizedString( - "TUNNEL_STATE_NO_NETWORK", - tableName: "Main", - value: "No network", - comment: "" - ) - } - } - - var localizedTitleForSelectLocationButton: String? { - switch self { - case .disconnecting(.reconnect), .pendingReconnect: - NSLocalizedString( - "SWITCH_LOCATION_BUTTON_TITLE", - tableName: "Main", - value: "Select location", - comment: "" - ) - - case .disconnected, .disconnecting(.nothing): - NSLocalizedString( - "SELECT_LOCATION_BUTTON_TITLE", - tableName: "Main", - value: "Select location", - comment: "" - ) - - case .connecting, .connected, .reconnecting, .waitingForConnectivity, .error: - NSLocalizedString( - "SWITCH_LOCATION_BUTTON_TITLE", - tableName: "Main", - value: "Switch location", - comment: "" - ) - - // TODO: Is this correct ? - case .negotiatingPostQuantumKey: - NSLocalizedString( - "SWITCH_LOCATION_BUTTON_TITLE", - tableName: "Main", - value: "Switch location", - comment: "" - ) - } - } - - var localizedAccessibilityLabel: String { - switch self { - case .connecting: - NSLocalizedString( - "TUNNEL_STATE_CONNECTING_ACCESSIBILITY_LABEL", - tableName: "Main", - value: "Creating secure connection", - comment: "" - ) - - // TODO: Is this correct ? - case .negotiatingPostQuantumKey: - NSLocalizedString( - "TUNNEL_STATE_CONNECTING_ACCESSIBILITY_LABEL", - tableName: "Main", - value: "Creating secure connection", - comment: "" - ) - - case let .connected(tunnelInfo): - String( - format: NSLocalizedString( - "TUNNEL_STATE_CONNECTED_ACCESSIBILITY_LABEL", - tableName: "Main", - value: "Secure connection. Connected to %@, %@", - comment: "" - ), - tunnelInfo.location.city, - tunnelInfo.location.country - ) - - case .disconnected: - NSLocalizedString( - "TUNNEL_STATE_DISCONNECTED_ACCESSIBILITY_LABEL", - tableName: "Main", - value: "Unsecured connection", - comment: "" - ) - - case let .reconnecting(tunnelInfo): - String( - format: NSLocalizedString( - "TUNNEL_STATE_RECONNECTING_ACCESSIBILITY_LABEL", - tableName: "Main", - value: "Reconnecting to %@, %@", - comment: "" - ), - tunnelInfo.location.city, - tunnelInfo.location.country - ) - - case .waitingForConnectivity(.noConnection), .error: - NSLocalizedString( - "TUNNEL_STATE_WAITING_FOR_CONNECTIVITY_ACCESSIBILITY_LABEL", - tableName: "Main", - value: "Blocked connection", - comment: "" - ) - - case .waitingForConnectivity(.noNetwork): - NSLocalizedString( - "TUNNEL_STATE_NO_NETWORK_ACCESSIBILITY_LABEL", - tableName: "Main", - value: "No network", - comment: "" - ) - - case .disconnecting(.nothing): - NSLocalizedString( - "TUNNEL_STATE_DISCONNECTING_ACCESSIBILITY_LABEL", - tableName: "Main", - value: "Disconnecting", - comment: "" - ) - - case .disconnecting(.reconnect), .pendingReconnect: - NSLocalizedString( - "TUNNEL_STATE_PENDING_RECONNECT_ACCESSIBILITY_LABEL", - tableName: "Main", - value: "Reconnecting", - comment: "" - ) - } - } - func actionButtons(traitCollection: UITraitCollection) -> [TunnelControlActionButton] { switch (traitCollection.userInterfaceIdiom, traitCollection.horizontalSizeClass) { case (.phone, _), (.pad, .compact): diff --git a/ios/MullvadVPN/View controllers/Tunnel/TunnelViewController.swift b/ios/MullvadVPN/View controllers/Tunnel/TunnelViewController.swift index ce7b72a9f357..36f853504768 100644 --- a/ios/MullvadVPN/View controllers/Tunnel/TunnelViewController.swift +++ b/ios/MullvadVPN/View controllers/Tunnel/TunnelViewController.swift @@ -147,17 +147,17 @@ class TunnelViewController: UIViewController, RootContainment { private func updateMap(animated: Bool) { switch tunnelState { - case let .connecting(tunnelRelay): + case let .connecting(tunnelRelay, _): mapViewController.removeLocationMarker() contentView.setAnimatingActivity(true) mapViewController.setCenter(tunnelRelay?.location.geoCoordinate, animated: animated) - case let .reconnecting(tunnelRelay), let .negotiatingPostQuantumKey(tunnelRelay, _): + case let .reconnecting(tunnelRelay, _), let .negotiatingPostQuantumKey(tunnelRelay, _): mapViewController.removeLocationMarker() contentView.setAnimatingActivity(true) mapViewController.setCenter(tunnelRelay.location.geoCoordinate, animated: animated) - case let .connected(tunnelRelay): + case let .connected(tunnelRelay, _): let center = tunnelRelay.location.geoCoordinate mapViewController.setCenter(center, animated: animated) { self.contentView.setAnimatingActivity(false) diff --git a/ios/MullvadVPNTests/MullvadVPN/TunnelManager/TunnelStateTests.swift b/ios/MullvadVPNTests/MullvadVPN/TunnelManager/TunnelStateTests.swift new file mode 100644 index 000000000000..9a707fb30d3f --- /dev/null +++ b/ios/MullvadVPNTests/MullvadVPN/TunnelManager/TunnelStateTests.swift @@ -0,0 +1,139 @@ +// +// TunnelStateTests.swift +// MullvadVPNTests +// +// Created by Andrew Bulhak on 2024-05-03. +// Copyright © 2024 Mullvad VPN AB. All rights reserved. +// + +import MullvadTypes +import PacketTunnelCore +import XCTest + +final class TunnelStateTests: XCTestCase { + let arbitrarySelectedRelay = SelectedRelay( + endpoint: MullvadEndpoint( + ipv4Relay: IPv4Endpoint(ip: .any, port: 0), + ipv4Gateway: .any, + ipv6Gateway: .any, + publicKey: Data() + ), + hostname: "hostname-goes-here", + location: Location(country: "country", countryCode: "", city: "city", cityCode: "", latitude: 0, longitude: 0), + retryAttempts: 0 + ) + + // MARK: description + + func testDescription_Connecting_NoRelay() { + XCTAssertEqual( + TunnelState.connecting(nil, isPostQuantum: false).description, + "connecting, fetching relay" + ) + + XCTAssertEqual( + TunnelState.connecting(nil, isPostQuantum: true).description, + "connecting (PQ), fetching relay" + ) + } + + func testDescription_Connecting_WithRelay() { + XCTAssertEqual( + TunnelState.connecting(arbitrarySelectedRelay, isPostQuantum: false).description, + "connecting to hostname-goes-here" + ) + + XCTAssertEqual( + TunnelState.connecting(arbitrarySelectedRelay, isPostQuantum: true).description, + "connecting (PQ) to hostname-goes-here" + ) + } + + func testDescription_Connected() { + XCTAssertEqual( + TunnelState.connected(arbitrarySelectedRelay, isPostQuantum: false).description, + "connected to hostname-goes-here" + ) + + XCTAssertEqual( + TunnelState.connected(arbitrarySelectedRelay, isPostQuantum: true).description, + "connected (PQ) to hostname-goes-here" + ) + } + + // MARK: localizedTitleForSecureLabel + + func testLocalizedTitleForSecureLabel_Connecting() { + XCTAssertEqual( + TunnelState.connecting(nil, isPostQuantum: false).localizedTitleForSecureLabel, + "Creating secure connection" + ) + + XCTAssertEqual( + TunnelState.connecting(nil, isPostQuantum: true).localizedTitleForSecureLabel, + "Creating quantum secure connection" + ) + } + + func testLocalizedTitleForSecureLabel_Reconnecting() { + XCTAssertEqual( + TunnelState.reconnecting(arbitrarySelectedRelay, isPostQuantum: false).localizedTitleForSecureLabel, + "Creating secure connection" + ) + + XCTAssertEqual( + TunnelState.reconnecting(arbitrarySelectedRelay, isPostQuantum: true).localizedTitleForSecureLabel, + "Creating quantum secure connection" + ) + } + + func testLocalizedTitleForSecureLabel_Connected() { + XCTAssertEqual( + TunnelState.connected(arbitrarySelectedRelay, isPostQuantum: false).localizedTitleForSecureLabel, + "Secure connection" + ) + + XCTAssertEqual( + TunnelState.connected(arbitrarySelectedRelay, isPostQuantum: true).localizedTitleForSecureLabel, + "Quantum secure connection" + ) + } + + // MARK: localizedAccessibilityLabel + + func testLocalizedAccessibilityLabel_Connecting() { + XCTAssertEqual( + TunnelState.connecting(nil, isPostQuantum: false).localizedAccessibilityLabel, + "Creating secure connection" + ) + + XCTAssertEqual( + TunnelState.connecting(nil, isPostQuantum: true).localizedAccessibilityLabel, + "Creating quantum secure connection" + ) + } + + func testLocalizedAccessibilityLabel_Reconnecting() { + XCTAssertEqual( + TunnelState.reconnecting(arbitrarySelectedRelay, isPostQuantum: false).localizedAccessibilityLabel, + "Reconnecting to city, country" + ) + + XCTAssertEqual( + TunnelState.reconnecting(arbitrarySelectedRelay, isPostQuantum: true).localizedAccessibilityLabel, + "Reconnecting to city, country" + ) + } + + func testLocalizedAccessibilityLabel_Connected() { + XCTAssertEqual( + TunnelState.connected(arbitrarySelectedRelay, isPostQuantum: false).localizedAccessibilityLabel, + "Secure connection. Connected to city, country" + ) + + XCTAssertEqual( + TunnelState.connected(arbitrarySelectedRelay, isPostQuantum: true).localizedAccessibilityLabel, + "Quantum secure connection. Connected to city, country" + ) + } +} diff --git a/ios/PacketTunnelCore/Actor/ObservedState.swift b/ios/PacketTunnelCore/Actor/ObservedState.swift index f05d68e5252c..bdb85a8e51b4 100644 --- a/ios/PacketTunnelCore/Actor/ObservedState.swift +++ b/ios/PacketTunnelCore/Actor/ObservedState.swift @@ -33,6 +33,7 @@ public struct ObservedConnectionState: Equatable, Codable { public var transportLayer: TransportLayer public var remotePort: UInt16 public var lastKeyRotation: Date? + public let isPostQuantum: Bool public var isNetworkReachable: Bool { networkReachability != .unreachable @@ -45,7 +46,8 @@ public struct ObservedConnectionState: Equatable, Codable { connectionAttemptCount: UInt, transportLayer: TransportLayer, remotePort: UInt16, - lastKeyRotation: Date? = nil + lastKeyRotation: Date? = nil, + isPostQuantum: Bool ) { self.selectedRelay = selectedRelay self.relayConstraints = relayConstraints @@ -54,6 +56,7 @@ public struct ObservedConnectionState: Equatable, Codable { self.transportLayer = transportLayer self.remotePort = remotePort self.lastKeyRotation = lastKeyRotation + self.isPostQuantum = isPostQuantum } } @@ -97,7 +100,8 @@ extension State.ConnectionData { connectionAttemptCount: connectionAttemptCount, transportLayer: transportLayer, remotePort: remotePort, - lastKeyRotation: lastKeyRotation + lastKeyRotation: lastKeyRotation, + isPostQuantum: isPostQuantum ) } } diff --git a/ios/PacketTunnelCore/Actor/PacketTunnelActor.swift b/ios/PacketTunnelCore/Actor/PacketTunnelActor.swift index 768024ab23cd..552624d504c7 100644 --- a/ios/PacketTunnelCore/Actor/PacketTunnelActor.swift +++ b/ios/PacketTunnelCore/Actor/PacketTunnelActor.swift @@ -283,7 +283,7 @@ extension PacketTunnelActor { ) async throws { let settings: Settings = try settingsReader.read() - guard settings.quantumResistance == .off || settings.quantumResistance == .automatic else { + if settings.quantumResistance.isEnabled { if let connectionState = try makeConnectionState(nextRelay: nextRelay, settings: settings, reason: reason) { let selectedEndpoint = connectionState.selectedRelay.endpoint let activeKey = activeKey(from: connectionState, in: settings) @@ -411,7 +411,8 @@ extension PacketTunnelActor { lastKeyRotation: lastKeyRotation, connectedEndpoint: selectedRelay.endpoint, transportLayer: .udp, - remotePort: selectedRelay.endpoint.ipv4Relay.port + remotePort: selectedRelay.endpoint.ipv4Relay.port, + isPostQuantum: settings.quantumResistance.isEnabled ) } @@ -449,7 +450,8 @@ extension PacketTunnelActor { lastKeyRotation: connectionState.lastKeyRotation, connectedEndpoint: obfuscatedEndpoint, transportLayer: transportLayer, - remotePort: protocolObfuscator.remotePort + remotePort: protocolObfuscator.remotePort, + isPostQuantum: connectionState.isPostQuantum ) } diff --git a/ios/PacketTunnelCore/Actor/State.swift b/ios/PacketTunnelCore/Actor/State.swift index 259993a7f8e0..f99799201cea 100644 --- a/ios/PacketTunnelCore/Actor/State.swift +++ b/ios/PacketTunnelCore/Actor/State.swift @@ -146,6 +146,9 @@ extension State { /// The remote port that was chosen to connect to `connectedEndpoint` public let remotePort: UInt16 + + /// True if post-quantum key exchange is enabled + public let isPostQuantum: Bool } /// Data associated with error state.