mirror of
https://github.com/maxgoedjen/secretive.git
synced 2026-05-07 16:08:58 +02:00
Certificate UI/Import (#798)
* Sketching out. * WIP * WIP * Dump * Apply stash * Merge + WIP * UI * More WIP * Agent config * UI cleanup * Restore dirty files * XPC * Edit/delete * UI fixes * Cleanup * Change id for OpenSSHCertificate to hex of md5 * Fix runtime warning for confirmation dialog * Mark strings as reviewed * Cleanup * Fix agent tests
This commit is contained in:
@@ -19,12 +19,18 @@ let package = Package(
|
||||
.library(
|
||||
name: "SmartCardSecretKit",
|
||||
targets: ["SmartCardSecretKit"]),
|
||||
.library(
|
||||
name: "CertificateKit",
|
||||
targets: ["CertificateKit"]),
|
||||
.library(
|
||||
name: "SecretAgentKit",
|
||||
targets: ["SecretAgentKit"]),
|
||||
.library(
|
||||
name: "Common",
|
||||
targets: ["Common"]),
|
||||
.library(
|
||||
name: "SharedXPCServices",
|
||||
targets: ["SharedXPCServices"]),
|
||||
.library(
|
||||
name: "Brief",
|
||||
targets: ["Brief"]),
|
||||
@@ -61,9 +67,15 @@ let package = Package(
|
||||
resources: [localization],
|
||||
swiftSettings: swiftSettings,
|
||||
),
|
||||
.target(
|
||||
name: "CertificateKit",
|
||||
dependencies: ["SecretKit", "SSHProtocolKit"],
|
||||
resources: [localization],
|
||||
swiftSettings: swiftSettings,
|
||||
),
|
||||
.target(
|
||||
name: "SecretAgentKit",
|
||||
dependencies: ["SecretKit", "SSHProtocolKit", "Common"],
|
||||
dependencies: ["SecretKit", "SSHProtocolKit", "CertificateKit", "Common"],
|
||||
resources: [localization],
|
||||
swiftSettings: swiftSettings,
|
||||
),
|
||||
@@ -88,6 +100,12 @@ let package = Package(
|
||||
resources: [localization],
|
||||
swiftSettings: swiftSettings,
|
||||
),
|
||||
.target(
|
||||
name: "SharedXPCServices",
|
||||
dependencies: ["CertificateKit"],
|
||||
resources: [localization],
|
||||
swiftSettings: swiftSettings,
|
||||
),
|
||||
.target(
|
||||
name: "Brief",
|
||||
dependencies: ["XPCWrappers", "SSHProtocolKit"],
|
||||
|
||||
@@ -5547,6 +5547,86 @@
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"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_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" : {
|
||||
"extractionState" : "manual",
|
||||
@@ -9994,181 +10074,181 @@
|
||||
"af" : {
|
||||
"stringUnit" : {
|
||||
"state" : "new",
|
||||
"value" : "Delete %1$(secretName)@?"
|
||||
"value" : "Delete %1$(name)@?"
|
||||
}
|
||||
},
|
||||
"ar" : {
|
||||
"stringUnit" : {
|
||||
"state" : "new",
|
||||
"value" : "Delete %1$(secretName)@?"
|
||||
"value" : "Delete %1$(name)@?"
|
||||
}
|
||||
},
|
||||
"ca" : {
|
||||
"stringUnit" : {
|
||||
"state" : "translated",
|
||||
"value" : "Esborrar %1$(secretName)@?"
|
||||
"value" : "Esborrar %1$(name)@?"
|
||||
}
|
||||
},
|
||||
"cs" : {
|
||||
"stringUnit" : {
|
||||
"state" : "new",
|
||||
"value" : "Delete %1$(secretName)@?"
|
||||
"value" : "Delete %1$(name)@?"
|
||||
}
|
||||
},
|
||||
"da" : {
|
||||
"stringUnit" : {
|
||||
"state" : "new",
|
||||
"value" : "Delete %1$(secretName)@?"
|
||||
"value" : "Delete %1$(name)@?"
|
||||
}
|
||||
},
|
||||
"de" : {
|
||||
"stringUnit" : {
|
||||
"state" : "translated",
|
||||
"value" : "%1$(secretName)@ Löschen?"
|
||||
"value" : "%1$(name)@ Löschen?"
|
||||
}
|
||||
},
|
||||
"el" : {
|
||||
"stringUnit" : {
|
||||
"state" : "new",
|
||||
"value" : "Delete %1$(secretName)@?"
|
||||
"value" : "Delete %1$(name)@?"
|
||||
}
|
||||
},
|
||||
"en" : {
|
||||
"stringUnit" : {
|
||||
"state" : "new",
|
||||
"value" : "Delete %1$(secretName)@?"
|
||||
"state" : "translated",
|
||||
"value" : "Delete %1$(name)@?"
|
||||
}
|
||||
},
|
||||
"es" : {
|
||||
"stringUnit" : {
|
||||
"state" : "new",
|
||||
"value" : "Delete %1$(secretName)@?"
|
||||
"value" : "Delete %1$(name)@?"
|
||||
}
|
||||
},
|
||||
"fi" : {
|
||||
"stringUnit" : {
|
||||
"state" : "translated",
|
||||
"value" : "Poista %1$(secretName)@?"
|
||||
"value" : "Poista %1$(name)@?"
|
||||
}
|
||||
},
|
||||
"fr" : {
|
||||
"stringUnit" : {
|
||||
"state" : "translated",
|
||||
"value" : "Supprimer %1$(secretName)@?"
|
||||
"value" : "Supprimer %1$(name)@?"
|
||||
}
|
||||
},
|
||||
"he" : {
|
||||
"stringUnit" : {
|
||||
"state" : "new",
|
||||
"value" : "Delete %1$(secretName)@?"
|
||||
"value" : "Delete %1$(name)@?"
|
||||
}
|
||||
},
|
||||
"hu" : {
|
||||
"stringUnit" : {
|
||||
"state" : "new",
|
||||
"value" : "Delete %1$(secretName)@?"
|
||||
"value" : "Delete %1$(name)@?"
|
||||
}
|
||||
},
|
||||
"it" : {
|
||||
"stringUnit" : {
|
||||
"state" : "translated",
|
||||
"value" : "Eliminare %1$(secretName)@?"
|
||||
"value" : "Eliminare %1$(name)@?"
|
||||
}
|
||||
},
|
||||
"ja" : {
|
||||
"stringUnit" : {
|
||||
"state" : "translated",
|
||||
"value" : "%1$(secretName)@を削除しますか?"
|
||||
"value" : "%1$(name)@を削除しますか?"
|
||||
}
|
||||
},
|
||||
"ko" : {
|
||||
"stringUnit" : {
|
||||
"state" : "translated",
|
||||
"value" : "%1$(secretName)@를 지우겠습니까?"
|
||||
"value" : "%1$(name)@를 지우겠습니까?"
|
||||
}
|
||||
},
|
||||
"nb" : {
|
||||
"stringUnit" : {
|
||||
"state" : "new",
|
||||
"value" : "Delete %1$(secretName)@?"
|
||||
"value" : "Delete %1$(name)@?"
|
||||
}
|
||||
},
|
||||
"nl" : {
|
||||
"stringUnit" : {
|
||||
"state" : "new",
|
||||
"value" : "Delete %1$(secretName)@?"
|
||||
"value" : "Delete %1$(name)@?"
|
||||
}
|
||||
},
|
||||
"pl" : {
|
||||
"stringUnit" : {
|
||||
"state" : "translated",
|
||||
"value" : "Usunąć %1$(secretName)@?"
|
||||
"value" : "Usunąć %1$(name)@?"
|
||||
}
|
||||
},
|
||||
"pt" : {
|
||||
"stringUnit" : {
|
||||
"state" : "new",
|
||||
"value" : "Delete %1$(secretName)@?"
|
||||
"value" : "Delete %1$(name)@?"
|
||||
}
|
||||
},
|
||||
"pt-BR" : {
|
||||
"stringUnit" : {
|
||||
"state" : "translated",
|
||||
"value" : "Deletar %1$(secretName)@?"
|
||||
"value" : "Deletar %1$(name)@?"
|
||||
}
|
||||
},
|
||||
"ro" : {
|
||||
"stringUnit" : {
|
||||
"state" : "new",
|
||||
"value" : "Delete %1$(secretName)@?"
|
||||
"value" : "Delete %1$(name)@?"
|
||||
}
|
||||
},
|
||||
"ru" : {
|
||||
"stringUnit" : {
|
||||
"state" : "translated",
|
||||
"value" : "Удалить %1$(secretName)@?"
|
||||
"value" : "Удалить %1$(name)@?"
|
||||
}
|
||||
},
|
||||
"sr" : {
|
||||
"stringUnit" : {
|
||||
"state" : "new",
|
||||
"value" : "Delete %1$(secretName)@?"
|
||||
"value" : "Delete %1$(name)@?"
|
||||
}
|
||||
},
|
||||
"sv" : {
|
||||
"stringUnit" : {
|
||||
"state" : "new",
|
||||
"value" : "Delete %1$(secretName)@?"
|
||||
"value" : "Delete %1$(name)@?"
|
||||
}
|
||||
},
|
||||
"tr" : {
|
||||
"stringUnit" : {
|
||||
"state" : "new",
|
||||
"value" : "Delete %1$(secretName)@?"
|
||||
"value" : "Delete %1$(name)@?"
|
||||
}
|
||||
},
|
||||
"uk" : {
|
||||
"stringUnit" : {
|
||||
"state" : "new",
|
||||
"value" : "Delete %1$(secretName)@?"
|
||||
"value" : "Delete %1$(name)@?"
|
||||
}
|
||||
},
|
||||
"vi" : {
|
||||
"stringUnit" : {
|
||||
"state" : "new",
|
||||
"value" : "Delete %1$(secretName)@?"
|
||||
"value" : "Delete %1$(name)@?"
|
||||
}
|
||||
},
|
||||
"zh-Hans" : {
|
||||
"stringUnit" : {
|
||||
"state" : "translated",
|
||||
"value" : "删除“%1$(secretName)@”吗?"
|
||||
"value" : "删除“%1$(name)@”吗?"
|
||||
}
|
||||
},
|
||||
"zh-Hant" : {
|
||||
"stringUnit" : {
|
||||
"state" : "translated",
|
||||
"value" : "刪除「%1$(secretName)@」嗎?"
|
||||
"value" : "刪除「%1$(name)@」嗎?"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -19637,6 +19717,28 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"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" : {
|
||||
"extractionState" : "manual",
|
||||
"localizations" : {
|
||||
@@ -19822,6 +19924,17 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"secret_detail_certificate_path_label" : {
|
||||
"extractionState" : "manual",
|
||||
"localizations" : {
|
||||
"en" : {
|
||||
"stringUnit" : {
|
||||
"state" : "translated",
|
||||
"value" : "Matching Certificates"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"secret_detail_md5_fingerprint_label" : {
|
||||
"extractionState" : "manual",
|
||||
"localizations" : {
|
||||
|
||||
152
Sources/Packages/Sources/CertificateKit/CertificateStore.swift
Normal file
152
Sources/Packages/Sources/CertificateKit/CertificateStore.swift
Normal file
@@ -0,0 +1,152 @@
|
||||
import Foundation
|
||||
import Observation
|
||||
import Security
|
||||
import os
|
||||
import SecretKit
|
||||
import SSHProtocolKit
|
||||
|
||||
@Observable @MainActor public final class CertificateStore: Sendable {
|
||||
|
||||
public private(set) var certificates: [OpenSSHCertificate] = []
|
||||
|
||||
/// 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: OpenSSHCertificate, originalData: Data) throws {
|
||||
let attributes = try JSONEncoder().encode(certificate)
|
||||
let keychainAttributes = KeychainDictionary([
|
||||
kSecClass: Constants.keyClass,
|
||||
kSecAttrService: Constants.keyTag,
|
||||
kSecAttrAccount: certificate.id,
|
||||
kSecUseDataProtectionKeychain: true,
|
||||
kSecAttrAccessible: kSecAttrAccessibleWhenUnlockedThisDeviceOnly,
|
||||
kSecValueData: originalData,
|
||||
kSecAttrGeneric: attributes
|
||||
])
|
||||
let status = SecItemAdd(keychainAttributes, nil)
|
||||
if status != errSecSuccess && status != errSecDuplicateItem {
|
||||
throw KeychainError(statusCode: status)
|
||||
}
|
||||
reloadCertificates()
|
||||
}
|
||||
|
||||
public func delete(certificate: OpenSSHCertificate) 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: OpenSSHCertificate) throws {
|
||||
let updateQuery = KeychainDictionary([
|
||||
kSecClass: Constants.keyClass,
|
||||
kSecAttrAccount: certificate.id,
|
||||
])
|
||||
|
||||
let cert = try JSONEncoder().encode(certificate)
|
||||
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) -> [OpenSSHCertificate] {
|
||||
certificates.filter { $0.publicKey == 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: [OpenSSHCertificate] = typed.compactMap {
|
||||
do {
|
||||
guard let attributesData = $0[kSecAttrGeneric] as? Data else {
|
||||
throw MissingAttributesError()
|
||||
}
|
||||
return try decoder.decode(OpenSSHCertificate.self, from: attributesData)
|
||||
} 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")
|
||||
|
||||
}
|
||||
@@ -20,6 +20,10 @@ extension URL {
|
||||
agentHomeURL.appending(component: "PublicKeys")
|
||||
}
|
||||
|
||||
public static var certificatesDirectory: URL {
|
||||
agentHomeURL.appending(component: "Certificates")
|
||||
}
|
||||
|
||||
/// The path for a Secret's public key.
|
||||
/// - Parameter secret: The Secret to return the path for.
|
||||
/// - Returns: The path to the Secret's public key.
|
||||
@@ -30,6 +34,14 @@ extension URL {
|
||||
return directory.appending(component: "\(minimalHex).pub").path()
|
||||
}
|
||||
|
||||
/// The path for a certificate.
|
||||
/// - Parameter certificate: The OpenSSHCertificate to return the path for.
|
||||
/// - Returns: The path to the OpenSSHCertificate.
|
||||
/// - 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 certificate: OpenSSHCertificate, in directory: URL) -> String {
|
||||
return directory.appending(component: "\(certificate.id)-cert.pub").path()
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
extension String {
|
||||
|
||||
@@ -28,6 +28,7 @@ extension FormatStyle where Self == HexDataStyle<Data> {
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
extension FormatStyle where Self == HexDataStyle<Insecure.MD5Digest> {
|
||||
|
||||
public static func hex(separator: String = ":") -> HexDataStyle<Insecure.MD5Digest> {
|
||||
@@ -35,3 +36,39 @@ extension FormatStyle where Self == HexDataStyle<Insecure.MD5Digest> {
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
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)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
104
Sources/Packages/Sources/SSHProtocolKit/OpenSSHCertificate.swift
Normal file
104
Sources/Packages/Sources/SSHProtocolKit/OpenSSHCertificate.swift
Normal file
@@ -0,0 +1,104 @@
|
||||
import Foundation
|
||||
import OSLog
|
||||
import CryptoKit
|
||||
|
||||
public struct OpenSSHCertificate: Sendable, Codable, Equatable, Hashable, Identifiable, CustomDebugStringConvertible {
|
||||
|
||||
public var id: String { Insecure.MD5.hash(data: data).formatted(.hex(separator: "")) }
|
||||
public var type: CertificateType
|
||||
public var name: String
|
||||
public let data: Data
|
||||
|
||||
public var publicKey: Data
|
||||
public var principals: [String]
|
||||
public var keyID: String
|
||||
public var serial: UInt64
|
||||
public var validityRange: Range<Date>?
|
||||
|
||||
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"
|
||||
|
||||
var keyIdentifier: String {
|
||||
rawValue.replacingOccurrences(of: "-cert-v01@openssh.com", with: "")
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
public protocol OpenSSHCertificateParserProtocol {
|
||||
func parse(data: Data) async throws -> OpenSSHCertificate
|
||||
}
|
||||
|
||||
public struct OpenSSHCertificateParser: OpenSSHCertificateParserProtocol, Sendable {
|
||||
|
||||
private let logger = Logger(subsystem: "com.maxgoedjen.secretive", category: "OpenSSHCertificateParser")
|
||||
|
||||
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)
|
||||
_ = try dataParser.readNextChunkAsString() // Redundant key type
|
||||
_ = try dataParser.readNextChunk() // Nonce
|
||||
_ = try dataParser.readNextChunkAsString() // curve
|
||||
let publicKey = try dataParser.readNextChunk()
|
||||
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))
|
||||
|
||||
return OpenSSHCertificate(
|
||||
type: type,
|
||||
name: comment ?? keyIdentifier,
|
||||
data: decoded,
|
||||
publicKey: publicKey,
|
||||
principals: principals,
|
||||
keyID: keyIdentifier,
|
||||
serial: serialNumber,
|
||||
validityRange: validityRange
|
||||
)
|
||||
} catch {
|
||||
throw .parsingFailed
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
public enum OpenSSHCertificateError: Error, Codable {
|
||||
case unsupportedType
|
||||
case parsingFailed
|
||||
}
|
||||
@@ -41,9 +41,7 @@ public struct OpenSSHPublicKeyWriter: Sendable {
|
||||
/// - Returns: OpenSSH SHA256 fingerprint string.
|
||||
public func openSSHSHA256Fingerprint<SecretType: Secret>(secret: SecretType) -> String {
|
||||
// OpenSSL format seems to strip the padding at the end.
|
||||
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)
|
||||
let cleaned = SHA256.hash(data: data(secret: secret)).formatted(.base64(stripPadding: true))
|
||||
return "SHA256:\(cleaned)"
|
||||
}
|
||||
|
||||
|
||||
@@ -10,6 +10,9 @@ public final class OpenSSHReader {
|
||||
/// - Parameter data: The data to read.
|
||||
public init(data: Data) {
|
||||
remaining = Data(data)
|
||||
if remaining.count == 0 {
|
||||
done = true
|
||||
}
|
||||
}
|
||||
|
||||
/// Reads the next chunk of data from the playload.
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
import Foundation
|
||||
import OSLog
|
||||
import SecretKit
|
||||
import SSHProtocolKit
|
||||
|
||||
public protocol SSHAgentInputParserProtocol {
|
||||
|
||||
@@ -14,7 +13,7 @@ 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 {
|
||||
@@ -75,21 +74,16 @@ extension SSHAgentInputParser {
|
||||
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":
|
||||
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.replacingOccurrences(of: "-cert-v01@openssh.com", with: "")
|
||||
let openSSHIdentifier = certType.keyIdentifier
|
||||
return openSSHIdentifier.lengthAndData +
|
||||
curveIdentifier.lengthAndData +
|
||||
publicKey.lengthAndData
|
||||
default:
|
||||
return nil
|
||||
}
|
||||
|
||||
} catch {
|
||||
return nil
|
||||
}
|
||||
@@ -2,6 +2,7 @@ import Foundation
|
||||
import CryptoKit
|
||||
import OSLog
|
||||
import SecretKit
|
||||
import CertificateKit
|
||||
import AppKit
|
||||
import SSHProtocolKit
|
||||
|
||||
@@ -9,23 +10,21 @@ import SSHProtocolKit
|
||||
public final class Agent: Sendable {
|
||||
|
||||
private let storeList: SecretStoreList
|
||||
private let certificateStore: CertificateStore
|
||||
private let witness: SigningWitness?
|
||||
private let publicKeyWriter = OpenSSHPublicKeyWriter()
|
||||
private let signatureWriter = OpenSSHSignatureWriter()
|
||||
private let certificateHandler = OpenSSHCertificateHandler()
|
||||
private let logger = Logger(subsystem: "com.maxgoedjen.secretive.secretagent", category: "Agent")
|
||||
|
||||
/// Initializes an agent with a store list and a witness.
|
||||
/// - Parameters:
|
||||
/// - storeList: The `SecretStoreList` to make available.
|
||||
/// - witness: A witness to notify of requests.
|
||||
public init(storeList: SecretStoreList, witness: SigningWitness? = nil) {
|
||||
public init(storeList: SecretStoreList, certificateStore: CertificateStore, witness: SigningWitness? = nil) {
|
||||
logger.debug("Agent is running")
|
||||
self.storeList = storeList
|
||||
self.certificateStore = certificateStore
|
||||
self.witness = witness
|
||||
Task { @MainActor in
|
||||
await certificateHandler.reloadCertificates(for: storeList.allSecrets)
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -68,7 +67,6 @@ extension Agent {
|
||||
/// - Returns: An OpenSSH formatted Data payload listing the identities available for signing operations.
|
||||
func identities() async -> Data {
|
||||
let secrets = await storeList.allSecrets
|
||||
await certificateHandler.reloadCertificates(for: secrets)
|
||||
var count = 0
|
||||
var keyData = Data()
|
||||
|
||||
@@ -77,10 +75,9 @@ extension Agent {
|
||||
keyData.append(keyBlob.lengthAndData)
|
||||
keyData.append(publicKeyWriter.comment(secret: secret).lengthAndData)
|
||||
count += 1
|
||||
|
||||
if let (certificateData, name) = try? await certificateHandler.keyBlobAndName(for: secret) {
|
||||
keyData.append(certificateData.lengthAndData)
|
||||
keyData.append(name.lengthAndData)
|
||||
for certificate in await certificateStore.certificates(for: secret) {
|
||||
keyData.append(certificate.data.lengthAndData)
|
||||
keyData.append(certificate.name.lengthAndData)
|
||||
count += 1
|
||||
}
|
||||
}
|
||||
@@ -97,7 +94,7 @@ extension Agent {
|
||||
/// - Returns: An OpenSSH formatted Data payload containing the signed data response.
|
||||
func sign(data: Data, keyBlob: Data, provenance: SigningRequestProvenance) async throws -> Data {
|
||||
guard let (secret, store) = await secret(matching: keyBlob) else {
|
||||
let keyBlobHex = keyBlob.compactMap { ("0" + String($0, radix: 16, uppercase: false)).suffix(2) }.joined()
|
||||
let keyBlobHex = keyBlob.formatted(.hex())
|
||||
logger.debug("Agent did not have a key matching \(keyBlobHex)")
|
||||
throw NoMatchingKeyError()
|
||||
}
|
||||
|
||||
@@ -1,89 +0,0 @@
|
||||
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"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -8,12 +8,14 @@ import Common
|
||||
public final class PublicKeyFileStoreController: Sendable {
|
||||
|
||||
private let logger = Logger(subsystem: "com.maxgoedjen.secretive.secretagent", category: "PublicKeyFileStoreController")
|
||||
private let directory: URL
|
||||
private let publicKeysURL: URL
|
||||
private let certificatesURL: URL
|
||||
private let keyWriter = OpenSSHPublicKeyWriter()
|
||||
|
||||
/// Initializes a PublicKeyFileStoreController.
|
||||
public init(directory: URL) {
|
||||
self.directory = directory
|
||||
public init(publicKeysURL: URL, certificatesURL: URL) {
|
||||
self.publicKeysURL = publicKeysURL
|
||||
self.certificatesURL = certificatesURL
|
||||
}
|
||||
|
||||
/// Writes out the keys specified to disk.
|
||||
@@ -22,10 +24,10 @@ public final class PublicKeyFileStoreController: Sendable {
|
||||
public func generatePublicKeys(for secrets: [AnySecret], clear: Bool = false) throws {
|
||||
logger.log("Writing public keys to disk")
|
||||
if clear {
|
||||
let validPaths = Set(secrets.map { URL.publicKeyPath(for: $0, in: directory) })
|
||||
.union(Set(secrets.map { sshCertificatePath(for: $0) }))
|
||||
let contentsOfDirectory = (try? FileManager.default.contentsOfDirectory(atPath: directory.path())) ?? []
|
||||
let fullPathContents = contentsOfDirectory.map { directory.appending(path: $0).path() }
|
||||
let validPaths = Set(secrets.map { URL.publicKeyPath(for: $0, in: publicKeysURL) })
|
||||
.union(Set(secrets.map { legacySSHCertificatePath(for: $0) }))
|
||||
let contentsOfDirectory = (try? FileManager.default.contentsOfDirectory(atPath: publicKeysURL.path())) ?? []
|
||||
let fullPathContents = contentsOfDirectory.map { publicKeysURL.appending(path: $0).path() }
|
||||
|
||||
let untracked = Set(fullPathContents)
|
||||
.subtracting(validPaths)
|
||||
@@ -34,35 +36,47 @@ public final class PublicKeyFileStoreController: Sendable {
|
||||
try? FileManager.default.removeItem(at: URL(string: path)!)
|
||||
}
|
||||
}
|
||||
try? FileManager.default.createDirectory(at: directory, withIntermediateDirectories: false, attributes: nil)
|
||||
try? FileManager.default.createDirectory(at: publicKeysURL, withIntermediateDirectories: false, attributes: nil)
|
||||
for secret in secrets {
|
||||
let path = URL.publicKeyPath(for: secret, in: directory)
|
||||
let path = URL.publicKeyPath(for: secret, in: publicKeysURL)
|
||||
let data = Data(keyWriter.openSSHString(secret: secret).utf8)
|
||||
FileManager.default.createFile(atPath: path, contents: data, attributes: nil)
|
||||
}
|
||||
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: [OpenSSHCertificate], clear: Bool = false) throws {
|
||||
logger.log("Writing certificates to disk")
|
||||
if clear {
|
||||
let validPaths = Set(certificates.map { URL.certificatePath(for: $0, in: certificatesURL) })
|
||||
let contentsOfDirectory = (try? FileManager.default.contentsOfDirectory(atPath: certificatesURL.path())) ?? []
|
||||
let fullPathContents = contentsOfDirectory.map { certificatesURL.appending(path: $0).path() }
|
||||
|
||||
/// Short-circuit check to ship enumerating a bunch of paths if there's nothing in the cert directory.
|
||||
public var hasAnyCertificates: Bool {
|
||||
do {
|
||||
return try FileManager.default
|
||||
.contentsOfDirectory(atPath: directory.path())
|
||||
.filter { $0.hasSuffix("-cert.pub") }
|
||||
.isEmpty == false
|
||||
} catch {
|
||||
return false
|
||||
let untracked = Set(fullPathContents)
|
||||
.subtracting(validPaths)
|
||||
for path in untracked {
|
||||
// string instead of fileURLWithPath since we're already using fileURL format.
|
||||
try? FileManager.default.removeItem(at: URL(string: path)!)
|
||||
}
|
||||
}
|
||||
try? FileManager.default.createDirectory(at: certificatesURL, withIntermediateDirectories: false, attributes: nil)
|
||||
for certificate in certificates {
|
||||
let path = URL.certificatePath(for: certificate, in: certificatesURL)
|
||||
FileManager.default.createFile(atPath: path, contents: certificate.data, attributes: nil)
|
||||
}
|
||||
logger.log("Finished writing certificates")
|
||||
}
|
||||
|
||||
/// The path for a Secret's SSH Certificate public key.
|
||||
/// - Parameter secret: The Secret to return the path for.
|
||||
/// - 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.
|
||||
public func sshCertificatePath<SecretType: Secret>(for secret: SecretType) -> String {
|
||||
private func legacySSHCertificatePath<SecretType: Secret>(for secret: SecretType) -> String {
|
||||
let minimalHex = keyWriter.openSSHMD5Fingerprint(secret: secret).replacingOccurrences(of: ":", with: "")
|
||||
return directory.appending(component: "\(minimalHex)-cert.pub").path()
|
||||
return publicKeysURL.appending(component: "\(minimalHex)-cert.pub").path()
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -0,0 +1,46 @@
|
||||
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 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: cert, originalData: 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)")
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,28 @@
|
||||
import Foundation
|
||||
import OSLog
|
||||
import SSHProtocolKit
|
||||
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()
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,16 +1,17 @@
|
||||
import Foundation
|
||||
import Testing
|
||||
import CryptoKit
|
||||
import CertificateKit
|
||||
@testable import SSHProtocolKit
|
||||
@testable import SecretKit
|
||||
@testable import SecretAgentKit
|
||||
|
||||
@Suite struct AgentTests {
|
||||
@Suite @MainActor struct AgentTests {
|
||||
|
||||
// MARK: Identity Listing
|
||||
|
||||
@Test func emptyStores() async throws {
|
||||
let agent = Agent(storeList: SecretStoreList())
|
||||
let agent = Agent(storeList: SecretStoreList(), certificateStore: CertificateStore())
|
||||
let request = try SSHAgentInputParser().parse(data: Constants.Requests.requestIdentities)
|
||||
let response = await agent.handle(request: request, provenance: .test)
|
||||
#expect(response == Constants.Responses.requestIdentitiesEmpty)
|
||||
@@ -18,7 +19,7 @@ import CryptoKit
|
||||
|
||||
@Test func identitiesList() async throws {
|
||||
let list = await storeList(with: [Constants.Secrets.ecdsa256Secret, Constants.Secrets.ecdsa384Secret])
|
||||
let agent = Agent(storeList: list)
|
||||
let agent = Agent(storeList: list, certificateStore: CertificateStore())
|
||||
let request = try SSHAgentInputParser().parse(data: Constants.Requests.requestIdentities)
|
||||
let response = await agent.handle(request: request, provenance: .test)
|
||||
|
||||
@@ -32,7 +33,7 @@ import CryptoKit
|
||||
|
||||
@Test func noMatchingIdentities() async throws {
|
||||
let list = await storeList(with: [Constants.Secrets.ecdsa256Secret, Constants.Secrets.ecdsa384Secret])
|
||||
let agent = Agent(storeList: list)
|
||||
let agent = Agent(storeList: list, certificateStore: CertificateStore())
|
||||
let request = try SSHAgentInputParser().parse(data: Constants.Requests.requestSignatureWithNoneMatching)
|
||||
let response = await agent.handle(request: request, provenance: .test)
|
||||
#expect(response == Constants.Responses.requestFailure)
|
||||
@@ -42,7 +43,7 @@ import CryptoKit
|
||||
let request = try SSHAgentInputParser().parse(data: Constants.Requests.requestSignature)
|
||||
guard case SSHAgent.Request.signRequest(let context) = request else { return }
|
||||
let list = await storeList(with: [Constants.Secrets.ecdsa256Secret, Constants.Secrets.ecdsa384Secret])
|
||||
let agent = Agent(storeList: list)
|
||||
let agent = Agent(storeList: list, certificateStore: CertificateStore())
|
||||
let response = await agent.handle(request: request, provenance: .test)
|
||||
let responseReader = OpenSSHReader(data: response)
|
||||
let length = try responseReader.readNextBytes(as: UInt32.self)
|
||||
@@ -77,7 +78,7 @@ import CryptoKit
|
||||
let witness = StubWitness(speakNow: { _,_ in
|
||||
return true
|
||||
}, witness: { _, _ in })
|
||||
let agent = Agent(storeList: list, witness: witness)
|
||||
let agent = Agent(storeList: list, certificateStore: CertificateStore(), witness: witness)
|
||||
let response = await agent.handle(request: .signRequest(.empty), provenance: .test)
|
||||
#expect(response == Constants.Responses.requestFailure)
|
||||
}
|
||||
@@ -90,7 +91,7 @@ import CryptoKit
|
||||
}, witness: { _, trace in
|
||||
witnessed = true
|
||||
})
|
||||
let agent = Agent(storeList: list, witness: witness)
|
||||
let agent = Agent(storeList: list, certificateStore: CertificateStore(), witness: witness)
|
||||
let request = try SSHAgentInputParser().parse(data: Constants.Requests.requestSignature)
|
||||
_ = await agent.handle(request: request, provenance: .test)
|
||||
#expect(witnessed)
|
||||
@@ -106,7 +107,7 @@ import CryptoKit
|
||||
}, witness: { _, trace in
|
||||
witnessTrace = trace
|
||||
})
|
||||
let agent = Agent(storeList: list, witness: witness)
|
||||
let agent = Agent(storeList: list, certificateStore: CertificateStore(), witness: witness)
|
||||
let request = try SSHAgentInputParser().parse(data: Constants.Requests.requestSignature)
|
||||
_ = await agent.handle(request: request, provenance: .test)
|
||||
#expect(witnessTrace == speakNowTrace)
|
||||
@@ -117,9 +118,9 @@ import CryptoKit
|
||||
|
||||
@Test func signatureException() async throws {
|
||||
let list = await storeList(with: [Constants.Secrets.ecdsa256Secret, Constants.Secrets.ecdsa384Secret])
|
||||
let store = await list.stores.first?.base as! Stub.Store
|
||||
let store = list.stores.first?.base as! Stub.Store
|
||||
store.shouldThrow = true
|
||||
let agent = Agent(storeList: list)
|
||||
let agent = Agent(storeList: list, certificateStore: CertificateStore())
|
||||
let request = try SSHAgentInputParser().parse(data: Constants.Requests.requestSignature)
|
||||
let response = await agent.handle(request: request, provenance: .test)
|
||||
#expect(response == Constants.Responses.requestFailure)
|
||||
@@ -128,7 +129,7 @@ import CryptoKit
|
||||
// MARK: Unsupported
|
||||
|
||||
@Test func unhandledAdd() async throws {
|
||||
let agent = Agent(storeList: SecretStoreList())
|
||||
let agent = Agent(storeList: SecretStoreList(), certificateStore: CertificateStore())
|
||||
let response = await agent.handle(request: .addIdentity, provenance: .test)
|
||||
#expect(response == Constants.Responses.requestFailure)
|
||||
}
|
||||
@@ -143,7 +144,7 @@ extension SigningRequestProvenance {
|
||||
|
||||
extension AgentTests {
|
||||
|
||||
@MainActor func storeList(with secrets: [Stub.Secret]) async -> SecretStoreList {
|
||||
func storeList(with secrets: [Stub.Secret]) async -> SecretStoreList {
|
||||
let store = Stub.Store()
|
||||
store.secrets.append(contentsOf: secrets)
|
||||
let storeList = SecretStoreList()
|
||||
|
||||
@@ -6,8 +6,21 @@ import SmartCardSecretKit
|
||||
import SecretAgentKit
|
||||
import Brief
|
||||
import Observation
|
||||
import SSHProtocolKit
|
||||
import CertificateKit
|
||||
import Common
|
||||
import SwiftUI
|
||||
|
||||
extension EnvironmentValues {
|
||||
|
||||
@MainActor fileprivate static let _certificateStore: CertificateStore = CertificateStore()
|
||||
|
||||
@MainActor var certificateStore: CertificateStore {
|
||||
EnvironmentValues._certificateStore
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
@main
|
||||
class AppDelegate: NSObject, NSApplicationDelegate {
|
||||
|
||||
@@ -18,13 +31,15 @@ class AppDelegate: NSObject, NSApplicationDelegate {
|
||||
try? migrator.migrate(to: cryptoKit)
|
||||
list.add(store: cryptoKit)
|
||||
list.add(store: SmartCard.Store())
|
||||
let certsMigrator = CertificateMigrator(homeDirectory: URL.homeDirectory, certificateStore: EnvironmentValues._certificateStore)
|
||||
try? certsMigrator.migrate()
|
||||
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 let publicKeyFileStoreController = PublicKeyFileStoreController(publicKeysURL: URL.publicKeyDirectory, certificatesURL: URL.certificatesDirectory)
|
||||
@MainActor private lazy var agent: Agent = {
|
||||
Agent(storeList: storeList, certificateStore: EnvironmentValues._certificateStore, witness: notifier)
|
||||
}()
|
||||
private lazy var socketController: SocketController = {
|
||||
let path = URL.socketPath as String
|
||||
@@ -55,7 +70,13 @@ class AppDelegate: NSObject, NSApplicationDelegate {
|
||||
try? publicKeyFileStoreController.generatePublicKeys(for: storeList.allSecrets, clear: true)
|
||||
}
|
||||
}
|
||||
Task {
|
||||
for await _ in NotificationCenter.default.notifications(named: .certificateStoreReloaded) {
|
||||
try? publicKeyFileStoreController.generateCertificates(for: EnvironmentValues._certificateStore.certificates, clear: true)
|
||||
}
|
||||
}
|
||||
try? publicKeyFileStoreController.generatePublicKeys(for: storeList.allSecrets, clear: true)
|
||||
try? publicKeyFileStoreController.generateCertificates(for: EnvironmentValues._certificateStore.certificates, clear: true)
|
||||
notifier.prompt()
|
||||
_ = withObservationTracking {
|
||||
updater.update
|
||||
|
||||
47
Sources/SecretAgent/CertificateMigrator.swift
Normal file
47
Sources/SecretAgent/CertificateMigrator.swift
Normal file
@@ -0,0 +1,47 @@
|
||||
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: cert, originalData: 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)")
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,5 +1,6 @@
|
||||
import Foundation
|
||||
import SecretAgentKit
|
||||
import OSLog
|
||||
import SSHProtocolKit
|
||||
import Brief
|
||||
import XPCWrappers
|
||||
import OSLog
|
||||
|
||||
@@ -31,6 +31,7 @@
|
||||
504788F62E68206F00B4556F /* GettingStartedView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 504788F52E68206F00B4556F /* GettingStartedView.swift */; };
|
||||
504789232E697DD300B4556F /* BoxBackgroundStyle.swift in Sources */ = {isa = PBXBuildFile; fileRef = 504789222E697DD300B4556F /* BoxBackgroundStyle.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 */; };
|
||||
50617D8523FCE48E0099B055 /* ContentView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 50617D8423FCE48E0099B055 /* ContentView.swift */; };
|
||||
50617D8A23FCE48E0099B055 /* Preview Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 50617D8923FCE48E0099B055 /* Preview Assets.xcassets */; };
|
||||
@@ -71,6 +72,20 @@
|
||||
50CF4ABC2E601B0F005588DC /* ActionButtonStyle.swift in Sources */ = {isa = PBXBuildFile; fileRef = 50CF4ABB2E601B0F005588DC /* ActionButtonStyle.swift */; };
|
||||
50E0145C2EDB9CDF00B121F1 /* Common in Frameworks */ = {isa = PBXBuildFile; productRef = 50E0145B2EDB9CDF00B121F1 /* Common */; };
|
||||
50E0145E2EDB9CE400B121F1 /* Common in Frameworks */ = {isa = PBXBuildFile; productRef = 50E0145D2EDB9CE400B121F1 /* Common */; };
|
||||
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 */; };
|
||||
50E4C4532E73C78C00C73783 /* WindowBackgroundStyle.swift in Sources */ = {isa = PBXBuildFile; fileRef = 50E4C4522E73C78900C73783 /* WindowBackgroundStyle.swift */; };
|
||||
50E4C4C32E7765DF00C73783 /* AboutView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 50E4C4C22E7765DF00C73783 /* AboutView.swift */; };
|
||||
50E4C4C82E777E4200C73783 /* AppIcon.icon in Resources */ = {isa = PBXBuildFile; fileRef = 50E4C4C72E777E4200C73783 /* AppIcon.icon */; };
|
||||
@@ -120,6 +135,20 @@
|
||||
remoteGlobalIDString = 50692E4F2E6FF9D20043C7BB;
|
||||
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 */
|
||||
|
||||
/* Begin PBXCopyFilesBuildPhase section */
|
||||
@@ -129,6 +158,7 @@
|
||||
dstPath = "$(CONTENTS_FOLDER_PATH)/XPCServices";
|
||||
dstSubfolderSpec = 16;
|
||||
files = (
|
||||
50E2051D2FAAB81C00402380 /* SecretiveCertificateParser.xpc in Embed XPC Services */,
|
||||
50692D1D2E6FDB880043C7BB /* SecretiveUpdater.xpc in Embed XPC Services */,
|
||||
50692E5B2E6FF9D20043C7BB /* SecretAgentInputParser.xpc in Embed XPC Services */,
|
||||
);
|
||||
@@ -142,6 +172,7 @@
|
||||
dstSubfolderSpec = 16;
|
||||
files = (
|
||||
50692E6D2E6FFA5F0043C7BB /* SecretiveUpdater.xpc in Embed XPC Services */,
|
||||
50E2052D2FAAB92000402380 /* SecretiveCertificateParser.xpc in Embed XPC Services */,
|
||||
50692E702E6FFA6E0043C7BB /* SecretAgentInputParser.xpc in Embed XPC Services */,
|
||||
);
|
||||
name = "Embed XPC Services";
|
||||
@@ -238,6 +269,17 @@
|
||||
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>"; };
|
||||
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>"; };
|
||||
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>"; };
|
||||
@@ -252,8 +294,10 @@
|
||||
50E0145C2EDB9CDF00B121F1 /* Common in Frameworks */,
|
||||
5003EF3B278005E800DF2006 /* SecretKit in Frameworks */,
|
||||
501421622781262300BBAA70 /* Brief in Frameworks */,
|
||||
50E205842FAB296A00402380 /* SharedXPCServices in Frameworks */,
|
||||
5003EF5F2780081600DF2006 /* SecureEnclaveSecretKit in Frameworks */,
|
||||
5003EF612780081600DF2006 /* SmartCardSecretKit in Frameworks */,
|
||||
505F5EF22FA9635700C45824 /* CertificateKit in Frameworks */,
|
||||
);
|
||||
runOnlyForDeploymentPostprocessing = 0;
|
||||
};
|
||||
@@ -282,12 +326,22 @@
|
||||
5003EF3D278005F300DF2006 /* Brief in Frameworks */,
|
||||
5003EF632780081B00DF2006 /* SecureEnclaveSecretKit in Frameworks */,
|
||||
5003EF652780081B00DF2006 /* SmartCardSecretKit in Frameworks */,
|
||||
50E205822FAB293B00402380 /* SharedXPCServices in Frameworks */,
|
||||
5003EF3F278005F300DF2006 /* SecretAgentKit in Frameworks */,
|
||||
5003EF41278005FA00DF2006 /* SecretKit in Frameworks */,
|
||||
50E0145E2EDB9CE400B121F1 /* Common in Frameworks */,
|
||||
);
|
||||
runOnlyForDeploymentPostprocessing = 0;
|
||||
};
|
||||
50E205112FAAB81C00402380 /* Frameworks */ = {
|
||||
isa = PBXFrameworksBuildPhase;
|
||||
buildActionMask = 2147483647;
|
||||
files = (
|
||||
50E205332FAAB95A00402380 /* SSHProtocolKit in Frameworks */,
|
||||
50E205312FAAB95500402380 /* XPCWrappers in Frameworks */,
|
||||
);
|
||||
runOnlyForDeploymentPostprocessing = 0;
|
||||
};
|
||||
/* End PBXFrameworksBuildPhase section */
|
||||
|
||||
/* Begin PBXGroup section */
|
||||
@@ -309,10 +363,14 @@
|
||||
5099A02323FD2AAA0062B6F2 /* CreateSecretView.swift */,
|
||||
50B8550C24138C4F009958AC /* DeleteSecretView.swift */,
|
||||
2C4A9D2E2636FFD3008CC8E2 /* EditSecretView.swift */,
|
||||
50E205342FAABC6300402380 /* DeleteCertificateView.swift */,
|
||||
50E205352FAABC6300402380 /* EditCertificateView.swift */,
|
||||
50BB046A2418AAAE00D6E079 /* EmptyStoreView.swift */,
|
||||
506772C82425BB8500034DED /* NoStoresView.swift */,
|
||||
50C385A42407A76D00AF2719 /* SecretDetailView.swift */,
|
||||
50E204E82FA9D12700402380 /* CertificateDetailView.swift */,
|
||||
50153E21250DECA300525160 /* SecretListItemView.swift */,
|
||||
50E204EC2FAA997F00402380 /* CertificateListItemView.swift */,
|
||||
5079BA0E250F29BF00EA86F4 /* StoreListView.swift */,
|
||||
);
|
||||
path = Secrets;
|
||||
@@ -338,6 +396,7 @@
|
||||
50BDCB712E63BAF20072D2E7 /* AgentStatusView.swift */,
|
||||
50617D8423FCE48E0099B055 /* ContentView.swift */,
|
||||
5066A6C72516FE6E004B5A36 /* CopyableView.swift */,
|
||||
50E204EE2FAA9C1400402380 /* MultilineInfoView.swift */,
|
||||
50153E1F250AFCB200525160 /* UpdateView.swift */,
|
||||
);
|
||||
path = Views;
|
||||
@@ -352,6 +411,7 @@
|
||||
508A58AF241E144C0069DC07 /* Config */,
|
||||
50692D272E6FDB8D0043C7BB /* SecretiveUpdater */,
|
||||
50692E662E6FF9E20043C7BB /* SecretAgentInputParser */,
|
||||
50E205262FAAB82700402380 /* SecretiveCertificateParser */,
|
||||
50617D8023FCE48E0099B055 /* Products */,
|
||||
5099A08B240243730062B6F2 /* Frameworks */,
|
||||
);
|
||||
@@ -364,6 +424,7 @@
|
||||
50A3B78A24026B7500D209EA /* SecretAgent.app */,
|
||||
50692D122E6FDB880043C7BB /* SecretiveUpdater.xpc */,
|
||||
50692E502E6FF9D20043C7BB /* SecretAgentInputParser.xpc */,
|
||||
50E205142FAAB81C00402380 /* SecretiveCertificateParser.xpc */,
|
||||
);
|
||||
name = Products;
|
||||
sourceTree = "<group>";
|
||||
@@ -462,6 +523,7 @@
|
||||
50020BAF24064869003D4025 /* AppDelegate.swift */,
|
||||
5018F54E24064786002EB505 /* Notifier.swift */,
|
||||
501578122E6C0479004A37D0 /* XPCInputParser.swift */,
|
||||
50E2057F2FAB291E00402380 /* CertificateMigrator.swift */,
|
||||
50A3B79524026B7600D209EA /* Main.storyboard */,
|
||||
50A3B79824026B7600D209EA /* Info.plist */,
|
||||
508BF29425B4F140009EFB7E /* InternetAccessPolicy.plist */,
|
||||
@@ -479,6 +541,17 @@
|
||||
path = "Preview Content";
|
||||
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 */
|
||||
|
||||
/* Begin PBXNativeTarget section */
|
||||
@@ -499,6 +572,7 @@
|
||||
50142167278126B500BBAA70 /* PBXTargetDependency */,
|
||||
50692D1C2E6FDB880043C7BB /* PBXTargetDependency */,
|
||||
50692E5A2E6FF9D20043C7BB /* PBXTargetDependency */,
|
||||
50E2051C2FAAB81C00402380 /* PBXTargetDependency */,
|
||||
);
|
||||
name = Secretive;
|
||||
packageProductDependencies = (
|
||||
@@ -507,6 +581,8 @@
|
||||
5003EF602780081600DF2006 /* SmartCardSecretKit */,
|
||||
501421612781262300BBAA70 /* Brief */,
|
||||
50E0145B2EDB9CDF00B121F1 /* Common */,
|
||||
505F5EF12FA9635700C45824 /* CertificateKit */,
|
||||
50E205832FAB296A00402380 /* SharedXPCServices */,
|
||||
);
|
||||
productName = Secretive;
|
||||
productReference = 50617D7F23FCE48E0099B055 /* Secretive.app */;
|
||||
@@ -570,6 +646,7 @@
|
||||
501577D42E6BC5DD004A37D0 /* PBXTargetDependency */,
|
||||
50692E6F2E6FFA5F0043C7BB /* PBXTargetDependency */,
|
||||
50692E722E6FFA6E0043C7BB /* PBXTargetDependency */,
|
||||
50E2052F2FAAB92000402380 /* PBXTargetDependency */,
|
||||
);
|
||||
name = SecretAgent;
|
||||
packageProductDependencies = (
|
||||
@@ -579,11 +656,33 @@
|
||||
5003EF622780081B00DF2006 /* SecureEnclaveSecretKit */,
|
||||
5003EF642780081B00DF2006 /* SmartCardSecretKit */,
|
||||
50E0145D2EDB9CE400B121F1 /* Common */,
|
||||
50E205812FAB293B00402380 /* SharedXPCServices */,
|
||||
);
|
||||
productName = SecretAgent;
|
||||
productReference = 50A3B78A24026B7500D209EA /* SecretAgent.app */;
|
||||
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 */,
|
||||
);
|
||||
productName = SecretAgentCertificateParser;
|
||||
productReference = 50E205142FAAB81C00402380 /* SecretiveCertificateParser.xpc */;
|
||||
productType = "com.apple.product-type.xpc-service";
|
||||
};
|
||||
/* End PBXNativeTarget section */
|
||||
|
||||
/* Begin PBXProject section */
|
||||
@@ -591,7 +690,7 @@
|
||||
isa = PBXProject;
|
||||
attributes = {
|
||||
BuildIndependentTargetsInParallel = YES;
|
||||
LastSwiftUpdateCheck = 2600;
|
||||
LastSwiftUpdateCheck = 2650;
|
||||
LastUpgradeCheck = 2640;
|
||||
ORGANIZATIONNAME = "Max Goedjen";
|
||||
TargetAttributes = {
|
||||
@@ -607,6 +706,9 @@
|
||||
50A3B78924026B7500D209EA = {
|
||||
CreatedOnToolsVersion = 11.4;
|
||||
};
|
||||
50E205132FAAB81C00402380 = {
|
||||
CreatedOnToolsVersion = 26.5;
|
||||
};
|
||||
};
|
||||
};
|
||||
buildConfigurationList = 50617D7A23FCE48D0099B055 /* Build configuration list for PBXProject "Secretive" */;
|
||||
@@ -634,6 +736,7 @@
|
||||
50A3B78924026B7500D209EA /* SecretAgent */,
|
||||
50692D112E6FDB880043C7BB /* SecretiveUpdater */,
|
||||
50692E4F2E6FF9D20043C7BB /* SecretAgentInputParser */,
|
||||
50E205132FAAB81C00402380 /* SecretiveCertificateParser */,
|
||||
);
|
||||
};
|
||||
/* End PBXProject section */
|
||||
@@ -677,6 +780,13 @@
|
||||
);
|
||||
runOnlyForDeploymentPostprocessing = 0;
|
||||
};
|
||||
50E205122FAAB81C00402380 /* Resources */ = {
|
||||
isa = PBXResourcesBuildPhase;
|
||||
buildActionMask = 2147483647;
|
||||
files = (
|
||||
);
|
||||
runOnlyForDeploymentPostprocessing = 0;
|
||||
};
|
||||
/* End PBXResourcesBuildPhase section */
|
||||
|
||||
/* Begin PBXSourcesBuildPhase section */
|
||||
@@ -689,13 +799,16 @@
|
||||
50E4C4C32E7765DF00C73783 /* AboutView.swift in Sources */,
|
||||
2C4A9D2F2636FFD3008CC8E2 /* EditSecretView.swift in Sources */,
|
||||
50E4C4532E73C78C00C73783 /* WindowBackgroundStyle.swift in Sources */,
|
||||
50E204E92FA9D12700402380 /* CertificateDetailView.swift in Sources */,
|
||||
5091D2BC25183B830049FD9B /* ApplicationDirectoryController.swift in Sources */,
|
||||
504789232E697DD300B4556F /* BoxBackgroundStyle.swift in Sources */,
|
||||
50E204EF2FAA9C1400402380 /* MultilineInfoView.swift in Sources */,
|
||||
5066A6C22516F303004B5A36 /* SetupView.swift in Sources */,
|
||||
5065E313295517C500E16645 /* ToolbarButtonStyle.swift in Sources */,
|
||||
50617D8523FCE48E0099B055 /* ContentView.swift in Sources */,
|
||||
504788F62E68206F00B4556F /* GettingStartedView.swift in Sources */,
|
||||
50CF4ABC2E601B0F005588DC /* ActionButtonStyle.swift in Sources */,
|
||||
50E204ED2FAA997F00402380 /* CertificateListItemView.swift in Sources */,
|
||||
50571E0324393C2600F76F6C /* JustUpdatedChecker.swift in Sources */,
|
||||
5079BA0F250F29BF00EA86F4 /* StoreListView.swift in Sources */,
|
||||
50617DD223FCEFA90099B055 /* PreviewStore.swift in Sources */,
|
||||
@@ -708,12 +821,14 @@
|
||||
5066A6C82516FE6E004B5A36 /* CopyableView.swift in Sources */,
|
||||
50B8550D24138C4F009958AC /* DeleteSecretView.swift in Sources */,
|
||||
50BB046B2418AAAE00D6E079 /* EmptyStoreView.swift in Sources */,
|
||||
50E205372FAABC6300402380 /* DeleteCertificateView.swift in Sources */,
|
||||
50BDCB762E6450950072D2E7 /* ConfigurationItemView.swift in Sources */,
|
||||
50617D8323FCE48E0099B055 /* App.swift in Sources */,
|
||||
504788F42E681F6900B4556F /* ToolConfigurationView.swift in Sources */,
|
||||
506772C92425BB8500034DED /* NoStoresView.swift in Sources */,
|
||||
50153E22250DECA300525160 /* SecretListItemView.swift in Sources */,
|
||||
508A58B5241ED48F0069DC07 /* PreviewAgentStatusChecker.swift in Sources */,
|
||||
50E205362FAABC6300402380 /* EditCertificateView.swift in Sources */,
|
||||
508A58AA241E06B40069DC07 /* PreviewUpdater.swift in Sources */,
|
||||
);
|
||||
runOnlyForDeploymentPostprocessing = 0;
|
||||
@@ -740,12 +855,22 @@
|
||||
isa = PBXSourcesBuildPhase;
|
||||
buildActionMask = 2147483647;
|
||||
files = (
|
||||
50E205802FAB291E00402380 /* CertificateMigrator.swift in Sources */,
|
||||
50020BB024064869003D4025 /* AppDelegate.swift in Sources */,
|
||||
5018F54F24064786002EB505 /* Notifier.swift in Sources */,
|
||||
501578132E6C0479004A37D0 /* XPCInputParser.swift in Sources */,
|
||||
);
|
||||
runOnlyForDeploymentPostprocessing = 0;
|
||||
};
|
||||
50E205102FAAB81C00402380 /* Sources */ = {
|
||||
isa = PBXSourcesBuildPhase;
|
||||
buildActionMask = 2147483647;
|
||||
files = (
|
||||
50E205282FAAB82700402380 /* main.swift in Sources */,
|
||||
50E2052C2FAAB85000402380 /* SecretiveCertificateParser.swift in Sources */,
|
||||
);
|
||||
runOnlyForDeploymentPostprocessing = 0;
|
||||
};
|
||||
/* End PBXSourcesBuildPhase section */
|
||||
|
||||
/* Begin PBXTargetDependency section */
|
||||
@@ -778,6 +903,16 @@
|
||||
target = 50692E4F2E6FF9D20043C7BB /* SecretAgentInputParser */;
|
||||
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 */
|
||||
|
||||
/* Begin PBXVariantGroup section */
|
||||
@@ -1479,6 +1614,101 @@
|
||||
};
|
||||
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 = 26.5;
|
||||
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 = 26.5;
|
||||
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 = 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 = 26.5;
|
||||
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 = Release;
|
||||
};
|
||||
/* End XCBuildConfiguration section */
|
||||
|
||||
/* Begin XCConfigurationList section */
|
||||
@@ -1532,6 +1762,16 @@
|
||||
defaultConfigurationIsVisible = 0;
|
||||
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 */
|
||||
|
||||
/* Begin XCSwiftPackageProductDependency section */
|
||||
@@ -1575,6 +1815,10 @@
|
||||
isa = XCSwiftPackageProductDependency;
|
||||
productName = Brief;
|
||||
};
|
||||
505F5EF12FA9635700C45824 /* CertificateKit */ = {
|
||||
isa = XCSwiftPackageProductDependency;
|
||||
productName = CertificateKit;
|
||||
};
|
||||
50692D2C2E6FDC000043C7BB /* XPCWrappers */ = {
|
||||
isa = XCSwiftPackageProductDependency;
|
||||
productName = XPCWrappers;
|
||||
@@ -1595,6 +1839,22 @@
|
||||
isa = XCSwiftPackageProductDependency;
|
||||
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;
|
||||
};
|
||||
/* End XCSwiftPackageProductDependency section */
|
||||
};
|
||||
rootObject = 50617D7723FCE48D0099B055 /* Project object */;
|
||||
|
||||
@@ -23,7 +23,7 @@
|
||||
</BuildActionEntries>
|
||||
</BuildAction>
|
||||
<TestAction
|
||||
buildConfiguration = "Test"
|
||||
buildConfiguration = "Debug"
|
||||
selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
|
||||
selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
|
||||
shouldUseLaunchSchemeArgsEnv = "YES">
|
||||
|
||||
@@ -3,6 +3,7 @@ import SecretKit
|
||||
import SecureEnclaveSecretKit
|
||||
import SmartCardSecretKit
|
||||
import Brief
|
||||
import CertificateKit
|
||||
|
||||
@main
|
||||
struct Secretive: App {
|
||||
@@ -14,6 +15,7 @@ struct Secretive: App {
|
||||
WindowGroup {
|
||||
ContentView()
|
||||
.environment(EnvironmentValues._secretStoreList)
|
||||
.environment(EnvironmentValues._certificateStore)
|
||||
.onReceive(NotificationCenter.default.publisher(for: NSApplication.didBecomeActiveNotification)) { _ in
|
||||
Task {
|
||||
@AppStorage("defaultsHasRunSetup") var hasRunSetup = false
|
||||
@@ -92,15 +94,18 @@ extension EnvironmentValues {
|
||||
@MainActor fileprivate static let _secretStoreList: SecretStoreList = {
|
||||
let list = SecretStoreList()
|
||||
let cryptoKit = SecureEnclave.Store()
|
||||
let migrator = SecureEnclave.CryptoKitMigrator()
|
||||
try? migrator.migrate(to: cryptoKit)
|
||||
let cryptoKitMigrator = SecureEnclave.CryptoKitMigrator()
|
||||
try? cryptoKitMigrator.migrate(to: cryptoKit)
|
||||
list.add(store: cryptoKit)
|
||||
list.add(store: SmartCard.Store())
|
||||
return list
|
||||
}()
|
||||
|
||||
@MainActor fileprivate static let _certificateStore: CertificateStore = CertificateStore()
|
||||
|
||||
private static let _agentLaunchController = AgentLaunchController()
|
||||
@Entry var agentLaunchController: any AgentLaunchControllerProtocol = _agentLaunchController
|
||||
|
||||
private static let _updater: any UpdaterProtocol = {
|
||||
@AppStorage("defaultsHasRunSetup") var hasRunSetup = false
|
||||
return Updater(checkOnLaunch: hasRunSetup)
|
||||
@@ -113,6 +118,10 @@ extension EnvironmentValues {
|
||||
@MainActor var secretStoreList: SecretStoreList {
|
||||
EnvironmentValues._secretStoreList
|
||||
}
|
||||
|
||||
@MainActor var certificateStore: CertificateStore {
|
||||
EnvironmentValues._certificateStore
|
||||
}
|
||||
}
|
||||
|
||||
extension FocusedValues {
|
||||
|
||||
68
Sources/Secretive/Views/Secrets/CertificateDetailView.swift
Normal file
68
Sources/Secretive/Views/Secrets/CertificateDetailView.swift
Normal file
@@ -0,0 +1,68 @@
|
||||
import SwiftUI
|
||||
import SecretKit
|
||||
import Common
|
||||
import SSHProtocolKit
|
||||
|
||||
struct CertificateDetailView: View {
|
||||
|
||||
let certificate: OpenSSHCertificate
|
||||
|
||||
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)
|
||||
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):
|
||||
MultilineInfoView(title: .certificateDetailValidUntilLabel, image: Image(systemName: "calendar.badge.clock"), items: [otherEnd.formatted()])
|
||||
Spacer()
|
||||
.frame(height: 20)
|
||||
case (let otherStart, end):
|
||||
MultilineInfoView(title: .certificateDetailValidAfterLabel, image: Image(systemName: "calendar.badge.clock"), items: [otherStart.formatted()])
|
||||
Spacer()
|
||||
.frame(height: 20)
|
||||
default:
|
||||
MultilineInfoView(title: .certificateDetailValidityRangeLabel, image: Image(systemName: "calendar.badge.clock"), items: [validityRange.formatted()])
|
||||
Spacer()
|
||||
.frame(height: 20)
|
||||
}
|
||||
}
|
||||
if !certificate.principals.isEmpty {
|
||||
MultilineInfoView(title: .certificateDetailPrincipalsLabel, image: Image(systemName: "person.2"), items: certificate.principals)
|
||||
Spacer()
|
||||
.frame(height: 20)
|
||||
}
|
||||
CopyableView(
|
||||
title: .certificateDetailPathLabel,
|
||||
image: Image(systemName: "checkmark.seal.text.page"),
|
||||
text: URL.certificatePath(for: certificate, in: URL.certificatesDirectory),
|
||||
showRevealInFinder: true
|
||||
)
|
||||
Spacer()
|
||||
}
|
||||
}
|
||||
.padding()
|
||||
}
|
||||
.frame(minHeight: 200, maxHeight: .infinity)
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
@@ -0,0 +1,42 @@
|
||||
import SwiftUI
|
||||
import CertificateKit
|
||||
import SSHProtocolKit
|
||||
|
||||
struct CertificateListItemView: View {
|
||||
|
||||
@Environment(\.certificateStore) private var store
|
||||
|
||||
var certificate: OpenSSHCertificate
|
||||
|
||||
@State var isDeleting: Bool = false
|
||||
@State var isRenaming: Bool = false
|
||||
|
||||
var deletedCertificate: (OpenSSHCertificate) -> Void
|
||||
var renamedCertificate: (OpenSSHCertificate) -> 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)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
52
Sources/Secretive/Views/Secrets/DeleteCertificateView.swift
Normal file
52
Sources/Secretive/Views/Secrets/DeleteCertificateView.swift
Normal file
@@ -0,0 +1,52 @@
|
||||
import SwiftUI
|
||||
import CertificateKit
|
||||
import SSHProtocolKit
|
||||
|
||||
extension View {
|
||||
|
||||
func showingDeleteConfirmation(isPresented: Binding<Bool>, _ certificate: OpenSSHCertificate, _ 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: OpenSSHCertificate
|
||||
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
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -21,7 +21,7 @@ struct DeleteSecretConfirmationModifier: ViewModifier {
|
||||
func body(content: Content) -> some View {
|
||||
content
|
||||
.confirmationDialog(
|
||||
.deleteConfirmationTitle(secretName: secret.name),
|
||||
.deleteConfirmationTitle(name: secret.name),
|
||||
isPresented: isPresented,
|
||||
titleVisibility: .visible,
|
||||
actions: {
|
||||
|
||||
60
Sources/Secretive/Views/Secrets/EditCertificateView.swift
Normal file
60
Sources/Secretive/Views/Secrets/EditCertificateView.swift
Normal file
@@ -0,0 +1,60 @@
|
||||
import SwiftUI
|
||||
import SSHProtocolKit
|
||||
import CertificateKit
|
||||
|
||||
struct EditCertificateView: View {
|
||||
|
||||
let store: CertificateStore
|
||||
let certificate: OpenSSHCertificate
|
||||
|
||||
@State private var name: String
|
||||
@State var errorText: String?
|
||||
|
||||
@Environment(\.dismiss) var dismiss
|
||||
|
||||
init(store: CertificateStore, certificate: OpenSSHCertificate) {
|
||||
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.name = name
|
||||
try store.update(certificate: updated)
|
||||
dismiss()
|
||||
} catch {
|
||||
errorText = error.localizedDescription
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -6,6 +6,8 @@ import SSHProtocolKit
|
||||
struct SecretDetailView<SecretType: Secret>: View {
|
||||
|
||||
let secret: SecretType
|
||||
let certificates: [OpenSSHCertificate]
|
||||
let navigateToCertificate: ((OpenSSHCertificate) -> Void)?
|
||||
|
||||
private let keyWriter = OpenSSHPublicKeyWriter()
|
||||
|
||||
@@ -13,16 +15,42 @@ struct SecretDetailView<SecretType: Secret>: View {
|
||||
ScrollView {
|
||||
Form {
|
||||
Section {
|
||||
CopyableView(title: .secretDetailSha256FingerprintLabel, image: Image(systemName: "touchid"), text: keyWriter.openSSHSHA256Fingerprint(secret: secret))
|
||||
CopyableView(
|
||||
title: .secretDetailSha256FingerprintLabel,
|
||||
image: Image(systemName: "touchid"),
|
||||
text: keyWriter.openSSHSHA256Fingerprint(secret: secret)
|
||||
)
|
||||
Spacer()
|
||||
.frame(height: 20)
|
||||
CopyableView(title: .secretDetailMd5FingerprintLabel, image: Image(systemName: "touchid"), text: keyWriter.openSSHMD5Fingerprint(secret: secret))
|
||||
CopyableView(
|
||||
title: .secretDetailMd5FingerprintLabel,
|
||||
image: Image(systemName: "touchid"),
|
||||
text: keyWriter.openSSHMD5Fingerprint(secret: secret)
|
||||
)
|
||||
Spacer()
|
||||
.frame(height: 20)
|
||||
CopyableView(title: .secretDetailPublicKeyLabel, image: Image(systemName: "key"), text: keyString)
|
||||
CopyableView(
|
||||
title: .secretDetailPublicKeyPathLabel,
|
||||
image: Image(systemName: "lock.doc"),
|
||||
text: URL.publicKeyPath(for: secret, in: URL.publicKeyDirectory),
|
||||
showRevealInFinder: true
|
||||
)
|
||||
if !certificates.isEmpty {
|
||||
Spacer()
|
||||
.frame(height: 20)
|
||||
CopyableView(title: .secretDetailPublicKeyPathLabel, image: Image(systemName: "lock.doc"), text: URL.publicKeyPath(for: secret, in: URL.publicKeyDirectory), showRevealInFinder: true)
|
||||
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()
|
||||
}
|
||||
}
|
||||
@@ -32,10 +60,6 @@ struct SecretDetailView<SecretType: Secret>: View {
|
||||
}
|
||||
|
||||
|
||||
var keyString: String {
|
||||
keyWriter.openSSHString(secret: secret)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
//#Preview {
|
||||
|
||||
@@ -1,25 +1,32 @@
|
||||
import SwiftUI
|
||||
import SecretKit
|
||||
import SSHProtocolKit
|
||||
|
||||
struct StoreListView: View {
|
||||
|
||||
@Binding var activeSecret: AnySecret?
|
||||
enum StoreListSelection: Hashable {
|
||||
case secret(AnySecret)
|
||||
case certificate(OpenSSHCertificate)
|
||||
}
|
||||
|
||||
@Binding var selection: StoreListSelection?
|
||||
|
||||
@Environment(\.secretStoreList) private var storeList
|
||||
@Environment(\.certificateStore) private var certificateStore
|
||||
|
||||
private func secretDeleted(secret: AnySecret) {
|
||||
activeSecret = nextDefaultSecret
|
||||
selection = nextDefaultSecret.map(StoreListSelection.secret)
|
||||
}
|
||||
|
||||
private func secretRenamed(secret: AnySecret) {
|
||||
// Pull new version from store, so we get all updated attributes
|
||||
activeSecret = nil
|
||||
activeSecret = storeList.allSecrets.first(where: { $0.id == secret.id })
|
||||
selection = nil
|
||||
selection = storeList.allSecrets.first(where: { $0.id == secret.id }).map(StoreListSelection.secret)
|
||||
}
|
||||
|
||||
var body: some View {
|
||||
NavigationSplitView {
|
||||
List(selection: $activeSecret) {
|
||||
List(selection: $selection) {
|
||||
ForEach(storeList.stores) { store in
|
||||
if store.isAvailable {
|
||||
Section(header: Text(store.name)) {
|
||||
@@ -30,18 +37,39 @@ struct StoreListView: View {
|
||||
deletedSecret: secretDeleted,
|
||||
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: {
|
||||
if let activeSecret {
|
||||
SecretDetailView(secret: activeSecret)
|
||||
} else if let nextDefaultSecret {
|
||||
switch selection {
|
||||
case .secret(let secret):
|
||||
SecretDetailView(secret: secret, certificates: certificateStore.certificates(for: secret)) {
|
||||
selection = .certificate($0)
|
||||
}
|
||||
case .certificate(let certificate):
|
||||
CertificateDetailView(certificate: certificate)
|
||||
case nil:
|
||||
if let nextDefaultSecret {
|
||||
// This just means onAppear hasn't executed yet.
|
||||
// Do this to avoid a blip.
|
||||
SecretDetailView(secret: nextDefaultSecret)
|
||||
SecretDetailView(secret: nextDefaultSecret, certificates: certificateStore.certificates(for: nextDefaultSecret)) {
|
||||
selection = .certificate($0)
|
||||
}
|
||||
} else {
|
||||
if let modifiable = storeList.modifiableStore, modifiable.isAvailable {
|
||||
EmptyStoreView(store: modifiable)
|
||||
@@ -50,9 +78,10 @@ struct StoreListView: View {
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
.navigationSplitViewStyle(.balanced)
|
||||
.onAppear {
|
||||
activeSecret = nextDefaultSecret
|
||||
selection = nextDefaultSecret.map(StoreListSelection.secret)
|
||||
}
|
||||
.frame(minWidth: 100, idealWidth: 240)
|
||||
|
||||
|
||||
@@ -3,16 +3,19 @@ import SecretKit
|
||||
import SecureEnclaveSecretKit
|
||||
import SmartCardSecretKit
|
||||
import Brief
|
||||
import SSHProtocolKit
|
||||
import SharedXPCServices
|
||||
|
||||
struct ContentView: View {
|
||||
|
||||
@State var activeSecret: AnySecret?
|
||||
@State var selection: StoreListView.StoreListSelection?
|
||||
|
||||
@State private var selectedUpdate: Release?
|
||||
|
||||
@Environment(\.colorScheme) private var colorScheme
|
||||
@Environment(\.openWindow) private var openWindow
|
||||
@Environment(\.secretStoreList) private var storeList
|
||||
@Environment(\.certificateStore) private var certificateStore
|
||||
@Environment(\.updater) private var updater
|
||||
@Environment(\.agentLaunchController) private var agentLaunchController
|
||||
|
||||
@@ -25,7 +28,7 @@ struct ContentView: View {
|
||||
var body: some View {
|
||||
VStack {
|
||||
if storeList.anyAvailable {
|
||||
StoreListView(activeSecret: $activeSecret)
|
||||
StoreListView(selection: $selection)
|
||||
} else {
|
||||
NoStoresView()
|
||||
}
|
||||
@@ -42,6 +45,21 @@ struct ContentView: View {
|
||||
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)
|
||||
try certificateStore.save(certificate: cert, originalData: data)
|
||||
selection = .certificate(cert)
|
||||
} catch {
|
||||
|
||||
}
|
||||
}
|
||||
return true
|
||||
} isTargeted: { _ in }
|
||||
.focusedSceneValue(\.showCreateSecret, .init(isEnabled: !runningSetup) {
|
||||
showingCreation = true
|
||||
})
|
||||
@@ -49,7 +67,7 @@ struct ContentView: View {
|
||||
if let modifiable = storeList.modifiableStore {
|
||||
CreateSecretView(store: modifiable) { created in
|
||||
if let created {
|
||||
activeSecret = created
|
||||
selection = .secret(created)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4,6 +4,7 @@ import UniformTypeIdentifiers
|
||||
struct CopyableView: View {
|
||||
|
||||
var title: LocalizedStringResource
|
||||
var subtitle: String?
|
||||
var image: Image
|
||||
var text: String
|
||||
var showRevealInFinder = false
|
||||
@@ -17,9 +18,16 @@ struct CopyableView: View {
|
||||
.renderingMode(.template)
|
||||
.imageScale(.large)
|
||||
.foregroundColor(primaryTextColor)
|
||||
VStack(alignment: .leading) {
|
||||
Text(title)
|
||||
.font(.headline)
|
||||
.foregroundColor(primaryTextColor)
|
||||
if let subtitle {
|
||||
Text(subtitle)
|
||||
.font(.system(.subheadline, design: .monospaced))
|
||||
.foregroundColor(secondaryTextColor)
|
||||
}
|
||||
}
|
||||
Spacer()
|
||||
if interactionState != .normal {
|
||||
HStack {
|
||||
|
||||
167
Sources/Secretive/Views/Views/MultilineInfoView.swift
Normal file
167
Sources/Secretive/Views/Views/MultilineInfoView.swift
Normal file
@@ -0,0 +1,167 @@
|
||||
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()
|
||||
}
|
||||
11
Sources/SecretiveCertificateParser/Info.plist
Normal file
11
Sources/SecretiveCertificateParser/Info.plist
Normal file
@@ -0,0 +1,11 @@
|
||||
<?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>
|
||||
@@ -0,0 +1,22 @@
|
||||
<?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>
|
||||
@@ -0,0 +1,17 @@
|
||||
import Foundation
|
||||
import OSLog
|
||||
import XPCWrappers
|
||||
import SSHProtocolKit
|
||||
|
||||
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 \(result.debugDescription)")
|
||||
return result
|
||||
}
|
||||
|
||||
}
|
||||
7
Sources/SecretiveCertificateParser/main.swift
Normal file
7
Sources/SecretiveCertificateParser/main.swift
Normal file
@@ -0,0 +1,7 @@
|
||||
import Foundation
|
||||
import XPCWrappers
|
||||
|
||||
let delegate = XPCServiceDelegate(exportedObject: SecretiveCertificateParser())
|
||||
let listener = NSXPCListener.service()
|
||||
listener.delegate = delegate
|
||||
listener.resume()
|
||||
Reference in New Issue
Block a user