Compare commits

..

3 Commits

Author SHA1 Message Date
Max Goedjen
ece3865d9a Merge branch 'main' into sshextensions 2026-03-25 15:07:22 -07:00
Max Goedjen
6b1f5bbb7c WIP 2026-03-12 12:46:45 -07:00
Max Goedjen
f848eb659e Messy WIP for agent extensions 2026-03-11 14:57:38 -07:00
66 changed files with 983 additions and 2472 deletions

View File

@@ -10,10 +10,6 @@ Security is obviously paramount for a project like Secretive. As such, any contr
Secretive is designed to be easily auditable by people who are considering using it. In keeping with this, Secretive has no third party dependencies, and any contributions which bring in new dependencies will be rejected. Secretive is designed to be easily auditable by people who are considering using it. In keeping with this, Secretive has no third party dependencies, and any contributions which bring in new dependencies will be rejected.
### AI/LLM Policy
For security and auditing reasons similar to the policy Secretive has on dependencies, any code generated with AI or LLM tools will not be accepted.
## Code of Conduct ## Code of Conduct
All contributors must abide by the [Code of Conduct](CODE_OF_CONDUCT.md) All contributors must abide by the [Code of Conduct](CODE_OF_CONDUCT.md)

View File

@@ -22,15 +22,9 @@ let package = Package(
.library( .library(
name: "SmartCardSecretKit", name: "SmartCardSecretKit",
targets: ["SmartCardSecretKit"]), targets: ["SmartCardSecretKit"]),
.library(
name: "CertificateKit",
targets: ["CertificateKit"]),
.library( .library(
name: "SSHProtocolKit", name: "SSHProtocolKit",
targets: ["SSHProtocolKit"]), targets: ["SSHProtocolKit"]),
.library(
name: "Formatters",
targets: ["Formatters"]),
], ],
dependencies: [ dependencies: [
], ],
@@ -62,16 +56,9 @@ let package = Package(
resources: [localization], resources: [localization],
swiftSettings: swiftSettings swiftSettings: swiftSettings
), ),
.target(
name: "CertificateKit",
dependencies: ["SecretKit", "Formatters"],
path: "Sources/Packages/Sources/CertificateKit",
resources: [localization],
swiftSettings: swiftSettings,
),
.target( .target(
name: "SSHProtocolKit", name: "SSHProtocolKit",
dependencies: ["SecretKit", "CertificateKit"], dependencies: ["SecretKit"],
path: "Sources/Packages/Sources/SSHProtocolKit", path: "Sources/Packages/Sources/SSHProtocolKit",
resources: [localization], resources: [localization],
swiftSettings: swiftSettings, swiftSettings: swiftSettings,
@@ -82,13 +69,6 @@ let package = Package(
path: "Sources/Packages/Tests/SSHProtocolKitTests", path: "Sources/Packages/Tests/SSHProtocolKitTests",
swiftSettings: swiftSettings, swiftSettings: swiftSettings,
), ),
.target(
name: "Formatters",
dependencies: [],
path: "Sources/Packages/Sources/Formatters",
resources: [localization],
swiftSettings: swiftSettings,
),
] ]
) )

View File

