mirror of
https://github.com/maxgoedjen/secretive.git
synced 2025-08-30 17:10:56 +00:00
Proxy through
This commit is contained in:
parent
d34d26884e
commit
3a62a855df
@ -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<SecretStoreType>(modifiable secretStore: SecretStoreType) where SecretStoreType: SecretStoreModifiable {
|
||||
public init<SecretStoreType>(_ 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) }
|
||||
|
@ -20,7 +20,7 @@ import Observation
|
||||
|
||||
/// Adds a non-type-erased modifiable SecretStore.
|
||||
public func add<SecretStoreType: SecretStoreModifiable>(store: SecretStoreType) {
|
||||
let modifiable = AnySecretStoreModifiable(modifiable: store)
|
||||
let modifiable = AnySecretStoreModifiable(store)
|
||||
if modifiableStore == nil {
|
||||
modifiableStore = modifiable
|
||||
}
|
||||
|
@ -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<SecretType>: 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<SecretType>: SecretStore {
|
||||
|
||||
/// Creates a new ``Secret`` in the store.
|
||||
/// - Parameters:
|
||||
|
@ -41,6 +41,7 @@ extension SecureEnclave {
|
||||
self.publicKey = publicKey
|
||||
self.attributes = attributes
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
@ -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<SecretType> {
|
||||
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)
|
||||
}
|
||||
|
||||
}
|
||||
|
@ -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
|
||||
}
|
||||
|
@ -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
|
||||
}
|
||||
|
||||
}
|
@ -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
|
||||
|
@ -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
|
||||
}()
|
||||
|
Loading…
Reference in New Issue
Block a user