mirror of
https://github.com/maxgoedjen/secretive.git
synced 2026-05-07 07:58:58 +02:00
Expand parsing + bug fixes for cert UI (#802)
* Expand parsing and display of cert types, some additional cleanup * Tweak cert
This commit is contained in:
@@ -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,
|
||||
),
|
||||
]
|
||||
)
|
||||
|
||||
|
||||
@@ -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,
|
||||
),
|
||||
|
||||
@@ -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" : {
|
||||
|
||||
24
Sources/Packages/Sources/CertificateKit/Certificate.swift
Normal file
24
Sources/Packages/Sources/CertificateKit/Certificate.swift
Normal file
@@ -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<T>(dynamicMember keyPath: KeyPath<OpenSSHCertificate, T>) -> T {
|
||||
openSSHCertificate[keyPath: keyPath]
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
|
||||
@@ -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<Date>?
|
||||
public var criticalOptions: [String]
|
||||
public var extensions: [String]
|
||||
public var signingKey: PublicKey
|
||||
|
||||
public init(
|
||||
type: OpenSSHCertificate.CertificateType,
|
||||
name: String,
|
||||
data: Data,
|
||||
publicKey: PublicKey,
|
||||
principals: [String],
|
||||
keyID: String,
|
||||
serial: UInt64,
|
||||
validityRange: Range<Date>? = nil,
|
||||
criticalOptions: [String],
|
||||
extensions: [String],
|
||||
signingKey: PublicKey,
|
||||
) {
|
||||
self.type = type
|
||||
self.name = name
|
||||
self.data = data
|
||||
self.publicKey = publicKey
|
||||
self.principals = principals
|
||||
self.keyID = keyID
|
||||
self.serial = serial
|
||||
self.validityRange = validityRange
|
||||
self.criticalOptions = criticalOptions
|
||||
self.extensions = extensions
|
||||
self.signingKey = signingKey
|
||||
}
|
||||
|
||||
public var debugDescription: String {
|
||||
"OpenSSH Certificate \(name, default: "Unnamed"): \(data.formatted(.hex()))"
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
extension OpenSSHCertificate {
|
||||
|
||||
public enum CertificateType: String, Sendable, Codable {
|
||||
case ecdsa256 = "ecdsa-sha2-nistp256-cert-v01@openssh.com"
|
||||
case ecdsa384 = "ecdsa-sha2-nistp384-cert-v01@openssh.com"
|
||||
case nistp521 = "ecdsa-sha2-nistp521-cert-v01@openssh.com"
|
||||
|
||||
public var keyIdentifier: String {
|
||||
rawValue.replacingOccurrences(of: "-cert-v01@openssh.com", with: "")
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
extension OpenSSHCertificate {
|
||||
|
||||
public struct PublicKey: Hashable, Sendable, Codable {
|
||||
|
||||
public let keyType: String
|
||||
public let curveName: String
|
||||
public let data: Data
|
||||
|
||||
public init(keyType: String, curveName: String, data: Data) {
|
||||
self.keyType = keyType
|
||||
self.curveName = curveName
|
||||
self.data = data
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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()
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -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)"
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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<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: "")
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
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))..<Date(timeIntervalSince1970: TimeInterval(validBefore))
|
||||
let criticalOptionsReader = try dataParser.readNextChunkAsSubReader()
|
||||
var criticalOptions: [String] = []
|
||||
while !criticalOptionsReader.done {
|
||||
let next = try criticalOptionsReader.readNextChunkAsString()
|
||||
if !next.isEmpty {
|
||||
criticalOptions.append(next)
|
||||
}
|
||||
}
|
||||
let extensionsReader = try dataParser.readNextChunkAsSubReader()
|
||||
var extensions: [String] = []
|
||||
while !extensionsReader.done {
|
||||
let next = try extensionsReader.readNextChunkAsString()
|
||||
if !next.isEmpty {
|
||||
extensions.append(next)
|
||||
}
|
||||
}
|
||||
_ = try dataParser.readNextChunk() // reserved
|
||||
let signingKeyReader = try dataParser.readNextChunkAsSubReader()
|
||||
let signingKeyType = try signingKeyReader.readNextChunkAsString()
|
||||
let signingKeyCurveName = try signingKeyReader.readNextChunkAsString()
|
||||
let signingKeyData = try signingKeyReader.readNextChunk()
|
||||
let signingKey = OpenSSHCertificate.PublicKey(keyType: signingKeyType, curveName: signingKeyCurveName, data: signingKeyData)
|
||||
|
||||
return OpenSSHCertificate(
|
||||
type: type,
|
||||
@@ -89,7 +77,10 @@ public struct OpenSSHCertificateParser: OpenSSHCertificateParserProtocol, Sendab
|
||||
principals: principals,
|
||||
keyID: keyIdentifier,
|
||||
serial: serialNumber,
|
||||
validityRange: validityRange
|
||||
validityRange: validityRange,
|
||||
criticalOptions: criticalOptions,
|
||||
extensions: extensions,
|
||||
signingKey: signingKey,
|
||||
)
|
||||
} catch {
|
||||
throw .parsingFailed
|
||||
@@ -1,6 +1,7 @@
|
||||
import Foundation
|
||||
import OSLog
|
||||
import SecretKit
|
||||
import CertificateKit
|
||||
|
||||
public protocol SSHAgentInputParserProtocol {
|
||||
|
||||
|
||||
@@ -2,6 +2,7 @@ import Foundation
|
||||
import OSLog
|
||||
import SecretKit
|
||||
import SSHProtocolKit
|
||||
import CertificateKit
|
||||
import Common
|
||||
|
||||
/// Controller responsible for writing public keys to disk, so that they're easily accessible by scripts.
|
||||
@@ -48,10 +49,10 @@ public final class PublicKeyFileStoreController: Sendable {
|
||||
/// 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 {
|
||||
public func generateCertificates(for certificates: [Certificate], clear: Bool = false) throws {
|
||||
logger.log("Writing certificates to disk")
|
||||
if clear {
|
||||
let validPaths = Set(certificates.map { URL.certificatePath(for: $0, in: certificatesURL) })
|
||||
let validPaths = Set(certificates.map { URL.certificatePath(for: $0.id, in: certificatesURL) })
|
||||
let contentsOfDirectory = (try? FileManager.default.contentsOfDirectory(atPath: certificatesURL.path())) ?? []
|
||||
let fullPathContents = contentsOfDirectory.map { certificatesURL.appending(path: $0).path() }
|
||||
|
||||
@@ -64,8 +65,8 @@ public final class PublicKeyFileStoreController: Sendable {
|
||||
}
|
||||
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)
|
||||
let path = URL.certificatePath(for: certificate.id, in: certificatesURL)
|
||||
FileManager.default.createFile(atPath: path, contents: certificate.rawData, attributes: nil)
|
||||
}
|
||||
logger.log("Finished writing certificates")
|
||||
}
|
||||
@@ -76,7 +77,7 @@ public final class PublicKeyFileStoreController: Sendable {
|
||||
/// - Warning: This method returning a path does not imply that a key has a SSH certificates. This method only describes where it will be.
|
||||
private func legacySSHCertificatePath<SecretType: Secret>(for secret: SecretType) -> String {
|
||||
let minimalHex = keyWriter.openSSHMD5Fingerprint(secret: secret).replacingOccurrences(of: ":", with: "")
|
||||
return publicKeysURL.appending(component: "\(minimalHex)-cert.pub").path()
|
||||
return publicKeysURL.appending(component: "\(minimalHex).pub").path()
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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 */;
|
||||
|
||||
@@ -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()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -4,7 +4,7 @@ import SSHProtocolKit
|
||||
|
||||
extension View {
|
||||
|
||||
func showingDeleteConfirmation(isPresented: Binding<Bool>, _ certificate: OpenSSHCertificate, _ store: CertificateStore, dismissalBlock: @escaping (Bool) -> ()) -> some View {
|
||||
func showingDeleteConfirmation(isPresented: Binding<Bool>, _ certificate: Certificate, _ store: CertificateStore, dismissalBlock: @escaping (Bool) -> ()) -> some View {
|
||||
modifier(DeleteCertificateConfirmationModifier(isPresented: isPresented, certificate: certificate, store: store, dismissalBlock: dismissalBlock))
|
||||
}
|
||||
|
||||
@@ -13,7 +13,7 @@ extension View {
|
||||
struct DeleteCertificateConfirmationModifier: ViewModifier {
|
||||
|
||||
var isPresented: Binding<Bool>
|
||||
var certificate: OpenSSHCertificate
|
||||
var certificate: Certificate
|
||||
var store: CertificateStore
|
||||
var dismissalBlock: (Bool) -> ()
|
||||
@State var confirmedSecretName = ""
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -1,13 +1,14 @@
|
||||
import SwiftUI
|
||||
import SecretKit
|
||||
import Common
|
||||
import CertificateKit
|
||||
import SSHProtocolKit
|
||||
|
||||
struct SecretDetailView<SecretType: Secret>: View {
|
||||
|
||||
let secret: SecretType
|
||||
let certificates: [OpenSSHCertificate]
|
||||
let navigateToCertificate: ((OpenSSHCertificate) -> Void)?
|
||||
let certificates: [Certificate]
|
||||
let navigateToCertificate: ((Certificate) -> Void)?
|
||||
|
||||
private let keyWriter = OpenSSHPublicKeyWriter()
|
||||
|
||||
|
||||
@@ -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?
|
||||
|
||||
@@ -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 {
|
||||
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user