diff options
| author | Andrej Mihajlov <and@mullvad.net> | 2022-02-02 17:38:55 +0100 |
|---|---|---|
| committer | Andrej Mihajlov <and@mullvad.net> | 2022-02-02 17:38:55 +0100 |
| commit | 38a9fe03fcf162088fe87d89b32ce1a07da85d62 (patch) | |
| tree | 56c435cb02b8f384381b3aa5c7d95a05be3fb2c7 | |
| parent | 8255584218ce8e2f4f96ccbaa3e8833a16020ba5 (diff) | |
| parent | 99cc94d4ff8ca807c6341b61d12c1b9c04d482c3 (diff) | |
| download | mullvadvpn-38a9fe03fcf162088fe87d89b32ce1a07da85d62.tar.xz mullvadvpn-38a9fe03fcf162088fe87d89b32ce1a07da85d62.zip | |
Merge branch 'store-next-key'
| -rw-r--r-- | ios/CHANGELOG.md | 2 | ||||
| -rw-r--r-- | ios/MullvadVPN.xcodeproj/project.pbxproj | 12 | ||||
| -rw-r--r-- | ios/MullvadVPN/DisplayChainedError.swift | 2 | ||||
| -rw-r--r-- | ios/MullvadVPN/Operations/OperationCompletion.swift | 11 | ||||
| -rw-r--r-- | ios/MullvadVPN/TunnelManager/ReloadTunnelOperation.swift | 2 | ||||
| -rw-r--r-- | ios/MullvadVPN/TunnelManager/ReplaceKeyOperation.swift | 214 | ||||
| -rw-r--r-- | ios/MullvadVPN/TunnelManager/RotatePrivateKeyOperation.swift | 123 | ||||
| -rw-r--r-- | ios/MullvadVPN/TunnelManager/SetAccountOperation.swift | 147 | ||||
| -rw-r--r-- | ios/MullvadVPN/TunnelManager/SetTunnelSettingsOperation.swift | 2 | ||||
| -rw-r--r-- | ios/MullvadVPN/TunnelManager/StartTunnelOperation.swift | 2 | ||||
| -rw-r--r-- | ios/MullvadVPN/TunnelManager/StopTunnelOperation.swift | 2 | ||||
| -rw-r--r-- | ios/MullvadVPN/TunnelManager/TunnelManager.swift | 22 | ||||
| -rw-r--r-- | ios/MullvadVPN/TunnelManager/TunnelManagerError.swift | 44 | ||||
| -rw-r--r-- | ios/MullvadVPN/TunnelSettings.swift | 11 |
14 files changed, 373 insertions, 223 deletions
diff --git a/ios/CHANGELOG.md b/ios/CHANGELOG.md index 92faecd5af..c7d3d9775b 100644 --- a/ios/CHANGELOG.md +++ b/ios/CHANGELOG.md @@ -29,6 +29,8 @@ Line wrap the file at 100 chars. Th ### Fixed - Fix crash occurring after completing in-app purchase. - Fix error when changing relays while in airplane mode. +- Prevent key rotation from clogging the server key list by storing the next key and reusing it + until receiving the successful response from Mullvad API. Add up to three retry attempts. ### Changed - Increase hit area of settings (cog) button. diff --git a/ios/MullvadVPN.xcodeproj/project.pbxproj b/ios/MullvadVPN.xcodeproj/project.pbxproj index c4410329e7..06d9d34cc6 100644 --- a/ios/MullvadVPN.xcodeproj/project.pbxproj +++ b/ios/MullvadVPN.xcodeproj/project.pbxproj @@ -283,8 +283,7 @@ 58F2E144276A13F300A79513 /* StartTunnelOperation.swift in Sources */ = {isa = PBXBuildFile; fileRef = 58F2E143276A13F300A79513 /* StartTunnelOperation.swift */; }; 58F2E146276A2C9900A79513 /* StopTunnelOperation.swift in Sources */ = {isa = PBXBuildFile; fileRef = 58F2E145276A2C9900A79513 /* StopTunnelOperation.swift */; }; 58F2E148276A307400A79513 /* MapConnectionStatusOperation.swift in Sources */ = {isa = PBXBuildFile; fileRef = 58F2E147276A307400A79513 /* MapConnectionStatusOperation.swift */; }; - 58F2E14A276A43AA00A79513 /* RegenerateTunnelPrivateKeyOperation.swift in Sources */ = {isa = PBXBuildFile; fileRef = 58F2E149276A43AA00A79513 /* RegenerateTunnelPrivateKeyOperation.swift */; }; - 58F2E14C276A61C000A79513 /* RotatePrivateKeyOperation.swift in Sources */ = {isa = PBXBuildFile; fileRef = 58F2E14B276A61C000A79513 /* RotatePrivateKeyOperation.swift */; }; + 58F2E14C276A61C000A79513 /* ReplaceKeyOperation.swift in Sources */ = {isa = PBXBuildFile; fileRef = 58F2E14B276A61C000A79513 /* ReplaceKeyOperation.swift */; }; 58F3C0A4249CB069003E76BE /* HeaderBarView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 58F3C0A3249CB069003E76BE /* HeaderBarView.swift */; }; 58F3C0A624A50157003E76BE /* relays.json in Resources */ = {isa = PBXBuildFile; fileRef = 58F3C0A524A50155003E76BE /* relays.json */; }; 58F3C0A724A50C02003E76BE /* relays.json in Resources */ = {isa = PBXBuildFile; fileRef = 58F3C0A524A50155003E76BE /* relays.json */; }; @@ -572,8 +571,7 @@ 58F2E143276A13F300A79513 /* StartTunnelOperation.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StartTunnelOperation.swift; sourceTree = "<group>"; }; 58F2E145276A2C9900A79513 /* StopTunnelOperation.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StopTunnelOperation.swift; sourceTree = "<group>"; }; 58F2E147276A307400A79513 /* MapConnectionStatusOperation.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MapConnectionStatusOperation.swift; sourceTree = "<group>"; }; - 58F2E149276A43AA00A79513 /* RegenerateTunnelPrivateKeyOperation.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RegenerateTunnelPrivateKeyOperation.swift; sourceTree = "<group>"; }; - 58F2E14B276A61C000A79513 /* RotatePrivateKeyOperation.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RotatePrivateKeyOperation.swift; sourceTree = "<group>"; }; + 58F2E14B276A61C000A79513 /* ReplaceKeyOperation.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ReplaceKeyOperation.swift; sourceTree = "<group>"; }; 58F3C0A3249CB069003E76BE /* HeaderBarView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HeaderBarView.swift; sourceTree = "<group>"; }; 58F3C0A524A50155003E76BE /* relays.json */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.json; path = relays.json; sourceTree = "<group>"; }; 58F558DC2695B85E00F630D0 /* en */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = en; path = en.lproj/Consent.strings; sourceTree = "<group>"; }; @@ -717,8 +715,7 @@ 58F2E143276A13F300A79513 /* StartTunnelOperation.swift */, 58F2E145276A2C9900A79513 /* StopTunnelOperation.swift */, 58F2E147276A307400A79513 /* MapConnectionStatusOperation.swift */, - 58F2E149276A43AA00A79513 /* RegenerateTunnelPrivateKeyOperation.swift */, - 58F2E14B276A61C000A79513 /* RotatePrivateKeyOperation.swift */, + 58F2E14B276A61C000A79513 /* ReplaceKeyOperation.swift */, 588527B5276B58B300BAA373 /* SetTunnelSettingsOperation.swift */, ); path = TunnelManager; @@ -1403,7 +1400,7 @@ 5806766C27048E3E00C858CB /* AnyOptional.swift in Sources */, 58ACF64D26567A5000ACE4B7 /* CustomSwitch.swift in Sources */, 5850367F25A481D800A43E93 /* IPAddressRange+Codable.swift in Sources */, - 58F2E14C276A61C000A79513 /* RotatePrivateKeyOperation.swift in Sources */, + 58F2E14C276A61C000A79513 /* ReplaceKeyOperation.swift in Sources */, 5871FB96254ADE4E0051A0A4 /* ConsolidatedApplicationLog.swift in Sources */, 58FEEB58260B662E00A621A8 /* AutomaticKeyboardResponder.swift in Sources */, 5846227326E22A160035F7C2 /* AppStorePaymentObserver.swift in Sources */, @@ -1533,7 +1530,6 @@ 58561C99239A5D1500BD6B5E /* IPEndpoint.swift in Sources */, 58FD5BF22424F7D700112C88 /* UserInterfaceInteractionRestriction.swift in Sources */, 5811DE50239014550011EB53 /* NEVPNStatus+Debug.swift in Sources */, - 58F2E14A276A43AA00A79513 /* RegenerateTunnelPrivateKeyOperation.swift in Sources */, 58C3A4B222456F1B00340BDB /* AccountInputGroupView.swift in Sources */, 58F840B22464491D0044E708 /* ChainedError.swift in Sources */, 58FAEDFF24533A7000CB0F5B /* KeychainReturn.swift in Sources */, diff --git a/ios/MullvadVPN/DisplayChainedError.swift b/ios/MullvadVPN/DisplayChainedError.swift index ce03bf5348..8baef34ece 100644 --- a/ios/MullvadVPN/DisplayChainedError.swift +++ b/ios/MullvadVPN/DisplayChainedError.swift @@ -236,7 +236,7 @@ extension TunnelManager.Error: DisplayChainedError { // This error is never displayed anywhere return nil - case .missingAccount: + case .unsetAccount: return NSLocalizedString( "MISSING_ACCOUNT_INTERNAL_ERROR", tableName: "TunnelManager", diff --git a/ios/MullvadVPN/Operations/OperationCompletion.swift b/ios/MullvadVPN/Operations/OperationCompletion.swift index 23d206d32d..21cb92e169 100644 --- a/ios/MullvadVPN/Operations/OperationCompletion.swift +++ b/ios/MullvadVPN/Operations/OperationCompletion.swift @@ -31,6 +31,17 @@ enum OperationCompletion<Success, Failure: Error> { } } + func map<NewSuccess>(_ block: (Success) -> NewSuccess) -> OperationCompletion<NewSuccess, Failure> { + switch self { + case .success(let value): + return .success(block(value)) + case .failure(let error): + return .failure(error) + case .cancelled: + return .cancelled + } + } + func mapError<NewFailure: Error>(_ block: (Failure) -> NewFailure) -> OperationCompletion<Success, NewFailure> { switch self { case .success(let value): diff --git a/ios/MullvadVPN/TunnelManager/ReloadTunnelOperation.swift b/ios/MullvadVPN/TunnelManager/ReloadTunnelOperation.swift index a520244e0f..3758c9e4e9 100644 --- a/ios/MullvadVPN/TunnelManager/ReloadTunnelOperation.swift +++ b/ios/MullvadVPN/TunnelManager/ReloadTunnelOperation.swift @@ -30,7 +30,7 @@ class ReloadTunnelOperation: AsyncOperation { } guard let tunnelProvider = self.state.tunnelProvider else { - self.completeOperation(completion: .failure(.missingAccount)) + self.completeOperation(completion: .failure(.unsetAccount)) return } diff --git a/ios/MullvadVPN/TunnelManager/ReplaceKeyOperation.swift b/ios/MullvadVPN/TunnelManager/ReplaceKeyOperation.swift new file mode 100644 index 0000000000..e99591c217 --- /dev/null +++ b/ios/MullvadVPN/TunnelManager/ReplaceKeyOperation.swift @@ -0,0 +1,214 @@ +// +// ReplaceKeyOperation.swift +// MullvadVPN +// +// Created by pronebird on 15/12/2021. +// Copyright © 2021 Mullvad VPN AB. All rights reserved. +// + +import Foundation +import Logging + +class ReplaceKeyOperation: AsyncOperation { + typealias CompletionHandler = (OperationCompletion<TunnelManager.KeyRotationResult, TunnelManager.Error>) -> Void + + private let queue: DispatchQueue + private let state: TunnelManager.State + + private let restClient: REST.Client + private var restRequest: Cancellable? + + private let rotationInterval: TimeInterval? + private var completionHandler: CompletionHandler? + + private let logger = Logger(label: "TunnelManager.ReplaceKeyOperation") + + class func operationForKeyRotation( + queue: DispatchQueue, + state: TunnelManager.State, + restClient: REST.Client, + rotationInterval: TimeInterval, + completionHandler: @escaping CompletionHandler + ) -> ReplaceKeyOperation { + return ReplaceKeyOperation( + queue: queue, + state: state, + restClient: restClient, + rotationInterval: rotationInterval, + completionHandler: completionHandler + ) + } + + class func operationForKeyRegeneration( + queue: DispatchQueue, + state: TunnelManager.State, + restClient: REST.Client, + completionHandler: @escaping (OperationCompletion<(), TunnelManager.Error>) -> Void + ) -> ReplaceKeyOperation { + return ReplaceKeyOperation( + queue: queue, + state: state, + restClient: restClient, + rotationInterval: nil + ) { completion in + let mappedCompletion = completion.map { keyRotationResult -> () in + switch keyRotationResult { + case .finished: + return () + case .throttled: + fatalError("ReplaceKeyOperation.operationForKeyRegeneration() must never recieve throttled!") + } + } + + completionHandler(mappedCompletion) + } + } + + private init( + queue: DispatchQueue, + state: TunnelManager.State, + restClient: REST.Client, + rotationInterval: TimeInterval?, + completionHandler: @escaping CompletionHandler + ) { + self.queue = queue + self.state = state + + self.restClient = restClient + self.rotationInterval = rotationInterval + + self.completionHandler = completionHandler + } + + override func main() { + queue.async { + self.execute { completion in + self.completionHandler?(completion) + self.completionHandler = nil + + self.finish() + } + } + } + + override func cancel() { + super.cancel() + + queue.async { + self.restRequest?.cancel() + } + } + + private func execute(completionHandler: @escaping CompletionHandler) { + guard !isCancelled else { + completionHandler(.cancelled) + return + } + + guard let tunnelInfo = state.tunnelInfo else { + completionHandler(.failure(.unsetAccount)) + return + } + + if let rotationInterval = rotationInterval { + let creationDate = tunnelInfo.tunnelSettings.interface.privateKey.creationDate + let nextRotationDate = creationDate.addingTimeInterval(rotationInterval) + + if nextRotationDate > Date() { + logger.debug("Throttle private key rotation.") + + completionHandler(.success(.throttled(creationDate))) + return + } else { + logger.debug("Private key is old enough, rotate right away.") + } + } else { + logger.debug("Rotate private key right away.") + } + + let newPrivateKey: PrivateKeyWithMetadata + let oldPublicKey = tunnelInfo.tunnelSettings.interface.publicKey + + if let nextPrivateKey = tunnelInfo.tunnelSettings.interface.nextPrivateKey { + newPrivateKey = nextPrivateKey + + logger.debug("Next private key is already created.") + } else { + newPrivateKey = PrivateKeyWithMetadata() + + logger.debug("Create next private key.") + + let saveResult = TunnelSettingsManager.update(searchTerm: .accountToken(tunnelInfo.token)) { newTunnelSettings in + newTunnelSettings.interface.nextPrivateKey = newPrivateKey + } + + switch saveResult { + case .success(let newTunnelSettings): + logger.debug("Saved next private key.") + + state.tunnelInfo?.tunnelSettings = newTunnelSettings + + case .failure(let error): + logger.error(chainedError: error, message: "Failed to save next private key.") + + completionHandler(.failure(.updateTunnelSettings(error))) + return + } + } + + logger.debug("Replacing old key with new key on server...") + + let requestAdapter = self.restClient.replaceWireguardKey( + token: tunnelInfo.token, + oldPublicKey: oldPublicKey, + newPublicKey: newPrivateKey.publicKey + ) + + restRequest = requestAdapter.execute(retryStrategy: .default) { result in + self.queue.async { + self.didReceiveResponse( + result: result, + accountToken: tunnelInfo.token, + newPrivateKey: newPrivateKey, + completionHandler: completionHandler + ) + } + } + } + + private func didReceiveResponse(result: Result<REST.WireguardAddressesResponse, REST.Error>, accountToken: String, newPrivateKey: PrivateKeyWithMetadata, completionHandler: @escaping CompletionHandler) { + switch result { + case .success(let associatedAddresses): + logger.debug("Replaced old key with new key on server.") + + let saveResult = TunnelSettingsManager.update(searchTerm: .accountToken(accountToken)) { newTunnelSettings in + newTunnelSettings.interface.privateKey = newPrivateKey + newTunnelSettings.interface.nextPrivateKey = nil + + newTunnelSettings.interface.addresses = [ + associatedAddresses.ipv4Address, + associatedAddresses.ipv6Address + ] + } + + switch saveResult { + case .success(let newTunnelSettings): + logger.debug("Saved associated addresses.") + + state.tunnelInfo?.tunnelSettings = newTunnelSettings + + completionHandler(.success(.finished)) + + case .failure(let error): + logger.error(chainedError: error, message: "Failed to save associated addresses.") + + completionHandler(.failure(.updateTunnelSettings(error))) + } + + case .failure(let restError): + logger.error(chainedError: restError, message: "Failed to replace old key with new key on server.") + + completionHandler(.failure(.replaceWireguardKey(restError))) + } + } +} diff --git a/ios/MullvadVPN/TunnelManager/RotatePrivateKeyOperation.swift b/ios/MullvadVPN/TunnelManager/RotatePrivateKeyOperation.swift deleted file mode 100644 index 1ec6d5cc2a..0000000000 --- a/ios/MullvadVPN/TunnelManager/RotatePrivateKeyOperation.swift +++ /dev/null @@ -1,123 +0,0 @@ -// -// RotatePrivateKeyOperation.swift -// MullvadVPN -// -// Created by pronebird on 15/12/2021. -// Copyright © 2021 Mullvad VPN AB. All rights reserved. -// - -import Foundation - -class RotatePrivateKeyOperation: AsyncOperation { - typealias CompletionHandler = (OperationCompletion<TunnelManager.KeyRotationResult, TunnelManager.Error>) -> Void - - private let queue: DispatchQueue - private let state: TunnelManager.State - private let restClient: REST.Client - private let rotationInterval: TimeInterval - private var completionHandler: CompletionHandler? - private var restRequest: Cancellable? - - init(queue: DispatchQueue, - state: TunnelManager.State, - restClient: REST.Client, - rotationInterval: TimeInterval, - completionHandler: @escaping CompletionHandler) - { - self.queue = queue - self.state = state - self.restClient = restClient - self.rotationInterval = rotationInterval - self.completionHandler = completionHandler - } - - override func main() { - queue.async { - self.execute { completion in - self.completionHandler?(completion) - self.completionHandler = nil - - self.finish() - } - } - } - - override func cancel() { - super.cancel() - - queue.async { - self.restRequest?.cancel() - } - } - - private func execute(completionHandler: @escaping CompletionHandler) { - guard !isCancelled else { - completionHandler(.cancelled) - return - } - - guard let tunnelInfo = state.tunnelInfo else { - completionHandler(.failure(.missingAccount)) - return - } - - let creationDate = tunnelInfo.tunnelSettings.interface.privateKey.creationDate - let timeInterval = Date().timeIntervalSince(creationDate) - - guard timeInterval >= rotationInterval else { - completionHandler(.success(.throttled(creationDate))) - return - } - - let newPrivateKey = PrivateKeyWithMetadata() - let oldPublicKey = tunnelInfo.tunnelSettings.interface.publicKey - - let requestAdapter = self.restClient.replaceWireguardKey( - token: tunnelInfo.token, - oldPublicKey: oldPublicKey, - newPublicKey: newPrivateKey.publicKey - ) - - restRequest = requestAdapter.execute(retryStrategy: .default) { result in - self.queue.async { - self.didRotatePrivateKey( - result: result, - accountToken: tunnelInfo.token, - newPrivateKey: newPrivateKey, - completionHandler: completionHandler - ) - } - } - } - - private func didRotatePrivateKey(result: Result<REST.WireguardAddressesResponse, REST.Error>, accountToken: String, newPrivateKey: PrivateKeyWithMetadata, completionHandler: @escaping CompletionHandler) { - let saveResult = Self.handleResponse(accountToken: accountToken, newPrivateKey: newPrivateKey, result: result) - - switch saveResult { - case .success(let tunnelSettings): - state.tunnelInfo?.tunnelSettings = tunnelSettings - - completionHandler(.success(.finished)) - - case .failure(let error): - completionHandler(.failure(error)) - } - } - - private class func handleResponse(accountToken: String, newPrivateKey: PrivateKeyWithMetadata, result: Result<REST.WireguardAddressesResponse, REST.Error>) -> Result<TunnelSettings, TunnelManager.Error> { - return result.flatMapError { restError in - return .failure(.replaceWireguardKey(restError)) - } - .flatMap { associatedAddresses in - return TunnelSettingsManager.update(searchTerm: .accountToken(accountToken)) { newTunnelSettings in - newTunnelSettings.interface.privateKey = newPrivateKey - newTunnelSettings.interface.addresses = [ - associatedAddresses.ipv4Address, - associatedAddresses.ipv6Address - ] - }.mapError { error -> TunnelManager.Error in - return .updateTunnelSettings(error) - } - } - } -} diff --git a/ios/MullvadVPN/TunnelManager/SetAccountOperation.swift b/ios/MullvadVPN/TunnelManager/SetAccountOperation.swift index 27843d3442..f20986d118 100644 --- a/ios/MullvadVPN/TunnelManager/SetAccountOperation.swift +++ b/ios/MullvadVPN/TunnelManager/SetAccountOperation.swift @@ -54,10 +54,11 @@ class SetAccountOperation: AsyncOperation { if let tunnelInfo = state.tunnelInfo, tunnelInfo.token != accountToken { let currentAccountToken = tunnelInfo.token let currentPublicKey = tunnelInfo.tunnelSettings.interface.publicKey + let nextPublicKey = tunnelInfo.tunnelSettings.interface.nextPrivateKey?.publicKey logger.debug("Unset current account token.") - deleteOldAccount(accountToken: currentAccountToken, publicKey: currentPublicKey) { + deleteOldAccount(accountToken: currentAccountToken, currentPublicKey: currentPublicKey, nextPublicKey: nextPublicKey) { self.setNewAccount(completionHandler: completionHandler) } } else { @@ -78,8 +79,16 @@ class SetAccountOperation: AsyncOperation { case .success(let tunnelSettings): let interfaceSettings = tunnelSettings.interface - // Push key if interface addresses were not received yet - if interfaceSettings.addresses.isEmpty { + if let newPrivateKey = interfaceSettings.nextPrivateKey { + // Replace key if key rotation had failed. + replaceOldAccountKey( + accountToken: accountToken, + oldPrivateKey: interfaceSettings.privateKey, + newPrivateKey: newPrivateKey, + completionHandler: completionHandler + ) + } else if interfaceSettings.addresses.isEmpty { + // Push key if interface addresses were not received yet pushNewAccountKey( accountToken: accountToken, publicKey: interfaceSettings.publicKey, @@ -117,28 +126,40 @@ class SetAccountOperation: AsyncOperation { } } - private func deleteOldAccount(accountToken: String, publicKey: PublicKey, completionHandler: @escaping () -> Void) { - _ = REST.Client.shared.deleteWireguardKey(token: accountToken, publicKey: publicKey) - .execute(retryStrategy: .default) { result in - self.queue.async { - self.didDeleteOldAccountKey(result: result, accountToken: accountToken, completionHandler: completionHandler) - } - } - } + private func deleteOldAccount(accountToken: String, currentPublicKey: PublicKey, nextPublicKey: PublicKey?, completionHandler: @escaping () -> Void) { + let dispatchGroup = DispatchGroup() - private func didDeleteOldAccountKey(result: Result<(), REST.Error>, accountToken: String, completionHandler: @escaping () -> Void) { - switch result { - case .success: - logger.info("Removed old key from server.") + let keysToDelete = [currentPublicKey, nextPublicKey].compactMap { $0 } - case .failure(let error): - if case .server(.pubKeyNotFound) = error { - logger.debug("Old key was not found on server.") - } else { - logger.error(chainedError: error, message: "Failed to delete old key on server.") - } + logger.debug("Deleting \(keysToDelete.count) key(s) from old account.") + + for (index, publicKey) in keysToDelete.enumerated() { + dispatchGroup.enter() + _ = REST.Client.shared.deleteWireguardKey(token: accountToken, publicKey: publicKey) + .execute(retryStrategy: .default) { result in + self.queue.async { + switch result { + case .success: + self.logger.info("Removed old key (\(index)) from server.") + + case .failure(.server(.pubKeyNotFound)): + self.logger.debug("Old key (\(index)) was not found on server.") + + case .failure(let error): + self.logger.error(chainedError: error, message: "Failed to delete old key (\(index)) on server.") + } + + dispatchGroup.leave() + } + } } + dispatchGroup.notify(queue: queue) { + self.deleteKeychainEntryAndVPNConfiguration(accountToken: accountToken, completionHandler: completionHandler) + } + } + + private func deleteKeychainEntryAndVPNConfiguration(accountToken: String, completionHandler: @escaping () -> Void) { // Tell the caller to unsubscribe from VPN status notifications. willDeleteVPNConfigurationHandler?() willDeleteVPNConfigurationHandler = nil @@ -169,65 +190,87 @@ class SetAccountOperation: AsyncOperation { // Remove VPN configuration tunnelProvider.removeFromPreferences { error in self.queue.async { + // Ignore error but log it if let error = error { - // Ignore error but log it self.logger.error( chainedError: AnyChainedError(error), message: "Failed to remove VPN configuration." ) - } else { - self.state.setTunnelProvider(nil, shouldRefreshTunnelState: false) } + self.state.setTunnelProvider(nil, shouldRefreshTunnelState: false) + completionHandler() } } } - private func pushNewAccountKey(accountToken: String, publicKey: PublicKey, completionHandler: @escaping CompletionHandler) { - _ = restClient.pushWireguardKey(token: accountToken, publicKey: publicKey) + private func replaceOldAccountKey(accountToken: String, oldPrivateKey: PrivateKeyWithMetadata, newPrivateKey: PrivateKeyWithMetadata, completionHandler: @escaping CompletionHandler) { + _ = restClient.replaceWireguardKey(token: accountToken, oldPublicKey: oldPrivateKey.publicKey, newPublicKey: newPrivateKey.publicKey) .execute(retryStrategy: .default) { result in self.queue.async { - self.didPushNewAccountKey(result: result, accountToken: accountToken, completionHandler: completionHandler) + switch result { + case .success(let associatedAddresses): + self.logger.debug("Replaced old key with new key on server.") + + self.saveAssociatedAddresses(associatedAddresses, accountToken: accountToken, newPrivateKey: newPrivateKey, completionHandler: completionHandler) + + case .failure(let error): + self.logger.error(chainedError: error, message: "Failed to replace old key with new key on server.") + + completionHandler(.failure(.replaceWireguardKey(error))) + } } } } - private func didPushNewAccountKey(result: Result<REST.WireguardAddressesResponse, REST.Error>, accountToken: String, completionHandler: @escaping (OperationCompletion<(), TunnelManager.Error>) -> Void) { - switch result { - case .success(let associatedAddresses): - logger.debug("Pushed new key to server.") + private func pushNewAccountKey(accountToken: String, publicKey: PublicKey, completionHandler: @escaping CompletionHandler) { + _ = restClient.pushWireguardKey(token: accountToken, publicKey: publicKey) + .execute(retryStrategy: .default) { result in + self.queue.async { + switch result { + case .success(let associatedAddresses): + self.logger.debug("Pushed new key to server.") - let saveSettingsResult = TunnelSettingsManager.update(searchTerm: .accountToken(accountToken)) { tunnelSettings in - tunnelSettings.interface.addresses = [ - associatedAddresses.ipv4Address, - associatedAddresses.ipv6Address - ] - } + self.saveAssociatedAddresses(associatedAddresses, accountToken: accountToken, newPrivateKey: nil, completionHandler: completionHandler) - switch saveSettingsResult { - case .success(let newTunnelSettings): - logger.debug("Saved associated addresses.") + case .failure(let error): + self.logger.error(chainedError: error, message: "Failed to push new key to server.") - let tunnelInfo = TunnelInfo( - token: accountToken, - tunnelSettings: newTunnelSettings - ) + completionHandler(.failure(.pushWireguardKey(error))) + } + } + } + } - state.tunnelInfo = tunnelInfo + private func saveAssociatedAddresses(_ associatedAddresses: REST.WireguardAddressesResponse, accountToken: String, newPrivateKey: PrivateKeyWithMetadata?, completionHandler: @escaping (OperationCompletion<(), TunnelManager.Error>) -> Void) { + let saveResult = TunnelSettingsManager.update(searchTerm: .accountToken(accountToken)) { tunnelSettings in + tunnelSettings.interface.addresses = [ + associatedAddresses.ipv4Address, + associatedAddresses.ipv6Address + ] - completionHandler(.success(())) + if let newPrivateKey = newPrivateKey { + tunnelSettings.interface.privateKey = newPrivateKey + tunnelSettings.interface.nextPrivateKey = nil + } + } - case .failure(let error): - logger.error(chainedError: error, message: "Failed to save associated addresses.") + switch saveResult { + case .success(let newTunnelSettings): + logger.debug("Saved associated addresses.") - completionHandler(.failure(.updateTunnelSettings(error))) - } + state.tunnelInfo = TunnelInfo( + token: accountToken, + tunnelSettings: newTunnelSettings + ) + + completionHandler(.success(())) case .failure(let error): - logger.error(chainedError: error, message: "Failed to push new key to server.") + logger.error(chainedError: error, message: "Failed to save associated addresses.") - completionHandler(.failure(.pushWireguardKey(error))) + completionHandler(.failure(.updateTunnelSettings(error))) } } } diff --git a/ios/MullvadVPN/TunnelManager/SetTunnelSettingsOperation.swift b/ios/MullvadVPN/TunnelManager/SetTunnelSettingsOperation.swift index 52b71538c8..86e6c1a4ff 100644 --- a/ios/MullvadVPN/TunnelManager/SetTunnelSettingsOperation.swift +++ b/ios/MullvadVPN/TunnelManager/SetTunnelSettingsOperation.swift @@ -42,7 +42,7 @@ class SetTunnelSettingsOperation: AsyncOperation { } guard let accountToken = state.tunnelInfo?.token else { - completionHandler(.failure(.missingAccount)) + completionHandler(.failure(.unsetAccount)) return } diff --git a/ios/MullvadVPN/TunnelManager/StartTunnelOperation.swift b/ios/MullvadVPN/TunnelManager/StartTunnelOperation.swift index 21b59bea5f..1037253da5 100644 --- a/ios/MullvadVPN/TunnelManager/StartTunnelOperation.swift +++ b/ios/MullvadVPN/TunnelManager/StartTunnelOperation.swift @@ -44,7 +44,7 @@ class StartTunnelOperation: AsyncOperation { } guard let tunnelInfo = self.state.tunnelInfo else { - completionHandler(.failure(.missingAccount)) + completionHandler(.failure(.unsetAccount)) return } diff --git a/ios/MullvadVPN/TunnelManager/StopTunnelOperation.swift b/ios/MullvadVPN/TunnelManager/StopTunnelOperation.swift index 1b06a6f68b..297805eb8e 100644 --- a/ios/MullvadVPN/TunnelManager/StopTunnelOperation.swift +++ b/ios/MullvadVPN/TunnelManager/StopTunnelOperation.swift @@ -39,7 +39,7 @@ class StopTunnelOperation: AsyncOperation { } guard let tunnelProvider = state.tunnelProvider else { - completionHandler(.failure(.missingAccount)) + completionHandler(.failure(.unsetAccount)) return } diff --git a/ios/MullvadVPN/TunnelManager/TunnelManager.swift b/ios/MullvadVPN/TunnelManager/TunnelManager.swift index d0eecaa4df..c2fe96adcd 100644 --- a/ios/MullvadVPN/TunnelManager/TunnelManager.swift +++ b/ios/MullvadVPN/TunnelManager/TunnelManager.swift @@ -161,7 +161,7 @@ class TunnelManager: TunnelManagerStateDelegate dispatchPrecondition(condition: .onQueue(self.stateQueue)) if case .failure(let error) = completion { - self.logger.error(chainedError: error, message: "Failed to load tunnel") + self.logger.error(chainedError: error, message: "Failed to load tunnel.") } self.updatePrivateKeyRotationTimer() @@ -201,7 +201,7 @@ class TunnelManager: TunnelManagerStateDelegate dispatchPrecondition(condition: .onQueue(self.stateQueue)) if case .failure(let error) = completion { - self.logger.error(chainedError: error) + self.logger.error(chainedError: error, message: "Failed to start the tunnel.") } }) @@ -255,7 +255,7 @@ class TunnelManager: TunnelManagerStateDelegate dispatchPrecondition(condition: .onQueue(self.stateQueue)) if let error = completion.error { - self.logger.error(chainedError: error) + self.logger.error(chainedError: error, message: "Failed to reconnect the tunnel.") } DispatchQueue.main.async { @@ -317,7 +317,7 @@ class TunnelManager: TunnelManagerStateDelegate } func regeneratePrivateKey(completionHandler: ((TunnelManager.Error?) -> Void)? = nil) { - let operation = RegeneratePrivateKeyOperation(queue: stateQueue, state: state, restClient: restClient) { [weak self] completion in + let operation = ReplaceKeyOperation.operationForKeyRegeneration(queue: stateQueue, state: state, restClient: restClient) { [weak self] completion in guard let self = self else { return } dispatchPrecondition(condition: .onQueue(self.stateQueue)) @@ -328,7 +328,7 @@ class TunnelManager: TunnelManagerStateDelegate self.reconnectTunnel(completionHandler: nil) case .failure(let error): - self.logger.error(chainedError: error, message: "Failed to regenerate private key") + self.logger.error(chainedError: error, message: "Failed to regenerate private key.") case .cancelled: break @@ -353,7 +353,7 @@ class TunnelManager: TunnelManagerStateDelegate } func rotatePrivateKey(completionHandler: @escaping (KeyRotationResult?, TunnelManager.Error?) -> Void) { - let operation = RotatePrivateKeyOperation( + let operation = ReplaceKeyOperation.operationForKeyRotation( queue: stateQueue, state: state, restClient: restClient, @@ -375,7 +375,7 @@ class TunnelManager: TunnelManagerStateDelegate case .failure(let error): rotationError = error - self.logger.error(chainedError: error, message: "Failed to rotate private key") + self.logger.error(chainedError: error, message: "Failed to rotate private key.") DispatchQueue.main.async { completionHandler(rotationResult, rotationError) @@ -570,7 +570,7 @@ class TunnelManager: TunnelManagerStateDelegate self.reconnectTunnel(completionHandler: nil) case .failure(let error): - self.logger.error(chainedError: error, message: "Failed to set tunnel settings") + self.logger.error(chainedError: error, message: "Failed to set tunnel settings.") case .cancelled: break @@ -644,7 +644,7 @@ extension TunnelManager { return submitBackgroundTask(at: beginDate) } else { - return .failure(.missingAccount) + return .failure(.unsetAccount) } } @@ -674,7 +674,7 @@ extension TunnelManager { self.logger.debug("Scheduled next private key rotation task at \(scheduleDate.logFormatDate())") case .failure(let error): - self.logger.error(chainedError: error, message: "Failed to schedule next private key rotation task") + self.logger.error(chainedError: error, message: "Failed to schedule next private key rotation task.") } } @@ -722,7 +722,7 @@ extension TunnelManager { fileprivate func nextRetryScheduleDate(_ error: TunnelManager.Error) -> Date? { switch error { - case .missingAccount: + case .unsetAccount: // Do not retry if logged out. return nil diff --git a/ios/MullvadVPN/TunnelManager/TunnelManagerError.swift b/ios/MullvadVPN/TunnelManager/TunnelManagerError.swift index 01fcbb136d..e60e3a3dd5 100644 --- a/ios/MullvadVPN/TunnelManager/TunnelManagerError.swift +++ b/ios/MullvadVPN/TunnelManager/TunnelManagerError.swift @@ -11,71 +11,71 @@ import Foundation extension TunnelManager { /// An error emitted by all public methods of TunnelManager enum Error: ChainedError { - /// Account token is not set - case missingAccount + /// Account is unset. + case unsetAccount - /// A failure to start the VPN tunnel via system call + /// A failure to start the VPN tunnel via system call. case startVPNTunnel(Swift.Error) - /// A failure to load the system VPN configurations created by the app + /// A failure to load the system VPN configurations created by the app. case loadAllVPNConfigurations(Swift.Error) - /// A failure to save the system VPN configuration + /// A failure to save the system VPN configuration. case saveVPNConfiguration(Swift.Error) - /// A failure to reload the system VPN configuration + /// A failure to reload the system VPN configuration. case reloadVPNConfiguration(Swift.Error) - /// A failure to remove the system VPN configuration + /// A failure to remove the system VPN configuration. case removeVPNConfiguration(Swift.Error) /// A failure to perform a recovery (by removing the VPN configuration) when a corrupt /// VPN configuration is detected. case removeInconsistentVPNConfiguration(Swift.Error) - /// A failure to read tunnel settings + /// A failure to read tunnel settings. case readTunnelSettings(TunnelSettingsManager.Error) - /// A failure to read relays cache + /// A failure to read relays cache. case readRelays(RelayCache.Error) - /// A failure to find a relay satisfying the given constraints + /// A failure to find a relay satisfying the given constraints. case cannotSatisfyRelayConstraints - /// A failure to add the tunnel settings + /// A failure to add the tunnel settings. case addTunnelSettings(TunnelSettingsManager.Error) - /// A failure to update the tunnel settings + /// A failure to update the tunnel settings. case updateTunnelSettings(TunnelSettingsManager.Error) - /// A failure to remove the tunnel settings from Keychain + /// A failure to remove the tunnel settings from Keychain. case removeTunnelSettings(TunnelSettingsManager.Error) - /// A failure to migrate tunnel settings + /// A failure to migrate tunnel settings. case migrateTunnelSettings(TunnelSettingsManager.Error) - /// Unable to obtain the persistent keychain reference for the tunnel settings + /// Unable to obtain the persistent keychain reference for the tunnel settings. case obtainPersistentKeychainReference(TunnelSettingsManager.Error) - /// A failure to push the public WireGuard key + /// A failure to push the public WireGuard key. case pushWireguardKey(REST.Error) - /// A failure to replace the public WireGuard key + /// A failure to replace the public WireGuard key. case replaceWireguardKey(REST.Error) - /// A failure to remove the public WireGuard key + /// A failure to remove the public WireGuard key. case removeWireguardKey(REST.Error) - /// A failure to schedule background task + /// A failure to schedule background task. case backgroundTaskScheduler(Swift.Error) - /// A failure to reload tunnel + /// A failure to reload tunnel. case reloadTunnel(TunnelIPC.Error) var errorDescription: String? { switch self { - case .missingAccount: - return "Missing account token" + case .unsetAccount: + return "Account is unset" case .startVPNTunnel: return "Failed to start the VPN tunnel" case .loadAllVPNConfigurations: diff --git a/ios/MullvadVPN/TunnelSettings.swift b/ios/MullvadVPN/TunnelSettings.swift index fcbdebbe81..c9a96fad80 100644 --- a/ios/MullvadVPN/TunnelSettings.swift +++ b/ios/MullvadVPN/TunnelSettings.swift @@ -13,6 +13,8 @@ import struct WireGuardKit.IPAddressRange /// A struct that holds a tun interface configuration. struct InterfaceSettings: Codable, Equatable { var privateKey: PrivateKeyWithMetadata + var nextPrivateKey: PrivateKeyWithMetadata? + var addresses: [IPAddressRange] var dnsSettings: DNSSettings @@ -21,11 +23,12 @@ struct InterfaceSettings: Codable, Equatable { } private enum CodingKeys: String, CodingKey { - case privateKey, addresses, dnsSettings + case privateKey, nextPrivateKey, addresses, dnsSettings } - init(privateKey: PrivateKeyWithMetadata = PrivateKeyWithMetadata(), addresses: [IPAddressRange] = [], dnsSettings: DNSSettings = DNSSettings()) { + init(privateKey: PrivateKeyWithMetadata = PrivateKeyWithMetadata(), nextPrivateKey: PrivateKeyWithMetadata? = nil, addresses: [IPAddressRange] = [], dnsSettings: DNSSettings = DNSSettings()) { self.privateKey = privateKey + self.nextPrivateKey = nextPrivateKey self.addresses = addresses self.dnsSettings = dnsSettings } @@ -36,6 +39,9 @@ struct InterfaceSettings: Codable, Equatable { privateKey = try container.decode(PrivateKeyWithMetadata.self, forKey: .privateKey) addresses = try container.decode([IPAddressRange].self, forKey: .addresses) + // Added in 2022.1 + nextPrivateKey = try container.decodeIfPresent(PrivateKeyWithMetadata.self, forKey: .nextPrivateKey) + // Provide default value, since `dnsSettings` key does not exist in <= 2021.2 dnsSettings = try container.decodeIfPresent(DNSSettings.self, forKey: .dnsSettings) ?? DNSSettings() @@ -45,6 +51,7 @@ struct InterfaceSettings: Codable, Equatable { var container = encoder.container(keyedBy: CodingKeys.self) try container.encode(privateKey, forKey: .privateKey) + try container.encode(nextPrivateKey, forKey: .nextPrivateKey) try container.encode(addresses, forKey: .addresses) try container.encode(dnsSettings, forKey: .dnsSettings) } |
