From 2ba73ff680d763fdc4e7efe49ddb9c16011f5adc Mon Sep 17 00:00:00 2001 From: Max Goedjen Date: Sat, 23 Aug 2025 20:33:12 -0700 Subject: [PATCH 1/2] Fixes for insertion handler on smartcard (#622) --- .../SmartCardSecretKit/SmartCardStore.swift | 119 ++++-------------- 1 file changed, 21 insertions(+), 98 deletions(-) diff --git a/Sources/Packages/Sources/SmartCardSecretKit/SmartCardStore.swift b/Sources/Packages/Sources/SmartCardSecretKit/SmartCardStore.swift index 2b96dbd..d52c1c8 100644 --- a/Sources/Packages/Sources/SmartCardSecretKit/SmartCardStore.swift +++ b/Sources/Packages/Sources/SmartCardSecretKit/SmartCardStore.swift @@ -1,7 +1,7 @@ import Foundation import Observation import Security -import CryptoTokenKit +@preconcurrency import CryptoTokenKit import LocalAuthentication import SecretKit @@ -23,6 +23,9 @@ extension SmartCard { public var isAvailable: Bool { state.isAvailable } + @MainActor public var smartcardTokenID: String? { + state.tokenID + } public let id = UUID() @MainActor public var name: String { @@ -34,17 +37,18 @@ extension SmartCard { /// Initializes a Store. public init() { - Task { @MainActor in - if let tokenID = state.tokenID { - state.isAvailable = true - state.watcher.addRemovalHandler(self.smartcardRemoved, forTokenID: tokenID) + Task { + await MainActor.run { + if let tokenID = smartcardTokenID { + state.isAvailable = true + state.watcher.addRemovalHandler(self.smartcardRemoved, forTokenID: tokenID) + } + loadSecrets() } - loadSecrets() - 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. + // Doing this inside a regular mainactor handler casues thread assertions in CryptoTokenKit to blow up when the handler executes. + await state.watcher.setInsertionHandler { id in Task { - self.smartcardInserted(for: id) + await self.smartcardInserted(for: id) } } } @@ -120,12 +124,13 @@ extension SmartCard.Store { /// Resets the token ID and reloads secrets. /// - Parameter tokenID: The ID of the token that was inserted. @MainActor private func smartcardInserted(for tokenID: String? = nil) { - 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 + 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 + reloadSecretsInternal() } /// 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 { /// All available tokens, excluding the Secure Enclave. From f259cf6bf3ac22fbf4fb2f05c4b52ca61d18a8bb Mon Sep 17 00:00:00 2001 From: Max Goedjen Date: Sat, 23 Aug 2025 20:35:36 -0700 Subject: [PATCH 2/2] Add slimmed Package.swift to root (#623) --- Package.swift | 69 +++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 69 insertions(+) create mode 100644 Package.swift diff --git a/Package.swift b/Package.swift new file mode 100644 index 0000000..700486e --- /dev/null +++ b/Package.swift @@ -0,0 +1,69 @@ +// swift-tools-version:6.2 +// The swift-tools-version declares the minimum version of Swift required to build this package. + +import PackageDescription + +// This is basically the same package as `Sources/Packages/Package.swift`, but thinned slightly. +// Ideally this would be the same package, but SPM requires it to be at the root of the project, +// and Xcode does _not_ like that, so they're separate. +let package = Package( + name: "SecretKit", + defaultLocalization: "en", + platforms: [ + .macOS(.v14) + ], + products: [ + .library( + name: "SecretKit", + targets: ["SecretKit"]), + .library( + name: "SecureEnclaveSecretKit", + targets: ["SecureEnclaveSecretKit"]), + .library( + name: "SmartCardSecretKit", + targets: ["SmartCardSecretKit"]), + ], + dependencies: [ + ], + targets: [ + .target( + name: "SecretKit", + dependencies: [], + path: "Sources/Packages/Sources/SecretKit", + resources: [localization], + swiftSettings: swiftSettings + ), + .testTarget( + name: "SecretKitTests", + dependencies: ["SecretKit", "SecureEnclaveSecretKit", "SmartCardSecretKit"], + path: "Sources/Packages/Tests/SecretKitTests", + swiftSettings: swiftSettings + ), + .target( + name: "SecureEnclaveSecretKit", + dependencies: ["SecretKit"], + path: "Sources/Packages/Sources/SecureEnclaveSecretKit", + resources: [localization], + swiftSettings: swiftSettings + ), + .target( + name: "SmartCardSecretKit", + dependencies: ["SecretKit"], + path: "Sources/Packages/Sources/SmartCardSecretKit", + resources: [localization], + swiftSettings: swiftSettings + ), + ] +) + +var localization: Resource { + .process("../../Localizable.xcstrings") +} + +var swiftSettings: [PackageDescription.SwiftSetting] { + [ + .swiftLanguageMode(.v6), + // This freaks out Xcode in a dependency context. + // .treatAllWarnings(as: .error), + ] +}