This commit is contained in:
Max Goedjen 2025-08-24 00:34:37 -07:00
parent 3a62a855df
commit cec13ea994
No known key found for this signature in database
13 changed files with 50 additions and 51 deletions

View File

@ -93,7 +93,7 @@ extension Agent {
for secret in secrets { for secret in secrets {
let keyBlob = writer.data(secret: secret) let keyBlob = writer.data(secret: secret)
let curveData = writer.curveType(for: secret.keyType).data(using: .utf8)! let curveData = Data(writer.curveType(for: secret.keyType).utf8)
keyData.append(writer.lengthAndData(of: keyBlob)) keyData.append(writer.lengthAndData(of: keyBlob))
keyData.append(writer.lengthAndData(of: curveData)) keyData.append(writer.lengthAndData(of: curveData))
@ -138,7 +138,7 @@ extension Agent {
let signed = try await store.sign(data: dataToSign, with: secret, for: provenance) let signed = try await store.sign(data: dataToSign, with: secret, for: provenance)
let derSignature = signed let derSignature = signed
let curveData = writer.curveType(for: secret.keyType).data(using: .utf8)! let curveData = Data(writer.curveType(for: secret.keyType).utf8)
// Convert from DER formatted rep to raw (r||s) // Convert from DER formatted rep to raw (r||s)

View File

@ -3,7 +3,7 @@ import Foundation
/// Type eraser for Secret. /// Type eraser for Secret.
public struct AnySecret: Secret, @unchecked Sendable { public struct AnySecret: Secret, @unchecked Sendable {
public let base: Any public let base: any Secret
private let hashable: AnyHashable private let hashable: AnyHashable
private let _id: () -> AnyHashable private let _id: () -> AnyHashable
private let _name: () -> String private let _name: () -> String
@ -19,7 +19,7 @@ public struct AnySecret: Secret, @unchecked Sendable {
_publicKey = secret._publicKey _publicKey = secret._publicKey
_attributes = secret._attributes _attributes = secret._attributes
} else { } else {
base = secret as Any base = secret
self.hashable = secret self.hashable = secret
_id = { secret.id as AnyHashable } _id = { secret.id as AnyHashable }
_name = { secret.name } _name = { secret.name }

View File

@ -3,7 +3,7 @@ import Foundation
/// Type eraser for SecretStore. /// Type eraser for SecretStore.
open class AnySecretStore: SecretStore, @unchecked Sendable { open class AnySecretStore: SecretStore, @unchecked Sendable {
let base: any Sendable let base: any SecretStore
private let _isAvailable: @MainActor @Sendable () -> Bool private let _isAvailable: @MainActor @Sendable () -> Bool
private let _id: @Sendable () -> UUID private let _id: @Sendable () -> UUID
private let _name: @MainActor @Sendable () -> String private let _name: @MainActor @Sendable () -> String

View File

@ -40,7 +40,7 @@ public actor OpenSSHCertificateHandler: Sendable {
let curveIdentifier = reader.readNextChunk() let curveIdentifier = reader.readNextChunk()
let publicKey = reader.readNextChunk() let publicKey = reader.readNextChunk()
let curveType = certType.replacingOccurrences(of: "-cert-v01@openssh.com", with: "").data(using: .utf8)! let curveType = Data(certType.replacingOccurrences(of: "-cert-v01@openssh.com", with: "").utf8)
return writer.lengthAndData(of: curveType) + return writer.lengthAndData(of: curveType) +
writer.lengthAndData(of: curveIdentifier) + writer.lengthAndData(of: curveIdentifier) +
writer.lengthAndData(of: publicKey) writer.lengthAndData(of: publicKey)
@ -78,14 +78,13 @@ public actor OpenSSHCertificateHandler: Sendable {
throw OpenSSHCertificateError.parsingFailed throw OpenSSHCertificateError.parsingFailed
} }
if certElements.count >= 3, let certName = certElements[2].data(using: .utf8) { if certElements.count >= 3 {
let certName = Data(certElements[2].utf8)
return (certDecoded, certName) return (certDecoded, certName)
} else if let certName = secret.name.data(using: .utf8) {
logger.info("Certificate for \(secret.name) does not have a name tag, using secret name instead")
return (certDecoded, certName)
} else {
throw OpenSSHCertificateError.parsingFailed
} }
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)
} }
} }

View File

@ -11,8 +11,8 @@ public struct OpenSSHKeyWriter: Sendable {
/// Generates an OpenSSH data payload identifying the secret. /// Generates an OpenSSH data payload identifying the secret.
/// - Returns: OpenSSH data payload identifying the secret. /// - Returns: OpenSSH data payload identifying the secret.
public func data<SecretType: Secret>(secret: SecretType) -> Data { public func data<SecretType: Secret>(secret: SecretType) -> Data {
lengthAndData(of: curveType(for: secret.keyType).data(using: .utf8)!) + lengthAndData(of: Data(curveType(for: secret.keyType).utf8)) +
lengthAndData(of: curveIdentifier(for: secret.keyType).data(using: .utf8)!) + lengthAndData(of: Data(curveIdentifier(for: secret.keyType).utf8)) +
lengthAndData(of: secret.publicKey) lengthAndData(of: secret.publicKey)
} }

View File

@ -32,7 +32,7 @@ public final class PublicKeyFileStoreController: Sendable {
try? FileManager.default.createDirectory(at: URL(fileURLWithPath: directory), withIntermediateDirectories: false, attributes: nil) try? FileManager.default.createDirectory(at: URL(fileURLWithPath: directory), withIntermediateDirectories: false, attributes: nil)
for secret in secrets { for secret in secrets {
let path = publicKeyPath(for: secret) let path = publicKeyPath(for: secret)
guard let data = keyWriter.openSSHString(secret: secret).data(using: .utf8) else { continue } let data = Data(keyWriter.openSSHString(secret: secret).utf8)
FileManager.default.createFile(atPath: path, contents: data, attributes: nil) FileManager.default.createFile(atPath: path, contents: data, attributes: nil)
} }
logger.log("Finished writing public keys") logger.log("Finished writing public keys")

View File

@ -20,7 +20,7 @@ extension SecureEnclave {
private let persistentAuthenticationHandler = PersistentAuthenticationHandler() private let persistentAuthenticationHandler = PersistentAuthenticationHandler()
/// Initializes a Store. /// Initializes a Store.
@MainActor public init() { @MainActor init() {
loadSecrets() loadSecrets()
Task { Task {
for await _ in DistributedNotificationCenter.default().notifications(named: .secretStoreUpdated) { for await _ in DistributedNotificationCenter.default().notifications(named: .secretStoreUpdated) {
@ -33,7 +33,7 @@ extension SecureEnclave {
// MARK: SecretStore // MARK: SecretStore
public func sign(data: Data, with secret: Secret, for provenance: SigningRequestProvenance) async throws -> Data { func sign(data: Data, with secret: Secret, for provenance: SigningRequestProvenance) async throws -> Data {
var context: LAContext var context: LAContext
if let existing = await persistentAuthenticationHandler.existingPersistedAuthenticationContext(secret: secret) { if let existing = await persistentAuthenticationHandler.existingPersistedAuthenticationContext(secret: secret) {
context = existing.context context = existing.context
@ -84,7 +84,7 @@ extension SecureEnclave {
} }
public func verify(signature: Data, for data: Data, with secret: Secret) throws -> Bool { func verify(signature: Data, for data: Data, with secret: Secret) throws -> Bool {
let context = LAContext() let context = LAContext()
context.localizedReason = String(localized: "auth_context_request_verify_description_\(secret.name)") context.localizedReason = String(localized: "auth_context_request_verify_description_\(secret.name)")
context.localizedCancelTitle = String(localized: "auth_context_request_deny_button") context.localizedCancelTitle = String(localized: "auth_context_request_deny_button")
@ -119,22 +119,22 @@ extension SecureEnclave {
return verified return verified
} }
public func existingPersistedAuthenticationContext(secret: Secret) async -> PersistedAuthenticationContext? { func existingPersistedAuthenticationContext(secret: Secret) async -> PersistedAuthenticationContext? {
await persistentAuthenticationHandler.existingPersistedAuthenticationContext(secret: secret) await persistentAuthenticationHandler.existingPersistedAuthenticationContext(secret: secret)
} }
public func persistAuthentication(secret: Secret, forDuration duration: TimeInterval) async throws { func persistAuthentication(secret: Secret, forDuration duration: TimeInterval) async throws {
try await persistentAuthenticationHandler.persistAuthentication(secret: secret, forDuration: duration) try await persistentAuthenticationHandler.persistAuthentication(secret: secret, forDuration: duration)
} }
@MainActor public func reloadSecrets() { @MainActor func reloadSecrets() {
secrets.removeAll() secrets.removeAll()
loadSecrets() loadSecrets()
} }
// MARK: SecretStoreModifiable // MARK: SecretStoreModifiable
public func create(name: String, attributes: Attributes) async throws { func create(name: String, attributes: Attributes) async throws {
var accessError: SecurityError? var accessError: SecurityError?
let flags: SecAccessControlCreateFlags = switch attributes.authentication { let flags: SecAccessControlCreateFlags = switch attributes.authentication {
case .notRequired: case .notRequired:
@ -174,7 +174,7 @@ extension SecureEnclave {
await reloadSecrets() await reloadSecrets()
} }
public func delete(secret: Secret) async throws { func delete(secret: Secret) async throws {
let deleteAttributes = KeychainDictionary([ let deleteAttributes = KeychainDictionary([
kSecClass: Constants.keyClass, kSecClass: Constants.keyClass,
kSecAttrService: SecureEnclave.Constants.keyTag, kSecAttrService: SecureEnclave.Constants.keyTag,
@ -188,7 +188,7 @@ extension SecureEnclave {
await reloadSecrets() await reloadSecrets()
} }
public func update(secret: Secret, name: String, attributes: Attributes) async throws { func update(secret: Secret, name: String, attributes: Attributes) async throws {
let updateQuery = KeychainDictionary([ let updateQuery = KeychainDictionary([
kSecClass: kSecClassKey, kSecClass: kSecClassKey,
kSecAttrApplicationLabel: secret.id as CFData kSecAttrApplicationLabel: secret.id as CFData
@ -205,7 +205,7 @@ extension SecureEnclave {
await reloadSecrets() await reloadSecrets()
} }
public var supportedKeyTypes: [KeyType] { var supportedKeyTypes: [KeyType] {
[ [
.init(algorithm: .ecdsa, size: 256), .init(algorithm: .ecdsa, size: 256),
.init(algorithm: .mldsa, size: 65), .init(algorithm: .mldsa, size: 65),
@ -245,7 +245,7 @@ extension SecureEnclave.CryptoKitStore {
switch (attributes.keyType.algorithm, attributes.keyType.size) { switch (attributes.keyType.algorithm, attributes.keyType.size) {
case (.ecdsa, 256): case (.ecdsa, 256):
let key = try CryptoKit.SecureEnclave.P256.Signing.PrivateKey(dataRepresentation: keyData) let key = try CryptoKit.SecureEnclave.P256.Signing.PrivateKey(dataRepresentation: keyData)
publicKey = key.publicKey.rawRepresentation publicKey = key.publicKey.x963Representation
case (.mldsa, 65): case (.mldsa, 65):
guard #available(macOS 26.0, *) else { throw UnsupportedAlgorithmError() } guard #available(macOS 26.0, *) else { throw UnsupportedAlgorithmError() }
let key = try CryptoKit.SecureEnclave.MLDSA65.PrivateKey(dataRepresentation: keyData) let key = try CryptoKit.SecureEnclave.MLDSA65.PrivateKey(dataRepresentation: keyData)

View File

@ -8,22 +8,22 @@ import SecretKit
extension SecureEnclave { extension SecureEnclave {
/// An implementation of Store backed by the Secure Enclave. /// An implementation of Store backed by the Secure Enclave.
@Observable public final class VanillaKeychainStore: SecretStoreModifiable { @Observable final class VanillaKeychainStore: SecretStoreModifiable {
@MainActor public var secrets: [Secret] = [] @MainActor var secrets: [Secret] = []
public var isAvailable: Bool { var isAvailable: Bool {
CryptoKit.SecureEnclave.isAvailable CryptoKit.SecureEnclave.isAvailable
} }
public let id = UUID() let id = UUID()
public let name = String(localized: .secureEnclave) let name = String(localized: .secureEnclave)
public var supportedKeyTypes: [KeyType] { var supportedKeyTypes: [KeyType] {
[KeyType(algorithm: .ecdsa, size: 256)] [KeyType(algorithm: .ecdsa, size: 256)]
} }
private let persistentAuthenticationHandler = PersistentAuthenticationHandler() private let persistentAuthenticationHandler = PersistentAuthenticationHandler()
/// Initializes a Store. /// Initializes a Store.
@MainActor public init() { @MainActor init() {
loadSecrets() loadSecrets()
} }
@ -31,7 +31,7 @@ extension SecureEnclave {
// MARK: SecretStore // MARK: SecretStore
public func sign(data: Data, with secret: Secret, for provenance: SigningRequestProvenance) async throws -> Data { func sign(data: Data, with secret: Secret, for provenance: SigningRequestProvenance) async throws -> Data {
let context: LAContext let context: LAContext
if let existing = await persistentAuthenticationHandler.existingPersistedAuthenticationContext(secret: secret) { if let existing = await persistentAuthenticationHandler.existingPersistedAuthenticationContext(secret: secret) {
context = existing.context context = existing.context
@ -68,26 +68,26 @@ extension SecureEnclave {
return signature as Data return signature as Data
} }
public func existingPersistedAuthenticationContext(secret: Secret) async -> PersistedAuthenticationContext? { func existingPersistedAuthenticationContext(secret: Secret) async -> PersistedAuthenticationContext? {
await persistentAuthenticationHandler.existingPersistedAuthenticationContext(secret: secret) await persistentAuthenticationHandler.existingPersistedAuthenticationContext(secret: secret)
} }
public func persistAuthentication(secret: Secret, forDuration duration: TimeInterval) async throws { func persistAuthentication(secret: Secret, forDuration duration: TimeInterval) async throws {
try await persistentAuthenticationHandler.persistAuthentication(secret: secret, forDuration: duration) try await persistentAuthenticationHandler.persistAuthentication(secret: secret, forDuration: duration)
} }
@MainActor public func reloadSecrets() { @MainActor func reloadSecrets() {
secrets.removeAll() secrets.removeAll()
loadSecrets() loadSecrets()
} }
// MARK: SecretStoreModifiable // MARK: SecretStoreModifiable
public func create(name: String, attributes: Attributes) async throws { func create(name: String, attributes: Attributes) async throws {
throw DeprecatedCreationStore() throw DeprecatedCreationStore()
} }
public func delete(secret: Secret) async throws { func delete(secret: Secret) async throws {
let deleteAttributes = KeychainDictionary([ let deleteAttributes = KeychainDictionary([
kSecClass: kSecClassKey, kSecClass: kSecClassKey,
kSecAttrApplicationLabel: secret.id as CFData kSecAttrApplicationLabel: secret.id as CFData
@ -99,7 +99,7 @@ extension SecureEnclave {
await reloadSecrets() await reloadSecrets()
} }
public func update(secret: Secret, name: String, attributes: Attributes) async throws { func update(secret: Secret, name: String, attributes: Attributes) async throws {
let updateQuery = KeychainDictionary([ let updateQuery = KeychainDictionary([
kSecClass: kSecClassKey, kSecClass: kSecClassKey,
kSecAttrApplicationLabel: secret.id as CFData kSecAttrApplicationLabel: secret.id as CFData

View File

@ -79,9 +79,9 @@ extension Stub {
struct Secret: SecretKit.Secret, CustomDebugStringConvertible { struct Secret: SecretKit.Secret, CustomDebugStringConvertible {
let id = UUID().uuidString.data(using: .utf8)! let id = Data(UUID().uuidString.utf8)
let name = UUID().uuidString let name = UUID().uuidString
let algorithm = Algorithm.ellipticCurve let algorithm = Algorithm.ecdsa
let keySize: Int let keySize: Int
let publicKey: Data let publicKey: Data

View File

@ -7,12 +7,12 @@ import Testing
@Suite struct AnySecretTests { @Suite struct AnySecretTests {
@Test func eraser() { @Test func eraser() {
let secret = SmartCard.Secret(id: UUID().uuidString.data(using: .utf8)!, name: "Name", algorithm: .ellipticCurve, keySize: 256, publicKey: UUID().uuidString.data(using: .utf8)!) let data = Data(UUID().uuidString.utf8)
let secret = SmartCard.Secret(id: data, name: "Name", publicKey: data, attributes: Attributes(keyType: KeyType(algorithm: .ecdsa, size: 256)))
let erased = AnySecret(secret) let erased = AnySecret(secret)
#expect(erased.id == secret.id as AnyHashable) #expect(erased.id == secret.id as AnyHashable)
#expect(erased.name == secret.name) #expect(erased.name == secret.name)
#expect(erased.algorithm == secret.algorithm) #expect(erased.keyType == secret.keyType)
#expect(erased.keySize == secret.keySize)
#expect(erased.publicKey == secret.publicKey) #expect(erased.publicKey == secret.publicKey)
} }

View File

@ -18,7 +18,7 @@ import Testing
@Test func ecdsa256PublicKey() { @Test func ecdsa256PublicKey() {
#expect(writer.openSSHString(secret: Constants.ecdsa256Secret) == #expect(writer.openSSHString(secret: Constants.ecdsa256Secret) ==
"ecdsa-sha2-nistp256 AAAAE2VjZHNhLXNoYTItbmlzdHAyNTYAAAAIbmlzdHAyNTYAAABBBOVEjgAA5PHqRgwykjN5qM21uWCHFSY/Sqo5gkHAkn+e1MMQKHOLga7ucB9b3mif33MBid59GRK9GEPVlMiSQwo=") "ecdsa-sha2-nistp256 AAAAE2VjZHNhLXNoYTItbmlzdHAyNTYAAAAIbmlzdHAyNTYAAABBBOVEjgAA5PHqRgwykjN5qM21uWCHFSY/Sqo5gkHAkn+e1MMQKHOLga7ucB9b3mif33MBid59GRK9GEPVlMiSQwo= test@example.com")
} }
@Test func ecdsa256Hash() { @Test func ecdsa256Hash() {
@ -35,7 +35,7 @@ import Testing
@Test func ecdsa384PublicKey() { @Test func ecdsa384PublicKey() {
#expect(writer.openSSHString(secret: Constants.ecdsa384Secret) == #expect(writer.openSSHString(secret: Constants.ecdsa384Secret) ==
"ecdsa-sha2-nistp384 AAAAE2VjZHNhLXNoYTItbmlzdHAzODQAAAAIbmlzdHAzODQAAABhBG2MNc/C5OTHFE2tBvbZCVcpOGa8vBMquiTLkH4lwkeqOPxhi+PyYUfQZMTRJNPiTyWPoMBqNiCIFRVv60yPN/AHufHaOgbdTP42EgMlMMImkAjYUEv9DESHTVIs2PW1yQ==") "ecdsa-sha2-nistp384 AAAAE2VjZHNhLXNoYTItbmlzdHAzODQAAAAIbmlzdHAzODQAAABhBG2MNc/C5OTHFE2tBvbZCVcpOGa8vBMquiTLkH4lwkeqOPxhi+PyYUfQZMTRJNPiTyWPoMBqNiCIFRVv60yPN/AHufHaOgbdTP42EgMlMMImkAjYUEv9DESHTVIs2PW1yQ== test@example.com")
} }
@Test func ecdsa384Hash() { @Test func ecdsa384Hash() {
@ -47,8 +47,8 @@ import Testing
extension OpenSSHWriterTests { extension OpenSSHWriterTests {
enum Constants { enum Constants {
static let ecdsa256Secret = SmartCard.Secret(id: Data(), name: "Test Key (ECDSA 256)", algorithm: .ellipticCurve, keySize: 256, publicKey: Data(base64Encoded: "BOVEjgAA5PHqRgwykjN5qM21uWCHFSY/Sqo5gkHAkn+e1MMQKHOLga7ucB9b3mif33MBid59GRK9GEPVlMiSQwo=")!) static let ecdsa256Secret = SmartCard.Secret(id: Data(), name: "Test Key (ECDSA 256)", publicKey: Data(base64Encoded: "BOVEjgAA5PHqRgwykjN5qM21uWCHFSY/Sqo5gkHAkn+e1MMQKHOLga7ucB9b3mif33MBid59GRK9GEPVlMiSQwo=")!, attributes: Attributes(keyType: KeyType(algorithm: .ecdsa, size: 256), publicKeyAttribution: "test@example.com"))
static let ecdsa384Secret = SmartCard.Secret(id: Data(), name: "Test Key (ECDSA 384)", algorithm: .ellipticCurve, keySize: 384, publicKey: Data(base64Encoded: "BG2MNc/C5OTHFE2tBvbZCVcpOGa8vBMquiTLkH4lwkeqOPxhi+PyYUfQZMTRJNPiTyWPoMBqNiCIFRVv60yPN/AHufHaOgbdTP42EgMlMMImkAjYUEv9DESHTVIs2PW1yQ==")!) static let ecdsa384Secret = SmartCard.Secret(id: Data(), name: "Test Key (ECDSA 384)", publicKey: Data(base64Encoded: "BG2MNc/C5OTHFE2tBvbZCVcpOGa8vBMquiTLkH4lwkeqOPxhi+PyYUfQZMTRJNPiTyWPoMBqNiCIFRVv60yPN/AHufHaOgbdTP42EgMlMMImkAjYUEv9DESHTVIs2PW1yQ==")!, attributes: Attributes(keyType: KeyType(algorithm: .ecdsa, size: 384), publicKeyAttribution: "test@example.com"))
} }

View File

@ -56,7 +56,7 @@ struct ShellConfigurationController {
} catch { } catch {
return false return false
} }
handle.write("\n# Secretive Config\n\(shellInstructions.text)\n".data(using: .utf8)!) handle.write(Data("\n# Secretive Config\n\(shellInstructions.text)\n".utf8))
return true return true
} }

View File

@ -9,7 +9,7 @@ extension Preview {
let id = UUID().uuidString let id = UUID().uuidString
let name: String let name: String
let publicKey = UUID().uuidString.data(using: .utf8)! let publicKey = Data(UUID().uuidString.utf8)
var attributes: Attributes { var attributes: Attributes {
Attributes( Attributes(
keyType: .init(algorithm: .ecdsa, size: 256), keyType: .init(algorithm: .ecdsa, size: 256),