diff --git a/.github/workflows/codeql.yml b/.github/workflows/codeql.yml index f8afbed..fedf2de 100644 --- a/.github/workflows/codeql.yml +++ b/.github/workflows/codeql.yml @@ -37,7 +37,7 @@ jobs: build-mode: ${{ matrix.build-mode }} - if: matrix.build-mode == 'manual' name: "Select Xcode" - run: sudo xcrun xcode-select -s /Applications/Xcode_26.0.app + run: sudo xcrun xcode-select -s /Applications/Xcode_26.2.app - if: matrix.build-mode == 'manual' name: "Build" run: xcrun xcodebuild -project Sources/Secretive.xcodeproj -scheme Secretive CODE_SIGN_IDENTITY="" CODE_SIGNING_REQUIRED=NO diff --git a/.github/workflows/nightly.yml b/.github/workflows/nightly.yml index 3e3c67d..3a20673 100644 --- a/.github/workflows/nightly.yml +++ b/.github/workflows/nightly.yml @@ -3,7 +3,6 @@ name: Nightly on: schedule: - cron: "0 8 * * *" - workflow_dispatch: jobs: build: @@ -26,7 +25,7 @@ jobs: APPLE_API_KEY_ID: ${{ secrets.APPLE_API_KEY_ID }} run: ./.github/scripts/signing.sh - name: Set Environment - run: sudo xcrun xcode-select -s /Applications/Xcode_26.0.app + run: sudo xcrun xcode-select -s /Applications/Xcode_26.2.app - name: Update Build Number env: RUN_ID: ${{ github.run_id }} diff --git a/.github/workflows/oneoff.yml b/.github/workflows/oneoff.yml new file mode 100644 index 0000000..1693abb --- /dev/null +++ b/.github/workflows/oneoff.yml @@ -0,0 +1,64 @@ +name: One-Off Build + +on: + workflow_dispatch: + +jobs: + build: + runs-on: macos-26 + permissions: + id-token: write + contents: write + attestations: write + actions: read + timeout-minutes: 10 + steps: + - uses: actions/checkout@v5 + - name: Setup Signing + env: + SIGNING_DATA: ${{ secrets.SIGNING_DATA }} + SIGNING_PASSWORD: ${{ secrets.SIGNING_PASSWORD }} + HOST_PROFILE_DATA: ${{ secrets.HOST_PROFILE_DATA }} + AGENT_PROFILE_DATA: ${{ secrets.AGENT_PROFILE_DATA }} + APPLE_API_KEY_DATA: ${{ secrets.APPLE_API_KEY_DATA }} + APPLE_API_KEY_ID: ${{ secrets.APPLE_API_KEY_ID }} + run: ./.github/scripts/signing.sh + - name: Set Environment + run: sudo xcrun xcode-select -s /Applications/Xcode_26.2.app + - name: Update Build Number + env: + RUN_ID: ${{ github.run_id }} + run: | + DATE=$(date "+%Y-%m-%d") + sed -i '' -e "s/GITHUB_CI_VERSION/0.0.0_oneoff-$DATE/g" Sources/Config/Config.xcconfig + sed -i '' -e "s/GITHUB_BUILD_NUMBER/1.$RUN_ID/g" Sources/Config/Config.xcconfig + sed -i '' -e "s/GITHUB_BUILD_URL/https:\/\/github.com\/maxgoedjen\/secretive\/actions\/runs\/$RUN_ID/g" Sources/Config/Config.xcconfig + - name: Build + run: xcrun xcodebuild -project Sources/Secretive.xcodeproj -scheme Secretive -configuration Release -archivePath Archive.xcarchive archive + - name: Move to Artifact Folder + run: mkdir Artifact; cp -r Archive.xcarchive/Products/Applications/Secretive.app Artifact + - name: Upload App to Artifacts + id: upload + uses: actions/upload-artifact@v4 + with: + name: Secretive + path: Artifact + - name: Download Zipped Artifact + id: download + env: + ZIP_ID: ${{ steps.upload.outputs.artifact-id }} + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + run: | + curl -L -H "Authorization: Bearer $GITHUB_TOKEN" -L \ + https://api.github.com/repos/maxgoedjen/secretive/actions/artifacts/$ZIP_ID/zip > Secretive.zip + - name: Notarize + env: + APPLE_API_KEY_ID: ${{ secrets.APPLE_API_KEY_ID }} + APPLE_API_ISSUER: ${{ secrets.APPLE_API_ISSUER }} + run: xcrun notarytool submit --key ~/.private_keys/AuthKey_$APPLE_API_KEY_ID.p8 --key-id $APPLE_API_KEY_ID --issuer $APPLE_API_ISSUER Secretive.zip + - name: Attest + id: attest + uses: actions/attest-build-provenance@v2 + with: + subject-name: "Secretive.zip" + subject-digest: sha256:${{ steps.upload.outputs.artifact-digest }} diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index ba5b220..7370f4f 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -22,7 +22,7 @@ jobs: APPLE_API_KEY_ID: ${{ secrets.APPLE_API_KEY_ID }} run: ./.github/scripts/signing.sh - name: Set Environment - run: sudo xcrun xcode-select -s /Applications/Xcode_26.0.app + run: sudo xcrun xcode-select -s /Applications/Xcode_26.2.app - name: Test run: xcrun xcodebuild -project Sources/Secretive.xcodeproj -scheme PackageTests test # SPM doesn't seem to pick up on the tests currently? @@ -47,7 +47,7 @@ jobs: APPLE_API_KEY_ID: ${{ secrets.APPLE_API_KEY_ID }} run: ./.github/scripts/signing.sh - name: Set Environment - run: sudo xcrun xcode-select -s /Applications/Xcode_26.0.app + run: sudo xcrun xcode-select -s /Applications/Xcode_26.2.app - name: Update Build Number env: TAG_NAME: ${{ github.ref }} diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index d79f525..d19c30c 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -10,7 +10,7 @@ jobs: steps: - uses: actions/checkout@v5 - name: Set Environment - run: sudo xcrun xcode-select -s /Applications/Xcode_26.0.app + run: sudo xcrun xcode-select -s /Applications/Xcode_26.2.app - name: Test Main Packages run: xcrun xcodebuild -project Sources/Secretive.xcodeproj -scheme PackageTests test # SPM doesn't seem to pick up on the tests currently? diff --git a/Package.swift b/Package.swift index 2ba06ef..3ca29ce 100644 --- a/Package.swift +++ b/Package.swift @@ -22,6 +22,9 @@ let package = Package( .library( name: "SmartCardSecretKit", targets: ["SmartCardSecretKit"]), + .library( + name: "SSHProtocolKit", + targets: ["SSHProtocolKit"]), ], dependencies: [ ], @@ -53,6 +56,19 @@ let package = Package( resources: [localization], swiftSettings: swiftSettings ), + .target( + name: "SSHProtocolKit", + dependencies: ["SecretKit"], + path: "Sources/Packages/Sources/SSHProtocolKit", + resources: [localization], + swiftSettings: swiftSettings, + ), + .testTarget( + name: "SSHProtocolKitTests", + dependencies: ["SSHProtocolKit"], + path: "Sources/Packages/Tests/SSHProtocolKitTests", + swiftSettings: swiftSettings, + ), ] ) diff --git a/Sources/Packages/Package.swift b/Sources/Packages/Package.swift index 4dcb725..3a8c855 100644 --- a/Sources/Packages/Package.swift +++ b/Sources/Packages/Package.swift @@ -21,13 +21,19 @@ let package = Package( targets: ["SmartCardSecretKit"]), .library( name: "SecretAgentKit", - targets: ["SecretAgentKit", "XPCWrappers"]), + targets: ["SecretAgentKit"]), + .library( + name: "Common", + targets: ["Common"]), .library( name: "Brief", targets: ["Brief"]), .library( name: "XPCWrappers", targets: ["XPCWrappers"]), + .library( + name: "SSHProtocolKit", + targets: ["SSHProtocolKit"]), ], dependencies: [ ], @@ -40,7 +46,7 @@ let package = Package( ), .testTarget( name: "SecretKitTests", - dependencies: ["SecretKit", "SecureEnclaveSecretKit", "SmartCardSecretKit"], + dependencies: ["SecretKit", "SecretAgentKit", "SecureEnclaveSecretKit", "SmartCardSecretKit"], swiftSettings: swiftSettings, ), .target( @@ -57,7 +63,7 @@ let package = Package( ), .target( name: "SecretAgentKit", - dependencies: ["SecretKit"], + dependencies: ["SecretKit", "SSHProtocolKit", "Common"], resources: [localization], swiftSettings: swiftSettings, ), @@ -65,9 +71,26 @@ let package = Package( name: "SecretAgentKitTests", dependencies: ["SecretAgentKit"], ), + .target( + name: "SSHProtocolKit", + dependencies: ["SecretKit"], + resources: [localization], + swiftSettings: swiftSettings, + ), + .testTarget( + name: "SSHProtocolKitTests", + dependencies: ["SSHProtocolKit"], + swiftSettings: swiftSettings, + ), + .target( + name: "Common", + dependencies: ["SSHProtocolKit", "SecretKit"], + resources: [localization], + swiftSettings: swiftSettings, + ), .target( name: "Brief", - dependencies: ["XPCWrappers"], + dependencies: ["XPCWrappers", "SSHProtocolKit"], resources: [localization], swiftSettings: swiftSettings, ), diff --git a/Sources/Secretive/Helpers/BundleIDs.swift b/Sources/Packages/Sources/Common/BundleIDs.swift similarity index 100% rename from Sources/Secretive/Helpers/BundleIDs.swift rename to Sources/Packages/Sources/Common/BundleIDs.swift diff --git a/Sources/Packages/Sources/Common/URLs.swift b/Sources/Packages/Sources/Common/URLs.swift new file mode 100644 index 0000000..9dfee59 --- /dev/null +++ b/Sources/Packages/Sources/Common/URLs.swift @@ -0,0 +1,46 @@ +import Foundation +import SSHProtocolKit +import SecretKit + +extension URL { + + public static var agentHomeURL: URL { + URL(fileURLWithPath: URL.homeDirectory.path().replacingOccurrences(of: Bundle.hostBundleID, with: Bundle.agentBundleID)) + } + + public static var socketPath: String { + #if DEBUG + URL.agentHomeURL.appendingPathComponent("socket-debug.ssh").path() + #else + URL.agentHomeURL.appendingPathComponent("socket.ssh").path() + #endif + } + + public static var publicKeyDirectory: URL { + agentHomeURL.appending(component: "PublicKeys") + } + + /// The path for a Secret's public key. + /// - Parameter secret: The Secret to return the path for. + /// - Returns: The path to the Secret's public key. + /// - Warning: This method returning a path does not imply that a key has been written to disk already. This method only describes where it will be written to. + public static func publicKeyPath(for secret: SecretType, in directory: URL) -> String { + let keyWriter = OpenSSHPublicKeyWriter() + let minimalHex = keyWriter.openSSHMD5Fingerprint(secret: secret).replacingOccurrences(of: ":", with: "") + return directory.appending(component: "\(minimalHex).pub").path() + } + +} + +extension String { + + public var normalizedPathAndFolder: (String, String) { + // All foundation-based normalization methods replace this with the container directly. + let processedPath = replacingOccurrences(of: "~", with: "/Users/\(NSUserName())") + let url = URL(filePath: processedPath) + let folder = url.deletingLastPathComponent().path() + return (processedPath, folder) + } + +} + diff --git a/Sources/Packages/Sources/SSHProtocolKit/Data+Hex.swift b/Sources/Packages/Sources/SSHProtocolKit/Data+Hex.swift new file mode 100644 index 0000000..bf1bb1d --- /dev/null +++ b/Sources/Packages/Sources/SSHProtocolKit/Data+Hex.swift @@ -0,0 +1,37 @@ +import Foundation +import CryptoKit + +public struct HexDataStyle: Hashable, Codable { + + let separator: String + + public init(separator: String) { + self.separator = separator + } + +} + +extension HexDataStyle: FormatStyle where SequenceType.Element == UInt8 { + + public func format(_ value: SequenceType) -> String { + value + .compactMap { ("0" + String($0, radix: 16, uppercase: false)).suffix(2) } + .joined(separator: separator) + } + +} + +extension FormatStyle where Self == HexDataStyle { + + public static func hex(separator: String = "") -> HexDataStyle { + HexDataStyle(separator: separator) + } + +} +extension FormatStyle where Self == HexDataStyle { + + public static func hex(separator: String = ":") -> HexDataStyle { + HexDataStyle(separator: separator) + } + +} diff --git a/Sources/Packages/Sources/SecretKit/OpenSSH/LengthAndData.swift b/Sources/Packages/Sources/SSHProtocolKit/LengthAndData.swift similarity index 100% rename from Sources/Packages/Sources/SecretKit/OpenSSH/LengthAndData.swift rename to Sources/Packages/Sources/SSHProtocolKit/LengthAndData.swift diff --git a/Sources/Packages/Sources/SecretKit/OpenSSH/OpenSSHPublicKeyWriter.swift b/Sources/Packages/Sources/SSHProtocolKit/OpenSSHPublicKeyWriter.swift similarity index 96% rename from Sources/Packages/Sources/SecretKit/OpenSSH/OpenSSHPublicKeyWriter.swift rename to Sources/Packages/Sources/SSHProtocolKit/OpenSSHPublicKeyWriter.swift index 30249e0..2c669db 100644 --- a/Sources/Packages/Sources/SecretKit/OpenSSH/OpenSSHPublicKeyWriter.swift +++ b/Sources/Packages/Sources/SSHProtocolKit/OpenSSHPublicKeyWriter.swift @@ -1,5 +1,6 @@ import Foundation import CryptoKit +import SecretKit /// Generates OpenSSH representations of the public key sof secrets. public struct OpenSSHPublicKeyWriter: Sendable { @@ -49,9 +50,7 @@ public struct OpenSSHPublicKeyWriter: Sendable { /// Generates an OpenSSH MD5 fingerprint string. /// - Returns: OpenSSH MD5 fingerprint string. public func openSSHMD5Fingerprint(secret: SecretType) -> String { - Insecure.MD5.hash(data: data(secret: secret)) - .compactMap { ("0" + String($0, radix: 16, uppercase: false)).suffix(2) } - .joined(separator: ":") + Insecure.MD5.hash(data: data(secret: secret)).formatted(.hex(separator: ":")) } public func comment(secret: SecretType) -> String { diff --git a/Sources/Packages/Sources/SecretAgentKit/OpenSSHReader.swift b/Sources/Packages/Sources/SSHProtocolKit/OpenSSHReader.swift similarity index 52% rename from Sources/Packages/Sources/SecretAgentKit/OpenSSHReader.swift rename to Sources/Packages/Sources/SSHProtocolKit/OpenSSHReader.swift index a3508e3..6378df2 100644 --- a/Sources/Packages/Sources/SecretAgentKit/OpenSSHReader.swift +++ b/Sources/Packages/Sources/SSHProtocolKit/OpenSSHReader.swift @@ -1,42 +1,49 @@ import Foundation /// Reads OpenSSH protocol data. -final class OpenSSHReader { +public final class OpenSSHReader { var remaining: Data + var done = false /// Initialize the reader with an OpenSSH data payload. /// - Parameter data: The data to read. - init(data: Data) { + public init(data: Data) { remaining = Data(data) } /// Reads the next chunk of data from the playload. /// - Returns: The next chunk of data. - func readNextChunk(convertEndianness: Bool = true) throws(OpenSSHReaderError) -> Data { - let littleEndianLength = try readNextBytes(as: UInt32.self) - let length = convertEndianness ? Int(littleEndianLength.bigEndian) : Int(littleEndianLength) + public func readNextChunk(convertEndianness: Bool = true) throws(OpenSSHReaderError) -> Data { + let length = try readNextBytes(as: UInt32.self, convertEndianness: convertEndianness) guard remaining.count >= length else { throw .beyondBounds } - let dataRange = 0..(as: T.Type) throws(OpenSSHReaderError) -> T { + public func readNextBytes(as: T.Type, convertEndianness: Bool = true) throws(OpenSSHReaderError) -> T { let size = MemoryLayout.size guard remaining.count >= size else { throw .beyondBounds } let lengthRange = 0.. String { + public func readNextChunkAsString(convertEndianness: Bool = true) throws(OpenSSHReaderError) -> String { try String(decoding: readNextChunk(convertEndianness: convertEndianness), as: UTF8.self) } - func readNextChunkAsSubReader(convertEndianness: Bool = true) throws(OpenSSHReaderError) -> OpenSSHReader { + public func readNextChunkAsSubReader(convertEndianness: Bool = true) throws(OpenSSHReaderError) -> OpenSSHReader { OpenSSHReader(data: try readNextChunk(convertEndianness: convertEndianness)) } diff --git a/Sources/Packages/Sources/SecretKit/OpenSSH/OpenSSHSignatureWriter.swift b/Sources/Packages/Sources/SSHProtocolKit/OpenSSHSignatureWriter.swift similarity index 70% rename from Sources/Packages/Sources/SecretKit/OpenSSH/OpenSSHSignatureWriter.swift rename to Sources/Packages/Sources/SSHProtocolKit/OpenSSHSignatureWriter.swift index b713d53..25397db 100644 --- a/Sources/Packages/Sources/SecretKit/OpenSSH/OpenSSHSignatureWriter.swift +++ b/Sources/Packages/Sources/SSHProtocolKit/OpenSSHSignatureWriter.swift @@ -1,5 +1,6 @@ import Foundation import CryptoKit +import SecretKit /// Generates OpenSSH representations of Secrets. public struct OpenSSHSignatureWriter: Sendable { @@ -29,19 +30,28 @@ public struct OpenSSHSignatureWriter: Sendable { extension OpenSSHSignatureWriter { + /// Converts a fixed-width big-endian integer (e.g. r/s from CryptoKit rawRepresentation) into an SSH mpint. + /// Strips unnecessary leading zeros and prefixes `0x00` if needed to keep the value positive. + private func mpint(fromFixedWidthPositiveBytes bytes: Data) -> Data { + // mpint zero is encoded as a string with zero bytes of data. + guard let firstNonZeroIndex = bytes.firstIndex(where: { $0 != 0x00 }) else { + return Data() + } + + let trimmed = Data(bytes[firstNonZeroIndex...]) + + if let first = trimmed.first, first >= 0x80 { + var prefixed = Data([0x00]) + prefixed.append(trimmed) + return prefixed + } + return trimmed + } + func ecdsaSignature(_ rawRepresentation: Data, keyType: KeyType) -> Data { let rawLength = rawRepresentation.count/2 - // Check if we need to pad with 0x00 to prevent certain - // ssh servers from thinking r or s is negative - let paddingRange: ClosedRange = 0x80...0xFF - var r = Data(rawRepresentation[0..(for secret: SecretType) -> String { - let minimalHex = keyWriter.openSSHMD5Fingerprint(secret: secret).replacingOccurrences(of: ":", with: "") - return directory.appending(component: "\(minimalHex).pub").path() - } /// Short-circuit check to ship enumerating a bunch of paths if there's nothing in the cert directory. public var hasAnyCertificates: Bool { diff --git a/Sources/Packages/Sources/SecretAgentKit/SSHAgentInputParser.swift b/Sources/Packages/Sources/SecretAgentKit/SSHAgentInputParser.swift index 6e9a2ee..e8c4d61 100644 --- a/Sources/Packages/Sources/SecretAgentKit/SSHAgentInputParser.swift +++ b/Sources/Packages/Sources/SecretAgentKit/SSHAgentInputParser.swift @@ -1,11 +1,12 @@ import Foundation import OSLog import SecretKit +import SSHProtocolKit public protocol SSHAgentInputParserProtocol { func parse(data: Data) async throws -> SSHAgent.Request - + } public struct SSHAgentInputParser: SSHAgentInputParserProtocol { @@ -13,7 +14,7 @@ public struct SSHAgentInputParser: SSHAgentInputParserProtocol { private let logger = Logger(subsystem: "com.maxgoedjen.secretive.secretagent", category: "InputParser") public init() { - + } public func parse(data: Data) throws(AgentParsingError) -> SSHAgent.Request { diff --git a/Sources/Packages/Sources/SecretAgentKit/SigningRequestTracer.swift b/Sources/Packages/Sources/SecretAgentKit/SigningRequestTracer.swift index 8801d6f..1a2226f 100644 --- a/Sources/Packages/Sources/SecretAgentKit/SigningRequestTracer.swift +++ b/Sources/Packages/Sources/SecretAgentKit/SigningRequestTracer.swift @@ -36,7 +36,7 @@ extension SigningRequestTracer { /// - Parameter pid: The process ID to look up. /// - Returns: A ``SecretKit.SigningRequestProvenance.Process`` describing the process. func process(from pid: Int32) -> SigningRequestProvenance.Process { - var pidAndNameInfo = self.pidAndNameInfo(from: pid) + var pidAndNameInfo = unsafe self.pidAndNameInfo(from: pid) let ppid = unsafe pidAndNameInfo.kp_eproc.e_ppid != 0 ? pidAndNameInfo.kp_eproc.e_ppid : nil let procName = unsafe withUnsafeMutablePointer(to: &pidAndNameInfo.kp_proc.p_comm.0) { pointer in unsafe String(cString: pointer) diff --git a/Sources/Packages/Sources/SecretAgentKit/SocketController.swift b/Sources/Packages/Sources/SecretAgentKit/SocketController.swift index 9de2564..7839037 100644 --- a/Sources/Packages/Sources/SecretAgentKit/SocketController.swift +++ b/Sources/Packages/Sources/SecretAgentKit/SocketController.swift @@ -36,16 +36,21 @@ public struct SocketController { logger.debug("Socket controller path is clear") port = SocketPort(path: path) fileHandle = FileHandle(fileDescriptor: port.socket, closeOnDealloc: true) - Task { [fileHandle, sessionsContinuation, logger] in - for await notification in NotificationCenter.default.notifications(named: .NSFileHandleConnectionAccepted) { + Task { @MainActor [fileHandle, sessionsContinuation, logger] in + // Create the sequence before triggering the notification to + // ensure it will not be missed. + let connectionAcceptedNotifications = NotificationCenter.default.notifications(named: .NSFileHandleConnectionAccepted) + + fileHandle.acceptConnectionInBackgroundAndNotify() + + for await notification in connectionAcceptedNotifications { logger.debug("Socket controller accepted connection") guard let new = notification.userInfo?[NSFileHandleNotificationFileHandleItem] as? FileHandle else { continue } let session = Session(fileHandle: new) sessionsContinuation.yield(session) - await fileHandle.acceptConnectionInBackgroundAndNotifyOnMainActor() + fileHandle.acceptConnectionInBackgroundAndNotify() } } - fileHandle.acceptConnectionInBackgroundAndNotify(forModes: [RunLoop.Mode.common]) logger.debug("Socket listening at \(path)") } @@ -77,8 +82,14 @@ extension SocketController { self.fileHandle = fileHandle provenance = SigningRequestTracer().provenance(from: fileHandle) (messages, messagesContinuation) = AsyncStream.makeStream() - Task { [messagesContinuation, logger] in - for await _ in NotificationCenter.default.notifications(named: .NSFileHandleDataAvailable, object: fileHandle) { + Task { @MainActor [messagesContinuation, logger] in + // Create the sequence before triggering the notification to + // ensure it will not be missed. + let dataAvailableNotifications = NotificationCenter.default.notifications(named: .NSFileHandleDataAvailable, object: fileHandle) + + fileHandle.waitForDataInBackgroundAndNotify() + + for await _ in dataAvailableNotifications { let data = fileHandle.availableData guard !data.isEmpty else { logger.debug("Socket controller received empty data, ending continuation.") @@ -90,16 +101,13 @@ extension SocketController { logger.debug("Socket controller yielded data.") } } - Task { - await fileHandle.waitForDataInBackgroundAndNotifyOnMainActor() - } } /// Writes new data to the socket. /// - Parameter data: The data to write. - public func write(_ data: Data) async throws { - try fileHandle.write(contentsOf: data) - await fileHandle.waitForDataInBackgroundAndNotifyOnMainActor() + @MainActor public func write(_ data: Data) throws { + try fileHandle.write(contentsOf: data) + fileHandle.waitForDataInBackgroundAndNotify() } /// Closes the socket and cleans up resources. @@ -113,22 +121,6 @@ extension SocketController { } -private extension FileHandle { - - /// Ensures waitForDataInBackgroundAndNotify will be called on the main actor. - @MainActor func waitForDataInBackgroundAndNotifyOnMainActor() { - waitForDataInBackgroundAndNotify() - } - - - /// Ensures acceptConnectionInBackgroundAndNotify will be called on the main actor. - /// - Parameter modes: the runloop modes to use. - @MainActor func acceptConnectionInBackgroundAndNotifyOnMainActor(forModes modes: [RunLoop.Mode]? = [RunLoop.Mode.common]) { - acceptConnectionInBackgroundAndNotify(forModes: modes) - } - -} - private extension SocketPort { convenience init(path: String) { diff --git a/Sources/Packages/Tests/SecretKitTests/OpenSSHPublicKeyWriterTests.swift b/Sources/Packages/Tests/SSHProtocolKitTests/OpenSSHPublicKeyWriterTests.swift similarity index 72% rename from Sources/Packages/Tests/SecretKitTests/OpenSSHPublicKeyWriterTests.swift rename to Sources/Packages/Tests/SSHProtocolKitTests/OpenSSHPublicKeyWriterTests.swift index 92c3132..806d399 100644 --- a/Sources/Packages/Tests/SecretKitTests/OpenSSHPublicKeyWriterTests.swift +++ b/Sources/Packages/Tests/SSHProtocolKitTests/OpenSSHPublicKeyWriterTests.swift @@ -1,8 +1,7 @@ import Foundation import Testing @testable import SecretKit -@testable import SecureEnclaveSecretKit -@testable import SmartCardSecretKit +import SSHProtocolKit @Suite struct OpenSSHPublicKeyWriterTests { @@ -47,8 +46,8 @@ import Testing extension OpenSSHPublicKeyWriterTests { enum Constants { - static let ecdsa256Secret = SmartCard.Secret(id: Data(), name: "Test Key (ECDSA 256)", publicKey: Data(base64Encoded: "BOVEjgAA5PHqRgwykjN5qM21uWCHFSY/Sqo5gkHAkn+e1MMQKHOLga7ucB9b3mif33MBid59GRK9GEPVlMiSQwo=")!, attributes: Attributes(keyType: KeyType(algorithm: .ecdsa, size: 256), authentication: .notRequired, publicKeyAttribution: "test@example.com")) - static let ecdsa384Secret = SmartCard.Secret(id: Data(), name: "Test Key (ECDSA 384)", publicKey: Data(base64Encoded: "BG2MNc/C5OTHFE2tBvbZCVcpOGa8vBMquiTLkH4lwkeqOPxhi+PyYUfQZMTRJNPiTyWPoMBqNiCIFRVv60yPN/AHufHaOgbdTP42EgMlMMImkAjYUEv9DESHTVIs2PW1yQ==")!, attributes: Attributes(keyType: KeyType(algorithm: .ecdsa, size: 384), authentication: .notRequired, publicKeyAttribution: "test@example.com")) + static let ecdsa256Secret = TestSecret(id: Data(), name: "Test Key (ECDSA 256)", publicKey: Data(base64Encoded: "BOVEjgAA5PHqRgwykjN5qM21uWCHFSY/Sqo5gkHAkn+e1MMQKHOLga7ucB9b3mif33MBid59GRK9GEPVlMiSQwo=")!, attributes: Attributes(keyType: KeyType(algorithm: .ecdsa, size: 256), authentication: .notRequired, publicKeyAttribution: "test@example.com")) + static let ecdsa384Secret = TestSecret(id: Data(), name: "Test Key (ECDSA 384)", publicKey: Data(base64Encoded: "BG2MNc/C5OTHFE2tBvbZCVcpOGa8vBMquiTLkH4lwkeqOPxhi+PyYUfQZMTRJNPiTyWPoMBqNiCIFRVv60yPN/AHufHaOgbdTP42EgMlMMImkAjYUEv9DESHTVIs2PW1yQ==")!, attributes: Attributes(keyType: KeyType(algorithm: .ecdsa, size: 384), authentication: .notRequired, publicKeyAttribution: "test@example.com")) } diff --git a/Sources/Packages/Tests/SecretAgentKitTests/OpenSSHReaderTests.swift b/Sources/Packages/Tests/SSHProtocolKitTests/OpenSSHReaderTests.swift similarity index 93% rename from Sources/Packages/Tests/SecretAgentKitTests/OpenSSHReaderTests.swift rename to Sources/Packages/Tests/SSHProtocolKitTests/OpenSSHReaderTests.swift index 34201c6..1e3d95e 100644 --- a/Sources/Packages/Tests/SecretAgentKitTests/OpenSSHReaderTests.swift +++ b/Sources/Packages/Tests/SSHProtocolKitTests/OpenSSHReaderTests.swift @@ -1,8 +1,6 @@ import Foundation import Testing -@testable import SecretAgentKit -@testable import SecureEnclaveSecretKit -@testable import SmartCardSecretKit +import SSHProtocolKit @Suite struct OpenSSHReaderTests { diff --git a/Sources/Packages/Tests/SSHProtocolKitTests/OpenSSHSignatureWriterTests.swift b/Sources/Packages/Tests/SSHProtocolKitTests/OpenSSHSignatureWriterTests.swift new file mode 100644 index 0000000..008fc77 --- /dev/null +++ b/Sources/Packages/Tests/SSHProtocolKitTests/OpenSSHSignatureWriterTests.swift @@ -0,0 +1,83 @@ +import Foundation +import Testing +import SSHProtocolKit +@testable import SecretKit + +@Suite struct OpenSSHSignatureWriterTests { + + private let writer = OpenSSHSignatureWriter() + + @Test func ecdsaMpintStripsUnnecessaryLeadingZeros() throws { + let secret = Constants.ecdsa256Secret + + // r has a leading 0x00 followed by 0x01 (< 0x80): the mpint must not keep the leading zero. + let rBytes: [UInt8] = [0x00] + (1...31).map { UInt8($0) } + let r = Data(rBytes) + // s has two leading 0x00 bytes followed by 0x7f (< 0x80): the mpint must not keep the leading zeros. + let sBytes: [UInt8] = [0x00, 0x00, 0x7f] + Array(repeating: UInt8(0x01), count: 29) + let s = Data(sBytes) + let rawRepresentation = r + s + + let response = writer.data(secret: secret, signature: rawRepresentation) + let (parsedR, parsedS) = try parseEcdsaSignatureMpints(from: response) + + #expect(parsedR == Data((1...31).map { UInt8($0) })) + #expect(parsedS == Data([0x7f] + Array(repeating: UInt8(0x01), count: 29))) + } + + @Test func ecdsaMpintPrefixesZeroWhenHighBitSet() throws { + let secret = Constants.ecdsa256Secret + + // r starts with 0x80 (high bit set): mpint must be prefixed with 0x00. + let r = Data([UInt8(0x80)] + Array(repeating: UInt8(0x01), count: 31)) + let s = Data([UInt8(0x01)] + Array(repeating: UInt8(0x02), count: 31)) + let rawRepresentation = r + s + + let response = writer.data(secret: secret, signature: rawRepresentation) + let (parsedR, parsedS) = try parseEcdsaSignatureMpints(from: response) + + #expect(parsedR == Data([0x00, 0x80] + Array(repeating: UInt8(0x01), count: 31))) + #expect(parsedS == Data([0x01] + Array(repeating: UInt8(0x02), count: 31))) + } + +} + +private extension OpenSSHSignatureWriterTests { + + enum Constants { + static let ecdsa256Secret = TestSecret( + id: Data(), + name: "Test Key (ECDSA 256)", + publicKey: Data(repeating: 0x01, count: 65), + attributes: Attributes( + keyType: KeyType(algorithm: .ecdsa, size: 256), + authentication: .notRequired, + publicKeyAttribution: "test@example.com" + ) + ) + } + + enum ParseError: Error { + case eof + case invalidAlgorithm + } + + func parseEcdsaSignatureMpints(from openSSHSignedData: Data) throws -> (r: Data, s: Data) { + let reader = OpenSSHReader(data: openSSHSignedData) + + // Prefix + _ = try reader.readNextBytes(as: UInt32.self) + + let algorithm = try reader.readNextChunkAsString() + guard algorithm == "ecdsa-sha2-nistp256" else { + throw ParseError.invalidAlgorithm + } + + let sigReader = try reader.readNextChunkAsSubReader() + let r = try sigReader.readNextChunk() + let s = try sigReader.readNextChunk() + return (r, s) + } + +} + diff --git a/Sources/Packages/Tests/SSHProtocolKitTests/TestSecret.swift b/Sources/Packages/Tests/SSHProtocolKitTests/TestSecret.swift new file mode 100644 index 0000000..7dca504 --- /dev/null +++ b/Sources/Packages/Tests/SSHProtocolKitTests/TestSecret.swift @@ -0,0 +1,11 @@ +import Foundation +import SecretKit + +public struct TestSecret: SecretKit.Secret { + + public let id: Data + public let name: String + public let publicKey: Data + public var attributes: Attributes + +} diff --git a/Sources/Packages/Tests/SecretAgentKitTests/AgentTests.swift b/Sources/Packages/Tests/SecretAgentKitTests/AgentTests.swift index bbef669..f946524 100644 --- a/Sources/Packages/Tests/SecretAgentKitTests/AgentTests.swift +++ b/Sources/Packages/Tests/SecretAgentKitTests/AgentTests.swift @@ -1,6 +1,7 @@ import Foundation import Testing import CryptoKit +@testable import SSHProtocolKit @testable import SecretKit @testable import SecretAgentKit @@ -44,8 +45,8 @@ import CryptoKit let agent = Agent(storeList: list) let response = await agent.handle(request: request, provenance: .test) let responseReader = OpenSSHReader(data: response) - let length = try responseReader.readNextBytes(as: UInt32.self).bigEndian - let type = try responseReader.readNextBytes(as: UInt8.self).bigEndian + let length = try responseReader.readNextBytes(as: UInt32.self) + let type = try responseReader.readNextBytes(as: UInt8.self) #expect(length == response.count - MemoryLayout.size) #expect(type == SSHAgent.Response.agentSignResponse.rawValue) let outer = OpenSSHReader(data: responseReader.remaining) diff --git a/Sources/Packages/Tests/SecretAgentKitTests/StubStore.swift b/Sources/Packages/Tests/SecretAgentKitTests/StubStore.swift index c3a01d7..381f14d 100644 --- a/Sources/Packages/Tests/SecretAgentKitTests/StubStore.swift +++ b/Sources/Packages/Tests/SecretAgentKitTests/StubStore.swift @@ -1,6 +1,7 @@ import Foundation import SecretKit import CryptoKit +import SSHProtocolKit struct Stub {} diff --git a/Sources/SecretAgent/AppDelegate.swift b/Sources/SecretAgent/AppDelegate.swift index 28bef7e..49c109a 100644 --- a/Sources/SecretAgent/AppDelegate.swift +++ b/Sources/SecretAgent/AppDelegate.swift @@ -6,6 +6,7 @@ import SmartCardSecretKit import SecretAgentKit import Brief import Observation +import Common @main class AppDelegate: NSObject, NSApplicationDelegate { @@ -21,12 +22,12 @@ class AppDelegate: NSObject, NSApplicationDelegate { }() private let updater = Updater(checkOnLaunch: true) private let notifier = Notifier() - private let publicKeyFileStoreController = PublicKeyFileStoreController(homeDirectory: URL.homeDirectory) + private let publicKeyFileStoreController = PublicKeyFileStoreController(directory: URL.publicKeyDirectory) private lazy var agent: Agent = { Agent(storeList: storeList, witness: notifier) }() private lazy var socketController: SocketController = { - let path = (NSHomeDirectory() as NSString).appendingPathComponent("socket.ssh") as String + let path = URL.socketPath as String return SocketController(path: path) }() private let logger = Logger(subsystem: "com.maxgoedjen.secretive.secretagent", category: "AppDelegate") @@ -41,7 +42,7 @@ class AppDelegate: NSObject, NSApplicationDelegate { for await message in session.messages { let request = try await inputParser.parse(data: message) let agentResponse = await agent.handle(request: request, provenance: session.provenance) - try await session.write(agentResponse) + try session.write(agentResponse) } } catch { try session.close() diff --git a/Sources/SecretAgent/XPCInputParser.swift b/Sources/SecretAgent/XPCInputParser.swift index b78f316..a3d7f28 100644 --- a/Sources/SecretAgent/XPCInputParser.swift +++ b/Sources/SecretAgent/XPCInputParser.swift @@ -3,6 +3,7 @@ import SecretAgentKit import Brief import XPCWrappers import OSLog +import SSHProtocolKit /// Delegates all agent input parsing to an XPC service which wraps OpenSSH public final class XPCAgentInputParser: SSHAgentInputParserProtocol { diff --git a/Sources/SecretAgentInputParser/SecretAgentInputParser.swift b/Sources/SecretAgentInputParser/SecretAgentInputParser.swift index cc0c8fd..6f69047 100644 --- a/Sources/SecretAgentInputParser/SecretAgentInputParser.swift +++ b/Sources/SecretAgentInputParser/SecretAgentInputParser.swift @@ -2,6 +2,7 @@ import Foundation import OSLog import XPCWrappers import SecretAgentKit +import SSHProtocolKit final class SecretAgentInputParser: NSObject, XPCProtocol { diff --git a/Sources/Secretive.xcodeproj/project.pbxproj b/Sources/Secretive.xcodeproj/project.pbxproj index 89cb503..7d70771 100644 --- a/Sources/Secretive.xcodeproj/project.pbxproj +++ b/Sources/Secretive.xcodeproj/project.pbxproj @@ -9,7 +9,7 @@ /* Begin PBXBuildFile section */ 2C4A9D2F2636FFD3008CC8E2 /* EditSecretView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2C4A9D2E2636FFD3008CC8E2 /* EditSecretView.swift */; }; 50020BB024064869003D4025 /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 50020BAF24064869003D4025 /* AppDelegate.swift */; }; - 50033AC327813F1700253856 /* BundleIDs.swift in Sources */ = {isa = PBXBuildFile; fileRef = 50033AC227813F1700253856 /* BundleIDs.swift */; }; + 5002C3AB2EEF483300FFAD22 /* XPCWrappers in Frameworks */ = {isa = PBXBuildFile; productRef = 5002C3AA2EEF483300FFAD22 /* XPCWrappers */; }; 5003EF3B278005E800DF2006 /* SecretKit in Frameworks */ = {isa = PBXBuildFile; productRef = 5003EF3A278005E800DF2006 /* SecretKit */; }; 5003EF3D278005F300DF2006 /* Brief in Frameworks */ = {isa = PBXBuildFile; productRef = 5003EF3C278005F300DF2006 /* Brief */; }; 5003EF3F278005F300DF2006 /* SecretAgentKit in Frameworks */ = {isa = PBXBuildFile; productRef = 5003EF3E278005F300DF2006 /* SecretAgentKit */; }; @@ -26,7 +26,6 @@ 50153E22250DECA300525160 /* SecretListItemView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 50153E21250DECA300525160 /* SecretListItemView.swift */; }; 501578132E6C0479004A37D0 /* XPCInputParser.swift in Sources */ = {isa = PBXBuildFile; fileRef = 501578122E6C0479004A37D0 /* XPCInputParser.swift */; }; 5018F54F24064786002EB505 /* Notifier.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5018F54E24064786002EB505 /* Notifier.swift */; }; - 504788EC2E680DC800B4556F /* URLs.swift in Sources */ = {isa = PBXBuildFile; fileRef = 504788EB2E680DC400B4556F /* URLs.swift */; }; 504788F22E681F3A00B4556F /* Instructions.swift in Sources */ = {isa = PBXBuildFile; fileRef = 504788F12E681F3A00B4556F /* Instructions.swift */; }; 504788F42E681F6900B4556F /* ToolConfigurationView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 504788F32E681F6900B4556F /* ToolConfigurationView.swift */; }; 504788F62E68206F00B4556F /* GettingStartedView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 504788F52E68206F00B4556F /* GettingStartedView.swift */; }; @@ -69,6 +68,8 @@ 50BDCB762E6450950072D2E7 /* ConfigurationItemView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 50BDCB752E6450950072D2E7 /* ConfigurationItemView.swift */; }; 50C385A52407A76D00AF2719 /* SecretDetailView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 50C385A42407A76D00AF2719 /* SecretDetailView.swift */; }; 50CF4ABC2E601B0F005588DC /* ActionButtonStyle.swift in Sources */ = {isa = PBXBuildFile; fileRef = 50CF4ABB2E601B0F005588DC /* ActionButtonStyle.swift */; }; + 50E0145C2EDB9CDF00B121F1 /* Common in Frameworks */ = {isa = PBXBuildFile; productRef = 50E0145B2EDB9CDF00B121F1 /* Common */; }; + 50E0145E2EDB9CE400B121F1 /* Common in Frameworks */ = {isa = PBXBuildFile; productRef = 50E0145D2EDB9CE400B121F1 /* Common */; }; 50E4C4532E73C78C00C73783 /* WindowBackgroundStyle.swift in Sources */ = {isa = PBXBuildFile; fileRef = 50E4C4522E73C78900C73783 /* WindowBackgroundStyle.swift */; }; 50E4C4C32E7765DF00C73783 /* AboutView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 50E4C4C22E7765DF00C73783 /* AboutView.swift */; }; 50E4C4C82E777E4200C73783 /* AppIcon.icon in Resources */ = {isa = PBXBuildFile; fileRef = 50E4C4C72E777E4200C73783 /* AppIcon.icon */; }; @@ -180,14 +181,12 @@ /* Begin PBXFileReference section */ 2C4A9D2E2636FFD3008CC8E2 /* EditSecretView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EditSecretView.swift; sourceTree = ""; }; 50020BAF24064869003D4025 /* AppDelegate.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = ""; }; - 50033AC227813F1700253856 /* BundleIDs.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BundleIDs.swift; sourceTree = ""; }; 5003EF39278005C800DF2006 /* Packages */ = {isa = PBXFileReference; lastKnownFileType = wrapper; path = Packages; sourceTree = ""; }; 5008C23D2E525D8200507AC2 /* Localizable.xcstrings */ = {isa = PBXFileReference; lastKnownFileType = text.json.xcstrings; name = Localizable.xcstrings; path = Packages/Resources/Localizable.xcstrings; sourceTree = SOURCE_ROOT; }; 50153E1F250AFCB200525160 /* UpdateView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UpdateView.swift; sourceTree = ""; }; 50153E21250DECA300525160 /* SecretListItemView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SecretListItemView.swift; sourceTree = ""; }; 501578122E6C0479004A37D0 /* XPCInputParser.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = XPCInputParser.swift; sourceTree = ""; }; 5018F54E24064786002EB505 /* Notifier.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Notifier.swift; sourceTree = ""; }; - 504788EB2E680DC400B4556F /* URLs.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = URLs.swift; sourceTree = ""; }; 504788F12E681F3A00B4556F /* Instructions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Instructions.swift; sourceTree = ""; }; 504788F32E681F6900B4556F /* ToolConfigurationView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ToolConfigurationView.swift; sourceTree = ""; }; 504788F52E68206F00B4556F /* GettingStartedView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GettingStartedView.swift; sourceTree = ""; }; @@ -246,6 +245,7 @@ isa = PBXFrameworksBuildPhase; buildActionMask = 2147483647; files = ( + 50E0145C2EDB9CDF00B121F1 /* Common in Frameworks */, 5003EF3B278005E800DF2006 /* SecretKit in Frameworks */, 501421622781262300BBAA70 /* Brief in Frameworks */, 5003EF5F2780081600DF2006 /* SecureEnclaveSecretKit in Frameworks */, @@ -266,6 +266,7 @@ isa = PBXFrameworksBuildPhase; buildActionMask = 2147483647; files = ( + 5002C3AB2EEF483300FFAD22 /* XPCWrappers in Frameworks */, 50692E6C2E6FFA510043C7BB /* SecretAgentKit in Frameworks */, ); runOnlyForDeploymentPostprocessing = 0; @@ -279,20 +280,13 @@ 5003EF652780081B00DF2006 /* SmartCardSecretKit in Frameworks */, 5003EF3F278005F300DF2006 /* SecretAgentKit in Frameworks */, 5003EF41278005FA00DF2006 /* SecretKit in Frameworks */, + 50E0145E2EDB9CE400B121F1 /* Common in Frameworks */, ); runOnlyForDeploymentPostprocessing = 0; }; /* End PBXFrameworksBuildPhase section */ /* Begin PBXGroup section */ - 50033AC427813F1C00253856 /* Helpers */ = { - isa = PBXGroup; - children = ( - 50033AC227813F1700253856 /* BundleIDs.swift */, - ); - path = Helpers; - sourceTree = ""; - }; 504788ED2E681EB200B4556F /* Modifiers */ = { isa = PBXGroup; children = ( @@ -376,7 +370,6 @@ 50617D8223FCE48E0099B055 /* App.swift */, 508A58B0241ED1C40069DC07 /* Views */, 508A58B1241ED1EA0069DC07 /* Controllers */, - 50033AC427813F1C00253856 /* Helpers */, 50617D8E23FCE48E0099B055 /* Info.plist */, 508BF28D25B4F005009EFB7E /* InternetAccessPolicy.plist */, 50E4C4C72E777E4200C73783 /* AppIcon.icon */, @@ -442,7 +435,6 @@ 508A58B1241ED1EA0069DC07 /* Controllers */ = { isa = PBXGroup; children = ( - 504788EB2E680DC400B4556F /* URLs.swift */, 508A58B2241ED2180069DC07 /* AgentStatusChecker.swift */, 5091D2BB25183B830049FD9B /* ApplicationDirectoryController.swift */, 50571E0224393C2600F76F6C /* JustUpdatedChecker.swift */, @@ -507,6 +499,7 @@ 5003EF5E2780081600DF2006 /* SecureEnclaveSecretKit */, 5003EF602780081600DF2006 /* SmartCardSecretKit */, 501421612781262300BBAA70 /* Brief */, + 50E0145B2EDB9CDF00B121F1 /* Common */, ); productName = Secretive; productReference = 50617D7F23FCE48E0099B055 /* Secretive.app */; @@ -548,6 +541,7 @@ name = SecretAgentInputParser; packageProductDependencies = ( 50692E6B2E6FFA510043C7BB /* SecretAgentKit */, + 5002C3AA2EEF483300FFAD22 /* XPCWrappers */, ); productName = SecretAgentInputParser; productReference = 50692E502E6FF9D20043C7BB /* SecretAgentInputParser.xpc */; @@ -577,6 +571,7 @@ 5003EF40278005FA00DF2006 /* SecretKit */, 5003EF622780081B00DF2006 /* SecureEnclaveSecretKit */, 5003EF642780081B00DF2006 /* SmartCardSecretKit */, + 50E0145D2EDB9CE400B121F1 /* Common */, ); productName = SecretAgent; productReference = 50A3B78A24026B7500D209EA /* SecretAgent.app */; @@ -687,7 +682,6 @@ 2C4A9D2F2636FFD3008CC8E2 /* EditSecretView.swift in Sources */, 50E4C4532E73C78C00C73783 /* WindowBackgroundStyle.swift in Sources */, 5091D2BC25183B830049FD9B /* ApplicationDirectoryController.swift in Sources */, - 504788EC2E680DC800B4556F /* URLs.swift in Sources */, 504789232E697DD300B4556F /* BoxBackgroundStyle.swift in Sources */, 5066A6C22516F303004B5A36 /* SetupView.swift in Sources */, 5065E313295517C500E16645 /* ToolbarButtonStyle.swift in Sources */, @@ -697,7 +691,6 @@ 50571E0324393C2600F76F6C /* JustUpdatedChecker.swift in Sources */, 5079BA0F250F29BF00EA86F4 /* StoreListView.swift in Sources */, 50617DD223FCEFA90099B055 /* PreviewStore.swift in Sources */, - 50033AC327813F1700253856 /* BundleIDs.swift in Sources */, 50BDCB722E63BAF20072D2E7 /* AgentStatusView.swift in Sources */, 508A58B3241ED2180069DC07 /* AgentStatusChecker.swift in Sources */, 50C385A52407A76D00AF2719 /* SecretDetailView.swift in Sources */, @@ -1509,6 +1502,10 @@ /* End XCConfigurationList section */ /* Begin XCSwiftPackageProductDependency section */ + 5002C3AA2EEF483300FFAD22 /* XPCWrappers */ = { + isa = XCSwiftPackageProductDependency; + productName = XPCWrappers; + }; 5003EF3A278005E800DF2006 /* SecretKit */ = { isa = XCSwiftPackageProductDependency; productName = SecretKit; @@ -1557,6 +1554,14 @@ isa = XCSwiftPackageProductDependency; productName = SecretAgentKit; }; + 50E0145B2EDB9CDF00B121F1 /* Common */ = { + isa = XCSwiftPackageProductDependency; + productName = Common; + }; + 50E0145D2EDB9CE400B121F1 /* Common */ = { + isa = XCSwiftPackageProductDependency; + productName = Common; + }; /* End XCSwiftPackageProductDependency section */ }; rootObject = 50617D7723FCE48D0099B055 /* Project object */; diff --git a/Sources/Secretive.xcodeproj/xcshareddata/xcschemes/PackageTests.xcscheme b/Sources/Secretive.xcodeproj/xcshareddata/xcschemes/PackageTests.xcscheme index 500661b..7b1c414 100644 --- a/Sources/Secretive.xcodeproj/xcshareddata/xcschemes/PackageTests.xcscheme +++ b/Sources/Secretive.xcodeproj/xcshareddata/xcschemes/PackageTests.xcscheme @@ -14,7 +14,8 @@ shouldUseLaunchSchemeArgsEnv = "YES"> + reference = "container:Config/Secretive.xctestplan" + default = "YES"> diff --git a/Sources/Secretive/Controllers/AgentStatusChecker.swift b/Sources/Secretive/Controllers/AgentStatusChecker.swift index 4cb4620..6e6cf4e 100644 --- a/Sources/Secretive/Controllers/AgentStatusChecker.swift +++ b/Sources/Secretive/Controllers/AgentStatusChecker.swift @@ -4,6 +4,7 @@ import SecretKit import Observation import OSLog import ServiceManagement +import Common @MainActor protocol AgentLaunchControllerProtocol: Observable, Sendable { var running: Bool { get } diff --git a/Sources/Secretive/Controllers/URLs.swift b/Sources/Secretive/Controllers/URLs.swift deleted file mode 100644 index 3ea1fe5..0000000 --- a/Sources/Secretive/Controllers/URLs.swift +++ /dev/null @@ -1,25 +0,0 @@ -import Foundation - -extension URL { - - static var agentHomeURL: URL { - URL(fileURLWithPath: URL.homeDirectory.path().replacingOccurrences(of: Bundle.hostBundleID, with: Bundle.agentBundleID)) - } - - static var socketPath: String { - URL.agentHomeURL.appendingPathComponent("socket.ssh").path() - } - -} - -extension String { - - var normalizedPathAndFolder: (String, String) { - // All foundation-based normalization methods replace this with the container directly. - let processedPath = replacingOccurrences(of: "~", with: "/Users/\(NSUserName())") - let url = URL(filePath: processedPath) - let folder = url.deletingLastPathComponent().path() - return (processedPath, folder) - } - -} diff --git a/Sources/Secretive/Views/Configuration/ToolConfigurationView.swift b/Sources/Secretive/Views/Configuration/ToolConfigurationView.swift index 8696e06..495254b 100644 --- a/Sources/Secretive/Views/Configuration/ToolConfigurationView.swift +++ b/Sources/Secretive/Views/Configuration/ToolConfigurationView.swift @@ -1,5 +1,7 @@ import SwiftUI import SecretKit +import SSHProtocolKit +import Common struct ToolConfigurationView: View { @@ -111,10 +113,9 @@ struct ToolConfigurationView: View { let writer = OpenSSHPublicKeyWriter() let gitAllowedSignersString = [email.isEmpty ? String(localized: .integrationsConfigureUsingEmailPlaceholder) : email, writer.openSSHString(secret: selectedSecret)] .joined(separator: " ") - let fileController = PublicKeyFileStoreController(homeDirectory: URL.agentHomeURL) return text .replacingOccurrences(of: Instructions.Constants.publicKeyPlaceholder, with: gitAllowedSignersString) - .replacingOccurrences(of: Instructions.Constants.publicKeyPathPlaceholder, with: fileController.publicKeyPath(for: selectedSecret)) + .replacingOccurrences(of: Instructions.Constants.publicKeyPathPlaceholder, with: URL.publicKeyPath(for: selectedSecret, in: URL.publicKeyDirectory)) } } diff --git a/Sources/Secretive/Views/Secrets/SecretDetailView.swift b/Sources/Secretive/Views/Secrets/SecretDetailView.swift index b3940ff..da9cf75 100644 --- a/Sources/Secretive/Views/Secrets/SecretDetailView.swift +++ b/Sources/Secretive/Views/Secrets/SecretDetailView.swift @@ -1,12 +1,13 @@ import SwiftUI import SecretKit +import Common +import SSHProtocolKit struct SecretDetailView: View { let secret: SecretType private let keyWriter = OpenSSHPublicKeyWriter() - private let publicKeyFileStoreController = PublicKeyFileStoreController(homeDirectory: URL.agentHomeURL) var body: some View { ScrollView { @@ -21,7 +22,7 @@ struct SecretDetailView: View { CopyableView(title: .secretDetailPublicKeyLabel, image: Image(systemName: "key"), text: keyString) Spacer() .frame(height: 20) - CopyableView(title: .secretDetailPublicKeyPathLabel, image: Image(systemName: "lock.doc"), text: publicKeyFileStoreController.publicKeyPath(for: secret), showRevealInFinder: true) + CopyableView(title: .secretDetailPublicKeyPathLabel, image: Image(systemName: "lock.doc"), text: URL.publicKeyPath(for: secret, in: URL.publicKeyDirectory), showRevealInFinder: true) Spacer() } } diff --git a/Sources/Secretive/Views/Secrets/SecretListItemView.swift b/Sources/Secretive/Views/Secrets/SecretListItemView.swift index 6cffb94..ed63006 100644 --- a/Sources/Secretive/Views/Secrets/SecretListItemView.swift +++ b/Sources/Secretive/Views/Secrets/SecretListItemView.swift @@ -24,6 +24,18 @@ struct SecretListItemView: View { Text(secret.name) } } + .sheet(isPresented: $isRenaming, onDismiss: { + renamedSecret(secret) + }, content: { + if let modifiable = store as? AnySecretStoreModifiable { + EditSecretView(store: modifiable, secret: secret) + } + }) + .showingDeleteConfirmation(isPresented: $isDeleting, secret, store as? AnySecretStoreModifiable) { deleted in + if deleted { + deletedSecret(secret) + } + } .contextMenu { if store is AnySecretStoreModifiable { Button(action: { isRenaming = true }) { @@ -36,17 +48,5 @@ struct SecretListItemView: View { } } } - .showingDeleteConfirmation(isPresented: $isDeleting, secret, store as? AnySecretStoreModifiable) { deleted in - if deleted { - deletedSecret(secret) - } - } - .sheet(isPresented: $isRenaming, onDismiss: { - renamedSecret(secret) - }, content: { - if let modifiable = store as? AnySecretStoreModifiable { - EditSecretView(store: modifiable, secret: secret) - } - }) } }