diff options
| author | Andrej Mihajlov <and@mullvad.net> | 2019-12-02 15:48:15 +0100 |
|---|---|---|
| committer | Andrej Mihajlov <and@mullvad.net> | 2019-12-03 14:22:54 +0100 |
| commit | 7bd2a790aaf576d2d2793d693412ec8032f2a565 (patch) | |
| tree | 1f421dd6387fc8ec05f54fd76a0e8f254df78a62 /ios | |
| parent | da7b3f0ead28deee5991dff7d159c0b5cf5a5d40 (diff) | |
| download | mullvadvpn-7bd2a790aaf576d2d2793d693412ec8032f2a565.tar.xz mullvadvpn-7bd2a790aaf576d2d2793d693412ec8032f2a565.zip | |
Add tunnel configuration
Diffstat (limited to 'ios')
| -rw-r--r-- | ios/MullvadVPN/KeychainError.swift | 29 | ||||
| -rw-r--r-- | ios/MullvadVPN/RelayConstraints.swift | 140 | ||||
| -rw-r--r-- | ios/MullvadVPN/TunnelConfiguration.swift | 23 | ||||
| -rw-r--r-- | ios/MullvadVPN/TunnelConfigurationCoder.swift | 30 | ||||
| -rw-r--r-- | ios/MullvadVPN/TunnelConfigurationManager.swift | 214 | ||||
| -rw-r--r-- | ios/MullvadVPN/WireguardPrivateKey.swift | 54 |
6 files changed, 490 insertions, 0 deletions
diff --git a/ios/MullvadVPN/KeychainError.swift b/ios/MullvadVPN/KeychainError.swift new file mode 100644 index 0000000000..0871e3fb18 --- /dev/null +++ b/ios/MullvadVPN/KeychainError.swift @@ -0,0 +1,29 @@ +// +// KeychainError.swift +// MullvadVPN +// +// Created by pronebird on 02/10/2019. +// Copyright © 2019 Amagicom AB. All rights reserved. +// + +import Foundation +import Security + +struct KeychainError: Error, LocalizedError { + let code: OSStatus + + var errorDescription: String? { + return SecCopyErrorMessageString(code, nil) as String? + } +} + +extension KeychainError { + + static let duplicateItem = KeychainError(code: errSecDuplicateItem) + static let itemNotFound = KeychainError(code: errSecItemNotFound) + + static func ~= (lhs: KeychainError, rhs: Error) -> Bool { + guard let rhsError = rhs as? KeychainError else { return false } + return lhs.code == rhsError.code + } +} diff --git a/ios/MullvadVPN/RelayConstraints.swift b/ios/MullvadVPN/RelayConstraints.swift new file mode 100644 index 0000000000..0b7f84aa48 --- /dev/null +++ b/ios/MullvadVPN/RelayConstraints.swift @@ -0,0 +1,140 @@ +// +// RelayConstraint.swift +// MullvadVPN +// +// Created by pronebird on 10/06/2019. +// Copyright © 2019 Amagicom AB. All rights reserved. +// + +import Foundation + +private let kRelayConstraintAnyRepr = "any" + +enum RelayConstraint<T: Codable>: Codable { + case any + case only(T) + + var value: T? { + if case .only(let value) = self { + return value + } else { + return nil + } + } + + private struct OnlyRepr: Codable { + var only: T + } + + init(from decoder: Decoder) throws { + let container = try decoder.singleValueContainer() + + let decoded = try? container.decode(String.self) + if decoded == kRelayConstraintAnyRepr { + self = .any + } else { + let onlyVariant = try container.decode(OnlyRepr.self) + + self = .only(onlyVariant.only) + } + } + + func encode(to encoder: Encoder) throws { + var container = encoder.singleValueContainer() + + switch self { + case .any: + try container.encode(kRelayConstraintAnyRepr) + case .only(let inner): + try container.encode(OnlyRepr(only: inner)) + } + } +} + +extension RelayConstraint: CustomDebugStringConvertible { + var debugDescription: String { + var output = "RelayConstraint." + switch self { + case .any: + output += "any" + case .only(let value): + output += "only(\(String(reflecting: value)))" + } + return output + } +} + +enum RelayLocation: Codable, Equatable { + case country(String) + case city(String, String) + case hostname(String, String, String) + + init(from decoder: Decoder) throws { + let container = try decoder.singleValueContainer() + + let components = try container.decode([String].self) + + switch components.count { + case 1: + self = .country(components[0]) + case 2: + self = .city(components[0], components[1]) + case 3: + self = .hostname(components[0], components[1], components[2]) + default: + throw DecodingError.dataCorruptedError( + in: container, + debugDescription: "Invalid enum representation") + } + } + + func encode(to encoder: Encoder) throws { + var container = encoder.singleValueContainer() + + switch self { + case .country(let code): + try container.encode([code]) + + case .city(let countryCode, let cityCode): + try container.encode([countryCode, cityCode]) + + case .hostname(let countryCode, let cityCode, let hostname): + try container.encode([countryCode, cityCode, hostname]) + } + } + +} + +extension RelayLocation: CustomDebugStringConvertible { + var debugDescription: String { + var output = "RelayLocation." + + switch self { + case .country(let country): + output += "country(\(String(reflecting: country)))" + + case .city(let country, let city): + output += "city(\(String(reflecting: country)), \(String(reflecting: city)))" + + case .hostname(let country, let city, let host): + output += "hostname(\(String(reflecting: country)), " + + "\(String(reflecting: city)), " + + "\(String(reflecting: host)))" + } + + return output + } +} + +struct RelayConstraints: Codable { + var location: RelayConstraint<RelayLocation> = .only(.country("se")) +} + +extension RelayConstraints: CustomDebugStringConvertible { + var debugDescription: String { + var output = "RelayConstraints { " + output += "location: \(String(reflecting: location))" + output += " }" + return output + } +} diff --git a/ios/MullvadVPN/TunnelConfiguration.swift b/ios/MullvadVPN/TunnelConfiguration.swift new file mode 100644 index 0000000000..8cd7ab15aa --- /dev/null +++ b/ios/MullvadVPN/TunnelConfiguration.swift @@ -0,0 +1,23 @@ +// +// TunnelConfiguration.swift +// MullvadVPN +// +// Created by pronebird on 19/06/2019. +// Copyright © 2019 Amagicom AB. All rights reserved. +// + +import Foundation +import Network +import NetworkExtension + +/// A struct that holds a tun interface configuration +struct InterfaceConfiguration: Codable { + var privateKey = WireguardPrivateKey() + var addresses = [IPAddressRange]() +} + +/// A struct that holds the configuration passed via NETunnelProviderProtocol +struct TunnelConfiguration: Codable { + var relayConstraints = RelayConstraints() + var interface = InterfaceConfiguration() +} diff --git a/ios/MullvadVPN/TunnelConfigurationCoder.swift b/ios/MullvadVPN/TunnelConfigurationCoder.swift new file mode 100644 index 0000000000..db844c252d --- /dev/null +++ b/ios/MullvadVPN/TunnelConfigurationCoder.swift @@ -0,0 +1,30 @@ +// +// TunnelConfigurationCoder.swift +// MullvadVPN +// +// Created by pronebird on 02/10/2019. +// Copyright © 2019 Amagicom AB. All rights reserved. +// + +import Foundation + +/// Tunnel configuration encoding and decoding helper +enum TunnelConfigurationCoder {} + +extension TunnelConfigurationCoder { + + enum Error: Swift.Error { + case encode(Swift.Error) + case decode(Swift.Error) + } + + static func decode(data: Data) -> Result<TunnelConfiguration, Error> { + return Result { try JSONDecoder().decode(TunnelConfiguration.self, from: data) } + .mapError { Error.decode($0) } + } + + static func encode(tunnelConfig: TunnelConfiguration) -> Result<Data, Error> { + return Result { try JSONEncoder().encode(tunnelConfig) } + .mapError { Error.encode($0) } + } +} diff --git a/ios/MullvadVPN/TunnelConfigurationManager.swift b/ios/MullvadVPN/TunnelConfigurationManager.swift new file mode 100644 index 0000000000..b48e04844b --- /dev/null +++ b/ios/MullvadVPN/TunnelConfigurationManager.swift @@ -0,0 +1,214 @@ +// +// TunnelConfigurationManager.swift +// MullvadVPN +// +// Created by pronebird on 02/10/2019. +// Copyright © 2019 Amagicom AB. All rights reserved. +// + +import Foundation +import Security + +/// Service name used for keychain items +private let kServiceName = "Mullvad VPN" + +enum TunnelConfigurationManagerError: Error { + case encode(TunnelConfigurationCoder.Error) + case decode(TunnelConfigurationCoder.Error) + case addToKeychain(KeychainError) + case updateKeychain(KeychainError) + case removeKeychainItem(KeychainError) + case getFromKeychain(KeychainError) + case getPersistentKeychainRef(KeychainError) +} + +enum TunnelConfigurationManager {} + +extension TunnelConfigurationManager { + + static func save(configuration: TunnelConfiguration, account: String) -> Result<(), TunnelConfigurationManagerError> { + TunnelConfigurationCoder.encode(tunnelConfig: configuration) + .mapError { .encode($0) } + .flatMap { (data) -> Result<(), TunnelConfigurationManagerError> in + Keychain.updateItem(account: account, data: data) + .flatMapError { (keychainError) -> Result<(), TunnelConfigurationManagerError> in + if case .itemNotFound = keychainError { + return Keychain.addItem(account: account, data: data) + .mapError { .addToKeychain($0) } + } else { + return .failure(.updateKeychain(keychainError)) + } + } + } + } + + static func load(account: String) -> Result<TunnelConfiguration, TunnelConfigurationManagerError> { + Keychain.getItemData(account: account) + .mapError { .getFromKeychain($0) } + .flatMap { (data) in + TunnelConfigurationCoder.decode(data: data) + .mapError { .decode($0) } + } + } + + static func load(persistentKeychainRef: Data) -> Result<TunnelConfiguration, TunnelConfigurationManagerError> { + Keychain.getItemData(persistentKeychainRef: persistentKeychainRef) + .mapError { .getFromKeychain($0) } + .flatMap { (data) in + TunnelConfigurationCoder.decode(data: data) + .mapError { .decode($0) } + } + } + + static func remove(account: String) -> Result<(), TunnelConfigurationManagerError> { + Keychain.removeItem(account: account) + .mapError { .removeKeychainItem($0) } + } + + static func getPersistentKeychainRef(account: String) -> Result<Data, TunnelConfigurationManagerError> { + Keychain.getPersistentRef(account: account) + .mapError { .getPersistentKeychainRef($0) } + } + +} + +private enum Keychain {} + +private extension Keychain { + + /// A Keychain Result type + typealias Result<T> = Swift.Result<T, KeychainError> + + /// List all of the account tokens in Keychain + static func listAccounts() -> Result<[String]> { + let query: [CFString: Any] = [ + kSecClass: kSecClassGenericPassword, + kSecAttrService: kServiceName, + kSecReturnAttributes: true, + kSecMatchLimit: kSecMatchLimitAll, + ] + + return executeSecCopyMatching(query: query) + .map { (result) in + let attrs = result as! [[CFString: Any]] + let accountTokens = attrs.compactMap { (dict) in + dict[kSecAttrAccount] as? String + } + + return accountTokens + } + } + + /// Get a persistent reference to the Keychain item for the given account token + static func getPersistentRef(account: String) -> Result<Data> { + let query: [CFString: Any] = [ + kSecClass: kSecClassGenericPassword, + kSecAttrAccount: account, + kSecAttrService: kServiceName, + kSecReturnPersistentRef: true + ] + + return executeSecCopyMatching(query: query) + .map { $0 as! Data } + } + + /// Get data associated with the given persistent Keychain reference + static func getItemData(persistentKeychainRef: Data) -> Result<Data> { + let query: [CFString: Any] = [ + kSecClass: kSecClassGenericPassword, + kSecValuePersistentRef: persistentKeychainRef, + kSecReturnData: true + ] + + return executeSecCopyMatching(query: query) + .map { $0 as! Data } + } + + /// Get data associated with the given account token + static func getItemData(account: String) -> Result<Data> { + let query: [CFString: Any] = [ + kSecClass: kSecClassGenericPassword, + kSecAttrAccount: account, + kSecAttrService: kServiceName, + kSecReturnData: true + ] + + return executeSecCopyMatching(query: query) + .map { $0 as! Data } + } + + /// Store data in the Keychain and associate it with the given account token + static func addItem(account: String, data: Data) -> Result<()> { + let attributes: [CFString: Any] = [ + kSecClass: kSecClassGenericPassword, + kSecAttrAccount: account, + kSecAttrService: kServiceName, + kSecValueData: data, + kSecReturnData: false, + + // Share the item with the application group + kSecAttrAccessGroup: ApplicationConfiguration.securityGroupIdentifier, + ] + + let status = SecItemAdd(attributes as CFDictionary, nil) + + return mapSecResult(status: status) { + () + } + } + + /// Replace the data associated with the given account token. + static func updateItem(account: String, data: Data) -> Result<()> { + let query: [CFString: Any] = [ + kSecClass: kSecClassGenericPassword, + kSecAttrAccount: account, + kSecAttrService: kServiceName, + ] + + let update: [CFString: Any] = [ + kSecValueData: data + ] + + let status = SecItemUpdate(query as CFDictionary, update as CFDictionary) + + return mapSecResult(status: status) { + () + } + } + + /// Remove the data associated with the given account token + static func removeItem(account: String) -> Result<()> { + let query: [CFString: Any] = [ + kSecClass: kSecClassGenericPassword, + kSecAttrAccount: account, + kSecAttrService: kServiceName, + ] + + let status = SecItemDelete(query as CFDictionary) + + return mapSecResult(status: status) { + () + } + } + + /// A private helper that verifies the given `status` and executes `body` on success + static private func mapSecResult<T>(status: OSStatus, body: () -> T) -> Result<T> { + if status == errSecSuccess { + return .success(body()) + } else { + return .failure(KeychainError(code: status)) + } + } + + /// A private helper to execute the given query using `SecCopyMatching` and map the result to + /// the `Result<CFTypeRef?>` type. + static private func executeSecCopyMatching(query: [CFString: Any]) -> Result<CFTypeRef?> { + var result: CFTypeRef? + let status = SecItemCopyMatching(query as CFDictionary, &result) + + return mapSecResult(status: status) { + result + } + } + +} diff --git a/ios/MullvadVPN/WireguardPrivateKey.swift b/ios/MullvadVPN/WireguardPrivateKey.swift new file mode 100644 index 0000000000..781e4f3917 --- /dev/null +++ b/ios/MullvadVPN/WireguardPrivateKey.swift @@ -0,0 +1,54 @@ +// +// WireguardPrivateKey.swift +// MullvadVPN +// +// Created by pronebird on 20/06/2019. +// Copyright © 2019 Amagicom AB. All rights reserved. +// + +import CryptoKit +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 + + /// Private key's raw representation + var rawRepresentation: Data { + return innerPrivateKey.rawRepresentation + } + + /// Public key's raw representation + var publicKeyRawRepresentation: Data { + return innerPrivateKey.publicKey.rawRepresentation + } + + /// Initialize the new private key + init() { + innerPrivateKey = CryptoKit.Curve25519.KeyAgreement.PrivateKey() + } + + /// Load with the existing private key + init(rawRepresentation: Data) throws { + innerPrivateKey = try CryptoKit.Curve25519.KeyAgreement.PrivateKey(rawRepresentation: rawRepresentation) + } + +} + +extension WireguardPrivateKey: Codable { + func encode(to encoder: Encoder) throws { + var container = encoder.singleValueContainer() + + try container.encode(innerPrivateKey.rawRepresentation) + } + + init(from decoder: Decoder) throws { + let container = try decoder.singleValueContainer() + + let privateKeyBytes = try container.decode(Data.self) + + self = try .init(rawRepresentation: privateKeyBytes) + } +} |
