From 2efba1bc214f18304e7fb11bf75bfa738d36afe3 Mon Sep 17 00:00:00 2001 From: Max Goedjen Date: Sat, 6 Sep 2025 17:30:05 -0700 Subject: [PATCH] . --- Sources/AgentRequestParser/main.swift | 38 ++++++++++++++----- .../Sources/SecretAgentKit/Agent.swift | 2 + .../SecretAgentKit/OpenSSHReader.swift | 18 +++++---- .../SecretAgentKit/SSHAgentInputParser.swift | 21 ++++++---- Sources/SecretAgent/AppDelegate.swift | 7 ---- Sources/SecretAgent/XPCInputParser.swift | 21 +++++++--- 6 files changed, 70 insertions(+), 37 deletions(-) diff --git a/Sources/AgentRequestParser/main.swift b/Sources/AgentRequestParser/main.swift index 3ba02e4..5629e8d 100644 --- a/Sources/AgentRequestParser/main.swift +++ b/Sources/AgentRequestParser/main.swift @@ -6,19 +6,39 @@ private let logger = Logger(subsystem: "com.maxgoedjen.secretive.secretagent.Age func handleRequest(_ request: XPCListener.IncomingSessionRequest) -> XPCListener.IncomingSessionRequest.Decision { logger.log("Parser received inbound request") - return request.accept { message in - logger.log("Parser accepted inbound request") - do { - let result = try SSHAgentInputParser().parse(data: message) - logger.log("Parser parsed message as type \(result.debugDescription)") - return result - } catch { - logger.error("Parser failed with error \(error)") - return nil + return request.accept { xpcDictionary in + xpcDictionary.handoffReply(to: .global(qos: .userInteractive)) { + logger.log("Parser accepted inbound request") + do { + let parser = SSHAgentInputParser() + let result = try parser.parse(data: xpcDictionary.decode(as: Data.self)) + logger.log("Parser parsed message as type \(result.debugDescription)") + xpcDictionary.reply(result) + } catch let error as SSHAgentInputParser.AgentParsingError { + logger.error("Parser failed with error \(error)") + xpcDictionary.reply(error) + } catch { + // This should never actually happen because SSHAgentInputParser is a typed thrower, but the type system doesn't seem to know that across framework boundaries? + logger.error("Parser failed with unknown error \(error)") + } } } } +public struct WrappedError: Codable { + + public struct DescriptionOnlyError: Error, Codable { + let localizedDescription: String + } + + public let wrapped: Wrapped + + public init(_ error: Wrapped) { + wrapped = error + } + +} + do { if #available(macOS 26.0, *) { _ = try XPCListener( diff --git a/Sources/Packages/Sources/SecretAgentKit/Agent.swift b/Sources/Packages/Sources/SecretAgentKit/Agent.swift index 2adebb0..cd156c6 100644 --- a/Sources/Packages/Sources/SecretAgentKit/Agent.swift +++ b/Sources/Packages/Sources/SecretAgentKit/Agent.swift @@ -45,6 +45,8 @@ extension Agent { response.append(SSHAgent.Response.agentSignResponse.data) response.append(try await sign(data: context.dataToSign, keyBlob: context.keyBlob, provenance: provenance)) logger.debug("Agent returned \(SSHAgent.Response.agentSignResponse.debugDescription)") + case .unknown(let value): + logger.error("Agent received unknown request of type \(value).") default: logger.debug("Agent received valid request of type \(request.debugDescription), but not currently supported.") throw UnhandledRequestError() diff --git a/Sources/Packages/Sources/SecretAgentKit/OpenSSHReader.swift b/Sources/Packages/Sources/SecretAgentKit/OpenSSHReader.swift index 63107f9..0ca5042 100644 --- a/Sources/Packages/Sources/SecretAgentKit/OpenSSHReader.swift +++ b/Sources/Packages/Sources/SecretAgentKit/OpenSSHReader.swift @@ -7,25 +7,25 @@ final class OpenSSHReader { /// Initialize the reader with an OpenSSH data payload. /// - Parameter data: The data to read. - public init(data: Data) { + init(data: Data) { remaining = Data(data) } /// Reads the next chunk of data from the playload. /// - Returns: The next chunk of data. - public func readNextChunk(convertEndianness: Bool = true) throws -> Data { + func readNextChunk(convertEndianness: Bool = true) throws(OpenSSHReaderError) -> Data { let littleEndianLength = try readNextBytes(as: UInt32.self) let length = convertEndianness ? Int(littleEndianLength.bigEndian) : Int(littleEndianLength) - guard remaining.count >= length else { throw EndOfData() } + guard remaining.count >= length else { throw .beyondBounds } let dataRange = 0..(as: T.Type) throws -> T { + func readNextBytes(as: T.Type) throws(OpenSSHReaderError) -> T { let size = MemoryLayout.size - guard remaining.count >= size else { throw EndOfData() } + guard remaining.count >= size else { throw .beyondBounds } let lengthRange = 0.. String { + func readNextChunkAsString() throws -> String { try String(decoding: readNextChunk(), as: UTF8.self) } - public struct EndOfData: Error {} - +} + +public enum OpenSSHReaderError: Error, Codable { + case beyondBounds } diff --git a/Sources/Packages/Sources/SecretAgentKit/SSHAgentInputParser.swift b/Sources/Packages/Sources/SecretAgentKit/SSHAgentInputParser.swift index b51c697..c6a44f6 100644 --- a/Sources/Packages/Sources/SecretAgentKit/SSHAgentInputParser.swift +++ b/Sources/Packages/Sources/SecretAgentKit/SSHAgentInputParser.swift @@ -16,10 +16,10 @@ public struct SSHAgentInputParser: SSHAgentInputParserProtocol { } - public func parse(data: Data) throws -> SSHAgent.Request { + public func parse(data: Data) throws(AgentParsingError) -> SSHAgent.Request { logger.debug("Parsing new data") guard data.count > 4 else { - throw InvalidDataProvidedError() + throw .invalidData } let specifiedLength = (data[0..<4].bytes.unsafeLoad(as: UInt32.self).bigEndian) + 4 let rawRequestInt = data[4] @@ -29,7 +29,11 @@ public struct SSHAgentInputParser: SSHAgentInputParserProtocol { case SSHAgent.Request.requestIdentities.protocolID: return .requestIdentities case SSHAgent.Request.signRequest(.empty).protocolID: - return .signRequest(try signatureRequestContext(from: body)) + do { + return .signRequest(try signatureRequestContext(from: body)) + } catch { + throw .openSSHReader(error) + } case SSHAgent.Request.addIdentity.protocolID: return .addIdentity case SSHAgent.Request.removeIdentity.protocolID: @@ -59,7 +63,7 @@ public struct SSHAgentInputParser: SSHAgentInputParserProtocol { extension SSHAgentInputParser { - func signatureRequestContext(from data: Data) throws -> SSHAgent.Request.SignatureRequestContext { + func signatureRequestContext(from data: Data) throws(OpenSSHReaderError) -> SSHAgent.Request.SignatureRequestContext { let reader = OpenSSHReader(data: data) let rawKeyBlob = try reader.readNextChunk() let keyBlob = certificatePublicKeyBlob(from: rawKeyBlob) ?? rawKeyBlob @@ -95,8 +99,11 @@ extension SSHAgentInputParser { extension SSHAgentInputParser { - struct AgentUnknownRequestError: Error {} - struct AgentUnhandledRequestError: Error {} - struct InvalidDataProvidedError: Error {} + public enum AgentParsingError: Error, Codable { + case unknownRequest + case unhandledRequest + case invalidData + case openSSHReader(OpenSSHReaderError) + } } diff --git a/Sources/SecretAgent/AppDelegate.swift b/Sources/SecretAgent/AppDelegate.swift index be1c44e..237f595 100644 --- a/Sources/SecretAgent/AppDelegate.swift +++ b/Sources/SecretAgent/AppDelegate.swift @@ -35,13 +35,6 @@ class AppDelegate: NSObject, NSApplicationDelegate { logger.debug("SecretAgent finished launching") Task { let inputParser = try XPCAgentInputParser() - Task { - try? await Task.sleep(for: .seconds(1)) - var len = (5 as UInt32).littleEndian - var raw = SSHAgent.Request.requestIdentities.protocolID - let data = Data(bytes: &len, count: MemoryLayout.size) + Data(bytes: &raw, count: MemoryLayout.size) - print(try? await inputParser.parse(data: data)) - } for await session in socketController.sessions { Task { do { diff --git a/Sources/SecretAgent/XPCInputParser.swift b/Sources/SecretAgent/XPCInputParser.swift index d5d8c9b..089a390 100644 --- a/Sources/SecretAgent/XPCInputParser.swift +++ b/Sources/SecretAgent/XPCInputParser.swift @@ -1,6 +1,7 @@ import Foundation import SecretAgentKit +/// Delegates all agent input parsing to an XPC service which wraps OpenSSH public final class XPCAgentInputParser: SSHAgentInputParserProtocol { private let session: XPCSession @@ -8,11 +9,15 @@ public final class XPCAgentInputParser: SSHAgentInputParserProtocol { public init() throws { if #available(macOS 26.0, *) { - session = try XPCSession(xpcService: "com.maxgoedjen.Secretive.AgentRequestParser", targetQueue: queue, options: .inactive, requirement: .isFromSameTeam()) + session = try XPCSession(xpcService: "com.maxgoedjen.Secretive.AgentRequestParser", targetQueue: queue, requirement: .isFromSameTeam()) } else { - session = try XPCSession(xpcService: "com.maxgoedjen.Secretive.AgentRequestParser", targetQueue: queue, options: .inactive) + session = try XPCSession(xpcService: "com.maxgoedjen.Secretive.AgentRequestParser", targetQueue: queue) + } + Task { + // Warm up the XPC endpoint. + _ = try? await parse(data: Data()) + } - try session.activate() } public func parse(data: Data) async throws -> SSHAgent.Request { @@ -21,10 +26,12 @@ public final class XPCAgentInputParser: SSHAgentInputParserProtocol { try session.send(data) { result in switch result { case .success(let result): - do { - continuation.resume(returning: try result.decode(as: SSHAgent.Request.self)) - } catch { + if let result = try? result.decode(as: SSHAgent.Request.self) { + continuation.resume(returning: result) + } else if let error = try? result.decode(as: SSHAgentInputParser.AgentParsingError.self) { continuation.resume(throwing: error) + } else { + continuation.resume(throwing: UnknownMessageError()) } case .failure(let error): continuation.resume(throwing: error) @@ -40,4 +47,6 @@ public final class XPCAgentInputParser: SSHAgentInputParserProtocol { session.cancel(reason: "Done") } + struct UnknownMessageError: Error {} + }