Fix concurrency issues in SmartCardStore

This commit is contained in:
Max Goedjen 2025-01-04 01:06:54 -08:00
parent 970e407e29
commit c2563be404
No known key found for this signature in database

View File

@ -8,55 +8,44 @@ import SecretKit
extension SmartCard { extension SmartCard {
private struct State {
var isAvailable = false
var name = String(localized: "smart_card")
var secrets: [Secret] = []
let watcher = TKTokenWatcher()
var tokenID: String? = nil
}
/// An implementation of Store backed by a Smart Card. /// An implementation of Store backed by a Smart Card.
@Observable public final class Store: SecretStore { @Observable public final class Store: SecretStore {
private let state: Mutex<State> = .init(.init())
public var isAvailable: Bool { public var isAvailable: Bool {
_isAvailable.withLock { $0 } state.withLock { $0.isAvailable }
} }
private let _isAvailable: Mutex<Bool> = .init(false)
public let id = UUID() public let id = UUID()
public var name: String { public var name: String {
_name.withLock { $0 } state.withLock { $0.name }
} }
private let _name: Mutex<String> = .init(String(localized: "smart_card"))
public var secrets: [Secret] { public var secrets: [Secret] {
_secrets.withLock { $0 } state.withLock { $0.secrets }
} }
private let _secrets: Mutex<[Secret]> = .init([])
private let watcher: Mutex<TKTokenWatcher> = .init(TKTokenWatcher())
private let tokenID: Mutex<String?> = .init(nil)
/// Initializes a Store. /// Initializes a Store.
public init() { public init() {
tokenID.withLock { tokenID in state.withLock { state in
watcher.withLock { watcher in if let tokenID = state.tokenID {
let id = watcher.nonSecureEnclaveTokens.first state.isAvailable = true
watcher.setInsertionHandler { string in state.watcher.addRemovalHandler(self.smartcardRemoved, forTokenID: tokenID)
// guard self.tokenID == nil else { return }
// guard !string.contains("setoken") else { return }
//
//// self.tokenID.withLock {
//// $0 = string
//// }
// // DispatchQueue.main.async {
// // reload()
// // }
// watcher.addRemovalHandler(self.smartcardRemoved, forTokenID: string)
} }
tokenID = id state.watcher.setInsertionHandler { id in
// Setting insertion handler will cause it to be called immediately.
// Make a thread jump so we don't hit a recursive lock attempt.
Task {
self.smartcardInserted(for: id)
} }
} }
// FIXME: THIS
if let tokenID = tokenID.withLock({ $0 }) {
_isAvailable.withLock {
$0 = true
}
watcher.withLock {
$0.addRemovalHandler(self.smartcardRemoved, forTokenID: tokenID)
}
} }
loadSecrets() loadSecrets()
} }
@ -72,7 +61,7 @@ extension SmartCard {
} }
public func sign(data: Data, with secret: Secret, for provenance: SigningRequestProvenance) throws -> Data { public func sign(data: Data, with secret: Secret, for provenance: SigningRequestProvenance) throws -> Data {
guard let tokenID = tokenID.withLock({ $0 }) else { fatalError() } guard let tokenID = state.withLock({ $0.tokenID }) else { fatalError() }
let context = LAContext() let context = LAContext()
context.localizedReason = String(localized: "auth_context_request_signature_description_\(provenance.origin.displayName)_\(secret.name)") context.localizedReason = String(localized: "auth_context_request_signature_description_\(provenance.origin.displayName)_\(secret.name)")
context.localizedCancelTitle = String(localized: "auth_context_request_deny_button") context.localizedCancelTitle = String(localized: "auth_context_request_deny_button")
@ -142,12 +131,11 @@ extension SmartCard {
extension SmartCard.Store { extension SmartCard.Store {
private func reloadSecretsInternal() { private func reloadSecretsInternal() {
_isAvailable.withLock { let before = state.withLock {
$0 = tokenID.withLock({ $0 }) != nil $0.isAvailable = $0.tokenID != nil
} let before = $0.secrets
let before = self.secrets $0.secrets.removeAll()
self._secrets.withLock { return before
$0.removeAll()
} }
self.loadSecrets() self.loadSecrets()
if self.secrets != before { if self.secrets != before {
@ -155,25 +143,38 @@ extension SmartCard.Store {
} }
} }
/// Resets the token ID and reloads secrets.
/// - Parameter tokenID: The ID of the token that was inserted.
private func smartcardInserted(for tokenID: String? = nil) {
state.withLock { state in
guard let string = state.watcher.nonSecureEnclaveTokens.first else { return }
guard state.tokenID == nil else { return }
guard !string.contains("setoken") else { return }
state.tokenID = string
state.watcher.addRemovalHandler(self.smartcardRemoved, forTokenID: string)
state.tokenID = string
}
}
/// Resets the token ID and reloads secrets. /// Resets the token ID and reloads secrets.
/// - Parameter tokenID: The ID of the token that was removed. /// - Parameter tokenID: The ID of the token that was removed.
private func smartcardRemoved(for tokenID: String? = nil) { private func smartcardRemoved(for tokenID: String? = nil) {
self.tokenID.withLock { state.withLock {
$0 = nil $0.tokenID = nil
} }
reloadSecrets() reloadSecrets()
} }
/// Loads all secrets from the store. /// Loads all secrets from the store.
private func loadSecrets() { private func loadSecrets() {
guard let tokenID = tokenID.withLock({ $0 }) else { return } guard let tokenID = state.withLock({ $0.tokenID }) else { return }
let fallbackName = String(localized: "smart_card") let fallbackName = String(localized: "smart_card")
_name.withLock { state.withLock {
if let driverName = watcher.withLock({ $0.tokenInfo(forTokenID: tokenID)?.driverName }) { if let driverName = $0.watcher.tokenInfo(forTokenID: tokenID)?.driverName {
$0 = driverName $0.name = driverName
} else { } else {
$0 = fallbackName $0.name = fallbackName
} }
} }
@ -198,8 +199,8 @@ extension SmartCard.Store {
let publicKey = publicKeyAttributes[kSecValueData] as! Data let publicKey = publicKeyAttributes[kSecValueData] as! Data
return SmartCard.Secret(id: tokenID, name: name, algorithm: algorithm, keySize: keySize, publicKey: publicKey) return SmartCard.Secret(id: tokenID, name: name, algorithm: algorithm, keySize: keySize, publicKey: publicKey)
} }
_secrets.withLock { state.withLock {
$0.append(contentsOf: wrapped) $0.secrets.append(contentsOf: wrapped)
} }
} }
@ -244,7 +245,7 @@ extension SmartCard.Store {
/// - Returns: The decrypted data. /// - 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. /// - 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) throws -> Data { public func decrypt(data: Data, with secret: SecretType) throws -> Data {
guard let tokenID = tokenID.withLock({ $0 }) else { fatalError() } guard let tokenID = state.withLock({ $0.tokenID }) else { fatalError() }
let context = LAContext() let context = LAContext()
context.localizedReason = String(localized: "auth_context_request_decrypt_description_\(secret.name)") context.localizedReason = String(localized: "auth_context_request_decrypt_description_\(secret.name)")
context.localizedCancelTitle = String(localized: "auth_context_request_deny_button") context.localizedCancelTitle = String(localized: "auth_context_request_deny_button")