@@ -19,21 +19,12 @@ let package = Package(
.library( .library(
name: "SmartCardSecretKit", name: "SmartCardSecretKit",
targets: ["SmartCardSecretKit"]), targets: ["SmartCardSecretKit"]),
.library(
name: "CertificateKit",
targets: ["CertificateKit"]),
.library( .library(
name: "SecretAgentKit", name: "SecretAgentKit",
targets: ["SecretAgentKit"]), targets: ["SecretAgentKit"]),
.library(
name: "Formatters",
targets: ["Formatters"]),
.library( .library(
name: "Common", name: "Common",
targets: ["Common"]), targets: ["Common"]),
.library(
name: "SharedXPCServices",
targets: ["SharedXPCServices"]),
.library( .library(
name: "Brief", name: "Brief",
targets: ["Brief"]), targets: ["Brief"]),
@@ -70,15 +61,9 @@ let package = Package(
resources: [localization], resources: [localization],
swiftSettings: swiftSettings, swiftSettings: swiftSettings,
), ),
.target(
name: "CertificateKit",
dependencies: ["SecretKit", "Formatters"],
resources: [localization],
swiftSettings: swiftSettings,
),
.target( .target(
name: "SecretAgentKit", name: "SecretAgentKit",
dependencies: ["SecretKit", "SSHProtocolKit", "CertificateKit", "Common", "Formatters"], dependencies: ["SecretKit", "SSHProtocolKit", "Common"],
resources: [localization], resources: [localization],
swiftSettings: swiftSettings, swiftSettings: swiftSettings,
), ),
@@ -88,7 +73,7 @@ let package = Package(
), ),
.target( .target(
name: "SSHProtocolKit", name: "SSHProtocolKit",
dependencies: ["SecretKit", "CertificateKit"], dependencies: ["SecretKit"],
resources: [localization], resources: [localization],
swiftSettings: swiftSettings, swiftSettings: swiftSettings,
), ),
@@ -97,24 +82,12 @@ let package = Package(
dependencies: ["SSHProtocolKit"], dependencies: ["SSHProtocolKit"],
swiftSettings: swiftSettings, swiftSettings: swiftSettings,
), ),
.target(
name: "Formatters",
dependencies: [],
resources: [localization],
swiftSettings: swiftSettings,
),
.target( .target(
name: "Common", name: "Common",
dependencies: ["SSHProtocolKit", "SecretKit"], dependencies: ["SSHProtocolKit", "SecretKit"],
resources: [localization], resources: [localization],
swiftSettings: swiftSettings, swiftSettings: swiftSettings,
), ),
.target(
name: "SharedXPCServices",
dependencies: ["CertificateKit", "SSHProtocolKit"],
resources: [localization],
swiftSettings: swiftSettings,
),
.target( .target(
name: "Brief", name: "Brief",
dependencies: ["XPCWrappers", "SSHProtocolKit"], dependencies: ["XPCWrappers", "SSHProtocolKit"],

View File

@@ -365,16 +365,6 @@
}, },
"shouldTranslate" : false "shouldTranslate" : false
}, },
"%@ - %@" : {
"localizations" : {
"en" : {
"stringUnit" : {
"state" : "new",
"value" : "%1$@ - %2$@"
}
}
}
},
"about_build_log_button" : { "about_build_log_button" : {
"extractionState" : "manual", "extractionState" : "manual",
"localizations" : { "localizations" : {
@@ -5557,130 +5547,6 @@
} }
} }
} }
},
"certificate_detail_critical_options_label" : {
"extractionState" : "manual",
"localizations" : {
"en" : {
"stringUnit" : {
"state" : "translated",
"value" : "Critical Options"
}
}
}
},
"certificate_detail_extensions_label" : {
"extractionState" : "manual",
"localizations" : {
"en" : {
"stringUnit" : {
"state" : "translated",
"value" : "Extensions"
}
}
}
},
"certificate_detail_key_id_label" : {
"extractionState" : "manual",
"localizations" : {
"en" : {
"stringUnit" : {
"state" : "translated",
"value" : "Key ID"
}
}
}
},
"certificate_detail_path_label" : {
"extractionState" : "manual",
"localizations" : {
"en" : {
"stringUnit" : {
"state" : "translated",
"value" : "Certificate Path"
}
}
}
},
"certificate_detail_principals_label" : {
"extractionState" : "manual",
"localizations" : {
"en" : {
"stringUnit" : {
"state" : "translated",
"value" : "Principals"
}
}
}
},
"certificate_detail_serial_label" : {
"extractionState" : "manual",
"localizations" : {
"en" : {
"stringUnit" : {
"state" : "translated",
"value" : "Serial Number"
}
}
}
},
"certificate_detail_sha256_public_key_fingerprint_label" : {
"extractionState" : "manual",
"localizations" : {
"en" : {
"stringUnit" : {
"state" : "translated",
"value" : "Public Key Fingerprint"
}
}
}
},
"certificate_detail_sha256_signing_key_fingerprint_label" : {
"extractionState" : "manual",
"localizations" : {
"en" : {
"stringUnit" : {
"state" : "translated",
"value" : "Signing CA Fingerprint"
}
}
}
},
"certificate_detail_valid_after_label" : {
"extractionState" : "manual",
"localizations" : {
"en" : {
"stringUnit" : {
"state" : "translated",
"value" : "Valid After"
}
}
}
},
"certificate_detail_valid_until_label" : {
"extractionState" : "manual",
"localizations" : {
"en" : {
"stringUnit" : {
"state" : "translated",
"value" : "Valid Until"
}
}
}
},
"certificate_detail_validity_range_label" : {
"extractionState" : "manual",
"localizations" : {
"en" : {
"stringUnit" : {
"state" : "translated",
"value" : "Validity Range"
}
}
}
},
"Certificates" : {
}, },
"copyable_click_to_copy_button" : { "copyable_click_to_copy_button" : {
"extractionState" : "manual", "extractionState" : "manual",
@@ -10128,181 +9994,181 @@
"af" : { "af" : {
"stringUnit" : { "stringUnit" : {
"state" : "new", "state" : "new",
"value" : "Delete %1$(name)@?" "value" : "Delete %1$(secretName)@?"
} }
}, },
"ar" : { "ar" : {
"stringUnit" : { "stringUnit" : {
"state" : "new", "state" : "new",
"value" : "Delete %1$(name)@?" "value" : "Delete %1$(secretName)@?"
} }
}, },
"ca" : { "ca" : {
"stringUnit" : { "stringUnit" : {
"state" : "translated", "state" : "translated",
"value" : "Esborrar %1$(name)@?" "value" : "Esborrar %1$(secretName)@?"
} }
}, },
"cs" : { "cs" : {
"stringUnit" : { "stringUnit" : {
"state" : "new", "state" : "new",
"value" : "Delete %1$(name)@?" "value" : "Delete %1$(secretName)@?"
} }
}, },
"da" : { "da" : {
"stringUnit" : { "stringUnit" : {
"state" : "new", "state" : "new",
"value" : "Delete %1$(name)@?" "value" : "Delete %1$(secretName)@?"
} }
}, },
"de" : { "de" : {
"stringUnit" : { "stringUnit" : {
"state" : "translated", "state" : "translated",
"value" : "%1$(name)@ Löschen?" "value" : "%1$(secretName)@ Löschen?"
} }
}, },
"el" : { "el" : {
"stringUnit" : { "stringUnit" : {
"state" : "new", "state" : "new",
"value" : "Delete %1$(name)@?" "value" : "Delete %1$(secretName)@?"
} }
}, },
"en" : { "en" : {
"stringUnit" : { "stringUnit" : {
"state" : "translated", "state" : "new",
"value" : "Delete %1$(name)@?" "value" : "Delete %1$(secretName)@?"
} }
}, },
"es" : { "es" : {
"stringUnit" : { "stringUnit" : {
"state" : "new", "state" : "new",
"value" : "Delete %1$(name)@?" "value" : "Delete %1$(secretName)@?"
} }
}, },
"fi" : { "fi" : {
"stringUnit" : { "stringUnit" : {
"state" : "translated", "state" : "translated",
"value" : "Poista %1$(name)@?" "value" : "Poista %1$(secretName)@?"
} }
}, },
"fr" : { "fr" : {
"stringUnit" : { "stringUnit" : {
"state" : "translated", "state" : "translated",
"value" : "Supprimer %1$(name)@?" "value" : "Supprimer %1$(secretName)@?"
} }
}, },
"he" : { "he" : {
"stringUnit" : { "stringUnit" : {
"state" : "new", "state" : "new",
"value" : "Delete %1$(name)@?" "value" : "Delete %1$(secretName)@?"
} }
}, },
"hu" : { "hu" : {
"stringUnit" : { "stringUnit" : {
"state" : "new", "state" : "new",
"value" : "Delete %1$(name)@?" "value" : "Delete %1$(secretName)@?"
} }
}, },
"it" : { "it" : {
"stringUnit" : { "stringUnit" : {
"state" : "translated", "state" : "translated",
"value" : "Eliminare %1$(name)@?" "value" : "Eliminare %1$(secretName)@?"
} }
}, },
"ja" : { "ja" : {
"stringUnit" : { "stringUnit" : {
"state" : "translated", "state" : "translated",
"value" : "%1$(name)@を削除しますか?" "value" : "%1$(secretName)@を削除しますか?"
} }
}, },
"ko" : { "ko" : {
"stringUnit" : { "stringUnit" : {
"state" : "translated", "state" : "translated",
"value" : "%1$(name)@를 지우겠습니까?" "value" : "%1$(secretName)@를 지우겠습니까?"
} }
}, },
"nb" : { "nb" : {
"stringUnit" : { "stringUnit" : {
"state" : "new", "state" : "new",
"value" : "Delete %1$(name)@?" "value" : "Delete %1$(secretName)@?"
} }
}, },
"nl" : { "nl" : {
"stringUnit" : { "stringUnit" : {
"state" : "new", "state" : "new",
"value" : "Delete %1$(name)@?" "value" : "Delete %1$(secretName)@?"
} }
}, },
"pl" : { "pl" : {
"stringUnit" : { "stringUnit" : {
"state" : "translated", "state" : "translated",
"value" : "Usunąć %1$(name)@?" "value" : "Usunąć %1$(secretName)@?"
} }
}, },
"pt" : { "pt" : {
"stringUnit" : { "stringUnit" : {
"state" : "new", "state" : "new",
"value" : "Delete %1$(name)@?" "value" : "Delete %1$(secretName)@?"
} }
}, },
"pt-BR" : { "pt-BR" : {
"stringUnit" : { "stringUnit" : {
"state" : "translated", "state" : "translated",
"value" : "Deletar %1$(name)@?" "value" : "Deletar %1$(secretName)@?"
} }
}, },
"ro" : { "ro" : {
"stringUnit" : { "stringUnit" : {
"state" : "new", "state" : "new",
"value" : "Delete %1$(name)@?" "value" : "Delete %1$(secretName)@?"
} }
}, },
"ru" : { "ru" : {
"stringUnit" : { "stringUnit" : {
"state" : "translated", "state" : "translated",
"value" : "Удалить %1$(name)@?" "value" : "Удалить %1$(secretName)@?"
} }
}, },
"sr" : { "sr" : {
"stringUnit" : { "stringUnit" : {
"state" : "new", "state" : "new",
"value" : "Delete %1$(name)@?" "value" : "Delete %1$(secretName)@?"
} }
}, },
"sv" : { "sv" : {
"stringUnit" : { "stringUnit" : {
"state" : "new", "state" : "new",
"value" : "Delete %1$(name)@?" "value" : "Delete %1$(secretName)@?"
} }
}, },
"tr" : { "tr" : {
"stringUnit" : { "stringUnit" : {
"state" : "new", "state" : "new",
"value" : "Delete %1$(name)@?" "value" : "Delete %1$(secretName)@?"
} }
}, },
"uk" : { "uk" : {
"stringUnit" : { "stringUnit" : {
"state" : "new", "state" : "new",
"value" : "Delete %1$(name)@?" "value" : "Delete %1$(secretName)@?"
} }
}, },
"vi" : { "vi" : {
"stringUnit" : { "stringUnit" : {
"state" : "new", "state" : "new",
"value" : "Delete %1$(name)@?" "value" : "Delete %1$(secretName)@?"
} }
}, },
"zh-Hans" : { "zh-Hans" : {
"stringUnit" : { "stringUnit" : {
"state" : "translated", "state" : "translated",
"value" : "删除“%1$(name)@”吗?" "value" : "删除“%1$(secretName)@”吗?"
} }
}, },
"zh-Hant" : { "zh-Hant" : {
"stringUnit" : { "stringUnit" : {
"state" : "translated", "state" : "translated",
"value" : "刪除「%1$(name)@」嗎?" "value" : "刪除「%1$(secretName)@」嗎?"
} }
} }
} }
@@ -19771,28 +19637,6 @@
} }
} }
}, },
"rename_certificate_label" : {
"extractionState" : "manual",
"localizations" : {
"en" : {
"stringUnit" : {
"state" : "translated",
"value" : "Name"
}
}
}
},
"rename_certificate_name_placeholder" : {
"extractionState" : "manual",
"localizations" : {
"en" : {
"stringUnit" : {
"state" : "translated",
"value" : "Certificate Name"
}
}
}
},
"reveal_in_finder_button" : { "reveal_in_finder_button" : {
"extractionState" : "manual", "extractionState" : "manual",
"localizations" : { "localizations" : {
@@ -19978,23 +19822,6 @@
} }
} }
}, },
"Review" : {
},
"Review All" : {
},
"secret_detail_certificate_path_label" : {
"extractionState" : "manual",
"localizations" : {
"en" : {
"stringUnit" : {
"state" : "translated",
"value" : "Matching Certificates"
}
}
}
},
"secret_detail_md5_fingerprint_label" : { "secret_detail_md5_fingerprint_label" : {
"extractionState" : "manual", "extractionState" : "manual",
"localizations" : { "localizations" : {

View File

@@ -35,15 +35,13 @@ import XPCWrappers
self.osVersion = osVersion self.osVersion = osVersion
self.currentVersion = currentVersion self.currentVersion = currentVersion
Task { Task {
do { if checkOnLaunch {
if checkOnLaunch { try await checkForUpdates()
try await checkForUpdates() }
} while !Task.isCancelled {
while !Task.isCancelled { try? await Task.sleep(for: .seconds(Int(checkFrequency)))
try? await Task.sleep(for: .seconds(Int(checkFrequency))) try await checkForUpdates()
try await checkForUpdates() }
}
} catch {}
} }
} }

View File

@@ -1,24 +0,0 @@
import Foundation
import CryptoKit
import Formatters
@dynamicMemberLookup
public struct Certificate: Sendable, Codable, Equatable, Hashable, Identifiable, CustomDebugStringConvertible {
public var openSSHCertificate: OpenSSHCertificate
public let rawData: Data
public init(openSSHCertificate: OpenSSHCertificate, rawData: Data) {
self.openSSHCertificate = openSSHCertificate
self.rawData = rawData
}
public var id: String { Insecure.MD5.hash(data: rawData).formatted(.hex(separator: "")) }
public var debugDescription: String { openSSHCertificate.debugDescription }
public subscript<T>(dynamicMember keyPath: KeyPath<OpenSSHCertificate, T>) -> T {
openSSHCertificate[keyPath: keyPath]
}
}

View File

@@ -1,153 +0,0 @@
import Foundation
import Observation
import Security
import os
import SecretKit
@Observable @MainActor public final class CertificateStore: Sendable {
public private(set) var certificates: [Certificate] = []
/// Initializes a Store.
public init() {
loadCertificates()
Task {
for await note in DistributedNotificationCenter.default().notifications(named: .certificateStoreUpdated) {
guard Constants.notificationToken != (note.object as? String) else {
// Don't reload if we're the ones triggering this by reloading.
continue
}
loadCertificates()
}
}
}
public func reloadCertificates() {
let before = certificates
certificates.removeAll()
loadCertificates()
if certificates != before {
NotificationCenter.default.post(name: .certificateStoreReloaded, object: self)
DistributedNotificationCenter.default().postNotificationName(.certificateStoreUpdated, object: Constants.notificationToken, deliverImmediately: true)
}
}
public func save(certificate: Certificate) throws {
let attributes = try JSONEncoder().encode(certificate.openSSHCertificate)
let keychainAttributes = KeychainDictionary([
kSecClass: Constants.keyClass,
kSecAttrService: Constants.keyTag,
kSecAttrAccount: certificate.id,
kSecUseDataProtectionKeychain: true,
kSecAttrAccessible: kSecAttrAccessibleWhenUnlockedThisDeviceOnly,
kSecValueData: certificate.rawData,
kSecAttrGeneric: attributes
])
let status = SecItemAdd(keychainAttributes, nil)
if status != errSecSuccess && status != errSecDuplicateItem {
throw KeychainError(statusCode: status)
}
reloadCertificates()
}
public func delete(certificate: Certificate) throws {
let deleteAttributes = KeychainDictionary([
kSecClass: Constants.keyClass,
kSecAttrService: Constants.keyTag,
kSecUseDataProtectionKeychain: true,
kSecAttrAccount: certificate.id,
])
let status = SecItemDelete(deleteAttributes)
if status != errSecSuccess {
throw KeychainError(statusCode: status)
}
reloadCertificates()
}
public func update(certificate: Certificate) throws {
let updateQuery = KeychainDictionary([
kSecClass: Constants.keyClass,
kSecAttrAccount: certificate.id,
])
let cert = try JSONEncoder().encode(certificate.openSSHCertificate)
let updatedAttributes = KeychainDictionary([
kSecAttrGeneric: cert,
])
let status = SecItemUpdate(updateQuery, updatedAttributes)
if status != errSecSuccess {
throw KeychainError(statusCode: status)
}
reloadCertificates()
}
public func certificates(for secret: any Secret) -> [Certificate] {
certificates.filter { $0.openSSHCertificate.publicKey.data == secret.publicKey }
}
}
extension CertificateStore {
/// Loads all certificates from the store.
private func loadCertificates() {
let queryAttributes = KeychainDictionary([
kSecClass: Constants.keyClass,
kSecAttrService: Constants.keyTag,
kSecUseDataProtectionKeychain: true,
kSecReturnData: true,
kSecMatchLimit: kSecMatchLimitAll,
kSecReturnAttributes: true
])
var untyped: CFTypeRef?
unsafe SecItemCopyMatching(queryAttributes, &untyped)
guard let typed = untyped as? [[CFString: Any]] else { return }
let decoder = JSONDecoder()
let wrapped: [Certificate] = typed.compactMap {
do {
guard let data = $0[kSecValueData] as? Data,
let attributesData = $0[kSecAttrGeneric] as? Data else {
throw MissingAttributesError()
}
return Certificate(openSSHCertificate: try decoder.decode(OpenSSHCertificate.self, from: attributesData), rawData: data)
} catch {
return nil
}
}
.filter {
if let validityRange = $0.validityRange {
validityRange.contains(Date())
} else {
true
}
}
certificates.append(contentsOf: wrapped)
}
}
extension CertificateStore {
enum Constants {
static let keyClass = kSecClassGenericPassword as String
static let keyTag = Data("com.maxgoedjen.certificatestore.opensshcertificate".utf8)
static let notificationToken = UUID().uuidString
}
struct UnsupportedAlgorithmError: Error {}
struct MissingAttributesError: Error {}
}
extension NSNotification.Name {
// Distributed notification that keys were modified out of process (ie, that the management tool added/removed certificates)
public static let certificateStoreUpdated = NSNotification.Name("com.maxgoedjen.Secretive.certificateStore.updated")
// Internal notification that certificates were reloaded from the backing store.
public static let certificateStoreReloaded = NSNotification.Name("com.maxgoedjen.Secretive.certificateStore.reloaded")
}

View File

@@ -1,82 +0,0 @@
import Foundation
import Formatters
public struct OpenSSHCertificate: Sendable, Codable, Equatable, Hashable, CustomDebugStringConvertible {
public var type: CertificateType
public var name: String
public var data: Data
public var publicKey: PublicKey
public var principals: [String]
public var keyID: String
public var serial: UInt64
public var validityRange: Range<Date>?
public var criticalOptions: [String]
public var extensions: [String]
public var signingKey: PublicKey
public init(
type: OpenSSHCertificate.CertificateType,
name: String,
data: Data,
publicKey: PublicKey,
principals: [String],
keyID: String,
serial: UInt64,
validityRange: Range<Date>? = nil,
criticalOptions: [String],
extensions: [String],
signingKey: PublicKey,
) {
self.type = type
self.name = name
self.data = data
self.publicKey = publicKey
self.principals = principals
self.keyID = keyID
self.serial = serial
self.validityRange = validityRange
self.criticalOptions = criticalOptions
self.extensions = extensions
self.signingKey = signingKey
}
public var debugDescription: String {
"OpenSSH Certificate \(name, default: "Unnamed"): \(data.formatted(.hex()))"
}
}
extension OpenSSHCertificate {
public enum CertificateType: String, Sendable, Codable {
case ecdsa256 = "ecdsa-sha2-nistp256-cert-v01@openssh.com"
case ecdsa384 = "ecdsa-sha2-nistp384-cert-v01@openssh.com"
case nistp521 = "ecdsa-sha2-nistp521-cert-v01@openssh.com"
public var keyIdentifier: String {
rawValue.replacingOccurrences(of: "-cert-v01@openssh.com", with: "")
}
}
}
extension OpenSSHCertificate {
public struct PublicKey: Hashable, Sendable, Codable {
public let keyType: String
public let curveName: String
public let data: Data
public init(keyType: String, curveName: String, data: Data) {
self.keyType = keyType
self.curveName = curveName
self.data = data
}
}
}

View File

@@ -1,6 +1,5 @@
import Foundation import Foundation
import SSHProtocolKit import SSHProtocolKit
import CertificateKit
import SecretKit import SecretKit
extension URL { extension URL {
@@ -21,10 +20,6 @@ extension URL {
agentHomeURL.appending(component: "PublicKeys") agentHomeURL.appending(component: "PublicKeys")
} }
public static var certificatesDirectory: URL {
agentHomeURL.appending(component: "Certificates")
}
/// The path for a Secret's public key. /// The path for a Secret's public key.
/// - Parameter secret: The Secret to return the path for. /// - Parameter secret: The Secret to return the path for.
/// - Returns: The path to the Secret's public key. /// - Returns: The path to the Secret's public key.
@@ -35,14 +30,6 @@ extension URL {
return directory.appending(component: "\(minimalHex).pub").path() return directory.appending(component: "\(minimalHex).pub").path()
} }
/// The path for a certificate.
/// - Parameter certificate: The Certificate to return the path for.
/// - Returns: The path to the Certificate.
/// - Warning: This method returning a path does not imply that a certificate has been written to disk already. This method only describes where it will be written to.
public static func certificatePath(for certificateID: String, in directory: URL) -> String {
return directory.appending(component: "\(certificateID)-cert.pub").path()
}
} }
extension String { extension String {

View File

@@ -1,74 +0,0 @@
import Foundation
import CryptoKit
public struct HexDataStyle<SequenceType: Sequence>: 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<Data> {
public static func hex(separator: String = "") -> HexDataStyle<Data> {
HexDataStyle(separator: separator)
}
}
extension FormatStyle where Self == HexDataStyle<Insecure.MD5Digest> {
public static func hex(separator: String = ":") -> HexDataStyle<Insecure.MD5Digest> {
HexDataStyle(separator: separator)
}
}
public struct Base64DataStyle<SequenceType: Sequence>: Hashable, Codable {
private let stripPadding: Bool
public init(stripPadding: Bool) {
self.stripPadding = stripPadding
}
}
extension Base64DataStyle: FormatStyle where SequenceType.Element == UInt8 {
public func format(_ value: SequenceType) -> String {
let base64 = Data(value).base64EncodedString()
let paddingRange = base64.index(base64.endIndex, offsetBy: -2)..<base64.endIndex
return base64.replacingOccurrences(of: "=", with: "", range: paddingRange)
}
}
extension FormatStyle where Self == Base64DataStyle<Data> {
public static func base64(stripPadding: Bool) -> Base64DataStyle<Data> {
Base64DataStyle(stripPadding: stripPadding)
}
}
extension FormatStyle where Self == Base64DataStyle<SHA256.Digest> {
public static func base64(stripPadding: Bool) -> Base64DataStyle<SHA256.Digest> {
Base64DataStyle(stripPadding: stripPadding)
}
}

View File

@@ -0,0 +1,37 @@
import Foundation
import CryptoKit
public struct HexDataStyle<SequenceType: Sequence>: 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<Data> {
public static func hex(separator: String = "") -> HexDataStyle<Data> {
HexDataStyle(separator: separator)
}
}
extension FormatStyle where Self == HexDataStyle<Insecure.MD5Digest> {
public static func hex(separator: String = ":") -> HexDataStyle<Insecure.MD5Digest> {
HexDataStyle(separator: separator)
}
}

View File

@@ -1,30 +0,0 @@
import Foundation
import CryptoKit
import CertificateKit
import Formatters
/// Generates OpenSSH representations of Certificates.
public struct OpenSSHCertificateWriter: Sendable {
/// Initializes the writer.
public init() {
}
/// Generates an OpenSSH data payload identifying the certificate.
/// - Returns: OpenSSH data payload identifying the certificate.
public func data(publicKey: OpenSSHCertificate.PublicKey) -> Data {
// https://datatracker.ietf.org/doc/html/rfc5656#section-3.1
publicKey.keyType.lengthAndData +
publicKey.curveName.lengthAndData +
publicKey.data.lengthAndData
}
/// Generates an OpenSSH SHA256 fingerprint string.
/// - Returns: OpenSSH SHA256 fingerprint string.
public func openSSHSHA256KeyFingerprint(publicKey: OpenSSHCertificate.PublicKey) -> String {
// OpenSSL format seems to strip the padding at the end.
let cleaned = SHA256.hash(data: data(publicKey: publicKey)).formatted(.base64(stripPadding: true))
return "SHA256:\(cleaned)"
}
}

View File

@@ -1,95 +0,0 @@
import Foundation
import CertificateKit
public protocol OpenSSHCertificateParserProtocol {
func parse(data: Data) async throws -> OpenSSHCertificate
}
public struct OpenSSHCertificateParser: OpenSSHCertificateParserProtocol, Sendable {
public init() {
assert(Bundle.main.bundleURL.pathExtension == "xpc" || ProcessInfo.processInfo.processName == "xctest", "Potentially unsafe parsing code should run in an XPC service")
}
public func parse(data: Data) throws(OpenSSHCertificateError) -> OpenSSHCertificate {
let string = String(decoding: data, as: UTF8.self)
var elements = string
.trimmingCharacters(in: .whitespacesAndNewlines)
.components(separatedBy: " ")
guard elements.count >= 2 else {
throw OpenSSHCertificateError.parsingFailed
}
let typeString = elements.removeFirst()
guard let type = OpenSSHCertificate.CertificateType(rawValue: typeString) else { throw .unsupportedType }
let encodedKey = elements.removeFirst()
guard let decoded = Data(base64Encoded: encodedKey) else {
throw OpenSSHCertificateError.parsingFailed
}
let comment = elements.first
do {
let dataParser = OpenSSHReader(data: decoded)
let publicKeyType = try dataParser.readNextChunkAsString() // Theoretically the same as typeString, but
.replacingOccurrences(of: "-cert-v01@openssh.com", with: "")
_ = try dataParser.readNextChunk() // Nonce
let publicKeyCurveName = try dataParser.readNextChunkAsString()
let publicKeyData = try dataParser.readNextChunk()
let publicKey = OpenSSHCertificate.PublicKey(keyType: publicKeyType, curveName: publicKeyCurveName, data: publicKeyData)
let serialNumber = try dataParser.readNextBytes(as: UInt64.self, convertEndianness: true)
let role = try dataParser.readNextBytes(as: UInt32.self, convertEndianness: true)
_ = role
let keyIdentifier = try dataParser.readNextChunkAsString()
let principalsReader = try dataParser.readNextChunkAsSubReader()
var principals: [String] = []
while !principalsReader.done {
try principals.append(principalsReader.readNextChunkAsString())
}
let validAfter = try dataParser.readNextBytes(as: UInt64.self, convertEndianness: true)
let validBefore = try dataParser.readNextBytes(as: UInt64.self, convertEndianness: true)
let validityRange = Date(timeIntervalSince1970: TimeInterval(validAfter))..<Date(timeIntervalSince1970: TimeInterval(validBefore))
let criticalOptionsReader = try dataParser.readNextChunkAsSubReader()
var criticalOptions: [String] = []
while !criticalOptionsReader.done {
let next = try criticalOptionsReader.readNextChunkAsString()
if !next.isEmpty {
criticalOptions.append(next)
}
}
let extensionsReader = try dataParser.readNextChunkAsSubReader()
var extensions: [String] = []
while !extensionsReader.done {
let next = try extensionsReader.readNextChunkAsString()
if !next.isEmpty {
extensions.append(next)
}
}
_ = try dataParser.readNextChunk() // reserved
let signingKeyReader = try dataParser.readNextChunkAsSubReader()
let signingKeyType = try signingKeyReader.readNextChunkAsString()
let signingKeyCurveName = try signingKeyReader.readNextChunkAsString()
let signingKeyData = try signingKeyReader.readNextChunk()
let signingKey = OpenSSHCertificate.PublicKey(keyType: signingKeyType, curveName: signingKeyCurveName, data: signingKeyData)
return OpenSSHCertificate(
type: type,
name: comment ?? keyIdentifier,
data: decoded,
publicKey: publicKey,
principals: principals,
keyID: keyIdentifier,
serial: serialNumber,
validityRange: validityRange,
criticalOptions: criticalOptions,
extensions: extensions,
signingKey: signingKey,
)
} catch {
throw .parsingFailed
}
}
}
public enum OpenSSHCertificateError: Error, Codable {
case unsupportedType
case parsingFailed
}

View File

@@ -19,7 +19,7 @@ public struct OpenSSHPublicKeyWriter: Sendable {
("nistp" + String(describing: secret.keyType.size)).lengthAndData + ("nistp" + String(describing: secret.keyType.size)).lengthAndData +
secret.publicKey.lengthAndData secret.publicKey.lengthAndData
case .mldsa: case .mldsa:
// https://www.ietf.org/archive/id/draft-sfluhrer-ssh-mldsa-04.txt // https://datatracker.ietf.org/doc/html/draft-sfluhrer-ssh-mldsa-05
openSSHIdentifier(for: secret.keyType).lengthAndData + openSSHIdentifier(for: secret.keyType).lengthAndData +
secret.publicKey.lengthAndData secret.publicKey.lengthAndData
case .rsa: case .rsa:
@@ -41,7 +41,9 @@ public struct OpenSSHPublicKeyWriter: Sendable {
/// - Returns: OpenSSH SHA256 fingerprint string. /// - Returns: OpenSSH SHA256 fingerprint string.
public func openSSHSHA256Fingerprint<SecretType: Secret>(secret: SecretType) -> String { public func openSSHSHA256Fingerprint<SecretType: Secret>(secret: SecretType) -> String {
// OpenSSL format seems to strip the padding at the end. // OpenSSL format seems to strip the padding at the end.
let cleaned = SHA256.hash(data: data(secret: secret)).formatted(.base64(stripPadding: true)) let base64 = Data(SHA256.hash(data: data(secret: secret))).base64EncodedString()
let paddingRange = base64.index(base64.endIndex, offsetBy: -2)..<base64.endIndex
let cleaned = base64.replacingOccurrences(of: "=", with: "", range: paddingRange)
return "SHA256:\(cleaned)" return "SHA256:\(cleaned)"
} }

View File

@@ -10,9 +10,6 @@ public final class OpenSSHReader {
/// - Parameter data: The data to read. /// - Parameter data: The data to read.
public init(data: Data) { public init(data: Data) {
remaining = Data(data) remaining = Data(data)
if remaining.count == 0 {
done = true
}
} }
/// Reads the next chunk of data from the playload. /// Reads the next chunk of data from the playload.
@@ -42,6 +39,18 @@ public final class OpenSSHReader {
return convertEndianness ? T(value.bigEndian) : T(value) return convertEndianness ? T(value.bigEndian) : T(value)
} }
public func readNextByteAsBool() throws(OpenSSHReaderError) -> Bool {
let size = MemoryLayout<Bool>.size
guard remaining.count >= size else { throw .beyondBounds }
let lengthRange = 0..<size
let lengthChunk = remaining[lengthRange]
remaining.removeSubrange(lengthRange)
if remaining.isEmpty {
done = true
}
return unsafe lengthChunk.bytes.unsafeLoad(as: Bool.self)
}
public func readNextChunkAsString(convertEndianness: Bool = true) throws(OpenSSHReaderError) -> String { public func readNextChunkAsString(convertEndianness: Bool = true) throws(OpenSSHReaderError) -> String {
try String(decoding: readNextChunk(convertEndianness: convertEndianness), as: UTF8.self) try String(decoding: readNextChunk(convertEndianness: convertEndianness), as: UTF8.self)
} }
@@ -53,5 +62,6 @@ public final class OpenSSHReader {
} }
public enum OpenSSHReaderError: Error, Codable { public enum OpenSSHReaderError: Error, Codable {
case incorrectFormat
case beyondBounds case beyondBounds
} }

View File

@@ -17,7 +17,7 @@ public struct OpenSSHSignatureWriter: Sendable {
// https://datatracker.ietf.org/doc/html/rfc5656#section-3.1 // https://datatracker.ietf.org/doc/html/rfc5656#section-3.1
ecdsaSignature(signature, keyType: secret.keyType) ecdsaSignature(signature, keyType: secret.keyType)
case .mldsa: case .mldsa:
// https://datatracker.ietf.org/doc/html/draft-sfluhrer-ssh-mldsa-00#name-public-key-algorithms // https://datatracker.ietf.org/doc/html/draft-sfluhrer-ssh-mldsa-05
mldsaSignature(signature, keyType: secret.keyType) mldsaSignature(signature, keyType: secret.keyType)
case .rsa: case .rsa:
// https://datatracker.ietf.org/doc/html/rfc4253#section-6.6 // https://datatracker.ietf.org/doc/html/rfc4253#section-6.6

View File

@@ -1,105 +0,0 @@
import Foundation
import OSLog
import SecretKit
import CertificateKit
public protocol SSHAgentInputParserProtocol {
func parse(data: Data) async throws -> SSHAgent.Request
}
public struct SSHAgentInputParser: SSHAgentInputParserProtocol {
private let logger = Logger(subsystem: "com.maxgoedjen.secretive.secretagent", category: "InputParser")
public init() {
assert(Bundle.main.bundleURL.pathExtension == "xpc" || ProcessInfo.processInfo.processName == "xctest", "Potentially unsafe parsing code should run in an XPC service")
}
public func parse(data: Data) throws(AgentParsingError) -> SSHAgent.Request {
logger.debug("Parsing new data")
guard data.count > 4 else {
throw .invalidData
}
let specifiedLength = unsafe (data[0..<4].bytes.unsafeLoad(as: UInt32.self).bigEndian) + 4
let rawRequestInt = data[4]
let remainingDataRange = 5..<min(Int(specifiedLength), data.count)
lazy var body: Data = { Data(data[remainingDataRange]) }()
switch rawRequestInt {
case SSHAgent.Request.requestIdentities.protocolID:
return .requestIdentities
case SSHAgent.Request.signRequest(.empty).protocolID:
do {
return .signRequest(try signatureRequestContext(from: body))
} catch {
throw .openSSHReader(error)
}
case SSHAgent.Request.addIdentity.protocolID:
return .addIdentity
case SSHAgent.Request.removeIdentity.protocolID:
return .removeIdentity
case SSHAgent.Request.removeAllIdentities.protocolID:
return .removeAllIdentities
case SSHAgent.Request.addIDConstrained.protocolID:
return .addIDConstrained
case SSHAgent.Request.addSmartcardKey.protocolID:
return .addSmartcardKey
case SSHAgent.Request.removeSmartcardKey.protocolID:
return .removeSmartcardKey
case SSHAgent.Request.lock.protocolID:
return .lock
case SSHAgent.Request.unlock.protocolID:
return .unlock
case SSHAgent.Request.addSmartcardKeyConstrained.protocolID:
return .addSmartcardKeyConstrained
case SSHAgent.Request.protocolExtension.protocolID:
return .protocolExtension
default:
return .unknown(rawRequestInt)
}
}
}
extension SSHAgentInputParser {
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
let dataToSign = try reader.readNextChunk()
return SSHAgent.Request.SignatureRequestContext(keyBlob: keyBlob, dataToSign: dataToSign)
}
func certificatePublicKeyBlob(from hash: Data) -> Data? {
let reader = OpenSSHReader(data: hash)
do {
let certType = try reader.readNextChunkAsString()
guard let certType = OpenSSHCertificate.CertificateType(rawValue: certType) else { return nil }
_ = try reader.readNextChunk() // nonce
let curveIdentifier = try reader.readNextChunk()
let publicKey = try reader.readNextChunk()
let openSSHIdentifier = certType.keyIdentifier
return openSSHIdentifier.lengthAndData +
curveIdentifier.lengthAndData +
publicKey.lengthAndData
} catch {
return nil
}
}
}
extension SSHAgentInputParser {
public enum AgentParsingError: Error, Codable {
case unknownRequest
case unhandledRequest
case invalidData
case openSSHReader(OpenSSHReaderError)
}
}

View File

@@ -19,7 +19,7 @@ extension SSHAgent {
case lock case lock
case unlock case unlock
case addSmartcardKeyConstrained case addSmartcardKeyConstrained
case protocolExtension case protocolExtension(ProtocolExtension)
case unknown(UInt8) case unknown(UInt8)
public var protocolID: UInt8 { public var protocolID: UInt8 {
@@ -60,18 +60,82 @@ extension SSHAgent {
public struct SignatureRequestContext: Sendable, Codable { public struct SignatureRequestContext: Sendable, Codable {
public let keyBlob: Data public let keyBlob: Data
public let dataToSign: Data public let dataToSign: SignaturePayload
public init(keyBlob: Data, dataToSign: Data) { public init(keyBlob: Data, dataToSign: SignaturePayload) {
self.keyBlob = keyBlob self.keyBlob = keyBlob
self.dataToSign = dataToSign self.dataToSign = dataToSign
} }
public static var empty: SignatureRequestContext { public static var empty: SignatureRequestContext {
SignatureRequestContext(keyBlob: Data(), dataToSign: Data()) SignatureRequestContext(keyBlob: Data(), dataToSign: SignaturePayload(raw: Data(), decoded: nil))
}
public struct SignaturePayload: Sendable, Codable {
public let raw: Data
public let decoded: DecodedPayload?
public init(
raw: Data,
decoded: DecodedPayload?
) {
self.raw = raw
self.decoded = decoded
}
public enum DecodedPayload: Sendable, Codable {
case sshConnection(SSHConnectionPayload)
case sshSig(SSHSigPayload)
public struct SSHConnectionPayload: Sendable, Codable {
public let username: String
public let hasSignature: Bool
public let publicKeyAlgorithm: String
public let publicKey: Data
public let hostKey: Data
public init(
username: String,
hasSignature: Bool,
publicKeyAlgorithm: String,
publicKey: Data,
hostKey: Data
) {
self.username = username
self.hasSignature = hasSignature
self.publicKeyAlgorithm = publicKeyAlgorithm
self.publicKey = publicKey
self.hostKey = hostKey
}
}
public struct SSHSigPayload: Sendable, Codable {
public let namespace: String
public let hashAlgorithm: String
public let hash: Data
public init(
namespace: String,
hashAlgorithm: String,
hash: Data,
) {
self.namespace = namespace
self.hashAlgorithm = hashAlgorithm
self.hash = hash
}
}
}
} }
} }
} }
/// The type of the SSH Agent Response, as described in https://datatracker.ietf.org/doc/html/draft-miller-ssh-agent#section-5.1 /// The type of the SSH Agent Response, as described in https://datatracker.ietf.org/doc/html/draft-miller-ssh-agent#section-5.1
@@ -88,8 +152,8 @@ extension SSHAgent {
switch self { switch self {
case .agentFailure: "SSH_AGENT_FAILURE" case .agentFailure: "SSH_AGENT_FAILURE"
case .agentSuccess: "SSH_AGENT_SUCCESS" case .agentSuccess: "SSH_AGENT_SUCCESS"
case .agentIdentitiesAnswer: "SSH_AGENT_IDENTITIES_ANSWER" case .agentIdentitiesAnswer: "SSH2_AGENT_IDENTITIES_ANSWER"
case .agentSignResponse: "SSH_AGENT_SIGN_RESPONSE" case .agentSignResponse: "SSH2_AGENT_SIGN_RESPONSE"
case .agentExtensionFailure: "SSH_AGENT_EXTENSION_FAILURE" case .agentExtensionFailure: "SSH_AGENT_EXTENSION_FAILURE"
case .agentExtensionResponse: "SSH_AGENT_EXTENSION_RESPONSE" case .agentExtensionResponse: "SSH_AGENT_EXTENSION_RESPONSE"
} }

View File

@@ -0,0 +1,76 @@
import Foundation
// Extensions, as defined in https://github.com/openssh/openssh-portable/blob/master/PROTOCOL.agent
extension SSHAgent {
public enum ProtocolExtension: CustomDebugStringConvertible, Codable, Sendable {
case openSSH(OpenSSHExtension)
case unknown(String)
public var debugDescription: String {
switch self {
case let .openSSH(protocolExtension):
protocolExtension.debugDescription
case .unknown(let string):
"Unknown (\(string))"
}
}
public static var empty: ProtocolExtension {
.unknown("empty")
}
private struct ProtocolExtensionParsingError: Error {}
}
}
extension SSHAgent.ProtocolExtension {
public enum OpenSSHExtension: CustomDebugStringConvertible, Codable, Sendable {
case sessionBind(SessionBindContext)
case unknown(String)
public static var domain: String {
"openssh.com"
}
public var name: String {
switch self {
case .sessionBind:
"session-bind"
case .unknown(let name):
name
}
}
public var debugDescription: String {
"\(name)@\(OpenSSHExtension.domain)"
}
}
}
extension SSHAgent.ProtocolExtension.OpenSSHExtension {
public struct SessionBindContext: Codable, Sendable {
public let hostKey: Data
public let sessionID: Data
public let signature: Data
public let forwarding: Bool
public init(hostKey: Data, sessionID: Data, signature: Data, forwarding: Bool) {
self.hostKey = hostKey
self.sessionID = sessionID
self.signature = signature
self.forwarding = forwarding
}
public static let empty = SessionBindContext(hostKey: Data(), sessionID: Data(), signature: Data(), forwarding: false)
}
}

View File

@@ -2,7 +2,6 @@ import Foundation
import CryptoKit import CryptoKit
import OSLog import OSLog
import SecretKit import SecretKit
import CertificateKit
import AppKit import AppKit
import SSHProtocolKit import SSHProtocolKit
@@ -10,28 +9,25 @@ import SSHProtocolKit
public final class Agent: Sendable { public final class Agent: Sendable {
private let storeList: SecretStoreList private let storeList: SecretStoreList
private let authenticationHandler: AuthenticationHandler
private let certificateStore: CertificateStore
private let witness: SigningWitness? private let witness: SigningWitness?
private let publicKeyWriter = OpenSSHPublicKeyWriter() private let publicKeyWriter = OpenSSHPublicKeyWriter()
private let signatureWriter = OpenSSHSignatureWriter() private let signatureWriter = OpenSSHSignatureWriter()
private let certificateHandler = OpenSSHCertificateHandler()
private let logger = Logger(subsystem: "com.maxgoedjen.secretive.secretagent", category: "Agent") private let logger = Logger(subsystem: "com.maxgoedjen.secretive.secretagent", category: "Agent")
@MainActor private var sessionID: SSHAgent.ProtocolExtension.OpenSSHExtension.SessionBindContext?
/// Initializes an agent with a store list and a witness. /// Initializes an agent with a store list and a witness.
/// - Parameters: /// - Parameters:
/// - storeList: The `SecretStoreList` to make available. /// - storeList: The `SecretStoreList` to make available.
/// - witness: A witness to notify of requests. /// - witness: A witness to notify of requests.
public init( public init(storeList: SecretStoreList, witness: SigningWitness? = nil) {
storeList: SecretStoreList,
certificateStore: CertificateStore,
authenticationHandler: AuthenticationHandler,
witness: SigningWitness? = nil
) {
logger.debug("Agent is running") logger.debug("Agent is running")
self.storeList = storeList self.storeList = storeList
self.certificateStore = certificateStore
self.authenticationHandler = authenticationHandler
self.witness = witness self.witness = witness
Task { @MainActor in
await certificateHandler.reloadCertificates(for: storeList.allSecrets)
}
} }
} }
@@ -39,6 +35,7 @@ public final class Agent: Sendable {
extension Agent { extension Agent {
public func handle(request: SSHAgent.Request, provenance: SigningRequestProvenance) async -> Data { public func handle(request: SSHAgent.Request, provenance: SigningRequestProvenance) async -> Data {
logger.debug("Agent received request of type \(request.debugDescription)")
// Depending on the launch context (such as after macOS update), the agent may need to reload secrets before acting // Depending on the launch context (such as after macOS update), the agent may need to reload secrets before acting
await reloadSecretsIfNeccessary() await reloadSecretsIfNeccessary()
var response = Data() var response = Data()
@@ -49,9 +46,34 @@ extension Agent {
response.append(await identities()) response.append(await identities())
logger.debug("Agent returned \(SSHAgent.Response.agentIdentitiesAnswer.debugDescription)") logger.debug("Agent returned \(SSHAgent.Response.agentIdentitiesAnswer.debugDescription)")
case .signRequest(let context): case .signRequest(let context):
if let boundSession = await sessionID {
switch context.dataToSign.decoded {
case .sshConnection(let payload):
guard payload.hostKey == boundSession.hostKey else {
logger.error("Agent received bind request, but host key does not match signature reqeust host key.")
throw BindingFailure()
}
case .sshSig:
// SSHSIG does not have a host binding payload.
break
default:
break
}
}
response.append(SSHAgent.Response.agentSignResponse.data) response.append(SSHAgent.Response.agentSignResponse.data)
response.append(try await sign(data: context.dataToSign, keyBlob: context.keyBlob, provenance: provenance)) response.append(try await sign(data: context.dataToSign.raw, keyBlob: context.keyBlob, provenance: provenance))
logger.debug("Agent returned \(SSHAgent.Response.agentSignResponse.debugDescription)") logger.debug("Agent returned \(SSHAgent.Response.agentSignResponse.debugDescription)")
case .protocolExtension(.openSSH(.sessionBind(let bind))):
response = try await MainActor.run {
guard sessionID == nil else {
logger.error("Agent received bind request, but already bound.")
throw BindingFailure()
}
logger.debug("Agent bound")
sessionID = bind
return SSHAgent.Response.agentSuccess.data
}
logger.debug("Agent returned \(SSHAgent.Response.agentSuccess.debugDescription)")
case .unknown(let value): case .unknown(let value):
logger.error("Agent received unknown request of type \(value).") logger.error("Agent received unknown request of type \(value).")
throw UnhandledRequestError() throw UnhandledRequestError()
@@ -74,6 +96,7 @@ extension Agent {
/// - Returns: An OpenSSH formatted Data payload listing the identities available for signing operations. /// - Returns: An OpenSSH formatted Data payload listing the identities available for signing operations.
func identities() async -> Data { func identities() async -> Data {
let secrets = await storeList.allSecrets let secrets = await storeList.allSecrets
await certificateHandler.reloadCertificates(for: secrets)
var count = 0 var count = 0
var keyData = Data() var keyData = Data()
@@ -82,9 +105,10 @@ extension Agent {
keyData.append(keyBlob.lengthAndData) keyData.append(keyBlob.lengthAndData)
keyData.append(publicKeyWriter.comment(secret: secret).lengthAndData) keyData.append(publicKeyWriter.comment(secret: secret).lengthAndData)
count += 1 count += 1
for certificate in await certificateStore.certificates(for: secret) {
keyData.append(certificate.data.lengthAndData) if let (certificateData, name) = try? await certificateHandler.keyBlobAndName(for: secret) {
keyData.append(certificate.name.lengthAndData) keyData.append(certificateData.lengthAndData)
keyData.append(name.lengthAndData)
count += 1 count += 1
} }
} }
@@ -101,57 +125,20 @@ extension Agent {
/// - Returns: An OpenSSH formatted Data payload containing the signed data response. /// - Returns: An OpenSSH formatted Data payload containing the signed data response.
func sign(data: Data, keyBlob: Data, provenance: SigningRequestProvenance) async throws -> Data { func sign(data: Data, keyBlob: Data, provenance: SigningRequestProvenance) async throws -> Data {
guard let (secret, store) = await secret(matching: keyBlob) else { guard let (secret, store) = await secret(matching: keyBlob) else {
let keyBlobHex = keyBlob.formatted(.hex()) let keyBlobHex = keyBlob.compactMap { ("0" + String($0, radix: 16, uppercase: false)).suffix(2) }.joined()
logger.debug("Agent did not have a key matching \(keyBlobHex)") logger.debug("Agent did not have a key matching \(keyBlobHex)")
throw NoMatchingKeyError() throw NoMatchingKeyError()
} }
logger.debug("Agent offering witness chance to object") try await witness?.speakNowOrForeverHoldYourPeace(forAccessTo: secret, from: store, by: provenance)
do {
try await witness?.speakNowOrForeverHoldYourPeace(forAccessTo: secret, from: store, by: provenance)
} catch {
logger.debug("Witness objected")
throw error
}
logger.debug("Witness did not object")
if secret.authenticationRequirement.required { let rawRepresentation = try await store.sign(data: data, with: secret, for: provenance)
// Slow path, may block or suggest batching.
return try await signWithRequiredAuthentication(data: data, store: store, secret: secret, provenance: provenance)
} else {
// Fast path, no blocking/enqueing required
return try await signWithoutRequiredAuthentication(data: data, store: store, secret: secret, provenance: provenance)
}
}
func signWithoutRequiredAuthentication(data: Data, store: AnySecretStore, secret: AnySecret, provenance: SigningRequestProvenance) async throws -> Data {
let rawRepresentation = try await store.sign(data: data, with: secret, for: provenance, context: nil)
let signedData = signatureWriter.data(secret: secret, signature: rawRepresentation) let signedData = signatureWriter.data(secret: secret, signature: rawRepresentation)
try await witness?.witness(accessTo: secret, from: store, by: provenance, offerPersistence: false)
try await witness?.witness(accessTo: secret, from: store, by: provenance)
logger.debug("Agent signed request") logger.debug("Agent signed request")
return signedData
}
func signWithRequiredAuthentication(data: Data, store: AnySecretStore, secret: AnySecret, provenance: SigningRequestProvenance) async throws -> Data {
// let context: any AuthenticationContextProtocol
// let offerPersistence: Bool
// if let existing = await authenticationHandler.existingAuthenticationContextProtocol(for: SignatureRequest(secret: secret, provenance: provenance)) {
// context = existing
// offerPersistence = false
// logger.debug("Using existing auth context")
// } else {
// context = authenticationHandler.createAuthenticationContext(for: SignatureRequest(secret: secret, provenance: provenance))
// offerPersistence = secret.authenticationRequirement.required
// logger.debug("Creating fresh auth context")
// }
let context = try await authenticationHandler.waitForAuthentication(for: SignatureRequest(secret: secret, provenance: provenance))
let result = try await store.sign(data: data, with: secret, for: provenance, context: context.laContext)
let signedData = signatureWriter.data(secret: secret, signature: result)
try await witness?.witness(accessTo: secret, from: store, by: provenance, offerPersistence: false) // FIXME: THIS
logger.debug("Agent signed request")
return signedData return signedData
} }
@@ -186,6 +173,7 @@ extension Agent {
struct NoMatchingKeyError: Error {} struct NoMatchingKeyError: Error {}
struct UnhandledRequestError: Error {} struct UnhandledRequestError: Error {}
struct BindingFailure: Error {}
} }

View File

@@ -1,185 +0,0 @@
@unsafe @preconcurrency import LocalAuthentication
import SecretKit
import OSLog
/// A context describing a persisted authentication.
public final class AuthenticationContext: AuthenticationContextProtocol {
/// The Secret to persist authentication for.
public let secret: AnySecret
/// The LAContext used to authorize the persistent context.
public let laContext: LAContext
enum Validity {
/// - Note - Monotonic time instead of Date() to prevent people setting the clock back.
case time(monotonicExpiration: UInt64)
case requestIDs(Set<UUID>)
case exclusive(UUID)
}
let validity: Validity
/// Initializes a context.
/// - Parameters:
/// - secret: The Secret to persist authentication for.
/// - context: The LAContext used to authorize the persistent context.
/// - duration: The duration of the authorization context, in seconds.
init<SecretType: Secret>(secret: SecretType, context: LAContext, duration: TimeInterval) {
self.secret = AnySecret(secret)
self.laContext = context
let durationInNanoSeconds = Measurement(value: duration, unit: UnitDuration.seconds).converted(to: .nanoseconds).value
self.validity = .time(monotonicExpiration: clock_gettime_nsec_np(CLOCK_MONOTONIC) + UInt64(durationInNanoSeconds))
}
init<SecretType: Secret>(secret: SecretType, context: LAContext, requestIDs: Set<UUID>) {
self.secret = AnySecret(secret)
self.laContext = context
self.validity = .requestIDs(requestIDs)
}
init<SecretType: Secret>(secret: SecretType, context: LAContext, requestID: UUID) {
self.secret = AnySecret(secret)
self.laContext = context
self.validity = .exclusive(requestID)
}
/// A boolean describing whether or not the context is still valid.
public func valid(for request: SignatureRequest) -> Bool {
switch validity {
case .time(let monotonicExpiration):
clock_gettime_nsec_np(CLOCK_MONOTONIC) < monotonicExpiration
case .requestIDs(let set):
set.contains(request.id)
case .exclusive(let id):
id == request.id
}
}
}
public actor AuthenticationHandler {
private var persistedContexts: [AnySecret: AuthenticationContext] = [:]
private var holdingRequests: Set<SignatureRequest> = []
private var activeTask: Task<Void, any Error>?
private var lastBatchAuthPresentation: Set<SignatureRequest>?
private var presentBatchAuth: (([[SignatureRequest]], @escaping @Sendable (Set<SignatureRequest>) async throws -> Void) async throws -> Void)?
private let logger = Logger(subsystem: "com.maxgoedjen.secretive.secretagent", category: "Agent")
public init() {
}
public func setBatchAuthHandler(_ handler: @escaping (@Sendable ([[SignatureRequest]], @escaping @Sendable (Set<SignatureRequest>) async throws -> Void) async throws -> Void)) {
self.presentBatchAuth = handler
}
public func waitForAuthentication(for request: SignatureRequest) async throws -> any AuthenticationContextProtocol {
logger.log("Entering waitForAuthentication for \(request.id)")
if let existing = existingAuthenticationContext(for: request) {
logger.log("Short circuiting wait, existing valid context already exists.")
return existing
}
holdingRequests.insert(request)
logger.log("Waiting for authentication for \(request.id)")
defer {
logger.log("Removed hold for \(request.id)")
holdingRequests.remove(request)
}
while holdingRequests.count > 1 {
if hasBatchableRequests, holdingRequests != lastBatchAuthPresentation {
logger.log("Batchable requests exist, cancelling existing auth prompt")
activeTask?.cancel()
lastBatchAuthPresentation = holdingRequests
logger.log("Requesting batch auth presentation")
try await presentBatchAuth?(batchableRequests, persistAuthentication(for:))
logger.log("Requested batch auth presentation")
}
if let preauthorized = existingAuthenticationContext(for: request) {
logger.log("Batch auth context found, proceededing with preauthorized context")
return preauthorized
} else {
logger.log("Waiting for batch request handling")
}
try await Task.sleep(for: .milliseconds(100))
}
let laContext = LAContext()
laContext.localizedReason = String(localized: .authContextRequestSignatureDescription(appName: request.provenance.origin.displayName, secretName: request.secret.name))
laContext.localizedCancelTitle = String(localized: .authContextRequestDenyButton)
let context = AuthenticationContext(secret: request.secret, context: laContext, requestID: request.id)
activeTask = Task {
logger.log("Beginning individual auth prompt")
try await Task.sleep(for: .seconds(1000))
// _ = try? await laContext.evaluatePolicy(.deviceOwnerAuthentication, localizedReason: laContext.localizedReason)
logger.log("Ended individual auth prompt")
}
_ = try await activeTask?.value
// TODO: Check something beyond cancellation? id?
// Is this okay? Do we always assume that a cancelled task will be the proceeded on?
if activeTask?.isCancelled ?? false {
logger.log("Auth prompt was cancelled, waiting for explicit auth")
// If we explicitly cancelled the task, hang on until we auth it.
while true {
if let preauthorized = existingAuthenticationContext(for: request) {
logger.log("Explicit auth context found")
return preauthorized
}
try await Task.sleep(for: .milliseconds(100))
}
}
return context
}
private var batchableRequests: [[SignatureRequest]] {
holdingRequests.reduce(into: [:]) { partialResult, next in
partialResult[next.batchID, default: []].append(next)
}
.values
.map { $0.sorted() }
}
private var hasBatchableRequests: Bool {
guard presentBatchAuth != nil else { return false }
return batchableRequests.count < holdingRequests.count
}
private func existingAuthenticationContext(for request: SignatureRequest) -> (any AuthenticationContextProtocol)? {
guard let persisted = persistedContexts[request.secret], persisted.valid(for: request) else { return nil }
return persisted
}
public func persistAuthentication<SecretType: Secret>(secret: SecretType, forDuration duration: TimeInterval) async throws {
let newContext = LAContext()
newContext.touchIDAuthenticationAllowableReuseDuration = duration
newContext.localizedCancelTitle = String(localized: .authContextRequestDenyButton)
let formatter = DateComponentsFormatter()
formatter.unitsStyle = .spellOut
formatter.allowedUnits = [.hour, .minute, .day]
let durationString = formatter.string(from: duration)!
newContext.localizedReason = String(localized: .authContextPersistForDuration(secretName: secret.name, duration: durationString))
let success = try await newContext.evaluatePolicy(.deviceOwnerAuthentication, localizedReason: newContext.localizedReason)
guard success else { return }
let context = AuthenticationContext(secret: secret, context: newContext, duration: duration)
persistedContexts[AnySecret(secret)] = context
}
private func persistAuthentication(for requests: Set<SignatureRequest>) async throws {
activeTask?.cancel()
guard let first = requests.first else { return }
let newContext = LAContext()
newContext.localizedCancelTitle = String(localized: .authContextRequestDenyButton)
newContext.localizedReason = String("Multiple")
// newContext.localizedReason = String(localized: .authContextPersistForDuration(secretName: secret.name, duration: durationString))
let success = try await newContext.evaluatePolicy(.deviceOwnerAuthentication, localizedReason: newContext.localizedReason)
guard success else { return }
let context = AuthenticationContext(secret: first.secret, context: newContext, requestIDs: Set(requests.map(\.id)))
persistedContexts[AnySecret(first.secret)] = context
}
}

View File

@@ -0,0 +1,89 @@
import Foundation
import OSLog
import SecretKit
import SSHProtocolKit
/// Manages storage and lookup for OpenSSH certificates.
public actor OpenSSHCertificateHandler: Sendable {
private let publicKeyFileStoreController = PublicKeyFileStoreController(directory: URL.publicKeyDirectory)
private let logger = Logger(subsystem: "com.maxgoedjen.secretive.secretagent", category: "OpenSSHCertificateHandler")
private let writer = OpenSSHPublicKeyWriter()
private var keyBlobsAndNames: [AnySecret: (Data, Data)] = [:]
/// Initializes an OpenSSHCertificateHandler.
public init() {
}
/// Reloads any certificates in the PublicKeys folder.
/// - Parameter secrets: the secrets to look up corresponding certificates for.
public func reloadCertificates(for secrets: [AnySecret]) {
guard publicKeyFileStoreController.hasAnyCertificates else {
logger.log("No certificates, short circuiting")
return
}
keyBlobsAndNames = secrets.reduce(into: [:]) { partialResult, next in
partialResult[next] = try? loadKeyblobAndName(for: next)
}
}
/// Attempts to find an OpenSSH Certificate that corresponds to a ``Secret``
/// - Parameter secret: The secret to search for a certificate with
/// - Returns: A (``Data``, ``Data``) tuple containing the certificate and certificate name, respectively.
public func keyBlobAndName<SecretType: Secret>(for secret: SecretType) throws -> (Data, Data)? {
keyBlobsAndNames[AnySecret(secret)]
}
/// Attempts to find an OpenSSH Certificate that corresponds to a ``Secret``
/// - Parameter secret: The secret to search for a certificate with
/// - Returns: A (``Data``, ``Data``) tuple containing the certificate and certificate name, respectively.
private func loadKeyblobAndName<SecretType: Secret>(for secret: SecretType) throws -> (Data, Data)? {
let certificatePath = publicKeyFileStoreController.sshCertificatePath(for: secret)
guard FileManager.default.fileExists(atPath: certificatePath) else {
return nil
}
logger.debug("Found certificate for \(secret.name)")
let certContent = try String(contentsOfFile:certificatePath, encoding: .utf8)
let certElements = certContent.trimmingCharacters(in: .whitespacesAndNewlines).components(separatedBy: " ")
guard certElements.count >= 2 else {
logger.warning("Certificate found for \(secret.name) but failed to load")
throw OpenSSHCertificateError.parsingFailed
}
guard let certDecoded = Data(base64Encoded: certElements[1] as String) else {
logger.warning("Certificate found for \(secret.name) but failed to decode base64 key")
throw OpenSSHCertificateError.parsingFailed
}
if certElements.count >= 3 {
let certName = Data(certElements[2].utf8)
return (certDecoded, certName)
}
let certName = Data(secret.name.utf8)
logger.info("Certificate for \(secret.name) does not have a name tag, using secret name instead")
return (certDecoded, certName)
}
}
extension OpenSSHCertificateHandler {
enum OpenSSHCertificateError: LocalizedError {
case unsupportedType
case parsingFailed
case doesNotExist
public var errorDescription: String? {
switch self {
case .unsupportedType:
return "The key type was unsupported"
case .parsingFailed:
return "Failed to properly parse the SSH certificate"
case .doesNotExist:
return "Certificate does not exist"
}
}
}
}

View File

@@ -2,21 +2,18 @@ import Foundation
import OSLog import OSLog
import SecretKit import SecretKit
import SSHProtocolKit import SSHProtocolKit
import CertificateKit
import Common import Common
/// Controller responsible for writing public keys to disk, so that they're easily accessible by scripts. /// Controller responsible for writing public keys to disk, so that they're easily accessible by scripts.
public final class PublicKeyFileStoreController: Sendable { public final class PublicKeyFileStoreController: Sendable {
private let logger = Logger(subsystem: "com.maxgoedjen.secretive.secretagent", category: "PublicKeyFileStoreController") private let logger = Logger(subsystem: "com.maxgoedjen.secretive.secretagent", category: "PublicKeyFileStoreController")
private let publicKeysURL: URL private let directory: URL
private let certificatesURL: URL
private let keyWriter = OpenSSHPublicKeyWriter() private let keyWriter = OpenSSHPublicKeyWriter()
/// Initializes a PublicKeyFileStoreController. /// Initializes a PublicKeyFileStoreController.
public init(publicKeysURL: URL, certificatesURL: URL) { public init(directory: URL) {
self.publicKeysURL = publicKeysURL self.directory = directory
self.certificatesURL = certificatesURL
} }
/// Writes out the keys specified to disk. /// Writes out the keys specified to disk.
@@ -25,10 +22,10 @@ public final class PublicKeyFileStoreController: Sendable {
public func generatePublicKeys(for secrets: [AnySecret], clear: Bool = false) throws { public func generatePublicKeys(for secrets: [AnySecret], clear: Bool = false) throws {
logger.log("Writing public keys to disk") logger.log("Writing public keys to disk")
if clear { if clear {
let validPaths = Set(secrets.map { URL.publicKeyPath(for: $0, in: publicKeysURL) }) let validPaths = Set(secrets.map { URL.publicKeyPath(for: $0, in: directory) })
.union(Set(secrets.map { legacySSHCertificatePath(for: $0) })) .union(Set(secrets.map { sshCertificatePath(for: $0) }))
let contentsOfDirectory = (try? FileManager.default.contentsOfDirectory(atPath: publicKeysURL.path())) ?? [] let contentsOfDirectory = (try? FileManager.default.contentsOfDirectory(atPath: directory.path())) ?? []
let fullPathContents = contentsOfDirectory.map { publicKeysURL.appending(path: $0).path() } let fullPathContents = contentsOfDirectory.map { directory.appending(path: $0).path() }
let untracked = Set(fullPathContents) let untracked = Set(fullPathContents)
.subtracting(validPaths) .subtracting(validPaths)
@@ -37,47 +34,35 @@ public final class PublicKeyFileStoreController: Sendable {
try? FileManager.default.removeItem(at: URL(string: path)!) try? FileManager.default.removeItem(at: URL(string: path)!)
} }
} }
try? FileManager.default.createDirectory(at: publicKeysURL, withIntermediateDirectories: false, attributes: nil) try? FileManager.default.createDirectory(at: directory, withIntermediateDirectories: false, attributes: nil)
for secret in secrets { for secret in secrets {
let path = URL.publicKeyPath(for: secret, in: publicKeysURL) let path = URL.publicKeyPath(for: secret, in: directory)
let data = Data(keyWriter.openSSHString(secret: secret).utf8) let data = Data(keyWriter.openSSHString(secret: secret).utf8)
FileManager.default.createFile(atPath: path, contents: data, attributes: nil) FileManager.default.createFile(atPath: path, contents: data, attributes: nil)
} }
logger.log("Finished writing public keys") logger.log("Finished writing public keys")
} }
/// Writes out the certificates specified to disk.
/// - Parameter certificates: The Secrets to generate keys for.
/// - Parameter clear: Whether or not any untracked files in the directory should be removed.
public func generateCertificates(for certificates: [Certificate], clear: Bool = false) throws {
logger.log("Writing certificates to disk")
if clear {
let validPaths = Set(certificates.map { URL.certificatePath(for: $0.id, in: certificatesURL) })
let contentsOfDirectory = (try? FileManager.default.contentsOfDirectory(atPath: certificatesURL.path())) ?? []
let fullPathContents = contentsOfDirectory.map { certificatesURL.appending(path: $0).path() }
let untracked = Set(fullPathContents) /// Short-circuit check to ship enumerating a bunch of paths if there's nothing in the cert directory.
.subtracting(validPaths) public var hasAnyCertificates: Bool {
for path in untracked { do {
// string instead of fileURLWithPath since we're already using fileURL format. return try FileManager.default
try? FileManager.default.removeItem(at: URL(string: path)!) .contentsOfDirectory(atPath: directory.path())
} .filter { $0.hasSuffix("-cert.pub") }
.isEmpty == false
} catch {
return false
} }
try? FileManager.default.createDirectory(at: certificatesURL, withIntermediateDirectories: false, attributes: nil)
for certificate in certificates {
let path = URL.certificatePath(for: certificate.id, in: certificatesURL)
FileManager.default.createFile(atPath: path, contents: certificate.rawData, attributes: nil)
}
logger.log("Finished writing certificates")
} }
/// The path for a Secret's SSH Certificate public key. /// The path for a Secret's SSH Certificate public key.
/// - Parameter secret: The Secret to return the path for. /// - Parameter secret: The Secret to return the path for.
/// - Returns: The path to the SSH Certificate public key. /// - Returns: The path to the SSH Certificate public key.
/// - Warning: This method returning a path does not imply that a key has a SSH certificates. This method only describes where it will be. /// - Warning: This method returning a path does not imply that a key has a SSH certificates. This method only describes where it will be.
private func legacySSHCertificatePath<SecretType: Secret>(for secret: SecretType) -> String { public func sshCertificatePath<SecretType: Secret>(for secret: SecretType) -> String {
let minimalHex = keyWriter.openSSHMD5Fingerprint(secret: secret).replacingOccurrences(of: ":", with: "") let minimalHex = keyWriter.openSSHMD5Fingerprint(secret: secret).replacingOccurrences(of: ":", with: "")
return publicKeysURL.appending(component: "\(minimalHex).pub").path() return directory.appending(component: "\(minimalHex)-cert.pub").path()
} }
} }

View File

@@ -0,0 +1,253 @@
import Foundation
import OSLog
import SecretKit
import SSHProtocolKit
import CryptoKit
public protocol SSHAgentInputParserProtocol {
func parse(data: Data) async throws -> SSHAgent.Request
}
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 {
logger.debug("Parsing new data")
guard data.count > 4 else {
throw .invalidData
}
let specifiedLength = unsafe (data[0..<4].bytes.unsafeLoad(as: UInt32.self).bigEndian) + 4
let rawRequestInt = data[4]
let remainingDataRange = 5..<min(Int(specifiedLength), data.count)
lazy var body: Data = { Data(data[remainingDataRange]) }()
switch rawRequestInt {
case SSHAgent.Request.requestIdentities.protocolID:
return .requestIdentities
case SSHAgent.Request.signRequest(.empty).protocolID:
do {
return .signRequest(try signatureRequestContext(from: body))
} catch {
throw .openSSHReader(error)
}
case SSHAgent.Request.addIdentity.protocolID:
return .addIdentity
case SSHAgent.Request.removeIdentity.protocolID:
return .removeIdentity
case SSHAgent.Request.removeAllIdentities.protocolID:
return .removeAllIdentities
case SSHAgent.Request.addIDConstrained.protocolID:
return .addIDConstrained
case SSHAgent.Request.addSmartcardKey.protocolID:
return .addSmartcardKey
case SSHAgent.Request.removeSmartcardKey.protocolID:
return .removeSmartcardKey
case SSHAgent.Request.lock.protocolID:
return .lock
case SSHAgent.Request.unlock.protocolID:
return .unlock
case SSHAgent.Request.addSmartcardKeyConstrained.protocolID:
return .addSmartcardKeyConstrained
case SSHAgent.Request.protocolExtension(.empty).protocolID:
return .protocolExtension(try protocolExtension(from: body))
default:
return .unknown(rawRequestInt)
}
}
}
extension SSHAgentInputParser {
private enum Constants {
static let userAuthMagic: UInt8 = 50 // SSH2_MSG_USERAUTH_REQUEST
static let sshSigMagic = Data("SSHSIG".utf8)
}
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
let rawPayload = try reader.readNextChunk()
let payload: SSHAgent.Request.SignatureRequestContext.SignaturePayload
do {
if rawPayload.count > 6 && rawPayload[0..<6] == Constants.sshSigMagic {
payload = .init(raw: rawPayload, decoded: .sshSig(try sshSigPayload(from: rawPayload[6...])))
} else {
payload = .init(raw: rawPayload, decoded: .sshConnection(try sshConnectionPayload(from: rawPayload)))
}
} catch {
payload = .init(raw: rawPayload, decoded: nil)
}
return SSHAgent.Request.SignatureRequestContext(keyBlob: keyBlob, dataToSign: payload)
}
func sshSigPayload(from data: Data) throws(OpenSSHReaderError) -> SSHAgent.Request.SignatureRequestContext.SignaturePayload.DecodedPayload.SSHSigPayload {
// https://github.com/openssh/openssh-portable/blob/master/PROTOCOL.sshsig#L79
let payloadReader = OpenSSHReader(data: data)
let namespace = try payloadReader.readNextChunkAsString()
_ = try payloadReader.readNextChunk() // reserved
let hashAlgorithm = try payloadReader.readNextChunkAsString()
let hash = try payloadReader.readNextChunk()
return .init(
namespace: namespace,
hashAlgorithm: hashAlgorithm,
hash: hash
)
}
func sshConnectionPayload(from data: Data) throws(OpenSSHReaderError) -> SSHAgent.Request.SignatureRequestContext.SignaturePayload.DecodedPayload.SSHConnectionPayload {
let payloadReader = OpenSSHReader(data: data)
_ = try payloadReader.readNextChunk()
let magic = try payloadReader.readNextBytes(as: UInt8.self, convertEndianness: false)
guard magic == Constants.userAuthMagic else { throw .incorrectFormat }
let username = try payloadReader.readNextChunkAsString()
_ = try payloadReader.readNextChunkAsString() // "ssh-connection"
_ = try payloadReader.readNextChunkAsString() // "publickey-hostbound-v00@openssh.com"
let hasSignature = try payloadReader.readNextByteAsBool()
let algorithm = try payloadReader.readNextChunkAsString()
let publicKeyReader = try payloadReader.readNextChunkAsSubReader()
_ = try publicKeyReader.readNextChunk()
_ = try publicKeyReader.readNextChunk()
let publicKey = try publicKeyReader.readNextChunk()
let hostKeyReader = try payloadReader.readNextChunkAsSubReader()
_ = try hostKeyReader.readNextChunk()
let hostKey = try hostKeyReader.readNextChunk()
return .init(
username: username,
hasSignature: hasSignature,
publicKeyAlgorithm: algorithm,
publicKey: publicKey,
hostKey: hostKey,
)
}
func protocolExtension(from data: Data) throws(AgentParsingError) -> SSHAgent.ProtocolExtension {
do {
let reader = OpenSSHReader(data: data)
let nameRaw = try reader.readNextChunkAsString()
let nameSplit = nameRaw.split(separator: "@")
guard nameSplit.count == 2 else {
throw AgentParsingError.invalidData
}
let (name, domain) = (nameSplit[0], nameSplit[1])
switch domain {
case SSHAgent.ProtocolExtension.OpenSSHExtension.domain:
switch name {
case SSHAgent.ProtocolExtension.OpenSSHExtension.sessionBind(.empty).name:
let hostkeyBlob = try reader.readNextChunkAsSubReader()
let hostKeyType = try hostkeyBlob.readNextChunkAsString()
let hostKeyData = try hostkeyBlob.readNextChunk()
let sessionID = try reader.readNextChunk()
let signatureBlob = try reader.readNextChunkAsSubReader()
_ = try signatureBlob.readNextChunk() // key type again
let signature = try signatureBlob.readNextChunk()
let forwarding = try reader.readNextByteAsBool()
switch hostKeyType {
// FIXME: FACTOR OUT?
case "ssh-ed25519":
let hostKey = try CryptoKit.Curve25519.Signing.PublicKey(rawRepresentation: hostKeyData)
guard hostKey.isValidSignature(signature, for: sessionID) else {
throw AgentParsingError.incorrectSignature
}
case "ecdsa-sha2-nistp256":
let hostKey = try CryptoKit.P256.Signing.PublicKey(rawRepresentation: hostKeyData)
guard hostKey.isValidSignature(try .init(rawRepresentation: signature), for: sessionID) else {
throw AgentParsingError.incorrectSignature
}
case "ecdsa-sha2-nistp384":
let hostKey = try CryptoKit.P384.Signing.PublicKey(rawRepresentation: hostKeyData)
guard hostKey.isValidSignature(try .init(rawRepresentation: signature), for: sessionID) else {
throw AgentParsingError.incorrectSignature
}
case "ssh-mldsa-65":
if #available(macOS 26.0, *) {
let hostKey = try CryptoKit.MLDSA65.PublicKey(rawRepresentation: hostKeyData)
guard hostKey.isValidSignature(signature, for: sessionID) else {
throw AgentParsingError.incorrectSignature
}
} else {
throw AgentParsingError.unhandledRequest
}
case "ssh-mldsa-87":
if #available(macOS 26.0, *) {
let hostKey = try CryptoKit.MLDSA65.PublicKey(rawRepresentation: hostKeyData)
guard hostKey.isValidSignature(signature, for: sessionID) else {
throw AgentParsingError.incorrectSignature
}
} else {
throw AgentParsingError.unhandledRequest
}
case "ssh-rsa":
// FIXME: HANDLE
throw AgentParsingError.unhandledRequest
default:
throw AgentParsingError.unhandledRequest
}
let context = SSHAgent.ProtocolExtension.OpenSSHExtension.SessionBindContext(
hostKey: hostKeyData,
sessionID: sessionID,
signature: signature,
forwarding: forwarding
)
return .openSSH(.sessionBind(context))
default:
return .openSSH(.unknown(String(name)))
}
default:
return .unknown(nameRaw)
}
} catch let error as OpenSSHReaderError {
throw .openSSHReader(error)
} catch let error as AgentParsingError {
throw error
} catch {
throw .unknownRequest
}
}
func certificatePublicKeyBlob(from hash: Data) -> Data? {
let reader = OpenSSHReader(data: hash)
do {
let certType = String(decoding: try reader.readNextChunk(), as: UTF8.self)
switch certType {
case "ecdsa-sha2-nistp256-cert-v01@openssh.com",
"ecdsa-sha2-nistp384-cert-v01@openssh.com",
"ecdsa-sha2-nistp521-cert-v01@openssh.com":
_ = try reader.readNextChunk() // nonce
let curveIdentifier = try reader.readNextChunk()
let publicKey = try reader.readNextChunk()
let openSSHIdentifier = certType.replacingOccurrences(of: "-cert-v01@openssh.com", with: "")
return openSSHIdentifier.lengthAndData +
curveIdentifier.lengthAndData +
publicKey.lengthAndData
default:
return nil
}
} catch {
return nil
}
}
}
extension SSHAgentInputParser {
public enum AgentParsingError: Error, Codable {
case unknownRequest
case unhandledRequest
case invalidData
case incorrectSignature
case openSSHReader(OpenSSHReaderError)
}
}

View File

@@ -17,6 +17,6 @@ public protocol SigningWitness: Sendable {
/// - secret: The `Secret` that will was used to sign the request. /// - secret: The `Secret` that will was used to sign the request.
/// - store: The `Store` that signed the request.. /// - store: The `Store` that signed the request..
/// - provenance: A `SigningRequestProvenance` object describing the origin of the request. /// - provenance: A `SigningRequestProvenance` object describing the origin of the request.
func witness(accessTo secret: AnySecret, from store: AnySecretStore, by provenance: SigningRequestProvenance, offerPersistence: Bool) async throws func witness(accessTo secret: AnySecret, from store: AnySecretStore, by provenance: SigningRequestProvenance) async throws
} }

View File

@@ -39,7 +39,7 @@ public struct SocketController {
Task { @MainActor [fileHandle, sessionsContinuation, logger] in Task { @MainActor [fileHandle, sessionsContinuation, logger] in
// Create the sequence before triggering the notification to // Create the sequence before triggering the notification to
// ensure it will not be missed. // ensure it will not be missed.
let connectionAcceptedNotifications = NotificationCenter.default.notifications(named: .NSFileHandleConnectionAccepted, object: fileHandle) let connectionAcceptedNotifications = NotificationCenter.default.notifications(named: .NSFileHandleConnectionAccepted)
fileHandle.acceptConnectionInBackgroundAndNotify() fileHandle.acceptConnectionInBackgroundAndNotify()
@@ -94,7 +94,7 @@ extension SocketController {
guard !data.isEmpty else { guard !data.isEmpty else {
logger.debug("Socket controller received empty data, ending continuation.") logger.debug("Socket controller received empty data, ending continuation.")
messagesContinuation.finish() messagesContinuation.finish()
try? fileHandle.close() try fileHandle.close()
return return
} }
messagesContinuation.yield(data) messagesContinuation.yield(data)
@@ -126,8 +126,8 @@ private extension SocketPort {
convenience init(path: String) { convenience init(path: String) {
var addr = sockaddr_un() var addr = sockaddr_un()
let length = withUnsafeMutablePointer(to: &addr.sun_path.0) { pointer in let length = unsafe withUnsafeMutablePointer(to: &addr.sun_path.0) { pointer in
path.withCString { cstring in unsafe path.withCString { cstring in
let len = unsafe strlen(cstring) let len = unsafe strlen(cstring)
unsafe strncpy(pointer, cstring, len) unsafe strncpy(pointer, cstring, len)
return len return len

View File

@@ -0,0 +1,69 @@
import LocalAuthentication
/// A context describing a persisted authentication.
package final class PersistentAuthenticationContext<SecretType: Secret>: PersistedAuthenticationContext {
/// The Secret to persist authentication for.
let secret: SecretType
/// The LAContext used to authorize the persistent context.
package nonisolated(unsafe) let context: LAContext
/// An expiration date for the context.
/// - Note - Monotonic time instead of Date() to prevent people setting the clock back.
let monotonicExpiration: UInt64
/// Initializes a context.
/// - Parameters:
/// - secret: The Secret to persist authentication for.
/// - context: The LAContext used to authorize the persistent context.
/// - duration: The duration of the authorization context, in seconds.
init(secret: SecretType, context: LAContext, duration: TimeInterval) {
self.secret = secret
unsafe self.context = context
let durationInNanoSeconds = Measurement(value: duration, unit: UnitDuration.seconds).converted(to: .nanoseconds).value
self.monotonicExpiration = clock_gettime_nsec_np(CLOCK_MONOTONIC) + UInt64(durationInNanoSeconds)
}
/// A boolean describing whether or not the context is still valid.
package var valid: Bool {
clock_gettime_nsec_np(CLOCK_MONOTONIC) < monotonicExpiration
}
package var expiration: Date {
let remainingNanoseconds = monotonicExpiration - clock_gettime_nsec_np(CLOCK_MONOTONIC)
let remainingInSeconds = Measurement(value: Double(remainingNanoseconds), unit: UnitDuration.nanoseconds).converted(to: .seconds).value
return Date(timeIntervalSinceNow: remainingInSeconds)
}
}
package actor PersistentAuthenticationHandler<SecretType: Secret>: Sendable {
private var persistedAuthenticationContexts: [SecretType: PersistentAuthenticationContext<SecretType>] = [:]
package init() {
}
package func existingPersistedAuthenticationContext(secret: SecretType) -> PersistentAuthenticationContext<SecretType>? {
guard let persisted = persistedAuthenticationContexts[secret], persisted.valid else { return nil }
return persisted
}
package func persistAuthentication(secret: SecretType, forDuration duration: TimeInterval) async throws {
let newContext = LAContext()
newContext.touchIDAuthenticationAllowableReuseDuration = duration
newContext.localizedCancelTitle = String(localized: .authContextRequestDenyButton)
let formatter = DateComponentsFormatter()
formatter.unitsStyle = .spellOut
formatter.allowedUnits = [.hour, .minute, .day]
let durationString = formatter.string(from: duration)!
newContext.localizedReason = String(localized: .authContextPersistForDuration(secretName: secret.name, duration: durationString))
let success = try await newContext.evaluatePolicy(.deviceOwnerAuthentication, localizedReason: newContext.localizedReason)
guard success else { return }
let context = PersistentAuthenticationContext(secret: secret, context: newContext, duration: duration)
persistedAuthenticationContexts[secret] = context
}
}

View File

@@ -31,7 +31,7 @@ SecretKit is a collection of protocols describing secrets and stores.
### Authentication Persistence ### Authentication Persistence
- ``AuthenticationContextProtocol`` - ``PersistedAuthenticationContext``
### Errors ### Errors

View File

@@ -1,5 +1,4 @@
import Foundation import Foundation
import LocalAuthentication
/// Type eraser for SecretStore. /// Type eraser for SecretStore.
open class AnySecretStore: SecretStore, @unchecked Sendable { open class AnySecretStore: SecretStore, @unchecked Sendable {
@@ -9,7 +8,9 @@ open class AnySecretStore: SecretStore, @unchecked Sendable {
private let _id: @Sendable () -> UUID private let _id: @Sendable () -> UUID
private let _name: @MainActor @Sendable () -> String private let _name: @MainActor @Sendable () -> String
private let _secrets: @MainActor @Sendable () -> [AnySecret] private let _secrets: @MainActor @Sendable () -> [AnySecret]
private let _sign: @Sendable (Data, AnySecret, SigningRequestProvenance, LAContext?) async throws -> Data private let _sign: @Sendable (Data, AnySecret, SigningRequestProvenance) async throws -> Data
private let _existingPersistedAuthenticationContext: @Sendable (AnySecret) async -> PersistedAuthenticationContext?
private let _persistAuthentication: @Sendable (AnySecret, TimeInterval) async throws -> Void
private let _reloadSecrets: @Sendable () async -> Void private let _reloadSecrets: @Sendable () async -> Void
public init<SecretStoreType>(_ secretStore: SecretStoreType) where SecretStoreType: SecretStore { public init<SecretStoreType>(_ secretStore: SecretStoreType) where SecretStoreType: SecretStore {
@@ -18,7 +19,9 @@ open class AnySecretStore: SecretStore, @unchecked Sendable {
_name = { secretStore.name } _name = { secretStore.name }
_id = { secretStore.id } _id = { secretStore.id }
_secrets = { secretStore.secrets.map { AnySecret($0) } } _secrets = { secretStore.secrets.map { AnySecret($0) } }
_sign = { try await secretStore.sign(data: $0, with: $1.base as! SecretStoreType.SecretType, for: $2, context: $3) } _sign = { try await secretStore.sign(data: $0, with: $1.base as! SecretStoreType.SecretType, for: $2) }
_existingPersistedAuthenticationContext = { await secretStore.existingPersistedAuthenticationContext(secret: $0.base as! SecretStoreType.SecretType) }
_persistAuthentication = { try await secretStore.persistAuthentication(secret: $0.base as! SecretStoreType.SecretType, forDuration: $1) }
_reloadSecrets = { await secretStore.reloadSecrets() } _reloadSecrets = { await secretStore.reloadSecrets() }
} }
@@ -38,8 +41,16 @@ open class AnySecretStore: SecretStore, @unchecked Sendable {
return _secrets() return _secrets()
} }
public func sign(data: Data, with secret: AnySecret, for provenance: SigningRequestProvenance, context: LAContext?) async throws -> Data { public func sign(data: Data, with secret: AnySecret, for provenance: SigningRequestProvenance) async throws -> Data {
try await _sign(data, secret, provenance, context) try await _sign(data, secret, provenance)
}
public func existingPersistedAuthenticationContext(secret: AnySecret) async -> PersistedAuthenticationContext? {
await _existingPersistedAuthenticationContext(secret)
}
public func persistAuthentication(secret: AnySecret, forDuration duration: TimeInterval) async throws {
try await _persistAuthentication(secret, duration)
} }
public func reloadSecrets() async { public func reloadSecrets() async {

View File

@@ -1,41 +0,0 @@
import Foundation
import LocalAuthentication
/// Protocol describing an authentication context. This is an authorization that can be reused for multiple access to a secret that requires authentication for a specific period of time.
public protocol AuthenticationContextProtocol: Sendable, Identifiable {
/// Whether the context remains valid.
var secret: AnySecret { get }
var laContext: LAContext { get }
func valid(for request: SignatureRequest) -> Bool
}
public struct SignatureRequest: Identifiable, Hashable, Sendable, Comparable {
public let id: UUID
public let date: Date
public let secret: AnySecret
public let provenance: SigningRequestProvenance
public init(secret: AnySecret, provenance: SigningRequestProvenance) {
self.id = UUID()
self.date = Date()
self.secret = secret
self.provenance = provenance
}
public var batchID: Int {
var hasher = Hasher()
provenance.batchID.hash(into: &hasher)
secret.id.hash(into: &hasher)
return hasher.finalize()
}
public static func < (lhs: SignatureRequest, rhs: SignatureRequest) -> Bool {
lhs.date < rhs.date
}
}

View File

@@ -0,0 +1,9 @@
import Foundation
/// Protocol describing a persisted authentication context. This is an authorization that can be reused for multiple access to a secret that requires authentication for a specific period of time.
public protocol PersistedAuthenticationContext: Sendable {
/// Whether the context remains valid.
var valid: Bool { get }
/// The date at which the authorization expires and the context becomes invalid.
var expiration: Date { get }
}

View File

@@ -1,5 +1,4 @@
import Foundation import Foundation
import LocalAuthentication
/// Manages access to Secrets, and performs signature operations on data using those Secrets. /// Manages access to Secrets, and performs signature operations on data using those Secrets.
public protocol SecretStore<SecretType>: Identifiable, Sendable { public protocol SecretStore<SecretType>: Identifiable, Sendable {
@@ -21,7 +20,20 @@ public protocol SecretStore<SecretType>: Identifiable, Sendable {
/// - secret: The ``Secret`` to sign with. /// - secret: The ``Secret`` to sign with.
/// - provenance: A ``SigningRequestProvenance`` describing where the request came from. /// - provenance: A ``SigningRequestProvenance`` describing where the request came from.
/// - Returns: The signed data. /// - Returns: The signed data.
func sign(data: Data, with secret: SecretType, for provenance: SigningRequestProvenance, context: LAContext?) async throws -> Data func sign(data: Data, with secret: SecretType, for provenance: SigningRequestProvenance) async throws -> Data
/// Checks to see if there is currently a valid persisted authentication for a given secret.
/// - Parameters:
/// - secret: The ``Secret`` to check if there is a persisted authentication for.
/// - Returns: A persisted authentication context, if a valid one exists.
func existingPersistedAuthenticationContext(secret: SecretType) async -> PersistedAuthenticationContext?
/// Persists user authorization for access to a secret.
/// - Parameters:
/// - secret: The ``Secret`` to persist the authorization for.
/// - duration: The duration that the authorization should persist for.
/// - Note: This is used for temporarily unlocking access to a secret which would otherwise require authentication every single use. This is useful for situations where the user anticipates several rapid accesses to a authorization-guarded secret.
func persistAuthentication(secret: SecretType, forDuration duration: TimeInterval) async throws
/// Requests that the store reload secrets from any backing store, if neccessary. /// Requests that the store reload secrets from any backing store, if neccessary.
func reloadSecrets() async func reloadSecrets() async

View File

@@ -2,23 +2,13 @@ import Foundation
import AppKit import AppKit
/// Describes the chain of applications that requested a signature operation. /// Describes the chain of applications that requested a signature operation.
public struct SigningRequestProvenance: Hashable, Sendable { public struct SigningRequestProvenance: Equatable, Sendable {
/// A list of processes involved in the request. /// A list of processes involved in the request.
/// - Note: A chain will typically consist of many elements even for a simple request. For example, running `git fetch` in Terminal.app would generate a request chain of `ssh` -> `git` -> `zsh` -> `login` -> `Terminal.app` /// - Note: A chain will typically consist of many elements even for a simple request. For example, running `git fetch` in Terminal.app would generate a request chain of `ssh` -> `git` -> `zsh` -> `login` -> `Terminal.app`
public var chain: [Process] public var chain: [Process]
public init(root: Process) {
public var date: Date
public init(root: Process, date: Date = .now) {
self.chain = [root] self.chain = [root]
self.date = date
}
public var batchID: Int {
var hasher = Hasher()
chain.map(\.path).hash(into: &hasher)
return hasher.finalize()
} }
} }
@@ -40,7 +30,7 @@ extension SigningRequestProvenance {
extension SigningRequestProvenance { extension SigningRequestProvenance {
/// Describes a process in a `SigningRequestProvenance` chain. /// Describes a process in a `SigningRequestProvenance` chain.
public struct Process: Hashable, Sendable { public struct Process: Equatable, Sendable {
/// The pid of the process. /// The pid of the process.
public let pid: Int32 public let pid: Int32

View File

@@ -17,6 +17,7 @@ extension SecureEnclave {
} }
public let id = UUID() public let id = UUID()
public let name = String(localized: .secureEnclave) public let name = String(localized: .secureEnclave)
private let persistentAuthenticationHandler = PersistentAuthenticationHandler<Secret>()
/// Initializes a Store. /// Initializes a Store.
@MainActor public init() { @MainActor public init() {
@@ -36,7 +37,16 @@ extension SecureEnclave {
// MARK: SecretStore // MARK: SecretStore
public func sign(data: Data, with secret: Secret, for provenance: SigningRequestProvenance, context: LAContext?) async throws -> Data { public func sign(data: Data, with secret: Secret, for provenance: SigningRequestProvenance) async throws -> Data {
var context: LAContext
if let existing = await persistentAuthenticationHandler.existingPersistedAuthenticationContext(secret: secret) {
context = unsafe existing.context
} else {
let newContext = LAContext()
newContext.localizedReason = String(localized: .authContextRequestSignatureDescription(appName: provenance.origin.displayName, secretName: secret.name))
newContext.localizedCancelTitle = String(localized: .authContextRequestDenyButton)
context = newContext
}
let queryAttributes = KeychainDictionary([ let queryAttributes = KeychainDictionary([
kSecClass: Constants.keyClass, kSecClass: Constants.keyClass,
@@ -78,6 +88,14 @@ extension SecureEnclave {
} }
public func existingPersistedAuthenticationContext(secret: Secret) async -> PersistedAuthenticationContext? {
await persistentAuthenticationHandler.existingPersistedAuthenticationContext(secret: secret)
}
public func persistAuthentication(secret: Secret, forDuration duration: TimeInterval) async throws {
try await persistentAuthenticationHandler.persistAuthentication(secret: secret, forDuration: duration)
}
@MainActor public func reloadSecrets() { @MainActor public func reloadSecrets() {
let before = secrets let before = secrets
secrets.removeAll() secrets.removeAll()

View File

@@ -1,53 +0,0 @@
import Foundation
import Security
import CryptoTokenKit
import CryptoKit
import os
import SSHProtocolKit
import CertificateKit
public struct CertificateMigrator {
private let logger = Logger(subsystem: "com.maxgoedjen.secretive.migration", category: "CertificateKitMigrator")
private let publicKeysDirectory: URL
private let certificatesDirectory: URL
private let certificateStore: CertificateStore
/// Initializes a PublicKeyFileStoreController.
public init(homeDirectory: URL, certificateStore: CertificateStore) {
publicKeysDirectory = homeDirectory.appending(component: "PublicKeys")
certificatesDirectory = homeDirectory.appending(component: "Certificates")
self.certificateStore = certificateStore
}
@MainActor public func migrate() throws {
try migrate(directory: publicKeysDirectory)
try migrate(directory: certificatesDirectory)
}
@MainActor public func migrate(directory: URL) throws {
let fileCerts = try FileManager.default
.contentsOfDirectory(atPath: directory.path())
.filter { $0.hasSuffix("-cert.pub") }
Task {
for path in fileCerts {
do {
let url = directory.appending(component: path)
let data = try Data(contentsOf: url)
let parser = try await XPCCertificateParser()
let cert = try await parser.parse(data: data)
try certificateStore.save(certificate: Certificate(openSSHCertificate: cert, rawData: data))
do {
try FileManager.default.removeItem(at: url)
} catch {
logger.error("Failed to delete successfully migrated cert: \(path)")
}
} catch {
logger.error("Failed to migrate cert: \(path)")
}
}
}
}
}

View File

@@ -1,29 +0,0 @@
import Foundation
import OSLog
import SSHProtocolKit
import CertificateKit
import XPCWrappers
/// Delegates all agent input parsing to an XPC service which wraps OpenSSH
public final class XPCCertificateParser: OpenSSHCertificateParserProtocol {
private let logger = Logger(subsystem: "com.maxgoedjen.secretive", category: "XPCCertificateParser")
private let session: XPCTypedSession<OpenSSHCertificate, OpenSSHCertificateError>
public init() async throws {
logger.debug("Creating XPCCertificateParser")
session = try await XPCTypedSession(serviceName: "com.maxgoedjen.Secretive.SecretiveCertificateParser", warmup: true)
logger.debug("XPCCertificateParser is warmed up.")
}
public func parse(data: Data) async throws -> OpenSSHCertificate {
logger.debug("Parsing input")
defer { logger.debug("Parsed input") }
return try await session.send(data)
}
deinit {
session.complete()
}
}

View File

@@ -34,6 +34,7 @@ extension SmartCard {
public var secrets: [Secret] { public var secrets: [Secret] {
state.secrets state.secrets
} }
private let persistentAuthenticationHandler = PersistentAuthenticationHandler<Secret>()
/// Initializes a Store. /// Initializes a Store.
public init() { public init() {
@@ -56,15 +57,23 @@ extension SmartCard {
// MARK: Public API // MARK: Public API
public func sign(data: Data, with secret: Secret, for provenance: SigningRequestProvenance) async throws -> Data {
public func sign(data: Data, with secret: SmartCard.Secret, for provenance: SigningRequestProvenance, context: LAContext?) async throws -> Data {
guard let tokenID = await state.tokenID else { fatalError() } guard let tokenID = await state.tokenID else { fatalError() }
var context: LAContext
if let existing = await persistentAuthenticationHandler.existingPersistedAuthenticationContext(secret: secret) {
context = unsafe existing.context
} else {
let newContext = LAContext()
newContext.localizedReason = String(localized: .authContextRequestSignatureDescription(appName: provenance.origin.displayName, secretName: secret.name))
newContext.localizedCancelTitle = String(localized: .authContextRequestDenyButton)
context = newContext
}
let attributes = KeychainDictionary([ let attributes = KeychainDictionary([
kSecClass: kSecClassKey, kSecClass: kSecClassKey,
kSecAttrKeyClass: kSecAttrKeyClassPrivate, kSecAttrKeyClass: kSecAttrKeyClassPrivate,
kSecAttrApplicationLabel: secret.id as CFData, kSecAttrApplicationLabel: secret.id as CFData,
kSecAttrTokenID: tokenID, kSecAttrTokenID: tokenID,
kSecUseAuthenticationContext: context!, // FIXME: THIS kSecUseAuthenticationContext: context,
kSecReturnRef: true kSecReturnRef: true
]) ])
var untyped: CFTypeRef? var untyped: CFTypeRef?
@@ -84,6 +93,14 @@ extension SmartCard {
return signature as Data return signature as Data
} }
public func existingPersistedAuthenticationContext(secret: Secret) async -> PersistedAuthenticationContext? {
await persistentAuthenticationHandler.existingPersistedAuthenticationContext(secret: secret)
}
public func persistAuthentication(secret: Secret, forDuration duration: TimeInterval) async throws {
try await persistentAuthenticationHandler.persistAuthentication(secret: secret, forDuration: duration)
}
/// Reloads all secrets from the store. /// Reloads all secrets from the store.
@MainActor public func reloadSecrets() { @MainActor public func reloadSecrets() {
reloadSecretsInternal() reloadSecretsInternal()

View File

@@ -11,7 +11,7 @@ extension ProcessInfo {
} }
guard let value = SecTaskCopyValueForEntitlement(task, "com.apple.developer.team-identifier" as CFString, nil) as? String else { guard let value = SecTaskCopyValueForEntitlement(task, "com.apple.developer.team-identifier" as CFString, nil) as? String else {
// assertionFailure("SecTaskCopyValueForEntitlement(com.apple.developer.team-identifier) failed") assertionFailure("SecTaskCopyValueForEntitlement(com.apple.developer.team-identifier) failed")
return fallbackTeamID return fallbackTeamID
} }

View File

@@ -1,17 +1,16 @@
import Foundation import Foundation
import Testing import Testing
import CryptoKit import CryptoKit
import CertificateKit
@testable import SSHProtocolKit @testable import SSHProtocolKit
@testable import SecretKit @testable import SecretKit
@testable import SecretAgentKit @testable import SecretAgentKit
@Suite @MainActor struct AgentTests { @Suite struct AgentTests {
// MARK: Identity Listing // MARK: Identity Listing
@Test func emptyStores() async throws { @Test func emptyStores() async throws {
let agent = Agent(storeList: SecretStoreList(), certificateStore: CertificateStore()) let agent = Agent(storeList: SecretStoreList())
let request = try SSHAgentInputParser().parse(data: Constants.Requests.requestIdentities) let request = try SSHAgentInputParser().parse(data: Constants.Requests.requestIdentities)
let response = await agent.handle(request: request, provenance: .test) let response = await agent.handle(request: request, provenance: .test)
#expect(response == Constants.Responses.requestIdentitiesEmpty) #expect(response == Constants.Responses.requestIdentitiesEmpty)
@@ -19,7 +18,7 @@ import CertificateKit
@Test func identitiesList() async throws { @Test func identitiesList() async throws {
let list = await storeList(with: [Constants.Secrets.ecdsa256Secret, Constants.Secrets.ecdsa384Secret]) let list = await storeList(with: [Constants.Secrets.ecdsa256Secret, Constants.Secrets.ecdsa384Secret])
let agent = Agent(storeList: list, certificateStore: CertificateStore()) let agent = Agent(storeList: list)
let request = try SSHAgentInputParser().parse(data: Constants.Requests.requestIdentities) let request = try SSHAgentInputParser().parse(data: Constants.Requests.requestIdentities)
let response = await agent.handle(request: request, provenance: .test) let response = await agent.handle(request: request, provenance: .test)
@@ -33,7 +32,7 @@ import CertificateKit
@Test func noMatchingIdentities() async throws { @Test func noMatchingIdentities() async throws {
let list = await storeList(with: [Constants.Secrets.ecdsa256Secret, Constants.Secrets.ecdsa384Secret]) let list = await storeList(with: [Constants.Secrets.ecdsa256Secret, Constants.Secrets.ecdsa384Secret])
let agent = Agent(storeList: list, certificateStore: CertificateStore()) let agent = Agent(storeList: list)
let request = try SSHAgentInputParser().parse(data: Constants.Requests.requestSignatureWithNoneMatching) let request = try SSHAgentInputParser().parse(data: Constants.Requests.requestSignatureWithNoneMatching)
let response = await agent.handle(request: request, provenance: .test) let response = await agent.handle(request: request, provenance: .test)
#expect(response == Constants.Responses.requestFailure) #expect(response == Constants.Responses.requestFailure)
@@ -43,7 +42,7 @@ import CertificateKit
let request = try SSHAgentInputParser().parse(data: Constants.Requests.requestSignature) let request = try SSHAgentInputParser().parse(data: Constants.Requests.requestSignature)
guard case SSHAgent.Request.signRequest(let context) = request else { return } guard case SSHAgent.Request.signRequest(let context) = request else { return }
let list = await storeList(with: [Constants.Secrets.ecdsa256Secret, Constants.Secrets.ecdsa384Secret]) let list = await storeList(with: [Constants.Secrets.ecdsa256Secret, Constants.Secrets.ecdsa384Secret])
let agent = Agent(storeList: list, certificateStore: CertificateStore()) let agent = Agent(storeList: list)
let response = await agent.handle(request: request, provenance: .test) let response = await agent.handle(request: request, provenance: .test)
let responseReader = OpenSSHReader(data: response) let responseReader = OpenSSHReader(data: response)
let length = try responseReader.readNextBytes(as: UInt32.self) let length = try responseReader.readNextBytes(as: UInt32.self)
@@ -78,7 +77,7 @@ import CertificateKit
let witness = StubWitness(speakNow: { _,_ in let witness = StubWitness(speakNow: { _,_ in
return true return true
}, witness: { _, _ in }) }, witness: { _, _ in })
let agent = Agent(storeList: list, certificateStore: CertificateStore(), witness: witness) let agent = Agent(storeList: list, witness: witness)
let response = await agent.handle(request: .signRequest(.empty), provenance: .test) let response = await agent.handle(request: .signRequest(.empty), provenance: .test)
#expect(response == Constants.Responses.requestFailure) #expect(response == Constants.Responses.requestFailure)
} }
@@ -91,7 +90,7 @@ import CertificateKit
}, witness: { _, trace in }, witness: { _, trace in
witnessed = true witnessed = true
}) })
let agent = Agent(storeList: list, certificateStore: CertificateStore(), witness: witness) let agent = Agent(storeList: list, witness: witness)
let request = try SSHAgentInputParser().parse(data: Constants.Requests.requestSignature) let request = try SSHAgentInputParser().parse(data: Constants.Requests.requestSignature)
_ = await agent.handle(request: request, provenance: .test) _ = await agent.handle(request: request, provenance: .test)
#expect(witnessed) #expect(witnessed)
@@ -107,7 +106,7 @@ import CertificateKit
}, witness: { _, trace in }, witness: { _, trace in
witnessTrace = trace witnessTrace = trace
}) })
let agent = Agent(storeList: list, certificateStore: CertificateStore(), witness: witness) let agent = Agent(storeList: list, witness: witness)
let request = try SSHAgentInputParser().parse(data: Constants.Requests.requestSignature) let request = try SSHAgentInputParser().parse(data: Constants.Requests.requestSignature)
_ = await agent.handle(request: request, provenance: .test) _ = await agent.handle(request: request, provenance: .test)
#expect(witnessTrace == speakNowTrace) #expect(witnessTrace == speakNowTrace)
@@ -118,9 +117,9 @@ import CertificateKit
@Test func signatureException() async throws { @Test func signatureException() async throws {
let list = await storeList(with: [Constants.Secrets.ecdsa256Secret, Constants.Secrets.ecdsa384Secret]) let list = await storeList(with: [Constants.Secrets.ecdsa256Secret, Constants.Secrets.ecdsa384Secret])
let store = list.stores.first?.base as! Stub.Store let store = await list.stores.first?.base as! Stub.Store
store.shouldThrow = true store.shouldThrow = true
let agent = Agent(storeList: list, certificateStore: CertificateStore()) let agent = Agent(storeList: list)
let request = try SSHAgentInputParser().parse(data: Constants.Requests.requestSignature) let request = try SSHAgentInputParser().parse(data: Constants.Requests.requestSignature)
let response = await agent.handle(request: request, provenance: .test) let response = await agent.handle(request: request, provenance: .test)
#expect(response == Constants.Responses.requestFailure) #expect(response == Constants.Responses.requestFailure)
@@ -129,7 +128,7 @@ import CertificateKit
// MARK: Unsupported // MARK: Unsupported
@Test func unhandledAdd() async throws { @Test func unhandledAdd() async throws {
let agent = Agent(storeList: SecretStoreList(), certificateStore: CertificateStore()) let agent = Agent(storeList: SecretStoreList())
let response = await agent.handle(request: .addIdentity, provenance: .test) let response = await agent.handle(request: .addIdentity, provenance: .test)
#expect(response == Constants.Responses.requestFailure) #expect(response == Constants.Responses.requestFailure)
} }
@@ -144,7 +143,7 @@ extension SigningRequestProvenance {
extension AgentTests { extension AgentTests {
func storeList(with secrets: [Stub.Secret]) async -> SecretStoreList { @MainActor func storeList(with secrets: [Stub.Secret]) async -> SecretStoreList {
let store = Stub.Store() let store = Stub.Store()
store.secrets.append(contentsOf: secrets) store.secrets.append(contentsOf: secrets)
let storeList = SecretStoreList() let storeList = SecretStoreList()

View File

@@ -49,7 +49,7 @@ extension Stub {
print("Public Key OpenSSH: \(OpenSSHPublicKeyWriter().openSSHString(secret: secret))") print("Public Key OpenSSH: \(OpenSSHPublicKeyWriter().openSSHString(secret: secret))")
} }
public func sign(data: Data, with secret: Secret, for provenance: SigningRequestProvenance, context: AuthenticationContextProtocol?) throws -> Data { public func sign(data: Data, with secret: Secret, for provenance: SigningRequestProvenance) throws -> Data {
guard !shouldThrow else { guard !shouldThrow else {
throw NSError(domain: "test", code: 0, userInfo: nil) throw NSError(domain: "test", code: 0, userInfo: nil)
} }
@@ -57,7 +57,7 @@ extension Stub {
return try privateKey.signature(for: data).rawRepresentation return try privateKey.signature(for: data).rawRepresentation
} }
public func existingAuthenticationContextProtocol(secret: Stub.Secret) -> AuthenticationContextProtocol? { public func existingPersistedAuthenticationContext(secret: Stub.Secret) -> PersistedAuthenticationContext? {
nil nil
} }

View File

@@ -1,138 +0,0 @@
import Cocoa
import OSLog
import SecretKit
import SecureEnclaveSecretKit
import SmartCardSecretKit
import SecretAgentKit
import Brief
import Observation
import Common
import SwiftUI
import CertificateKit
@main
struct SecretAgent: App {
@MainActor private let storeList: SecretStoreList = {
let list = SecretStoreList()
let cryptoKit = SecureEnclave.Store()
let migrator = SecureEnclave.CryptoKitMigrator()
try? migrator.migrate(to: cryptoKit)
list.add(store: cryptoKit)
list.add(store: SmartCard.Store())
return list
}()
@MainActor private let certificateStore: CertificateStore = CertificateStore()
private let updater = Updater(checkOnLaunch: true)
private let notifier = Notifier()
private let authenticationHandler = AuthenticationHandler()
private let publicKeyFileStoreController = PublicKeyFileStoreController(publicKeysURL: URL.publicKeyDirectory, certificatesURL: URL.certificatesDirectory)
@State var pending: ([[SignatureRequest]], (Set<SignatureRequest>) async throws -> Void)?
@Environment(\.openWindow) var openWindow
private let logger = Logger(subsystem: "com.maxgoedjen.secretive.secretagent", category: "App")
@SceneBuilder var body: some Scene {
MenuBarExtra(isInserted: .constant(false)) {
EmptyView()
} label: {
Image(systemName: "lock")
.task {
await notifier.registerPersistenceHandler {
try await authenticationHandler.persistAuthentication(secret: $0, forDuration: $1)
}
}
.task {
let socketController = SocketController(path: URL.socketPath)
let agent = Agent(
storeList: storeList,
certificateStore: certificateStore,
authenticationHandler: authenticationHandler,
witness: notifier
)
for await session in socketController.sessions {
Task {
do {
let inputParser = try await XPCAgentInputParser()
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 session.write(agentResponse)
}
} catch {
try? session.close()
}
}
}
}
// .task {
// let socketController = SocketController(path: URL.agentHomeURL.appendingPathComponent("socket-two.ssh").path())
// let socketController = SocketController(path: "/Users/max/Downloads/test.ssh")
// let agent = Agent(storeList: storeList, authenticationHandler: authenticationHandler, witness: notifier)
// for await session in socketController.sessions {
// Task {
// let inputParser = try await XPCAgentInputParser()
// do {
// 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 session.write(agentResponse)
// }
// } catch {
// try session.close()
// }
// }
// }
// }
.task {
try? publicKeyFileStoreController.generatePublicKeys(for: storeList.allSecrets, clear: true)
for await _ in NotificationCenter.default.notifications(named: .secretStoreReloaded) {
try? publicKeyFileStoreController.generatePublicKeys(for: storeList.allSecrets, clear: true)
}
}
.task {
let certsMigrator = CertificateMigrator(homeDirectory: URL.homeDirectory, certificateStore: certificateStore)
try? certsMigrator.migrate()
try? publicKeyFileStoreController.generateCertificates(for: certificateStore.certificates, clear: true)
for await _ in NotificationCenter.default.notifications(named: .certificateStoreReloaded) {
try? publicKeyFileStoreController.generateCertificates(for: certificateStore.certificates, clear: true)
}
}
.task {
await authenticationHandler.setBatchAuthHandler { @MainActor pending, authorize in
self.pending = (pending, authorize)
openWindow(id: String(describing: BatchedRequestsView.self))
}
}
.task {
notifier.prompt()
_ = withObservationTracking {
updater.update
} onChange: { [updater, notifier] in
Task {
guard !updater.currentVersion.isTestBuild else { return }
await notifier.notify(update: updater.update!) { release in
await updater.ignore(release: release)
}
}
}
}
}
WindowGroup(id: String(describing: BatchedRequestsView.self)) {
pendingView
}
.windowStyle(.hiddenTitleBar)
.windowResizability(.contentSize)
}
@ViewBuilder
var pendingView: some View {
if let (requests, authorize) = pending {
BatchedRequestsView(pending: requests, review: authorize)
}
}
}

View File

@@ -0,0 +1,73 @@
import Cocoa
import OSLog
import SecretKit
import SecureEnclaveSecretKit
import SmartCardSecretKit
import SecretAgentKit
import Brief
import Observation
import Common
@main
class AppDelegate: NSObject, NSApplicationDelegate {
@MainActor private let storeList: SecretStoreList = {
let list = SecretStoreList()
let cryptoKit = SecureEnclave.Store()
let migrator = SecureEnclave.CryptoKitMigrator()
try? migrator.migrate(to: cryptoKit)
list.add(store: cryptoKit)
list.add(store: SmartCard.Store())
return list
}()
private let updater = Updater(checkOnLaunch: true)
private let notifier = Notifier()
private let publicKeyFileStoreController = PublicKeyFileStoreController(directory: URL.publicKeyDirectory)
private lazy var agent: Agent = {
Agent(storeList: storeList, witness: notifier)
}()
private lazy var socketController: SocketController = {
let path = URL.socketPath as String
return SocketController(path: path)
}()
private let logger = Logger(subsystem: "com.maxgoedjen.secretive.secretagent", category: "AppDelegate")
func applicationDidFinishLaunching(_ aNotification: Notification) {
logger.debug("SecretAgent finished launching")
Task {
for await session in socketController.sessions {
Task {
let inputParser = try await XPCAgentInputParser()
do {
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 session.write(agentResponse)
}
} catch {
try session.close()
}
}
}
}
Task {
for await _ in NotificationCenter.default.notifications(named: .secretStoreReloaded) {
try? publicKeyFileStoreController.generatePublicKeys(for: storeList.allSecrets, clear: true)
}
}
try? publicKeyFileStoreController.generatePublicKeys(for: storeList.allSecrets, clear: true)
notifier.prompt()
_ = withObservationTracking {
updater.update
} onChange: { [updater, notifier] in
Task {
guard !updater.currentVersion.isTestBuild else { return }
await notifier.notify(update: updater.update!) { release in
await updater.ignore(release: release)
}
}
}
}
}

View File

@@ -1,58 +0,0 @@
import SwiftUI
import SecretKit
import SecretAgentKit
import SmartCardSecretKit
struct BatchedRequestsView: View {
let pending: [[SignatureRequest]]
let review: (Set<SignatureRequest>) async throws -> Void
init(pending: [[SignatureRequest]], review: @escaping (Set<SignatureRequest>) async throws -> Void) {
self.pending = pending
self.review = review
}
var body: some View {
VStack(alignment: .leading) {
// .padding()
Form {
// Text("Multiple authenticated requests are pending. You can approve them batches, or request they all proceed individually.")
ForEach(Array(pending.enumerated()), id: \.offset) { group in
Section {
ForEach(Array(group.element.enumerated()), id: \.offset) { pending in
HStack {
VStack(alignment: .leading) {
Text(pending.element.provenance.origin.displayName)
.font(.headline)
Text(pending.element.provenance.date.formatted())
.font(.footnote)
}
Spacer()
Button("Review") {
Task {
try? await review([pending.element])
}
}
}
}
} header: {
HStack {
Text("\(group.element.first!.provenance.origin.displayName) - \(group.element.first!.secret.name)")
Spacer()
Button("Review All") {
Task {
try? await review(Set(group.element))
}
}
}
}
}
}
.formStyle(.grouped)
}
.frame(maxWidth: .infinity, maxHeight: .infinity)
}
}

View File

@@ -1,47 +0,0 @@
import Foundation
import Security
import CryptoTokenKit
import CryptoKit
import os
import SSHProtocolKit
import CertificateKit
import SharedXPCServices
public struct CertificateMigrator {
private let logger = Logger(subsystem: "com.maxgoedjen.secretive.migration", category: "CertificateKitMigrator")
private let directory: URL
private let certificateStore: CertificateStore
/// Initializes a PublicKeyFileStoreController.
public init(homeDirectory: URL, certificateStore: CertificateStore) {
directory = homeDirectory.appending(component: "PublicKeys")
self.certificateStore = certificateStore
}
@MainActor public func migrate() throws {
let fileCerts = try FileManager.default
.contentsOfDirectory(atPath: directory.path())
.filter { $0.hasSuffix("-cert.pub") }
Task {
for path in fileCerts {
do {
let url = directory.appending(component: path)
let data = try Data(contentsOf: url)
let parser = try await XPCCertificateParser()
let cert = try await parser.parse(data: data)
try certificateStore.save(certificate: Certificate(openSSHCertificate: cert, rawData: data))
do {
try FileManager.default.removeItem(at: url)
} catch {
logger.error("Failed to delete successfully migrated cert: \(path)")
}
} catch {
logger.error("Failed to migrate cert: \(path)")
}
}
}
}
}

View File

@@ -5,8 +5,6 @@ import SecretKit
import SecretAgentKit import SecretAgentKit
import Brief import Brief
typealias PersistAction = (@Sendable (AnySecret, TimeInterval) async throws -> Void)
final class Notifier: Sendable { final class Notifier: Sendable {
private let notificationDelegate = NotificationDelegate() private let notificationDelegate = NotificationDelegate()
@@ -17,12 +15,6 @@ final class Notifier: Sendable {
let updateCategory = UNNotificationCategory(identifier: Constants.updateCategoryIdentitifier, actions: [updateAction, ignoreAction], intentIdentifiers: [], options: []) let updateCategory = UNNotificationCategory(identifier: Constants.updateCategoryIdentitifier, actions: [updateAction, ignoreAction], intentIdentifiers: [], options: [])
let criticalUpdateCategory = UNNotificationCategory(identifier: Constants.criticalUpdateCategoryIdentitifier, actions: [updateAction], intentIdentifiers: [], options: []) let criticalUpdateCategory = UNNotificationCategory(identifier: Constants.criticalUpdateCategoryIdentitifier, actions: [updateAction], intentIdentifiers: [], options: [])
UNUserNotificationCenter.current().setNotificationCategories([updateCategory, criticalUpdateCategory])
UNUserNotificationCenter.current().delegate = notificationDelegate
}
func registerPersistenceHandler(action: @escaping PersistAction) async {
let rawDurations = [ let rawDurations = [
Measurement(value: 1, unit: UnitDuration.minutes), Measurement(value: 1, unit: UnitDuration.minutes),
Measurement(value: 5, unit: UnitDuration.minutes), Measurement(value: 5, unit: UnitDuration.minutes),
@@ -32,9 +24,11 @@ final class Notifier: Sendable {
let doNotPersistAction = UNNotificationAction(identifier: Constants.doNotPersistActionIdentitifier, title: String(localized: .persistAuthenticationDeclineButton), options: []) let doNotPersistAction = UNNotificationAction(identifier: Constants.doNotPersistActionIdentitifier, title: String(localized: .persistAuthenticationDeclineButton), options: [])
var allPersistenceActions = [doNotPersistAction] var allPersistenceActions = [doNotPersistAction]
let formatter = DateComponentsFormatter() let formatter = DateComponentsFormatter()
formatter.unitsStyle = .spellOut formatter.unitsStyle = .spellOut
formatter.allowedUnits = [.hour, .minute, .day] formatter.allowedUnits = [.hour, .minute, .day]
var identifiers: [String: TimeInterval] = [:] var identifiers: [String: TimeInterval] = [:]
for duration in rawDurations { for duration in rawDurations {
let seconds = duration.converted(to: .seconds).value let seconds = duration.converted(to: .seconds).value
@@ -49,11 +43,16 @@ final class Notifier: Sendable {
if persistAuthenticationCategory.responds(to: Selector(("actionsMenuTitle"))) { if persistAuthenticationCategory.responds(to: Selector(("actionsMenuTitle"))) {
persistAuthenticationCategory.setValue(String(localized: .persistAuthenticationAcceptButton), forKey: "_actionsMenuTitle") persistAuthenticationCategory.setValue(String(localized: .persistAuthenticationAcceptButton), forKey: "_actionsMenuTitle")
} }
var categories = await UNUserNotificationCenter.current().notificationCategories() UNUserNotificationCenter.current().setNotificationCategories([updateCategory, criticalUpdateCategory, persistAuthenticationCategory])
categories.insert(persistAuthenticationCategory) UNUserNotificationCenter.current().delegate = notificationDelegate
UNUserNotificationCenter.current().setNotificationCategories(categories)
Task {
await notificationDelegate.state.setPersistenceState(options: identifiers) { secret, store, duration in
guard let duration = duration else { return }
try? await store.persistAuthentication(secret: secret, forDuration: duration)
}
}
await notificationDelegate.state.setPersistenceState(options: identifiers, action: action)
} }
func prompt() { func prompt() {
@@ -61,7 +60,7 @@ final class Notifier: Sendable {
notificationCenter.requestAuthorization(options: .alert) { _, _ in } notificationCenter.requestAuthorization(options: .alert) { _, _ in }
} }
func notify(accessTo secret: AnySecret, from store: AnySecretStore, by provenance: SigningRequestProvenance, offerPersistence: Bool) async { func notify(accessTo secret: AnySecret, from store: AnySecretStore, by provenance: SigningRequestProvenance) async {
await notificationDelegate.state.setPending(secret: secret, store: store) await notificationDelegate.state.setPending(secret: secret, store: store)
let notificationCenter = UNUserNotificationCenter.current() let notificationCenter = UNUserNotificationCenter.current()
let notificationContent = UNMutableNotificationContent() let notificationContent = UNMutableNotificationContent()
@@ -70,7 +69,7 @@ final class Notifier: Sendable {
notificationContent.userInfo[Constants.persistSecretIDKey] = secret.id.description notificationContent.userInfo[Constants.persistSecretIDKey] = secret.id.description
notificationContent.userInfo[Constants.persistStoreIDKey] = store.id.description notificationContent.userInfo[Constants.persistStoreIDKey] = store.id.description
notificationContent.interruptionLevel = .timeSensitive notificationContent.interruptionLevel = .timeSensitive
if offerPersistence { if await store.existingPersistedAuthenticationContext(secret: secret) == nil && secret.authenticationRequirement.required {
notificationContent.categoryIdentifier = Constants.persistAuthenticationCategoryIdentitifier notificationContent.categoryIdentifier = Constants.persistAuthenticationCategoryIdentitifier
} }
if let iconURL = provenance.origin.iconURL, let attachment = try? UNNotificationAttachment(identifier: "icon", url: iconURL, options: nil) { if let iconURL = provenance.origin.iconURL, let attachment = try? UNNotificationAttachment(identifier: "icon", url: iconURL, options: nil) {
@@ -104,8 +103,8 @@ extension Notifier: SigningWitness {
func speakNowOrForeverHoldYourPeace(forAccessTo secret: AnySecret, from store: AnySecretStore, by provenance: SigningRequestProvenance) async throws { func speakNowOrForeverHoldYourPeace(forAccessTo secret: AnySecret, from store: AnySecretStore, by provenance: SigningRequestProvenance) async throws {
} }
func witness(accessTo secret: AnySecret, from store: AnySecretStore, by provenance: SigningRequestProvenance, offerPersistence: Bool) async throws { func witness(accessTo secret: AnySecret, from store: AnySecretStore, by provenance: SigningRequestProvenance) async throws {
await notify(accessTo: secret, from: store, by: provenance, offerPersistence: offerPersistence) await notify(accessTo: secret, from: store, by: provenance)
} }
} }
@@ -134,24 +133,28 @@ extension Notifier {
final class NotificationDelegate: NSObject, UNUserNotificationCenterDelegate, Sendable { final class NotificationDelegate: NSObject, UNUserNotificationCenterDelegate, Sendable {
fileprivate actor State { fileprivate actor State {
typealias PersistAction = (@Sendable (AnySecret, AnySecretStore, TimeInterval?) async -> Void)
typealias IgnoreAction = (@Sendable (Release) async -> Void) typealias IgnoreAction = (@Sendable (Release) async -> Void)
fileprivate var release: Release? fileprivate var release: Release?
fileprivate var ignoreAction: IgnoreAction? fileprivate var ignoreAction: IgnoreAction?
fileprivate var persistAction: PersistAction? fileprivate var persistAction: PersistAction?
fileprivate var persistOptions: [String: TimeInterval] = [:] fileprivate var persistOptions: [String: TimeInterval] = [:]
fileprivate var pendingPersistableStores: [String: AnySecretStore] = [:]
fileprivate var pendingPersistableSecrets: [String: AnySecret] = [:] fileprivate var pendingPersistableSecrets: [String: AnySecret] = [:]
func setPending(secret: AnySecret, store: AnySecretStore) { func setPending(secret: AnySecret, store: AnySecretStore) {
pendingPersistableSecrets[secret.id.description] = secret pendingPersistableSecrets[secret.id.description] = secret
pendingPersistableStores[store.id.description] = store
} }
func retrievePending(secretID: String, optionID: String) -> (AnySecret, TimeInterval)? { func retrievePending(secretID: String, storeID: String, optionID: String) -> (AnySecret, AnySecretStore, TimeInterval)? {
guard let secret = pendingPersistableSecrets[secretID], guard let secret = pendingPersistableSecrets[secretID],
let store = pendingPersistableStores[storeID],
let options = persistOptions[optionID] else { let options = persistOptions[optionID] else {
return nil return nil
} }
pendingPersistableSecrets.removeValue(forKey: secretID) pendingPersistableSecrets.removeValue(forKey: secretID)
return (secret, options) return (secret, store, options)
} }
func setPersistenceState(options: [String: TimeInterval], action: @escaping PersistAction) { func setPersistenceState(options: [String: TimeInterval], action: @escaping PersistAction) {
@@ -199,12 +202,13 @@ final class NotificationDelegate: NSObject, UNUserNotificationCenterDelegate, Se
} }
func handlePersistAuthenticationResponse(response: UNNotificationResponse) async { func handlePersistAuthenticationResponse(response: UNNotificationResponse) async {
guard let secretID = response.notification.request.content.userInfo[Notifier.Constants.persistSecretIDKey] as? String else { guard let secretID = response.notification.request.content.userInfo[Notifier.Constants.persistSecretIDKey] as? String,
let storeID = response.notification.request.content.userInfo[Notifier.Constants.persistStoreIDKey] as? String else {
return return
} }
let optionID = response.actionIdentifier let optionID = response.actionIdentifier
guard let (secret, persistOptions) = await state.retrievePending(secretID: secretID, optionID: optionID) else { return } guard let (secret, store, persistOptions) = await state.retrievePending(secretID: secretID, storeID: storeID, optionID: optionID) else { return }
try? await state.persistAction?(secret, persistOptions) await state.persistAction?(secret, store, persistOptions)
} }

View File

@@ -16,10 +16,10 @@
<string>1</string> <string>1</string>
<key>com.apple.security.hardened-process.hardened-heap</key> <key>com.apple.security.hardened-process.hardened-heap</key>
<true/> <true/>
<key>com.apple.security.hardened-process.platform-restrictions-string</key>
<string>2</string>
<key>com.apple.security.smartcard</key> <key>com.apple.security.smartcard</key>
<true/> <true/>
<key>com.apple.security.hardened-process.platform-restrictions-string</key>
<string>2</string>
<key>keychain-access-groups</key> <key>keychain-access-groups</key>
<array> <array>
<string>$(AppIdentifierPrefix)com.maxgoedjen.Secretive</string> <string>$(AppIdentifierPrefix)com.maxgoedjen.Secretive</string>

View File

@@ -1,6 +1,5 @@
import Foundation import Foundation
import OSLog import SecretAgentKit
import SSHProtocolKit
import Brief import Brief
import XPCWrappers import XPCWrappers
import OSLog import OSLog

View File

@@ -8,7 +8,7 @@
/* Begin PBXBuildFile section */ /* Begin PBXBuildFile section */
2C4A9D2F2636FFD3008CC8E2 /* EditSecretView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2C4A9D2E2636FFD3008CC8E2 /* EditSecretView.swift */; }; 2C4A9D2F2636FFD3008CC8E2 /* EditSecretView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2C4A9D2E2636FFD3008CC8E2 /* EditSecretView.swift */; };
50020BB024064869003D4025 /* App.swift in Sources */ = {isa = PBXBuildFile; fileRef = 50020BAF24064869003D4025 /* App.swift */; }; 50020BB024064869003D4025 /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 50020BAF24064869003D4025 /* AppDelegate.swift */; };
5002C3AB2EEF483300FFAD22 /* XPCWrappers in Frameworks */ = {isa = PBXBuildFile; productRef = 5002C3AA2EEF483300FFAD22 /* XPCWrappers */; }; 5002C3AB2EEF483300FFAD22 /* XPCWrappers in Frameworks */ = {isa = PBXBuildFile; productRef = 5002C3AA2EEF483300FFAD22 /* XPCWrappers */; };
5003EF3B278005E800DF2006 /* SecretKit in Frameworks */ = {isa = PBXBuildFile; productRef = 5003EF3A278005E800DF2006 /* SecretKit */; }; 5003EF3B278005E800DF2006 /* SecretKit in Frameworks */ = {isa = PBXBuildFile; productRef = 5003EF3A278005E800DF2006 /* SecretKit */; };
5003EF3D278005F300DF2006 /* Brief in Frameworks */ = {isa = PBXBuildFile; productRef = 5003EF3C278005F300DF2006 /* Brief */; }; 5003EF3D278005F300DF2006 /* Brief in Frameworks */ = {isa = PBXBuildFile; productRef = 5003EF3C278005F300DF2006 /* Brief */; };
@@ -26,13 +26,11 @@
50153E22250DECA300525160 /* SecretListItemView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 50153E21250DECA300525160 /* SecretListItemView.swift */; }; 50153E22250DECA300525160 /* SecretListItemView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 50153E21250DECA300525160 /* SecretListItemView.swift */; };
501578132E6C0479004A37D0 /* XPCInputParser.swift in Sources */ = {isa = PBXBuildFile; fileRef = 501578122E6C0479004A37D0 /* XPCInputParser.swift */; }; 501578132E6C0479004A37D0 /* XPCInputParser.swift in Sources */ = {isa = PBXBuildFile; fileRef = 501578122E6C0479004A37D0 /* XPCInputParser.swift */; };
5018F54F24064786002EB505 /* Notifier.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5018F54E24064786002EB505 /* Notifier.swift */; }; 5018F54F24064786002EB505 /* Notifier.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5018F54E24064786002EB505 /* Notifier.swift */; };
503647482F870B7800977A23 /* BatchedRequestsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 503647472F870B7800977A23 /* BatchedRequestsView.swift */; };
504788F22E681F3A00B4556F /* Instructions.swift in Sources */ = {isa = PBXBuildFile; fileRef = 504788F12E681F3A00B4556F /* Instructions.swift */; }; 504788F22E681F3A00B4556F /* Instructions.swift in Sources */ = {isa = PBXBuildFile; fileRef = 504788F12E681F3A00B4556F /* Instructions.swift */; };
504788F42E681F6900B4556F /* ToolConfigurationView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 504788F32E681F6900B4556F /* ToolConfigurationView.swift */; }; 504788F42E681F6900B4556F /* ToolConfigurationView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 504788F32E681F6900B4556F /* ToolConfigurationView.swift */; };
504788F62E68206F00B4556F /* GettingStartedView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 504788F52E68206F00B4556F /* GettingStartedView.swift */; }; 504788F62E68206F00B4556F /* GettingStartedView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 504788F52E68206F00B4556F /* GettingStartedView.swift */; };
504789232E697DD300B4556F /* BoxBackgroundStyle.swift in Sources */ = {isa = PBXBuildFile; fileRef = 504789222E697DD300B4556F /* BoxBackgroundStyle.swift */; }; 504789232E697DD300B4556F /* BoxBackgroundStyle.swift in Sources */ = {isa = PBXBuildFile; fileRef = 504789222E697DD300B4556F /* BoxBackgroundStyle.swift */; };
50571E0324393C2600F76F6C /* JustUpdatedChecker.swift in Sources */ = {isa = PBXBuildFile; fileRef = 50571E0224393C2600F76F6C /* JustUpdatedChecker.swift */; }; 50571E0324393C2600F76F6C /* JustUpdatedChecker.swift in Sources */ = {isa = PBXBuildFile; fileRef = 50571E0224393C2600F76F6C /* JustUpdatedChecker.swift */; };
505F5EF22FA9635700C45824 /* CertificateKit in Frameworks */ = {isa = PBXBuildFile; productRef = 505F5EF12FA9635700C45824 /* CertificateKit */; };
50617D8323FCE48E0099B055 /* App.swift in Sources */ = {isa = PBXBuildFile; fileRef = 50617D8223FCE48E0099B055 /* App.swift */; }; 50617D8323FCE48E0099B055 /* App.swift in Sources */ = {isa = PBXBuildFile; fileRef = 50617D8223FCE48E0099B055 /* App.swift */; };
50617D8523FCE48E0099B055 /* ContentView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 50617D8423FCE48E0099B055 /* ContentView.swift */; }; 50617D8523FCE48E0099B055 /* ContentView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 50617D8423FCE48E0099B055 /* ContentView.swift */; };
50617D8A23FCE48E0099B055 /* Preview Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 50617D8923FCE48E0099B055 /* Preview Assets.xcassets */; }; 50617D8A23FCE48E0099B055 /* Preview Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 50617D8923FCE48E0099B055 /* Preview Assets.xcassets */; };
@@ -73,23 +71,6 @@
50CF4ABC2E601B0F005588DC /* ActionButtonStyle.swift in Sources */ = {isa = PBXBuildFile; fileRef = 50CF4ABB2E601B0F005588DC /* ActionButtonStyle.swift */; }; 50CF4ABC2E601B0F005588DC /* ActionButtonStyle.swift in Sources */ = {isa = PBXBuildFile; fileRef = 50CF4ABB2E601B0F005588DC /* ActionButtonStyle.swift */; };
50E0145C2EDB9CDF00B121F1 /* Common in Frameworks */ = {isa = PBXBuildFile; productRef = 50E0145B2EDB9CDF00B121F1 /* Common */; }; 50E0145C2EDB9CDF00B121F1 /* Common in Frameworks */ = {isa = PBXBuildFile; productRef = 50E0145B2EDB9CDF00B121F1 /* Common */; };
50E0145E2EDB9CE400B121F1 /* Common in Frameworks */ = {isa = PBXBuildFile; productRef = 50E0145D2EDB9CE400B121F1 /* Common */; }; 50E0145E2EDB9CE400B121F1 /* Common in Frameworks */ = {isa = PBXBuildFile; productRef = 50E0145D2EDB9CE400B121F1 /* Common */; };
50E204E92FA9D12700402380 /* CertificateDetailView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 50E204E82FA9D12700402380 /* CertificateDetailView.swift */; };
50E204ED2FAA997F00402380 /* CertificateListItemView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 50E204EC2FAA997F00402380 /* CertificateListItemView.swift */; };
50E204EF2FAA9C1400402380 /* MultilineInfoView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 50E204EE2FAA9C1400402380 /* MultilineInfoView.swift */; };
50E2051D2FAAB81C00402380 /* SecretiveCertificateParser.xpc in Embed XPC Services */ = {isa = PBXBuildFile; fileRef = 50E205142FAAB81C00402380 /* SecretiveCertificateParser.xpc */; settings = {ATTRIBUTES = (RemoveHeadersOnCopy, ); }; };
50E205282FAAB82700402380 /* main.swift in Sources */ = {isa = PBXBuildFile; fileRef = 50E205242FAAB82700402380 /* main.swift */; };
50E2052C2FAAB85000402380 /* SecretiveCertificateParser.swift in Sources */ = {isa = PBXBuildFile; fileRef = 50E2052B2FAAB85000402380 /* SecretiveCertificateParser.swift */; };
50E2052D2FAAB92000402380 /* SecretiveCertificateParser.xpc in Embed XPC Services */ = {isa = PBXBuildFile; fileRef = 50E205142FAAB81C00402380 /* SecretiveCertificateParser.xpc */; settings = {ATTRIBUTES = (RemoveHeadersOnCopy, ); }; };
50E205312FAAB95500402380 /* XPCWrappers in Frameworks */ = {isa = PBXBuildFile; productRef = 50E205302FAAB95500402380 /* XPCWrappers */; };
50E205332FAAB95A00402380 /* SSHProtocolKit in Frameworks */ = {isa = PBXBuildFile; productRef = 50E205322FAAB95A00402380 /* SSHProtocolKit */; };
50E205362FAABC6300402380 /* EditCertificateView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 50E205352FAABC6300402380 /* EditCertificateView.swift */; };
50E205372FAABC6300402380 /* DeleteCertificateView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 50E205342FAABC6300402380 /* DeleteCertificateView.swift */; };
50E205802FAB291E00402380 /* CertificateMigrator.swift in Sources */ = {isa = PBXBuildFile; fileRef = 50E2057F2FAB291E00402380 /* CertificateMigrator.swift */; };
50E205822FAB293B00402380 /* SharedXPCServices in Frameworks */ = {isa = PBXBuildFile; productRef = 50E205812FAB293B00402380 /* SharedXPCServices */; };
50E205842FAB296A00402380 /* SharedXPCServices in Frameworks */ = {isa = PBXBuildFile; productRef = 50E205832FAB296A00402380 /* SharedXPCServices */; };
50E205862FAC2EA000402380 /* Formatters in Frameworks */ = {isa = PBXBuildFile; productRef = 50E205852FAC2EA000402380 /* Formatters */; };
50E205882FAC2EAB00402380 /* Formatters in Frameworks */ = {isa = PBXBuildFile; productRef = 50E205872FAC2EAB00402380 /* Formatters */; };
50E2058A2FAC2EB600402380 /* Formatters in Frameworks */ = {isa = PBXBuildFile; productRef = 50E205892FAC2EB600402380 /* Formatters */; };
50E4C4532E73C78C00C73783 /* WindowBackgroundStyle.swift in Sources */ = {isa = PBXBuildFile; fileRef = 50E4C4522E73C78900C73783 /* WindowBackgroundStyle.swift */; }; 50E4C4532E73C78C00C73783 /* WindowBackgroundStyle.swift in Sources */ = {isa = PBXBuildFile; fileRef = 50E4C4522E73C78900C73783 /* WindowBackgroundStyle.swift */; };
50E4C4C32E7765DF00C73783 /* AboutView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 50E4C4C22E7765DF00C73783 /* AboutView.swift */; }; 50E4C4C32E7765DF00C73783 /* AboutView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 50E4C4C22E7765DF00C73783 /* AboutView.swift */; };
50E4C4C82E777E4200C73783 /* AppIcon.icon in Resources */ = {isa = PBXBuildFile; fileRef = 50E4C4C72E777E4200C73783 /* AppIcon.icon */; }; 50E4C4C82E777E4200C73783 /* AppIcon.icon in Resources */ = {isa = PBXBuildFile; fileRef = 50E4C4C72E777E4200C73783 /* AppIcon.icon */; };
@@ -139,20 +120,6 @@
remoteGlobalIDString = 50692E4F2E6FF9D20043C7BB; remoteGlobalIDString = 50692E4F2E6FF9D20043C7BB;
remoteInfo = SecretAgentInputParser; remoteInfo = SecretAgentInputParser;
}; };
50E2051B2FAAB81C00402380 /* PBXContainerItemProxy */ = {
isa = PBXContainerItemProxy;
containerPortal = 50617D7723FCE48D0099B055 /* Project object */;
proxyType = 1;
remoteGlobalIDString = 50E205132FAAB81C00402380;
remoteInfo = SecretAgentCertificateParser;
};
50E2052E2FAAB92000402380 /* PBXContainerItemProxy */ = {
isa = PBXContainerItemProxy;
containerPortal = 50617D7723FCE48D0099B055 /* Project object */;
proxyType = 1;
remoteGlobalIDString = 50E205132FAAB81C00402380;
remoteInfo = SecretiveCertificateParser;
};
/* End PBXContainerItemProxy section */ /* End PBXContainerItemProxy section */
/* Begin PBXCopyFilesBuildPhase section */ /* Begin PBXCopyFilesBuildPhase section */
@@ -162,7 +129,6 @@
dstPath = "$(CONTENTS_FOLDER_PATH)/XPCServices"; dstPath = "$(CONTENTS_FOLDER_PATH)/XPCServices";
dstSubfolderSpec = 16; dstSubfolderSpec = 16;
files = ( files = (
50E2051D2FAAB81C00402380 /* SecretiveCertificateParser.xpc in Embed XPC Services */,
50692D1D2E6FDB880043C7BB /* SecretiveUpdater.xpc in Embed XPC Services */, 50692D1D2E6FDB880043C7BB /* SecretiveUpdater.xpc in Embed XPC Services */,
50692E5B2E6FF9D20043C7BB /* SecretAgentInputParser.xpc in Embed XPC Services */, 50692E5B2E6FF9D20043C7BB /* SecretAgentInputParser.xpc in Embed XPC Services */,
); );
@@ -176,7 +142,6 @@
dstSubfolderSpec = 16; dstSubfolderSpec = 16;
files = ( files = (
50692E6D2E6FFA5F0043C7BB /* SecretiveUpdater.xpc in Embed XPC Services */, 50692E6D2E6FFA5F0043C7BB /* SecretiveUpdater.xpc in Embed XPC Services */,
50E2052D2FAAB92000402380 /* SecretiveCertificateParser.xpc in Embed XPC Services */,
50692E702E6FFA6E0043C7BB /* SecretAgentInputParser.xpc in Embed XPC Services */, 50692E702E6FFA6E0043C7BB /* SecretAgentInputParser.xpc in Embed XPC Services */,
); );
name = "Embed XPC Services"; name = "Embed XPC Services";
@@ -216,7 +181,7 @@
/* Begin PBXFileReference section */ /* Begin PBXFileReference section */
2C4A9D2E2636FFD3008CC8E2 /* EditSecretView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EditSecretView.swift; sourceTree = "<group>"; }; 2C4A9D2E2636FFD3008CC8E2 /* EditSecretView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EditSecretView.swift; sourceTree = "<group>"; };
50020BAF24064869003D4025 /* App.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = App.swift; sourceTree = "<group>"; }; 50020BAF24064869003D4025 /* AppDelegate.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = "<group>"; };
5003EF39278005C800DF2006 /* Packages */ = {isa = PBXFileReference; lastKnownFileType = wrapper; path = Packages; sourceTree = "<group>"; }; 5003EF39278005C800DF2006 /* Packages */ = {isa = PBXFileReference; lastKnownFileType = wrapper; path = Packages; sourceTree = "<group>"; };
500666D02F04786900328939 /* SecretiveUpdater.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = SecretiveUpdater.entitlements; sourceTree = "<group>"; }; 500666D02F04786900328939 /* SecretiveUpdater.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = SecretiveUpdater.entitlements; sourceTree = "<group>"; };
500666D12F04787200328939 /* SecretAgentInputParser.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = SecretAgentInputParser.entitlements; sourceTree = "<group>"; }; 500666D12F04787200328939 /* SecretAgentInputParser.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = SecretAgentInputParser.entitlements; sourceTree = "<group>"; };
@@ -225,7 +190,6 @@
50153E21250DECA300525160 /* SecretListItemView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SecretListItemView.swift; sourceTree = "<group>"; }; 50153E21250DECA300525160 /* SecretListItemView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SecretListItemView.swift; sourceTree = "<group>"; };
501578122E6C0479004A37D0 /* XPCInputParser.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = XPCInputParser.swift; sourceTree = "<group>"; }; 501578122E6C0479004A37D0 /* XPCInputParser.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = XPCInputParser.swift; sourceTree = "<group>"; };
5018F54E24064786002EB505 /* Notifier.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Notifier.swift; sourceTree = "<group>"; }; 5018F54E24064786002EB505 /* Notifier.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Notifier.swift; sourceTree = "<group>"; };
503647472F870B7800977A23 /* BatchedRequestsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BatchedRequestsView.swift; sourceTree = "<group>"; };
504788F12E681F3A00B4556F /* Instructions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Instructions.swift; sourceTree = "<group>"; }; 504788F12E681F3A00B4556F /* Instructions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Instructions.swift; sourceTree = "<group>"; };
504788F32E681F6900B4556F /* ToolConfigurationView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ToolConfigurationView.swift; sourceTree = "<group>"; }; 504788F32E681F6900B4556F /* ToolConfigurationView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ToolConfigurationView.swift; sourceTree = "<group>"; };
504788F52E68206F00B4556F /* GettingStartedView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GettingStartedView.swift; sourceTree = "<group>"; }; 504788F52E68206F00B4556F /* GettingStartedView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GettingStartedView.swift; sourceTree = "<group>"; };
@@ -274,17 +238,6 @@
50BDCB752E6450950072D2E7 /* ConfigurationItemView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ConfigurationItemView.swift; sourceTree = "<group>"; }; 50BDCB752E6450950072D2E7 /* ConfigurationItemView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ConfigurationItemView.swift; sourceTree = "<group>"; };
50C385A42407A76D00AF2719 /* SecretDetailView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SecretDetailView.swift; sourceTree = "<group>"; }; 50C385A42407A76D00AF2719 /* SecretDetailView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SecretDetailView.swift; sourceTree = "<group>"; };
50CF4ABB2E601B0F005588DC /* ActionButtonStyle.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ActionButtonStyle.swift; sourceTree = "<group>"; }; 50CF4ABB2E601B0F005588DC /* ActionButtonStyle.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ActionButtonStyle.swift; sourceTree = "<group>"; };
50E204E82FA9D12700402380 /* CertificateDetailView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CertificateDetailView.swift; sourceTree = "<group>"; };
50E204EC2FAA997F00402380 /* CertificateListItemView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CertificateListItemView.swift; sourceTree = "<group>"; };
50E204EE2FAA9C1400402380 /* MultilineInfoView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MultilineInfoView.swift; sourceTree = "<group>"; };
50E205142FAAB81C00402380 /* SecretiveCertificateParser.xpc */ = {isa = PBXFileReference; explicitFileType = "wrapper.xpc-service"; includeInIndex = 0; path = SecretiveCertificateParser.xpc; sourceTree = BUILT_PRODUCTS_DIR; };
50E205232FAAB82700402380 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = "<group>"; };
50E205242FAAB82700402380 /* main.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = main.swift; sourceTree = "<group>"; };
50E2052A2FAAB85000402380 /* SecretiveCertificateParser.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = SecretiveCertificateParser.entitlements; sourceTree = "<group>"; };
50E2052B2FAAB85000402380 /* SecretiveCertificateParser.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SecretiveCertificateParser.swift; sourceTree = "<group>"; };
50E205342FAABC6300402380 /* DeleteCertificateView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DeleteCertificateView.swift; sourceTree = "<group>"; };
50E205352FAABC6300402380 /* EditCertificateView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EditCertificateView.swift; sourceTree = "<group>"; };
50E2057F2FAB291E00402380 /* CertificateMigrator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CertificateMigrator.swift; sourceTree = "<group>"; };
50E4C4522E73C78900C73783 /* WindowBackgroundStyle.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WindowBackgroundStyle.swift; sourceTree = "<group>"; }; 50E4C4522E73C78900C73783 /* WindowBackgroundStyle.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WindowBackgroundStyle.swift; sourceTree = "<group>"; };
50E4C4C22E7765DF00C73783 /* AboutView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AboutView.swift; sourceTree = "<group>"; }; 50E4C4C22E7765DF00C73783 /* AboutView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AboutView.swift; sourceTree = "<group>"; };
50E4C4C72E777E4200C73783 /* AppIcon.icon */ = {isa = PBXFileReference; lastKnownFileType = folder.iconcomposer.icon; path = AppIcon.icon; sourceTree = "<group>"; }; 50E4C4C72E777E4200C73783 /* AppIcon.icon */ = {isa = PBXFileReference; lastKnownFileType = folder.iconcomposer.icon; path = AppIcon.icon; sourceTree = "<group>"; };
@@ -297,13 +250,10 @@
buildActionMask = 2147483647; buildActionMask = 2147483647;
files = ( files = (
50E0145C2EDB9CDF00B121F1 /* Common in Frameworks */, 50E0145C2EDB9CDF00B121F1 /* Common in Frameworks */,
50E2058A2FAC2EB600402380 /* Formatters in Frameworks */,
5003EF3B278005E800DF2006 /* SecretKit in Frameworks */, 5003EF3B278005E800DF2006 /* SecretKit in Frameworks */,
501421622781262300BBAA70 /* Brief in Frameworks */, 501421622781262300BBAA70 /* Brief in Frameworks */,
50E205842FAB296A00402380 /* SharedXPCServices in Frameworks */,
5003EF5F2780081600DF2006 /* SecureEnclaveSecretKit in Frameworks */, 5003EF5F2780081600DF2006 /* SecureEnclaveSecretKit in Frameworks */,
5003EF612780081600DF2006 /* SmartCardSecretKit in Frameworks */, 5003EF612780081600DF2006 /* SmartCardSecretKit in Frameworks */,
505F5EF22FA9635700C45824 /* CertificateKit in Frameworks */,
); );
runOnlyForDeploymentPostprocessing = 0; runOnlyForDeploymentPostprocessing = 0;
}; };
@@ -311,7 +261,6 @@
isa = PBXFrameworksBuildPhase; isa = PBXFrameworksBuildPhase;
buildActionMask = 2147483647; buildActionMask = 2147483647;
files = ( files = (
50E205862FAC2EA000402380 /* Formatters in Frameworks */,
50692D2D2E6FDC000043C7BB /* XPCWrappers in Frameworks */, 50692D2D2E6FDC000043C7BB /* XPCWrappers in Frameworks */,
50692D312E6FDC390043C7BB /* Brief in Frameworks */, 50692D312E6FDC390043C7BB /* Brief in Frameworks */,
); );
@@ -333,23 +282,12 @@
5003EF3D278005F300DF2006 /* Brief in Frameworks */, 5003EF3D278005F300DF2006 /* Brief in Frameworks */,
5003EF632780081B00DF2006 /* SecureEnclaveSecretKit in Frameworks */, 5003EF632780081B00DF2006 /* SecureEnclaveSecretKit in Frameworks */,
5003EF652780081B00DF2006 /* SmartCardSecretKit in Frameworks */, 5003EF652780081B00DF2006 /* SmartCardSecretKit in Frameworks */,
50E205822FAB293B00402380 /* SharedXPCServices in Frameworks */,
5003EF3F278005F300DF2006 /* SecretAgentKit in Frameworks */, 5003EF3F278005F300DF2006 /* SecretAgentKit in Frameworks */,
5003EF41278005FA00DF2006 /* SecretKit in Frameworks */, 5003EF41278005FA00DF2006 /* SecretKit in Frameworks */,
50E0145E2EDB9CE400B121F1 /* Common in Frameworks */, 50E0145E2EDB9CE400B121F1 /* Common in Frameworks */,
); );
runOnlyForDeploymentPostprocessing = 0; runOnlyForDeploymentPostprocessing = 0;
}; };
50E205112FAAB81C00402380 /* Frameworks */ = {
isa = PBXFrameworksBuildPhase;
buildActionMask = 2147483647;
files = (
50E205882FAC2EAB00402380 /* Formatters in Frameworks */,
50E205332FAAB95A00402380 /* SSHProtocolKit in Frameworks */,
50E205312FAAB95500402380 /* XPCWrappers in Frameworks */,
);
runOnlyForDeploymentPostprocessing = 0;
};
/* End PBXFrameworksBuildPhase section */ /* End PBXFrameworksBuildPhase section */
/* Begin PBXGroup section */ /* Begin PBXGroup section */
@@ -371,14 +309,10 @@
5099A02323FD2AAA0062B6F2 /* CreateSecretView.swift */, 5099A02323FD2AAA0062B6F2 /* CreateSecretView.swift */,
50B8550C24138C4F009958AC /* DeleteSecretView.swift */, 50B8550C24138C4F009958AC /* DeleteSecretView.swift */,
2C4A9D2E2636FFD3008CC8E2 /* EditSecretView.swift */, 2C4A9D2E2636FFD3008CC8E2 /* EditSecretView.swift */,
50E205342FAABC6300402380 /* DeleteCertificateView.swift */,
50E205352FAABC6300402380 /* EditCertificateView.swift */,
50BB046A2418AAAE00D6E079 /* EmptyStoreView.swift */, 50BB046A2418AAAE00D6E079 /* EmptyStoreView.swift */,
506772C82425BB8500034DED /* NoStoresView.swift */, 506772C82425BB8500034DED /* NoStoresView.swift */,
50C385A42407A76D00AF2719 /* SecretDetailView.swift */, 50C385A42407A76D00AF2719 /* SecretDetailView.swift */,
50E204E82FA9D12700402380 /* CertificateDetailView.swift */,
50153E21250DECA300525160 /* SecretListItemView.swift */, 50153E21250DECA300525160 /* SecretListItemView.swift */,
50E204EC2FAA997F00402380 /* CertificateListItemView.swift */,
5079BA0E250F29BF00EA86F4 /* StoreListView.swift */, 5079BA0E250F29BF00EA86F4 /* StoreListView.swift */,
); );
path = Secrets; path = Secrets;
@@ -404,7 +338,6 @@
50BDCB712E63BAF20072D2E7 /* AgentStatusView.swift */, 50BDCB712E63BAF20072D2E7 /* AgentStatusView.swift */,
50617D8423FCE48E0099B055 /* ContentView.swift */, 50617D8423FCE48E0099B055 /* ContentView.swift */,
5066A6C72516FE6E004B5A36 /* CopyableView.swift */, 5066A6C72516FE6E004B5A36 /* CopyableView.swift */,
50E204EE2FAA9C1400402380 /* MultilineInfoView.swift */,
50153E1F250AFCB200525160 /* UpdateView.swift */, 50153E1F250AFCB200525160 /* UpdateView.swift */,
); );
path = Views; path = Views;
@@ -419,7 +352,6 @@
508A58AF241E144C0069DC07 /* Config */, 508A58AF241E144C0069DC07 /* Config */,
50692D272E6FDB8D0043C7BB /* SecretiveUpdater */, 50692D272E6FDB8D0043C7BB /* SecretiveUpdater */,
50692E662E6FF9E20043C7BB /* SecretAgentInputParser */, 50692E662E6FF9E20043C7BB /* SecretAgentInputParser */,
50E205262FAAB82700402380 /* SecretiveCertificateParser */,
50617D8023FCE48E0099B055 /* Products */, 50617D8023FCE48E0099B055 /* Products */,
5099A08B240243730062B6F2 /* Frameworks */, 5099A08B240243730062B6F2 /* Frameworks */,
); );
@@ -432,7 +364,6 @@
50A3B78A24026B7500D209EA /* SecretAgent.app */, 50A3B78A24026B7500D209EA /* SecretAgent.app */,
50692D122E6FDB880043C7BB /* SecretiveUpdater.xpc */, 50692D122E6FDB880043C7BB /* SecretiveUpdater.xpc */,
50692E502E6FF9D20043C7BB /* SecretAgentInputParser.xpc */, 50692E502E6FF9D20043C7BB /* SecretAgentInputParser.xpc */,
50E205142FAAB81C00402380 /* SecretiveCertificateParser.xpc */,
); );
name = Products; name = Products;
sourceTree = "<group>"; sourceTree = "<group>";
@@ -528,11 +459,9 @@
50A3B78B24026B7500D209EA /* SecretAgent */ = { 50A3B78B24026B7500D209EA /* SecretAgent */ = {
isa = PBXGroup; isa = PBXGroup;
children = ( children = (
50020BAF24064869003D4025 /* App.swift */, 50020BAF24064869003D4025 /* AppDelegate.swift */,
5018F54E24064786002EB505 /* Notifier.swift */, 5018F54E24064786002EB505 /* Notifier.swift */,
501578122E6C0479004A37D0 /* XPCInputParser.swift */, 501578122E6C0479004A37D0 /* XPCInputParser.swift */,
503647472F870B7800977A23 /* BatchedRequestsView.swift */,
50E2057F2FAB291E00402380 /* CertificateMigrator.swift */,
50A3B79524026B7600D209EA /* Main.storyboard */, 50A3B79524026B7600D209EA /* Main.storyboard */,
50A3B79824026B7600D209EA /* Info.plist */, 50A3B79824026B7600D209EA /* Info.plist */,
508BF29425B4F140009EFB7E /* InternetAccessPolicy.plist */, 508BF29425B4F140009EFB7E /* InternetAccessPolicy.plist */,
@@ -550,17 +479,6 @@
path = "Preview Content"; path = "Preview Content";
sourceTree = "<group>"; sourceTree = "<group>";
}; };
50E205262FAAB82700402380 /* SecretiveCertificateParser */ = {
isa = PBXGroup;
children = (
50E205232FAAB82700402380 /* Info.plist */,
50E205242FAAB82700402380 /* main.swift */,
50E2052A2FAAB85000402380 /* SecretiveCertificateParser.entitlements */,
50E2052B2FAAB85000402380 /* SecretiveCertificateParser.swift */,
);
path = SecretiveCertificateParser;
sourceTree = "<group>";
};
/* End PBXGroup section */ /* End PBXGroup section */
/* Begin PBXNativeTarget section */ /* Begin PBXNativeTarget section */
@@ -581,7 +499,6 @@
50142167278126B500BBAA70 /* PBXTargetDependency */, 50142167278126B500BBAA70 /* PBXTargetDependency */,
50692D1C2E6FDB880043C7BB /* PBXTargetDependency */, 50692D1C2E6FDB880043C7BB /* PBXTargetDependency */,
50692E5A2E6FF9D20043C7BB /* PBXTargetDependency */, 50692E5A2E6FF9D20043C7BB /* PBXTargetDependency */,
50E2051C2FAAB81C00402380 /* PBXTargetDependency */,
); );
name = Secretive; name = Secretive;
packageProductDependencies = ( packageProductDependencies = (
@@ -590,9 +507,6 @@
5003EF602780081600DF2006 /* SmartCardSecretKit */, 5003EF602780081600DF2006 /* SmartCardSecretKit */,
501421612781262300BBAA70 /* Brief */, 501421612781262300BBAA70 /* Brief */,
50E0145B2EDB9CDF00B121F1 /* Common */, 50E0145B2EDB9CDF00B121F1 /* Common */,
505F5EF12FA9635700C45824 /* CertificateKit */,
50E205832FAB296A00402380 /* SharedXPCServices */,
50E205892FAC2EB600402380 /* Formatters */,
); );
productName = Secretive; productName = Secretive;
productReference = 50617D7F23FCE48E0099B055 /* Secretive.app */; productReference = 50617D7F23FCE48E0099B055 /* Secretive.app */;
@@ -614,7 +528,6 @@
packageProductDependencies = ( packageProductDependencies = (
50692D2C2E6FDC000043C7BB /* XPCWrappers */, 50692D2C2E6FDC000043C7BB /* XPCWrappers */,
50692D302E6FDC390043C7BB /* Brief */, 50692D302E6FDC390043C7BB /* Brief */,
50E205852FAC2EA000402380 /* Formatters */,
); );
productName = SecretiveUpdater; productName = SecretiveUpdater;
productReference = 50692D122E6FDB880043C7BB /* SecretiveUpdater.xpc */; productReference = 50692D122E6FDB880043C7BB /* SecretiveUpdater.xpc */;
@@ -657,7 +570,6 @@
501577D42E6BC5DD004A37D0 /* PBXTargetDependency */, 501577D42E6BC5DD004A37D0 /* PBXTargetDependency */,
50692E6F2E6FFA5F0043C7BB /* PBXTargetDependency */, 50692E6F2E6FFA5F0043C7BB /* PBXTargetDependency */,
50692E722E6FFA6E0043C7BB /* PBXTargetDependency */, 50692E722E6FFA6E0043C7BB /* PBXTargetDependency */,
50E2052F2FAAB92000402380 /* PBXTargetDependency */,
); );
name = SecretAgent; name = SecretAgent;
packageProductDependencies = ( packageProductDependencies = (
@@ -667,34 +579,11 @@
5003EF622780081B00DF2006 /* SecureEnclaveSecretKit */, 5003EF622780081B00DF2006 /* SecureEnclaveSecretKit */,
5003EF642780081B00DF2006 /* SmartCardSecretKit */, 5003EF642780081B00DF2006 /* SmartCardSecretKit */,
50E0145D2EDB9CE400B121F1 /* Common */, 50E0145D2EDB9CE400B121F1 /* Common */,
50E205812FAB293B00402380 /* SharedXPCServices */,
); );
productName = SecretAgent; productName = SecretAgent;
productReference = 50A3B78A24026B7500D209EA /* SecretAgent.app */; productReference = 50A3B78A24026B7500D209EA /* SecretAgent.app */;
productType = "com.apple.product-type.application"; productType = "com.apple.product-type.application";
}; };
50E205132FAAB81C00402380 /* SecretiveCertificateParser */ = {
isa = PBXNativeTarget;
buildConfigurationList = 50E2051F2FAAB81C00402380 /* Build configuration list for PBXNativeTarget "SecretiveCertificateParser" */;
buildPhases = (
50E205102FAAB81C00402380 /* Sources */,
50E205112FAAB81C00402380 /* Frameworks */,
50E205122FAAB81C00402380 /* Resources */,
);
buildRules = (
);
dependencies = (
);
name = SecretiveCertificateParser;
packageProductDependencies = (
50E205302FAAB95500402380 /* XPCWrappers */,
50E205322FAAB95A00402380 /* SSHProtocolKit */,
50E205872FAC2EAB00402380 /* Formatters */,
);
productName = SecretAgentCertificateParser;
productReference = 50E205142FAAB81C00402380 /* SecretiveCertificateParser.xpc */;
productType = "com.apple.product-type.xpc-service";
};
/* End PBXNativeTarget section */ /* End PBXNativeTarget section */
/* Begin PBXProject section */ /* Begin PBXProject section */
@@ -702,7 +591,7 @@
isa = PBXProject; isa = PBXProject;
attributes = { attributes = {
BuildIndependentTargetsInParallel = YES; BuildIndependentTargetsInParallel = YES;
LastSwiftUpdateCheck = 2650; LastSwiftUpdateCheck = 2600;
LastUpgradeCheck = 2640; LastUpgradeCheck = 2640;
ORGANIZATIONNAME = "Max Goedjen"; ORGANIZATIONNAME = "Max Goedjen";
TargetAttributes = { TargetAttributes = {
@@ -718,9 +607,6 @@
50A3B78924026B7500D209EA = { 50A3B78924026B7500D209EA = {
CreatedOnToolsVersion = 11.4; CreatedOnToolsVersion = 11.4;
}; };
50E205132FAAB81C00402380 = {
CreatedOnToolsVersion = 26.5;
};
}; };
}; };
buildConfigurationList = 50617D7A23FCE48D0099B055 /* Build configuration list for PBXProject "Secretive" */; buildConfigurationList = 50617D7A23FCE48D0099B055 /* Build configuration list for PBXProject "Secretive" */;
@@ -748,7 +634,6 @@
50A3B78924026B7500D209EA /* SecretAgent */, 50A3B78924026B7500D209EA /* SecretAgent */,
50692D112E6FDB880043C7BB /* SecretiveUpdater */, 50692D112E6FDB880043C7BB /* SecretiveUpdater */,
50692E4F2E6FF9D20043C7BB /* SecretAgentInputParser */, 50692E4F2E6FF9D20043C7BB /* SecretAgentInputParser */,
50E205132FAAB81C00402380 /* SecretiveCertificateParser */,
); );
}; };
/* End PBXProject section */ /* End PBXProject section */
@@ -792,13 +677,6 @@
); );
runOnlyForDeploymentPostprocessing = 0; runOnlyForDeploymentPostprocessing = 0;
}; };
50E205122FAAB81C00402380 /* Resources */ = {
isa = PBXResourcesBuildPhase;
buildActionMask = 2147483647;
files = (
);
runOnlyForDeploymentPostprocessing = 0;
};
/* End PBXResourcesBuildPhase section */ /* End PBXResourcesBuildPhase section */
/* Begin PBXSourcesBuildPhase section */ /* Begin PBXSourcesBuildPhase section */
@@ -811,16 +689,13 @@
50E4C4C32E7765DF00C73783 /* AboutView.swift in Sources */, 50E4C4C32E7765DF00C73783 /* AboutView.swift in Sources */,
2C4A9D2F2636FFD3008CC8E2 /* EditSecretView.swift in Sources */, 2C4A9D2F2636FFD3008CC8E2 /* EditSecretView.swift in Sources */,
50E4C4532E73C78C00C73783 /* WindowBackgroundStyle.swift in Sources */, 50E4C4532E73C78C00C73783 /* WindowBackgroundStyle.swift in Sources */,
50E204E92FA9D12700402380 /* CertificateDetailView.swift in Sources */,
5091D2BC25183B830049FD9B /* ApplicationDirectoryController.swift in Sources */, 5091D2BC25183B830049FD9B /* ApplicationDirectoryController.swift in Sources */,
504789232E697DD300B4556F /* BoxBackgroundStyle.swift in Sources */, 504789232E697DD300B4556F /* BoxBackgroundStyle.swift in Sources */,
50E204EF2FAA9C1400402380 /* MultilineInfoView.swift in Sources */,
5066A6C22516F303004B5A36 /* SetupView.swift in Sources */, 5066A6C22516F303004B5A36 /* SetupView.swift in Sources */,
5065E313295517C500E16645 /* ToolbarButtonStyle.swift in Sources */, 5065E313295517C500E16645 /* ToolbarButtonStyle.swift in Sources */,
50617D8523FCE48E0099B055 /* ContentView.swift in Sources */, 50617D8523FCE48E0099B055 /* ContentView.swift in Sources */,
504788F62E68206F00B4556F /* GettingStartedView.swift in Sources */, 504788F62E68206F00B4556F /* GettingStartedView.swift in Sources */,
50CF4ABC2E601B0F005588DC /* ActionButtonStyle.swift in Sources */, 50CF4ABC2E601B0F005588DC /* ActionButtonStyle.swift in Sources */,
50E204ED2FAA997F00402380 /* CertificateListItemView.swift in Sources */,
50571E0324393C2600F76F6C /* JustUpdatedChecker.swift in Sources */, 50571E0324393C2600F76F6C /* JustUpdatedChecker.swift in Sources */,
5079BA0F250F29BF00EA86F4 /* StoreListView.swift in Sources */, 5079BA0F250F29BF00EA86F4 /* StoreListView.swift in Sources */,
50617DD223FCEFA90099B055 /* PreviewStore.swift in Sources */, 50617DD223FCEFA90099B055 /* PreviewStore.swift in Sources */,
@@ -833,14 +708,12 @@
5066A6C82516FE6E004B5A36 /* CopyableView.swift in Sources */, 5066A6C82516FE6E004B5A36 /* CopyableView.swift in Sources */,
50B8550D24138C4F009958AC /* DeleteSecretView.swift in Sources */, 50B8550D24138C4F009958AC /* DeleteSecretView.swift in Sources */,
50BB046B2418AAAE00D6E079 /* EmptyStoreView.swift in Sources */, 50BB046B2418AAAE00D6E079 /* EmptyStoreView.swift in Sources */,
50E205372FAABC6300402380 /* DeleteCertificateView.swift in Sources */,
50BDCB762E6450950072D2E7 /* ConfigurationItemView.swift in Sources */, 50BDCB762E6450950072D2E7 /* ConfigurationItemView.swift in Sources */,
50617D8323FCE48E0099B055 /* App.swift in Sources */, 50617D8323FCE48E0099B055 /* App.swift in Sources */,
504788F42E681F6900B4556F /* ToolConfigurationView.swift in Sources */, 504788F42E681F6900B4556F /* ToolConfigurationView.swift in Sources */,
506772C92425BB8500034DED /* NoStoresView.swift in Sources */, 506772C92425BB8500034DED /* NoStoresView.swift in Sources */,
50153E22250DECA300525160 /* SecretListItemView.swift in Sources */, 50153E22250DECA300525160 /* SecretListItemView.swift in Sources */,
508A58B5241ED48F0069DC07 /* PreviewAgentStatusChecker.swift in Sources */, 508A58B5241ED48F0069DC07 /* PreviewAgentStatusChecker.swift in Sources */,
50E205362FAABC6300402380 /* EditCertificateView.swift in Sources */,
508A58AA241E06B40069DC07 /* PreviewUpdater.swift in Sources */, 508A58AA241E06B40069DC07 /* PreviewUpdater.swift in Sources */,
); );
runOnlyForDeploymentPostprocessing = 0; runOnlyForDeploymentPostprocessing = 0;
@@ -867,23 +740,12 @@
isa = PBXSourcesBuildPhase; isa = PBXSourcesBuildPhase;
buildActionMask = 2147483647; buildActionMask = 2147483647;
files = ( files = (
50E205802FAB291E00402380 /* CertificateMigrator.swift in Sources */,
50020BB024064869003D4025 /* AppDelegate.swift in Sources */, 50020BB024064869003D4025 /* AppDelegate.swift in Sources */,
5018F54F24064786002EB505 /* Notifier.swift in Sources */, 5018F54F24064786002EB505 /* Notifier.swift in Sources */,
503647482F870B7800977A23 /* BatchedRequestsView.swift in Sources */,
501578132E6C0479004A37D0 /* XPCInputParser.swift in Sources */, 501578132E6C0479004A37D0 /* XPCInputParser.swift in Sources */,
); );
runOnlyForDeploymentPostprocessing = 0; runOnlyForDeploymentPostprocessing = 0;
}; };
50E205102FAAB81C00402380 /* Sources */ = {
isa = PBXSourcesBuildPhase;
buildActionMask = 2147483647;
files = (
50E205282FAAB82700402380 /* main.swift in Sources */,
50E2052C2FAAB85000402380 /* SecretiveCertificateParser.swift in Sources */,
);
runOnlyForDeploymentPostprocessing = 0;
};
/* End PBXSourcesBuildPhase section */ /* End PBXSourcesBuildPhase section */
/* Begin PBXTargetDependency section */ /* Begin PBXTargetDependency section */
@@ -916,16 +778,6 @@
target = 50692E4F2E6FF9D20043C7BB /* SecretAgentInputParser */; target = 50692E4F2E6FF9D20043C7BB /* SecretAgentInputParser */;
targetProxy = 50692E712E6FFA6E0043C7BB /* PBXContainerItemProxy */; targetProxy = 50692E712E6FFA6E0043C7BB /* PBXContainerItemProxy */;
}; };
50E2051C2FAAB81C00402380 /* PBXTargetDependency */ = {
isa = PBXTargetDependency;
target = 50E205132FAAB81C00402380 /* SecretiveCertificateParser */;
targetProxy = 50E2051B2FAAB81C00402380 /* PBXContainerItemProxy */;
};
50E2052F2FAAB92000402380 /* PBXTargetDependency */ = {
isa = PBXTargetDependency;
target = 50E205132FAAB81C00402380 /* SecretiveCertificateParser */;
targetProxy = 50E2052E2FAAB92000402380 /* PBXContainerItemProxy */;
};
/* End PBXTargetDependency section */ /* End PBXTargetDependency section */
/* Begin PBXVariantGroup section */ /* Begin PBXVariantGroup section */
@@ -1526,7 +1378,6 @@
DEVELOPMENT_ASSET_PATHS = "\"SecretAgent/Preview Content\""; DEVELOPMENT_ASSET_PATHS = "\"SecretAgent/Preview Content\"";
ENABLE_APP_SANDBOX = YES; ENABLE_APP_SANDBOX = YES;
ENABLE_ENHANCED_SECURITY = YES; ENABLE_ENHANCED_SECURITY = YES;
ENABLE_FILE_ACCESS_DOWNLOADS_FOLDER = readwrite;
ENABLE_HARDENED_RUNTIME = YES; ENABLE_HARDENED_RUNTIME = YES;
ENABLE_INCOMING_NETWORK_CONNECTIONS = NO; ENABLE_INCOMING_NETWORK_CONNECTIONS = NO;
ENABLE_OUTGOING_NETWORK_CONNECTIONS = NO; ENABLE_OUTGOING_NETWORK_CONNECTIONS = NO;
@@ -1564,7 +1415,6 @@
DEVELOPMENT_TEAM = "$(SECRETIVE_DEVELOPMENT_TEAM)"; DEVELOPMENT_TEAM = "$(SECRETIVE_DEVELOPMENT_TEAM)";
ENABLE_APP_SANDBOX = YES; ENABLE_APP_SANDBOX = YES;
ENABLE_ENHANCED_SECURITY = YES; ENABLE_ENHANCED_SECURITY = YES;
ENABLE_FILE_ACCESS_DOWNLOADS_FOLDER = readwrite;
ENABLE_HARDENED_RUNTIME = YES; ENABLE_HARDENED_RUNTIME = YES;
ENABLE_INCOMING_NETWORK_CONNECTIONS = NO; ENABLE_INCOMING_NETWORK_CONNECTIONS = NO;
ENABLE_OUTGOING_NETWORK_CONNECTIONS = NO; ENABLE_OUTGOING_NETWORK_CONNECTIONS = NO;
@@ -1603,7 +1453,6 @@
DEVELOPMENT_TEAM = "$(SECRETIVE_DEVELOPMENT_TEAM)"; DEVELOPMENT_TEAM = "$(SECRETIVE_DEVELOPMENT_TEAM)";
ENABLE_APP_SANDBOX = YES; ENABLE_APP_SANDBOX = YES;
ENABLE_ENHANCED_SECURITY = YES; ENABLE_ENHANCED_SECURITY = YES;
ENABLE_FILE_ACCESS_DOWNLOADS_FOLDER = readwrite;
ENABLE_HARDENED_RUNTIME = YES; ENABLE_HARDENED_RUNTIME = YES;
ENABLE_INCOMING_NETWORK_CONNECTIONS = NO; ENABLE_INCOMING_NETWORK_CONNECTIONS = NO;
ENABLE_OUTGOING_NETWORK_CONNECTIONS = NO; ENABLE_OUTGOING_NETWORK_CONNECTIONS = NO;
@@ -1630,103 +1479,6 @@
}; };
name = Release; name = Release;
}; };
50E205202FAAB81C00402380 /* Debug */ = {
isa = XCBuildConfiguration;
buildSettings = {
CLANG_CXX_LANGUAGE_STANDARD = "gnu++20";
CODE_SIGN_ENTITLEMENTS = SecretiveCertificateParser/SecretiveCertificateParser.entitlements;
CODE_SIGN_IDENTITY = "Apple Development";
CODE_SIGN_STYLE = Automatic;
COMBINE_HIDPI_IMAGES = YES;
CURRENT_PROJECT_VERSION = 1;
DEVELOPMENT_TEAM = "$(SECRETIVE_DEVELOPMENT_TEAM)";
ENABLE_APP_SANDBOX = YES;
ENABLE_HARDENED_RUNTIME = YES;
GCC_C_LANGUAGE_STANDARD = gnu17;
GENERATE_INFOPLIST_FILE = YES;
INFOPLIST_FILE = SecretiveCertificateParser/Info.plist;
INFOPLIST_KEY_CFBundleDisplayName = SecretiveCertificateParser;
INFOPLIST_KEY_NSHumanReadableCopyright = "Copyright © 2026 Max Goedjen. All rights reserved.";
LOCALIZATION_PREFERS_STRING_CATALOGS = YES;
MACOSX_DEPLOYMENT_TARGET = 14.0;
MARKETING_VERSION = 1.0;
PRODUCT_BUNDLE_IDENTIFIER = "$(SECRETIVE_BASE_BUNDLE_ID).SecretiveCertificateParser";
PRODUCT_NAME = "$(TARGET_NAME)";
REGISTER_APP_GROUPS = YES;
SKIP_INSTALL = YES;
STRING_CATALOG_GENERATE_SYMBOLS = YES;
SWIFT_ACTIVE_COMPILATION_CONDITIONS = "DEBUG $(inherited)";
SWIFT_APPROACHABLE_CONCURRENCY = YES;
SWIFT_EMIT_LOC_STRINGS = YES;
SWIFT_UPCOMING_FEATURE_MEMBER_IMPORT_VISIBILITY = YES;
SWIFT_VERSION = 5.0;
};
name = Debug;
};
50E205212FAAB81C00402380 /* Test */ = {
isa = XCBuildConfiguration;
buildSettings = {
CLANG_CXX_LANGUAGE_STANDARD = "gnu++20";
CODE_SIGN_ENTITLEMENTS = SecretiveCertificateParser/SecretiveCertificateParser.entitlements;
CODE_SIGN_STYLE = Automatic;
COMBINE_HIDPI_IMAGES = YES;
CURRENT_PROJECT_VERSION = 1;
ENABLE_APP_SANDBOX = YES;
ENABLE_HARDENED_RUNTIME = YES;
GCC_C_LANGUAGE_STANDARD = gnu17;
GENERATE_INFOPLIST_FILE = YES;
INFOPLIST_FILE = SecretiveCertificateParser/Info.plist;
INFOPLIST_KEY_CFBundleDisplayName = SecretiveCertificateParser;
INFOPLIST_KEY_NSHumanReadableCopyright = "Copyright © 2026 Max Goedjen. All rights reserved.";
LOCALIZATION_PREFERS_STRING_CATALOGS = YES;
MACOSX_DEPLOYMENT_TARGET = 14.0;
MARKETING_VERSION = 1.0;
PRODUCT_BUNDLE_IDENTIFIER = "$(SECRETIVE_BASE_BUNDLE_ID).SecretiveCertificateParser";
PRODUCT_NAME = "$(TARGET_NAME)";
REGISTER_APP_GROUPS = YES;
SKIP_INSTALL = YES;
STRING_CATALOG_GENERATE_SYMBOLS = YES;
SWIFT_APPROACHABLE_CONCURRENCY = YES;
SWIFT_EMIT_LOC_STRINGS = YES;
SWIFT_UPCOMING_FEATURE_MEMBER_IMPORT_VISIBILITY = YES;
SWIFT_VERSION = 5.0;
};
name = Test;
};
50E205222FAAB81C00402380 /* Release */ = {
isa = XCBuildConfiguration;
buildSettings = {
CLANG_CXX_LANGUAGE_STANDARD = "gnu++20";
CODE_SIGN_ENTITLEMENTS = SecretiveCertificateParser/SecretiveCertificateParser.entitlements;
CODE_SIGN_IDENTITY = "Developer ID Application";
CODE_SIGN_STYLE = Manual;
COMBINE_HIDPI_IMAGES = YES;
CURRENT_PROJECT_VERSION = 1;
DEVELOPMENT_TEAM = "";
"DEVELOPMENT_TEAM[sdk=macosx*]" = Z72PRUAWF6;
ENABLE_APP_SANDBOX = YES;
ENABLE_HARDENED_RUNTIME = YES;
GCC_C_LANGUAGE_STANDARD = gnu17;
GENERATE_INFOPLIST_FILE = YES;
INFOPLIST_FILE = SecretiveCertificateParser/Info.plist;
INFOPLIST_KEY_CFBundleDisplayName = SecretiveCertificateParser;
INFOPLIST_KEY_NSHumanReadableCopyright = "Copyright © 2026 Max Goedjen. All rights reserved.";
LOCALIZATION_PREFERS_STRING_CATALOGS = YES;
MACOSX_DEPLOYMENT_TARGET = 14.0;
MARKETING_VERSION = 1.0;
PRODUCT_BUNDLE_IDENTIFIER = "$(SECRETIVE_BASE_BUNDLE_ID).SecretiveCertificateParser";
PRODUCT_NAME = "$(TARGET_NAME)";
PROVISIONING_PROFILE_SPECIFIER = "";
REGISTER_APP_GROUPS = YES;
SKIP_INSTALL = YES;
STRING_CATALOG_GENERATE_SYMBOLS = YES;
SWIFT_APPROACHABLE_CONCURRENCY = YES;
SWIFT_EMIT_LOC_STRINGS = YES;
SWIFT_UPCOMING_FEATURE_MEMBER_IMPORT_VISIBILITY = YES;
SWIFT_VERSION = 5.0;
};
name = Release;
};
/* End XCBuildConfiguration section */ /* End XCBuildConfiguration section */
/* Begin XCConfigurationList section */ /* Begin XCConfigurationList section */
@@ -1780,16 +1532,6 @@
defaultConfigurationIsVisible = 0; defaultConfigurationIsVisible = 0;
defaultConfigurationName = Release; defaultConfigurationName = Release;
}; };
50E2051F2FAAB81C00402380 /* Build configuration list for PBXNativeTarget "SecretiveCertificateParser" */ = {
isa = XCConfigurationList;
buildConfigurations = (
50E205202FAAB81C00402380 /* Debug */,
50E205212FAAB81C00402380 /* Test */,
50E205222FAAB81C00402380 /* Release */,
);
defaultConfigurationIsVisible = 0;
defaultConfigurationName = Release;
};
/* End XCConfigurationList section */ /* End XCConfigurationList section */
/* Begin XCSwiftPackageProductDependency section */ /* Begin XCSwiftPackageProductDependency section */
@@ -1833,10 +1575,6 @@
isa = XCSwiftPackageProductDependency; isa = XCSwiftPackageProductDependency;
productName = Brief; productName = Brief;
}; };
505F5EF12FA9635700C45824 /* CertificateKit */ = {
isa = XCSwiftPackageProductDependency;
productName = CertificateKit;
};
50692D2C2E6FDC000043C7BB /* XPCWrappers */ = { 50692D2C2E6FDC000043C7BB /* XPCWrappers */ = {
isa = XCSwiftPackageProductDependency; isa = XCSwiftPackageProductDependency;
productName = XPCWrappers; productName = XPCWrappers;
@@ -1857,34 +1595,6 @@
isa = XCSwiftPackageProductDependency; isa = XCSwiftPackageProductDependency;
productName = Common; productName = Common;
}; };
50E205302FAAB95500402380 /* XPCWrappers */ = {
isa = XCSwiftPackageProductDependency;
productName = XPCWrappers;
};
50E205322FAAB95A00402380 /* SSHProtocolKit */ = {
isa = XCSwiftPackageProductDependency;
productName = SSHProtocolKit;
};
50E205812FAB293B00402380 /* SharedXPCServices */ = {
isa = XCSwiftPackageProductDependency;
productName = SharedXPCServices;
};
50E205832FAB296A00402380 /* SharedXPCServices */ = {
isa = XCSwiftPackageProductDependency;
productName = SharedXPCServices;
};
50E205852FAC2EA000402380 /* Formatters */ = {
isa = XCSwiftPackageProductDependency;
productName = Formatters;
};
50E205872FAC2EAB00402380 /* Formatters */ = {
isa = XCSwiftPackageProductDependency;
productName = Formatters;
};
50E205892FAC2EB600402380 /* Formatters */ = {
isa = XCSwiftPackageProductDependency;
productName = Formatters;
};
/* End XCSwiftPackageProductDependency section */ /* End XCSwiftPackageProductDependency section */
}; };
rootObject = 50617D7723FCE48D0099B055 /* Project object */; rootObject = 50617D7723FCE48D0099B055 /* Project object */;

