diff --git a/Package.swift b/Package.swift index 3ca29ce..8a57e55 100644 --- a/Package.swift +++ b/Package.swift @@ -22,9 +22,15 @@ let package = Package( .library( name: "SmartCardSecretKit", targets: ["SmartCardSecretKit"]), + .library( + name: "CertificateKit", + targets: ["CertificateKit"]), .library( name: "SSHProtocolKit", targets: ["SSHProtocolKit"]), + .library( + name: "Formatters", + targets: ["Formatters"]), ], dependencies: [ ], @@ -56,9 +62,16 @@ let package = Package( resources: [localization], swiftSettings: swiftSettings ), + .target( + name: "CertificateKit", + dependencies: ["SecretKit", "Formatters"], + path: "Sources/Packages/Sources/CertificateKit", + resources: [localization], + swiftSettings: swiftSettings, + ), .target( name: "SSHProtocolKit", - dependencies: ["SecretKit"], + dependencies: ["SecretKit", "CertificateKit"], path: "Sources/Packages/Sources/SSHProtocolKit", resources: [localization], swiftSettings: swiftSettings, @@ -69,6 +82,13 @@ let package = Package( path: "Sources/Packages/Tests/SSHProtocolKitTests", swiftSettings: swiftSettings, ), + .target( + name: "Formatters", + dependencies: [], + path: "Sources/Packages/Sources/Formatters", + resources: [localization], + swiftSettings: swiftSettings, + ), ] ) diff --git a/Sources/Packages/Package.swift b/Sources/Packages/Package.swift index 966e36b..866fe79 100644 --- a/Sources/Packages/Package.swift +++ b/Sources/Packages/Package.swift @@ -25,6 +25,9 @@ let package = Package( .library( name: "SecretAgentKit", targets: ["SecretAgentKit"]), + .library( + name: "Formatters", + targets: ["Formatters"]), .library( name: "Common", targets: ["Common"]), @@ -69,13 +72,13 @@ let package = Package( ), .target( name: "CertificateKit", - dependencies: ["SecretKit", "SSHProtocolKit"], + dependencies: ["SecretKit", "Formatters"], resources: [localization], swiftSettings: swiftSettings, ), .target( name: "SecretAgentKit", - dependencies: ["SecretKit", "SSHProtocolKit", "CertificateKit", "Common"], + dependencies: ["SecretKit", "SSHProtocolKit", "CertificateKit", "Common", "Formatters"], resources: [localization], swiftSettings: swiftSettings, ), @@ -85,7 +88,7 @@ let package = Package( ), .target( name: "SSHProtocolKit", - dependencies: ["SecretKit"], + dependencies: ["SecretKit", "CertificateKit"], resources: [localization], swiftSettings: swiftSettings, ), @@ -94,6 +97,12 @@ let package = Package( dependencies: ["SSHProtocolKit"], swiftSettings: swiftSettings, ), + .target( + name: "Formatters", + dependencies: [], + resources: [localization], + swiftSettings: swiftSettings, + ), .target( name: "Common", dependencies: ["SSHProtocolKit", "SecretKit"], @@ -102,7 +111,7 @@ let package = Package( ), .target( name: "SharedXPCServices", - dependencies: ["CertificateKit"], + dependencies: ["CertificateKit", "SSHProtocolKit"], resources: [localization], swiftSettings: swiftSettings, ), diff --git a/Sources/Packages/Resources/Localizable.xcstrings b/Sources/Packages/Resources/Localizable.xcstrings index a9ad383..d66c69a 100644 --- a/Sources/Packages/Resources/Localizable.xcstrings +++ b/Sources/Packages/Resources/Localizable.xcstrings @@ -5548,6 +5548,28 @@ } } }, + "certificate_detail_critical_options_label" : { + "extractionState" : "manual", + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Critical Options" + } + } + } + }, + "certificate_detail_extensions_label" : { + "extractionState" : "manual", + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Extensions" + } + } + } + }, "certificate_detail_key_id_label" : { "extractionState" : "manual", "localizations" : { @@ -5592,6 +5614,28 @@ } } }, + "certificate_detail_sha256_public_key_fingerprint_label" : { + "extractionState" : "manual", + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Public Key Fingerprint" + } + } + } + }, + "certificate_detail_sha256_signing_key_fingerprint_label" : { + "extractionState" : "manual", + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Signing CA Fingerprint" + } + } + } + }, "certificate_detail_valid_after_label" : { "extractionState" : "manual", "localizations" : { diff --git a/Sources/Packages/Sources/CertificateKit/Certificate.swift b/Sources/Packages/Sources/CertificateKit/Certificate.swift new file mode 100644 index 0000000..3e2a40f --- /dev/null +++ b/Sources/Packages/Sources/CertificateKit/Certificate.swift @@ -0,0 +1,24 @@ +import Foundation +import CryptoKit +import Formatters + +@dynamicMemberLookup +public struct Certificate: Sendable, Codable, Equatable, Hashable, Identifiable, CustomDebugStringConvertible { + + public var openSSHCertificate: OpenSSHCertificate + public let rawData: Data + + public init(openSSHCertificate: OpenSSHCertificate, rawData: Data) { + self.openSSHCertificate = openSSHCertificate + self.rawData = rawData + } + + public var id: String { Insecure.MD5.hash(data: rawData).formatted(.hex(separator: "")) } + + public var debugDescription: String { openSSHCertificate.debugDescription } + + public subscript(dynamicMember keyPath: KeyPath) -> T { + openSSHCertificate[keyPath: keyPath] + } + +} diff --git a/Sources/Packages/Sources/CertificateKit/CertificateStore.swift b/Sources/Packages/Sources/CertificateKit/CertificateStore.swift index f44bfc5..27c873b 100644 --- a/Sources/Packages/Sources/CertificateKit/CertificateStore.swift +++ b/Sources/Packages/Sources/CertificateKit/CertificateStore.swift @@ -3,11 +3,10 @@ import Observation import Security import os import SecretKit -import SSHProtocolKit @Observable @MainActor public final class CertificateStore: Sendable { - public private(set) var certificates: [OpenSSHCertificate] = [] + public private(set) var certificates: [Certificate] = [] /// Initializes a Store. public init() { @@ -33,15 +32,15 @@ import SSHProtocolKit } } - public func save(certificate: OpenSSHCertificate, originalData: Data) throws { - let attributes = try JSONEncoder().encode(certificate) + public func save(certificate: Certificate) throws { + let attributes = try JSONEncoder().encode(certificate.openSSHCertificate) let keychainAttributes = KeychainDictionary([ kSecClass: Constants.keyClass, kSecAttrService: Constants.keyTag, kSecAttrAccount: certificate.id, kSecUseDataProtectionKeychain: true, kSecAttrAccessible: kSecAttrAccessibleWhenUnlockedThisDeviceOnly, - kSecValueData: originalData, + kSecValueData: certificate.rawData, kSecAttrGeneric: attributes ]) let status = SecItemAdd(keychainAttributes, nil) @@ -51,7 +50,7 @@ import SSHProtocolKit reloadCertificates() } - public func delete(certificate: OpenSSHCertificate) throws { + public func delete(certificate: Certificate) throws { let deleteAttributes = KeychainDictionary([ kSecClass: Constants.keyClass, kSecAttrService: Constants.keyTag, @@ -65,13 +64,13 @@ import SSHProtocolKit reloadCertificates() } - public func update(certificate: OpenSSHCertificate) throws { + public func update(certificate: Certificate) throws { let updateQuery = KeychainDictionary([ kSecClass: Constants.keyClass, kSecAttrAccount: certificate.id, ]) - let cert = try JSONEncoder().encode(certificate) + let cert = try JSONEncoder().encode(certificate.openSSHCertificate) let updatedAttributes = KeychainDictionary([ kSecAttrGeneric: cert, ]) @@ -83,8 +82,8 @@ import SSHProtocolKit reloadCertificates() } - public func certificates(for secret: any Secret) -> [OpenSSHCertificate] { - certificates.filter { $0.publicKey == secret.publicKey } + public func certificates(for secret: any Secret) -> [Certificate] { + certificates.filter { $0.openSSHCertificate.publicKey.data == secret.publicKey } } @@ -106,12 +105,13 @@ extension CertificateStore { unsafe SecItemCopyMatching(queryAttributes, &untyped) guard let typed = untyped as? [[CFString: Any]] else { return } let decoder = JSONDecoder() - let wrapped: [OpenSSHCertificate] = typed.compactMap { + let wrapped: [Certificate] = typed.compactMap { do { - guard let attributesData = $0[kSecAttrGeneric] as? Data else { + guard let data = $0[kSecValueData] as? Data, + let attributesData = $0[kSecAttrGeneric] as? Data else { throw MissingAttributesError() } - return try decoder.decode(OpenSSHCertificate.self, from: attributesData) + return Certificate(openSSHCertificate: try decoder.decode(OpenSSHCertificate.self, from: attributesData), rawData: data) } catch { return nil } @@ -123,6 +123,7 @@ extension CertificateStore { true } } + certificates.append(contentsOf: wrapped) } diff --git a/Sources/Packages/Sources/CertificateKit/OpenSSHCertificate.swift b/Sources/Packages/Sources/CertificateKit/OpenSSHCertificate.swift new file mode 100644 index 0000000..c65c2a7 --- /dev/null +++ b/Sources/Packages/Sources/CertificateKit/OpenSSHCertificate.swift @@ -0,0 +1,82 @@ +import Foundation +import Formatters + +public struct OpenSSHCertificate: Sendable, Codable, Equatable, Hashable, CustomDebugStringConvertible { + + public var type: CertificateType + public var name: String + public var data: Data + + public var publicKey: PublicKey + public var principals: [String] + public var keyID: String + public var serial: UInt64 + public var validityRange: Range? + public var criticalOptions: [String] + public var extensions: [String] + public var signingKey: PublicKey + + public init( + type: OpenSSHCertificate.CertificateType, + name: String, + data: Data, + publicKey: PublicKey, + principals: [String], + keyID: String, + serial: UInt64, + validityRange: Range? = nil, + criticalOptions: [String], + extensions: [String], + signingKey: PublicKey, + ) { + self.type = type + self.name = name + self.data = data + self.publicKey = publicKey + self.principals = principals + self.keyID = keyID + self.serial = serial + self.validityRange = validityRange + self.criticalOptions = criticalOptions + self.extensions = extensions + self.signingKey = signingKey + } + + public var debugDescription: String { + "OpenSSH Certificate \(name, default: "Unnamed"): \(data.formatted(.hex()))" + } + +} + +extension OpenSSHCertificate { + + public enum CertificateType: String, Sendable, Codable { + case ecdsa256 = "ecdsa-sha2-nistp256-cert-v01@openssh.com" + case ecdsa384 = "ecdsa-sha2-nistp384-cert-v01@openssh.com" + case nistp521 = "ecdsa-sha2-nistp521-cert-v01@openssh.com" + + public var keyIdentifier: String { + rawValue.replacingOccurrences(of: "-cert-v01@openssh.com", with: "") + } + + } + +} + +extension OpenSSHCertificate { + + public struct PublicKey: Hashable, Sendable, Codable { + + public let keyType: String + public let curveName: String + public let data: Data + + public init(keyType: String, curveName: String, data: Data) { + self.keyType = keyType + self.curveName = curveName + self.data = data + } + + } + +} diff --git a/Sources/Packages/Sources/Common/URLs.swift b/Sources/Packages/Sources/Common/URLs.swift index e3d21e0..d6638b6 100644 --- a/Sources/Packages/Sources/Common/URLs.swift +++ b/Sources/Packages/Sources/Common/URLs.swift @@ -1,5 +1,6 @@ import Foundation import SSHProtocolKit +import CertificateKit import SecretKit extension URL { @@ -35,11 +36,11 @@ extension URL { } /// The path for a certificate. - /// - Parameter certificate: The OpenSSHCertificate to return the path for. - /// - Returns: The path to the OpenSSHCertificate. + /// - Parameter certificate: The Certificate to return the path for. + /// - Returns: The path to the Certificate. /// - Warning: This method returning a path does not imply that a certificate has been written to disk already. This method only describes where it will be written to. - public static func certificatePath(for certificate: OpenSSHCertificate, in directory: URL) -> String { - return directory.appending(component: "\(certificate.id)-cert.pub").path() + public static func certificatePath(for certificateID: String, in directory: URL) -> String { + return directory.appending(component: "\(certificateID)-cert.pub").path() } } diff --git a/Sources/Packages/Sources/SSHProtocolKit/Data+Hex.swift b/Sources/Packages/Sources/Formatters/Data+Hex.swift similarity index 100% rename from Sources/Packages/Sources/SSHProtocolKit/Data+Hex.swift rename to Sources/Packages/Sources/Formatters/Data+Hex.swift diff --git a/Sources/Packages/Sources/SSHProtocolKit/OpenSSHCertficateWriter.swift b/Sources/Packages/Sources/SSHProtocolKit/OpenSSHCertficateWriter.swift new file mode 100644 index 0000000..84892cd --- /dev/null +++ b/Sources/Packages/Sources/SSHProtocolKit/OpenSSHCertficateWriter.swift @@ -0,0 +1,30 @@ +import Foundation +import CryptoKit +import CertificateKit +import Formatters + +/// Generates OpenSSH representations of Certificates. +public struct OpenSSHCertificateWriter: Sendable { + + /// Initializes the writer. + public init() { + } + + /// Generates an OpenSSH data payload identifying the certificate. + /// - Returns: OpenSSH data payload identifying the certificate. + public func data(publicKey: OpenSSHCertificate.PublicKey) -> Data { + // https://datatracker.ietf.org/doc/html/rfc5656#section-3.1 + publicKey.keyType.lengthAndData + + publicKey.curveName.lengthAndData + + publicKey.data.lengthAndData + } + + /// Generates an OpenSSH SHA256 fingerprint string. + /// - Returns: OpenSSH SHA256 fingerprint string. + public func openSSHSHA256KeyFingerprint(publicKey: OpenSSHCertificate.PublicKey) -> String { + // OpenSSL format seems to strip the padding at the end. + let cleaned = SHA256.hash(data: data(publicKey: publicKey)).formatted(.base64(stripPadding: true)) + return "SHA256:\(cleaned)" + } + +} diff --git a/Sources/Packages/Sources/SSHProtocolKit/OpenSSHCertificate.swift b/Sources/Packages/Sources/SSHProtocolKit/OpenSSHCertificateParser.swift similarity index 58% rename from Sources/Packages/Sources/SSHProtocolKit/OpenSSHCertificate.swift rename to Sources/Packages/Sources/SSHProtocolKit/OpenSSHCertificateParser.swift index f1354a5..efedc96 100644 --- a/Sources/Packages/Sources/SSHProtocolKit/OpenSSHCertificate.swift +++ b/Sources/Packages/Sources/SSHProtocolKit/OpenSSHCertificateParser.swift @@ -1,39 +1,5 @@ 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? - - 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: "") - } - } - -} +import CertificateKit public protocol OpenSSHCertificateParserProtocol { func parse(data: Data) async throws -> OpenSSHCertificate @@ -41,8 +7,6 @@ public protocol OpenSSHCertificateParserProtocol { 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") } @@ -64,10 +28,12 @@ public struct OpenSSHCertificateParser: OpenSSHCertificateParserProtocol, Sendab let comment = elements.first do { let dataParser = OpenSSHReader(data: decoded) - _ = try dataParser.readNextChunkAsString() // Redundant key type + let publicKeyType = try dataParser.readNextChunkAsString() // Theoretically the same as typeString, but + .replacingOccurrences(of: "-cert-v01@openssh.com", with: "") _ = try dataParser.readNextChunk() // Nonce - _ = try dataParser.readNextChunkAsString() // curve - let publicKey = try dataParser.readNextChunk() + let publicKeyCurveName = try dataParser.readNextChunkAsString() + let publicKeyData = try dataParser.readNextChunk() + let publicKey = OpenSSHCertificate.PublicKey(keyType: publicKeyType, curveName: publicKeyCurveName, data: publicKeyData) let serialNumber = try dataParser.readNextBytes(as: UInt64.self, convertEndianness: true) let role = try dataParser.readNextBytes(as: UInt32.self, convertEndianness: true) _ = role @@ -80,6 +46,28 @@ public struct OpenSSHCertificateParser: OpenSSHCertificateParserProtocol, Sendab 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))..(for secret: SecretType) -> String { let minimalHex = keyWriter.openSSHMD5Fingerprint(secret: secret).replacingOccurrences(of: ":", with: "") - return publicKeysURL.appending(component: "\(minimalHex)-cert.pub").path() + return publicKeysURL.appending(component: "\(minimalHex).pub").path() } } diff --git a/Sources/Packages/Sources/SharedXPCServices/CertificateMigrator.swift b/Sources/Packages/Sources/SharedXPCServices/CertificateMigrator.swift index 8de3f14..e15f792 100644 --- a/Sources/Packages/Sources/SharedXPCServices/CertificateMigrator.swift +++ b/Sources/Packages/Sources/SharedXPCServices/CertificateMigrator.swift @@ -9,16 +9,23 @@ import CertificateKit public struct CertificateMigrator { private let logger = Logger(subsystem: "com.maxgoedjen.secretive.migration", category: "CertificateKitMigrator") - private let directory: URL + private let publicKeysDirectory: URL + private let certificatesDirectory: URL private let certificateStore: CertificateStore /// Initializes a PublicKeyFileStoreController. public init(homeDirectory: URL, certificateStore: CertificateStore) { - directory = homeDirectory.appending(component: "PublicKeys") + publicKeysDirectory = homeDirectory.appending(component: "PublicKeys") + certificatesDirectory = homeDirectory.appending(component: "Certificates") self.certificateStore = certificateStore } @MainActor public func migrate() throws { + try migrate(directory: publicKeysDirectory) + try migrate(directory: certificatesDirectory) + } + + @MainActor public func migrate(directory: URL) throws { let fileCerts = try FileManager.default .contentsOfDirectory(atPath: directory.path()) .filter { $0.hasSuffix("-cert.pub") } @@ -29,7 +36,7 @@ public struct CertificateMigrator { 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) + try certificateStore.save(certificate: Certificate(openSSHCertificate: cert, rawData: data)) do { try FileManager.default.removeItem(at: url) } catch { diff --git a/Sources/Packages/Sources/SharedXPCServices/XPCCertificateParser.swift b/Sources/Packages/Sources/SharedXPCServices/XPCCertificateParser.swift index 11a8eae..fea407f 100644 --- a/Sources/Packages/Sources/SharedXPCServices/XPCCertificateParser.swift +++ b/Sources/Packages/Sources/SharedXPCServices/XPCCertificateParser.swift @@ -1,6 +1,7 @@ import Foundation import OSLog import SSHProtocolKit +import CertificateKit import XPCWrappers /// Delegates all agent input parsing to an XPC service which wraps OpenSSH diff --git a/Sources/SecretAgent/CertificateMigrator.swift b/Sources/SecretAgent/CertificateMigrator.swift index 0c0966b..9a37032 100644 --- a/Sources/SecretAgent/CertificateMigrator.swift +++ b/Sources/SecretAgent/CertificateMigrator.swift @@ -30,7 +30,7 @@ public struct CertificateMigrator { 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) + try certificateStore.save(certificate: Certificate(openSSHCertificate: cert, rawData: data)) do { try FileManager.default.removeItem(at: url) } catch { diff --git a/Sources/Secretive.xcodeproj/project.pbxproj b/Sources/Secretive.xcodeproj/project.pbxproj index c3b7c5b..54f5f08 100644 --- a/Sources/Secretive.xcodeproj/project.pbxproj +++ b/Sources/Secretive.xcodeproj/project.pbxproj @@ -86,6 +86,9 @@ 50E205802FAB291E00402380 /* CertificateMigrator.swift in Sources */ = {isa = PBXBuildFile; fileRef = 50E2057F2FAB291E00402380 /* CertificateMigrator.swift */; }; 50E205822FAB293B00402380 /* SharedXPCServices in Frameworks */ = {isa = PBXBuildFile; productRef = 50E205812FAB293B00402380 /* SharedXPCServices */; }; 50E205842FAB296A00402380 /* SharedXPCServices in Frameworks */ = {isa = PBXBuildFile; productRef = 50E205832FAB296A00402380 /* SharedXPCServices */; }; + 50E205862FAC2EA000402380 /* Formatters in Frameworks */ = {isa = PBXBuildFile; productRef = 50E205852FAC2EA000402380 /* Formatters */; }; + 50E205882FAC2EAB00402380 /* Formatters in Frameworks */ = {isa = PBXBuildFile; productRef = 50E205872FAC2EAB00402380 /* Formatters */; }; + 50E2058A2FAC2EB600402380 /* Formatters in Frameworks */ = {isa = PBXBuildFile; productRef = 50E205892FAC2EB600402380 /* Formatters */; }; 50E4C4532E73C78C00C73783 /* WindowBackgroundStyle.swift in Sources */ = {isa = PBXBuildFile; fileRef = 50E4C4522E73C78900C73783 /* WindowBackgroundStyle.swift */; }; 50E4C4C32E7765DF00C73783 /* AboutView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 50E4C4C22E7765DF00C73783 /* AboutView.swift */; }; 50E4C4C82E777E4200C73783 /* AppIcon.icon in Resources */ = {isa = PBXBuildFile; fileRef = 50E4C4C72E777E4200C73783 /* AppIcon.icon */; }; @@ -292,6 +295,7 @@ buildActionMask = 2147483647; files = ( 50E0145C2EDB9CDF00B121F1 /* Common in Frameworks */, + 50E2058A2FAC2EB600402380 /* Formatters in Frameworks */, 5003EF3B278005E800DF2006 /* SecretKit in Frameworks */, 501421622781262300BBAA70 /* Brief in Frameworks */, 50E205842FAB296A00402380 /* SharedXPCServices in Frameworks */, @@ -305,6 +309,7 @@ isa = PBXFrameworksBuildPhase; buildActionMask = 2147483647; files = ( + 50E205862FAC2EA000402380 /* Formatters in Frameworks */, 50692D2D2E6FDC000043C7BB /* XPCWrappers in Frameworks */, 50692D312E6FDC390043C7BB /* Brief in Frameworks */, ); @@ -337,6 +342,7 @@ isa = PBXFrameworksBuildPhase; buildActionMask = 2147483647; files = ( + 50E205882FAC2EAB00402380 /* Formatters in Frameworks */, 50E205332FAAB95A00402380 /* SSHProtocolKit in Frameworks */, 50E205312FAAB95500402380 /* XPCWrappers in Frameworks */, ); @@ -583,6 +589,7 @@ 50E0145B2EDB9CDF00B121F1 /* Common */, 505F5EF12FA9635700C45824 /* CertificateKit */, 50E205832FAB296A00402380 /* SharedXPCServices */, + 50E205892FAC2EB600402380 /* Formatters */, ); productName = Secretive; productReference = 50617D7F23FCE48E0099B055 /* Secretive.app */; @@ -604,6 +611,7 @@ packageProductDependencies = ( 50692D2C2E6FDC000043C7BB /* XPCWrappers */, 50692D302E6FDC390043C7BB /* Brief */, + 50E205852FAC2EA000402380 /* Formatters */, ); productName = SecretiveUpdater; productReference = 50692D122E6FDB880043C7BB /* SecretiveUpdater.xpc */; @@ -678,6 +686,7 @@ packageProductDependencies = ( 50E205302FAAB95500402380 /* XPCWrappers */, 50E205322FAAB95A00402380 /* SSHProtocolKit */, + 50E205872FAC2EAB00402380 /* Formatters */, ); productName = SecretAgentCertificateParser; productReference = 50E205142FAAB81C00402380 /* SecretiveCertificateParser.xpc */; @@ -1857,6 +1866,18 @@ isa = XCSwiftPackageProductDependency; productName = SharedXPCServices; }; + 50E205852FAC2EA000402380 /* Formatters */ = { + isa = XCSwiftPackageProductDependency; + productName = Formatters; + }; + 50E205872FAC2EAB00402380 /* Formatters */ = { + isa = XCSwiftPackageProductDependency; + productName = Formatters; + }; + 50E205892FAC2EB600402380 /* Formatters */ = { + isa = XCSwiftPackageProductDependency; + productName = Formatters; + }; /* End XCSwiftPackageProductDependency section */ }; rootObject = 50617D7723FCE48D0099B055 /* Project object */; diff --git a/Sources/Secretive/Views/Secrets/CertificateDetailView.swift b/Sources/Secretive/Views/Secrets/CertificateDetailView.swift index 92be0df..debd863 100644 --- a/Sources/Secretive/Views/Secrets/CertificateDetailView.swift +++ b/Sources/Secretive/Views/Secrets/CertificateDetailView.swift @@ -1,11 +1,12 @@ import SwiftUI import SecretKit import Common +import CertificateKit import SSHProtocolKit - +import CryptoKit struct CertificateDetailView: View { - let certificate: OpenSSHCertificate + let certificate: Certificate var body: some View { ScrollView { @@ -25,6 +26,26 @@ struct CertificateDetailView: View { ) Spacer() .frame(height: 20) + CopyableView( + title: .secretDetailSha256FingerprintLabel, + image: Image(systemName: "touchid"), + text: OpenSSHCertificateWriter().openSSHSHA256KeyFingerprint(publicKey: certificate.publicKey) + ) + Spacer() + .frame(height: 20) + CopyableView( + title: .secretDetailSha256FingerprintLabel, + image: Image(systemName: "touchid"), + text: OpenSSHCertificateWriter().openSSHSHA256KeyFingerprint(publicKey: certificate.signingKey) + ) + Spacer() + .frame(height: 20) + CopyableView( + title: .certificateDetailPathLabel, + image: Image(systemName: "checkmark.seal.text.page"), + text: URL.certificatePath(for: certificate.id, in: URL.certificatesDirectory), + showRevealInFinder: true + ) if let validityRange = certificate.validityRange { let epoch = Date(timeIntervalSince1970: 0) let end = Date(timeIntervalSince1970: TimeInterval(UInt64.max)) @@ -32,30 +53,34 @@ struct CertificateDetailView: View { case (epoch, end): EmptyView() case (epoch, let otherEnd): + Spacer() + .frame(height: 20) MultilineInfoView(title: .certificateDetailValidUntilLabel, image: Image(systemName: "calendar.badge.clock"), items: [otherEnd.formatted()]) - Spacer() - .frame(height: 20) case (let otherStart, end): + Spacer() + .frame(height: 20) 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) + MultilineInfoView(title: .certificateDetailValidityRangeLabel, image: Image(systemName: "calendar.badge.clock"), items: [validityRange.formatted()]) } } if !certificate.principals.isEmpty { - MultilineInfoView(title: .certificateDetailPrincipalsLabel, image: Image(systemName: "person.2"), items: certificate.principals) Spacer() .frame(height: 20) + MultilineInfoView(title: .certificateDetailPrincipalsLabel, image: Image(systemName: "person.2"), items: certificate.principals) + } + if !certificate.criticalOptions.isEmpty { + Spacer() + .frame(height: 20) + MultilineInfoView(title: .certificateDetailCriticalOptionsLabel, image: Image(systemName: "person.2"), items: certificate.criticalOptions) + } + if !certificate.extensions.isEmpty { + Spacer() + .frame(height: 20) + MultilineInfoView(title: .certificateDetailExtensionsLabel, image: Image(systemName: "person.2"), items: certificate.extensions) } - CopyableView( - title: .certificateDetailPathLabel, - image: Image(systemName: "checkmark.seal.text.page"), - text: URL.certificatePath(for: certificate, in: URL.certificatesDirectory), - showRevealInFinder: true - ) Spacer() } } diff --git a/Sources/Secretive/Views/Secrets/CertificateListItemView.swift b/Sources/Secretive/Views/Secrets/CertificateListItemView.swift index 32da8cd..e5095d1 100644 --- a/Sources/Secretive/Views/Secrets/CertificateListItemView.swift +++ b/Sources/Secretive/Views/Secrets/CertificateListItemView.swift @@ -6,13 +6,13 @@ struct CertificateListItemView: View { @Environment(\.certificateStore) private var store - var certificate: OpenSSHCertificate + var certificate: Certificate @State var isDeleting: Bool = false @State var isRenaming: Bool = false - var deletedCertificate: (OpenSSHCertificate) -> Void - var renamedCertificate: (OpenSSHCertificate) -> Void + var deletedCertificate: (Certificate) -> Void + var renamedCertificate: (Certificate) -> Void var body: some View { NavigationLink(value: certificate) { diff --git a/Sources/Secretive/Views/Secrets/DeleteCertificateView.swift b/Sources/Secretive/Views/Secrets/DeleteCertificateView.swift index 6d2a589..562351e 100644 --- a/Sources/Secretive/Views/Secrets/DeleteCertificateView.swift +++ b/Sources/Secretive/Views/Secrets/DeleteCertificateView.swift @@ -4,7 +4,7 @@ import SSHProtocolKit extension View { - func showingDeleteConfirmation(isPresented: Binding, _ certificate: OpenSSHCertificate, _ store: CertificateStore, dismissalBlock: @escaping (Bool) -> ()) -> some View { + func showingDeleteConfirmation(isPresented: Binding, _ certificate: Certificate, _ store: CertificateStore, dismissalBlock: @escaping (Bool) -> ()) -> some View { modifier(DeleteCertificateConfirmationModifier(isPresented: isPresented, certificate: certificate, store: store, dismissalBlock: dismissalBlock)) } @@ -13,7 +13,7 @@ extension View { struct DeleteCertificateConfirmationModifier: ViewModifier { var isPresented: Binding - var certificate: OpenSSHCertificate + var certificate: Certificate var store: CertificateStore var dismissalBlock: (Bool) -> () @State var confirmedSecretName = "" diff --git a/Sources/Secretive/Views/Secrets/EditCertificateView.swift b/Sources/Secretive/Views/Secrets/EditCertificateView.swift index 4568eed..9b72e06 100644 --- a/Sources/Secretive/Views/Secrets/EditCertificateView.swift +++ b/Sources/Secretive/Views/Secrets/EditCertificateView.swift @@ -5,14 +5,14 @@ import CertificateKit struct EditCertificateView: View { let store: CertificateStore - let certificate: OpenSSHCertificate + let certificate: Certificate @State private var name: String @State var errorText: String? @Environment(\.dismiss) var dismiss - init(store: CertificateStore, certificate: OpenSSHCertificate) { + init(store: CertificateStore, certificate: Certificate) { self.store = store self.certificate = certificate name = certificate.name @@ -49,7 +49,7 @@ struct EditCertificateView: View { Task { do { var updated = certificate - updated.name = name + updated.openSSHCertificate.name = name try store.update(certificate: updated) dismiss() } catch { diff --git a/Sources/Secretive/Views/Secrets/SecretDetailView.swift b/Sources/Secretive/Views/Secrets/SecretDetailView.swift index 4fd30de..a092bcf 100644 --- a/Sources/Secretive/Views/Secrets/SecretDetailView.swift +++ b/Sources/Secretive/Views/Secrets/SecretDetailView.swift @@ -1,13 +1,14 @@ import SwiftUI import SecretKit import Common +import CertificateKit import SSHProtocolKit struct SecretDetailView: View { let secret: SecretType - let certificates: [OpenSSHCertificate] - let navigateToCertificate: ((OpenSSHCertificate) -> Void)? + let certificates: [Certificate] + let navigateToCertificate: ((Certificate) -> Void)? private let keyWriter = OpenSSHPublicKeyWriter() diff --git a/Sources/Secretive/Views/Secrets/StoreListView.swift b/Sources/Secretive/Views/Secrets/StoreListView.swift index 12e2419..b461ad5 100644 --- a/Sources/Secretive/Views/Secrets/StoreListView.swift +++ b/Sources/Secretive/Views/Secrets/StoreListView.swift @@ -1,12 +1,13 @@ import SwiftUI import SecretKit import SSHProtocolKit +import CertificateKit struct StoreListView: View { enum StoreListSelection: Hashable { case secret(AnySecret) - case certificate(OpenSSHCertificate) + case certificate(Certificate) } @Binding var selection: StoreListSelection? diff --git a/Sources/Secretive/Views/Views/ContentView.swift b/Sources/Secretive/Views/Views/ContentView.swift index 2d58ae9..ef9d8f5 100644 --- a/Sources/Secretive/Views/Views/ContentView.swift +++ b/Sources/Secretive/Views/Views/ContentView.swift @@ -5,6 +5,7 @@ import SmartCardSecretKit import Brief import SSHProtocolKit import SharedXPCServices +import CertificateKit struct ContentView: View { @@ -52,8 +53,9 @@ struct ContentView: View { 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) + let wrapped = Certificate(openSSHCertificate: cert, rawData: data) + try certificateStore.save(certificate: wrapped) + selection = .certificate(wrapped) } catch { } diff --git a/Sources/SecretiveCertificateParser/SecretiveCertificateParser.swift b/Sources/SecretiveCertificateParser/SecretiveCertificateParser.swift index 8c7aab7..2d453e8 100644 --- a/Sources/SecretiveCertificateParser/SecretiveCertificateParser.swift +++ b/Sources/SecretiveCertificateParser/SecretiveCertificateParser.swift @@ -2,6 +2,7 @@ import Foundation import OSLog import XPCWrappers import SSHProtocolKit +import CertificateKit final class SecretiveCertificateParser: NSObject, XPCProtocol { @@ -10,7 +11,7 @@ final class SecretiveCertificateParser: NSObject, XPCProtocol { func process(_ data: Data) async throws -> OpenSSHCertificate { let parser = OpenSSHCertificateParser() let result = try parser.parse(data: data) - logger.log("Parser parsed certificate \(result.debugDescription)") + logger.log("Parser parsed certificate") return result }