diff --git a/Package.swift b/Package.swift index 6869780..ba64da6 100644 --- a/Package.swift +++ b/Package.swift @@ -23,5 +23,8 @@ let package = Package( .target( name: "KeychainUtility", dependencies: []), + .testTarget( + name: "KeychainUtilityTests", + dependencies: ["KeychainUtility"]), ] ) diff --git a/README.md b/README.md index 95fc9e0..bf46175 100644 --- a/README.md +++ b/README.md @@ -2,6 +2,23 @@ Keychain Utility is a wrapper to help you interact with the Keychain APIs in an easier way +# Usage +Add `@KeychainStorage` annotation in the properties you want to store into the keychain (or read from it). Provide a `Key` and an `ItemType` (you can check the list [here](https://blorenzo10.github.io/keychain-utility/documentation/keychainutility/itemclass)) + +```swift +@KeychainStorage(key: "API-Token", itemClass: .generic) +var token: String? + +// It will print the current value +print(token) + +// Update item +token = "c0c61d55558b0c8dac82a16c04981eea7c99e37d714367e575028221028b0d4cff122d6a7556fc0ab1c66d1d4b05b378" + +// Delete item +token = nil +``` + # Documentation Check [Keychain Utility Documentation Page](https://blorenzo10.github.io/keychain-utility/documentation/keychainutility/) for more info. diff --git a/Sources/KeychainUtility/KeychainManager.swift b/Sources/KeychainUtility/KeychainManager.swift index 45a8655..9b71978 100644 --- a/Sources/KeychainUtility/KeychainManager.swift +++ b/Sources/KeychainUtility/KeychainManager.swift @@ -3,13 +3,15 @@ import Foundation /// Manager with all the necessary methods to interact with the keychain public class KeychainManager { - /// Shared instance to access the manager - public static let shared = KeychainManager() + private var attributes: ItemAttributes? - public typealias KeychainDictionary = [String : Any] - public typealias ItemAttributes = [CFString : Any] + public class var standard: KeychainManager { + return KeychainManager() + } - private init() {} + public init(attributes: ItemAttributes? = nil) { + self.attributes = attributes + } /// Save any Encodable data into the keychain /// @@ -31,8 +33,7 @@ public class KeychainManager { /// - Parameter itemClass: The item class /// - Parameter key: Key to identifiy the item /// - Parameter attributes: Optional dictionary with attributes to narrow the search - public func saveItem(_ item: T, itemClass: ItemClass, key: String, attributes: ItemAttributes? = nil) throws { - + public func saveItem(_ item: T, itemClass: ItemClass, key: String) throws { let itemData = try JSONEncoder().encode(item) var query: KeychainDictionary = [ kSecClass as String: itemClass.rawValue, @@ -69,7 +70,7 @@ public class KeychainManager { /// - Parameter key: Key to identifiy the item /// - Parameter attributes: Optional dictionary with attributes to narrow the search /// - Returns: An instance of type `T` - public func retrieveItem(ofClass itemClass: ItemClass, key: String, attributes: ItemAttributes? = nil) throws -> T { + public func retrieveItem(ofClass itemClass: ItemClass, key: String) throws -> T { var query: KeychainDictionary = [ kSecClass as String: itemClass.rawValue, kSecAttrAccount as String: key as AnyObject, @@ -113,7 +114,7 @@ public class KeychainManager { /// - Parameter itemClass: The item class /// - Parameter key: Key to identifiy the item /// - Parameter attributes: Optional dictionary with attributes to narrow the search - public func updateItem(with item: T, ofClass itemClass: ItemClass, key: String, attributes: ItemAttributes? = nil) throws { + public func updateItem(with item: T, ofClass itemClass: ItemClass, key: String) throws { var query: KeychainDictionary = [ kSecClass as String: itemClass.rawValue, kSecAttrAccount as String: key as AnyObject, @@ -153,7 +154,7 @@ public class KeychainManager { /// - Parameter itemClass: The item class /// - Parameter key: Key to identifiy the item /// - Parameter attributes: Optional dictionary with attributes to narrow the search - public func deleteItem(ofClass itemClass: ItemClass, key: String, attributes: ItemAttributes? = nil) throws { + public func deleteItem(ofClass itemClass: ItemClass, key: String) throws { var query: KeychainDictionary = [ kSecClass as String: itemClass.rawValue, kSecAttrAccount as String: key as AnyObject @@ -188,13 +189,3 @@ private extension KeychainManager { } } - -// MARK: - Dictionary - -extension KeychainManager.KeychainDictionary { - mutating func addAttributes(_ attributes: KeychainManager.ItemAttributes) { - for(key, value) in attributes { - self[key as String] = value - } - } -} diff --git a/Sources/KeychainUtility/KeychainStorage.swift b/Sources/KeychainUtility/KeychainStorage.swift new file mode 100644 index 0000000..be93810 --- /dev/null +++ b/Sources/KeychainUtility/KeychainStorage.swift @@ -0,0 +1,73 @@ +import Foundation + +@propertyWrapper +public struct KeychainStorage { + let key: String + let itemClass: ItemClass + let keychain: KeychainManager + + private var currentValue: T? + + public var wrappedValue: T? { + get { + return getItem() + } + + set { + if let newValue { + getItem() != nil + ? updateItem(newValue) + : saveItem(newValue) + } else { + deleteItem() + } + } + } + + public init(key: String, itemClass: ItemClass, keychain: KeychainManager = .standard) { + self.key = key + self.itemClass = itemClass + self.keychain = keychain + } +} + +// MARK: - Helpers +private extension KeychainStorage { + + func getItem() -> T? { + do { + return try keychain.retrieveItem(ofClass: itemClass, key: key) + } catch { + handleError(error) + } + return nil + } + + func saveItem(_ item: T) { + do { + try keychain.saveItem(item, itemClass: itemClass, key: key) + } catch { + handleError(error) + } + } + + func updateItem(_ item: T) { + do { + try keychain.updateItem(with: item, ofClass: itemClass, key: key) + } catch { + handleError(error) + } + } + + func deleteItem() { + do { + try keychain.deleteItem(ofClass: itemClass, key: key) + } catch { + handleError(error) + } + } + + func handleError(_ error: Error) { + print(error) + } +} diff --git a/Sources/KeychainUtility/KeychainUtils.swift b/Sources/KeychainUtility/KeychainUtils.swift new file mode 100644 index 0000000..700081f --- /dev/null +++ b/Sources/KeychainUtility/KeychainUtils.swift @@ -0,0 +1,19 @@ +// +// File.swift +// +// +// Created by Bruno Lorenzo on 5/10/23. +// + +import Foundation + +public typealias KeychainDictionary = [String : Any] +public typealias ItemAttributes = [CFString : Any] + +extension KeychainDictionary { + mutating func addAttributes(_ attributes: ItemAttributes) { + for(key, value) in attributes { + self[key as String] = value + } + } +} diff --git a/Tests/KeychainUtilityTests/PropertyWrapperTests.swift b/Tests/KeychainUtilityTests/PropertyWrapperTests.swift new file mode 100644 index 0000000..8da161b --- /dev/null +++ b/Tests/KeychainUtilityTests/PropertyWrapperTests.swift @@ -0,0 +1,47 @@ + +import Foundation +import XCTest +@testable import KeychainUtility + +final class PropertyWrapperTests: XCTestCase { + + @KeychainStorage(key: "API-Token", itemClass: .generic) + var token: String? + + override func setUp() { + super.setUp() + } + + override func tearDown() { + super.tearDown() + token = nil + } + + func testSaveItem() { + token = "c0c61d55558b0c8dac82a16c04981eea7c99e37d714367e575028221028b0d4cff122d6a7556fc0ab1c66d1d4b05b378" + let keychainToken: String? = try? KeychainManager.standard.retrieveItem(ofClass: .generic, key: "API-Token") + XCTAssertEqual(token, keychainToken) + } + + func testUpdateItem() { + token = "c0c61d55558b0c8dac82a16c04981eea7c99e37d714367e575028221028b0d4cff122d6a7556fc0ab1c66d1d4b05b378" + var keychainToken: String? = try? KeychainManager.standard.retrieveItem(ofClass: .generic, key: "API-Token") + XCTAssertEqual(token, keychainToken) + + token = "b7bb1d55558b0c8dac82a16c04981eea7c99e37d714367e575028221028b0d4cff122d6a7556fc0ab1c66d1d4b05b378" + XCTAssertNotEqual(token, keychainToken) + + keychainToken = try? KeychainManager.standard.retrieveItem(ofClass: .generic, key: "API-Token") + XCTAssertEqual(token, keychainToken) + } + + func testRemoveItem() { + token = "c0c61d55558b0c8dac82a16c04981eea7c99e37d714367e575028221028b0d4cff122d6a7556fc0ab1c66d1d4b05b378" + var keychainToken: String? = try? KeychainManager.standard.retrieveItem(ofClass: .generic, key: "API-Token") + XCTAssertEqual(token, keychainToken) + + token = nil + keychainToken = try? KeychainManager.standard.retrieveItem(ofClass: .generic, key: "API-Token") + XCTAssertNil(token) + } +}