From 149aa77bb50bfb2ab29faf3398d13423c2501a9c Mon Sep 17 00:00:00 2001 From: Carlos Cabanero Date: Mon, 10 Jun 2024 19:08:09 -0400 Subject: [PATCH] Default Agent configuration --- Blink.xcodeproj/project.pbxproj | 20 +- Blink/Commands/ssh/SSHAgentAdd.swift | 85 ++++---- Blink/Commands/ssh/SSHAgentPool.swift | 74 ------- Blink/Commands/ssh/SSHConfigProvider.swift | 2 +- Blink/Commands/ssh/SSHDefaultAgent.swift | 179 +++++++++++++++ BlinkConfig/BlinkPaths.h | 4 + BlinkConfig/BlinkPaths.m | 35 +-- SSH/Agent.swift | 22 +- Settings/SettingsView.swift | 25 ++- .../AgentSettings/AgentSettingsView.swift | 205 ++++++++++++++++++ .../BKPubKey/KeyPickerView.swift | 4 + 11 files changed, 497 insertions(+), 158 deletions(-) delete mode 100644 Blink/Commands/ssh/SSHAgentPool.swift create mode 100644 Blink/Commands/ssh/SSHDefaultAgent.swift create mode 100644 Settings/ViewControllers/AgentSettings/AgentSettingsView.swift diff --git a/Blink.xcodeproj/project.pbxproj b/Blink.xcodeproj/project.pbxproj index 93d8507d4..53544f7d1 100644 --- a/Blink.xcodeproj/project.pbxproj +++ b/Blink.xcodeproj/project.pbxproj @@ -139,7 +139,7 @@ BD8DB648279B512900497C88 /* CodeFileSystemService.swift in Sources */ = {isa = PBXBuildFile; fileRef = BD8DB646279B512900497C88 /* CodeFileSystemService.swift */; }; BD90BE4A2A18466E00DA5686 /* AgentForwardPromptPickerView.swift in Sources */ = {isa = PBXBuildFile; fileRef = BD90BE492A18466E00DA5686 /* AgentForwardPromptPickerView.swift */; }; BD98AC84260BD8DC00B4E6A1 /* SSHAgentAdd.swift in Sources */ = {isa = PBXBuildFile; fileRef = BD98AC83260BD8DC00B4E6A1 /* SSHAgentAdd.swift */; }; - BD98AC95260BE20000B4E6A1 /* SSHAgentPool.swift in Sources */ = {isa = PBXBuildFile; fileRef = BD98AC94260BE20000B4E6A1 /* SSHAgentPool.swift */; }; + BD98AC95260BE20000B4E6A1 /* SSHDefaultAgent.swift in Sources */ = {isa = PBXBuildFile; fileRef = BD98AC94260BE20000B4E6A1 /* SSHDefaultAgent.swift */; }; BD9BF7E7262A6B0300B02074 /* SOCKS.swift in Sources */ = {isa = PBXBuildFile; fileRef = BD9BF7E3262A6B0300B02074 /* SOCKS.swift */; }; BD9BF7E9262A6B0F00B02074 /* SOCKSTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = BD9BF7E8262A6B0F00B02074 /* SOCKSTests.swift */; }; BD9EA1802718D6C400874007 /* NSFileProviderError.swift in Sources */ = {isa = PBXBuildFile; fileRef = BD9EA17C2718D6C400874007 /* NSFileProviderError.swift */; }; @@ -199,6 +199,7 @@ BDF2B8D82BC4820F00B9C7EA /* curl_ios.xcframework in Embed Frameworks */ = {isa = PBXBuildFile; fileRef = BDF2B8D62BC481F000B9C7EA /* curl_ios.xcframework */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; }; BDF2B8DD2BC48D2800B9C7EA /* LibSSH.xcframework in Frameworks */ = {isa = PBXBuildFile; fileRef = D2334EC425C1C04700385378 /* LibSSH.xcframework */; }; BDF2B8DF2BC48D2D00B9C7EA /* libssh2.xcframework in Frameworks */ = {isa = PBXBuildFile; fileRef = D2F64C9525CA99AD00F2225D /* libssh2.xcframework */; }; + BDF40FEB2C14A6CE00DF41C1 /* AgentSettingsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = BDF40FE92C14A6CE00DF41C1 /* AgentSettingsView.swift */; }; BDF471BA268CD17B00A7A41B /* SSH.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 07FABB8425C9AEC000E1CC2C /* SSH.framework */; }; C94437571D8311960096F84E /* BKResource.m in Sources */ = {isa = PBXBuildFile; fileRef = C94437561D8311960096F84E /* BKResource.m */; }; C94437601D831CD30096F84E /* Themes in Resources */ = {isa = PBXBuildFile; fileRef = C944375F1D831CD30096F84E /* Themes */; }; @@ -861,7 +862,7 @@ BD8DB646279B512900497C88 /* CodeFileSystemService.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = CodeFileSystemService.swift; sourceTree = ""; }; BD90BE492A18466E00DA5686 /* AgentForwardPromptPickerView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AgentForwardPromptPickerView.swift; sourceTree = ""; }; BD98AC83260BD8DC00B4E6A1 /* SSHAgentAdd.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SSHAgentAdd.swift; sourceTree = ""; }; - BD98AC94260BE20000B4E6A1 /* SSHAgentPool.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SSHAgentPool.swift; sourceTree = ""; }; + BD98AC94260BE20000B4E6A1 /* SSHDefaultAgent.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SSHDefaultAgent.swift; sourceTree = ""; }; BD9BF7E3262A6B0300B02074 /* SOCKS.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SOCKS.swift; sourceTree = ""; }; BD9BF7E8262A6B0F00B02074 /* SOCKSTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SOCKSTests.swift; sourceTree = ""; }; BD9EA17C2718D6C400874007 /* NSFileProviderError.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = NSFileProviderError.swift; sourceTree = ""; }; @@ -913,6 +914,7 @@ BDE84C772BB33AD700457391 /* vim.xcframework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.xcframework; name = vim.xcframework; path = xcfs/.build/artifacts/xcfs/vim/vim.xcframework; sourceTree = SOURCE_ROOT; }; BDEEE36B2B8951D3003003FD /* get_frameworks.sh */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.script.sh; path = get_frameworks.sh; sourceTree = ""; }; BDF2B8D62BC481F000B9C7EA /* curl_ios.xcframework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.xcframework; name = curl_ios.xcframework; path = xcfs/.build/artifacts/xcfs/curl_ios/curl_ios.xcframework; sourceTree = SOURCE_ROOT; }; + BDF40FE92C14A6CE00DF41C1 /* AgentSettingsView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AgentSettingsView.swift; sourceTree = ""; }; C94437551D8311960096F84E /* BKResource.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = BKResource.h; sourceTree = ""; }; C94437561D8311960096F84E /* BKResource.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = BKResource.m; sourceTree = ""; }; C944375F1D831CD30096F84E /* Themes */ = {isa = PBXFileReference; lastKnownFileType = folder; path = Themes; sourceTree = ""; }; @@ -1535,7 +1537,7 @@ 07FAB8EE25C8E6C500E1CC2C /* SSHConfig.swift */, 07FAB8EF25C8E6C500E1CC2C /* SSHConfigProvider.swift */, BD98AC83260BD8DC00B4E6A1 /* SSHAgentAdd.swift */, - BD98AC94260BE20000B4E6A1 /* SSHAgentPool.swift */, + BD98AC94260BE20000B4E6A1 /* SSHDefaultAgent.swift */, BDE7125C2A141E3100164F70 /* SSHAgentUserPrompt.swift */, ); path = ssh; @@ -1833,6 +1835,14 @@ path = BlinkConfigTests; sourceTree = ""; }; + BDF40FEA2C14A6CE00DF41C1 /* AgentSettings */ = { + isa = PBXGroup; + children = ( + BDF40FE92C14A6CE00DF41C1 /* AgentSettingsView.swift */, + ); + path = AgentSettings; + sourceTree = ""; + }; C989E53B1D6CC488003E0079 /* BKHosts */ = { isa = PBXGroup; children = ( @@ -1924,6 +1934,7 @@ C9B2E0141D6B612300B89F69 /* ViewControllers */ = { isa = PBXGroup; children = ( + BDF40FEA2C14A6CE00DF41C1 /* AgentSettings */, D21076992A69231D00B3D77E /* Snippets */, D2B788862949E8A400F19E4F /* Build */, D2AD8E8527A2C81900DED28D /* Subscriptions */, @@ -3298,7 +3309,7 @@ 07F670761D05EEE200C0A53C /* SSHCopyIDSession.m in Sources */, D264D2B428F84592002B1B14 /* Models.swift in Sources */, D22B16D828CF6ED20004EEC1 /* NewPasskeyView.swift in Sources */, - BD98AC95260BE20000B4E6A1 /* SSHAgentPool.swift in Sources */, + BD98AC95260BE20000B4E6A1 /* SSHDefaultAgent.swift in Sources */, D2C244352390FEEF0082C69C /* KeyBindingAction.swift in Sources */, D241CBDA23040734003D64A5 /* KBTraits.swift in Sources */, B752EE2B1DFEF19D00E305C8 /* BKUserConfigurationManager.m in Sources */, @@ -3441,6 +3452,7 @@ D22277FE2A26204900D4C708 /* SnippetView.swift in Sources */, D2499BEC2362EFD40009C701 /* cpp.cpp in Sources */, D264D2B328F84592002B1B14 /* UnavailErrorView.swift in Sources */, + BDF40FEB2C14A6CE00DF41C1 /* AgentSettingsView.swift in Sources */, D2B788852949C53100F19E4F /* BuildView.swift in Sources */, D20CBA5A2360324100D93301 /* CompleteUtils.swift in Sources */, D265FBC9231905AC0017EAC4 /* NSCoder+CodingKey.swift in Sources */, diff --git a/Blink/Commands/ssh/SSHAgentAdd.swift b/Blink/Commands/ssh/SSHAgentAdd.swift index ef89263d0..8a1fb16ca 100644 --- a/Blink/Commands/ssh/SSHAgentAdd.swift +++ b/Blink/Commands/ssh/SSHAgentAdd.swift @@ -40,42 +40,43 @@ import ios_system struct BlinkSSHAgentAddCommand: ParsableCommand { static var configuration = CommandConfiguration( commandName: "ssh-agent", - abstract: "Blink Agent Control", + abstract: "Blink Default Agent Control", discussion: """ + You can also configure the default agent from Settings > Agent. """, version: "1.0.0" ) - + @Flag(name: [.customShort("L")], help: "List keys stored on agent") var list: Bool = false - + @Flag(name: [.customShort("l")], help: "Lists fingerprints of keys stored on agent") var listFingerprints: Bool = false - + // Remove @Flag(name: [.customShort("d")], help: "Remove key from agent") var remove: Bool = false - + // Hash algorithm @Option( name: [.customShort("E")], help: "Specify hash algorithm used for fingerprints" ) var hashAlgorithm: String = "sha256" - - @Flag(name: [.customShort("c")], - help: "Confirm before using identity" - ) - var askConfirmation: Bool = false + + // @Flag(name: [.customShort("c")], + // help: "Confirm before using identity" + // ) + // var askConfirmation: Bool = false @Argument(help: "Key name") var keyName: String? - - @Argument(help: "Agent name") - var agentName: String? + + // @Argument(help: "Agent name") + // var agentName: String? } @_cdecl("blink_ssh_add") @@ -91,42 +92,43 @@ public func blink_ssh_add(argc: Int32, argv: Argv) -> Int32 { public class BlinkSSHAgentAdd: NSObject { var command: BlinkSSHAgentAddCommand! - + var stdout = OutputStream(file: thread_stdout) var stderr = OutputStream(file: thread_stderr) let currentRunLoop = RunLoop.current - + public func start(_ argc: Int32, argv: [String], session: MCPSession) -> Int32 { - let bkConfig: BKConfig - do { - bkConfig = try BKConfig() - command = try BlinkSSHAgentAddCommand.parse(Array(argv[1...])) + do { + command = try BlinkSSHAgentAddCommand.parse(Array(argv[1...])) } catch { let message = BlinkSSHAgentAddCommand.message(for: error) print(message, to: &stderr) return -1 } - + + let _ = SSHDefaultAgent.instance + if command.remove { let keyName = command.keyName ?? "id_rsa" - if let _ = SSHAgentPool.removeKey(named: keyName) { + do { + let _ = try SSHDefaultAgent.removeKey(named: keyName) print("Key \(keyName) removed.", to: &stdout) return 0 - } else { - print("Key not found on Agent", to: &stderr) + } catch { + print("Couldn't remove key: \(error)", to: &stderr) return -1 } } - + if command.list { - for key in SSHAgentPool.get()?.ring ?? [] { + for key in SSHDefaultAgent.instance.ring { let str = BKPubKey.withID(key.name)?.publicKey ?? "" print("\(str) \(key.name)", to: &stdout) } - + return 0; } - + if command.listFingerprints { guard let alg = SSHDigest(rawValue: command.hashAlgorithm) @@ -134,37 +136,26 @@ public class BlinkSSHAgentAdd: NSObject { print("Invalid hash algorithm \"\(command.hashAlgorithm)\"", to: &stderr) return -1; } - - for key in SSHAgentPool.get()?.ring ?? [] { + + for key in SSHDefaultAgent.instance.ring { if let blob = try? key.signer.publicKey.encode()[4...], let sshkey = try? SSHKey(fromPublicBlob: blob) { let str = sshkey.fingerprint(digest: alg) - + print("\(sshkey.size) \(str) \(key.name) (\(sshkey.sshKeyType.shortName))", to: &stdout) } } return 0 } - - // TODO Can we have the same key under different constraints? - + // Default case: add key - if let (signer, name) = bkConfig.signer(forIdentity: command.keyName ?? "id_rsa") { - if let signer = signer as? BlinkConfig.InputPrompter { - signer.setPromptOnView(session.device.view) - } - var constraints: [SSHAgentConstraint]? = nil - if command.askConfirmation { - constraints = [SSHAgentUserPrompt()] - } - - SSHAgentPool.addKey(signer, named: name, constraints: constraints) - print("Key \(name) - added to agent.", to: &stdout) + do { + try SSHDefaultAgent.addKey(named: command.keyName ?? "id_rsa") return 0 - } else { - print("Key not found", to: &stderr) - return -1 + } catch { + print("Could not add key \(error)", to: &stderr) + return -1; } } } diff --git a/Blink/Commands/ssh/SSHAgentPool.swift b/Blink/Commands/ssh/SSHAgentPool.swift deleted file mode 100644 index 55c509c21..000000000 --- a/Blink/Commands/ssh/SSHAgentPool.swift +++ /dev/null @@ -1,74 +0,0 @@ -////////////////////////////////////////////////////////////////////////////////// -// -// B L I N K -// -// Copyright (C) 2016-2019 Blink Mobile Shell Project -// -// This file is part of Blink. -// -// Blink is free software: you can redistribute it and/or modify -// it under the terms of the GNU General Public License as published by -// the Free Software Foundation, either version 3 of the License, or -// (at your option) any later version. -// -// Blink is distributed in the hope that it will be useful, -// but WITHOUT ANY WARRANTY; without even the implied warranty of -// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -// GNU General Public License for more details. -// -// You should have received a copy of the GNU General Public License -// along with Blink. If not, see . -// -// In addition, Blink is also subject to certain additional terms under -// GNU GPL version 3 section 7. -// -// You should have received a copy of these additional terms immediately -// following the terms and conditions of the GNU General Public License -// which accompanied the Blink Source Code. If not, see -// . -// -//////////////////////////////////////////////////////////////////////////////// - - -import Foundation - -import SSH - - -public let DefaultAgentName = "default" - -final class SSHAgentPool { - private static let shared = SSHAgentPool() - public static let defaultAgent = SSHAgentPool.shared.agent(DefaultAgentName) - - private var agents: [String:SSHAgent] = [:] - - private init() {} - - func agent(_ name: String) -> SSHAgent { - if let agent = agents[name] { - return agent - } else { - let agent = SSHAgent() - agents[name] = agent - return agent - } - } - - static func get(agent name: String = DefaultAgentName) -> SSHAgent? { - return Self.shared.agents[name] - } - - static func addKey(_ key: Signer, named keyName: String, constraints: [SSHAgentConstraint]? = nil, toAgent agentName: String = DefaultAgentName) { - let agent = Self.shared.agent(agentName) - agent.loadKey(key, aka: keyName, constraints: constraints) - } - - static func removeKey(named keyName: String, fromAgent agentName: String = DefaultAgentName) -> Signer? { - guard let agent = Self.shared.agents[agentName] else { - return nil - } - - return agent.removeKey(keyName) - } -} diff --git a/Blink/Commands/ssh/SSHConfigProvider.swift b/Blink/Commands/ssh/SSHConfigProvider.swift index 76d2fbbc8..6f5fa41ae 100644 --- a/Blink/Commands/ssh/SSHConfigProvider.swift +++ b/Blink/Commands/ssh/SSHConfigProvider.swift @@ -142,7 +142,7 @@ extension SSHClientConfigProvider { } // Link to Default Agent - agent.linkTo(agent: SSHAgentPool.defaultAgent) + agent.linkTo(agent: SSHDefaultAgent.instance) return agent } diff --git a/Blink/Commands/ssh/SSHDefaultAgent.swift b/Blink/Commands/ssh/SSHDefaultAgent.swift new file mode 100644 index 000000000..8a6a884b1 --- /dev/null +++ b/Blink/Commands/ssh/SSHDefaultAgent.swift @@ -0,0 +1,179 @@ +////////////////////////////////////////////////////////////////////////////////// +// +// B L I N K +// +// Copyright (C) 2016-2019 Blink Mobile Shell Project +// +// This file is part of Blink. +// +// Blink is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// Blink is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with Blink. If not, see . +// +// In addition, Blink is also subject to certain additional terms under +// GNU GPL version 3 section 7. +// +// You should have received a copy of these additional terms immediately +// following the terms and conditions of the GNU General Public License +// which accompanied the Blink Source Code. If not, see +// . +// +//////////////////////////////////////////////////////////////////////////////// + + +import Foundation +import SSH + + +final class SSHDefaultAgent { + public static var instance: SSHAgent { + if let agent = Self._instance { + return agent + } else { + return Self.load() + } + } + private static var _instance: SSHAgent? = nil + private init() {} + private static let defaultAgentFile: URL = BlinkPaths.blinkAgentSettingsURL().appendingPathComponent("default") + + enum Error: Swift.Error, LocalizedError { + case KeyIsMissing + + var localizedDescription: String { + switch self { + case .KeyIsMissing: + "Key is missing" + } + } + } + + private static func load() -> SSHAgent { + let instance = SSHAgent() + Self._instance = instance + // If the Settings are not available, the agent is initialized with the default configuration. + // If there is a problem with the location, it is safe to assume that it will persist. + if let settings = try? getSettings() { + try? applySettings(settings) + } else { + try? setSettings(BKAgentSettings(prompt: .Confirm, keys: [])) + } + return instance + } + + static func setSettings(_ settings: BKAgentSettings) throws { + try BKAgentSettings.save(settings: settings, to: defaultAgentFile) + try applySettings(settings) + } + + private static func applySettings(_ settings: BKAgentSettings) throws { + let agent = Self.instance + agent.clear() + + let bkConfig = try BKConfig() + + settings.keys.forEach { key in + if let (signer, name) = bkConfig.signer(forIdentity: key) { + if let constraints = settings.constraints() { + agent.loadKey(signer, aka: name, constraints: constraints) + } + } + } + } + + static func getSettings() throws -> BKAgentSettings { + try BKAgentSettings.load(from: defaultAgentFile) + } + + // Applying settings clears the agent first. Adding a key doesn't modify or reset previous constraints. + static func addKey(named keyName: String) throws { + let settings = try getSettings() + if settings.keys.contains(keyName) { + return + } + + let agent = Self.instance + let bkConfig = try BKConfig() + + if let (signer, name) = bkConfig.signer(forIdentity: keyName) { + var keys = settings.keys + keys.append(keyName) + let newSettings = BKAgentSettings(prompt: settings.prompt, keys: keys) + + do { + try BKAgentSettings.save(settings: newSettings, to: defaultAgentFile) + } catch { + } + + if let constraints = settings.constraints() { + agent.loadKey(signer, aka: keyName, constraints: constraints) + } + } else { + throw Error.KeyIsMissing + } + } + + static func removeKey(named keyName: String) throws -> Signer? { + // Remove from settings and apply + let settings = try getSettings() + guard settings.keys.contains(keyName) else { + return nil + } + + var keys = settings.keys + keys.removeAll(where: { $0 == keyName }) + try BKAgentSettings.save(settings: BKAgentSettings(prompt: settings.prompt, keys: keys), to: defaultAgentFile) + + return Self.instance.removeKey(keyName) + } +} + +// NOTE Another way to represent the Agent would be to just share the current state by reading the file, +// instead of having the state stored in a variable. This would be best for concurrency too, but that shouldn't +// be a problem now. + +enum BKAgentSettingsPrompt: String, Codable { + case Deny = "Deny" + case Confirm = "Confirm" + case Allow = "Allow" +} + +struct BKAgentSettings: Codable, Equatable { + let prompt: BKAgentSettingsPrompt + let keys: [String] + +// init(prompt: BKAgentSettingsPrompt, keys: [String]) { +// self.prompt = prompt +// self.keys = keys +// } + + static func save(settings: BKAgentSettings, to file: URL) throws { + let data = try JSONEncoder().encode(settings) + try data.write(to: file) + } + + static func load(from file: URL) throws -> BKAgentSettings { + let data = try Data(contentsOf: file) + return try JSONDecoder().decode(BKAgentSettings.self, from: data) + } + + func constraints() -> [SSHAgentConstraint]? { + return switch prompt { + case .Confirm: + [SSHAgentUserPrompt()] + case .Allow: + [] + default: + nil + } + } +} diff --git a/BlinkConfig/BlinkPaths.h b/BlinkConfig/BlinkPaths.h index 1a8240e7f..a60652311 100644 --- a/BlinkConfig/BlinkPaths.h +++ b/BlinkConfig/BlinkPaths.h @@ -43,10 +43,14 @@ + (NSString *) blink; // ~/.blink-build + (NSString *)blinkBuild; +// ~/.blink/agents ++ (NSString *)blinkAgentSettings; + // ~/.ssh + (NSString *) ssh; + (NSURL *) blinkURL; ++ (NSURL *) blinkAgentSettingsURL; + (NSURL *) blinkBuildURL; + (NSURL *) blinkBuildTokenURL; + (NSURL *)blinkBuildStagingMarkURL; diff --git a/BlinkConfig/BlinkPaths.m b/BlinkConfig/BlinkPaths.m index 950e6642a..9654f958d 100644 --- a/BlinkConfig/BlinkPaths.m +++ b/BlinkConfig/BlinkPaths.m @@ -43,7 +43,7 @@ + (NSString *)homePath { if (__homePath == nil) { __homePath = [[self groupContainerPath] stringByAppendingPathComponent:@"home"]; } - + return __homePath; } @@ -63,7 +63,7 @@ + (NSString *)groupContainerPath { if (__groupContainerPath == nil) { NSString *groupID = [XCConfig infoPlistFullGroupID]; - + NSFileManager *fm = [NSFileManager defaultManager]; NSString *path = [fm containerURLForSecurityApplicationGroupIdentifier:groupID].path; __groupContainerPath = path; @@ -80,7 +80,7 @@ + (NSString *)iCloudDriveDocuments [self _ensureFolderAtPath:path]; __iCloudsDriveDocumentsPath = path; } - + return __iCloudsDriveDocumentsPath; } @@ -100,9 +100,9 @@ + (void)_linkAtPath:(NSString *)atPath destinationPath:(NSString *)destinationPa if ([fm fileExistsAtPath:atPath]) { return; } - + NSError *error = nil; - + BOOL ok = [fm createSymbolicLinkAtPath:atPath withDestinationPath:destinationPath error:&error]; @@ -131,6 +131,12 @@ + (NSString *)ssh { return dotSSH; } ++ (NSString *)blinkAgentSettings { + NSString *path = [[self blink] stringByAppendingPathComponent:@"agents"]; + [self _ensureFolderAtPath:path]; + return path; +} + + (void)_ensureFolderAtPath:(NSString *)path { BOOL isDir = NO; NSFileManager *fm = [NSFileManager defaultManager]; @@ -138,7 +144,7 @@ + (void)_ensureFolderAtPath:(NSString *)path { if (isDir) { return; } - + [fm removeItemAtPath:path error:nil]; } [fm createDirectoryAtPath:path withIntermediateDirectories:YES attributes:@{} error:nil]; @@ -167,7 +173,10 @@ + (NSURL *)blinkBuildStagingMarkURL return [NSURL fileURLWithPath:[url stringByAppendingPathComponent:@".staging"]]; } - ++ (NSURL *)blinkAgentSettingsURL +{ + return [NSURL fileURLWithPath:[self blinkAgentSettings]]; +} + (NSURL *)sshURL { @@ -257,29 +266,29 @@ + (NSURL *)blinkCodeErrorLogURL { NSFileManager *fm = [NSFileManager defaultManager]; NSMutableArray *allowedPaths = [[NSMutableArray alloc] init]; - + NSString *homePath = [BlinkPaths homePath]; NSArray * files = [fm contentsOfDirectoryAtPath:homePath error:nil]; - + for (NSString *path in files) { NSString *filePath = [homePath stringByAppendingPathComponent:path]; NSDictionary * attrs = [fm attributesOfItemAtPath:filePath error:nil]; if (attrs[NSFileType] != NSFileTypeSymbolicLink) { continue; } - + NSString *destPath = [fm destinationOfSymbolicLinkAtPath:filePath error:nil]; if (!destPath) { continue; } - + if (![fm isReadableFileAtPath:destPath]) { - + // We lost access. Remove that symlink [fm removeItemAtPath:filePath error:nil]; continue; } - + [allowedPaths addObject:destPath]; } return allowedPaths; diff --git a/SSH/Agent.swift b/SSH/Agent.swift index 4e82416c3..417100b96 100644 --- a/SSH/Agent.swift +++ b/SSH/Agent.swift @@ -55,7 +55,7 @@ public class SSHAgentKey { // var expiration: Int public let signer: Signer public let name: String - + init(_ key: Signer, named: String, constraints: [SSHAgentConstraint]? = nil) { self.signer = key self.name = named @@ -76,7 +76,7 @@ public class SSHAgent { private class AgentCtxt { weak var agent: SSHAgent? weak var client: SSHClient? - + init(agent: SSHAgent, client: SSHClient) { self.agent = agent self.client = client @@ -86,7 +86,7 @@ public class SSHAgent { public func linkTo(agent: SSHAgent) { self.superAgent = agent } - + public func attachTo(client: SSHClient) { let agentCtxt = AgentCtxt(agent: self, client: client) contexts.append(agentCtxt) @@ -118,7 +118,7 @@ public class SSHAgent { return Int32(replyData.count) } - + ssh_set_agent_callback(client.session, cb, ctxt) } @@ -133,7 +133,7 @@ public class SSHAgent { } ring.append(cKey) } - + public func removeKey(_ name: String) -> Signer? { if let idx = ring.firstIndex(where: { $0.name == name }) { let key = ring.remove(at: idx) @@ -143,6 +143,10 @@ public class SSHAgent { } } + public func clear() { + ring = [] + } + func request(_ message: Data, context: SSHAgentRequestType, client: SSHClient) throws -> Data { switch context { case .requestIdentities: @@ -151,7 +155,7 @@ public class SSHAgent { var respType = SSHAgentResponseType.answerIdentities.rawValue let preamble = Data(bytes: &respType, count: MemoryLayout.size) + Data(bytes: &keys, count: MemoryLayout.size) - + return ring.reduce(preamble) { $0 + $1 } case .requestSignature: guard let signature = try encodedSignature(message, for: client) else { @@ -234,7 +238,7 @@ extension SSHAgent { let reply = SSHEncode.data(from: UInt32(replyData.count)) + replyData let dd = reply.withUnsafeBytes { DispatchData(bytes: $0) } - + return stream.write(dd, max: dd.count) }.sink( receiveCompletion: { c in @@ -281,12 +285,12 @@ public enum SSHEncode { public static func data(from str: String) -> Data { self.data(from: UInt32(str.count)) + (str.data(using: .utf8) ?? Data()) } - + public static func data(from int: UInt32) -> Data { var val: UInt32 = UInt32(int).bigEndian return Data(bytes: &val, count: MemoryLayout.size) } - + public static func data(from bytes: Data) -> Data { self.data(from: UInt32(bytes.count)) + bytes } diff --git a/Settings/SettingsView.swift b/Settings/SettingsView.swift index dbb969a9f..7d5164b50 100644 --- a/Settings/SettingsView.swift +++ b/Settings/SettingsView.swift @@ -35,7 +35,7 @@ import SwiftUI import LocalAuthentication struct SettingsView: View { - + @EnvironmentObject private var _nav: Nav @State private var _biometryType = LAContext().biometryType @State private var _blinkVersion = UIApplication.blinkShortVersion() ?? "" @@ -45,7 +45,7 @@ struct SettingsView: View { @State private var _defaultUser = BLKDefaults.defaultUserName() ?? "" @StateObject private var _entitlements: EntitlementsManager = .shared @StateObject private var _model = PurchasesUserModel.shared - + var body: some View { List { if _entitlements.earlyAccessFeatures.active && _entitlements.earlyAccessFeatures.period == .Trial { @@ -101,6 +101,11 @@ struct SettingsView: View { } details: { HostListView() } + Row { + Label("Default Agent", systemImage: "key.viewfinder") + } details: { + DefaultAgentSettingsView() + } RowWithStoryBoardId(content: { HStack { Label("Default User", systemImage: "person") @@ -109,7 +114,7 @@ struct SettingsView: View { } }, storyBoardId: "BKDefaultUserViewController") } - + Section("Terminal") { RowWithStoryBoardId(content: { Label("Appearance", systemImage: "paintpalette") @@ -135,7 +140,7 @@ struct SettingsView: View { } #endif } - + Section("Configuration") { if EntitlementsManager.shared.earlyAccessFeatures.active || FeatureFlags.earlyAccessFeatures { Row { @@ -151,7 +156,7 @@ struct SettingsView: View { Text(_iCloudSyncOn ? "On" : "Off").foregroundColor(.secondary) } }, storyBoardId: "BKiCloudConfigurationViewController") - + RowWithStoryBoardId(content: { HStack { Label("Auto Lock", systemImage: _biometryType == .faceID ? "faceid" : "touchid") @@ -167,7 +172,7 @@ struct SettingsView: View { } }, storyBoardId: "BKXCallBackUrlConfigurationViewController") } - + Section("Get in touch") { Row { Label("Support", systemImage: "book") @@ -185,12 +190,12 @@ struct SettingsView: View { // } label: { // Label("Rate Blink", systemImage: "star") // } - + // Spacer() // Text("App Store").foregroundColor(.secondary) // } } - + Section { RowWithStoryBoardId(content: { HStack { @@ -220,10 +225,10 @@ struct SettingsView: View { _autoLockOn = BKUserConfigurationManager.userSettingsValue(forKey: BKUserConfigAutoLock) _xCallbackUrlOn = BLKDefaults.isXCallBackURLEnabled() _defaultUser = BLKDefaults.defaultUserName() ?? "" - + } .listStyle(.grouped) .navigationTitle("Settings") - + } } diff --git a/Settings/ViewControllers/AgentSettings/AgentSettingsView.swift b/Settings/ViewControllers/AgentSettings/AgentSettingsView.swift new file mode 100644 index 000000000..ceb3ee8a6 --- /dev/null +++ b/Settings/ViewControllers/AgentSettings/AgentSettingsView.swift @@ -0,0 +1,205 @@ +////////////////////////////////////////////////////////////////////////////////// +// +// B L I N K +// +// Copyright (C) 2016-2019 Blink Mobile Shell Project +// +// This file is part of Blink. +// +// Blink is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// Blink is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with Blink. If not, see . +// +// In addition, Blink is also subject to certain additional terms under +// GNU GPL version 3 section 7. +// +// You should have received a copy of these additional terms immediately +// following the terms and conditions of the GNU General Public License +// which accompanied the Blink Source Code. If not, see +// . +// +//////////////////////////////////////////////////////////////////////////////// + + +import Foundation +import SwiftUI + + +struct DefaultAgentSettingsView: View { + @State private var agentSettings: BKAgentSettings? = nil + @State private var showAlert = false + @State private var alertMessage = "" + @State private var dismissAfterAlert = false + + @Environment(\.dismiss) private var dismiss + + var body: some View { + List { + if let binding = Binding($agentSettings) { + AgentSettingsOptions(agentSettings: binding, enabled: true) + .onChange(of: agentSettings!) { new in + do { + try SSHDefaultAgent.setSettings(new) + } catch { + self.alertMessage = "Failed to set settings: \(error.localizedDescription)" + self.showAlert = true + self.dismissAfterAlert = true + } + } + } + } + .navigationBarTitle("Default Agent") + .onAppear { + print("OnAppear") + if agentSettings == nil { + print("Init settings") + do { + let agentSettings = try SSHDefaultAgent.getSettings() + self.agentSettings = agentSettings + } catch { + self.alertMessage = "Failed to get settings: \(error.localizedDescription)" + self.showAlert = true + self.dismissAfterAlert = true + } + } + } + .alert(isPresented: $showAlert) { + Alert( + title: Text("Error"), + message: Text(alertMessage), + dismissButton: .default(Text("OK")) { + if self.dismissAfterAlert { + dismiss() + } + } + ) + } + } +} + +struct AgentSettingsOptions: View { + @Binding var agentSettings: BKAgentSettings + @State var prompt: BKAgentSettingsPrompt + @State var keys: [String] + var enabled: Bool + + init(agentSettings: Binding, enabled: Bool) { + self._agentSettings = agentSettings + self.prompt = agentSettings.wrappedValue.prompt + self.keys = agentSettings.wrappedValue.keys + self.enabled = enabled + } + + var body: some View { + FieldAgentSettingsPrompt(value: $prompt, enabled: enabled) + .onChange(of: prompt) { newPrompt in + self.agentSettings = BKAgentSettings(prompt: newPrompt, keys: keys) + } + if prompt != .Deny { + FieldAgentSettingsKeys(value: $keys, enabled: enabled) + .onChange(of: keys) { newKeys in + self.agentSettings = BKAgentSettings(prompt: prompt, keys: newKeys) + } + } + } +} + +fileprivate struct FieldAgentSettingsPrompt: View { + @Binding var value: BKAgentSettingsPrompt + var enabled: Bool + + var body: some View { + Row( + content: { + HStack { + FormLabel(text: "Agent Forwarding") + Spacer() + Text(value.label) + } + }, + details: { + AgentSettingsPromptPickerView(currentValue: enabled ? $value : .constant(value)) + } + ).disabled(!enabled) + } +} + +fileprivate struct FieldAgentSettingsKeys: View { + @Binding var value: [String] + var enabled: Bool + + var body: some View { + Row( + content: { + HStack { + FormLabel(text: "Load Keys") + Spacer() + Text(value.isEmpty ? "None" : value.joined(separator: ", ")) + .font(.system(.subheadline)).foregroundColor(.secondary) + } + }, + details: { + KeyPickerView(currentKey: enabled ? $value : .constant(value), multipleSelection: true) + } + ).disabled(!enabled) + } +} + +struct AgentSettingsPromptPickerView: View { + @Binding var currentValue: BKAgentSettingsPrompt + + var body: some View { + List { + Section(footer: Text(currentValue.hint)) { + ForEach(BKAgentSettingsPrompt.all, id: \.self) { value in + HStack { + Text(value.label).tag(value) + Spacer() + Checkmark(checked: currentValue == value) + } + .contentShape(Rectangle()) + .onTapGesture { currentValue = value } + } + } + } + .listStyle(InsetGroupedListStyle()) + .navigationTitle("Agent Forwarding") + } +} + +extension BKAgentSettingsPrompt: Hashable { + var label: String { + switch self { + case .Deny: return "No" + case .Confirm: return "Confirm" + case .Allow: return "Allow" + case _: return "" + } + } + + var hint: String { + switch self { + case .Deny: return "Deny usage of the key for signatures." + case .Confirm: return "Confirm each use of a key before signature." + case .Allow: return "Allow to use the key for signature without confirmation." + case _: return "" + } + } + + static var all: [BKAgentSettingsPrompt] { + [ + Deny, + Confirm, + Allow, + ] + } +} diff --git a/Settings/ViewControllers/BKPubKey/KeyPickerView.swift b/Settings/ViewControllers/BKPubKey/KeyPickerView.swift index 816253b99..0dd453cc7 100644 --- a/Settings/ViewControllers/BKPubKey/KeyPickerView.swift +++ b/Settings/ViewControllers/BKPubKey/KeyPickerView.swift @@ -73,6 +73,10 @@ struct KeyPickerView: View { } .listStyle(InsetGroupedListStyle()) .navigationTitle("Select a Key") + .onAppear { + // Make sure the key selection can only be based on the canonical list. + currentKey = currentKey.filter { key in _list.contains(where: { $0.id == key }) } + } } private func _selectKey(_ key: String) {