View File

@@ -23,7 +23,7 @@
</BuildActionEntries> </BuildActionEntries>
</BuildAction> </BuildAction>
<TestAction <TestAction
buildConfiguration = "Debug" buildConfiguration = "Test"
selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB" selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB" selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
shouldUseLaunchSchemeArgsEnv = "YES"> shouldUseLaunchSchemeArgsEnv = "YES">

View File

@@ -3,7 +3,6 @@ import SecretKit
import SecureEnclaveSecretKit import SecureEnclaveSecretKit
import SmartCardSecretKit import SmartCardSecretKit
import Brief import Brief
import CertificateKit
@main @main
struct Secretive: App { struct Secretive: App {
@@ -15,7 +14,6 @@ struct Secretive: App {
WindowGroup { WindowGroup {
ContentView() ContentView()
.environment(EnvironmentValues._secretStoreList) .environment(EnvironmentValues._secretStoreList)
.environment(EnvironmentValues._certificateStore)
.onReceive(NotificationCenter.default.publisher(for: NSApplication.didBecomeActiveNotification)) { _ in .onReceive(NotificationCenter.default.publisher(for: NSApplication.didBecomeActiveNotification)) { _ in
Task { Task {
@AppStorage("defaultsHasRunSetup") var hasRunSetup = false @AppStorage("defaultsHasRunSetup") var hasRunSetup = false
@@ -25,7 +23,7 @@ struct Secretive: App {
guard !agentLaunchController.developmentBuild else { return } guard !agentLaunchController.developmentBuild else { return }
if justUpdatedChecker.justUpdatedBuild || !agentLaunchController.running { if justUpdatedChecker.justUpdatedBuild || !agentLaunchController.running {
// Relaunch the agent, since it'll be running from earlier update still // Relaunch the agent, since it'll be running from earlier update still
try? await agentLaunchController.forceLaunch() try await agentLaunchController.forceLaunch()
} }
} }
} }
@@ -94,18 +92,15 @@ extension EnvironmentValues {
@MainActor fileprivate static let _secretStoreList: SecretStoreList = { @MainActor fileprivate static let _secretStoreList: SecretStoreList = {
let list = SecretStoreList() let list = SecretStoreList()
let cryptoKit = SecureEnclave.Store() let cryptoKit = SecureEnclave.Store()
let cryptoKitMigrator = SecureEnclave.CryptoKitMigrator() let migrator = SecureEnclave.CryptoKitMigrator()
try? cryptoKitMigrator.migrate(to: cryptoKit) try? migrator.migrate(to: cryptoKit)
list.add(store: cryptoKit) list.add(store: cryptoKit)
list.add(store: SmartCard.Store()) list.add(store: SmartCard.Store())
return list return list
}() }()
@MainActor fileprivate static let _certificateStore: CertificateStore = CertificateStore()
private static let _agentLaunchController = AgentLaunchController() private static let _agentLaunchController = AgentLaunchController()
@Entry var agentLaunchController: any AgentLaunchControllerProtocol = _agentLaunchController @Entry var agentLaunchController: any AgentLaunchControllerProtocol = _agentLaunchController
private static let _updater: any UpdaterProtocol = { private static let _updater: any UpdaterProtocol = {
@AppStorage("defaultsHasRunSetup") var hasRunSetup = false @AppStorage("defaultsHasRunSetup") var hasRunSetup = false
return Updater(checkOnLaunch: hasRunSetup) return Updater(checkOnLaunch: hasRunSetup)
@@ -118,10 +113,6 @@ extension EnvironmentValues {
@MainActor var secretStoreList: SecretStoreList { @MainActor var secretStoreList: SecretStoreList {
EnvironmentValues._secretStoreList EnvironmentValues._secretStoreList
} }
@MainActor var certificateStore: CertificateStore {
EnvironmentValues._certificateStore
}
} }
extension FocusedValues { extension FocusedValues {

View File

@@ -1,6 +1,5 @@
import Foundation import Foundation
import SecretKit import SecretKit
import LocalAuthentication
enum Preview {} enum Preview {}
@@ -39,10 +38,17 @@ extension Preview {
self.init(secrets: new) self.init(secrets: new)
} }
func sign(data: Data, with secret: Preview.Secret, for provenance: SigningRequestProvenance, context: LAContext?) async throws -> Data { func sign(data: Data, with secret: Preview.Secret, for provenance: SigningRequestProvenance) throws -> Data {
return data return data
} }
func existingPersistedAuthenticationContext(secret: Preview.Secret) -> PersistedAuthenticationContext? {
nil
}
func persistAuthentication(secret: Preview.Secret, forDuration duration: TimeInterval) throws {
}
func reloadSecrets() { func reloadSecrets() {
} }
@@ -76,10 +82,16 @@ extension Preview {
self.init(secrets: new) self.init(secrets: new)
} }
func sign(data: Data, with secret: Preview.Secret, for provenance: SigningRequestProvenance, context: LAContext?) async throws -> Data { func sign(data: Data, with secret: Preview.Secret, for provenance: SigningRequestProvenance) throws -> Data {
return data return data
} }
func existingPersistedAuthenticationContext(secret: Preview.Secret) -> PersistedAuthenticationContext? {
nil
}
func persistAuthentication(secret: Preview.Secret, forDuration duration: TimeInterval) throws {
}
func reloadSecrets() { func reloadSecrets() {
} }

View File

@@ -1,93 +0,0 @@
import SwiftUI
import SecretKit
import Common
import CertificateKit
import SSHProtocolKit
import CryptoKit
struct CertificateDetailView: View {
let certificate: Certificate
var body: some View {
ScrollView {
Form {
Section {
CopyableView(
title: .certificateDetailKeyIdLabel,
image: Image(systemName: "person.text.rectangle"),
text: certificate.keyID
)
Spacer()
.frame(height: 20)
CopyableView(
title: .certificateDetailSerialLabel,
image: Image(systemName: "number.circle"),
text: certificate.serial.formatted()
)
Spacer()
.frame(height: 20)
CopyableView(
title: .secretDetailSha256FingerprintLabel,
image: Image(systemName: "touchid"),
text: OpenSSHCertificateWriter().openSSHSHA256KeyFingerprint(publicKey: certificate.publicKey)
)
Spacer()
.frame(height: 20)
CopyableView(
title: .secretDetailSha256FingerprintLabel,
image: Image(systemName: "touchid"),
text: OpenSSHCertificateWriter().openSSHSHA256KeyFingerprint(publicKey: certificate.signingKey)
)
Spacer()
.frame(height: 20)
CopyableView(
title: .certificateDetailPathLabel,
image: Image(systemName: "checkmark.seal.text.page"),
text: URL.certificatePath(for: certificate.id, in: URL.certificatesDirectory),
showRevealInFinder: true
)
if let validityRange = certificate.validityRange {
let epoch = Date(timeIntervalSince1970: 0)
let end = Date(timeIntervalSince1970: TimeInterval(UInt64.max))
switch (validityRange.lowerBound, validityRange.upperBound) {
case (epoch, end):
EmptyView()
case (epoch, let otherEnd):
Spacer()
.frame(height: 20)
MultilineInfoView(title: .certificateDetailValidUntilLabel, image: Image(systemName: "calendar.badge.clock"), items: [otherEnd.formatted()])
case (let otherStart, end):
Spacer()
.frame(height: 20)
MultilineInfoView(title: .certificateDetailValidAfterLabel, image: Image(systemName: "calendar.badge.clock"), items: [otherStart.formatted()])
default:
Spacer()
.frame(height: 20)
MultilineInfoView(title: .certificateDetailValidityRangeLabel, image: Image(systemName: "calendar.badge.clock"), items: [validityRange.formatted()])
}
}
if !certificate.principals.isEmpty {
Spacer()
.frame(height: 20)
MultilineInfoView(title: .certificateDetailPrincipalsLabel, image: Image(systemName: "person.2"), items: certificate.principals)
}
if !certificate.criticalOptions.isEmpty {
Spacer()
.frame(height: 20)
MultilineInfoView(title: .certificateDetailCriticalOptionsLabel, image: Image(systemName: "person.2"), items: certificate.criticalOptions)
}
if !certificate.extensions.isEmpty {
Spacer()
.frame(height: 20)
MultilineInfoView(title: .certificateDetailExtensionsLabel, image: Image(systemName: "person.2"), items: certificate.extensions)
}
Spacer()
}
}
.padding()
}
.frame(minHeight: 200, maxHeight: .infinity)
}
}

View File

@@ -1,42 +0,0 @@
import SwiftUI
import CertificateKit
import SSHProtocolKit
struct CertificateListItemView: View {
@Environment(\.certificateStore) private var store
var certificate: Certificate
@State var isDeleting: Bool = false
@State var isRenaming: Bool = false
var deletedCertificate: (Certificate) -> Void
var renamedCertificate: (Certificate) -> Void
var body: some View {
NavigationLink(value: certificate) {
Text(certificate.name)
}
.sheet(isPresented: $isRenaming, onDismiss: {
renamedCertificate(certificate)
}, content: {
EditCertificateView(store: store, certificate: certificate)
})
.showingDeleteConfirmation(isPresented: $isDeleting, certificate, store) { deleted in
if deleted {
deletedCertificate(certificate)
}
}
.contextMenu {
Button(action: { isRenaming = true }) {
Image(systemName: "pencil")
Text(.secretListEditButton)
}
Button(action: { isDeleting = true }) {
Image(systemName: "trash")
Text(.secretListDeleteButton)
}
}
}
}

View File

@@ -1,52 +0,0 @@
import SwiftUI
import CertificateKit
import SSHProtocolKit
extension View {
func showingDeleteConfirmation(isPresented: Binding<Bool>, _ certificate: Certificate, _ store: CertificateStore, dismissalBlock: @escaping (Bool) -> ()) -> some View {
modifier(DeleteCertificateConfirmationModifier(isPresented: isPresented, certificate: certificate, store: store, dismissalBlock: dismissalBlock))
}
}
struct DeleteCertificateConfirmationModifier: ViewModifier {
var isPresented: Binding<Bool>
var certificate: Certificate
var store: CertificateStore
var dismissalBlock: (Bool) -> ()
@State var confirmedSecretName = ""
@State private var errorText: String?
func body(content: Content) -> some View {
content
.confirmationDialog(
String(localized: .deleteConfirmationTitle(name: certificate.name)),
isPresented: isPresented,
titleVisibility: .visible,
actions: {
Button(.deleteConfirmationDeleteButton, action: delete)
Button(.deleteConfirmationCancelButton, role: .cancel) {
dismissalBlock(false)
}
},
)
.dialogIcon(Image(systemName: "lock.trianglebadge.exclamationmark.fill"))
.onExitCommand {
dismissalBlock(false)
}
}
func delete() {
Task {
do {
try store.delete(certificate: certificate)
dismissalBlock(true)
} catch {
errorText = error.localizedDescription
}
}
}
}

View File

@@ -21,7 +21,7 @@ struct DeleteSecretConfirmationModifier: ViewModifier {
func body(content: Content) -> some View { func body(content: Content) -> some View {
content content
.confirmationDialog( .confirmationDialog(
.deleteConfirmationTitle(name: secret.name), .deleteConfirmationTitle(secretName: secret.name),
isPresented: isPresented, isPresented: isPresented,
titleVisibility: .visible, titleVisibility: .visible,
actions: { actions: {

View File

@@ -1,60 +0,0 @@
import SwiftUI
import SSHProtocolKit
import CertificateKit
struct EditCertificateView: View {
let store: CertificateStore
let certificate: Certificate
@State private var name: String
@State var errorText: String?
@Environment(\.dismiss) var dismiss
init(store: CertificateStore, certificate: Certificate) {
self.store = store
self.certificate = certificate
name = certificate.name
}
var body: some View {
VStack(alignment: .trailing) {
Form {
Section {
TextField(String(localized: .renameCertificateLabel), text: $name, prompt: Text(.renameCertificateNamePlaceholder))
} footer: {
if let errorText {
Text(verbatim: errorText)
.errorStyle()
}
}
}
HStack {
Button(.editCancelButton) {
dismiss()
}
.keyboardShortcut(.cancelAction)
Button(.editSaveButton, action: rename)
.disabled(name.isEmpty)
.keyboardShortcut(.return)
.primaryButton()
}
.padding()
}
.formStyle(.grouped)
}
func rename() {
Task {
do {
var updated = certificate
updated.openSSHCertificate.name = name
try store.update(certificate: updated)
dismiss()
} catch {
errorText = error.localizedDescription
}
}
}
}

View File

@@ -1,14 +1,11 @@
import SwiftUI import SwiftUI
import SecretKit import SecretKit
import Common import Common
import CertificateKit
import SSHProtocolKit import SSHProtocolKit
struct SecretDetailView<SecretType: Secret>: View { struct SecretDetailView<SecretType: Secret>: View {
let secret: SecretType let secret: SecretType
let certificates: [Certificate]
let navigateToCertificate: ((Certificate) -> Void)?
private let keyWriter = OpenSSHPublicKeyWriter() private let keyWriter = OpenSSHPublicKeyWriter()
@@ -16,42 +13,16 @@ struct SecretDetailView<SecretType: Secret>: View {
ScrollView { ScrollView {
Form { Form {
Section { Section {
CopyableView( CopyableView(title: .secretDetailSha256FingerprintLabel, image: Image(systemName: "touchid"), text: keyWriter.openSSHSHA256Fingerprint(secret: secret))
title: .secretDetailSha256FingerprintLabel,
image: Image(systemName: "touchid"),
text: keyWriter.openSSHSHA256Fingerprint(secret: secret)
)
Spacer() Spacer()
.frame(height: 20) .frame(height: 20)
CopyableView( CopyableView(title: .secretDetailMd5FingerprintLabel, image: Image(systemName: "touchid"), text: keyWriter.openSSHMD5Fingerprint(secret: secret))
title: .secretDetailMd5FingerprintLabel,
image: Image(systemName: "touchid"),
text: keyWriter.openSSHMD5Fingerprint(secret: secret)
)
Spacer() Spacer()
.frame(height: 20) .frame(height: 20)
CopyableView( CopyableView(title: .secretDetailPublicKeyLabel, image: Image(systemName: "key"), text: keyString)
title: .secretDetailPublicKeyPathLabel, Spacer()
image: Image(systemName: "lock.doc"), .frame(height: 20)
text: URL.publicKeyPath(for: secret, in: URL.publicKeyDirectory), CopyableView(title: .secretDetailPublicKeyPathLabel, image: Image(systemName: "lock.doc"), text: URL.publicKeyPath(for: secret, in: URL.publicKeyDirectory), showRevealInFinder: true)
showRevealInFinder: true
)
if !certificates.isEmpty {
Spacer()
.frame(height: 20)
MultilineInfoView(
title: .secretDetailCertificatePathLabel,
image: Image(
systemName: "checkmark.seal.text.page"
),
items: certificates.map({ certificate in
MultilineInfoView.Item(
text: certificate.name,
action: (Image(systemName: "chevron.forward"), { navigateToCertificate?(certificate) })
)
})
)
}
Spacer() Spacer()
} }
} }
@@ -61,6 +32,10 @@ struct SecretDetailView<SecretType: Secret>: View {
} }
var keyString: String {
keyWriter.openSSHString(secret: secret)
}
} }
//#Preview { //#Preview {

View File

@@ -1,33 +1,25 @@
import SwiftUI import SwiftUI
import SecretKit import SecretKit
import SSHProtocolKit
import CertificateKit
struct StoreListView: View { struct StoreListView: View {
enum StoreListSelection: Hashable { @Binding var activeSecret: AnySecret?
case secret(AnySecret)
case certificate(Certificate)
}
@Binding var selection: StoreListSelection?
@Environment(\.secretStoreList) private var storeList @Environment(\.secretStoreList) private var storeList
@Environment(\.certificateStore) private var certificateStore
private func secretDeleted(secret: AnySecret) { private func secretDeleted(secret: AnySecret) {
selection = nextDefaultSecret.map(StoreListSelection.secret) activeSecret = nextDefaultSecret
} }
private func secretRenamed(secret: AnySecret) { private func secretRenamed(secret: AnySecret) {
// Pull new version from store, so we get all updated attributes // Pull new version from store, so we get all updated attributes
selection = nil activeSecret = nil
selection = storeList.allSecrets.first(where: { $0.id == secret.id }).map(StoreListSelection.secret) activeSecret = storeList.allSecrets.first(where: { $0.id == secret.id })
} }
var body: some View { var body: some View {
NavigationSplitView { NavigationSplitView {
List(selection: $selection) { List(selection: $activeSecret) {
ForEach(storeList.stores) { store in ForEach(storeList.stores) { store in
if store.isAvailable { if store.isAvailable {
Section(header: Text(store.name)) { Section(header: Text(store.name)) {
@@ -38,51 +30,29 @@ struct StoreListView: View {
deletedSecret: secretDeleted, deletedSecret: secretDeleted,
renamedSecret: secretRenamed, renamedSecret: secretRenamed,
) )
.tag(StoreListSelection.secret(secret))
} }
} }
} }
} }
if !certificateStore.certificates.isEmpty {
Section("Certificates") {
ForEach(certificateStore.certificates) { certificate in
CertificateListItemView(
certificate: certificate,
deletedCertificate: { _ in },
renamedCertificate: { _ in }
)
.tag(StoreListSelection.certificate(certificate))
}
}
}
} }
} detail: { } detail: {
switch selection { if let activeSecret {
case .secret(let secret): SecretDetailView(secret: activeSecret)
SecretDetailView(secret: secret, certificates: certificateStore.certificates(for: secret)) { } else if let nextDefaultSecret {
selection = .certificate($0) // This just means onAppear hasn't executed yet.
} // Do this to avoid a blip.
case .certificate(let certificate): SecretDetailView(secret: nextDefaultSecret)
CertificateDetailView(certificate: certificate) } else {
case nil: if let modifiable = storeList.modifiableStore, modifiable.isAvailable {
if let nextDefaultSecret { EmptyStoreView(store: modifiable)
// This just means onAppear hasn't executed yet.
// Do this to avoid a blip.
SecretDetailView(secret: nextDefaultSecret, certificates: certificateStore.certificates(for: nextDefaultSecret)) {
selection = .certificate($0)
}
} else { } else {
if let modifiable = storeList.modifiableStore, modifiable.isAvailable { EmptyStoreView(store: storeList.stores.first(where: \.isAvailable))
EmptyStoreView(store: modifiable)
} else {
EmptyStoreView(store: storeList.stores.first(where: \.isAvailable))
}
} }
} }
} }
.navigationSplitViewStyle(.balanced) .navigationSplitViewStyle(.balanced)
.onAppear { .onAppear {
selection = nextDefaultSecret.map(StoreListSelection.secret) activeSecret = nextDefaultSecret
} }
.frame(minWidth: 100, idealWidth: 240) .frame(minWidth: 100, idealWidth: 240)

View File

@@ -101,7 +101,7 @@ struct AgentNotRunningView: View {
guard !loading else { return } guard !loading else { return }
loading = true loading = true
Task { Task {
try? await agentLaunchController.forceLaunch() try await agentLaunchController.forceLaunch()
loading = false loading = false
if !agentLaunchController.running { if !agentLaunchController.running {

View File

@@ -3,20 +3,16 @@ import SecretKit
import SecureEnclaveSecretKit import SecureEnclaveSecretKit
import SmartCardSecretKit import SmartCardSecretKit
import Brief import Brief
import SSHProtocolKit
import SharedXPCServices
import CertificateKit
struct ContentView: View { struct ContentView: View {
@State var selection: StoreListView.StoreListSelection? @State var activeSecret: AnySecret?
@State private var selectedUpdate: Release? @State private var selectedUpdate: Release?
@Environment(\.colorScheme) private var colorScheme @Environment(\.colorScheme) private var colorScheme
@Environment(\.openWindow) private var openWindow @Environment(\.openWindow) private var openWindow
@Environment(\.secretStoreList) private var storeList @Environment(\.secretStoreList) private var storeList
@Environment(\.certificateStore) private var certificateStore
@Environment(\.updater) private var updater @Environment(\.updater) private var updater
@Environment(\.agentLaunchController) private var agentLaunchController @Environment(\.agentLaunchController) private var agentLaunchController
@@ -29,7 +25,7 @@ struct ContentView: View {
var body: some View { var body: some View {
VStack { VStack {
if storeList.anyAvailable { if storeList.anyAvailable {
StoreListView(selection: $selection) StoreListView(activeSecret: $activeSecret)
} else { } else {
NoStoresView() NoStoresView()
} }
@@ -46,22 +42,6 @@ struct ContentView: View {
runningSetup = true runningSetup = true
} }
} }
.dropDestination(for: URL.self) { items, location in
guard let url = items.first, url.pathExtension == "pub" else { return false }
Task {
do {
let data = try Data(contentsOf: url)
let parser = try await XPCCertificateParser()
let cert = try await parser.parse(data: data)
let wrapped = Certificate(openSSHCertificate: cert, rawData: data)
try certificateStore.save(certificate: wrapped)
selection = .certificate(wrapped)
} catch {
}
}
return true
} isTargeted: { _ in }
.focusedSceneValue(\.showCreateSecret, .init(isEnabled: !runningSetup) { .focusedSceneValue(\.showCreateSecret, .init(isEnabled: !runningSetup) {
showingCreation = true showingCreation = true
}) })
@@ -69,7 +49,7 @@ struct ContentView: View {
if let modifiable = storeList.modifiableStore { if let modifiable = storeList.modifiableStore {
CreateSecretView(store: modifiable) { created in CreateSecretView(store: modifiable) { created in
if let created { if let created {
selection = .secret(created) activeSecret = created
} }
} }
} }

View File

@@ -4,7 +4,6 @@ import UniformTypeIdentifiers
struct CopyableView: View { struct CopyableView: View {
var title: LocalizedStringResource var title: LocalizedStringResource
var subtitle: String?
var image: Image var image: Image
var text: String var text: String
var showRevealInFinder = false var showRevealInFinder = false
@@ -18,16 +17,9 @@ struct CopyableView: View {
.renderingMode(.template) .renderingMode(.template)
.imageScale(.large) .imageScale(.large)
.foregroundColor(primaryTextColor) .foregroundColor(primaryTextColor)
VStack(alignment: .leading) { Text(title)
Text(title) .font(.headline)
.font(.headline) .foregroundColor(primaryTextColor)
.foregroundColor(primaryTextColor)
if let subtitle {
Text(subtitle)
.font(.system(.subheadline, design: .monospaced))
.foregroundColor(secondaryTextColor)
}
}
Spacer() Spacer()
if interactionState != .normal { if interactionState != .normal {
HStack { HStack {
@@ -163,9 +155,10 @@ fileprivate struct BackgroundViewModifier: ViewModifier {
} else { } else {
if #available(macOS 26.0, *) { if #available(macOS 26.0, *) {
content content
// Very thin opacity lets user hover anywhere over the view, glassEffect doesn't allow.
.background(.white.opacity(0.01), in: RoundedRectangle(cornerRadius: 15))
.glassEffect(.regular.tint(backgroundColor(interactionState: interactionState)), in: RoundedRectangle(cornerRadius: 15)) .glassEffect(.regular.tint(backgroundColor(interactionState: interactionState)), in: RoundedRectangle(cornerRadius: 15))
.mask(RoundedRectangle(cornerRadius: 15)) .mask(RoundedRectangle(cornerRadius: 15))
.contentShape(RoundedRectangle(cornerRadius: 15))
.shadow(color: .black.opacity(0.1), radius: 5) .shadow(color: .black.opacity(0.1), radius: 5)
} else { } else {
content content
@@ -181,7 +174,7 @@ fileprivate struct BackgroundViewModifier: ViewModifier {
let base = colorScheme == .dark ? Color(white: 0.2) : Color(white: 1) let base = colorScheme == .dark ? Color(white: 0.2) : Color(white: 1)
switch interactionState { switch interactionState {
case .normal: case .normal:
return base.mix(with: .accentColor, by: 0) return base
case .hovering: case .hovering:
return base.mix(with: .accentColor, by: colorScheme == .dark ? 0.2 : 0.1) return base.mix(with: .accentColor, by: colorScheme == .dark ? 0.2 : 0.1)
case .clicking, .dragging: case .clicking, .dragging:

View File

@@ -1,167 +0,0 @@
import SwiftUI
import UniformTypeIdentifiers
struct MultilineInfoView: View {
struct Item {
let text: String
let action: (Image, () -> Void)?
}
var title: LocalizedStringResource
var image: Image
var items: [Item]
init(title: LocalizedStringResource, image: Image, items: [Item]) {
self.title = title
self.image = image
self.items = items
}
init(title: LocalizedStringResource, image: Image, items: [String]) {
self.title = title
self.image = image
self.items = items.map({ Item(text: $0, action: nil) })
}
@State private var interactionState: InteractionState = .normal
@State private var interactionStateIndex: Int?
var body: some View {
VStack(alignment: .leading, spacing: 0) {
HStack {
image
.renderingMode(.template)
.imageScale(.large)
.foregroundColor(primaryTextColor)
Text(title)
.font(.headline)
.foregroundColor(primaryTextColor)
Spacer()
}
.safeAreaPadding(20)
ForEach(Array(items.enumerated()), id: \.offset) { item in
Divider()
.ignoresSafeArea()
.opacity(item.offset == 0 ? 1 : 0.75)
HStack {
Text(item.element.text)
Spacer()
if let (image, _) = item.element.action {
image
.foregroundStyle(.secondary)
}
}
.safeAreaPadding(20)
._background(interactionState: interactionStateIndex == item.offset ? interactionState : .normal, cornerRadius: 0)
.onHover { hovering in
withAnimation {
guard item.element.action != nil else { return }
interactionState = hovering ? .hovering : .normal
interactionStateIndex = item.offset
}
}
.gesture(
TapGesture()
.onEnded {
item.element.action?.1()
withAnimation {
interactionState = .normal
interactionStateIndex = nil
}
}
)
}
}
._background(interactionState: .normal)
.frame(minWidth: 150, maxWidth: .infinity)
}
var primaryTextColor: Color {
switch interactionState {
case .normal, .hovering:
return Color(.textColor)
}
}
var secondaryTextColor: Color {
switch interactionState {
case .normal, .hovering:
return Color(.secondaryLabelColor)
}
}
}
fileprivate enum InteractionState {
case normal, hovering
}
extension View {
fileprivate func _background(interactionState: InteractionState, cornerRadius: Double = 15) -> some View {
modifier(BackgroundViewModifier(interactionState: interactionState, cornerRadius: cornerRadius))
}
}
fileprivate struct BackgroundViewModifier: ViewModifier {
@Environment(\.colorScheme) private var colorScheme
@Environment(\.appearsActive) private var appearsActive
let interactionState: InteractionState
let cornerRadius: Double
func body(content: Content) -> some View {
if #available(macOS 26.0, *) {
content
// Very thin opacity lets user hover anywhere over the view, glassEffect doesn't allow.
.background(.white.opacity(0.01), in: RoundedRectangle(cornerRadius: 15))
.glassEffect(.regular.tint(backgroundColor(interactionState: interactionState)), in: RoundedRectangle(cornerRadius: cornerRadius))
.mask(RoundedRectangle(cornerRadius: cornerRadius))
.shadow(color: .black.opacity(0.1), radius: 5)
} else {
content
.background(backgroundColor(interactionState: interactionState))
.cornerRadius(10)
}
}
func backgroundColor(interactionState: InteractionState) -> Color {
guard appearsActive else { return Color.clear }
if #available(macOS 26.0, *) {
let base = colorScheme == .dark ? Color(white: 0.2) : Color(white: 1)
switch interactionState {
case .normal:
return base
case .hovering:
return base.mix(with: .accentColor, by: colorScheme == .dark ? 0.2 : 0.1)
}
} else {
switch interactionState {
case .normal:
return colorScheme == .dark ? Color(white: 0.2) : Color(white: 0.885)
case .hovering:
return colorScheme == .dark ? Color(white: 0.275) : Color(white: 0.82)
}
}
}
}
#Preview {
MultilineInfoView(title: "Multiple", image: Image(systemName: "figure.wave"), items: [
MultilineInfoView.Item(text: "hello", action: (Image(systemName: "chevron.forward"), {})),
MultilineInfoView.Item(text: "World", action: (Image(systemName: "chevron.forward"), {})),
])
.padding()
}
#Preview {
MultilineInfoView(title: "One", image: Image(systemName: "figure.wave"), items: ["Hello world."])
.padding()
}

View File

@@ -1,11 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>XPCService</key>
<dict>
<key>ServiceType</key>
<string>Application</string>
</dict>
</dict>
</plist>

View File

@@ -1,22 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>com.apple.security.hardened-process</key>
<true/>
<key>com.apple.security.hardened-process.checked-allocations</key>
<true/>
<key>com.apple.security.hardened-process.checked-allocations.enable-pure-data</key>
<true/>
<key>com.apple.security.hardened-process.checked-allocations.no-tagged-receive</key>
<true/>
<key>com.apple.security.hardened-process.dyld-ro</key>
<true/>
<key>com.apple.security.hardened-process.enhanced-security-version</key>
<integer>1</integer>
<key>com.apple.security.hardened-process.hardened-heap</key>
<true/>
<key>com.apple.security.hardened-process.platform-restrictions</key>
<integer>2</integer>
</dict>
</plist>

View File

@@ -1,18 +0,0 @@
import Foundation
import OSLog
import XPCWrappers
import SSHProtocolKit
import CertificateKit
final class SecretiveCertificateParser: NSObject, XPCProtocol {
private let logger = Logger(subsystem: "com.maxgoedjen.secretive.SecretiveCertificateParser", category: "SecretiveCertificateParser")
func process(_ data: Data) async throws -> OpenSSHCertificate {
let parser = OpenSSHCertificateParser()
let result = try parser.parse(data: data)
logger.log("Parser parsed certificate")
return result
}
}

View File

@@ -1,7 +0,0 @@
import Foundation
import XPCWrappers
let delegate = XPCServiceDelegate(exportedObject: SecretiveCertificateParser())
let listener = NSXPCListener.service()
listener.delegate = delegate
listener.resume()