mirror of
https://github.com/maxgoedjen/secretive.git
synced 2024-11-22 05:27:28 +00:00
Document SecretAgentKit (#312)
* Kit * More * More * More * Organization
This commit is contained in:
parent
7b7615ca38
commit
b0322b4c1f
@ -4,6 +4,7 @@ import OSLog
|
|||||||
import SecretKit
|
import SecretKit
|
||||||
import AppKit
|
import AppKit
|
||||||
|
|
||||||
|
/// The `Agent` is an implementation of an SSH agent. It manages coordination and access between a socket, traces requests, notifies witnesses and passes requests to stores.
|
||||||
public class Agent {
|
public class Agent {
|
||||||
|
|
||||||
private let storeList: SecretStoreList
|
private let storeList: SecretStoreList
|
||||||
@ -11,6 +12,10 @@ public class Agent {
|
|||||||
private let writer = OpenSSHKeyWriter()
|
private let writer = OpenSSHKeyWriter()
|
||||||
private let requestTracer = SigningRequestTracer()
|
private let requestTracer = SigningRequestTracer()
|
||||||
|
|
||||||
|
/// Initializes an agent with a store list and a witness.
|
||||||
|
/// - Parameters:
|
||||||
|
/// - storeList: The `SecretStoreList` to make available.
|
||||||
|
/// - witness: A witness to notify of requests.
|
||||||
public init(storeList: SecretStoreList, witness: SigningWitness? = nil) {
|
public init(storeList: SecretStoreList, witness: SigningWitness? = nil) {
|
||||||
Logger().debug("Agent is running")
|
Logger().debug("Agent is running")
|
||||||
self.storeList = storeList
|
self.storeList = storeList
|
||||||
@ -21,6 +26,10 @@ public class Agent {
|
|||||||
|
|
||||||
extension Agent {
|
extension Agent {
|
||||||
|
|
||||||
|
/// Handles an incoming request.
|
||||||
|
/// - Parameters:
|
||||||
|
/// - reader: A ``FileHandleReader`` to read the content of the request.
|
||||||
|
/// - writer: A ``FileHandleWriter`` to write the response to.
|
||||||
public func handle(reader: FileHandleReader, writer: FileHandleWriter) {
|
public func handle(reader: FileHandleReader, writer: FileHandleWriter) {
|
||||||
Logger().debug("Agent handling new data")
|
Logger().debug("Agent handling new data")
|
||||||
let data = Data(reader.availableData)
|
let data = Data(reader.availableData)
|
||||||
@ -64,6 +73,8 @@ extension Agent {
|
|||||||
|
|
||||||
extension Agent {
|
extension Agent {
|
||||||
|
|
||||||
|
/// Lists the identities available for signing operations
|
||||||
|
/// - Returns: An OpenSSH formatted Data payload listing the identities available for signing operations.
|
||||||
func identities() -> Data {
|
func identities() -> Data {
|
||||||
let secrets = storeList.stores.flatMap(\.secrets)
|
let secrets = storeList.stores.flatMap(\.secrets)
|
||||||
var count = UInt32(secrets.count).bigEndian
|
var count = UInt32(secrets.count).bigEndian
|
||||||
@ -80,6 +91,11 @@ extension Agent {
|
|||||||
return countData + keyData
|
return countData + keyData
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Notifies witnesses of a pending signature request, and performs the signing operation if none object.
|
||||||
|
/// - Parameters:
|
||||||
|
/// - data: The data to sign.
|
||||||
|
/// - provenance: A ``SecretKit.SigningRequestProvenance`` object describing the origin of the request.
|
||||||
|
/// - Returns: An OpenSSH formatted Data payload containing the signed data response.
|
||||||
func sign(data: Data, provenance: SigningRequestProvenance) throws -> Data {
|
func sign(data: Data, provenance: SigningRequestProvenance) throws -> Data {
|
||||||
let reader = OpenSSHReader(data: data)
|
let reader = OpenSSHReader(data: data)
|
||||||
let hash = reader.readNextChunk()
|
let hash = reader.readNextChunk()
|
||||||
@ -147,6 +163,9 @@ extension Agent {
|
|||||||
|
|
||||||
extension Agent {
|
extension Agent {
|
||||||
|
|
||||||
|
/// Finds a ``Secret`` matching a specified hash whos signature was requested.
|
||||||
|
/// - Parameter hash: The hash to match against.
|
||||||
|
/// - Returns: A ``Secret`` and the ``SecretStore`` containing it, if a match is found.
|
||||||
func secret(matching hash: Data) -> (AnySecretStore, AnySecret)? {
|
func secret(matching hash: Data) -> (AnySecretStore, AnySecret)? {
|
||||||
storeList.stores.compactMap { store -> (AnySecretStore, AnySecret)? in
|
storeList.stores.compactMap { store -> (AnySecretStore, AnySecret)? in
|
||||||
let allMatching = store.secrets.filter { secret in
|
let allMatching = store.secrets.filter { secret in
|
||||||
@ -164,6 +183,7 @@ extension Agent {
|
|||||||
|
|
||||||
extension Agent {
|
extension Agent {
|
||||||
|
|
||||||
|
/// An error involving agent operations..
|
||||||
enum AgentError: Error {
|
enum AgentError: Error {
|
||||||
case unhandledType
|
case unhandledType
|
||||||
case noMatchingKey
|
case noMatchingKey
|
||||||
|
@ -1,13 +1,23 @@
|
|||||||
# ``SecretAgentKit``
|
# ``SecretAgentKit``
|
||||||
|
|
||||||
<!--@START_MENU_TOKEN@-->Summary<!--@END_MENU_TOKEN@-->
|
SecretAgentKit is a collection of types that allow SecretAgent to conform to the SSH agent protocol.
|
||||||
|
|
||||||
## Overview
|
|
||||||
|
|
||||||
<!--@START_MENU_TOKEN@-->Text<!--@END_MENU_TOKEN@-->
|
|
||||||
|
|
||||||
## Topics
|
## Topics
|
||||||
|
|
||||||
### <!--@START_MENU_TOKEN@-->Group<!--@END_MENU_TOKEN@-->
|
### Agent
|
||||||
|
|
||||||
- <!--@START_MENU_TOKEN@-->``Symbol``<!--@END_MENU_TOKEN@-->
|
- ``Agent``
|
||||||
|
|
||||||
|
### Protocol
|
||||||
|
|
||||||
|
- ``SSHAgent``
|
||||||
|
|
||||||
|
### Request Notification
|
||||||
|
|
||||||
|
- ``SigningWitness``
|
||||||
|
|
||||||
|
### Socket Operations
|
||||||
|
|
||||||
|
- ``SocketController``
|
||||||
|
- ``FileHandleReader``
|
||||||
|
- ``FileHandleWriter``
|
||||||
|
@ -1,15 +1,21 @@
|
|||||||
import Foundation
|
import Foundation
|
||||||
|
|
||||||
|
/// Protocol abstraction of the reading aspects of FileHandle.
|
||||||
public protocol FileHandleReader {
|
public protocol FileHandleReader {
|
||||||
|
|
||||||
|
/// Gets data that is available for reading.
|
||||||
var availableData: Data { get }
|
var availableData: Data { get }
|
||||||
|
/// A file descriptor of the handle.
|
||||||
var fileDescriptor: Int32 { get }
|
var fileDescriptor: Int32 { get }
|
||||||
|
/// The process ID of the process coonnected to the other end of the FileHandle.
|
||||||
var pidOfConnectedProcess: Int32 { get }
|
var pidOfConnectedProcess: Int32 { get }
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Protocol abstraction of the writing aspects of FileHandle.
|
||||||
public protocol FileHandleWriter {
|
public protocol FileHandleWriter {
|
||||||
|
|
||||||
|
/// Writes data to the handle.
|
||||||
func write(_ data: Data)
|
func write(_ data: Data)
|
||||||
|
|
||||||
}
|
}
|
||||||
|
@ -1,10 +1,13 @@
|
|||||||
import Foundation
|
import Foundation
|
||||||
|
|
||||||
|
/// A namespace for the SSH Agent Protocol, as described in https://tools.ietf.org/id/draft-miller-ssh-agent-01.html
|
||||||
public enum SSHAgent {}
|
public enum SSHAgent {}
|
||||||
|
|
||||||
extension SSHAgent {
|
extension SSHAgent {
|
||||||
|
|
||||||
|
/// The type of the SSH Agent Request, as described in https://tools.ietf.org/id/draft-miller-ssh-agent-01.html#rfc.section.5.1
|
||||||
public enum RequestType: UInt8, CustomDebugStringConvertible {
|
public enum RequestType: UInt8, CustomDebugStringConvertible {
|
||||||
|
|
||||||
case requestIdentities = 11
|
case requestIdentities = 11
|
||||||
case signRequest = 13
|
case signRequest = 13
|
||||||
|
|
||||||
@ -18,7 +21,9 @@ extension SSHAgent {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// The type of the SSH Agent Response, as described in https://tools.ietf.org/id/draft-miller-ssh-agent-01.html#rfc.section.5.1
|
||||||
public enum ResponseType: UInt8, CustomDebugStringConvertible {
|
public enum ResponseType: UInt8, CustomDebugStringConvertible {
|
||||||
|
|
||||||
case agentFailure = 5
|
case agentFailure = 5
|
||||||
case agentIdentitiesAnswer = 12
|
case agentIdentitiesAnswer = 12
|
||||||
case agentSignResponse = 14
|
case agentSignResponse = 14
|
||||||
|
@ -4,11 +4,15 @@ import Security
|
|||||||
import SecretKit
|
import SecretKit
|
||||||
import SecretAgentKitHeaders
|
import SecretAgentKitHeaders
|
||||||
|
|
||||||
|
/// An object responsible for generating ``SecretKit.SigningRequestProvenance`` objects.
|
||||||
struct SigningRequestTracer {
|
struct SigningRequestTracer {
|
||||||
}
|
}
|
||||||
|
|
||||||
extension SigningRequestTracer {
|
extension SigningRequestTracer {
|
||||||
|
|
||||||
|
/// Generates a ``SecretKit.SigningRequestProvenance`` from a ``FileHandleReader``.
|
||||||
|
/// - Parameter fileHandleReader: The reader involved in processing the request.
|
||||||
|
/// - Returns: A ``SecretKit.SigningRequestProvenance`` describing the origin of the request.
|
||||||
func provenance(from fileHandleReader: FileHandleReader) -> SigningRequestProvenance {
|
func provenance(from fileHandleReader: FileHandleReader) -> SigningRequestProvenance {
|
||||||
let firstInfo = process(from: fileHandleReader.pidOfConnectedProcess)
|
let firstInfo = process(from: fileHandleReader.pidOfConnectedProcess)
|
||||||
|
|
||||||
@ -19,6 +23,9 @@ extension SigningRequestTracer {
|
|||||||
return provenance
|
return provenance
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Generates a `kinfo_proc` representation of the provided process ID.
|
||||||
|
/// - Parameter pid: The process ID to look up.
|
||||||
|
/// - Returns: a `kinfo_proc` struct describing the process ID.
|
||||||
func pidAndNameInfo(from pid: Int32) -> kinfo_proc {
|
func pidAndNameInfo(from pid: Int32) -> kinfo_proc {
|
||||||
var len = MemoryLayout<kinfo_proc>.size
|
var len = MemoryLayout<kinfo_proc>.size
|
||||||
let infoPointer = UnsafeMutableRawPointer.allocate(byteCount: len, alignment: 1)
|
let infoPointer = UnsafeMutableRawPointer.allocate(byteCount: len, alignment: 1)
|
||||||
@ -27,6 +34,9 @@ extension SigningRequestTracer {
|
|||||||
return infoPointer.load(as: kinfo_proc.self)
|
return infoPointer.load(as: kinfo_proc.self)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Generates a ``SecretKit.SigningRequestProvenance.Process`` from a provided process ID.
|
||||||
|
/// - Parameter pid: The process ID to look up.
|
||||||
|
/// - Returns: A ``SecretKit.SigningRequestProvenance.Process`` describing the process.
|
||||||
func process(from pid: Int32) -> SigningRequestProvenance.Process {
|
func process(from pid: Int32) -> SigningRequestProvenance.Process {
|
||||||
var pidAndNameInfo = self.pidAndNameInfo(from: pid)
|
var pidAndNameInfo = self.pidAndNameInfo(from: pid)
|
||||||
let ppid = pidAndNameInfo.kp_eproc.e_ppid != 0 ? pidAndNameInfo.kp_eproc.e_ppid : nil
|
let ppid = pidAndNameInfo.kp_eproc.e_ppid != 0 ? pidAndNameInfo.kp_eproc.e_ppid : nil
|
||||||
@ -41,6 +51,9 @@ extension SigningRequestTracer {
|
|||||||
return SigningRequestProvenance.Process(pid: pid, processName: procName, appName: appName(for: pid), iconURL: iconURL(for: pid), path: path, validSignature: valid, parentPID: ppid)
|
return SigningRequestProvenance.Process(pid: pid, processName: procName, appName: appName(for: pid), iconURL: iconURL(for: pid), path: path, validSignature: valid, parentPID: ppid)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Looks up the URL for the icon of a process ID, if it has one.
|
||||||
|
/// - Parameter pid: The process ID to look up.
|
||||||
|
/// - Returns: A URL to the icon, if the process has one.
|
||||||
func iconURL(for pid: Int32) -> URL? {
|
func iconURL(for pid: Int32) -> URL? {
|
||||||
do {
|
do {
|
||||||
if let app = NSRunningApplication(processIdentifier: pid), let icon = app.icon?.tiffRepresentation {
|
if let app = NSRunningApplication(processIdentifier: pid), let icon = app.icon?.tiffRepresentation {
|
||||||
@ -54,6 +67,9 @@ extension SigningRequestTracer {
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Looks up the application name of a process ID, if it has one.
|
||||||
|
/// - Parameter pid: The process ID to look up.
|
||||||
|
/// - Returns: The process's display name, if the process has one.
|
||||||
func appName(for pid: Int32) -> String? {
|
func appName(for pid: Int32) -> String? {
|
||||||
NSRunningApplication(processIdentifier: pid)?.localizedName
|
NSRunningApplication(processIdentifier: pid)?.localizedName
|
||||||
}
|
}
|
||||||
|
@ -1,9 +1,23 @@
|
|||||||
import Foundation
|
import Foundation
|
||||||
import SecretKit
|
import SecretKit
|
||||||
|
|
||||||
|
/// A protocol that allows conformers to be notified of access to secrets, and optionally prevent access.
|
||||||
public protocol SigningWitness {
|
public protocol SigningWitness {
|
||||||
|
|
||||||
|
/// A ridiculously named method that notifies the callee that a signing operation is about to be performed using a secret. The callee may `throw` an `Error` to prevent access from occurring.
|
||||||
|
/// - Parameters:
|
||||||
|
/// - secret: The ``SecretKit.Secret`` that will be used to sign the request.
|
||||||
|
/// - store: The ``SecretKit.Store`` being asked to sign the request..
|
||||||
|
/// - provenance: A ``SecretKit.SigningRequestProvenance`` object describing the origin of the request.
|
||||||
|
/// - Note: This method being called does not imply that the requst has been authorized. If a secret requires authentication, authentication will still need to be performed by the user before the request will be performed. If the user declines or fails to authenticate, the request will fail.
|
||||||
func speakNowOrForeverHoldYourPeace(forAccessTo secret: AnySecret, from store: AnySecretStore, by provenance: SigningRequestProvenance) throws
|
func speakNowOrForeverHoldYourPeace(forAccessTo secret: AnySecret, from store: AnySecretStore, by provenance: SigningRequestProvenance) throws
|
||||||
|
|
||||||
|
/// Notifies the callee that a signing operation has been performed for a given secret.
|
||||||
|
/// - Parameters:
|
||||||
|
/// - secret: The ``SecretKit.Secret`` that will was used to sign the request.
|
||||||
|
/// - store: The ``SecretKit.Store`` that signed the request..
|
||||||
|
/// - provenance: A ``SecretKit.SigningRequestProvenance`` object describing the origin of the request.
|
||||||
|
/// - requiredAuthentication: A boolean describing whether or not authentication was required for the request.
|
||||||
func witness(accessTo secret: AnySecret, from store: AnySecretStore, by provenance: SigningRequestProvenance, requiredAuthentication: Bool) throws
|
func witness(accessTo secret: AnySecret, from store: AnySecretStore, by provenance: SigningRequestProvenance, requiredAuthentication: Bool) throws
|
||||||
|
|
||||||
}
|
}
|
||||||
|
@ -1,12 +1,16 @@
|
|||||||
import Foundation
|
import Foundation
|
||||||
import OSLog
|
import OSLog
|
||||||
|
|
||||||
|
/// A controller that manages socket configuration and request dispatching.
|
||||||
public class SocketController {
|
public class SocketController {
|
||||||
|
|
||||||
|
/// The active FileHandle.
|
||||||
private var fileHandle: FileHandle?
|
private var fileHandle: FileHandle?
|
||||||
private var port: SocketPort?
|
/// A handler that will be notified when a new read/write handle is available.
|
||||||
public var handler: ((FileHandleReader, FileHandleWriter) -> Void)?
|
public var handler: ((FileHandleReader, FileHandleWriter) -> Void)?
|
||||||
|
|
||||||
|
/// Initializes a socket controller with a specified path.
|
||||||
|
/// - Parameter path: The path to use as a socket.
|
||||||
public init(path: String) {
|
public init(path: String) {
|
||||||
Logger().debug("Socket controller setting up at \(path)")
|
Logger().debug("Socket controller setting up at \(path)")
|
||||||
if let _ = try? FileManager.default.removeItem(atPath: path) {
|
if let _ = try? FileManager.default.removeItem(atPath: path) {
|
||||||
@ -15,19 +19,23 @@ public class SocketController {
|
|||||||
let exists = FileManager.default.fileExists(atPath: path)
|
let exists = FileManager.default.fileExists(atPath: path)
|
||||||
assert(!exists)
|
assert(!exists)
|
||||||
Logger().debug("Socket controller path is clear")
|
Logger().debug("Socket controller path is clear")
|
||||||
port = socketPort(at: path)
|
|
||||||
configureSocket(at: path)
|
configureSocket(at: path)
|
||||||
Logger().debug("Socket listening at \(path)")
|
Logger().debug("Socket listening at \(path)")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Configures the socket and a corresponding FileHandle.
|
||||||
|
/// - Parameter path: The path to use as a socket.
|
||||||
func configureSocket(at path: String) {
|
func configureSocket(at path: String) {
|
||||||
guard let port = port else { return }
|
let port = socketPort(at: path)
|
||||||
fileHandle = FileHandle(fileDescriptor: port.socket, closeOnDealloc: true)
|
fileHandle = FileHandle(fileDescriptor: port.socket, closeOnDealloc: true)
|
||||||
NotificationCenter.default.addObserver(self, selector: #selector(handleConnectionAccept(notification:)), name: .NSFileHandleConnectionAccepted, object: nil)
|
NotificationCenter.default.addObserver(self, selector: #selector(handleConnectionAccept(notification:)), name: .NSFileHandleConnectionAccepted, object: nil)
|
||||||
NotificationCenter.default.addObserver(self, selector: #selector(handleConnectionDataAvailable(notification:)), name: .NSFileHandleDataAvailable, object: nil)
|
NotificationCenter.default.addObserver(self, selector: #selector(handleConnectionDataAvailable(notification:)), name: .NSFileHandleDataAvailable, object: nil)
|
||||||
fileHandle?.acceptConnectionInBackgroundAndNotify(forModes: [RunLoop.current.currentMode!])
|
fileHandle?.acceptConnectionInBackgroundAndNotify(forModes: [RunLoop.current.currentMode!])
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Creates a SocketPort for a path.
|
||||||
|
/// - Parameter path: The path to use as a socket.
|
||||||
|
/// - Returns: A configured SocketPort.
|
||||||
func socketPort(at path: String) -> SocketPort {
|
func socketPort(at path: String) -> SocketPort {
|
||||||
var addr = sockaddr_un()
|
var addr = sockaddr_un()
|
||||||
addr.sun_family = sa_family_t(AF_UNIX)
|
addr.sun_family = sa_family_t(AF_UNIX)
|
||||||
@ -49,6 +57,8 @@ public class SocketController {
|
|||||||
return SocketPort(protocolFamily: AF_UNIX, socketType: SOCK_STREAM, protocol: 0, address: data)!
|
return SocketPort(protocolFamily: AF_UNIX, socketType: SOCK_STREAM, protocol: 0, address: data)!
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Handles a new connection being accepted, invokes the handler, and prepares to accept new connections.
|
||||||
|
/// - Parameter notification: A `Notification` that triggered the call.
|
||||||
@objc func handleConnectionAccept(notification: Notification) {
|
@objc func handleConnectionAccept(notification: Notification) {
|
||||||
Logger().debug("Socket controller accepted connection")
|
Logger().debug("Socket controller accepted connection")
|
||||||
guard let new = notification.userInfo?[NSFileHandleNotificationFileHandleItem] as? FileHandle else { return }
|
guard let new = notification.userInfo?[NSFileHandleNotificationFileHandleItem] as? FileHandle else { return }
|
||||||
@ -57,6 +67,8 @@ public class SocketController {
|
|||||||
fileHandle?.acceptConnectionInBackgroundAndNotify(forModes: [RunLoop.current.currentMode!])
|
fileHandle?.acceptConnectionInBackgroundAndNotify(forModes: [RunLoop.current.currentMode!])
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Handles a new connection providing data and invokes the handler callback.
|
||||||
|
/// - Parameter notification: A `Notification` that triggered the call.
|
||||||
@objc func handleConnectionDataAvailable(notification: Notification) {
|
@objc func handleConnectionDataAvailable(notification: Notification) {
|
||||||
Logger().debug("Socket controller has new data available")
|
Logger().debug("Socket controller has new data available")
|
||||||
guard let new = notification.object as? FileHandle else { return }
|
guard let new = notification.object as? FileHandle else { return }
|
||||||
|
Loading…
Reference in New Issue
Block a user