From 39ae9db11c3b243b8171301e31f690b9ba3941ae Mon Sep 17 00:00:00 2001 From: Eric Kuck Date: Tue, 21 Aug 2018 11:00:41 -0500 Subject: [PATCH] Added ability to add tunnels with a QR code scan. Logic in place to parse conf files as well. Signed-off-by: Eric Kuck --- WireGuard.xcodeproj/project.pbxproj | 8 ++ WireGuard/Base.lproj/Main.storyboard | 37 +++++- WireGuard/Coordinators/AppCoordinator.swift | 41 ++++++- WireGuard/Info.plist | 2 + WireGuard/Models/Attribute.swift | 44 +++++++ WireGuard/Models/Interface+Extension.swift | 21 ++++ WireGuard/Models/Peer+Extension.swift | 19 ++++ WireGuard/Models/Tunnel+Extension.swift | 53 +++++++++ .../QRScanViewController.swift | 107 ++++++++++++++++++ ...nnelConfigurationTableViewController.swift | 1 - 10 files changed, 325 insertions(+), 8 deletions(-) create mode 100644 WireGuard/Models/Attribute.swift create mode 100644 WireGuard/ViewControllers/QRScanViewController.swift diff --git a/WireGuard.xcodeproj/project.pbxproj b/WireGuard.xcodeproj/project.pbxproj index 620f25c..e731b4e 100644 --- a/WireGuard.xcodeproj/project.pbxproj +++ b/WireGuard.xcodeproj/project.pbxproj @@ -46,6 +46,8 @@ 5FA1D4CD2124A05C00DBA2E6 /* Interface+Extension.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5FA1D4CC2124A05C00DBA2E6 /* Interface+Extension.swift */; }; 5FA1D5102124D80C00DBA2E6 /* String+Arrays.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5FA1D50F2124D80C00DBA2E6 /* String+Arrays.swift */; }; 5FA1D5122124DA6400DBA2E6 /* String+Base64.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5FA1D5112124DA6400DBA2E6 /* String+Base64.swift */; }; + 5FCC4343212B3092009A9C58 /* QRScanViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5FCC4342212B3092009A9C58 /* QRScanViewController.swift */; }; + 5FCC4347212B3E2C009A9C58 /* Attribute.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5FCC4346212B3E2C009A9C58 /* Attribute.swift */; }; /* End PBXBuildFile section */ /* Begin PBXContainerItemProxy section */ @@ -126,6 +128,8 @@ 5FA1D4CC2124A05C00DBA2E6 /* Interface+Extension.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Interface+Extension.swift"; sourceTree = ""; }; 5FA1D50F2124D80C00DBA2E6 /* String+Arrays.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "String+Arrays.swift"; sourceTree = ""; }; 5FA1D5112124DA6400DBA2E6 /* String+Base64.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "String+Base64.swift"; sourceTree = ""; }; + 5FCC4342212B3092009A9C58 /* QRScanViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = QRScanViewController.swift; sourceTree = ""; }; + 5FCC4346212B3E2C009A9C58 /* Attribute.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Attribute.swift; sourceTree = ""; }; 861983CAE8FDC13BC83E7E04 /* Pods_WireGuard.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_WireGuard.framework; sourceTree = BUILT_PRODUCTS_DIR; }; /* End PBXFileReference section */ @@ -235,6 +239,7 @@ 4A8AABD720B6A79100B6D8C1 /* UITableView+WireGuard.swift */, 4A4BACE720B5F1BF00F12B28 /* TunnelsTableViewController.swift */, 4A4BA6D720B73CBA00223AB8 /* TunnelConfigurationTableViewController.swift */, + 5FCC4342212B3092009A9C58 /* QRScanViewController.swift */, ); path = ViewControllers; sourceTree = ""; @@ -252,6 +257,7 @@ 4A4BAD1920B5F8FF00F12B28 /* Tunnel+CoreDataProperties.swift */, 4AC5462D2116306F00749D21 /* Tunnel+Extension.swift */, 4A4BAD1520B5F8DE00F12B28 /* WireGuard.xcdatamodeld */, + 5FCC4346212B3E2C009A9C58 /* Attribute.swift */, ); path = Models; sourceTree = ""; @@ -550,6 +556,7 @@ 4AEAC32B20F14BA9007B67AB /* Log.swift in Sources */, 4A4BAD1320B5F82400F12B28 /* Identifyable.swift in Sources */, 4A4BAD1720B5F8DE00F12B28 /* WireGuard.xcdatamodeld in Sources */, + 5FCC4347212B3E2C009A9C58 /* Attribute.swift in Sources */, 4A4BAD1A20B5F8FF00F12B28 /* Tunnel+CoreDataClass.swift in Sources */, 4A4BACE820B5F1BF00F12B28 /* TunnelsTableViewController.swift in Sources */, 4A4BAD1020B5F6EC00F12B28 /* RootCoordinator.swift in Sources */, @@ -557,6 +564,7 @@ 5FA1D4CB21249F7D00DBA2E6 /* Peer+Extension.swift in Sources */, 5FA1D5122124DA6400DBA2E6 /* String+Base64.swift in Sources */, 4AC5462E2116306F00749D21 /* Tunnel+Extension.swift in Sources */, + 5FCC4343212B3092009A9C58 /* QRScanViewController.swift in Sources */, 4A4BAD0E20B5F6C300F12B28 /* Coordinator.swift in Sources */, 4A4BA6D820B73CBA00223AB8 /* TunnelConfigurationTableViewController.swift in Sources */, 4A4BAD2020B6026900F12B28 /* Peer+CoreDataProperties.swift in Sources */, diff --git a/WireGuard/Base.lproj/Main.storyboard b/WireGuard/Base.lproj/Main.storyboard index 49fa373..80639f1 100644 --- a/WireGuard/Base.lproj/Main.storyboard +++ b/WireGuard/Base.lproj/Main.storyboard @@ -6,6 +6,7 @@ + @@ -73,12 +74,12 @@ - + - + @@ -472,7 +473,37 @@ - + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/WireGuard/Coordinators/AppCoordinator.swift b/WireGuard/Coordinators/AppCoordinator.swift index 75657f0..f57a9d5 100644 --- a/WireGuard/Coordinators/AppCoordinator.swift +++ b/WireGuard/Coordinators/AppCoordinator.swift @@ -32,7 +32,6 @@ class AppCoordinator: RootViewCoordinator { return self.tunnelsTableViewController } - var tunnelsTableViewController: TunnelsTableViewController! /// Window to manage @@ -134,10 +133,33 @@ class AppCoordinator: RootViewCoordinator { extension AppCoordinator: TunnelsTableViewControllerDelegate { func addProvider(tunnelsTableViewController: TunnelsTableViewController) { + let actionSheet = UIAlertController(title: nil, message: nil, preferredStyle: .actionSheet) + actionSheet.addAction(UIAlertAction(title: "Add Manually", style: .default) { [unowned self] _ in + self.addProviderManually() + }) + actionSheet.addAction(UIAlertAction(title: "Scan QR Code", style: .default) { [unowned self] _ in + self.addProviderWithQRScan() + }) + actionSheet.addAction(UIAlertAction(title: "Cancel", style: .cancel)) + + tunnelsTableViewController.present(actionSheet, animated: true, completion: nil) + } + + func addProviderManually() { let addContext = persistentContainer.newBackgroundContext() showTunnelConfigurationViewController(tunnel: nil, context: addContext) } + func addProviderWithQRScan() { + let addContext = persistentContainer.newBackgroundContext() + + let qrScanViewController = storyboard.instantiateViewController(type: QRScanViewController.self) + + qrScanViewController.configure(context: addContext, delegate: self) + + self.navigationController.pushViewController(qrScanViewController, animated: true) + } + func connect(tunnel: Tunnel, tunnelsTableViewController: TunnelsTableViewController) { let manager = self.providerManager(for: tunnel)! let block = { @@ -237,10 +259,8 @@ extension AppCoordinator: TunnelsTableViewControllerDelegate { return tunnelIdentifier == tunnel.tunnelIdentifier } } -} -extension AppCoordinator: TunnelConfigurationTableViewControllerDelegate { - func didSave(tunnel: Tunnel, tunnelConfigurationTableViewController: TunnelConfigurationTableViewController) { + private func saveTunnel(_ tunnel: Tunnel) { let manager = providerManager(for: tunnel) ?? NETunnelProviderManager() manager.localizedDescription = tunnel.title @@ -265,5 +285,18 @@ extension AppCoordinator: TunnelConfigurationTableViewControllerDelegate { navigationController.popToRootViewController(animated: true) } +} + +extension AppCoordinator: TunnelConfigurationTableViewControllerDelegate { + func didSave(tunnel: Tunnel, tunnelConfigurationTableViewController: TunnelConfigurationTableViewController) { + saveTunnel(tunnel) + } + +} + +extension AppCoordinator: QRScanViewControllerDelegate { + func didSave(tunnel: Tunnel, qrScanViewController: QRScanViewController) { + showTunnelConfigurationViewController(tunnel: tunnel, context: tunnel.managedObjectContext!) + } } diff --git a/WireGuard/Info.plist b/WireGuard/Info.plist index 685b9e2..f3073e3 100644 --- a/WireGuard/Info.plist +++ b/WireGuard/Info.plist @@ -2,6 +2,8 @@ + NSCameraUsageDescription + Camera is used to scan QR codes CFBundleDevelopmentRegion $(DEVELOPMENT_LANGUAGE) CFBundleExecutable diff --git a/WireGuard/Models/Attribute.swift b/WireGuard/Models/Attribute.swift new file mode 100644 index 0000000..51f7864 --- /dev/null +++ b/WireGuard/Models/Attribute.swift @@ -0,0 +1,44 @@ +// +// Attribute.swift +// WireGuard +// +// Created by Eric Kuck on 8/20/18. +// Copyright © 2018 Jason A. Donenfeld . All rights reserved. +// + +import Foundation + +struct Attribute { + + enum Key: String, CaseIterable { + case address = "Address" + case allowedIPs = "AllowedIPs" + case dns = "DNS" + case endpoint = "Endpoint" + case listenPort = "ListenPort" + case mtu = "MTU" + case persistentKeepalive = "PersistentKeepalive" + case presharedKey = "PresharedKey" + case privateKey = "PrivateKey" + case publicKey = "PublicKey" + } + + private static let separatorPattern = (try? NSRegularExpression(pattern: "\\s|=", options: []))! + + let line: String + let key: Key + let stringValue: String + var arrayValue: [String] { + return stringValue.commaSeparatedToArray() + } + + static func match(line: String) -> Attribute? { + guard let equalsIndex = line.firstIndex(of: "=") else { return nil } + let keyString = line[.. Tunnel { + let lines = text.split(separator: "\n") + + var currentPeer: Peer? + var isInInterfaceSection = false + + var tunnel: Tunnel! + context.performAndWait { + tunnel = Tunnel(context: context) + tunnel.interface = Interface(context: context) + } + tunnel.tunnelIdentifier = UUID().uuidString + + for line in lines { + var trimmedLine: String + if let commentRange = line.range(of: "#") { + trimmedLine = String(line[.. 0 else { continue } + + if "[interface]" == line.lowercased() { + currentPeer = nil + isInInterfaceSection = true + } else if "[peer]" == line.lowercased() { + context.performAndWait { currentPeer = Peer(context: context) } + tunnel.insertIntoPeers(currentPeer!, at: tunnel.peers?.count ?? 0) + isInInterfaceSection = false + } else if isInInterfaceSection, let attribute = Attribute.match(line: String(line)) { + try tunnel.interface!.parse(attribute: attribute) + } else if let currentPeer = currentPeer, let attribute = Attribute.match(line: String(line)) { + try currentPeer.parse(attribute: attribute) + } else { + throw TunnelParseError.invalidLine(String(line)) + } + } + + if !isInInterfaceSection && currentPeer == nil { + throw TunnelParseError.noConfigInfo + } + + return tunnel + } + } private func base64KeyToHex(_ base64: String?) -> String? { @@ -146,3 +194,8 @@ enum TunnelValidationError: Error { case nilPeers case invalidPeer } + +enum TunnelParseError: Error { + case invalidLine(_ line: String) + case noConfigInfo +} diff --git a/WireGuard/ViewControllers/QRScanViewController.swift b/WireGuard/ViewControllers/QRScanViewController.swift new file mode 100644 index 0000000..f15d30b --- /dev/null +++ b/WireGuard/ViewControllers/QRScanViewController.swift @@ -0,0 +1,107 @@ +// +// QRScanViewController.swift +// WireGuard +// +// Created by Eric Kuck on 8/20/18. +// Copyright © 2018 Jason A. Donenfeld . All rights reserved. +// + +import AVFoundation +import CoreData +import UIKit + +protocol QRScanViewControllerDelegate: class { + func didSave(tunnel: Tunnel, qrScanViewController: QRScanViewController) +} + +class QRScanViewController: UIViewController { + + private var viewContext: NSManagedObjectContext! + private weak var delegate: QRScanViewControllerDelegate? + var captureSession: AVCaptureSession? = AVCaptureSession() + let metadataOutput = AVCaptureMetadataOutput() + var previewLayer: AVCaptureVideoPreviewLayer! + + func configure(context: NSManagedObjectContext, delegate: QRScanViewControllerDelegate? = nil) { + viewContext = context + self.delegate = delegate + } + + override func viewDidLoad() { + super.viewDidLoad() + + guard let videoCaptureDevice = AVCaptureDevice.default(for: .video), + let videoInput = try? AVCaptureDeviceInput(device: videoCaptureDevice), + let captureSession = captureSession, + captureSession.canAddInput(videoInput), + captureSession.canAddOutput(metadataOutput) else { + scanDidEncounterError(title: "Scanning Not Supported", message: "This device does not have the ability to scan QR codes.") + return + } + + captureSession.addInput(videoInput) + captureSession.addOutput(metadataOutput) + + metadataOutput.setMetadataObjectsDelegate(self, queue: .main) + metadataOutput.metadataObjectTypes = [.qr] + + previewLayer = AVCaptureVideoPreviewLayer(session: captureSession) + previewLayer.frame = view.layer.bounds + previewLayer.videoGravity = .resizeAspectFill + view.layer.addSublayer(previewLayer) + } + + override func viewWillAppear(_ animated: Bool) { + super.viewWillAppear(animated) + + if captureSession?.isRunning == false { + captureSession?.startRunning() + } + } + + override func viewWillDisappear(_ animated: Bool) { + super.viewWillDisappear(animated) + + if captureSession?.isRunning == true { + captureSession?.stopRunning() + } + } + + func scanDidComplete(withCode code: String) { + do { + let tunnel = try Tunnel.fromConfig(code, context: viewContext) + delegate?.didSave(tunnel: tunnel, qrScanViewController: self) + } catch { + scanDidEncounterError(title: "Invalid Code", message: "The scanned code is not a valid WireGuard config file.") + } + } + + func scanDidEncounterError(title: String, message: String) { + let alertController = UIAlertController(title: title, message: message, preferredStyle: .alert) + alertController.addAction(UIAlertAction(title: "OK", style: .default, handler: { [weak self] _ in + self?.navigationController?.popViewController(animated: true) + })) + present(alertController, animated: true) + captureSession = nil + } + +} + +extension QRScanViewController: AVCaptureMetadataOutputObjectsDelegate { + func metadataOutput(_ output: AVCaptureMetadataOutput, didOutput metadataObjects: [AVMetadataObject], from connection: AVCaptureConnection) { + captureSession?.stopRunning() + + guard let metadataObject = metadataObjects.first, + let readableObject = metadataObject as? AVMetadataMachineReadableCodeObject, + let stringValue = readableObject.stringValue else { + scanDidEncounterError(title: "Invalid Code", message: "The scanned code could not be read.") + return + } + + AudioServicesPlaySystemSound(SystemSoundID(kSystemSoundID_Vibrate)) + scanDidComplete(withCode: stringValue) + } + +} + +extension QRScanViewController: Identifyable {} diff --git a/WireGuard/ViewControllers/TunnelConfigurationTableViewController.swift b/WireGuard/ViewControllers/TunnelConfigurationTableViewController.swift index 6159b00..8aff110 100644 --- a/WireGuard/ViewControllers/TunnelConfigurationTableViewController.swift +++ b/WireGuard/ViewControllers/TunnelConfigurationTableViewController.swift @@ -27,7 +27,6 @@ class TunnelConfigurationTableViewController: UITableViewController { viewContext = context self.delegate = delegate self.tunnel = tunnel ?? generateNewTunnelConfig() - } private func generateNewTunnelConfig() -> Tunnel {