diff --git a/ios/CHANGELOG.md b/ios/CHANGELOG.md index a02f0cf2e2b7..c88dca7e5049 100644 --- a/ios/CHANGELOG.md +++ b/ios/CHANGELOG.md @@ -32,6 +32,7 @@ Line wrap the file at 100 chars. Th - Show disabled servers in location view. - Add ability which types of local notifications are delivered. - Remove invalid Shadowsocks ciphers. +- Remove Automatic quantum-resistant tunnel option [TunnelCrack]: https://tunnelcrack.mathyvanhoef.com/ diff --git a/ios/MullvadSettings/QuantumResistanceSettings.swift b/ios/MullvadSettings/QuantumResistanceSettings.swift index 58a2594bc1c9..0d88166672fd 100644 --- a/ios/MullvadSettings/QuantumResistanceSettings.swift +++ b/ios/MullvadSettings/QuantumResistanceSettings.swift @@ -9,14 +9,49 @@ import Foundation public enum TunnelQuantumResistance: Codable, Sendable { - case automatic case on case off + + private enum CodingKeys: String, CodingKey { + case automatic, on, off + } + + public init(from decoder: Decoder) throws { + let container = try decoder.container(keyedBy: CodingKeys.self) + if container.contains(.automatic) { + self = .on + return + } + + if container.contains(.on) { + self = .on + return + } + + if container.contains(.off) { + self = .off + return + } + + self = .on + } + + public func encode(to encoder: Encoder) throws { + var container = encoder.container(keyedBy: CodingKeys.self) + + switch self { + case .on: + try container.encode([String: String](), forKey: .on) + + case .off: + try container.encode([String: String](), forKey: .off) + } + } } public extension TunnelQuantumResistance { /// A single source of truth for whether the current state counts as on var isEnabled: Bool { - [.on, .automatic].contains(self) + self == .on } } diff --git a/ios/MullvadSettings/TunnelSettingsV3.swift b/ios/MullvadSettings/TunnelSettingsV3.swift index 46da788b5d30..90dd34398a22 100644 --- a/ios/MullvadSettings/TunnelSettingsV3.swift +++ b/ios/MullvadSettings/TunnelSettingsV3.swift @@ -34,7 +34,7 @@ public struct TunnelSettingsV3: Codable, Equatable, TunnelSettings { relayConstraints: relayConstraints, dnsSettings: dnsSettings, wireGuardObfuscation: wireGuardObfuscation, - tunnelQuantumResistance: .automatic + tunnelQuantumResistance: .on ) } diff --git a/ios/MullvadSettings/TunnelSettingsV4.swift b/ios/MullvadSettings/TunnelSettingsV4.swift index c7f2d3f5ac62..c3340e368f71 100644 --- a/ios/MullvadSettings/TunnelSettingsV4.swift +++ b/ios/MullvadSettings/TunnelSettingsV4.swift @@ -26,7 +26,7 @@ public struct TunnelSettingsV4: Codable, Equatable, TunnelSettings { relayConstraints: RelayConstraints = RelayConstraints(), dnsSettings: DNSSettings = DNSSettings(), wireGuardObfuscation: WireGuardObfuscationSettings = WireGuardObfuscationSettings(), - tunnelQuantumResistance: TunnelQuantumResistance = .automatic + tunnelQuantumResistance: TunnelQuantumResistance = .on ) { self.relayConstraints = relayConstraints self.dnsSettings = dnsSettings diff --git a/ios/MullvadSettings/TunnelSettingsV5.swift b/ios/MullvadSettings/TunnelSettingsV5.swift index cfb111757af2..eeda5c3da63e 100644 --- a/ios/MullvadSettings/TunnelSettingsV5.swift +++ b/ios/MullvadSettings/TunnelSettingsV5.swift @@ -29,7 +29,7 @@ public struct TunnelSettingsV5: Codable, Equatable, TunnelSettings { relayConstraints: RelayConstraints = RelayConstraints(), dnsSettings: DNSSettings = DNSSettings(), wireGuardObfuscation: WireGuardObfuscationSettings = WireGuardObfuscationSettings(), - tunnelQuantumResistance: TunnelQuantumResistance = .automatic, + tunnelQuantumResistance: TunnelQuantumResistance = .on, tunnelMultihopState: MultihopStateV1 = .off ) { diff --git a/ios/MullvadSettings/TunnelSettingsV6.swift b/ios/MullvadSettings/TunnelSettingsV6.swift index 7d0d5d85f3a0..22b461db869c 100644 --- a/ios/MullvadSettings/TunnelSettingsV6.swift +++ b/ios/MullvadSettings/TunnelSettingsV6.swift @@ -32,7 +32,7 @@ public struct TunnelSettingsV6: Codable, Equatable, TunnelSettings, Sendable { relayConstraints: RelayConstraints = RelayConstraints(), dnsSettings: DNSSettings = DNSSettings(), wireGuardObfuscation: WireGuardObfuscationSettings = WireGuardObfuscationSettings(), - tunnelQuantumResistance: TunnelQuantumResistance = .automatic, + tunnelQuantumResistance: TunnelQuantumResistance = .on, tunnelMultihopState: MultihopStateV1 = .off, daita: DAITASettings = DAITASettings() ) { diff --git a/ios/MullvadSettings/TunnelSettingsV7.swift b/ios/MullvadSettings/TunnelSettingsV7.swift index 03c6e5a80080..c08549e5960b 100644 --- a/ios/MullvadSettings/TunnelSettingsV7.swift +++ b/ios/MullvadSettings/TunnelSettingsV7.swift @@ -35,7 +35,7 @@ public struct TunnelSettingsV7: Codable, Equatable, TunnelSettings, Sendable { relayConstraints: RelayConstraints = RelayConstraints(), dnsSettings: DNSSettings = DNSSettings(), wireGuardObfuscation: WireGuardObfuscationSettings = WireGuardObfuscationSettings(), - tunnelQuantumResistance: TunnelQuantumResistance = .automatic, + tunnelQuantumResistance: TunnelQuantumResistance = .on, tunnelMultihopState: MultihopStateV1 = .off, daita: DAITASettings = DAITASettings(), includeAllNetworks: IncludeAllNetworksSettings = IncludeAllNetworksSettings() diff --git a/ios/MullvadSettings/TunnelSettingsV8.swift b/ios/MullvadSettings/TunnelSettingsV8.swift index 3ce982a04c04..50adce8647f9 100644 --- a/ios/MullvadSettings/TunnelSettingsV8.swift +++ b/ios/MullvadSettings/TunnelSettingsV8.swift @@ -35,7 +35,7 @@ public struct TunnelSettingsV8: Codable, Equatable, TunnelSettings, Sendable { relayConstraints: RelayConstraints = RelayConstraints(), dnsSettings: DNSSettings = DNSSettings(), wireGuardObfuscation: WireGuardObfuscationSettings = WireGuardObfuscationSettings(), - tunnelQuantumResistance: TunnelQuantumResistance = .automatic, + tunnelQuantumResistance: TunnelQuantumResistance = .on, tunnelMultihopState: MultihopStateV2 = .never, daita: DAITASettings = DAITASettings(), includeAllNetworks: IncludeAllNetworksSettings = IncludeAllNetworksSettings() diff --git a/ios/MullvadVPN/Classes/AccessbilityIdentifier.swift b/ios/MullvadVPN/Classes/AccessbilityIdentifier.swift index b6b238d15ee9..87a6da8f1619 100644 --- a/ios/MullvadVPN/Classes/AccessbilityIdentifier.swift +++ b/ios/MullvadVPN/Classes/AccessbilityIdentifier.swift @@ -257,7 +257,6 @@ public enum AccessibilityIdentifier: Equatable { case localNetworkSharingSwitch // Quantum resistance - case quantumResistanceAutomatic case quantumResistanceOff case quantumResistanceOn diff --git a/ios/MullvadVPN/View controllers/VPNSettings/VPNSettingsCellFactory.swift b/ios/MullvadVPN/View controllers/VPNSettings/VPNSettingsCellFactory.swift index c4a73e28fdb4..f83a7f52399d 100644 --- a/ios/MullvadVPN/View controllers/VPNSettings/VPNSettingsCellFactory.swift +++ b/ios/MullvadVPN/View controllers/VPNSettings/VPNSettingsCellFactory.swift @@ -175,13 +175,6 @@ final class VPNSettingsCellFactory: @preconcurrency CellFactoryProtocol { cell.setAccessibilityIdentifier(item.accessibilityIdentifier) cell.applySubCellStyling() - case .quantumResistanceAutomatic: - guard let cell = cell as? SelectableSettingsCell else { return } - - cell.titleLabel.text = NSLocalizedString("Automatic", comment: "") - cell.setAccessibilityIdentifier(item.accessibilityIdentifier) - cell.applySubCellStyling() - case .quantumResistanceOn: guard let cell = cell as? SelectableSettingsCell else { return } diff --git a/ios/MullvadVPN/View controllers/VPNSettings/VPNSettingsDataSource.swift b/ios/MullvadVPN/View controllers/VPNSettings/VPNSettingsDataSource.swift index fdd27485be36..d29c1e4869d2 100644 --- a/ios/MullvadVPN/View controllers/VPNSettings/VPNSettingsDataSource.swift +++ b/ios/MullvadVPN/View controllers/VPNSettings/VPNSettingsDataSource.swift @@ -76,7 +76,6 @@ final class VPNSettingsDataSource: UITableViewDiffableDataSource< case wireGuardObfuscationQuic case wireGuardObfuscationLwo case wireGuardObfuscationOff - case quantumResistanceAutomatic case quantumResistanceOn case quantumResistanceOff @@ -99,7 +98,7 @@ final class VPNSettingsDataSource: UITableViewDiffableDataSource< } static var quantumResistance: [Item] { - [.quantumResistanceAutomatic, .quantumResistanceOn, .quantumResistanceOff] + [.quantumResistanceOn, .quantumResistanceOff] } var accessibilityIdentifier: AccessibilityIdentifier { @@ -124,8 +123,6 @@ final class VPNSettingsDataSource: UITableViewDiffableDataSource< .wireGuardObfuscationLwo case .wireGuardObfuscationOff: .wireGuardObfuscationOff - case .quantumResistanceAutomatic: - .quantumResistanceAutomatic case .quantumResistanceOn: .quantumResistanceOn case .quantumResistanceOff: @@ -147,7 +144,7 @@ final class VPNSettingsDataSource: UITableViewDiffableDataSource< .wireGuardObfuscation case .wireGuardObfuscationUdpOverTcp, .wireGuardObfuscationShadowsocks, .wireGuardObfuscationLwo: .wireGuardObfuscationOption - case .quantumResistanceAutomatic, .quantumResistanceOn, .quantumResistanceOff: + case .quantumResistanceOn, .quantumResistanceOff: .quantumResistance } } @@ -192,7 +189,6 @@ final class VPNSettingsDataSource: UITableViewDiffableDataSource< let quantumResistanceItem: Item = switch viewModel.quantumResistance { - case .automatic: .quantumResistanceAutomatic case .off: .quantumResistanceOff case .on: .quantumResistanceOn } @@ -331,9 +327,6 @@ final class VPNSettingsDataSource: UITableViewDiffableDataSource< case .wireGuardObfuscationOff: selectObfuscationState(.off) delegate?.didUpdateTunnelSettings(TunnelSettingsUpdate.obfuscation(obfuscationSettings)) - case .quantumResistanceAutomatic: - selectQuantumResistance(.automatic) - delegate?.didUpdateTunnelSettings(TunnelSettingsUpdate.quantumResistance(viewModel.quantumResistance)) case .quantumResistanceOn: selectQuantumResistance(.on) delegate?.didUpdateTunnelSettings(TunnelSettingsUpdate.quantumResistance(viewModel.quantumResistance)) diff --git a/ios/MullvadVPNTests/MullvadSettings/MigrationManagerTests.swift b/ios/MullvadVPNTests/MullvadSettings/MigrationManagerTests.swift index d7747a727eb4..70cdfe9a5791 100644 --- a/ios/MullvadVPNTests/MullvadSettings/MigrationManagerTests.swift +++ b/ios/MullvadVPNTests/MullvadSettings/MigrationManagerTests.swift @@ -333,6 +333,59 @@ final class MigrationManagerTests: XCTestCase, @unchecked Sendable { XCTAssertFalse(settings.includeAllNetworks.localNetworkSharingIsEnabled) } + /// Migration test: ensures that previously stored settings using the removed + /// `automatic` case for `tunnelQuantumResistance` are safely mapped to `.on`. + /// Prevents crashes and guarantees consistent behavior for existing users + /// after upgrading to versions where `automatic` no longer exists. + func testTunnelQuantumResistanceMigratesAutomaticToOn() throws { + let oldSettingsJSON = Data( + """ + { + "version": 4, + "data": { + "relayConstraints": { + "location": {"only": ["se"]}, + "locations": {"only": {"locations": [["se"]]}}, + "entryLocations": {"only": {"locations": [["se"]]}}, + "exitLocations": {"only": {"locations": [["se"]]}}, + "port": "any", + "filter": "any" + }, + "dnsSettings": { + "blockingOptions": 0, + "enableCustomDNS": false, + "customDNSDomains": [] + }, + "wireGuardObfuscation": { + "port": 0, + "state": {"automatic": {}}, + "udpOverTcpPort": {"automatic": {}}, + "shadowsocksPort": {"automatic": {}} + }, + "tunnelQuantumResistance": {"automatic": {}} + } + } + """.utf8) + + let store = Self.store + let parser = SettingsParser(decoder: JSONDecoder(), encoder: JSONEncoder()) + let tunnelSettingsV4 = try parser.parsePayload(as: TunnelSettingsV4.self, from: oldSettingsJSON) + try write(settings: tunnelSettingsV4, version: 4, in: store) + + let successfulMigrationExpectation = expectation(description: "Successful migration") + manager.migrateSettings(store: store) { result in + if case .success = result { + successfulMigrationExpectation.fulfill() + } + } + wait(for: [successfulMigrationExpectation], timeout: .UnitTest.timeout) + + let latestSettingsData = try XCTUnwrap(store.read(key: .settings)) + let latestSettings = try parser.parsePayload(as: LatestTunnelSettings.self, from: latestSettingsData) + + XCTAssertEqual(latestSettings.tunnelQuantumResistance, .on) + } + private func migrateToLatest(_ settings: any TunnelSettings, version: SchemaVersion) throws { let store = Self.store try write(settings: settings, version: version.rawValue, in: store) diff --git a/ios/MullvadVPNTests/MullvadSettings/TunnelSettingsUpdateTests.swift b/ios/MullvadVPNTests/MullvadSettings/TunnelSettingsUpdateTests.swift index b340a835972d..0618fbb13519 100644 --- a/ios/MullvadVPNTests/MullvadSettings/TunnelSettingsUpdateTests.swift +++ b/ios/MullvadVPNTests/MullvadSettings/TunnelSettingsUpdateTests.swift @@ -119,7 +119,7 @@ final class TunnelSettingsUpdateTests: XCTestCase { XCTAssertTrue(settings.tunnelQuantumResistance.isEnabled) // When again: - update = TunnelSettingsUpdate.quantumResistance(.automatic) + update = TunnelSettingsUpdate.quantumResistance(.on) update.apply(to: &settings) // Then again: diff --git a/ios/MullvadVPNUITests/Pages/VPNSettingsPage.swift b/ios/MullvadVPNUITests/Pages/VPNSettingsPage.swift index 2719bbf12f19..a1078818e403 100644 --- a/ios/MullvadVPNUITests/Pages/VPNSettingsPage.swift +++ b/ios/MullvadVPNUITests/Pages/VPNSettingsPage.swift @@ -88,12 +88,6 @@ class VPNSettingsPage: Page { return self } - @discardableResult func tapQuantumResistantTunnelAutomaticCell() -> Self { - app.cells[AccessibilityIdentifier.quantumResistanceAutomatic] - .tap() - return self - } - @discardableResult func tapQuantumResistantTunnelOnCell() -> Self { app.cells[AccessibilityIdentifier.quantumResistanceOn] .tap() diff --git a/ios/PacketTunnelCoreTests/Mocks/SettingsReaderStub.swift b/ios/PacketTunnelCoreTests/Mocks/SettingsReaderStub.swift index 462379df0a21..f922454c911c 100644 --- a/ios/PacketTunnelCoreTests/Mocks/SettingsReaderStub.swift +++ b/ios/PacketTunnelCoreTests/Mocks/SettingsReaderStub.swift @@ -32,7 +32,7 @@ extension SettingsReaderStub { relayConstraints: RelayConstraints(), dnsSettings: DNSSettings(), wireGuardObfuscation: WireGuardObfuscationSettings(state: .off), - tunnelQuantumResistance: .automatic, + tunnelQuantumResistance: .on, tunnelMultihopState: .never, daita: DAITASettings() )