This commit is contained in:
Max Goedjen 2025-09-20 17:43:43 -07:00
parent 940b6b1b86
commit a0cf74f9dc
No known key found for this signature in database
6 changed files with 100 additions and 9 deletions

View File

@ -19,9 +19,12 @@ let package = Package(
.library( .library(
name: "SmartCardSecretKit", name: "SmartCardSecretKit",
targets: ["SmartCardSecretKit"]), targets: ["SmartCardSecretKit"]),
.library(
name: "CertificateKit",
targets: ["CertificateKit"]),
.library( .library(
name: "SecretAgentKit", name: "SecretAgentKit",
targets: ["SecretAgentKit", "XPCWrappers"]), targets: ["SecretAgentKit"]),
.library( .library(
name: "Brief", name: "Brief",
targets: ["Brief"]), targets: ["Brief"]),
@ -58,9 +61,15 @@ let package = Package(
resources: [localization], resources: [localization],
swiftSettings: swiftSettings, swiftSettings: swiftSettings,
), ),
.target(
name: "CertificateKit",
dependencies: ["SecretKit"],
resources: [localization],
// swiftSettings: swiftSettings,
),
.target( .target(
name: "SecretAgentKit", name: "SecretAgentKit",
dependencies: ["SecretKit", "SSHProtocolKit"], dependencies: ["SecretKit", "SSHProtocolKit", "CertificateKit"],
resources: [localization], resources: [localization],
swiftSettings: swiftSettings, swiftSettings: swiftSettings,
), ),
@ -72,7 +81,7 @@ let package = Package(
name: "SSHProtocolKit", name: "SSHProtocolKit",
dependencies: ["SecretKit"], dependencies: ["SecretKit"],
resources: [localization], resources: [localization],
swiftSettings: swiftSettings, // swiftSettings: swiftSettings,
), ),
.testTarget( .testTarget(
name: "SSHProtocolKitTests", name: "SSHProtocolKitTests",

View File

@ -1,5 +1,6 @@
import Foundation import Foundation
import OSLog import OSLog
import CryptoKit
public struct OpenSSHCertificate: Sendable, Codable, Equatable, Hashable, Identifiable, CustomDebugStringConvertible { public struct OpenSSHCertificate: Sendable, Codable, Equatable, Hashable, Identifiable, CustomDebugStringConvertible {
@ -8,6 +9,12 @@ public struct OpenSSHCertificate: Sendable, Codable, Equatable, Hashable, Identi
public let name: String? public let name: String?
public let data: Data 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 { public var debugDescription: String {
"OpenSSH Certificate \(name, default: "Unnamed"): \(data.formatted(.hex()))" "OpenSSH Certificate \(name, default: "Unnamed"): \(data.formatted(.hex()))"
} }
@ -54,7 +61,53 @@ public struct OpenSSHCertificateParser: OpenSSHCertificateParserProtocol, Sendab
throw OpenSSHCertificateError.parsingFailed throw OpenSSHCertificateError.parsingFailed
} }
let name = elements.first let name = elements.first
return OpenSSHCertificate(type: type, name: name, data: decoded) 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)
let keyIdentifier = try dataParser.readNextChunkAsString()
let principalsReader = try dataParser.readNextChunkAsSubReader()
var principals: [String] = []
while !principalsReader.done {
try principals.append(principalsReader.readNextChunkAsString())
}
let validAfter = try dataParser.readNextBytes(as: UInt64.self, convertEndianness: true)
let validBefore = try dataParser.readNextBytes(as: UInt64.self, convertEndianness: true)
let validityRange = Date(timeIntervalSince1970: TimeInterval(validAfter))..<Date(timeIntervalSince1970: TimeInterval(validBefore
))
let criticalOptionsReader = try dataParser.readNextChunkAsSubReader()
let extensionsReader = try dataParser.readNextChunkAsSubReader()
_ = try dataParser.readNextChunk() // reserved
let signatureKey = try dataParser.readNextChunk()
let signature = try dataParser.readNextChunk()
print(pkw(data: signatureKey), pkw(data: publicKey), pkw(data: signature))
return OpenSSHCertificate(
type: type,
name: name,
data: data,
publicKey: publicKey,
principals: principals,
keyID: keyIdentifier,
serial: serialNumber,
validityRange: validityRange
)
} catch {
throw .parsingFailed
}
}
func pkw(data: Data) -> String {
let base64 = Data(SHA256.hash(data: data)).base64EncodedString()
let paddingRange = base64.index(base64.endIndex, offsetBy: -2)..<base64.endIndex
let cleaned = base64.replacingOccurrences(of: "=", with: "", range: paddingRange)
return "SHA256:\(cleaned)"
} }
} }

View File

