diff --git a/Sources/LegacyValet/Public/VALLegacySecureEnclaveValet.h b/Sources/LegacyValet/Public/VALLegacySecureEnclaveValet.h index 6116639e..38ee1403 100644 --- a/Sources/LegacyValet/Public/VALLegacySecureEnclaveValet.h +++ b/Sources/LegacyValet/Public/VALLegacySecureEnclaveValet.h @@ -14,7 +14,7 @@ // limitations under the License. // -#import "VALLegacyValet.h" +#import /// Compiler flag for building against an SDK where Secure Enclave is available. diff --git a/Sources/LegacyValet/Public/VALLegacySinglePromptSecureEnclaveValet.h b/Sources/LegacyValet/Public/VALLegacySinglePromptSecureEnclaveValet.h index 744dc07b..cf411963 100644 --- a/Sources/LegacyValet/Public/VALLegacySinglePromptSecureEnclaveValet.h +++ b/Sources/LegacyValet/Public/VALLegacySinglePromptSecureEnclaveValet.h @@ -14,7 +14,7 @@ // limitations under the License. // -#import "VALLegacySecureEnclaveValet.h" +#import /// Reads and writes keychain elements that are stored on the Secure Enclave (available on iOS 8.0 and later and macOS 10.11 and later) using accessibility attribute VALLegacyAccessibilityWhenPasscodeSetThisDeviceOnly. The first access of these keychain elements will require the user to confirm their presence via Touch ID or passcode entry. diff --git a/Sources/LegacyValet/Public/VALSynchronizableValet.h b/Sources/LegacyValet/Public/VALSynchronizableValet.h index 25fd28f2..6c7666a4 100644 --- a/Sources/LegacyValet/Public/VALSynchronizableValet.h +++ b/Sources/LegacyValet/Public/VALSynchronizableValet.h @@ -14,7 +14,7 @@ // limitations under the License. // -#import "VALLegacyValet.h" +#import /// Reads and writes keychain elements that are synchronized with iCloud (supported on devices on iOS 7.0.3 and later). Accessibility must not be scoped to this device. diff --git a/Sources/Valet/Internal/Keychain.swift b/Sources/Valet/Internal/Keychain.swift index c117ad2b..f35b24d2 100644 --- a/Sources/Valet/Internal/Keychain.swift +++ b/Sources/Valet/Internal/Keychain.swift @@ -52,7 +52,7 @@ internal final class Keychain { // MARK: Getters - internal static func string(forKey key: String, options: [String : AnyHashable]) throws -> String { + internal static func string(forKey key: String, options: [String : AnyHashable]) throws(KeychainError) -> String { let data = try object(forKey: key, options: options) if let string = String(data: data, encoding: .utf8) { return string @@ -61,7 +61,7 @@ internal final class Keychain { } } - internal static func object(forKey key: String, options: [String : AnyHashable]) throws -> Data { + internal static func object(forKey key: String, options: [String : AnyHashable]) throws(KeychainError) -> Data { guard !key.isEmpty else { throw KeychainError.emptyKey } @@ -76,7 +76,7 @@ internal final class Keychain { // MARK: Setters - internal static func setString(_ string: String, forKey key: String, options: [String: AnyHashable]) throws { + internal static func setString(_ string: String, forKey key: String, options: [String: AnyHashable]) throws(KeychainError) { let data = Data(string.utf8) try setObject(data, forKey: key, options: options) } @@ -110,7 +110,7 @@ internal final class Keychain { // MARK: Removal - internal static func removeObject(forKey key: String, options: [String : AnyHashable]) throws { + internal static func removeObject(forKey key: String, options: [String : AnyHashable]) throws(KeychainError) { guard !key.isEmpty else { throw KeychainError.emptyKey } @@ -121,7 +121,7 @@ internal final class Keychain { try SecItem.deleteItems(matching: secItemQuery) } - internal static func removeAllObjects(matching options: [String : AnyHashable]) throws { + internal static func removeAllObjects(matching options: [String : AnyHashable]) throws(KeychainError) { try SecItem.deleteItems(matching: options) } @@ -140,7 +140,7 @@ internal final class Keychain { // MARK: AllObjects - internal static func allKeys(options: [String: AnyHashable]) throws -> Set { + internal static func allKeys(options: [String: AnyHashable]) throws(KeychainError) -> Set { var secItemQuery = options secItemQuery[kSecMatchLimit as String] = kSecMatchLimitAll secItemQuery[kSecReturnAttributes as String] = true diff --git a/Sources/Valet/SecureEnclave.swift b/Sources/Valet/SecureEnclave.swift index 9e3b4e37..03a2e8b7 100644 --- a/Sources/Valet/SecureEnclave.swift +++ b/Sources/Valet/SecureEnclave.swift @@ -14,6 +14,9 @@ // limitations under the License. // +#if !os(tvOS) && canImport(LocalAuthentication) +import LocalAuthentication +#endif import Foundation @@ -50,7 +53,7 @@ public final class SecureEnclave: Sendable { /// - key: A key that can be used to retrieve the `object` from the keychain. /// - options: A base query used to scope the calls in the keychain. /// - Throws: An error of type `KeychainError`. - internal static func setObject(_ object: Data, forKey key: String, options: [String : AnyHashable]) throws { + internal static func setObject(_ object: Data, forKey key: String, options: [String : AnyHashable]) throws(KeychainError) { // Remove the key before trying to set it. This will prevent us from calling SecItemUpdate on an item stored on the Secure Enclave, which would cause iOS to prompt the user for authentication. try Keychain.removeObject(forKey: key, options: options) @@ -63,23 +66,30 @@ public final class SecureEnclave: Sendable { /// - options: A base query used to scope the calls in the keychain. /// - Returns: The data currently stored in the keychain for the provided key. /// - Throws: An error of type `KeychainError`. - internal static func object(forKey key: String, withPrompt userPrompt: String, options: [String : AnyHashable]) throws -> Data { + internal static func object(forKey key: String, withPrompt userPrompt: String, options: [String : AnyHashable]) throws(KeychainError) -> Data { var secItemQuery = options +#if !os(tvOS) && !os(watchOS) && canImport(LocalAuthentication) if !userPrompt.isEmpty { - secItemQuery[kSecUseOperationPrompt as String] = userPrompt + let context = LAContext() + context.localizedReason = userPrompt + secItemQuery[kSecUseAuthenticationContext as String] = context } - +#endif + return try Keychain.object(forKey: key, options: secItemQuery) } +#if !os(tvOS) && canImport(LocalAuthentication) /// - Parameters: /// - key: The key to look up in the keychain. /// - options: A base query used to scope the calls in the keychain. /// - Returns: `true` if a value has been set for the given key, `false` otherwise. /// - Throws: An error of type `KeychainError`. - internal static func containsObject(forKey key: String, options: [String : AnyHashable]) throws -> Bool { + internal static func containsObject(forKey key: String, options: [String : AnyHashable]) throws(KeychainError) -> Bool { var secItemQuery = options - secItemQuery[kSecUseAuthenticationUI as String] = kSecUseAuthenticationUIFail + let context = LAContext() + context.interactionNotAllowed = true + secItemQuery[kSecUseAuthenticationContext as String] = context let status = Keychain.performCopy(forKey: key, options: secItemQuery) switch status { @@ -93,13 +103,14 @@ public final class SecureEnclave: Sendable { throw KeychainError(status: status) } } +#endif /// - Parameters: /// - string: A String value to be inserted into the keychain. /// - key: A key that can be used to retrieve the `string` from the keychain. /// - options: A base query used to scope the calls in the keychain. /// - Throws: An error of type `KeychainError`. - internal static func setString(_ string: String, forKey key: String, options: [String : AnyHashable]) throws { + internal static func setString(_ string: String, forKey key: String, options: [String : AnyHashable]) throws(KeychainError) { // Remove the key before trying to set it. This will prevent us from calling SecItemUpdate on an item stored on the Secure Enclave, which would cause iOS to prompt the user for authentication. try Keychain.removeObject(forKey: key, options: options) @@ -112,11 +123,15 @@ public final class SecureEnclave: Sendable { /// - options: A base query used to scope the calls in the keychain. /// - Returns: The string currently stored in the keychain for the provided key. /// - Throws: An error of type `KeychainError`. - internal static func string(forKey key: String, withPrompt userPrompt: String, options: [String : AnyHashable]) throws -> String { + internal static func string(forKey key: String, withPrompt userPrompt: String, options: [String : AnyHashable]) throws(KeychainError) -> String { var secItemQuery = options +#if !os(tvOS) && !os(watchOS) && canImport(LocalAuthentication) if !userPrompt.isEmpty { - secItemQuery[kSecUseOperationPrompt as String] = userPrompt + let context = LAContext() + context.localizedReason = userPrompt + secItemQuery[kSecUseAuthenticationContext as String] = context } +#endif return try Keychain.string(forKey: key, options: secItemQuery) } diff --git a/Sources/Valet/SecureEnclaveValet.swift b/Sources/Valet/SecureEnclaveValet.swift index efaf7a41..49b86070 100644 --- a/Sources/Valet/SecureEnclaveValet.swift +++ b/Sources/Valet/SecureEnclaveValet.swift @@ -121,33 +121,55 @@ public final class SecureEnclaveValet: NSObject, Sendable { /// - Throws: An error of type `KeychainError`. /// - Important: Inserted data should be no larger than 4kb. @objc - public func setObject(_ object: Data, forKey key: String) throws { - try execute(in: lock) { - try SecureEnclave.setObject(object, forKey: key, options: baseKeychainQuery) + public func setObject(_ object: Data, forKey key: String) throws(KeychainError) { + lock.lock() + defer { + lock.unlock() } + return try SecureEnclave.setObject(object, forKey: key, options: baseKeychainQuery) } +#if !os(tvOS) && !os(watchOS) && canImport(LocalAuthentication) /// - Parameters: /// - key: A key used to retrieve the desired object from the keychain. /// - userPrompt: The prompt displayed to the user in Apple's Face ID, Touch ID, or passcode entry UI. /// - Returns: The data currently stored in the keychain for the provided key. /// - Throws: An error of type `KeychainError`. @objc - public func object(forKey key: String, withPrompt userPrompt: String) throws -> Data { - try execute(in: lock) { - try SecureEnclave.object(forKey: key, withPrompt: userPrompt, options: baseKeychainQuery) + public func object(forKey key: String, withPrompt userPrompt: String) throws(KeychainError) -> Data { + lock.lock() + defer { + lock.unlock() + } + return try SecureEnclave.object(forKey: key, withPrompt: userPrompt, options: baseKeychainQuery) + } +#else + /// - Parameter key: A key used to retrieve the desired object from the keychain. + /// - Returns: The data currently stored in the keychain for the provided key. + /// - Throws: An error of type `KeychainError`. + @objc + public func object(forKey key: String) throws(KeychainError) -> Data { + lock.lock() + defer { + lock.unlock() } + return try SecureEnclave.object(forKey: key, withPrompt: "", options: baseKeychainQuery) } +#endif +#if !os(tvOS) && canImport(LocalAuthentication) /// - Parameter key: The key to look up in the keychain. /// - Returns: `true` if a value has been set for the given key, `false` otherwise. /// - Throws: An error of type `KeychainError`. /// - Note: Will never prompt the user for Face ID, Touch ID, or password. - public func containsObject(forKey key: String) throws -> Bool { - try execute(in: lock) { - try SecureEnclave.containsObject(forKey: key, options: baseKeychainQuery) + public func containsObject(forKey key: String) throws(KeychainError) -> Bool { + lock.lock() + defer { + lock.unlock() } + return try SecureEnclave.containsObject(forKey: key, options: baseKeychainQuery) } +#endif /// - Parameters: /// - string: A String value to be inserted into the keychain. @@ -155,41 +177,63 @@ public final class SecureEnclaveValet: NSObject, Sendable { /// - Throws: An error of type `KeychainError`. /// - Important: Inserted data should be no larger than 4kb. @objc - public func setString(_ string: String, forKey key: String) throws { - try execute(in: lock) { - try SecureEnclave.setString(string, forKey: key, options: baseKeychainQuery) + public func setString(_ string: String, forKey key: String) throws(KeychainError) { + lock.lock() + defer { + lock.unlock() } + return try SecureEnclave.setString(string, forKey: key, options: baseKeychainQuery) } +#if !os(tvOS) && !os(watchOS) && canImport(LocalAuthentication) /// - Parameters: /// - key: A key used to retrieve the desired object from the keychain. /// - userPrompt: The prompt displayed to the user in Apple's Face ID, Touch ID, or passcode entry UI. /// - Returns: The string currently stored in the keychain for the provided key. /// - Throws: An error of type `KeychainError`. @objc - public func string(forKey key: String, withPrompt userPrompt: String) throws -> String { - try execute(in: lock) { - try SecureEnclave.string(forKey: key, withPrompt: userPrompt, options: baseKeychainQuery) + public func string(forKey key: String, withPrompt userPrompt: String) throws(KeychainError) -> String { + lock.lock() + defer { + lock.unlock() } + return try SecureEnclave.string(forKey: key, withPrompt: userPrompt, options: baseKeychainQuery) } - +#else + /// - Parameter key: A key used to retrieve the desired object from the keychain. + /// - Returns: The string currently stored in the keychain for the provided key. + /// - Throws: An error of type `KeychainError`. + @objc + public func string(forKey key: String) throws(KeychainError) -> String { + lock.lock() + defer { + lock.unlock() + } + return try SecureEnclave.string(forKey: key, withPrompt: "", options: baseKeychainQuery) + } +#endif + /// Removes a key/object pair from the keychain. /// - Parameter key: A key used to remove the desired object from the keychain. /// - Throws: An error of type `KeychainError`. @objc - public func removeObject(forKey key: String) throws { - try execute(in: lock) { - try Keychain.removeObject(forKey: key, options: baseKeychainQuery) + public func removeObject(forKey key: String) throws(KeychainError) { + lock.lock() + defer { + lock.unlock() } + return try Keychain.removeObject(forKey: key, options: baseKeychainQuery) } /// Removes all key/object pairs accessible by this Valet instance from the keychain. /// - Throws: An error of type `KeychainError`. @objc - public func removeAllObjects() throws { - try execute(in: lock) { - try Keychain.removeAllObjects(matching: baseKeychainQuery) + public func removeAllObjects() throws(KeychainError) { + lock.lock() + defer { + lock.unlock() } + return try Keychain.removeAllObjects(matching: baseKeychainQuery) } /// Migrates objects matching the input query into the receiving SecureEnclaveValet instance. @@ -284,6 +328,7 @@ extension SecureEnclaveValet { return sharedGroupValet(with: identifier, accessControl: accessControl) } +#if !os(tvOS) && canImport(LocalAuthentication) /// - Parameter key: The key to look up in the keychain. /// - Returns: `true` if a value has been set for the given key, `false` otherwise. Will return `false` if the keychain is not accessible. /// - Note: Will never prompt the user for Face ID, Touch ID, or password. @@ -295,5 +340,6 @@ extension SecureEnclaveValet { } return containsObject } +#endif } diff --git a/Sources/Valet/SinglePromptSecureEnclaveValet.swift b/Sources/Valet/SinglePromptSecureEnclaveValet.swift index 67dd0b64..baa9ad3d 100644 --- a/Sources/Valet/SinglePromptSecureEnclaveValet.swift +++ b/Sources/Valet/SinglePromptSecureEnclaveValet.swift @@ -14,9 +14,7 @@ // limitations under the License. // -// Xcode 13 and prior incorrectly say that LocalAuthentication is available on tvOS, so we have to check both as long as Xcode 13 and prior are supported. -// Xcode 14 moved the LAContext availability to watchOS 3, so only that version is explicitly annotated. -#if !os(tvOS) && canImport(LocalAuthentication) +#if !os(tvOS) && !os(watchOS) && canImport(LocalAuthentication) import LocalAuthentication import Foundation @@ -196,7 +194,9 @@ public final class SinglePromptSecureEnclaveValet: NSObject, @unchecked Sendable try execute(in: lock) { var secItemQuery = try continuedAuthenticationKeychainQuery() if !userPrompt.isEmpty { - secItemQuery[kSecUseOperationPrompt as String] = userPrompt + let context = LAContext() + context.localizedReason = userPrompt + secItemQuery[kSecUseAuthenticationContext as String] = context } return try Keychain.allKeys(options: secItemQuery) diff --git a/Sources/Valet/Valet.swift b/Sources/Valet/Valet.swift index 060220f3..a5802202 100644 --- a/Sources/Valet/Valet.swift +++ b/Sources/Valet/Valet.swift @@ -251,36 +251,42 @@ public final class Valet: NSObject, Sendable { /// - Throws: An error of type `KeychainError`. /// - Important: Inserted data should be no larger than 4kb. @objc - public func setObject(_ object: Data, forKey key: String) throws { - try execute(in: lock) { - try Keychain.setObject(object, forKey: key, options: baseKeychainQuery) + public func setObject(_ object: Data, forKey key: String) throws(KeychainError) { + lock.lock() + defer { + lock.unlock() } + return try Keychain.setObject(object, forKey: key, options: baseKeychainQuery) } /// - Parameter key: A key used to retrieve the desired object from the keychain. /// - Returns: The data currently stored in the keychain for the provided key. /// - Throws: An error of type `KeychainError`. @objc - public func object(forKey key: String) throws -> Data { - try execute(in: lock) { - try Keychain.object(forKey: key, options: baseKeychainQuery) + public func object(forKey key: String) throws(KeychainError) -> Data { + lock.lock() + defer { + lock.unlock() } + return try Keychain.object(forKey: key, options: baseKeychainQuery) } /// - Parameter key: The key to look up in the keychain. /// - Returns: `true` if a value has been set for the given key, `false` otherwise. /// - Throws: An error of type `KeychainError`. - public func containsObject(forKey key: String) throws -> Bool { - try execute(in: lock) { - let status = Keychain.performCopy(forKey: key, options: baseKeychainQuery) - switch status { - case errSecSuccess: - return true - case errSecItemNotFound: - return false - default: - throw KeychainError(status: status) - } + public func containsObject(forKey key: String) throws(KeychainError) -> Bool { + lock.lock() + defer { + lock.unlock() + } + let status = Keychain.performCopy(forKey: key, options: baseKeychainQuery) + switch status { + case errSecSuccess: + return true + case errSecItemNotFound: + return false + default: + throw KeychainError(status: status) } } @@ -290,29 +296,35 @@ public final class Valet: NSObject, Sendable { /// - Throws: An error of type `KeychainError`. /// - Important: Inserted data should be no larger than 4kb. @objc - public func setString(_ string: String, forKey key: String) throws { - try execute(in: lock) { - try Keychain.setString(string, forKey: key, options: baseKeychainQuery) + public func setString(_ string: String, forKey key: String) throws(KeychainError) { + lock.lock() + defer { + lock.unlock() } + return try Keychain.setString(string, forKey: key, options: baseKeychainQuery) } /// - Parameter key: A key used to retrieve the desired object from the keychain. /// - Returns: The string currently stored in the keychain for the provided key. /// - Throws: An error of type `KeychainError`. @objc - public func string(forKey key: String) throws -> String { - try execute(in: lock) { - try Keychain.string(forKey: key, options: baseKeychainQuery) + public func string(forKey key: String) throws(KeychainError) -> String { + lock.lock() + defer { + lock.unlock() } + return try Keychain.string(forKey: key, options: baseKeychainQuery) } /// - Returns: The set of all (String) keys currently stored in this Valet instance. If no items are found, will return an empty set. /// - Throws: An error of type `KeychainError`. @objc - public func allKeys() throws -> Set { - try execute(in: lock) { - try Keychain.allKeys(options: baseKeychainQuery) + public func allKeys() throws(KeychainError) -> Set { + lock.lock() + defer { + lock.unlock() } + return try Keychain.allKeys(options: baseKeychainQuery) } /// Removes a key/object pair from the keychain. @@ -320,19 +332,23 @@ public final class Valet: NSObject, Sendable { /// - Throws: An error of type `KeychainError`. /// - Note: No error is thrown if the `key` is not found in the keychain. @objc - public func removeObject(forKey key: String) throws { - try execute(in: lock) { - try Keychain.removeObject(forKey: key, options: baseKeychainQuery) + public func removeObject(forKey key: String) throws(KeychainError) { + lock.lock() + defer { + lock.unlock() } + return try Keychain.removeObject(forKey: key, options: baseKeychainQuery) } /// Removes all key/object pairs accessible by this Valet instance from the keychain. /// - Throws: An error of type `KeychainError`. @objc - public func removeAllObjects() throws { - try execute(in: lock) { - try Keychain.removeAllObjects(matching: baseKeychainQuery) + public func removeAllObjects() throws(KeychainError) { + lock.lock() + defer { + lock.unlock() } + return try Keychain.removeAllObjects(matching: baseKeychainQuery) } /// Migrates objects matching the input query into the receiving Valet instance. diff --git a/Tests/ValetIntegrationTests/BackwardsCompatibilityTests/SecureEnclaveBackwardsCompatibilityTests.swift b/Tests/ValetIntegrationTests/BackwardsCompatibilityTests/SecureEnclaveBackwardsCompatibilityTests.swift index 5f428b36..c3b653ad 100644 --- a/Tests/ValetIntegrationTests/BackwardsCompatibilityTests/SecureEnclaveBackwardsCompatibilityTests.swift +++ b/Tests/ValetIntegrationTests/BackwardsCompatibilityTests/SecureEnclaveBackwardsCompatibilityTests.swift @@ -22,7 +22,7 @@ import XCTest extension SecureEnclaveIntegrationTests { - @available (*, deprecated) + @available(*, deprecated) func test_backwardsCompatibility_withLegacyValet() throws { guard testEnvironmentIsSignedOrDoesNotRequireEntitlement() && testEnvironmentSupportsWhenPasscodeSet() else { diff --git a/Tests/ValetIntegrationTests/BackwardsCompatibilityTests/SinglePromptSecureEnclaveBackwardsCompatibilityTests.swift b/Tests/ValetIntegrationTests/BackwardsCompatibilityTests/SinglePromptSecureEnclaveBackwardsCompatibilityTests.swift index 8d394e69..9126daac 100644 --- a/Tests/ValetIntegrationTests/BackwardsCompatibilityTests/SinglePromptSecureEnclaveBackwardsCompatibilityTests.swift +++ b/Tests/ValetIntegrationTests/BackwardsCompatibilityTests/SinglePromptSecureEnclaveBackwardsCompatibilityTests.swift @@ -23,7 +23,7 @@ import XCTest @available(tvOS 11.0, *) extension SinglePromptSecureEnclaveIntegrationTests { - @available (*, deprecated) + @available(*, deprecated) func test_backwardsCompatibility_withLegacyValet() throws { guard testEnvironmentIsSignedOrDoesNotRequireEntitlement() && testEnvironmentSupportsWhenPasscodeSet() else { diff --git a/Tests/ValetIntegrationTests/SecureEnclaveIntegrationTests.swift b/Tests/ValetIntegrationTests/SecureEnclaveIntegrationTests.swift index 38bb4a2b..5cf4cefc 100644 --- a/Tests/ValetIntegrationTests/SecureEnclaveIntegrationTests.swift +++ b/Tests/ValetIntegrationTests/SecureEnclaveIntegrationTests.swift @@ -52,7 +52,11 @@ class SecureEnclaveIntegrationTests: XCTestCase try valet.setString(passcode, forKey: key) let equivalentValet = SecureEnclaveValet.valet(with: valet.identifier, accessControl: valet.accessControl) XCTAssertEqual(valet, equivalentValet) +#if !os(tvOS) && !os(watchOS) && canImport(LocalAuthentication) XCTAssertEqual(passcode, try equivalentValet.string(forKey: key, withPrompt: "")) +#else + XCTAssertEqual(passcode, try equivalentValet.string(forKey: key)) +#endif } func test_secureEnclaveValetsWithDifferingAccessControl_canNotAccessSameData() throws @@ -64,10 +68,17 @@ class SecureEnclaveIntegrationTests: XCTestCase try valet.setString(passcode, forKey: key) let similarValet = SecureEnclaveValet.valet(with: valet.identifier, accessControl: .devicePasscode) XCTAssertNotEqual(valet, similarValet) +#if !os(tvOS) && !os(watchOS) && canImport(LocalAuthentication) XCTAssertEqual(passcode, try valet.string(forKey: key, withPrompt: "")) XCTAssertThrowsError(try similarValet.string(forKey: key, withPrompt: "")) { error in XCTAssertEqual(error as? KeychainError, .itemNotFound) } +#else + XCTAssertEqual(passcode, try valet.string(forKey: key)) + XCTAssertThrowsError(try similarValet.string(forKey: key)) { error in + XCTAssertEqual(error as? KeychainError, .itemNotFound) + } +#endif } func test_secureEnclaveSharedGroupValetsWithDifferingIdentifiers_canNotAccessSameData() throws @@ -82,10 +93,17 @@ class SecureEnclaveIntegrationTests: XCTestCase try valet1.setString(passcode, forKey: key) XCTAssertNotEqual(valet1, valet2) +#if !os(tvOS) && !os(watchOS) && canImport(LocalAuthentication) XCTAssertEqual(passcode, try valet1.string(forKey: key, withPrompt: "")) XCTAssertThrowsError(try valet2.string(forKey: key, withPrompt: "")) { error in XCTAssertEqual(error as? KeychainError, .itemNotFound) } +#else + XCTAssertEqual(passcode, try valet1.string(forKey: key)) + XCTAssertThrowsError(try valet2.string(forKey: key)) { error in + XCTAssertEqual(error as? KeychainError, .itemNotFound) + } +#endif } // MARK: canAccessKeychain @@ -180,10 +198,17 @@ class SecureEnclaveIntegrationTests: XCTestCase try valet.migrateObjects(from: plainOldValet, removeOnCompletion: true) for (key, value) in keyValuePairs { +#if !os(tvOS) && !os(watchOS) && canImport(LocalAuthentication) XCTAssertEqual(value, try valet.string(forKey: key, withPrompt: "")) XCTAssertThrowsError(try plainOldValet.string(forKey: key)) { error in XCTAssertEqual(error as? KeychainError, .itemNotFound) } +#else + XCTAssertEqual(value, try valet.string(forKey: key)) + XCTAssertThrowsError(try plainOldValet.string(forKey: key)) { error in + XCTAssertEqual(error as? KeychainError, .itemNotFound) + } +#endif } // Clean up items for the next test run (allKeys and removeAllObjects are unsupported in VALSecureEnclaveValet). @@ -213,7 +238,11 @@ class SecureEnclaveIntegrationTests: XCTestCase try valet.migrateObjects(from: otherValet, removeOnCompletion: false) for (key, value) in keyStringPairToMigrateMap { +#if !os(tvOS) && !os(watchOS) && canImport(LocalAuthentication) XCTAssertEqual(try valet.string(forKey: key, withPrompt: ""), value) +#else + XCTAssertEqual(try valet.string(forKey: key), value) +#endif XCTAssertEqual(try otherValet.string(forKey: key), value) } } diff --git a/Tests/ValetIntegrationTests/SinglePromptSecureEnclaveIntegrationTests.swift b/Tests/ValetIntegrationTests/SinglePromptSecureEnclaveIntegrationTests.swift index 4f803049..c9b6d3be 100644 --- a/Tests/ValetIntegrationTests/SinglePromptSecureEnclaveIntegrationTests.swift +++ b/Tests/ValetIntegrationTests/SinglePromptSecureEnclaveIntegrationTests.swift @@ -18,8 +18,7 @@ import Foundation @testable import Valet import XCTest -// Xcode 13 and prior incorrectly say that LocalAuthentication is available on tvOS, so we have to check both as long as Xcode 13 and prior are supported. -#if !os(tvOS) && canImport(LocalAuthentication) +#if !os(tvOS) && !os(watchOS) && canImport(LocalAuthentication) class SinglePromptSecureEnclaveIntegrationTests: XCTestCase { diff --git a/Tests/ValetIntegrationTests/ValetIntegrationTests.swift b/Tests/ValetIntegrationTests/ValetIntegrationTests.swift index 2d5e81f7..432c7a60 100644 --- a/Tests/ValetIntegrationTests/ValetIntegrationTests.swift +++ b/Tests/ValetIntegrationTests/ValetIntegrationTests.swift @@ -629,7 +629,7 @@ class ValetIntegrationTests: XCTestCase func test_stringForKey_failsForDataNotBackedByString() throws { try allPermutations.forEach { valet in let dictionary = [ "that's no" : "moon" ] - let nonStringData = NSKeyedArchiver.archivedData(withRootObject: dictionary) + let nonStringData = try NSKeyedArchiver.archivedData(withRootObject: dictionary, requiringSecureCoding: false) try valet.setObject(nonStringData, forKey: key) XCTAssertThrowsError(try valet.string(forKey: key)) { error in XCTAssertEqual(error as? KeychainError, .itemNotFound) @@ -724,7 +724,6 @@ class ValetIntegrationTests: XCTestCase try backgroundValet.setString(self.passcode, forKey: self.key) } catch { XCTFail("Threw \(error) trying to write value") - expectation.fulfill() } stringForKeyQueue.async { do { diff --git a/Tests/ValetTests/SinglePromptSecureEnclaveTests.swift b/Tests/ValetTests/SinglePromptSecureEnclaveTests.swift index 10fcf39d..dd3c3d85 100644 --- a/Tests/ValetTests/SinglePromptSecureEnclaveTests.swift +++ b/Tests/ValetTests/SinglePromptSecureEnclaveTests.swift @@ -14,8 +14,7 @@ // limitations under the License. // -// Xcode 13 and prior incorrectly say that LocalAuthentication is available on tvOS, so we have to check both as long as Xcode 13 and prior are supported. -#if !os(tvOS) && canImport(LocalAuthentication) +#if !os(tvOS) && !os(watchOS) && canImport(LocalAuthentication) import Foundation @testable import Valet diff --git a/Valet iOS Test Host App/ValetIOSTestHostAppDelegate.swift b/Valet iOS Test Host App/ValetIOSTestHostAppDelegate.swift index 2b28cbf5..4fee8df0 100644 --- a/Valet iOS Test Host App/ValetIOSTestHostAppDelegate.swift +++ b/Valet iOS Test Host App/ValetIOSTestHostAppDelegate.swift @@ -17,7 +17,7 @@ import UIKit -@UIApplicationMain +@main final class ValetIOSTestHostAppDelegate: UIResponder, UIApplicationDelegate { var window: UIWindow? } diff --git a/Valet tvOS Test Host App/AppDelegate.swift b/Valet tvOS Test Host App/AppDelegate.swift index 32d79a07..1c3e54d9 100644 --- a/Valet tvOS Test Host App/AppDelegate.swift +++ b/Valet tvOS Test Host App/AppDelegate.swift @@ -16,7 +16,7 @@ import UIKit -@UIApplicationMain +@main final class AppDelegate: UIResponder, UIApplicationDelegate { var window: UIWindow? } diff --git a/ValetTouchIDTest/ValetTouchIDTestAppDelegate.swift b/ValetTouchIDTest/ValetTouchIDTestAppDelegate.swift index f312940f..389edc06 100644 --- a/ValetTouchIDTest/ValetTouchIDTestAppDelegate.swift +++ b/ValetTouchIDTest/ValetTouchIDTestAppDelegate.swift @@ -17,7 +17,7 @@ import UIKit -@UIApplicationMain +@main final class ValetTouchIDTestAppDelegate: UIResponder, UIApplicationDelegate { var window: UIWindow? }