summaryrefslogtreecommitdiffhomepage
diff options
context:
space:
mode:
-rw-r--r--ios/MullvadVPN/KeychainError.swift29
-rw-r--r--ios/MullvadVPN/RelayConstraints.swift140
-rw-r--r--ios/MullvadVPN/TunnelConfiguration.swift23
-rw-r--r--ios/MullvadVPN/TunnelConfigurationCoder.swift30
-rw-r--r--ios/MullvadVPN/TunnelConfigurationManager.swift214
-rw-r--r--ios/MullvadVPN/WireguardPrivateKey.swift54
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)
+ }
+}