Fixes for insertion handler on smartcard (#622)

This commit is contained in:
Max Goedjen 2025-08-23 20:33:12 -07:00 committed by GitHub
parent e3938caecb
commit 2ba73ff680
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194

View File

@ -1,7 +1,7 @@
import Foundation import Foundation
import Observation import Observation
import Security import Security
import CryptoTokenKit @preconcurrency import CryptoTokenKit
import LocalAuthentication import LocalAuthentication
import SecretKit import SecretKit
@ -23,6 +23,9 @@ extension SmartCard {
public var isAvailable: Bool { public var isAvailable: Bool {
state.isAvailable state.isAvailable
} }
@MainActor public var smartcardTokenID: String? {
state.tokenID
}
public let id = UUID() public let id = UUID()
@MainActor public var name: String { @MainActor public var name: String {
@ -34,17 +37,18 @@ extension SmartCard {
/// Initializes a Store. /// Initializes a Store.
public init() { public init() {
Task { @MainActor in Task {
if let tokenID = state.tokenID { await MainActor.run {
if let tokenID = smartcardTokenID {
state.isAvailable = true state.isAvailable = true
state.watcher.addRemovalHandler(self.smartcardRemoved, forTokenID: tokenID) state.watcher.addRemovalHandler(self.smartcardRemoved, forTokenID: tokenID)
} }
loadSecrets() loadSecrets()
state.watcher.setInsertionHandler { id in }
// Setting insertion handler will cause it to be called immediately. // Doing this inside a regular mainactor handler casues thread assertions in CryptoTokenKit to blow up when the handler executes.
// Make a thread jump so we don't hit a recursive lock attempt. await state.watcher.setInsertionHandler { id in
Task { Task {
self.smartcardInserted(for: id) await self.smartcardInserted(for: id)
} }
} }
} }
@ -126,6 +130,7 @@ extension SmartCard.Store {
state.tokenID = string state.tokenID = string
state.watcher.addRemovalHandler(self.smartcardRemoved, forTokenID: string) state.watcher.addRemovalHandler(self.smartcardRemoved, forTokenID: string)
state.tokenID = string state.tokenID = string
reloadSecretsInternal()
} }
/// Resets the token ID and reloads secrets. /// Resets the token ID and reloads secrets.
@ -172,88 +177,6 @@ extension SmartCard.Store {
} }
// MARK: Smart Card specific encryption/decryption/verification
extension SmartCard.Store {
/// Encrypts a payload with a specified key.
/// - Parameters:
/// - data: The payload to encrypt.
/// - secret: The secret to encrypt with.
/// - Returns: The encrypted data.
/// - Warning: Encryption functions are deliberately only exposed on a library level, and are not exposed in Secretive itself to prevent users from data loss. Any pull requests which expose this functionality in the app will not be merged.
public func encrypt(data: Data, with secret: SecretType) throws -> Data {
let context = LAContext()
context.localizedReason = String(localized: .authContextRequestEncryptDescription(secretName: secret.name))
context.localizedCancelTitle = String(localized: .authContextRequestDenyButton)
let attributes = KeychainDictionary([
kSecAttrKeyType: secret.algorithm.secAttrKeyType,
kSecAttrKeySizeInBits: secret.keySize,
kSecAttrKeyClass: kSecAttrKeyClassPublic,
kSecUseAuthenticationContext: context
])
var encryptError: SecurityError?
let untyped: CFTypeRef? = SecKeyCreateWithData(secret.publicKey as CFData, attributes, &encryptError)
guard let untypedSafe = untyped else {
throw KeychainError(statusCode: errSecSuccess)
}
let key = untypedSafe as! SecKey
guard let signature = SecKeyCreateEncryptedData(key, encryptionAlgorithm(for: secret), data as CFData, &encryptError) else {
throw SigningError(error: encryptError)
}
return signature as Data
}
/// Decrypts a payload with a specified key.
/// - Parameters:
/// - data: The payload to decrypt.
/// - secret: The secret to decrypt with.
/// - Returns: The decrypted data.
/// - Warning: Encryption functions are deliberately only exposed on a library level, and are not exposed in Secretive itself to prevent users from data loss. Any pull requests which expose this functionality in the app will not be merged.
public func decrypt(data: Data, with secret: SecretType) async throws -> Data {
guard let tokenID = await state.tokenID else { fatalError() }
let context = LAContext()
context.localizedReason = String(localized: .authContextRequestDecryptDescription(secretName: secret.name))
context.localizedCancelTitle = String(localized: .authContextRequestDenyButton)
let attributes = KeychainDictionary([
kSecClass: kSecClassKey,
kSecAttrKeyClass: kSecAttrKeyClassPrivate,
kSecAttrApplicationLabel: secret.id as CFData,
kSecAttrTokenID: tokenID,
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 encryptError: SecurityError?
guard let signature = SecKeyCreateDecryptedData(key, encryptionAlgorithm(for: secret), data as CFData, &encryptError) else {
throw SigningError(error: encryptError)
}
return signature as Data
}
private func encryptionAlgorithm(for secret: SecretType) -> SecKeyAlgorithm {
switch (secret.algorithm, secret.keySize) {
case (.ellipticCurve, 256):
return .eciesEncryptionCofactorVariableIVX963SHA256AESGCM
case (.ellipticCurve, 384):
return .eciesEncryptionCofactorVariableIVX963SHA384AESGCM
case (.rsa, 1024), (.rsa, 2048):
return .rsaEncryptionOAEPSHA512AESGCM
default:
fatalError()
}
}
}
extension TKTokenWatcher { extension TKTokenWatcher {
/// All available tokens, excluding the Secure Enclave. /// All available tokens, excluding the Secure Enclave.