diff --git a/.gitignore b/.gitignore index 20b373c..e3d6c5f 100644 --- a/.gitignore +++ b/.gitignore @@ -44,3 +44,4 @@ Package.resolved # DocC docs.archive/ +.DerivedData.noindex \ No newline at end of file diff --git a/Package.swift b/Package.swift index b237a8e..983324d 100644 --- a/Package.swift +++ b/Package.swift @@ -3,8 +3,11 @@ import PackageDescription let package = Package( name: "SimpleKeychain", - platforms: [.iOS(.v14), .macOS(.v11), .tvOS(.v14), .watchOS(.v7), .visionOS(.v1)], - products: [.library(name: "SimpleKeychain", targets: ["SimpleKeychain"])], + platforms: [.iOS(.v14), .macOS(.v10_15), .tvOS(.v14), .watchOS(.v7), .visionOS(.v1)], + products: [ + .library(name: "SimpleKeychain", targets: ["SimpleKeychain"]), + .library(name: "SimpleKeychain-dynamic", type: .dynamic, targets: ["SimpleKeychain"]) + ], targets: [ .target( name: "SimpleKeychain", diff --git a/SimpleKeychain.xcodeproj/project.pbxproj b/SimpleKeychain.xcodeproj/project.pbxproj index a2e6ff8..cc41151 100644 --- a/SimpleKeychain.xcodeproj/project.pbxproj +++ b/SimpleKeychain.xcodeproj/project.pbxproj @@ -7,6 +7,15 @@ objects = { /* Begin PBXBuildFile section */ + 298132FD2D7300490089E3B8 /* SimpleKeychainObjC.swift in Sources */ = {isa = PBXBuildFile; fileRef = 298132FC2D7300490089E3B8 /* SimpleKeychainObjC.swift */; }; + 298132FE2D7300490089E3B8 /* SimpleKeychainObjC.swift in Sources */ = {isa = PBXBuildFile; fileRef = 298132FC2D7300490089E3B8 /* SimpleKeychainObjC.swift */; }; + 298132FF2D7300490089E3B8 /* SimpleKeychainObjC.swift in Sources */ = {isa = PBXBuildFile; fileRef = 298132FC2D7300490089E3B8 /* SimpleKeychainObjC.swift */; }; + 298133002D7300490089E3B8 /* SimpleKeychainObjC.swift in Sources */ = {isa = PBXBuildFile; fileRef = 298132FC2D7300490089E3B8 /* SimpleKeychainObjC.swift */; }; + 298133012D7300490089E3B8 /* SimpleKeychainObjC.swift in Sources */ = {isa = PBXBuildFile; fileRef = 298132FC2D7300490089E3B8 /* SimpleKeychainObjC.swift */; }; + 298133032D7301740089E3B8 /* SimpleKeychainObjCTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 298133022D7301740089E3B8 /* SimpleKeychainObjCTests.swift */; }; + 298133042D7301740089E3B8 /* SimpleKeychainObjCTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 298133022D7301740089E3B8 /* SimpleKeychainObjCTests.swift */; }; + 298133052D7301740089E3B8 /* SimpleKeychainObjCTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 298133022D7301740089E3B8 /* SimpleKeychainObjCTests.swift */; }; + 298133062D7301740089E3B8 /* SimpleKeychainObjCTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 298133022D7301740089E3B8 /* SimpleKeychainObjCTests.swift */; }; 5B0D47641EA63CD1009FF1BF /* SimpleKeychainSpec.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5F4D27651BCE995C003C27B3 /* SimpleKeychainSpec.swift */; }; 5C29744623FF457A00BC18FA /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5C29744523FF457A00BC18FA /* AppDelegate.swift */; }; 5C29744823FF457A00BC18FA /* ViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5C29744723FF457A00BC18FA /* ViewController.swift */; }; @@ -117,6 +126,8 @@ /* End PBXCopyFilesBuildPhase section */ /* Begin PBXFileReference section */ + 298132FC2D7300490089E3B8 /* SimpleKeychainObjC.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SimpleKeychainObjC.swift; sourceTree = ""; }; + 298133022D7301740089E3B8 /* SimpleKeychainObjCTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SimpleKeychainObjCTests.swift; sourceTree = ""; }; 5B0AB18F2088E2DB002D7109 /* README.md */ = {isa = PBXFileReference; lastKnownFileType = net.daringfireball.markdown; path = README.md; sourceTree = SOURCE_ROOT; }; 5B0D47591EA63C74009FF1BF /* SimpleKeychainTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = SimpleKeychainTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; }; 5B108AA91EA62F6100ED4DD2 /* SimpleKeychain.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = SimpleKeychain.framework; sourceTree = BUILT_PRODUCTS_DIR; }; @@ -323,6 +334,7 @@ children = ( 5FEEB99F1B7BD70A00501415 /* Supporting Files */, 5CDF40592852D88C003840E6 /* SimpleKeychain.swift */, + 298132FC2D7300490089E3B8 /* SimpleKeychainObjC.swift */, 5C737B2A285A7C0200B4BB25 /* SimpleKeychainError.swift */, 5C840111285AFF7B00689C01 /* Accessibility.swift */, ); @@ -343,6 +355,7 @@ children = ( 5FEEB9AC1B7BD70B00501415 /* Supporting Files */, 5F4D27651BCE995C003C27B3 /* SimpleKeychainSpec.swift */, + 298133022D7301740089E3B8 /* SimpleKeychainObjCTests.swift */, 5C737B2F285AB57A00B4BB25 /* SimpleKeychainErrorSpec.swift */, 5CEB577A285BCE7E00A32A80 /* AccessibilitySpec.swift */, ); @@ -930,6 +943,7 @@ 5B0D47641EA63CD1009FF1BF /* SimpleKeychainSpec.swift in Sources */, 5CEB577D285BCE7E00A32A80 /* AccessibilitySpec.swift in Sources */, 5C737B36285AB9B100B4BB25 /* SimpleKeychainErrorSpec.swift in Sources */, + 298133052D7301740089E3B8 /* SimpleKeychainObjCTests.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -939,6 +953,7 @@ files = ( 5C737B2D285A7C0200B4BB25 /* SimpleKeychainError.swift in Sources */, 5C840114285AFF7B00689C01 /* Accessibility.swift in Sources */, + 298133002D7300490089E3B8 /* SimpleKeychainObjC.swift in Sources */, 5CDF405C2852D88C003840E6 /* SimpleKeychain.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; @@ -949,6 +964,7 @@ files = ( 5C737B2E285A7C0200B4BB25 /* SimpleKeychainError.swift in Sources */, 5C840115285AFF7B00689C01 /* Accessibility.swift in Sources */, + 298132FF2D7300490089E3B8 /* SimpleKeychainObjC.swift in Sources */, 5CDF405D2852D88C003840E6 /* SimpleKeychain.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; @@ -969,6 +985,7 @@ 5F4D27851BCE99E7003C27B3 /* SimpleKeychainSpec.swift in Sources */, 5CEB577B285BCE7E00A32A80 /* AccessibilitySpec.swift in Sources */, 5C737B34285AB9B100B4BB25 /* SimpleKeychainErrorSpec.swift in Sources */, + 298133032D7301740089E3B8 /* SimpleKeychainObjCTests.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -979,6 +996,7 @@ 5F4D279F1BCEA6CE003C27B3 /* SimpleKeychainSpec.swift in Sources */, 5CEB577C285BCE7E00A32A80 /* AccessibilitySpec.swift in Sources */, 5C737B35285AB9B100B4BB25 /* SimpleKeychainErrorSpec.swift in Sources */, + 298133042D7301740089E3B8 /* SimpleKeychainObjCTests.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -996,6 +1014,7 @@ files = ( 5C737B2B285A7C0200B4BB25 /* SimpleKeychainError.swift in Sources */, 5C840112285AFF7B00689C01 /* Accessibility.swift in Sources */, + 298133012D7300490089E3B8 /* SimpleKeychainObjC.swift in Sources */, 5CDF405A2852D88C003840E6 /* SimpleKeychain.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; @@ -1006,6 +1025,7 @@ files = ( 5C737B2C285A7C0200B4BB25 /* SimpleKeychainError.swift in Sources */, 5C840113285AFF7B00689C01 /* Accessibility.swift in Sources */, + 298132FE2D7300490089E3B8 /* SimpleKeychainObjC.swift in Sources */, 5CDF405B2852D88C003840E6 /* SimpleKeychain.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; @@ -1025,6 +1045,7 @@ files = ( C1D1FBAD2C2192FA008E9E3F /* SimpleKeychain.swift in Sources */, C1D1FBAE2C2192FA008E9E3F /* SimpleKeychainError.swift in Sources */, + 298132FD2D7300490089E3B8 /* SimpleKeychainObjC.swift in Sources */, C1D1FBAF2C2192FA008E9E3F /* Accessibility.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; @@ -1036,6 +1057,7 @@ C1D1FBB02C219322008E9E3F /* SimpleKeychainSpec.swift in Sources */, C1D1FBB12C219322008E9E3F /* SimpleKeychainErrorSpec.swift in Sources */, C1D1FBB22C219322008E9E3F /* AccessibilitySpec.swift in Sources */, + 298133062D7301740089E3B8 /* SimpleKeychainObjCTests.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -1389,7 +1411,6 @@ "@executable_path/../Frameworks", "@loader_path/../Frameworks", ); - MACOSX_DEPLOYMENT_TARGET = 11.0; PRODUCT_BUNDLE_IDENTIFIER = com.auth0.SimpleKeychainTests; PRODUCT_NAME = SimpleKeychainTests; SDKROOT = macosx; @@ -1409,7 +1430,6 @@ "@executable_path/../Frameworks", "@loader_path/../Frameworks", ); - MACOSX_DEPLOYMENT_TARGET = 11.0; PRODUCT_BUNDLE_IDENTIFIER = com.auth0.SimpleKeychainTests; PRODUCT_NAME = SimpleKeychainTests; SDKROOT = macosx; @@ -1513,7 +1533,7 @@ GCC_WARN_UNUSED_FUNCTION = YES; GCC_WARN_UNUSED_VARIABLE = YES; IPHONEOS_DEPLOYMENT_TARGET = 14.0; - MACOSX_DEPLOYMENT_TARGET = 11.0; + MACOSX_DEPLOYMENT_TARGET = 10.15; MTL_ENABLE_DEBUG_INFO = YES; ONLY_ACTIVE_ARCH = YES; SDKROOT = iphoneos; @@ -1568,7 +1588,7 @@ GCC_WARN_UNUSED_FUNCTION = YES; GCC_WARN_UNUSED_VARIABLE = YES; IPHONEOS_DEPLOYMENT_TARGET = 14.0; - MACOSX_DEPLOYMENT_TARGET = 11.0; + MACOSX_DEPLOYMENT_TARGET = 10.15; MTL_ENABLE_DEBUG_INFO = NO; SDKROOT = iphoneos; SWIFT_COMPILATION_MODE = wholemodule; @@ -1654,7 +1674,6 @@ "@executable_path/../Frameworks", "@loader_path/Frameworks", ); - MACOSX_DEPLOYMENT_TARGET = 11.0; PRODUCT_BUNDLE_IDENTIFIER = "com.auth0.$(PRODUCT_NAME:rfc1034identifier)"; PRODUCT_NAME = SimpleKeychain; SDKROOT = macosx; @@ -1683,7 +1702,6 @@ "@executable_path/../Frameworks", "@loader_path/Frameworks", ); - MACOSX_DEPLOYMENT_TARGET = 11.0; PRODUCT_BUNDLE_IDENTIFIER = "com.auth0.$(PRODUCT_NAME:rfc1034identifier)"; PRODUCT_NAME = SimpleKeychain; SDKROOT = macosx; diff --git a/SimpleKeychain/SimpleKeychain.swift b/SimpleKeychain/SimpleKeychain.swift index 24191d5..a9152ed 100644 --- a/SimpleKeychain/SimpleKeychain.swift +++ b/SimpleKeychain/SimpleKeychain.swift @@ -1,7 +1,11 @@ import Foundation import Security -#if canImport(LocalAuthentication) +#if canImport(LocalAuthentication) && !os(tvOS) @preconcurrency import LocalAuthentication +public typealias SimpleKeychainContext = LAContext +#else +// Dummy context type for platforms without LAContext +public typealias SimpleKeychainContext = NSObject #endif typealias RetrieveFunction = (_ query: CFDictionary, _ result: UnsafeMutablePointer?) -> OSStatus @@ -21,8 +25,7 @@ public struct SimpleKeychain: @unchecked Sendable { var retrieve: RetrieveFunction = SecItemCopyMatching var remove: RemoveFunction = SecItemDelete - #if canImport(LocalAuthentication) && !os(tvOS) - let context: LAContext? + let context: SimpleKeychainContext? /// Initializes a ``SimpleKeychain`` instance. /// @@ -39,7 +42,7 @@ public struct SimpleKeychain: @unchecked Sendable { accessGroup: String? = nil, accessibility: Accessibility = .afterFirstUnlock, accessControlFlags: SecAccessControlCreateFlags? = nil, - context: LAContext? = nil, + context: SimpleKeychainContext? = nil, synchronizable: Bool = false, attributes: [String: Any] = [:]) { self.service = service @@ -50,31 +53,6 @@ public struct SimpleKeychain: @unchecked Sendable { self.isSynchronizable = synchronizable self.attributes = attributes } - #else - /// Initializes a ``SimpleKeychain`` instance. - /// - /// - Parameter service: Name of the service under which to save items. Defaults to the bundle identifier. - /// - Parameter accessGroup: access group for sharing Keychain items. Defaults to `nil`. - /// - Parameter accessibility: ``Accessibility`` type the stored items will have. Defaults to - /// ``Accessibility/afterFirstUnlock``. - /// - Parameter accessControlFlags: Access control conditions for `kSecAttrAccessControl`. Defaults to `nil`. - /// - Parameter synchronizable: Whether the items should be synchronized through iCloud. Defaults to `false`. - /// - Parameter attributes: Additional attributes to include in every query. Defaults to an empty dictionary. - /// - Returns: A ``SimpleKeychain`` instance. - public init(service: String = Bundle.main.bundleIdentifier!, - accessGroup: String? = nil, - accessibility: Accessibility = .afterFirstUnlock, - accessControlFlags: SecAccessControlCreateFlags? = nil, - synchronizable: Bool = false, - attributes: [String: Any] = [:]) { - self.service = service - self.accessGroup = accessGroup - self.accessibility = accessibility - self.accessControlFlags = accessControlFlags - self.isSynchronizable = synchronizable - self.attributes = attributes - } - #endif private func assertSuccess(forStatus status: OSStatus) throws { if status != errSecSuccess { diff --git a/SimpleKeychain/SimpleKeychainObjC.swift b/SimpleKeychain/SimpleKeychainObjC.swift new file mode 100644 index 0000000..8eb80f0 --- /dev/null +++ b/SimpleKeychain/SimpleKeychainObjC.swift @@ -0,0 +1,281 @@ +import Foundation + +@objc public enum SimpleKeychainAccessibility: Int { + case whenUnlocked + case whenUnlockedThisDeviceOnly + case afterFirstUnlock + case afterFirstUnlockThisDeviceOnly + case whenPasscodeSetThisDeviceOnly +} + +/// A 'Simple' Obj-C wrapper around `SimpleKeychain` +@objcMembers +public class SimpleKeychainObjC: NSObject { + private let simpleKeychain: SimpleKeychain + + /// Initializes a ``SimpleKeychain`` instance. + /// + /// - Parameters: + /// - service: Name of the service under which to save items. Defaults to the bundle identifier. + /// - accessGroup: access group for sharing Keychain items. Defaults to `nil`. + /// - accessibility: ``Accessibility`` type the stored items will have. Defaults to ``Accessibility/afterFirstUnlock``. + /// - accessControlFlags: Access control conditions for `kSecAttrAccessControl`. Defaults to `nil`. + /// - context: `LAContext` used to access Keychain items. Defaults to `nil`. + /// - synchronizable: Whether the items should be synchronized through iCloud. Defaults to `false`. + /// - attributes: Additional attributes to include in every query. Defaults to an empty dictionary. + @objc public init(service: String, + accessGroup: String?, + accessibility: SimpleKeychainAccessibility, + accessControlFlags: NSNumber?, + context: SimpleKeychainContext?, + synchronizable: Bool, + attributes: [String: Any]) { + + // Convert ObjC enum to Swift enum + let swiftAccessibility: Accessibility + switch accessibility { + case .whenUnlocked: + swiftAccessibility = .whenUnlocked + case .whenUnlockedThisDeviceOnly: + swiftAccessibility = .whenUnlockedThisDeviceOnly + case .afterFirstUnlock: + swiftAccessibility = .afterFirstUnlock + case .afterFirstUnlockThisDeviceOnly: + swiftAccessibility = .afterFirstUnlockThisDeviceOnly + case .whenPasscodeSetThisDeviceOnly: + swiftAccessibility = .whenPasscodeSetThisDeviceOnly + } + + // Convert NSNumber to SecAccessControlCreateFlags if provided + let flags: SecAccessControlCreateFlags? + if let accessControlFlags = accessControlFlags { + flags = SecAccessControlCreateFlags(rawValue: accessControlFlags.uintValue) + } else { + flags = nil + } + + // Add kSecUseDataProtectionKeychain on macOS by default + // as we want the accessibility to be used + var mutableAttributes = attributes +#if os(macOS) + if mutableAttributes[kSecUseDataProtectionKeychain as String] == nil { + mutableAttributes[kSecUseDataProtectionKeychain as String] = true + } +#endif + + // Initialize the underlying Swift SimpleKeychain + self.simpleKeychain = SimpleKeychain( + service: service, + accessGroup: accessGroup, + accessibility: swiftAccessibility, + accessControlFlags: flags, + context: context, + synchronizable: synchronizable, + attributes: attributes + ) + + super.init() + } + + /// Convenience initializer that uses the defaults + @objc public convenience init(service: String = Bundle.main.bundleIdentifier!) { + self.init( + service: service, + accessGroup: nil, + accessibility: .afterFirstUnlock, + accessControlFlags: nil, + context: nil, + synchronizable: false, + attributes: [:] + ) + } +} + +// MARK: - Retrieve items + +public extension SimpleKeychainObjC { + /// Retrieves a `String` value from the Keychain. + /// + /// ```swift + /// var error: NSError? + /// let value = try simpleKeychain.string(forKey: "your_key", error: &error) + /// ``` + /// + /// - Parameter key: Key of the Keychain item to retrieve. + /// - Parameter error: On failure, will be set to the error that occurred. + /// - Returns: The `String` value. + @objc func string(forKey key: String, error: NSErrorPointer) -> String? { + do { + return try simpleKeychain.string(forKey: key) + } catch let err { + if let error = error { + error.pointee = err as NSError + } + return nil + } + } + + /// Retrieves a `Data` value from the Keychain. + /// + /// ```swift + /// var error: NSError? + /// let value = try simpleKeychain.data(forKey: "your_key", error: &error) + /// ``` + /// + /// - Parameter key: Key of the Keychain item to retrieve. + /// - Parameter error: On failure, will be set to the error that occurred. + /// - Returns: The `Data` value. + @objc func data(forKey key: String, error: NSErrorPointer) -> Data? { + do { + return try simpleKeychain.data(forKey: key) + } catch let err { + if let error = error { + error.pointee = err as NSError + } + return nil + } + } +} + +// MARK: - Store items + +public extension SimpleKeychainObjC { + /// Saves a `String` value with the type `kSecClassGenericPassword` in the Keychain. + /// + /// ```swift + /// var error: NSError? + /// try simpleKeychain.setString("some string", forKey: "your_key", error: &error) + /// ``` + /// + /// - Parameter string: Value to save in the Keychain. + /// - Parameter key: Key for the Keychain item. + /// - Parameter error: On failure, will be set to the error that occurred. + /// - Returns: `True` on a successful save. When `False`, check error. + @objc func setString(_ string: String, forKey key: String, error: NSErrorPointer) -> Bool { + do { + try simpleKeychain.set(string, forKey: key) + return true + } catch let err { + if let error = error { + error.pointee = err as NSError + } + return false + } + } + + /// Saves a `Data` value with the type `kSecClassGenericPassword` in the Keychain. + /// + /// ```swift + /// var error: NSError? + /// try simpleKeychain.set(data, forKey: "your_key", error: &error) + /// ``` + /// + /// - Parameter data: Value to save in the Keychain. + /// - Parameter key: Key for the Keychain item. + /// - Parameter error: On failure, will be set to the error that occurred. + /// - Returns: `True` on a successful save. When `False`, check error. + @objc func setData(_ data: Data, forKey key: String, error: NSErrorPointer) -> Bool { + do { + try simpleKeychain.set(data, forKey: key) + return true + } catch let err { + if let error = error { + error.pointee = err as NSError + } + return false + } + } +} + +// MARK: - Delete items + +public extension SimpleKeychainObjC { + + /// Deletes an item from the Keychain. + /// + /// ```swift + /// var error: NSError? + /// try simpleKeychain.deleteItem(forKey: "your_key", error: &error) + /// ``` + /// + /// - Parameter key: Key of the Keychain item to delete.. + /// - Parameter error: On failure, will be set to the error that occurred. + /// - Returns: `True` on a successful save. When `False`, check error. + @objc func deleteItem(forKey key: String, error: NSErrorPointer) -> Bool { + do { + try simpleKeychain.deleteItem(forKey: key) + return true + } catch let err { + if let error = error { + error.pointee = err as NSError + } + return false + } + } + + /// Deletes all items from the Keychain for the service and access group values. + /// + /// ```swift + /// var error: NSError? + /// try simpleKeychain.deleteAll(error: &error) + /// ``` + /// + /// - Parameter error: On failure, will be set to the error that occurred. + /// - Returns: `True` on a successful save. When `False`, check error. + @objc func deleteAll(error: NSErrorPointer) -> Bool { + do { + try simpleKeychain.deleteAll() + return true + } catch let err { + if let error = error { + error.pointee = err as NSError + } + return false + } + } +} + +// MARK: - Convenience methods + +public extension SimpleKeychainObjC { + /// Checks if an item is stored in the Keychain. + /// + /// ```swift + /// var error: NSError? + /// let isStored = try simpleKeychain.hasItem(forKey: "your_key", error: &error) + /// ``` + /// + /// - Parameter key: Key of the Keychain item to check. + /// - Parameter error: On failure, will be set to the error that occurred. + /// - Returns: Whether the item is stored in the Keychain or not.. When `False`, check error. + @objc func hasItem(forKey key: String, error: NSErrorPointer) -> Bool { + do { + return try simpleKeychain.hasItem(forKey: key) + } catch let err { + if let error = error { + error.pointee = err as NSError + } + return false + } + } + + /// Retrieves the keys of all the items stored in the Keychain for the service and access group values. + /// + /// ```swift + /// var error: NSError? + /// let keys = try simpleKeychain.keys(error: &error) + /// ``` + /// + /// - Returns: A `String` array containing the keys. + /// - Parameter error: On failure, will be set to the error that occurred. + @objc func keys(error: NSErrorPointer) -> [String]? { + do { + return try simpleKeychain.keys() + } catch let err { + if let error = error { + error.pointee = err as NSError + } + return nil + } + } +} diff --git a/SimpleKeychainTests/SimpleKeychainObjCTests.swift b/SimpleKeychainTests/SimpleKeychainObjCTests.swift new file mode 100644 index 0000000..158f4dd --- /dev/null +++ b/SimpleKeychainTests/SimpleKeychainObjCTests.swift @@ -0,0 +1,202 @@ +import XCTest +import Security +@testable import SimpleKeychain + +class SimpleKeychainObjCTests: XCTestCase { + var sut: SimpleKeychainObjC! + let service = "com.auth0.simplekeychain.objc.tests" + + override func setUp() { + super.setUp() + sut = SimpleKeychainObjC(service: service) + + // Clear any existing items + var error: NSError? + _ = sut.deleteAll(error: &error) + } + + override func tearDown() { + var error: NSError? + _ = sut.deleteAll(error: &error) + sut = nil + super.tearDown() + } + + func testInitializationWithDefaultValues() { + // Default initialization should succeed + let keychain = SimpleKeychainObjC(service: service) + XCTAssertNotNil(keychain) + } + + func testInitializationWithCustomValues() { + // Custom initialization with all parameters should succeed + let accessFlags = NSNumber(value: SecAccessControlCreateFlags.userPresence.rawValue) + let keychain = SimpleKeychainObjC( + service: service, + accessGroup: "Group", + accessibility: .afterFirstUnlock, + accessControlFlags: accessFlags, + context: nil, + synchronizable: true, + attributes: ["testKey": "testValue"] + ) + XCTAssertNotNil(keychain) + } + + func testStoringAndRetrievingString() { + let key = UUID().uuidString + let value = "TestValue-\(UUID().uuidString)" + var error: NSError? + + // Store the string + let success = sut.setString(value, forKey: key, error: &error) + XCTAssertTrue(success, "String should be stored successfully") + XCTAssertNil(error, "Error should be nil when storing succeeds") + + // Retrieve the string + let retrievedValue = sut.string(forKey: key, error: &error) + XCTAssertEqual(retrievedValue, value, "Retrieved value should match stored value") + XCTAssertNil(error, "Error should be nil when retrieving succeeds") + } + + func testStoringAndRetrievingData() { + let key = UUID().uuidString + let originalString = "TestData-\(UUID().uuidString)" + let value = originalString.data(using: .utf8)! + var error: NSError? + + // Store the data + let success = sut.setData(value, forKey: key, error: &error) + XCTAssertTrue(success, "Data should be stored successfully") + XCTAssertNil(error, "Error should be nil when storing succeeds") + + // Retrieve the data + let retrievedData = sut.data(forKey: key, error: &error) + XCTAssertNotNil(retrievedData, "Retrieved data should not be nil") + XCTAssertNil(error, "Error should be nil when retrieving succeeds") + + // Convert back to string and compare + let retrievedString = String(data: retrievedData!, encoding: .utf8) + XCTAssertEqual(retrievedString, originalString, "Retrieved data should convert back to original string") + } + + func testRetrievingNonExistentItem() { + let nonExistentKey = "nonExistentKey-\(UUID().uuidString)" + var error: NSError? + + // Try to retrieve a non-existent string + let retrievedString = sut.string(forKey: nonExistentKey, error: &error) + XCTAssertNil(retrievedString, "Retrieved string should be nil for non-existent key") + XCTAssertNotNil(error, "Error should not be nil when item doesn't exist") + XCTAssertEqual(error?.domain, "SimpleKeychain.SimpleKeychainError", "Error domain should match") + + // Reset error for next test + error = nil + + // Try to retrieve non-existent data + let retrievedData = sut.data(forKey: nonExistentKey, error: &error) + XCTAssertNil(retrievedData, "Retrieved data should be nil for non-existent key") + XCTAssertNotNil(error, "Error should not be nil when item doesn't exist") + } + + func testDeletingItem() { + let key = UUID().uuidString + var error: NSError? + + // Store an item + _ = sut.setString("testValue", forKey: key, error: &error) + XCTAssertNil(error, "Error should be nil when storing succeeds") + + // Verify it exists + var exists = sut.hasItem(forKey: key, error: &error) + XCTAssertTrue(exists, "Item should exist after storing") + XCTAssertNil(error, "Error should be nil when checking succeeds") + + // Delete the item + let success = sut.deleteItem(forKey: key, error: &error) + XCTAssertTrue(success, "Deletion should succeed") + XCTAssertNil(error, "Error should be nil when deletion succeeds") + + // Verify it's gone + exists = sut.hasItem(forKey: key, error: &error) + XCTAssertFalse(exists, "Item should not exist after deletion") + } + + func testDeletingAllItems() { + let keys = [UUID().uuidString, UUID().uuidString, UUID().uuidString] + var error: NSError? + + // Store multiple items + for key in keys { + _ = sut.setString("value for \(key)", forKey: key, error: &error) + XCTAssertNil(error, "Error should be nil when storing succeeds") + } + + // Delete all items + let success = sut.deleteAll(error: &error) + XCTAssertTrue(success, "Deleting all items should succeed") + XCTAssertNil(error, "Error should be nil when deletion succeeds") + + // Verify all items are gone + for key in keys { + let exists = sut.hasItem(forKey: key, error: &error) + XCTAssertFalse(exists, "Item \(key) should not exist after deleteAll") + } + } + + func testRetrievingAllKeys() { + let keys = [UUID().uuidString, UUID().uuidString, UUID().uuidString] + var error: NSError? + + // Store multiple items + for key in keys { + _ = sut.setString("value for \(key)", forKey: key, error: &error) + XCTAssertNil(error, "Error should be nil when storing succeeds") + } + + // Retrieve all keys + let retrievedKeys = sut.keys(error: &error) + XCTAssertNotNil(retrievedKeys, "Retrieved keys should not be nil") + XCTAssertNil(error, "Error should be nil when retrieving keys succeeds") + + // Verify all our keys are included + for key in keys { + XCTAssertTrue(retrievedKeys!.contains(key), "Retrieved keys should contain \(key)") + } + + // Verify count (note: there may be other items from other tests) + XCTAssertGreaterThanOrEqual(retrievedKeys!.count, keys.count, + "Should retrieve at least as many keys as we stored") + } + + func testErrorHandlingForDeleteNonExistentItem() { + let nonExistentKey = "nonExistentKey-\(UUID().uuidString)" + var error: NSError? + + // Try to delete a non-existent item + let success = sut.deleteItem(forKey: nonExistentKey, error: &error) + XCTAssertFalse(success, "Deleting non-existent item should fail") + XCTAssertNotNil(error, "Error should not be nil when deleting non-existent item") + XCTAssertEqual(error?.domain, "SimpleKeychain.SimpleKeychainError", "Error domain should match") + } + + func testUpdatingExistingItem() { + let key = UUID().uuidString + let originalValue = "original value" + let updatedValue = "updated value" + var error: NSError? + + // Store original value + _ = sut.setString(originalValue, forKey: key, error: &error) + XCTAssertNil(error, "Error should be nil when storing succeeds") + + // Update with new value + let success = sut.setString(updatedValue, forKey: key, error: &error) + XCTAssertTrue(success, "Updating existing item should succeed") + XCTAssertNil(error, "Error should be nil when updating succeeds") + + // Verify updated value + let retrievedValue = sut.string(forKey: key, error: &error) + XCTAssertEqual(retrievedValue, updatedValue, "Retrieved value should match updated value") + } +}