diff --git a/Shared/Validators.swift b/Shared/Validators.swift index 17c8368..aef1019 100644 --- a/Shared/Validators.swift +++ b/Shared/Validators.swift @@ -28,6 +28,7 @@ public enum EndpointValidationError: Error { } } } + struct Endpoint { var ipAddress: String var port: Int32 @@ -72,3 +73,48 @@ func validateIpAddress(ipToValidate: String) -> AddressType { return .other } + +public enum CIDRAddressValidationError: Error { + case noIpAndSubnet(String) + case invalidIP(String) + case invalidSubnet(String) + + var localizedDescription: String { + switch self { + case .noIpAndSubnet: + return NSLocalizedString("CIDRAddressValidationError", comment: "Error message for malformed CIDR address.") + case .invalidIP: + return NSLocalizedString("CIDRAddressValidationError", comment: "Error message for invalid address ip.") + case .invalidSubnet: + return NSLocalizedString("CIDRAddressValidationError", comment: "Error message invalid address subnet.") + } + } +} + +struct CIDRAddress { + var ipAddress: String + var subnet: Int32 + var addressType: AddressType + + init?(stringRepresentation: String) throws { + guard let range = stringRepresentation.range(of: "/", options: .backwards, range: nil, locale: nil) else { + throw CIDRAddressValidationError.noIpAndSubnet(stringRepresentation) + } + + let ipString = stringRepresentation[.. - + - - - + @@ -140,7 +138,7 @@ - + @@ -336,7 +334,7 @@ - + @@ -390,16 +388,16 @@ - + - + @@ -411,16 +409,16 @@ - + - + diff --git a/WireGuard/Extensions/String+Arrays.swift b/WireGuard/Extensions/String+Arrays.swift new file mode 100644 index 0000000..2eadfdc --- /dev/null +++ b/WireGuard/Extensions/String+Arrays.swift @@ -0,0 +1,24 @@ +// +// String+Arrays.swift +// WireGuard +// +// Created by Eric Kuck on 8/15/18. +// Copyright © 2018 Jason A. Donenfeld . All rights reserved. +// + +import Foundation + +extension String { + + static func commaSeparatedStringFrom(elements: [String]) -> String { + return elements.joined(separator: ",") + } + + func commaSeparatedToArray() -> [String] { + return components(separatedBy: .whitespaces) + .joined() + .split(separator: ",") + .map(String.init) + } + +} diff --git a/WireGuard/Extensions/String+Base64.swift b/WireGuard/Extensions/String+Base64.swift new file mode 100644 index 0000000..e0b7d18 --- /dev/null +++ b/WireGuard/Extensions/String+Base64.swift @@ -0,0 +1,18 @@ +// +// String+Base64.swift +// WireGuard +// +// Created by Eric Kuck on 8/15/18. +// Copyright © 2018 Jason A. Donenfeld . All rights reserved. +// + +import Foundation + +extension String { + + func isBase64() -> Bool { + let base64Predicate = NSPredicate(format: "SELF MATCHES %@", "^([A-Za-z0-9+/]{4})*([A-Za-z0-9+/]{4}|[A-Za-z0-9+/]{3}=|[A-Za-z0-9+/]{2}==)$") + return base64Predicate.evaluate(with: self) + } + +} diff --git a/WireGuard/Models/Interface+Extension.swift b/WireGuard/Models/Interface+Extension.swift new file mode 100644 index 0000000..735c7a2 --- /dev/null +++ b/WireGuard/Models/Interface+Extension.swift @@ -0,0 +1,46 @@ +// +// Interface+Extension.swift +// WireGuard +// +// Created by Eric Kuck on 8/15/18. +// Copyright © 2018 Jason A. Donenfeld . All rights reserved. +// + +import Foundation + +extension Interface { + + func validate() throws { + guard let privateKey = privateKey, !privateKey.isEmpty else { + throw InterfaceValidationError.emptyPrivateKey + } + + guard privateKey.isBase64() else { + throw InterfaceValidationError.invalidPrivateKey + } + + try? addresses?.commaSeparatedToArray().forEach { address in + do { + try _ = CIDRAddress(stringRepresentation: address) + } catch { + throw InterfaceValidationError.invalidAddress(cause: error) + } + } + + try? dns?.commaSeparatedToArray().forEach { address in + do { + try _ = Endpoint(endpointString: address) + } catch { + throw InterfaceValidationError.invalidDNSServer(cause: error) + } + } + } + +} + +enum InterfaceValidationError: Error { + case emptyPrivateKey + case invalidPrivateKey + case invalidAddress(cause: Error) + case invalidDNSServer(cause: Error) +} diff --git a/WireGuard/Models/Peer+CoreDataProperties.swift b/WireGuard/Models/Peer+CoreDataProperties.swift index 0308c53..32187df 100644 --- a/WireGuard/Models/Peer+CoreDataProperties.swift +++ b/WireGuard/Models/Peer+CoreDataProperties.swift @@ -19,7 +19,7 @@ extension Peer { @NSManaged public var presharedKey: String? @NSManaged public var allowedIPs: String? @NSManaged public var endpoint: String? - @NSManaged public var persistentKeepalive: Int16 + @NSManaged public var persistentKeepalive: Int32 @NSManaged public var tunnel: Tunnel? } diff --git a/WireGuard/Models/Peer+Extension.swift b/WireGuard/Models/Peer+Extension.swift new file mode 100644 index 0000000..c27748c --- /dev/null +++ b/WireGuard/Models/Peer+Extension.swift @@ -0,0 +1,56 @@ +// +// Peer+Extension.swift +// WireGuard +// +// Created by Eric Kuck on 8/15/18. +// Copyright © 2018 Jason A. Donenfeld . All rights reserved. +// + +import Foundation + +extension Peer { + + func validate() throws { + guard let publicKey = publicKey, !publicKey.isEmpty else { + throw PeerValidationError.emptyPublicKey + } + + guard publicKey.isBase64() else { + throw PeerValidationError.invalidPublicKey + } + + guard let allowedIPs = allowedIPs, !allowedIPs.isEmpty else { + throw PeerValidationError.nilAllowedIps + } + + try allowedIPs.commaSeparatedToArray().forEach { address in + do { + try _ = CIDRAddress(stringRepresentation: address) + } catch { + throw PeerValidationError.invalidAllowedIPs(cause: error) + } + } + + if let endpoint = endpoint { + do { + try _ = Endpoint(endpointString: endpoint) + } catch { + throw PeerValidationError.invalidEndpoint(cause: error) + } + } + + guard persistentKeepalive >= 0, persistentKeepalive <= 65535 else { + throw PeerValidationError.invalidPersistedKeepAlive + } + } + +} + +enum PeerValidationError: Error { + case emptyPublicKey + case invalidPublicKey + case nilAllowedIps + case invalidAllowedIPs(cause: Error) + case invalidEndpoint(cause: Error) + case invalidPersistedKeepAlive +} diff --git a/WireGuard/Models/Tunnel+Extension.swift b/WireGuard/Models/Tunnel+Extension.swift index 4ef7948..f181e77 100644 --- a/WireGuard/Models/Tunnel+Extension.swift +++ b/WireGuard/Models/Tunnel+Extension.swift @@ -7,6 +7,7 @@ // import Foundation +import CoreData extension Tunnel { public func generateProviderConfiguration() -> [String: Any] { @@ -38,7 +39,7 @@ extension Tunnel { return providerConfiguration } - private func generateInterfaceProviderConfiguration(_ interface: Interface) -> String { + private func generateInterfaceProviderConfiguration(_ interface: Interface) -> String { var settingsString = "" if let hexPrivateKey = base64KeyToHex(interface.privateKey) { @@ -54,7 +55,7 @@ extension Tunnel { return settingsString } - private func generatePeerProviderConfiguration(_ peer: Peer) -> String { + private func generatePeerProviderConfiguration(_ peer: Peer) -> String { var settingsString = "" if let hexPublicKey = base64KeyToHex(peer.publicKey) { @@ -77,6 +78,39 @@ extension Tunnel { return settingsString } + + func validate() throws { + let nameRegex = "[a-zA-Z0-9_=+.-]{1,15}" + let nameTest = NSPredicate(format: "SELF MATCHES %@", nameRegex) + guard let title = title, nameTest.evaluate(with: title) else { + throw TunnelValidationError.invalidTitle + } + + let fetchRequest: NSFetchRequest = Tunnel.fetchRequest() + fetchRequest.predicate = NSPredicate(format: "title == %@", title) + guard (try? managedObjectContext?.count(for: fetchRequest)) == 1 else { + throw TunnelValidationError.titleExists + } + + guard let interface = interface else { + throw TunnelValidationError.nilInterface + } + + try interface.validate() + + guard let peers = peers else { + throw TunnelValidationError.nilPeers + } + + try peers.forEach { + guard let peer = $0 as? Peer else { + throw TunnelValidationError.invalidPeer + } + + try peer.validate() + } + } + } private func base64KeyToHex(_ base64: String?) -> String? { @@ -104,3 +138,11 @@ private func base64KeyToHex(_ base64: String?) -> String? { return hexKey } + +enum TunnelValidationError: Error { + case invalidTitle + case titleExists + case nilInterface + case nilPeers + case invalidPeer +} diff --git a/WireGuard/Models/WireGuard.xcdatamodeld/WireGuard.xcdatamodel/contents b/WireGuard/Models/WireGuard.xcdatamodeld/WireGuard.xcdatamodel/contents index 398ed39..e818bd0 100644 --- a/WireGuard/Models/WireGuard.xcdatamodeld/WireGuard.xcdatamodel/contents +++ b/WireGuard/Models/WireGuard.xcdatamodeld/WireGuard.xcdatamodel/contents @@ -1,5 +1,5 @@ - + @@ -11,7 +11,7 @@ - + diff --git a/WireGuard/ViewControllers/TunnelConfigurationTableViewController.swift b/WireGuard/ViewControllers/TunnelConfigurationTableViewController.swift index 85d3581..6159b00 100644 --- a/WireGuard/ViewControllers/TunnelConfigurationTableViewController.swift +++ b/WireGuard/ViewControllers/TunnelConfigurationTableViewController.swift @@ -101,6 +101,13 @@ class TunnelConfigurationTableViewController: UITableViewController { @IBAction func saveTunnelConfiguration(_ sender: Any) { Promise(resolver: { (seal) in + do { + try tunnel.validate() + } catch { + seal.reject(error) + return + } + viewContext.perform({ self.viewContext.saveContext({ (result) in switch result { @@ -115,7 +122,9 @@ class TunnelConfigurationTableViewController: UITableViewController { self.delegate?.didSave(tunnel: self.tunnel, tunnelConfigurationTableViewController: self) return Promise.value(()) }.catch { error in - print("Error saving: \(error)") + let alert = UIAlertController(title: "Error", message: "\(error)", preferredStyle: .alert) + alert.addAction(UIAlertAction(title: "OK", style: .default, handler: nil)) + self.present(alert, animated: true, completion: nil) } } } @@ -220,7 +229,7 @@ extension PeerTableViewCell: UITextFieldDelegate { } else if sender == endpointField { peer.endpoint = string } else if sender == persistentKeepaliveField { - if let string = string, let persistentKeepalive = Int16(string) { + if let string = string, let persistentKeepalive = Int32(string) { peer.persistentKeepalive = persistentKeepalive } } diff --git a/WireGuardTests/ValidatorsTests.swift b/WireGuardTests/ValidatorsTests.swift index df70c89..c3398bc 100644 --- a/WireGuardTests/ValidatorsTests.swift +++ b/WireGuardTests/ValidatorsTests.swift @@ -62,4 +62,62 @@ class ValidatorsTests: XCTestCase { executeTest(endpointString: "192.168.0.1") executeTest(endpointString: "12345") } + + func testCIDRAddress() throws { + _ = try CIDRAddress(stringRepresentation: "2607:f938:3001:4000::aac/24") + _ = try CIDRAddress(stringRepresentation: "192.168.0.1/24") + } + + func testIPv4CIDRAddress() throws { + _ = try CIDRAddress(stringRepresentation: "192.168.0.1/24") + } + + func testCIDRAddress_invalidIP() throws { + func executeTest(stringRepresentation: String, ipString: String, file: StaticString = #file, line: UInt = #line) { + XCTAssertThrowsError(try CIDRAddress(stringRepresentation: stringRepresentation)) { (error) in + guard case CIDRAddressValidationError.invalidIP(let value) = error else { + return XCTFail("Unexpected error: \(error)", file: file, line: line) + } + XCTAssertEqual(value, ipString, file: file, line: line) + } + } + + executeTest(stringRepresentation: "12345/12345", ipString: "12345") + executeTest(stringRepresentation: "/12345", ipString: "") + } + + func testCIDRAddress_invalidSubnet() throws { + func executeTest(stringRepresentation: String, subnetString: String, file: StaticString = #file, line: UInt = #line) { + XCTAssertThrowsError(try CIDRAddress(stringRepresentation: stringRepresentation)) { (error) in + guard case CIDRAddressValidationError.invalidSubnet(let value) = error else { + return XCTFail("Unexpected error: \(error)", file: file, line: line) + } + XCTAssertEqual(value, subnetString, file: file, line: line) + } + } + + executeTest(stringRepresentation: "/", subnetString: "") + executeTest(stringRepresentation: "2607:f938:3001:4000::aac/a", subnetString: "a") + executeTest(stringRepresentation: "2607:f938:3001:4000:/aac", subnetString: "aac") + executeTest(stringRepresentation: "2607:f938:3001:4000::aac/", subnetString: "") + executeTest(stringRepresentation: "192.168.0.1/a", subnetString: "a") + executeTest(stringRepresentation: "192.168.0.1/", subnetString: "") + + } + + func testCIDRAddress_noIpAndSubnet() throws { + + func executeTest(stringRepresentation: String, file: StaticString = #file, line: UInt = #line) { + XCTAssertThrowsError(try CIDRAddress(stringRepresentation: stringRepresentation)) { (error) in + guard case CIDRAddressValidationError.noIpAndSubnet(let value) = error else { + return XCTFail("Unexpected error: \(error)", file: file, line: line) + } + XCTAssertEqual(value, stringRepresentation, file: file, line: line) + } + } + + executeTest(stringRepresentation: "192.168.0.1") + executeTest(stringRepresentation: "12345") + } + }