diff options
| author | Andrej Mihajlov <and@mullvad.net> | 2020-01-03 11:43:10 +0100 |
|---|---|---|
| committer | Andrej Mihajlov <and@mullvad.net> | 2020-01-03 11:43:10 +0100 |
| commit | daee16640792a5a99de3954dbc80bf775f023497 (patch) | |
| tree | 69a05f974d2a0e261b91f6a50c90f6ff4e706936 | |
| parent | f424194d3a57c1f6c4dcb349ee8b93c138f3c1f3 (diff) | |
| parent | 9f31769e8801b25619f33fe7ca64ccd824a15f0b (diff) | |
| download | mullvadvpn-daee16640792a5a99de3954dbc80bf775f023497.tar.xz mullvadvpn-daee16640792a5a99de3954dbc80bf775f023497.zip | |
Merge branch 'regenerate-private-key-ios'
| -rw-r--r-- | ios/MullvadVPN/JsonRpc.swift | 17 | ||||
| -rw-r--r-- | ios/MullvadVPN/MullvadAPI.swift | 108 | ||||
| -rw-r--r-- | ios/MullvadVPN/MutuallyExclusive.swift | 108 | ||||
| -rw-r--r-- | ios/MullvadVPN/RelayCache.swift | 4 | ||||
| -rw-r--r-- | ios/MullvadVPN/TunnelManager.swift | 106 | ||||
| -rw-r--r-- | ios/MullvadVPN/WireguardPrivateKey.swift | 59 |
6 files changed, 328 insertions, 74 deletions
diff --git a/ios/MullvadVPN/JsonRpc.swift b/ios/MullvadVPN/JsonRpc.swift index 6c54bf6adb..58a6b01827 100644 --- a/ios/MullvadVPN/JsonRpc.swift +++ b/ios/MullvadVPN/JsonRpc.swift @@ -38,8 +38,10 @@ struct JsonRpcRequest: Encodable { } } -class JsonRpcResponseError: Error, Decodable { - let code: Int +class JsonRpcResponseError<ResponseCode>: Error, Decodable + where ResponseCode: Decodable +{ + let code: ResponseCode let message: String var localizedDescription: String? { @@ -53,15 +55,18 @@ class JsonRpcResponseError: Error, Decodable { required init(from decoder: Decoder) throws { let container = try decoder.container(keyedBy: CodingKeys.self) - code = try container.decode(Int.self, forKey: .code) + code = try container.decode(ResponseCode.self, forKey: .code) message = try container.decode(String.self, forKey: .message) } } -struct JsonRpcResponse<T: Decodable>: Decodable { +struct JsonRpcResponse<T, ResponseCode>: Decodable + where + T: Decodable, ResponseCode: Decodable +{ let version: String let id: String - let result: Result<T, JsonRpcResponseError> + let result: Result<T, JsonRpcResponseError<ResponseCode>> private enum CodingKeys: String, CodingKey { case version = "jsonrpc", id, result, error @@ -76,7 +81,7 @@ struct JsonRpcResponse<T: Decodable>: Decodable { if container.contains(.result) { self.result = .success(try container.decode(T.self, forKey: .result)) } else { - self.result = .failure(try container.decode(JsonRpcResponseError.self, forKey: .error)) + self.result = .failure(try container.decode(JsonRpcResponseError<ResponseCode>.self, forKey: .error)) } } } diff --git a/ios/MullvadVPN/MullvadAPI.swift b/ios/MullvadVPN/MullvadAPI.swift index ebe8956830..be87728b7f 100644 --- a/ios/MullvadVPN/MullvadAPI.swift +++ b/ios/MullvadVPN/MullvadAPI.swift @@ -16,18 +16,6 @@ private let kMullvadAPIURL = URL(string: "https://api.mullvad.net/rpc/")! /// Network request timeout in seconds private let kNetworkTimeout: TimeInterval = 10 -/// An error type emitted by `MullvadAPI` -enum MullvadAPIError: Error { - /// A network communication error - case network(URLError) - - /// An error occured when decoding the JSON response - case decoding(Error) - - /// An error occured when encoding the JSON request - case encoding(Error) -} - /// A type that describes the account verification result enum AccountVerification { /// The app should attempt to verify the account token at some point later because the network @@ -44,29 +32,79 @@ enum AccountVerification { /// An error type that describes why the account verification was deferred enum DeferReasonError: Error { /// Mullvad API communication error - case communication(MullvadAPIError) + case communication(MullvadAPI.Error) /// Mullvad API responded with an error - case server(JsonRpcResponseError) + case server(MullvadAPI.ResponseError) } -/// The error code returned by the API when it cannot find the given account token -private let kAccountDoesNotExistErrorCode = -200 - class MullvadAPI { private let session: URLSession + /// A enum mapping the integer codes returned by Mullvad API with the corresponding enum + /// variants + private enum RawResponseCode: Int { + case accountDoesNotExist = -200 + case tooManyWireguardKeys = -703 + } + + /// A enum describing the Mullvad API response code + enum ResponseCode: RawRepresentable, Codable { + var rawValue: Int { + switch self { + case .accountDoesNotExist: + return RawResponseCode.accountDoesNotExist.rawValue + + case .tooManyWireguardKeys: + return RawResponseCode.tooManyWireguardKeys.rawValue + + case .other(let value): + return value + } + } + + init?(rawValue: Int) { + switch RawResponseCode(rawValue: rawValue) { + case .accountDoesNotExist: + self = .accountDoesNotExist + case .tooManyWireguardKeys: + self = .tooManyWireguardKeys + case .none: + self = ResponseCode.other(rawValue) + } + } + + case accountDoesNotExist + case tooManyWireguardKeys + case other(Int) + } + + /// An error type emitted by `MullvadAPI` + enum Error: Swift.Error { + /// A network communication error + case network(URLError) + + /// An error occured when decoding the JSON response + case decoding(Swift.Error) + + /// An error occured when encoding the JSON request + case encoding(Swift.Error) + } + + typealias ResponseError = JsonRpcResponseError<ResponseCode> + typealias Response<T: Decodable> = JsonRpcResponse<T, ResponseCode> + init(session: URLSession = URLSession.shared) { self.session = session } - func getRelayList() -> AnyPublisher<JsonRpcResponse<RelayList>, MullvadAPIError> { + func getRelayList() -> AnyPublisher<Response<RelayList>, MullvadAPI.Error> { let request = JsonRpcRequest(method: "relay_list_v3", params: []) return MullvadAPI.makeDataTaskPublisher(request: request) } - func getAccountExpiry(accountToken: String) -> AnyPublisher<JsonRpcResponse<Date>, MullvadAPIError> { + func getAccountExpiry(accountToken: String) -> AnyPublisher<Response<Date>, MullvadAPI.Error> { let request = JsonRpcRequest(method: "get_expiry", params: [AnyEncodable(accountToken)]) return MullvadAPI.makeDataTaskPublisher(request: request) @@ -81,7 +119,7 @@ class MullvadAPI { return .verified(expiry) case .failure(let serverError): - if serverError.code == kAccountDoesNotExistErrorCode { + if case .accountDoesNotExist = serverError.code { // Report .invalid account if the server responds with the special code return .invalid } else { @@ -91,13 +129,13 @@ class MullvadAPI { } }) .catch({ (networkError) in - // Treat all network errors as .deferred verification + // Treat all communication errors as .deferred verification return Just(.deferred(.communication(networkError))) }) .eraseToAnyPublisher() } - func pushWireguardKey(accountToken: String, publicKey: Data) -> AnyPublisher<JsonRpcResponse<WireguardAssociatedAddresses>, MullvadAPIError> { + func pushWireguardKey(accountToken: String, publicKey: Data) -> AnyPublisher<Response<WireguardAssociatedAddresses>, MullvadAPI.Error> { let request = JsonRpcRequest(method: "push_wg_key", params: [ AnyEncodable(accountToken), AnyEncodable(publicKey) @@ -106,7 +144,17 @@ class MullvadAPI { return MullvadAPI.makeDataTaskPublisher(request: request) } - func checkWireguardKey(accountToken: String, publicKey: Data) -> AnyPublisher<JsonRpcResponse<WireguardAssociatedAddresses>, MullvadAPIError> { + func replaceWireguardKey(accountToken: String, oldPublicKey: Data, newPublicKey: Data) -> AnyPublisher<Response<WireguardAssociatedAddresses>, MullvadAPI.Error> { + let request = JsonRpcRequest(method: "replace_wg_key", params: [ + AnyEncodable(accountToken), + AnyEncodable(oldPublicKey), + AnyEncodable(newPublicKey) + ]) + + return MullvadAPI.makeDataTaskPublisher(request: request) + } + + func checkWireguardKey(accountToken: String, publicKey: Data) -> AnyPublisher<Response<Bool>, MullvadAPI.Error> { let request = JsonRpcRequest(method: "check_wg_key", params: [ AnyEncodable(accountToken), AnyEncodable(publicKey) @@ -115,17 +163,19 @@ class MullvadAPI { return MullvadAPI.makeDataTaskPublisher(request: request) } - private static func makeDataTaskPublisher<T: Decodable>(request: JsonRpcRequest) -> AnyPublisher<JsonRpcResponse<T>, MullvadAPIError> { + private static func makeDataTaskPublisher<T: Decodable>(request: JsonRpcRequest) -> AnyPublisher<Response<T>, MullvadAPI.Error> { return Just(request) .encode(encoder: makeJSONEncoder()) - .mapError { MullvadAPIError.encoding($0) } + .mapError { MullvadAPI.Error.encoding($0) } .map { self.makeURLRequest(httpBody: $0) } .flatMap { URLSession.shared.dataTaskPublisher(for: $0) - .mapError { MullvadAPIError.network($0) } - .map { $0.data } - .decode(type: JsonRpcResponse<T>.self, decoder: makeJSONDecoder()) - .mapError { MullvadAPIError.decoding($0) } + .mapError { MullvadAPI.Error.network($0) } + .flatMap { (data, response) in + Just(data) + .decode(type: Response<T>.self, decoder: makeJSONDecoder()) + .mapError { MullvadAPI.Error.decoding($0) } + } }.eraseToAnyPublisher() } diff --git a/ios/MullvadVPN/MutuallyExclusive.swift b/ios/MullvadVPN/MutuallyExclusive.swift index cb3803805a..c93cc5362e 100644 --- a/ios/MullvadVPN/MutuallyExclusive.swift +++ b/ios/MullvadVPN/MutuallyExclusive.swift @@ -13,13 +13,16 @@ extension Publishers { /// A publisher that blocks the given DispatchQueue until the produced publisher reported the /// completion. - final class MutuallyExclusive<PublisherType, Context>: Publisher where PublisherType: Publisher, Context: Scheduler { + final class MutuallyExclusive<PublisherType, Context>: Publisher + where + PublisherType: Publisher, + Context: Scheduler + { + typealias MakePublisherBlock = () -> PublisherType typealias Output = PublisherType.Output typealias Failure = PublisherType.Failure - typealias MakePublisherBlock = () -> PublisherType - private let exclusivityQueue: Context private let executionQueue: Context @@ -32,27 +35,100 @@ extension Publishers { } func receive<S>(subscriber: S) where S : Subscriber, S.Failure == Failure, S.Input == Output { - exclusivityQueue.schedule { - let sema = DispatchSemaphore(value: 0) - let releaseLock = { - _ = sema.signal() - } + let subscription = MutuallyExclusive.Subscription( + subscriber: subscriber, + createPublisher: createPublisher, + exclusivityQueue: exclusivityQueue, + executionQueue: executionQueue) + + subscriber.receive(subscription: subscription) + } + } +} + +private extension Publishers.MutuallyExclusive { + + /// A subscription used by `MutuallyExclusive` publisher + final class Subscription<SubscriberType, PublisherType, Context>: Combine.Subscription + where + SubscriberType: Subscriber, PublisherType: Publisher, + PublisherType.Output == SubscriberType.Input, + PublisherType.Failure == SubscriberType.Failure, + Context: Scheduler + { + typealias MakePublisherBlock = () -> PublisherType + + private let subscriber: SubscriberType + private var innerSubscriber: AnyCancellable? + private let createPublisher: MakePublisherBlock + + private let exclusivityQueue: Context + private let executionQueue: Context + private let sema = DispatchSemaphore(value: 0) + + private let cancelLock = NSLock() + private var isCancelled = false + + init(subscriber: SubscriberType, + createPublisher: @escaping MakePublisherBlock, + exclusivityQueue: Context, + executionQueue: Context) + { + self.subscriber = subscriber + self.createPublisher = createPublisher + self.exclusivityQueue = exclusivityQueue + self.executionQueue = executionQueue + } + func request(_ demand: Subscribers.Demand) { + self.exclusivityQueue.schedule { self.executionQueue.schedule { - self.createPublisher() - .handleEvents(receiveCompletion: { _ in - releaseLock() - }, receiveCancel: { - releaseLock() - }) - .subscribe(subscriber) + self.cancelLock.withCriticalBlock { + guard !self.isCancelled else { return } + + self.innerSubscriber = self.createPublisher() + .sink(receiveCompletion: { [weak self] (completion) in + guard let self = self else { return } + + self.subscriber.receive(completion: completion) + self.signalSemaphore() + }, receiveValue: { [weak self] (output) in + _ = self?.subscriber.receive(output) + }) + } } + self.sema.wait() + } + } + + func cancel() { + cancelLock.withCriticalBlock { + guard !isCancelled else { return } - sema.wait() + isCancelled = true + + innerSubscriber?.cancel() + innerSubscriber = nil + + signalSemaphore() } } + + private func signalSemaphore() { + _ = sema.signal() + } + } } +private extension NSLock { + func withCriticalBlock<T>(_ body: () -> T) -> T { + lock() + defer { unlock() } + + return body() + } +} + typealias MutuallyExclusive = Publishers.MutuallyExclusive diff --git a/ios/MullvadVPN/RelayCache.swift b/ios/MullvadVPN/RelayCache.swift index c80356da11..4c8fefbd92 100644 --- a/ios/MullvadVPN/RelayCache.swift +++ b/ios/MullvadVPN/RelayCache.swift @@ -15,8 +15,8 @@ enum RelayCacheError: Error { case defaultLocationNotFound case io(Error) case coding(Error) - case network(MullvadAPIError) - case server(JsonRpcResponseError) + case network(MullvadAPI.Error) + case server(JsonRpcResponseError<MullvadAPI.ResponseCode>) } /// A enum describing the source of the relay list diff --git a/ios/MullvadVPN/TunnelManager.swift b/ios/MullvadVPN/TunnelManager.swift index 9ea4ab98f9..f1b0ab8c59 100644 --- a/ios/MullvadVPN/TunnelManager.swift +++ b/ios/MullvadVPN/TunnelManager.swift @@ -33,6 +33,12 @@ enum TunnelManagerError: Error { /// A failure to get the relay constraints case getRelayConstraints(TunnelConfigurationManagerError) + + /// A failure to get a public key used for Wireguard + case getWireguardPublicKey(TunnelConfigurationManagerError) + + /// A failure to re-generate a private key used for Wireguard + case regenerateWireguardPrivateKey(RegenerateWireguardPrivateKeyError) } enum TunnelIpcRequestError: Error { @@ -63,7 +69,7 @@ enum SetAccountError: Error { /// A failure to push the wireguard key case pushWireguardKey(PushWireguardKeyError) - /// A failure to set up the tunnel + /// A failure to set up a tunnel case setup(SetupTunnelError) } @@ -75,9 +81,23 @@ enum UnsetAccountError: Error { case removeTunnelConfiguration(TunnelConfigurationManagerError) } +enum RegenerateWireguardPrivateKeyError: Error { + /// A failure to read the public Wireguard key from Keychain + case readPublicWireguardKey(TunnelConfigurationManagerError) + + /// A failure to replace the public Wireguard key + case replaceWireguardKey(PushWireguardKeyError) + + /// A failure to update tunnel configuration + case updateTunnelConfiguration(UpdateTunnelConfigurationError) + + /// A failure to set up a tunnel + case setupTunnel(SetupTunnelError) +} + enum PushWireguardKeyError: Error { - case network(MullvadAPIError) - case server(JsonRpcResponseError) + case transport(MullvadAPI.Error) + case server(MullvadAPI.ResponseError) } enum UpdateTunnelConfigurationError: Error { @@ -320,12 +340,12 @@ class TunnelManager { } // Send wireguard key to the server - let publicKey = tunnelConfig.interface.privateKey.publicKeyRawRepresentation + let publicKey = tunnelConfig.interface.privateKey.publicKey.rawRepresentation return self.apiClient.pushWireguardKey(accountToken: accountToken, publicKey: publicKey) .mapError { (networkError) -> SetAccountError in - return .pushWireguardKey(.network(networkError)) - }.flatMap { (response: JsonRpcResponse<WireguardAssociatedAddresses>) in + return .pushWireguardKey(.transport(networkError)) + }.flatMap { (response: MullvadAPI.Response<WireguardAssociatedAddresses>) in return response.result.publisher .mapError { (serverError) -> SetAccountError in return .pushWireguardKey(.server(serverError)) @@ -399,6 +419,67 @@ class TunnelManager { }.eraseToAnyPublisher() } + func regeneratePrivateKey() -> AnyPublisher<(), TunnelManagerError> { + MutuallyExclusive(exclusivityQueue: exclusivityQueue, executionQueue: executionQueue) { + Just(self.accountToken) + .setFailureType(to: TunnelManagerError.self) + .replaceNil(with: .missingAccount) + .flatMap { (accountToken) -> AnyPublisher<(), TunnelManagerError> in + let newPrivateKey = WireguardPrivateKey() + + return TunnelConfigurationManager.load(account: accountToken) + .map { $0.interface.privateKey.publicKey } + .mapError { RegenerateWireguardPrivateKeyError.readPublicWireguardKey($0) } + .publisher + .flatMap { (oldPublicKey) in + self.apiClient.replaceWireguardKey( + accountToken: accountToken, + oldPublicKey: oldPublicKey.rawRepresentation, + newPublicKey: newPrivateKey.publicKey.rawRepresentation) + .mapError { (networkError) -> RegenerateWireguardPrivateKeyError in + return .replaceWireguardKey(.transport(networkError)) + }.receive(on: self.executionQueue) + .flatMap { (response: MullvadAPI.Response<WireguardAssociatedAddresses>) in + return response.result.publisher + .mapError { (serverError) -> RegenerateWireguardPrivateKeyError in + return .replaceWireguardKey(.server(serverError)) + } + } + .flatMap { (addresses) in + self.updateTunnelConfiguration(accountToken: accountToken) { + (tunnelConfiguration) in + tunnelConfiguration.interface.privateKey = newPrivateKey + tunnelConfiguration.interface.addresses = [ + addresses.ipv4Address, + addresses.ipv6Address + ] + } + .mapError { .updateTunnelConfiguration($0) } + .publisher + } + .flatMap { + self.setupTunnel(accountToken: accountToken) + .map { _ in () } + .mapError { RegenerateWireguardPrivateKeyError.setupTunnel($0) } + }.receive(on: self.executionQueue) + .flatMap { _ in + // Ignore Packet Tunnel IPC errors but log them + self.reloadPacketTunnelConfiguration() + .replaceError(with: ()) + .setFailureType(to: RegenerateWireguardPrivateKeyError.self) + .handleEvents(receiveCompletion: { (completion) in + if case .failure(let error) = completion { + os_log(.error, "Failed to tell the tunnel to reload configuration: %{public}s", error.localizedDescription) + } + }) + } + } + .mapError { TunnelManagerError.regenerateWireguardPrivateKey($0) } + .eraseToAnyPublisher() + } + }.eraseToAnyPublisher() + } + func setRelayConstraints(_ constraints: RelayConstraints) -> AnyPublisher<(), TunnelManagerError> { MutuallyExclusive(exclusivityQueue: exclusivityQueue, executionQueue: executionQueue) { Just(self.accountToken) @@ -444,6 +525,19 @@ class TunnelManager { }.eraseToAnyPublisher() } + func getWireguardPublicKey() -> AnyPublisher<WireguardPublicKey, TunnelManagerError> { + MutuallyExclusive(exclusivityQueue: exclusivityQueue, executionQueue: executionQueue) { + Just(self.accountToken) + .setFailureType(to: TunnelManagerError.self) + .replaceNil(with: .missingAccount) + .flatMap { (accountToken) in + TunnelConfigurationManager.load(account: accountToken) + .map { $0.interface.privateKey.publicKey } + .mapError { TunnelManagerError.getWireguardPublicKey($0) }.publisher + } + }.eraseToAnyPublisher() + } + // MARK: - Private /// Tell Packet Tunnel process to reload the tunnel configuration diff --git a/ios/MullvadVPN/WireguardPrivateKey.swift b/ios/MullvadVPN/WireguardPrivateKey.swift index 781e4f3917..058055e77f 100644 --- a/ios/MullvadVPN/WireguardPrivateKey.swift +++ b/ios/MullvadVPN/WireguardPrivateKey.swift @@ -12,43 +12,72 @@ import Foundation /// A convenience wrapper around the wireguard key struct WireguardPrivateKey { - /// An inner impelementation of a private key - private let innerPrivateKey: CryptoKit.Curve25519.KeyAgreement.PrivateKey + /// When the key was created + let creationDate: Date /// Private key's raw representation var rawRepresentation: Data { - return innerPrivateKey.rawRepresentation + innerPrivateKey.rawRepresentation } - /// Public key's raw representation - var publicKeyRawRepresentation: Data { - return innerPrivateKey.publicKey.rawRepresentation + /// Public key + var publicKey: WireguardPublicKey { + WireguardPublicKey( + creationDate: creationDate, + rawRepresentation: innerPrivateKey.publicKey.rawRepresentation + ) } + /// An inner impelementation of a private key + private let innerPrivateKey: Curve25519.KeyAgreement.PrivateKey + /// Initialize the new private key init() { - innerPrivateKey = CryptoKit.Curve25519.KeyAgreement.PrivateKey() + innerPrivateKey = Curve25519.KeyAgreement.PrivateKey() + creationDate = Date() } /// Load with the existing private key - init(rawRepresentation: Data) throws { - innerPrivateKey = try CryptoKit.Curve25519.KeyAgreement.PrivateKey(rawRepresentation: rawRepresentation) + init(rawRepresentation: Data, createdAt: Date) throws { + innerPrivateKey = try Curve25519.KeyAgreement.PrivateKey(rawRepresentation: rawRepresentation) + creationDate = createdAt } } +extension WireguardPrivateKey: Equatable { + static func == (lhs: WireguardPrivateKey, rhs: WireguardPrivateKey) -> Bool { + lhs.rawRepresentation == rhs.rawRepresentation + } +} + +/// A struct holding a public key used for Wireguard with associated metadata +struct WireguardPublicKey { + /// Refers to private key creation date + let creationDate: Date + + /// Raw public key representation + let rawRepresentation: Data +} + extension WireguardPrivateKey: Codable { + + private enum CodingKeys: String, CodingKey { + case privateKeyData, creationDate + } + func encode(to encoder: Encoder) throws { - var container = encoder.singleValueContainer() + var container = encoder.container(keyedBy: CodingKeys.self) - try container.encode(innerPrivateKey.rawRepresentation) + try container.encode(innerPrivateKey.rawRepresentation, forKey: .privateKeyData) + try container.encode(creationDate, forKey: .creationDate) } init(from decoder: Decoder) throws { - let container = try decoder.singleValueContainer() - - let privateKeyBytes = try container.decode(Data.self) + let container = try decoder.container(keyedBy: CodingKeys.self) + let privateKeyBytes = try container.decode(Data.self, forKey: .privateKeyData) + let creationDate = try container.decode(Date.self, forKey: .creationDate) - self = try .init(rawRepresentation: privateKeyBytes) + self = try .init(rawRepresentation: privateKeyBytes, createdAt: creationDate) } } |
