From 3a62a855dfa17f5f30bf7d294a58897a7e4457c0 Mon Sep 17 00:00:00 2001 From: Max Goedjen Date: Sat, 23 Aug 2025 22:47:39 -0700 Subject: [PATCH] Proxy through --- .../SecretKit/Erasers/AnySecretStore.swift | 2 +- .../Sources/SecretKit/SecretStoreList.swift | 2 +- .../Sources/SecretKit/Types/SecretStore.swift | 4 +- .../SecureEnclaveSecret.swift | 1 + .../SecureEnclaveStore.swift | 225 +++++------------- .../SecureEnclaveCryptoKitStore.swift | 51 ++-- .../SecureEnclaveVanillaKeychainStore.swift | 166 +++++++++++++ Sources/SecretAgent/AppDelegate.swift | 1 - Sources/Secretive/App.swift | 1 - 9 files changed, 252 insertions(+), 201 deletions(-) rename Sources/Packages/Sources/SecureEnclaveSecretKit/{ => SecureEnclaveStores}/SecureEnclaveCryptoKitStore.swift (88%) create mode 100644 Sources/Packages/Sources/SecureEnclaveSecretKit/SecureEnclaveStores/SecureEnclaveVanillaKeychainStore.swift diff --git a/Sources/Packages/Sources/SecretKit/Erasers/AnySecretStore.swift b/Sources/Packages/Sources/SecretKit/Erasers/AnySecretStore.swift index 747abc5..03b357a 100644 --- a/Sources/Packages/Sources/SecretKit/Erasers/AnySecretStore.swift +++ b/Sources/Packages/Sources/SecretKit/Erasers/AnySecretStore.swift @@ -66,7 +66,7 @@ public final class AnySecretStoreModifiable: AnySecretStore, SecretStoreModifiab private let _update: @Sendable (AnySecret, String, Attributes) async throws -> Void private let _supportedKeyTypes: @Sendable () -> [KeyType] - public init(modifiable secretStore: SecretStoreType) where SecretStoreType: SecretStoreModifiable { + public init(_ secretStore: SecretStoreType) where SecretStoreType: SecretStoreModifiable { _create = { try await secretStore.create(name: $0, attributes: $1) } _delete = { try await secretStore.delete(secret: $0.base as! SecretStoreType.SecretType) } _update = { try await secretStore.update(secret: $0.base as! SecretStoreType.SecretType, name: $1, attributes: $2) } diff --git a/Sources/Packages/Sources/SecretKit/SecretStoreList.swift b/Sources/Packages/Sources/SecretKit/SecretStoreList.swift index d8d4074..8b96e3e 100644 --- a/Sources/Packages/Sources/SecretKit/SecretStoreList.swift +++ b/Sources/Packages/Sources/SecretKit/SecretStoreList.swift @@ -20,7 +20,7 @@ import Observation /// Adds a non-type-erased modifiable SecretStore. public func add(store: SecretStoreType) { - let modifiable = AnySecretStoreModifiable(modifiable: store) + let modifiable = AnySecretStoreModifiable(store) if modifiableStore == nil { modifiableStore = modifiable } diff --git a/Sources/Packages/Sources/SecretKit/Types/SecretStore.swift b/Sources/Packages/Sources/SecretKit/Types/SecretStore.swift index 6308a5b..c1dcdec 100644 --- a/Sources/Packages/Sources/SecretKit/Types/SecretStore.swift +++ b/Sources/Packages/Sources/SecretKit/Types/SecretStore.swift @@ -1,7 +1,7 @@ import Foundation /// Manages access to Secrets, and performs signature operations on data using those Secrets. -public protocol SecretStore: Identifiable, Sendable { +public protocol SecretStore: Identifiable, Sendable { associatedtype SecretType: Secret @@ -41,7 +41,7 @@ public protocol SecretStore: Identifiable, Sendable { } /// A SecretStore that the Secretive admin app can modify. -public protocol SecretStoreModifiable: SecretStore { +public protocol SecretStoreModifiable: SecretStore { /// Creates a new ``Secret`` in the store. /// - Parameters: diff --git a/Sources/Packages/Sources/SecureEnclaveSecretKit/SecureEnclaveSecret.swift b/Sources/Packages/Sources/SecureEnclaveSecretKit/SecureEnclaveSecret.swift index 272c3a6..1b12416 100644 --- a/Sources/Packages/Sources/SecureEnclaveSecretKit/SecureEnclaveSecret.swift +++ b/Sources/Packages/Sources/SecureEnclaveSecretKit/SecureEnclaveSecret.swift @@ -41,6 +41,7 @@ extension SecureEnclave { self.publicKey = publicKey self.attributes = attributes } + } } diff --git a/Sources/Packages/Sources/SecureEnclaveSecretKit/SecureEnclaveStore.swift b/Sources/Packages/Sources/SecureEnclaveSecretKit/SecureEnclaveStore.swift index 3956525..79b086d 100644 --- a/Sources/Packages/Sources/SecureEnclaveSecretKit/SecureEnclaveStore.swift +++ b/Sources/Packages/Sources/SecureEnclaveSecretKit/SecureEnclaveStore.swift @@ -8,8 +8,15 @@ import SecretKit extension SecureEnclave { /// An implementation of Store backed by the Secure Enclave. + /// Under the hood, this proxies to two sub-stores – both are backed by the Secure Enclave. + /// One is a legacy store (VanillaKeychainStore) which stores NIST-P256 keys directly in the keychain. + /// The other (CryptoKitStore) stores the keys using CryptoKit, and supports additional key types. @Observable public final class Store: SecretStoreModifiable { + @MainActor private let cryptoKit = CryptoKitStore() + @MainActor private let vanillaKeychain = VanillaKeychainStore() + @MainActor private var secretSourceMap: [Secret: Source] = [:] + @MainActor public var secrets: [Secret] = [] public var isAvailable: Bool { CryptoKit.SecureEnclave.isAvailable @@ -17,159 +24,102 @@ extension SecureEnclave { public let id = UUID() public let name = String(localized: .secureEnclave) public var supportedKeyTypes: [KeyType] { - [KeyType(algorithm: .ecdsa, size: 256)] + cryptoKit.supportedKeyTypes } private let persistentAuthenticationHandler = PersistentAuthenticationHandler() + // MARK: SecretStore + /// Initializes a Store. @MainActor public init() { - loadSecrets() + reloadSecrets() Task { for await _ in DistributedNotificationCenter.default().notifications(named: .secretStoreUpdated) { - await reloadSecretsInternal(notifyAgent: false) + reloadSecretsInternal(notifyAgent: false) } } } - // MARK: - Public API - - // MARK: SecretStore - - public func sign(data: Data, with secret: Secret, for provenance: SigningRequestProvenance) async throws -> Data { - let context: LAContext - if let existing = await persistentAuthenticationHandler.existingPersistedAuthenticationContext(secret: secret) { - context = existing.context - } else { - let newContext = LAContext() - newContext.localizedCancelTitle = String(localized: .authContextRequestDenyButton) - context = newContext - } - context.localizedReason = String(localized: .authContextRequestSignatureDescription(appName: provenance.origin.displayName, secretName: secret.name)) - let attributes = KeychainDictionary([ - kSecClass: kSecClassKey, - kSecAttrKeyClass: kSecAttrKeyClassPrivate, - kSecAttrApplicationLabel: secret.id as CFData, - kSecAttrKeyType: Constants.keyType, - kSecAttrTokenID: kSecAttrTokenIDSecureEnclave, - kSecAttrApplicationTag: Constants.keyTag, - kSecUseAuthenticationContext: context, - kSecReturnRef: true - ]) - var untyped: CFTypeRef? - let status = SecItemCopyMatching(attributes, &untyped) - if status != errSecSuccess { - throw KeychainError(statusCode: status) - } - guard let untypedSafe = untyped else { - throw KeychainError(statusCode: errSecSuccess) - } - let key = untypedSafe as! SecKey - var signError: SecurityError? - - guard let signature = SecKeyCreateSignature(key, .ecdsaSignatureMessageX962SHA256, data as CFData, &signError) else { - throw SigningError(error: signError) - } - return signature as Data + @MainActor public func reloadSecrets() { + reloadSecretsInternal(notifyAgent: false) } - public func existingPersistedAuthenticationContext(secret: Secret) async -> PersistedAuthenticationContext? { - await persistentAuthenticationHandler.existingPersistedAuthenticationContext(secret: secret) + public func sign(data: Data, with secret: Secret, for provenance: SigningRequestProvenance) async throws -> Data { + try await store(for: secret) + .sign(data: data, with: secret, for: provenance) + } + + public func existingPersistedAuthenticationContext(secret: Secret) async -> (any SecretKit.PersistedAuthenticationContext)? { + await store(for: secret) + .existingPersistedAuthenticationContext(secret: secret) } public func persistAuthentication(secret: Secret, forDuration duration: TimeInterval) async throws { - try await persistentAuthenticationHandler.persistAuthentication(secret: secret, forDuration: duration) + try await store(for: secret) + .persistAuthentication(secret: secret, forDuration: duration) } public func reloadSecrets() async { - await reloadSecretsInternal(notifyAgent: false) + await reloadSecretsInternal() } // MARK: SecretStoreModifiable public func create(name: String, attributes: Attributes) async throws { - var accessError: SecurityError? - let flags: SecAccessControlCreateFlags - if attributes.authentication.required { - flags = [.privateKeyUsage, .userPresence] - } else { - flags = .privateKeyUsage - } - let access = - SecAccessControlCreateWithFlags(kCFAllocatorDefault, - kSecAttrAccessibleWhenUnlockedThisDeviceOnly, - flags, - &accessError) as Any - if let error = accessError { - throw error.takeRetainedValue() as Error - } - - let attributes = KeychainDictionary([ - kSecAttrLabel: name, - kSecAttrKeyType: Constants.keyType, - kSecAttrTokenID: kSecAttrTokenIDSecureEnclave, - kSecAttrApplicationTag: Constants.keyTag, - kSecPrivateKeyAttrs: [ - kSecAttrIsPermanent: true, - kSecAttrAccessControl: access - ] - ]) - - var createKeyError: SecurityError? - let keypair = SecKeyCreateRandomKey(attributes, &createKeyError) - if let error = createKeyError { - throw error.takeRetainedValue() as Error - } - guard let keypair = keypair, let publicKey = SecKeyCopyPublicKey(keypair) else { - throw KeychainError(statusCode: nil) - } - try savePublicKey(publicKey, name: name) - await reloadSecretsInternal() + try await cryptoKit.create(name: name, attributes: attributes) } public func delete(secret: Secret) async throws { - let deleteAttributes = KeychainDictionary([ - kSecClass: kSecClassKey, - kSecAttrApplicationLabel: secret.id as CFData - ]) - let status = SecItemDelete(deleteAttributes) - if status != errSecSuccess { - throw KeychainError(statusCode: status) - } - await reloadSecretsInternal() + try await store(for: secret) + .delete(secret: secret) } - public func update(secret: Secret, name: String, attributes: Attributes) async throws { - let updateQuery = KeychainDictionary([ - kSecClass: kSecClassKey, - kSecAttrApplicationLabel: secret.id as CFData - ]) - - let updatedAttributes = KeychainDictionary([ - kSecAttrLabel: name, - ]) - - let status = SecItemUpdate(updateQuery, updatedAttributes) - if status != errSecSuccess { - throw KeychainError(statusCode: status) - } - await reloadSecretsInternal() + public func update(secret: Secret, name: String, attributes: SecretKit.Attributes) async throws { + try await store(for: secret) + .update(secret: secret, name: name, attributes: attributes) } - } } extension SecureEnclave.Store { + fileprivate enum Source { + case cryptoKit + case vanilla + } + + + @MainActor func store(for secret: SecretType) -> any SecretStoreModifiable { + switch secretSourceMap[secret, default: .cryptoKit] { + case .cryptoKit: + cryptoKit + case .vanilla: + vanillaKeychain + } + } + /// Reloads all secrets from the store. /// - Parameter notifyAgent: A boolean indicating whether a distributed notification should be posted, notifying other processes (ie, the SecretAgent) to reload their stores as well. - @MainActor private func reloadSecretsInternal(notifyAgent: Bool = true) async { + @MainActor private func reloadSecretsInternal(notifyAgent: Bool = true) { let before = secrets - secrets.removeAll() - loadSecrets() - if secrets != before { + var mapped: [SecretType: Source] = [:] + var new: [SecretType] = [] + cryptoKit.reloadSecrets() + new.append(contentsOf: cryptoKit.secrets) + for secret in cryptoKit.secrets { + mapped[secret] = .cryptoKit + } + vanillaKeychain.reloadSecrets() + new.append(contentsOf: vanillaKeychain.secrets) + for secret in vanillaKeychain.secrets { + mapped[secret] = .vanilla + } + secretSourceMap = mapped + secrets = new + if new != before { NotificationCenter.default.post(name: .secretStoreReloaded, object: self) if notifyAgent { DistributedNotificationCenter.default().postNotificationName(.secretStoreUpdated, object: nil, deliverImmediately: true) @@ -177,60 +127,13 @@ extension SecureEnclave.Store { } } - /// Loads all secrets from the store. - @MainActor private func loadSecrets() { - let publicAttributes = KeychainDictionary([ - kSecClass: kSecClassKey, - kSecAttrKeyType: SecureEnclave.Constants.keyType, - kSecAttrApplicationTag: SecureEnclave.Constants.keyTag, - kSecAttrKeyClass: kSecAttrKeyClassPublic, - kSecReturnRef: true, - kSecMatchLimit: kSecMatchLimitAll, - kSecReturnAttributes: true - ]) - var publicUntyped: CFTypeRef? - SecItemCopyMatching(publicAttributes, &publicUntyped) - guard let publicTyped = publicUntyped as? [[CFString: Any]] else { return } - let wrapped: [SecureEnclave.Secret] = publicTyped.map { - let name = $0[kSecAttrLabel] as? String ?? String(localized: .unnamedSecret) - let id = $0[kSecAttrApplicationLabel] as! Data - let publicKeyRef = $0[kSecValueRef] as! SecKey - let publicKeyAttributes = SecKeyCopyAttributes(publicKeyRef) as! [CFString: Any] - let publicKey = publicKeyAttributes[kSecValueData] as! Data - return SecureEnclave.Secret(id: id, name: name, authenticationRequirement: .unknown, publicKey: publicKey) - } - secrets.append(contentsOf: wrapped) - } - - /// Saves a public key. - /// - Parameters: - /// - publicKey: The public key to save. - /// - name: A user-facing name for the key. - private func savePublicKey(_ publicKey: SecKey, name: String) throws { - let attributes = KeychainDictionary([ - kSecClass: kSecClassKey, - kSecAttrKeyType: SecureEnclave.Constants.keyType, - kSecAttrKeyClass: kSecAttrKeyClassPublic, - kSecAttrApplicationTag: SecureEnclave.Constants.keyTag, - kSecValueRef: publicKey, - kSecAttrIsPermanent: true, - kSecReturnData: true, - kSecAttrLabel: name - ]) - let status = SecItemAdd(attributes, nil) - if status != errSecSuccess { - throw KeychainError(statusCode: status) - } - } } extension SecureEnclave { - public enum Constants { - public static let keyTag = Data("com.maxgoedjen.secretive.secureenclave.key".utf8) - public static let keyType = kSecAttrKeyTypeECSECPrimeRandom as String - static let unauthenticatedThreshold: TimeInterval = 0.05 + enum Constants { + static let keyTag = Data("com.maxgoedjen.secretive.secureenclave.key".utf8) } } diff --git a/Sources/Packages/Sources/SecureEnclaveSecretKit/SecureEnclaveCryptoKitStore.swift b/Sources/Packages/Sources/SecureEnclaveSecretKit/SecureEnclaveStores/SecureEnclaveCryptoKitStore.swift similarity index 88% rename from Sources/Packages/Sources/SecureEnclaveSecretKit/SecureEnclaveCryptoKitStore.swift rename to Sources/Packages/Sources/SecureEnclaveSecretKit/SecureEnclaveStores/SecureEnclaveCryptoKitStore.swift index 8c59e20..e7764a0 100644 --- a/Sources/Packages/Sources/SecureEnclaveSecretKit/SecureEnclaveCryptoKitStore.swift +++ b/Sources/Packages/Sources/SecureEnclaveSecretKit/SecureEnclaveStores/SecureEnclaveCryptoKitStore.swift @@ -9,15 +9,14 @@ import os extension SecureEnclave { /// An implementation of Store backed by the Secure Enclave using CryptoKit API. - @available(macOS 14, *) - @Observable public final class CryptoKitStore: SecretStoreModifiable { + @Observable final class CryptoKitStore: SecretStoreModifiable { - @MainActor public var secrets: [Secret] = [] - public var isAvailable: Bool { + @MainActor var secrets: [Secret] = [] + var isAvailable: Bool { CryptoKit.SecureEnclave.isAvailable } - public let id = UUID() - public let name = String(localized: .secureEnclave) + let id = UUID() + let name = String(localized: .secureEnclave) private let persistentAuthenticationHandler = PersistentAuthenticationHandler() /// Initializes a Store. @@ -25,7 +24,7 @@ extension SecureEnclave { loadSecrets() Task { for await _ in DistributedNotificationCenter.default().notifications(named: .secretStoreUpdated) { - await reloadSecretsInternal(notifyAgent: false) + reloadSecrets() } } } @@ -47,7 +46,7 @@ extension SecureEnclave { let queryAttributes = KeychainDictionary([ kSecClass: Constants.keyClass, - kSecAttrService: Constants.keyTag, + kSecAttrService: SecureEnclave.Constants.keyTag, kSecUseDataProtectionKeychain: true, kSecAttrAccount: String(decoding: secret.id, as: UTF8.self), kSecReturnAttributes: true, @@ -95,7 +94,7 @@ extension SecureEnclave { kSecAttrApplicationLabel: secret.id as CFData, kSecAttrKeyType: Constants.keyClass, kSecAttrTokenID: kSecAttrTokenIDSecureEnclave, - kSecAttrApplicationTag: Constants.keyTag, + kSecAttrApplicationTag: SecureEnclave.Constants.keyTag, kSecUseAuthenticationContext: context, kSecReturnRef: true ]) @@ -128,8 +127,9 @@ extension SecureEnclave { try await persistentAuthenticationHandler.persistAuthentication(secret: secret, forDuration: duration) } - public func reloadSecrets() async { - await reloadSecretsInternal(notifyAgent: false) + @MainActor public func reloadSecrets() { + secrets.removeAll() + loadSecrets() } // MARK: SecretStoreModifiable @@ -171,13 +171,13 @@ extension SecureEnclave { throw Attributes.UnsupportedOptionError() } try saveKey(dataRep, name: name, attributes: attributes) - await reloadSecretsInternal() + await reloadSecrets() } public func delete(secret: Secret) async throws { let deleteAttributes = KeychainDictionary([ kSecClass: Constants.keyClass, - kSecAttrService: Constants.keyTag, + kSecAttrService: SecureEnclave.Constants.keyTag, kSecUseDataProtectionKeychain: true, kSecAttrAccount: String(decoding: secret.id, as: UTF8.self) ]) @@ -185,7 +185,7 @@ extension SecureEnclave { if status != errSecSuccess { throw KeychainError(statusCode: status) } - await reloadSecretsInternal() + await reloadSecrets() } public func update(secret: Secret, name: String, attributes: Attributes) async throws { @@ -202,7 +202,7 @@ extension SecureEnclave { if status != errSecSuccess { throw KeychainError(statusCode: status) } - await reloadSecretsInternal() + await reloadSecrets() } public var supportedKeyTypes: [KeyType] { @@ -217,28 +217,13 @@ extension SecureEnclave { } -@available(macOS 14, *) extension SecureEnclave.CryptoKitStore { - /// Reloads all secrets from the store. - /// - Parameter notifyAgent: A boolean indicating whether a distributed notification should be posted, notifying other processes (ie, the SecretAgent) to reload their stores as well. - @MainActor private func reloadSecretsInternal(notifyAgent: Bool = true) async { - let before = secrets - secrets.removeAll() - loadSecrets() - if secrets != before { - NotificationCenter.default.post(name: .secretStoreReloaded, object: self) - if notifyAgent { - DistributedNotificationCenter.default().postNotificationName(.secretStoreUpdated, object: nil, deliverImmediately: true) - } - } - } - /// Loads all secrets from the store. @MainActor private func loadSecrets() { let queryAttributes = KeychainDictionary([ kSecClass: Constants.keyClass, - kSecAttrService: Constants.keyTag, + kSecAttrService: SecureEnclave.Constants.keyTag, kSecUseDataProtectionKeychain: true, kSecReturnData: true, kSecMatchLimit: kSecMatchLimitAll, @@ -290,7 +275,7 @@ extension SecureEnclave.CryptoKitStore { let attributes = try JSONEncoder().encode(attributes) let keychainAttributes = KeychainDictionary([ kSecClass: Constants.keyClass, - kSecAttrService: Constants.keyTag, + kSecAttrService: SecureEnclave.Constants.keyTag, kSecUseDataProtectionKeychain: true, kSecAttrAccessible: kSecAttrAccessibleWhenUnlockedThisDeviceOnly, kSecAttrAccount: UUID().uuidString, @@ -306,11 +291,9 @@ extension SecureEnclave.CryptoKitStore { } -@available(macOS 14, *) extension SecureEnclave.CryptoKitStore { enum Constants { - static let keyTag = Data("com.maxgoedjen.secretive.secureenclave.key".utf8) static let keyClass = kSecClassGenericPassword as String } diff --git a/Sources/Packages/Sources/SecureEnclaveSecretKit/SecureEnclaveStores/SecureEnclaveVanillaKeychainStore.swift b/Sources/Packages/Sources/SecureEnclaveSecretKit/SecureEnclaveStores/SecureEnclaveVanillaKeychainStore.swift new file mode 100644 index 0000000..a28723f --- /dev/null +++ b/Sources/Packages/Sources/SecureEnclaveSecretKit/SecureEnclaveStores/SecureEnclaveVanillaKeychainStore.swift @@ -0,0 +1,166 @@ +import Foundation +import Observation +import Security +import CryptoKit +import LocalAuthentication +import SecretKit + +extension SecureEnclave { + + /// An implementation of Store backed by the Secure Enclave. + @Observable public final class VanillaKeychainStore: SecretStoreModifiable { + + @MainActor public var secrets: [Secret] = [] + public var isAvailable: Bool { + CryptoKit.SecureEnclave.isAvailable + } + public let id = UUID() + public let name = String(localized: .secureEnclave) + public var supportedKeyTypes: [KeyType] { + [KeyType(algorithm: .ecdsa, size: 256)] + } + + private let persistentAuthenticationHandler = PersistentAuthenticationHandler() + + /// Initializes a Store. + @MainActor public init() { + loadSecrets() + } + + // MARK: - Public API + + // MARK: SecretStore + + public func sign(data: Data, with secret: Secret, for provenance: SigningRequestProvenance) async throws -> Data { + let context: LAContext + if let existing = await persistentAuthenticationHandler.existingPersistedAuthenticationContext(secret: secret) { + context = existing.context + } else { + let newContext = LAContext() + newContext.localizedCancelTitle = String(localized: .authContextRequestDenyButton) + context = newContext + } + context.localizedReason = String(localized: .authContextRequestSignatureDescription(appName: provenance.origin.displayName, secretName: secret.name)) + let attributes = KeychainDictionary([ + kSecClass: kSecClassKey, + kSecAttrKeyClass: kSecAttrKeyClassPrivate, + kSecAttrApplicationLabel: secret.id as CFData, + kSecAttrKeyType: Constants.keyType, + kSecAttrTokenID: kSecAttrTokenIDSecureEnclave, + kSecAttrApplicationTag: SecureEnclave.Constants.keyTag, + kSecUseAuthenticationContext: context, + kSecReturnRef: true + ]) + var untyped: CFTypeRef? + let status = SecItemCopyMatching(attributes, &untyped) + if status != errSecSuccess { + throw KeychainError(statusCode: status) + } + guard let untypedSafe = untyped else { + throw KeychainError(statusCode: errSecSuccess) + } + let key = untypedSafe as! SecKey + var signError: SecurityError? + + guard let signature = SecKeyCreateSignature(key, .ecdsaSignatureMessageX962SHA256, data as CFData, &signError) else { + throw SigningError(error: signError) + } + return signature as Data + } + + public func existingPersistedAuthenticationContext(secret: Secret) async -> PersistedAuthenticationContext? { + await persistentAuthenticationHandler.existingPersistedAuthenticationContext(secret: secret) + } + + public func persistAuthentication(secret: Secret, forDuration duration: TimeInterval) async throws { + try await persistentAuthenticationHandler.persistAuthentication(secret: secret, forDuration: duration) + } + + @MainActor public func reloadSecrets() { + secrets.removeAll() + loadSecrets() + } + + // MARK: SecretStoreModifiable + + public func create(name: String, attributes: Attributes) async throws { + throw DeprecatedCreationStore() + } + + public func delete(secret: Secret) async throws { + let deleteAttributes = KeychainDictionary([ + kSecClass: kSecClassKey, + kSecAttrApplicationLabel: secret.id as CFData + ]) + let status = SecItemDelete(deleteAttributes) + if status != errSecSuccess { + throw KeychainError(statusCode: status) + } + await reloadSecrets() + } + + public func update(secret: Secret, name: String, attributes: Attributes) async throws { + let updateQuery = KeychainDictionary([ + kSecClass: kSecClassKey, + kSecAttrApplicationLabel: secret.id as CFData + ]) + + let updatedAttributes = KeychainDictionary([ + kSecAttrLabel: name, + ]) + + let status = SecItemUpdate(updateQuery, updatedAttributes) + if status != errSecSuccess { + throw KeychainError(statusCode: status) + } + await reloadSecrets() + } + + + } + +} + +extension SecureEnclave.VanillaKeychainStore { + + /// Loads all secrets from the store. + @MainActor private func loadSecrets() { + let privateAttributes = KeychainDictionary([ + kSecClass: kSecClassKey, + kSecAttrKeyType: Constants.keyType, + kSecAttrApplicationTag: SecureEnclave.Constants.keyTag, + kSecAttrKeyClass: kSecAttrKeyClassPrivate, + kSecReturnRef: true, + kSecMatchLimit: kSecMatchLimitAll, + kSecReturnAttributes: true + ]) + var privateUntyped: CFTypeRef? + SecItemCopyMatching(privateAttributes, &privateUntyped) + guard let privateTyped = privateUntyped as? [[CFString: Any]] else { return } + let wrapped: [SecureEnclave.Secret] = privateTyped.map { + let name = $0[kSecAttrLabel] as? String ?? String(localized: .unnamedSecret) + let id = $0[kSecAttrApplicationLabel] as! Data + let publicKeyRef = $0[kSecValueRef] as! SecKey + let publicKeySecRef = SecKeyCopyPublicKey(publicKeyRef)! + let publicKeyAttributes = SecKeyCopyAttributes(publicKeySecRef) as! [CFString: Any] + let publicKey = publicKeyAttributes[kSecValueData] as! Data + return SecureEnclave.Secret(id: id, name: name, authenticationRequirement: .unknown, publicKey: publicKey) + } + secrets.append(contentsOf: wrapped) + } + +} + +extension SecureEnclave.VanillaKeychainStore { + + public struct DeprecatedCreationStore: Error {} + +} + +extension SecureEnclave.VanillaKeychainStore { + + public enum Constants { + public static let keyType = kSecAttrKeyTypeECSECPrimeRandom as String + } + +} diff --git a/Sources/SecretAgent/AppDelegate.swift b/Sources/SecretAgent/AppDelegate.swift index 86e187d..c714dbf 100644 --- a/Sources/SecretAgent/AppDelegate.swift +++ b/Sources/SecretAgent/AppDelegate.swift @@ -12,7 +12,6 @@ class AppDelegate: NSObject, NSApplicationDelegate { @MainActor private let storeList: SecretStoreList = { let list = SecretStoreList() -// list.add(store: SecureEnclave.CryptoKitStore()) list.add(store: SecureEnclave.Store()) list.add(store: SmartCard.Store()) return list diff --git a/Sources/Secretive/App.swift b/Sources/Secretive/App.swift index a639f39..5ab2a92 100644 --- a/Sources/Secretive/App.swift +++ b/Sources/Secretive/App.swift @@ -10,7 +10,6 @@ extension EnvironmentValues { @MainActor fileprivate static let _secretStoreList: SecretStoreList = { let list = SecretStoreList() list.add(store: SecureEnclave.Store()) - list.add(store: SecureEnclave.CryptoKitStore()) list.add(store: SmartCard.Store()) return list }()