diff --git a/ios/MullvadVPN/Classes/AccessbilityIdentifier.swift b/ios/MullvadVPN/Classes/AccessbilityIdentifier.swift index 4f31461e5e70..24a87ed14f78 100644 --- a/ios/MullvadVPN/Classes/AccessbilityIdentifier.swift +++ b/ios/MullvadVPN/Classes/AccessbilityIdentifier.swift @@ -84,6 +84,11 @@ public enum AccessibilityIdentifier: String { case dnsServer case dnsServerInfo + // Quantum resistance + case quantumResistanceAutomatic + case quantumResistanceOff + case quantumResistanceOn + // Error case unknown } diff --git a/ios/MullvadVPN/TunnelManager/TunnelManager.swift b/ios/MullvadVPN/TunnelManager/TunnelManager.swift index 225c25d42216..0d9b1e80c2a0 100644 --- a/ios/MullvadVPN/TunnelManager/TunnelManager.swift +++ b/ios/MullvadVPN/TunnelManager/TunnelManager.swift @@ -536,6 +536,16 @@ final class TunnelManager: StorePaymentObserver { ) } + func setQuantumResistance(_ newSetting: TunnelQuantumResistance) { + scheduleSettingsUpdate( + taskName: "Set quantum resistance", + modificationBlock: { settings in + settings.tunnelQuantumResistance = newSetting + }, + completionHandler: nil + ) + } + func refreshRelayCacheTracker() throws { try relayCacheTracker.refreshCachedRelays() } diff --git a/ios/MullvadVPN/View controllers/Preferences/PreferencesCellFactory.swift b/ios/MullvadVPN/View controllers/Preferences/PreferencesCellFactory.swift index bf734b5f8385..8e739c33fdf9 100644 --- a/ios/MullvadVPN/View controllers/Preferences/PreferencesCellFactory.swift +++ b/ios/MullvadVPN/View controllers/Preferences/PreferencesCellFactory.swift @@ -155,6 +155,43 @@ final class PreferencesCellFactory: CellFactoryProtocol { ) cell.accessibilityIdentifier = "\(item.accessibilityIdentifier.rawValue) (\(portString))" cell.applySubCellStyling() + + #if DEBUG + case .quantumResistanceAutomatic: + guard let cell = cell as? SelectableSettingsCell else { return } + + cell.titleLabel.text = NSLocalizedString( + "QUANTUM_RESISTANCE_AUTOMATIC_LABEL", + tableName: "Preferences", + value: "Automatic", + comment: "" + ) + cell.accessibilityIdentifier = item.accessibilityIdentifier + cell.applySubCellStyling() + + case .quantumResistanceOn: + guard let cell = cell as? SelectableSettingsCell else { return } + + cell.titleLabel.text = NSLocalizedString( + "QUANTUM_RESISTANCE_ON_LABEL", + tableName: "Preferences", + value: "On", + comment: "" + ) + cell.accessibilityIdentifier = item.accessibilityIdentifier + cell.applySubCellStyling() + case .quantumResistanceOff: + guard let cell = cell as? SelectableSettingsCell else { return } + + cell.titleLabel.text = NSLocalizedString( + "QUANTUM_RESISTANCE_OFF_LABEL", + tableName: "Preferences", + value: "Off", + comment: "" + ) + cell.accessibilityIdentifier = item.accessibilityIdentifier + cell.applySubCellStyling() + #endif } } } diff --git a/ios/MullvadVPN/View controllers/Preferences/PreferencesDataSource.swift b/ios/MullvadVPN/View controllers/Preferences/PreferencesDataSource.swift index b726bded698f..b984bc1f7176 100644 --- a/ios/MullvadVPN/View controllers/Preferences/PreferencesDataSource.swift +++ b/ios/MullvadVPN/View controllers/Preferences/PreferencesDataSource.swift @@ -21,6 +21,7 @@ final class PreferencesDataSource: UITableViewDiffableDataSource< case wireGuardCustomPort case wireGuardObfuscation case wireGuardObfuscationPort + case quantumResistance var reusableViewClass: AnyClass { switch self { case .dnsSettings: @@ -33,6 +34,8 @@ final class PreferencesDataSource: UITableViewDiffableDataSource< return SelectableSettingsCell.self case .wireGuardObfuscationPort: return SelectableSettingsCell.self + case .quantumResistance: + return SelectableSettingsCell.self } } } @@ -50,6 +53,9 @@ final class PreferencesDataSource: UITableViewDiffableDataSource< case wireGuardPorts case wireGuardObfuscation case wireGuardObfuscationPort + #if DEBUG + case quantumResistance + #endif } enum Item: Hashable { @@ -60,6 +66,11 @@ final class PreferencesDataSource: UITableViewDiffableDataSource< case wireGuardObfuscationOn case wireGuardObfuscationOff case wireGuardObfuscationPort(_ port: UInt16) + #if DEBUG + case quantumResistanceAutomatic + case quantumResistanceOn + case quantumResistanceOff + #endif static var wireGuardPorts: [Item] { let defaultPorts = PreferencesViewModel.defaultWireGuardPorts.map { @@ -76,6 +87,12 @@ final class PreferencesDataSource: UITableViewDiffableDataSource< [.wireGuardObfuscationPort(0), wireGuardObfuscationPort(80), wireGuardObfuscationPort(5001)] } + #if DEBUG + static var quantumResistance: [Item] { + [.quantumResistanceAutomatic, .quantumResistanceOn, .quantumResistanceOff] + } + #endif + var accessibilityIdentifier: AccessibilityIdentifier { switch self { case .dnsSettings: @@ -92,6 +109,14 @@ final class PreferencesDataSource: UITableViewDiffableDataSource< return .wireGuardObfuscationOff case .wireGuardObfuscationPort: return .wireGuardObfuscationAutomatic + #if DEBUG + case .quantumResistanceAutomatic: + return .quantumResistanceAutomatic + case .quantumResistanceOn: + return .quantumResistanceOn + case .quantumResistanceOff: + return .quantumResistanceOff + #endif } } @@ -107,6 +132,10 @@ final class PreferencesDataSource: UITableViewDiffableDataSource< return .wireGuardObfuscation case .wireGuardObfuscationPort: return .wireGuardObfuscationPort + #if DEBUG + case .quantumResistanceAutomatic, .quantumResistanceOn, .quantumResistanceOff: + return .quantumResistance + #endif } } } @@ -129,14 +158,30 @@ final class PreferencesDataSource: UITableViewDiffableDataSource< case .off: .wireGuardObfuscationOff case .on: .wireGuardObfuscationOn } + #if DEBUG + let quantumResistanceItem: Item = switch viewModel.quantumResistance { + case .automatic: .quantumResistanceAutomatic + case .off: .quantumResistanceOff + case .on: .quantumResistanceOn + } + #endif let obfuscationPortItem: Item = .wireGuardObfuscationPort(viewModel.obfuscationPort.portValue) + #if DEBUG + return [ + wireGuardPortItem, + obfuscationStateItem, + obfuscationPortItem, + quantumResistanceItem, + ].compactMap { indexPath(for: $0) } + #else return [ - indexPath(for: wireGuardPortItem), - indexPath(for: obfuscationStateItem), - indexPath(for: obfuscationPortItem), - ].compactMap { $0 } + wireGuardPortItem, + obfuscationStateItem, + obfuscationPortItem, + ].compactMap { indexPath(for: $0) } + #endif } init(tableView: UITableView) { @@ -240,6 +285,18 @@ final class PreferencesDataSource: UITableViewDiffableDataSource< case let .wireGuardObfuscationPort(port): selectObfuscationPort(port) delegate?.didChangeViewModel(viewModel) + + #if DEBUG + case .quantumResistanceAutomatic: + selectQuantumResistance(.automatic) + delegate?.didChangeViewModel(viewModel) + case .quantumResistanceOn: + selectQuantumResistance(.on) + delegate?.didChangeViewModel(viewModel) + case .quantumResistanceOff: + selectQuantumResistance(.off) + delegate?.didChangeViewModel(viewModel) + #endif default: break } @@ -277,6 +334,12 @@ final class PreferencesDataSource: UITableViewDiffableDataSource< case .wireGuardObfuscationPort: configureObfuscationPortHeader(view) return view + #if DEBUG + case .quantumResistance: + configureQuantumResistanceHeader(view) + return view + #endif + default: return nil } @@ -459,6 +522,36 @@ final class PreferencesDataSource: UITableViewDiffableDataSource< } } + #if DEBUG + private func configureQuantumResistanceHeader(_ header: SettingsHeaderView) { + let title = NSLocalizedString( + "QUANTUM_RESISTANCE_HEADER_LABEL", + tableName: "Preferences", + value: "Quantum-resistant tunnel", + comment: "" + ) + + header.titleLabel.text = title + header.accessibilityCustomActionName = title + header.didCollapseHandler = { [weak self] header in + guard let self else { return } + + var snapshot = snapshot() + if header.isExpanded { + snapshot.deleteItems(Item.quantumResistance) + } else { + snapshot.appendItems(Item.quantumResistance, toSection: .quantumResistance) + } + header.isExpanded.toggle() + applySnapshot(snapshot, animated: true) + } + + header.infoButtonHandler = { [weak self] in + self.map { $0.delegate?.showInfo(for: .quantumResistance) } + } + } + #endif + private func selectRow(at indexPath: IndexPath?, animated: Bool = false) { tableView?.selectRow(at: indexPath, animated: animated, scrollPosition: .none) } @@ -505,6 +598,10 @@ extension PreferencesDataSource: PreferencesCellEventHandler { let selectedPort = WireGuardObfuscationPort(rawValue: port)! viewModel.setWireGuardObfuscationPort(selectedPort) } + + func selectQuantumResistance(_ state: TunnelQuantumResistance) { + viewModel.setQuantumResistance(state) + } } // swiftlint:disable:this file_length diff --git a/ios/MullvadVPN/View controllers/Preferences/PreferencesInfoButtonItem.swift b/ios/MullvadVPN/View controllers/Preferences/PreferencesInfoButtonItem.swift index 8323d3311f67..a5b5ce5521e3 100644 --- a/ios/MullvadVPN/View controllers/Preferences/PreferencesInfoButtonItem.swift +++ b/ios/MullvadVPN/View controllers/Preferences/PreferencesInfoButtonItem.swift @@ -12,4 +12,5 @@ enum PreferencesInfoButtonItem { case wireGuardPorts case wireGuardObfuscation case wireGuardObfuscationPort + case quantumResistance } diff --git a/ios/MullvadVPN/View controllers/Preferences/PreferencesInteractor.swift b/ios/MullvadVPN/View controllers/Preferences/PreferencesInteractor.swift index 96958f26740d..8efc3095369c 100644 --- a/ios/MullvadVPN/View controllers/Preferences/PreferencesInteractor.swift +++ b/ios/MullvadVPN/View controllers/Preferences/PreferencesInteractor.swift @@ -55,6 +55,10 @@ final class PreferencesInteractor { tunnelManager.setRelayConstraints(relayConstraints, completionHandler: completion) } + + func setQuantumResistance(_ newSetting: TunnelQuantumResistance) { + tunnelManager.setQuantumResistance(newSetting) + } } extension PreferencesInteractor: RelayCacheTrackerObserver { diff --git a/ios/MullvadVPN/View controllers/Preferences/PreferencesViewController.swift b/ios/MullvadVPN/View controllers/Preferences/PreferencesViewController.swift index 569a09102561..7eee1420d727 100644 --- a/ios/MullvadVPN/View controllers/Preferences/PreferencesViewController.swift +++ b/ios/MullvadVPN/View controllers/Preferences/PreferencesViewController.swift @@ -105,6 +105,7 @@ class PreferencesViewController: UITableViewController, PreferencesDataSourceDel state: viewModel.obfuscationState, port: viewModel.obfuscationPort )) + interactor.setQuantumResistance(viewModel.quantumResistance) } func showInfo(for item: PreferencesInfoButtonItem) { @@ -152,6 +153,18 @@ class PreferencesViewController: UITableViewController, PreferencesDataSourceDel comment: "" ) + case .quantumResistance: + message = NSLocalizedString( + "PREFERENCES_QUANTUM_RESISTANCE_GENERAL", + tableName: "QuantumResistance", + value: """ + This feature makes the WireGuard tunnel resistant to potential attacks from quantum computers. + It does this by performing an extra key exchange using a quantum safe algorithm and mixing the result into WireGuard’s regular encryption. + This extra step uses approximately 500 kiB of traffic every time a new tunnel is established. + """, + comment: "" + ) + default: assertionFailure("No matching InfoButtonItem") } diff --git a/ios/MullvadVPN/View controllers/Preferences/PreferencesViewModel.swift b/ios/MullvadVPN/View controllers/Preferences/PreferencesViewModel.swift index cd5345db96d3..ed8d2b6313a2 100644 --- a/ios/MullvadVPN/View controllers/Preferences/PreferencesViewModel.swift +++ b/ios/MullvadVPN/View controllers/Preferences/PreferencesViewModel.swift @@ -98,6 +98,8 @@ struct PreferencesViewModel: Equatable { private(set) var obfuscationState: WireGuardObfuscationState private(set) var obfuscationPort: WireGuardObfuscationPort + private(set) var quantumResistance: TunnelQuantumResistance + static let defaultWireGuardPorts: [UInt16] = [51820, 53] mutating func setBlockAdvertising(_ newValue: Bool) { @@ -148,6 +150,10 @@ struct PreferencesViewModel: Equatable { obfuscationPort = newPort } + mutating func setQuantumResistance(_ newState: TunnelQuantumResistance) { + quantumResistance = newState + } + /// Precondition for enabling Custom DNS. var customDNSPrecondition: CustomDNSPrecondition { if blockAdvertising || blockTracking || blockMalware || @@ -193,6 +199,8 @@ struct PreferencesViewModel: Equatable { obfuscationState = tunnelSettings.wireGuardObfuscation.state obfuscationPort = tunnelSettings.wireGuardObfuscation.port + + quantumResistance = tunnelSettings.tunnelQuantumResistance } /// Produce merged view model keeping entry `identifier` for matching DNS entries.