summaryrefslogtreecommitdiffhomepage
diff options
context:
space:
mode:
authorAndrej Mihajlov <and@mullvad.net>2022-02-02 17:38:55 +0100
committerAndrej Mihajlov <and@mullvad.net>2022-02-02 17:38:55 +0100
commit38a9fe03fcf162088fe87d89b32ce1a07da85d62 (patch)
tree56c435cb02b8f384381b3aa5c7d95a05be3fb2c7
parent8255584218ce8e2f4f96ccbaa3e8833a16020ba5 (diff)
parent99cc94d4ff8ca807c6341b61d12c1b9c04d482c3 (diff)
downloadmullvadvpn-38a9fe03fcf162088fe87d89b32ce1a07da85d62.tar.xz
mullvadvpn-38a9fe03fcf162088fe87d89b32ce1a07da85d62.zip
Merge branch 'store-next-key'
-rw-r--r--ios/CHANGELOG.md2
-rw-r--r--ios/MullvadVPN.xcodeproj/project.pbxproj12
-rw-r--r--ios/MullvadVPN/DisplayChainedError.swift2
-rw-r--r--ios/MullvadVPN/Operations/OperationCompletion.swift11
-rw-r--r--ios/MullvadVPN/TunnelManager/ReloadTunnelOperation.swift2
-rw-r--r--ios/MullvadVPN/TunnelManager/ReplaceKeyOperation.swift214
-rw-r--r--ios/MullvadVPN/TunnelManager/RotatePrivateKeyOperation.swift123
-rw-r--r--ios/MullvadVPN/TunnelManager/SetAccountOperation.swift147
-rw-r--r--ios/MullvadVPN/TunnelManager/SetTunnelSettingsOperation.swift2
-rw-r--r--ios/MullvadVPN/TunnelManager/StartTunnelOperation.swift2
-rw-r--r--ios/MullvadVPN/TunnelManager/StopTunnelOperation.swift2
-rw-r--r--ios/MullvadVPN/TunnelManager/TunnelManager.swift22
-rw-r--r--ios/MullvadVPN/TunnelManager/TunnelManagerError.swift44
-rw-r--r--ios/MullvadVPN/TunnelSettings.swift11
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)
}