mirror of
https://github.com/maxgoedjen/secretive.git
synced 2025-08-31 01:20:57 +00:00
Migration.
This commit is contained in:
parent
4f691e4e69
commit
452aee59b1
@ -14,10 +14,9 @@ public struct Attributes: Sendable, Codable, Hashable {
|
|||||||
|
|
||||||
public init(
|
public init(
|
||||||
keyType: KeyType,
|
keyType: KeyType,
|
||||||
authentication: AuthenticationRequirement = .presenceRequired,
|
authentication: AuthenticationRequirement,
|
||||||
publicKeyAttribution: String? = nil
|
publicKeyAttribution: String? = nil
|
||||||
) {
|
) {
|
||||||
// assert(authentication != .unknown, "Secrets cannot be created with an unknown authentication requirement.")
|
|
||||||
self.keyType = keyType
|
self.keyType = keyType
|
||||||
self.authentication = authentication
|
self.authentication = authentication
|
||||||
self.publicKeyAttribution = publicKeyAttribution
|
self.publicKeyAttribution = publicKeyAttribution
|
||||||
|
@ -0,0 +1,96 @@
|
|||||||
|
import Foundation
|
||||||
|
import Security
|
||||||
|
import CryptoTokenKit
|
||||||
|
import CryptoKit
|
||||||
|
import SecretKit
|
||||||
|
import os
|
||||||
|
|
||||||
|
extension SecureEnclave {
|
||||||
|
|
||||||
|
public struct CryptoKitMigrator {
|
||||||
|
|
||||||
|
private let logger = Logger(subsystem: "com.maxgoedjen.secretive.migration", category: "CryptoKitMigrator")
|
||||||
|
|
||||||
|
public init() {
|
||||||
|
}
|
||||||
|
|
||||||
|
@MainActor public func migrate(to store: Store) throws {
|
||||||
|
let privateAttributes = KeychainDictionary([
|
||||||
|
kSecClass: kSecClassKey,
|
||||||
|
kSecAttrKeyType: Constants.oldKeyType,
|
||||||
|
kSecAttrApplicationTag: SecureEnclave.Store.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 migratedPublicKeys = Set(store.secrets.map(\.publicKey))
|
||||||
|
var migrated = false
|
||||||
|
for key in privateTyped {
|
||||||
|
let name = key[kSecAttrLabel] as? String ?? String(localized: .unnamedSecret)
|
||||||
|
let id = key[kSecAttrApplicationLabel] as! Data
|
||||||
|
guard !id.contains(Constants.migrationMagicNumber) else {
|
||||||
|
logger.log("Skipping \(name), already migrated.")
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
let ref = key[kSecValueRef] as! SecKey
|
||||||
|
let attributes = SecKeyCopyAttributes(ref) as! [CFString: Any]
|
||||||
|
let tokenObjectID = attributes[Constants.tokenObjectID] as! Data
|
||||||
|
let accessControl = attributes[kSecAttrAccessControl] as! SecAccessControl
|
||||||
|
// Best guess.
|
||||||
|
let auth: AuthenticationRequirement = String(describing: accessControl)
|
||||||
|
.contains("DeviceOwnerAuthentication") ? .presenceRequired : .unknown
|
||||||
|
let parsed = try CryptoKit.SecureEnclave.P256.Signing.PrivateKey(dataRepresentation: tokenObjectID)
|
||||||
|
let secret = Secret(id: id, name: name, publicKey: parsed.publicKey.x963Representation, attributes: Attributes(keyType: .init(algorithm: .ecdsa, size: 256), authentication: auth))
|
||||||
|
guard !migratedPublicKeys.contains(parsed.publicKey.x963Representation) else {
|
||||||
|
logger.log("Skipping \(name), public key already present. Marking as migrated.")
|
||||||
|
try markMigrated(secret: secret)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
logger.log("Migrating \(name).")
|
||||||
|
try store.saveKey(tokenObjectID, name: name, attributes: secret.attributes)
|
||||||
|
logger.log("Migrated \(name).")
|
||||||
|
try markMigrated(secret: secret)
|
||||||
|
migrated = true
|
||||||
|
}
|
||||||
|
if migrated {
|
||||||
|
store.reloadSecrets()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
public func markMigrated(secret: Secret) throws {
|
||||||
|
let updateQuery = KeychainDictionary([
|
||||||
|
kSecClass: kSecClassKey,
|
||||||
|
kSecAttrApplicationLabel: secret.id as CFData
|
||||||
|
])
|
||||||
|
|
||||||
|
let newID = secret.id + Constants.migrationMagicNumber
|
||||||
|
let updatedAttributes = KeychainDictionary([
|
||||||
|
kSecAttrApplicationLabel: newID as CFData
|
||||||
|
])
|
||||||
|
|
||||||
|
let status = SecItemUpdate(updateQuery, updatedAttributes)
|
||||||
|
if status != errSecSuccess {
|
||||||
|
throw KeychainError(statusCode: status)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
extension SecureEnclave.CryptoKitMigrator {
|
||||||
|
|
||||||
|
enum Constants {
|
||||||
|
public static let oldKeyType = kSecAttrKeyTypeECSECPrimeRandom as String
|
||||||
|
public static let migrationMagicNumber = Data("_cryptokit_1".utf8)
|
||||||
|
public static nonisolated(unsafe) let tokenObjectID = "toid" as CFString
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
@ -14,29 +14,10 @@ extension SecureEnclave {
|
|||||||
init(
|
init(
|
||||||
id: Data,
|
id: Data,
|
||||||
name: String,
|
name: String,
|
||||||
authenticationRequirement: AuthenticationRequirement,
|
|
||||||
publicKey: Data,
|
|
||||||
) {
|
|
||||||
self.id = id
|
|
||||||
self.name = name
|
|
||||||
self.publicKey = publicKey
|
|
||||||
self.attributes = Attributes(
|
|
||||||
keyType: .init(
|
|
||||||
algorithm: .ecdsa,
|
|
||||||
size: 256
|
|
||||||
),
|
|
||||||
authentication: authenticationRequirement,
|
|
||||||
publicKeyAttribution: nil
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
init(
|
|
||||||
id: String,
|
|
||||||
name: String,
|
|
||||||
publicKey: Data,
|
publicKey: Data,
|
||||||
attributes: Attributes
|
attributes: Attributes
|
||||||
) {
|
) {
|
||||||
self.id = Data(id.utf8)
|
self.id = id
|
||||||
self.name = name
|
self.name = name
|
||||||
self.publicKey = publicKey
|
self.publicKey = publicKey
|
||||||
self.attributes = attributes
|
self.attributes = attributes
|
||||||
|
@ -2,82 +2,179 @@ import Foundation
|
|||||||
import Observation
|
import Observation
|
||||||
import Security
|
import Security
|
||||||
import CryptoKit
|
import CryptoKit
|
||||||
import LocalAuthentication
|
@preconcurrency import LocalAuthentication
|
||||||
import SecretKit
|
import SecretKit
|
||||||
|
import os
|
||||||
|
|
||||||
extension SecureEnclave {
|
extension SecureEnclave {
|
||||||
|
|
||||||
/// An implementation of Store backed by the Secure Enclave.
|
/// An implementation of Store backed by the Secure Enclave using CryptoKit API.
|
||||||
/// 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 {
|
@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] = []
|
@MainActor public var secrets: [Secret] = []
|
||||||
public var isAvailable: Bool {
|
public var isAvailable: Bool {
|
||||||
CryptoKit.SecureEnclave.isAvailable
|
CryptoKit.SecureEnclave.isAvailable
|
||||||
}
|
}
|
||||||
public let id = UUID()
|
public let id = UUID()
|
||||||
public let name = String(localized: .secureEnclave)
|
public let name = String(localized: .secureEnclave)
|
||||||
public var supportedKeyTypes: [KeyType] {
|
|
||||||
cryptoKit.supportedKeyTypes
|
|
||||||
}
|
|
||||||
|
|
||||||
private let persistentAuthenticationHandler = PersistentAuthenticationHandler()
|
private let persistentAuthenticationHandler = PersistentAuthenticationHandler()
|
||||||
|
|
||||||
// MARK: SecretStore
|
|
||||||
|
|
||||||
/// Initializes a Store.
|
/// Initializes a Store.
|
||||||
@MainActor public init() {
|
@MainActor public init() {
|
||||||
reloadSecrets()
|
loadSecrets()
|
||||||
Task {
|
Task {
|
||||||
for await _ in DistributedNotificationCenter.default().notifications(named: .secretStoreUpdated) {
|
for await _ in DistributedNotificationCenter.default().notifications(named: .secretStoreUpdated) {
|
||||||
reloadSecretsInternal(notifyAgent: false)
|
reloadSecrets()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// MARK: - Public API
|
||||||
|
|
||||||
|
// MARK: SecretStore
|
||||||
|
|
||||||
|
public func sign(data: Data, with secret: Secret, for provenance: SigningRequestProvenance) async throws -> Data {
|
||||||
|
var context: LAContext
|
||||||
|
if let existing = await persistentAuthenticationHandler.existingPersistedAuthenticationContext(secret: secret) {
|
||||||
|
context = existing.context
|
||||||
|
} else {
|
||||||
|
let newContext = LAContext()
|
||||||
|
newContext.localizedCancelTitle = String(localized: "auth_context_request_deny_button")
|
||||||
|
context = newContext
|
||||||
|
}
|
||||||
|
context.localizedReason = String(localized: "auth_context_request_signature_description_\(provenance.origin.displayName)_\(secret.name)")
|
||||||
|
|
||||||
|
let queryAttributes = KeychainDictionary([
|
||||||
|
kSecClass: Constants.keyClass,
|
||||||
|
kSecAttrService: Constants.keyTag,
|
||||||
|
kSecUseDataProtectionKeychain: true,
|
||||||
|
kSecAttrAccount: String(decoding: secret.id, as: UTF8.self),
|
||||||
|
kSecReturnAttributes: true,
|
||||||
|
kSecReturnData: true
|
||||||
|
])
|
||||||
|
var untyped: CFTypeRef?
|
||||||
|
let status = SecItemCopyMatching(queryAttributes, &untyped)
|
||||||
|
if status != errSecSuccess {
|
||||||
|
throw KeychainError(statusCode: status)
|
||||||
|
}
|
||||||
|
guard let untypedSafe = untyped as? [CFString: Any] else {
|
||||||
|
throw KeychainError(statusCode: errSecSuccess)
|
||||||
|
}
|
||||||
|
guard let attributesData = untypedSafe[kSecAttrGeneric] as? Data,
|
||||||
|
let keyData = untypedSafe[kSecValueData] as? Data else {
|
||||||
|
throw MissingAttributesError()
|
||||||
|
}
|
||||||
|
let attributes = try JSONDecoder().decode(Attributes.self, from: attributesData)
|
||||||
|
|
||||||
|
switch (attributes.keyType.algorithm, attributes.keyType.size) {
|
||||||
|
case (.ecdsa, 256):
|
||||||
|
let key = try CryptoKit.SecureEnclave.P256.Signing.PrivateKey(dataRepresentation: keyData)
|
||||||
|
return try key.signature(for: data).derRepresentation
|
||||||
|
case (.mldsa, 65):
|
||||||
|
guard #available(macOS 26.0, *) else { throw UnsupportedAlgorithmError() }
|
||||||
|
let key = try CryptoKit.SecureEnclave.MLDSA65.PrivateKey(dataRepresentation: keyData)
|
||||||
|
return try key.signature(for: data)
|
||||||
|
case (.mldsa, 87):
|
||||||
|
guard #available(macOS 26.0, *) else { throw UnsupportedAlgorithmError() }
|
||||||
|
let key = try CryptoKit.SecureEnclave.MLDSA87.PrivateKey(dataRepresentation: keyData)
|
||||||
|
return try key.signature(for: data)
|
||||||
|
default:
|
||||||
|
throw UnsupportedAlgorithmError()
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
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() {
|
@MainActor public func reloadSecrets() {
|
||||||
reloadSecretsInternal(notifyAgent: false)
|
reloadSecretsInternal(notifyAgent: false)
|
||||||
}
|
}
|
||||||
|
|
||||||
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 store(for: secret)
|
|
||||||
.persistAuthentication(secret: secret, forDuration: duration)
|
|
||||||
}
|
|
||||||
|
|
||||||
public func reloadSecrets() async {
|
|
||||||
await reloadSecretsInternal()
|
|
||||||
}
|
|
||||||
|
|
||||||
// MARK: SecretStoreModifiable
|
// MARK: SecretStoreModifiable
|
||||||
|
|
||||||
public func create(name: String, attributes: Attributes) async throws {
|
public func create(name: String, attributes: Attributes) async throws {
|
||||||
try await cryptoKit.create(name: name, attributes: attributes)
|
var accessError: SecurityError?
|
||||||
|
let flags: SecAccessControlCreateFlags = switch attributes.authentication {
|
||||||
|
case .notRequired:
|
||||||
|
[]
|
||||||
|
case .presenceRequired:
|
||||||
|
.userPresence
|
||||||
|
case .biometryCurrent:
|
||||||
|
.biometryCurrentSet
|
||||||
|
case .unknown:
|
||||||
|
fatalError()
|
||||||
|
}
|
||||||
|
let access =
|
||||||
|
SecAccessControlCreateWithFlags(kCFAllocatorDefault,
|
||||||
|
kSecAttrAccessibleWhenUnlockedThisDeviceOnly,
|
||||||
|
flags,
|
||||||
|
&accessError)
|
||||||
|
if let error = accessError {
|
||||||
|
throw error.takeRetainedValue() as Error
|
||||||
|
}
|
||||||
|
let dataRep: Data
|
||||||
|
switch (attributes.keyType.algorithm, attributes.keyType.size) {
|
||||||
|
case (.ecdsa, 256):
|
||||||
|
let created = try CryptoKit.SecureEnclave.P256.Signing.PrivateKey(accessControl: access!)
|
||||||
|
dataRep = created.dataRepresentation
|
||||||
|
case (.mldsa, 65):
|
||||||
|
guard #available(macOS 26.0, *) else { throw Attributes.UnsupportedOptionError() }
|
||||||
|
let created = try CryptoKit.SecureEnclave.MLDSA65.PrivateKey(accessControl: access!)
|
||||||
|
dataRep = created.dataRepresentation
|
||||||
|
case (.mldsa, 87):
|
||||||
|
guard #available(macOS 26.0, *) else { throw Attributes.UnsupportedOptionError() }
|
||||||
|
let created = try CryptoKit.SecureEnclave.MLDSA87.PrivateKey(accessControl: access!)
|
||||||
|
dataRep = created.dataRepresentation
|
||||||
|
default:
|
||||||
|
throw Attributes.UnsupportedOptionError()
|
||||||
|
}
|
||||||
|
try saveKey(dataRep, name: name, attributes: attributes)
|
||||||
|
await reloadSecrets()
|
||||||
}
|
}
|
||||||
|
|
||||||
public func delete(secret: Secret) async throws {
|
public func delete(secret: Secret) async throws {
|
||||||
try await store(for: secret)
|
let deleteAttributes = KeychainDictionary([
|
||||||
.delete(secret: secret)
|
kSecClass: Constants.keyClass,
|
||||||
|
kSecAttrService: Constants.keyTag,
|
||||||
|
kSecUseDataProtectionKeychain: true,
|
||||||
|
kSecAttrAccount: String(decoding: secret.id, as: UTF8.self)
|
||||||
|
])
|
||||||
|
let status = SecItemDelete(deleteAttributes)
|
||||||
|
if status != errSecSuccess {
|
||||||
|
throw KeychainError(statusCode: status)
|
||||||
|
}
|
||||||
|
await reloadSecrets()
|
||||||
}
|
}
|
||||||
|
|
||||||
public func update(secret: Secret, name: String, attributes: SecretKit.Attributes) async throws {
|
public func update(secret: Secret, name: String, attributes: Attributes) async throws {
|
||||||
try await store(for: secret)
|
let updateQuery = KeychainDictionary([
|
||||||
.update(secret: secret, name: name, attributes: attributes)
|
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()
|
||||||
|
}
|
||||||
|
|
||||||
|
public var supportedKeyTypes: [KeyType] {
|
||||||
|
[
|
||||||
|
.init(algorithm: .ecdsa, size: 256),
|
||||||
|
.init(algorithm: .mldsa, size: 65),
|
||||||
|
.init(algorithm: .mldsa, size: 87),
|
||||||
|
]
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
@ -86,40 +183,11 @@ extension SecureEnclave {
|
|||||||
|
|
||||||
extension SecureEnclave.Store {
|
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) {
|
@MainActor private func reloadSecretsInternal(notifyAgent: Bool = true) {
|
||||||
let before = secrets
|
let before = secrets
|
||||||
var mapped: [SecretType: Source] = [:]
|
secrets.removeAll()
|
||||||
var new: [SecretType] = []
|
loadSecrets()
|
||||||
cryptoKit.reloadSecrets()
|
if secrets != before {
|
||||||
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)
|
NotificationCenter.default.post(name: .secretStoreReloaded, object: self)
|
||||||
if notifyAgent {
|
if notifyAgent {
|
||||||
DistributedNotificationCenter.default().postNotificationName(.secretStoreUpdated, object: nil, deliverImmediately: true)
|
DistributedNotificationCenter.default().postNotificationName(.secretStoreUpdated, object: nil, deliverImmediately: true)
|
||||||
@ -127,13 +195,87 @@ extension SecureEnclave.Store {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Loads all secrets from the store.
|
||||||
}
|
@MainActor private func loadSecrets() {
|
||||||
|
let queryAttributes = KeychainDictionary([
|
||||||
extension SecureEnclave {
|
kSecClass: Constants.keyClass,
|
||||||
|
kSecAttrService: Constants.keyTag,
|
||||||
enum Constants {
|
kSecUseDataProtectionKeychain: true,
|
||||||
static let keyTag = Data("com.maxgoedjen.secretive.secureenclave.key".utf8)
|
kSecReturnData: true,
|
||||||
|
kSecMatchLimit: kSecMatchLimitAll,
|
||||||
|
kSecReturnAttributes: true
|
||||||
|
])
|
||||||
|
var untyped: CFTypeRef?
|
||||||
|
SecItemCopyMatching(queryAttributes, &untyped)
|
||||||
|
guard let typed = untyped as? [[CFString: Any]] else { return }
|
||||||
|
let wrapped: [SecureEnclave.Secret] = typed.compactMap {
|
||||||
|
do {
|
||||||
|
let name = $0[kSecAttrLabel] as? String ?? String(localized: "unnamed_secret")
|
||||||
|
guard let attributesData = $0[kSecAttrGeneric] as? Data,
|
||||||
|
let idString = $0[kSecAttrAccount] as? String else {
|
||||||
|
throw MissingAttributesError()
|
||||||
|
}
|
||||||
|
let id = Data(idString.utf8)
|
||||||
|
let attributes = try JSONDecoder().decode(Attributes.self, from: attributesData)
|
||||||
|
let keyData = $0[kSecValueData] as! Data
|
||||||
|
let publicKey: Data
|
||||||
|
switch (attributes.keyType.algorithm, attributes.keyType.size) {
|
||||||
|
case (.ecdsa, 256):
|
||||||
|
let key = try CryptoKit.SecureEnclave.P256.Signing.PrivateKey(dataRepresentation: keyData)
|
||||||
|
publicKey = key.publicKey.x963Representation
|
||||||
|
case (.mldsa, 65):
|
||||||
|
guard #available(macOS 26.0, *) else { throw UnsupportedAlgorithmError() }
|
||||||
|
let key = try CryptoKit.SecureEnclave.MLDSA65.PrivateKey(dataRepresentation: keyData)
|
||||||
|
publicKey = key.publicKey.rawRepresentation
|
||||||
|
case (.mldsa, 87):
|
||||||
|
guard #available(macOS 26.0, *) else { throw UnsupportedAlgorithmError() }
|
||||||
|
let key = try CryptoKit.SecureEnclave.MLDSA87.PrivateKey(dataRepresentation: keyData)
|
||||||
|
publicKey = key.publicKey.rawRepresentation
|
||||||
|
default:
|
||||||
|
throw UnsupportedAlgorithmError()
|
||||||
|
}
|
||||||
|
return SecureEnclave.Secret(id: id, name: name, publicKey: publicKey, attributes: attributes)
|
||||||
|
} catch {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
secrets.append(contentsOf: wrapped)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Saves a public key.
|
||||||
|
/// - Parameters:
|
||||||
|
/// - key: The data representation key to save.
|
||||||
|
/// - name: A user-facing name for the key.
|
||||||
|
/// - attributes: Attributes of the key.
|
||||||
|
/// - Note: Despite the name, the "Data" of the key is _not_ actual key material. This is an opaque data representation that the SEP can manipulate.
|
||||||
|
func saveKey(_ key: Data, name: String, attributes: Attributes) throws {
|
||||||
|
let attributes = try JSONEncoder().encode(attributes)
|
||||||
|
let keychainAttributes = KeychainDictionary([
|
||||||
|
kSecClass: Constants.keyClass,
|
||||||
|
kSecAttrService: Constants.keyTag,
|
||||||
|
kSecUseDataProtectionKeychain: true,
|
||||||
|
kSecAttrAccessible: kSecAttrAccessibleWhenUnlockedThisDeviceOnly,
|
||||||
|
kSecAttrAccount: UUID().uuidString,
|
||||||
|
kSecValueData: key,
|
||||||
|
kSecAttrLabel: name,
|
||||||
|
kSecAttrGeneric: attributes
|
||||||
|
])
|
||||||
|
let status = SecItemAdd(keychainAttributes, nil)
|
||||||
|
if status != errSecSuccess {
|
||||||
|
throw KeychainError(statusCode: status)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
extension SecureEnclave.Store {
|
||||||
|
|
||||||
|
enum Constants {
|
||||||
|
static let keyClass = kSecClassGenericPassword as String
|
||||||
|
static let keyTag = Data("com.maxgoedjen.secretive.secureenclave.key".utf8)
|
||||||
|
}
|
||||||
|
|
||||||
|
struct UnsupportedAlgorithmError: Error {}
|
||||||
|
struct MissingAttributesError: Error {}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
@ -1,274 +0,0 @@
|
|||||||
import Foundation
|
|
||||||
import Observation
|
|
||||||
import Security
|
|
||||||
import CryptoKit
|
|
||||||
@preconcurrency import LocalAuthentication
|
|
||||||
import SecretKit
|
|
||||||
import os
|
|
||||||
|
|
||||||
extension SecureEnclave {
|
|
||||||
|
|
||||||
/// An implementation of Store backed by the Secure Enclave using CryptoKit API.
|
|
||||||
@Observable final class CryptoKitStore: SecretStoreModifiable {
|
|
||||||
|
|
||||||
@MainActor var secrets: [Secret] = []
|
|
||||||
var isAvailable: Bool {
|
|
||||||
CryptoKit.SecureEnclave.isAvailable
|
|
||||||
}
|
|
||||||
let id = UUID()
|
|
||||||
let name = String(localized: .secureEnclave)
|
|
||||||
private let persistentAuthenticationHandler = PersistentAuthenticationHandler()
|
|
||||||
|
|
||||||
/// Initializes a Store.
|
|
||||||
@MainActor init() {
|
|
||||||
loadSecrets()
|
|
||||||
Task {
|
|
||||||
for await _ in DistributedNotificationCenter.default().notifications(named: .secretStoreUpdated) {
|
|
||||||
reloadSecrets()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// MARK: - Public API
|
|
||||||
|
|
||||||
// MARK: SecretStore
|
|
||||||
|
|
||||||
func sign(data: Data, with secret: Secret, for provenance: SigningRequestProvenance) async throws -> Data {
|
|
||||||
var context: LAContext
|
|
||||||
if let existing = await persistentAuthenticationHandler.existingPersistedAuthenticationContext(secret: secret) {
|
|
||||||
context = existing.context
|
|
||||||
} else {
|
|
||||||
let newContext = LAContext()
|
|
||||||
newContext.localizedCancelTitle = String(localized: "auth_context_request_deny_button")
|
|
||||||
context = newContext
|
|
||||||
}
|
|
||||||
context.localizedReason = String(localized: "auth_context_request_signature_description_\(provenance.origin.displayName)_\(secret.name)")
|
|
||||||
|
|
||||||
let queryAttributes = KeychainDictionary([
|
|
||||||
kSecClass: Constants.keyClass,
|
|
||||||
kSecAttrService: SecureEnclave.Constants.keyTag,
|
|
||||||
kSecUseDataProtectionKeychain: true,
|
|
||||||
kSecAttrAccount: String(decoding: secret.id, as: UTF8.self),
|
|
||||||
kSecReturnAttributes: true,
|
|
||||||
kSecReturnData: true
|
|
||||||
])
|
|
||||||
var untyped: CFTypeRef?
|
|
||||||
let status = SecItemCopyMatching(queryAttributes, &untyped)
|
|
||||||
if status != errSecSuccess {
|
|
||||||
throw KeychainError(statusCode: status)
|
|
||||||
}
|
|
||||||
guard let untypedSafe = untyped as? [CFString: Any] else {
|
|
||||||
throw KeychainError(statusCode: errSecSuccess)
|
|
||||||
}
|
|
||||||
guard let attributesData = untypedSafe[kSecAttrGeneric] as? Data,
|
|
||||||
let keyData = untypedSafe[kSecValueData] as? Data else {
|
|
||||||
throw MissingAttributesError()
|
|
||||||
}
|
|
||||||
let attributes = try JSONDecoder().decode(Attributes.self, from: attributesData)
|
|
||||||
|
|
||||||
switch (attributes.keyType.algorithm, attributes.keyType.size) {
|
|
||||||
case (.ecdsa, 256):
|
|
||||||
let key = try CryptoKit.SecureEnclave.P256.Signing.PrivateKey(dataRepresentation: keyData)
|
|
||||||
return try key.signature(for: data).rawRepresentation
|
|
||||||
case (.mldsa, 65):
|
|
||||||
guard #available(macOS 26.0, *) else { throw UnsupportedAlgorithmError() }
|
|
||||||
let key = try CryptoKit.SecureEnclave.MLDSA65.PrivateKey(dataRepresentation: keyData)
|
|
||||||
return try key.signature(for: data)
|
|
||||||
case (.mldsa, 87):
|
|
||||||
guard #available(macOS 26.0, *) else { throw UnsupportedAlgorithmError() }
|
|
||||||
let key = try CryptoKit.SecureEnclave.MLDSA87.PrivateKey(dataRepresentation: keyData)
|
|
||||||
return try key.signature(for: data)
|
|
||||||
default:
|
|
||||||
throw UnsupportedAlgorithmError()
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
func existingPersistedAuthenticationContext(secret: Secret) async -> PersistedAuthenticationContext? {
|
|
||||||
await persistentAuthenticationHandler.existingPersistedAuthenticationContext(secret: secret)
|
|
||||||
}
|
|
||||||
|
|
||||||
func persistAuthentication(secret: Secret, forDuration duration: TimeInterval) async throws {
|
|
||||||
try await persistentAuthenticationHandler.persistAuthentication(secret: secret, forDuration: duration)
|
|
||||||
}
|
|
||||||
|
|
||||||
@MainActor func reloadSecrets() {
|
|
||||||
secrets.removeAll()
|
|
||||||
loadSecrets()
|
|
||||||
}
|
|
||||||
|
|
||||||
// MARK: SecretStoreModifiable
|
|
||||||
|
|
||||||
func create(name: String, attributes: Attributes) async throws {
|
|
||||||
var accessError: SecurityError?
|
|
||||||
let flags: SecAccessControlCreateFlags = switch attributes.authentication {
|
|
||||||
case .notRequired:
|
|
||||||
[]
|
|
||||||
case .presenceRequired:
|
|
||||||
.userPresence
|
|
||||||
case .biometryCurrent:
|
|
||||||
.biometryCurrentSet
|
|
||||||
case .unknown:
|
|
||||||
fatalError()
|
|
||||||
}
|
|
||||||
let access =
|
|
||||||
SecAccessControlCreateWithFlags(kCFAllocatorDefault,
|
|
||||||
kSecAttrAccessibleWhenUnlockedThisDeviceOnly,
|
|
||||||
flags,
|
|
||||||
&accessError)
|
|
||||||
if let error = accessError {
|
|
||||||
throw error.takeRetainedValue() as Error
|
|
||||||
}
|
|
||||||
let dataRep: Data
|
|
||||||
switch (attributes.keyType.algorithm, attributes.keyType.size) {
|
|
||||||
case (.ecdsa, 256):
|
|
||||||
let created = try CryptoKit.SecureEnclave.P256.Signing.PrivateKey(accessControl: access!)
|
|
||||||
dataRep = created.dataRepresentation
|
|
||||||
case (.mldsa, 65):
|
|
||||||
guard #available(macOS 26.0, *) else { throw Attributes.UnsupportedOptionError() }
|
|
||||||
let created = try CryptoKit.SecureEnclave.MLDSA65.PrivateKey(accessControl: access!)
|
|
||||||
dataRep = created.dataRepresentation
|
|
||||||
case (.mldsa, 87):
|
|
||||||
guard #available(macOS 26.0, *) else { throw Attributes.UnsupportedOptionError() }
|
|
||||||
let created = try CryptoKit.SecureEnclave.MLDSA87.PrivateKey(accessControl: access!)
|
|
||||||
dataRep = created.dataRepresentation
|
|
||||||
default:
|
|
||||||
throw Attributes.UnsupportedOptionError()
|
|
||||||
}
|
|
||||||
try saveKey(dataRep, name: name, attributes: attributes)
|
|
||||||
await reloadSecrets()
|
|
||||||
}
|
|
||||||
|
|
||||||
func delete(secret: Secret) async throws {
|
|
||||||
let deleteAttributes = KeychainDictionary([
|
|
||||||
kSecClass: Constants.keyClass,
|
|
||||||
kSecAttrService: SecureEnclave.Constants.keyTag,
|
|
||||||
kSecUseDataProtectionKeychain: true,
|
|
||||||
kSecAttrAccount: String(decoding: secret.id, as: UTF8.self)
|
|
||||||
])
|
|
||||||
let status = SecItemDelete(deleteAttributes)
|
|
||||||
if status != errSecSuccess {
|
|
||||||
throw KeychainError(statusCode: status)
|
|
||||||
}
|
|
||||||
await reloadSecrets()
|
|
||||||
}
|
|
||||||
|
|
||||||
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()
|
|
||||||
}
|
|
||||||
|
|
||||||
var supportedKeyTypes: [KeyType] {
|
|
||||||
[
|
|
||||||
.init(algorithm: .ecdsa, size: 256),
|
|
||||||
.init(algorithm: .mldsa, size: 65),
|
|
||||||
.init(algorithm: .mldsa, size: 87),
|
|
||||||
]
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
extension SecureEnclave.CryptoKitStore {
|
|
||||||
|
|
||||||
/// Loads all secrets from the store.
|
|
||||||
@MainActor private func loadSecrets() {
|
|
||||||
let queryAttributes = KeychainDictionary([
|
|
||||||
kSecClass: Constants.keyClass,
|
|
||||||
kSecAttrService: SecureEnclave.Constants.keyTag,
|
|
||||||
kSecUseDataProtectionKeychain: true,
|
|
||||||
kSecReturnData: true,
|
|
||||||
kSecMatchLimit: kSecMatchLimitAll,
|
|
||||||
kSecReturnAttributes: true
|
|
||||||
])
|
|
||||||
var untyped: CFTypeRef?
|
|
||||||
SecItemCopyMatching(queryAttributes, &untyped)
|
|
||||||
guard let typed = untyped as? [[CFString: Any]] else { return }
|
|
||||||
let wrapped: [SecureEnclave.Secret] = typed.compactMap {
|
|
||||||
do {
|
|
||||||
let name = $0[kSecAttrLabel] as? String ?? String(localized: "unnamed_secret")
|
|
||||||
guard let attributesData = $0[kSecAttrGeneric] as? Data,
|
|
||||||
let id = $0[kSecAttrAccount] as? String else {
|
|
||||||
throw MissingAttributesError()
|
|
||||||
}
|
|
||||||
let attributes = try JSONDecoder().decode(Attributes.self, from: attributesData)
|
|
||||||
let keyData = $0[kSecValueData] as! Data
|
|
||||||
let publicKey: Data
|
|
||||||
switch (attributes.keyType.algorithm, attributes.keyType.size) {
|
|
||||||
case (.ecdsa, 256):
|
|
||||||
let key = try CryptoKit.SecureEnclave.P256.Signing.PrivateKey(dataRepresentation: keyData)
|
|
||||||
publicKey = key.publicKey.x963Representation
|
|
||||||
case (.mldsa, 65):
|
|
||||||
guard #available(macOS 26.0, *) else { throw UnsupportedAlgorithmError() }
|
|
||||||
let key = try CryptoKit.SecureEnclave.MLDSA65.PrivateKey(dataRepresentation: keyData)
|
|
||||||
publicKey = key.publicKey.rawRepresentation
|
|
||||||
case (.mldsa, 87):
|
|
||||||
guard #available(macOS 26.0, *) else { throw UnsupportedAlgorithmError() }
|
|
||||||
let key = try CryptoKit.SecureEnclave.MLDSA87.PrivateKey(dataRepresentation: keyData)
|
|
||||||
publicKey = key.publicKey.rawRepresentation
|
|
||||||
default:
|
|
||||||
throw UnsupportedAlgorithmError()
|
|
||||||
}
|
|
||||||
return SecureEnclave.Secret(id: id, name: name, publicKey: publicKey, attributes: attributes)
|
|
||||||
} catch {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
}
|
|
||||||
secrets.append(contentsOf: wrapped)
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Saves a public key.
|
|
||||||
/// - Parameters:
|
|
||||||
/// - key: The data representation key to save.
|
|
||||||
/// - name: A user-facing name for the key.
|
|
||||||
/// - attributes: Attributes of the key.
|
|
||||||
/// - Note: Despite the name, the "Data" of the key is _not_ actual key material. This is an opaque data representation that the SEP can manipulate.
|
|
||||||
private func saveKey(_ key: Data, name: String, attributes: Attributes) throws {
|
|
||||||
let attributes = try JSONEncoder().encode(attributes)
|
|
||||||
let keychainAttributes = KeychainDictionary([
|
|
||||||
kSecClass: Constants.keyClass,
|
|
||||||
kSecAttrService: SecureEnclave.Constants.keyTag,
|
|
||||||
kSecUseDataProtectionKeychain: true,
|
|
||||||
kSecAttrAccessible: kSecAttrAccessibleWhenUnlockedThisDeviceOnly,
|
|
||||||
kSecAttrAccount: UUID().uuidString,
|
|
||||||
kSecValueData: key,
|
|
||||||
kSecAttrLabel: name,
|
|
||||||
kSecAttrGeneric: attributes
|
|
||||||
])
|
|
||||||
let status = SecItemAdd(keychainAttributes, nil)
|
|
||||||
if status != errSecSuccess {
|
|
||||||
throw KeychainError(statusCode: status)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
extension SecureEnclave.CryptoKitStore {
|
|
||||||
|
|
||||||
enum Constants {
|
|
||||||
static let keyClass = kSecClassGenericPassword as String
|
|
||||||
}
|
|
||||||
|
|
||||||
fileprivate protocol CryptoKitKey: Sendable {
|
|
||||||
init(dataRepresentation: Data, authenticationContext: LAContext?) throws
|
|
||||||
var dataRepresentation: Data { get }
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
struct UnsupportedAlgorithmError: Error {}
|
|
||||||
struct MissingAttributesError: Error {}
|
|
||||||
|
|
||||||
}
|
|
@ -1,166 +0,0 @@
|
|||||||
import Foundation
|
|
||||||
import Observation
|
|
||||||
import Security
|
|
||||||
import CryptoKit
|
|
||||||
import LocalAuthentication
|
|
||||||
import SecretKit
|
|
||||||
|
|
||||||
extension SecureEnclave {
|
|
||||||
|
|
||||||
/// An implementation of Store backed by the Secure Enclave.
|
|
||||||
@Observable final class VanillaKeychainStore: SecretStoreModifiable {
|
|
||||||
|
|
||||||
@MainActor var secrets: [Secret] = []
|
|
||||||
var isAvailable: Bool {
|
|
||||||
CryptoKit.SecureEnclave.isAvailable
|
|
||||||
}
|
|
||||||
let id = UUID()
|
|
||||||
let name = String(localized: .secureEnclave)
|
|
||||||
var supportedKeyTypes: [KeyType] {
|
|
||||||
[KeyType(algorithm: .ecdsa, size: 256)]
|
|
||||||
}
|
|
||||||
|
|
||||||
private let persistentAuthenticationHandler = PersistentAuthenticationHandler()
|
|
||||||
|
|
||||||
/// Initializes a Store.
|
|
||||||
@MainActor init() {
|
|
||||||
loadSecrets()
|
|
||||||
}
|
|
||||||
|
|
||||||
// MARK: - Public API
|
|
||||||
|
|
||||||
// MARK: SecretStore
|
|
||||||
|
|
||||||
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
|
|
||||||
}
|
|
||||||
|
|
||||||
func existingPersistedAuthenticationContext(secret: Secret) async -> PersistedAuthenticationContext? {
|
|
||||||
await persistentAuthenticationHandler.existingPersistedAuthenticationContext(secret: secret)
|
|
||||||
}
|
|
||||||
|
|
||||||
func persistAuthentication(secret: Secret, forDuration duration: TimeInterval) async throws {
|
|
||||||
try await persistentAuthenticationHandler.persistAuthentication(secret: secret, forDuration: duration)
|
|
||||||
}
|
|
||||||
|
|
||||||
@MainActor func reloadSecrets() {
|
|
||||||
secrets.removeAll()
|
|
||||||
loadSecrets()
|
|
||||||
}
|
|
||||||
|
|
||||||
// MARK: SecretStoreModifiable
|
|
||||||
|
|
||||||
func create(name: String, attributes: Attributes) async throws {
|
|
||||||
throw DeprecatedCreationStore()
|
|
||||||
}
|
|
||||||
|
|
||||||
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()
|
|
||||||
}
|
|
||||||
|
|
||||||
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,10 @@ class AppDelegate: NSObject, NSApplicationDelegate {
|
|||||||
|
|
||||||
@MainActor private let storeList: SecretStoreList = {
|
@MainActor private let storeList: SecretStoreList = {
|
||||||
let list = SecretStoreList()
|
let list = SecretStoreList()
|
||||||
list.add(store: SecureEnclave.Store())
|
let cryptoKit = SecureEnclave.Store()
|
||||||
|
let migrator = SecureEnclave.CryptoKitMigrator()
|
||||||
|
try? migrator.migrate(to: cryptoKit)
|
||||||
|
list.add(store: cryptoKit)
|
||||||
list.add(store: SmartCard.Store())
|
list.add(store: SmartCard.Store())
|
||||||
return list
|
return list
|
||||||
}()
|
}()
|
||||||
|
@ -9,7 +9,10 @@ extension EnvironmentValues {
|
|||||||
// This is injected through .environment modifier below instead of @Entry for performance reasons (basially, restrictions around init/mainactor causing delay in loading secrets/"empty screen" blip).
|
// This is injected through .environment modifier below instead of @Entry for performance reasons (basially, restrictions around init/mainactor causing delay in loading secrets/"empty screen" blip).
|
||||||
@MainActor fileprivate static let _secretStoreList: SecretStoreList = {
|
@MainActor fileprivate static let _secretStoreList: SecretStoreList = {
|
||||||
let list = SecretStoreList()
|
let list = SecretStoreList()
|
||||||
list.add(store: SecureEnclave.Store())
|
let cryptoKit = SecureEnclave.Store()
|
||||||
|
let migrator = SecureEnclave.CryptoKitMigrator()
|
||||||
|
try? migrator.migrate(to: cryptoKit)
|
||||||
|
list.add(store: cryptoKit)
|
||||||
list.add(store: SmartCard.Store())
|
list.add(store: SmartCard.Store())
|
||||||
return list
|
return list
|
||||||
}()
|
}()
|
||||||
|
Loading…
Reference in New Issue
Block a user