@ -4,6 +4,7 @@ import Foundation
final class OpenSSHReader { final class OpenSSHReader {
var remaining: Data var remaining: Data
var done = false
/// Initialize the reader with an OpenSSH data payload. /// Initialize the reader with an OpenSSH data payload.
/// - Parameter data: The data to read. /// - Parameter data: The data to read.
@ -14,22 +15,28 @@ final class OpenSSHReader {
/// Reads the next chunk of data from the playload. /// Reads the next chunk of data from the playload.
/// - Returns: The next chunk of data. /// - Returns: The next chunk of data.
func readNextChunk(convertEndianness: Bool = true) throws(OpenSSHReaderError) -> Data { func readNextChunk(convertEndianness: Bool = true) throws(OpenSSHReaderError) -> Data {
let littleEndianLength = try readNextBytes(as: UInt32.self) let length = try readNextBytes(as: UInt32.self, convertEndianness: convertEndianness)
let length = convertEndianness ? Int(littleEndianLength.bigEndian) : Int(littleEndianLength)
guard remaining.count >= length else { throw .beyondBounds } guard remaining.count >= length else { throw .beyondBounds }
let dataRange = 0..<length let dataRange = 0..<Int(length)
let ret = Data(remaining[dataRange]) let ret = Data(remaining[dataRange])
remaining.removeSubrange(dataRange) remaining.removeSubrange(dataRange)
if remaining.isEmpty {
done = true
}
return ret return ret
} }
func readNextBytes<T>(as: T.Type) throws(OpenSSHReaderError) -> T { func readNextBytes<T: FixedWidthInteger>(as: T.Type, convertEndianness: Bool = true) throws(OpenSSHReaderError) -> T {
let size = MemoryLayout<T>.size let size = MemoryLayout<T>.size
guard remaining.count >= size else { throw .beyondBounds } guard remaining.count >= size else { throw .beyondBounds }
let lengthRange = 0..<size let lengthRange = 0..<size
let lengthChunk = remaining[lengthRange] let lengthChunk = remaining[lengthRange]
remaining.removeSubrange(lengthRange) remaining.removeSubrange(lengthRange)
return unsafe lengthChunk.bytes.unsafeLoad(as: T.self) if remaining.isEmpty {
done = true
}
let value = unsafe lengthChunk.bytes.unsafeLoad(as: T.self)
return convertEndianness ? T(value.bigEndian) : T(value)
} }
func readNextChunkAsString(convertEndianness: Bool = true) throws(OpenSSHReaderError) -> String { func readNextChunkAsString(convertEndianness: Bool = true) throws(OpenSSHReaderError) -> String {

View File

@ -34,6 +34,7 @@
50571E0324393C2600F76F6C /* JustUpdatedChecker.swift in Sources */ = {isa = PBXBuildFile; fileRef = 50571E0224393C2600F76F6C /* JustUpdatedChecker.swift */; }; 50571E0324393C2600F76F6C /* JustUpdatedChecker.swift in Sources */ = {isa = PBXBuildFile; fileRef = 50571E0224393C2600F76F6C /* JustUpdatedChecker.swift */; };
50571E0524393D1500F76F6C /* LaunchAgentController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 50571E0424393D1500F76F6C /* LaunchAgentController.swift */; }; 50571E0524393D1500F76F6C /* LaunchAgentController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 50571E0424393D1500F76F6C /* LaunchAgentController.swift */; };
505993512E7E59FB0092CFFA /* XPCCertificateParser.swift in Sources */ = {isa = PBXBuildFile; fileRef = 505993502E7E59F70092CFFA /* XPCCertificateParser.swift */; }; 505993512E7E59FB0092CFFA /* XPCCertificateParser.swift in Sources */ = {isa = PBXBuildFile; fileRef = 505993502E7E59F70092CFFA /* XPCCertificateParser.swift */; };
505993532E7E70C90092CFFA /* CertificateKit in Frameworks */ = {isa = PBXBuildFile; productRef = 505993522E7E70C90092CFFA /* CertificateKit */; };
50617D8323FCE48E0099B055 /* App.swift in Sources */ = {isa = PBXBuildFile; fileRef = 50617D8223FCE48E0099B055 /* App.swift */; }; 50617D8323FCE48E0099B055 /* App.swift in Sources */ = {isa = PBXBuildFile; fileRef = 50617D8223FCE48E0099B055 /* App.swift */; };
50617D8523FCE48E0099B055 /* ContentView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 50617D8423FCE48E0099B055 /* ContentView.swift */; }; 50617D8523FCE48E0099B055 /* ContentView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 50617D8423FCE48E0099B055 /* ContentView.swift */; };
50617D8A23FCE48E0099B055 /* Preview Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 50617D8923FCE48E0099B055 /* Preview Assets.xcassets */; }; 50617D8A23FCE48E0099B055 /* Preview Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 50617D8923FCE48E0099B055 /* Preview Assets.xcassets */; };
@ -273,6 +274,7 @@
files = ( files = (
5003EF3B278005E800DF2006 /* SecretKit in Frameworks */, 5003EF3B278005E800DF2006 /* SecretKit in Frameworks */,
501421622781262300BBAA70 /* Brief in Frameworks */, 501421622781262300BBAA70 /* Brief in Frameworks */,
505993532E7E70C90092CFFA /* CertificateKit in Frameworks */,
5003EF5F2780081600DF2006 /* SecureEnclaveSecretKit in Frameworks */, 5003EF5F2780081600DF2006 /* SecureEnclaveSecretKit in Frameworks */,
5003EF612780081600DF2006 /* SmartCardSecretKit in Frameworks */, 5003EF612780081600DF2006 /* SmartCardSecretKit in Frameworks */,
); );
@ -560,6 +562,7 @@
5003EF5E2780081600DF2006 /* SecureEnclaveSecretKit */, 5003EF5E2780081600DF2006 /* SecureEnclaveSecretKit */,
5003EF602780081600DF2006 /* SmartCardSecretKit */, 5003EF602780081600DF2006 /* SmartCardSecretKit */,
501421612781262300BBAA70 /* Brief */, 501421612781262300BBAA70 /* Brief */,
505993522E7E70C90092CFFA /* CertificateKit */,
); );
productName = Secretive; productName = Secretive;
productReference = 50617D7F23FCE48E0099B055 /* Secretive.app */; productReference = 50617D7F23FCE48E0099B055 /* Secretive.app */;
@ -1785,6 +1788,10 @@
isa = XCSwiftPackageProductDependency; isa = XCSwiftPackageProductDependency;
productName = Brief; productName = Brief;
}; };
505993522E7E70C90092CFFA /* CertificateKit */ = {
isa = XCSwiftPackageProductDependency;
productName = CertificateKit;
};
50692D2C2E6FDC000043C7BB /* XPCWrappers */ = { 50692D2C2E6FDC000043C7BB /* XPCWrappers */ = {
isa = XCSwiftPackageProductDependency; isa = XCSwiftPackageProductDependency;
productName = XPCWrappers; productName = XPCWrappers;

View File

@ -3,6 +3,7 @@ import SecretKit
import SecureEnclaveSecretKit import SecureEnclaveSecretKit
import SmartCardSecretKit import SmartCardSecretKit
import Brief import Brief
import CertificateKit
@main @main
struct Secretive: App { struct Secretive: App {
@ -14,6 +15,7 @@ struct Secretive: App {
WindowGroup { WindowGroup {
ContentView() ContentView()
.environment(EnvironmentValues._secretStoreList) .environment(EnvironmentValues._secretStoreList)
.environment(EnvironmentValues._certificateStore)
.onReceive(NotificationCenter.default.publisher(for: NSApplication.didBecomeActiveNotification)) { _ in .onReceive(NotificationCenter.default.publisher(for: NSApplication.didBecomeActiveNotification)) { _ in
@AppStorage("defaultsHasRunSetup") var hasRunSetup = false @AppStorage("defaultsHasRunSetup") var hasRunSetup = false
guard hasRunSetup else { return } guard hasRunSetup else { return }
@ -121,6 +123,8 @@ extension EnvironmentValues {
return list return list
}() }()
@MainActor fileprivate static let _certificateStore: CertificateStore = CertificateStore()
private static let _agentStatusChecker = AgentStatusChecker() private static let _agentStatusChecker = AgentStatusChecker()
@Entry var agentStatusChecker: any AgentStatusCheckerProtocol = _agentStatusChecker @Entry var agentStatusChecker: any AgentStatusCheckerProtocol = _agentStatusChecker
private static let _updater: any UpdaterProtocol = { private static let _updater: any UpdaterProtocol = {
@ -135,6 +139,10 @@ extension EnvironmentValues {
@MainActor var secretStoreList: SecretStoreList { @MainActor var secretStoreList: SecretStoreList {
EnvironmentValues._secretStoreList EnvironmentValues._secretStoreList
} }
@MainActor var certificateStore: CertificateStore {
EnvironmentValues._certificateStore
}
} }
extension FocusedValues { extension FocusedValues {

View File

@ -14,6 +14,7 @@ struct ContentView: View {
@Environment(\.colorScheme) private var colorScheme @Environment(\.colorScheme) private var colorScheme
@Environment(\.openWindow) private var openWindow @Environment(\.openWindow) private var openWindow
@Environment(\.secretStoreList) private var storeList @Environment(\.secretStoreList) private var storeList
@Environment(\.certificateStore) private var certificateStore
@Environment(\.updater) private var updater @Environment(\.updater) private var updater
@Environment(\.agentStatusChecker) private var agentStatusChecker @Environment(\.agentStatusChecker) private var agentStatusChecker
@ -49,6 +50,12 @@ struct ContentView: View {
let data = try! Data(contentsOf: url) let data = try! Data(contentsOf: url)
let parser = try! await XPCCertificateParser() let parser = try! await XPCCertificateParser()
let cert = try! await parser.parse(data: data) let cert = try! await parser.parse(data: data)
let secret = storeList.allSecrets.first { secret in
secret.name == cert.name
}
guard let secret = secret ?? storeList.allSecrets.first else { return }
print(cert.data.formatted(.hex()))
certificateStore.saveCertificate(cert.data, for: secret)
print(cert) print(cert)
} }
return true return true