diff --git a/.swift-format b/.swift-format deleted file mode 100644 index b77abe6..0000000 --- a/.swift-format +++ /dev/null @@ -1,15 +0,0 @@ -{ - "fileScopedDeclarationPrivacy": { - "accessLevel": "private" - }, - "indentBlankLines": true, - "indentation": { - "spaces": 4 - }, - "lineLength": 9999, - "maximumBlankLines": 1, - "multiElementCollectionTrailingCommas": false, - "rules": { - "FileScopedDeclarationPrivacy": true - } -} diff --git a/Package.swift b/Package.swift index 273919b..88859d3 100644 --- a/Package.swift +++ b/Package.swift @@ -1,28 +1,18 @@ -// swift-tools-version: 6.0 +// swift-tools-version: 5.10 // The swift-tools-version declares the minimum version of Swift required to build this package. import PackageDescription let package = Package( name: "KeychainKit", - defaultLocalization: "en", - platforms: [.macOS(.v12), .iOS(.v15)], + platforms: [.macOS(.v10_15), .iOS(.v13)], products: [ - .library(name: "KeychainKit", targets: ["KeychainKit"]) + .library(name: "KeychainKit", targets: ["KeychainKit"]), ], dependencies: [ - .package(url: "https://github.com/angd-dev/localizable.git", from: "1.0.0"), .package(url: "https://github.com/apple/swift-docc-plugin", from: "1.0.0") ], targets: [ - .target( - name: "KeychainKit", - dependencies: [ - .product(name: "Localizable", package: "localizable") - ], - resources: [ - .process("Resources/Localizable.xcstrings") - ] - ) + .target(name: "KeychainKit") ] ) diff --git a/README.md b/README.md index c73e91a..1888a5c 100644 --- a/README.md +++ b/README.md @@ -10,6 +10,11 @@ It supports optional authentication via `LAContext`, allowing integration with F KeychainKit does not hide the complexity of Keychain operations but provides a clean API and convenient error handling via a custom `KeychainError` type. +## Requirements + +- **Swift**: 5.10+ +- **Platforms**: macOS 10.15+, iOS 13.0+ + ## Installation To add KeychainKit to your project, use Swift Package Manager (SPM). @@ -19,7 +24,7 @@ To add KeychainKit to your project, use Swift Package Manager (SPM). 1. Open your project in Xcode. 2. Navigate to the `File` menu and select `Add Package Dependencies`. 3. Enter the repository URL: `https://github.com/angd-dev/keychain-kit.git` -4. Choose the version to install (e.g., `3.0.0`). +4. Choose the version to install (e.g., `2.1.0`). 5. Add the library to your target module. ### Adding to Package.swift @@ -33,7 +38,7 @@ import PackageDescription let package = Package( name: "YourProject", dependencies: [ - .package(url: "https://github.com/angd-dev/keychain-kit.git", from: "3.0.0") + .package(url: "https://github.com/angd-dev/keychain-kit.git", from: "2.1.0") ], targets: [ .target( @@ -48,7 +53,7 @@ let package = Package( ## Additional Resources -For more information and usage examples, see the [documentation](https://docs.angd.dev/?package=keychain-kit&version=3.0.0). +For more information and usage examples, see the [documentation](https://docs.angd.dev/?package=keychain-kit&version=2.1.0). ## License diff --git a/Sources/KeychainKit/Classes/KeychainStorage.swift b/Sources/KeychainKit/Classes/KeychainStorage.swift index 9788e58..687abba 100644 --- a/Sources/KeychainKit/Classes/KeychainStorage.swift +++ b/Sources/KeychainKit/Classes/KeychainStorage.swift @@ -2,11 +2,10 @@ import Foundation import LocalAuthentication import Security -/// A service that provides access and management for keychain items. +/// A type-safe storage abstraction over the Keychain service. /// -/// This type provides direct access to the system keychain using `Security` and -/// `LocalAuthentication` frameworks. It supports querying, inserting, deleting, and checking item -/// existence, while handling authentication contexts and access controls automatically. +/// Supports storing, retrieving, and deleting generic data associated with +/// accounts and services, with optional local authentication context support. /// /// ## Topics /// @@ -19,31 +18,36 @@ import Security /// - ``service`` /// - ``context`` /// -/// ### Instance Methods +/// ### Retrieving Values /// -/// - ``get(by:)->Data?`` -/// - ``insert(_:by:)-(Data,_)`` -/// - ``delete(by:)`` -/// - ``exists(by:)`` +/// - ``get(_:)`` +/// +/// ### Storing Values +/// +/// - ``set(_:for:)`` +/// +/// ### Deleting Values +/// +/// - ``delete(_:)`` public final class KeychainStorage< Account: KeychainAccountProtocol, Service: KeychainServiceProtocol ->: KeychainStorageProtocol, @unchecked Sendable { +>: KeychainStorageProtocol { // MARK: - Properties - /// The service descriptor associated with this keychain storage. + /// The service metadata associated with this Keychain storage instance. public let service: Service? - /// The authentication context used for keychain operations. + /// An optional local authentication context used for biometric or passcode protection. public let context: LAContext? - // MARK: - Initialization + // MARK: - Inits - /// Creates a new keychain storage instance. + /// Creates a new `KeychainStorage` instance with the given service and authentication context. /// /// - Parameters: - /// - service: The service descriptor that defines the keychain group and access settings. - /// - context: The authentication context used for secure access, or `nil` to use a default one. + /// - service: An optional `Service` instance representing the keychain service metadata. + /// - context: An optional `LAContext` instance for authentication protection. public init(service: Service?, context: LAContext?) { self.service = service self.context = context @@ -51,20 +55,23 @@ public final class KeychainStorage< // MARK: - Methods - /// Retrieves raw data for the given account. + /// Retrieves raw `Data` stored in the keychain for the specified account. /// - /// - Parameter account: The account descriptor identifying the stored item. - /// - Returns: The stored data, or `nil` if no item exists. - /// - Throws: ``KeychainError/invalidData`` if the retrieved value cannot be cast to `Data`. - /// - Throws: ``KeychainError/authenticationFailed`` if user authentication fails. - /// - Throws: ``KeychainError/osStatus(_:)`` for unexpected system errors. - public func get(by account: Account) throws(KeychainError) -> Data? { + /// - Parameter account: The account identifier used to locate the stored value. + /// - Returns: The raw data associated with the specified account. + /// + /// - Throws: ``KeychainError/itemNotFound`` if no matching item is found in the keychain. + /// - Throws: ``KeychainError/authenticationFailed`` if biometric or device authentication fails. + /// - Throws: ``KeychainError/unexpectedData`` if the retrieved data is missing or corrupted. + /// - Throws: ``KeychainError/unexpectedCode(_:)`` for any other unexpected OSStatus error. + public func get(_ account: Account) throws(KeychainError) -> Data { var query: [CFString: Any] = [ kSecClass: kSecClassGenericPassword, kSecAttrAccount: account.identifier, kSecAttrSynchronizable: account.synchronizable, kSecUseDataProtectionKeychain: true, kSecMatchLimit: kSecMatchLimitOne, + kSecReturnAttributes: true, kSecReturnData: true ] @@ -72,41 +79,50 @@ public final class KeychainStorage< query[kSecAttrAccessGroup] = service?.accessGroup query[kSecUseAuthenticationContext] = context - var result: AnyObject? + var queryResult: AnyObject? + let status = withUnsafeMutablePointer(to: &queryResult) { + SecItemCopyMatching(query as CFDictionary, UnsafeMutablePointer($0)) + } - switch SecItemCopyMatching(query as CFDictionary, &result) { + switch status { case errSecSuccess: - if let data = result as? Data { - return data - } else { - throw .invalidData - } + guard + let item = queryResult as? [CFString : AnyObject], + let data = item[kSecValueData] as? Data + else { throw KeychainError.unexpectedData } + return data case errSecItemNotFound: - return nil - case errSecAuthFailed, errSecInteractionNotAllowed, errSecUserCanceled: - throw .authenticationFailed - case let status: - throw .osStatus(status) + throw KeychainError.itemNotFound + case errSecAuthFailed: + throw KeychainError.authenticationFailed + default: + throw KeychainError.unexpectedCode(status) } } - /// Inserts raw data for the given account. + /// Stores raw `Data` in the keychain for the specified account, replacing any existing value. + /// + /// This method first deletes any existing keychain item for the account, then creates a new + /// item with the specified data and applies the access control settings from the account's + /// protection and flags. /// /// - Parameters: - /// - value: The data to store. - /// - account: The account descriptor identifying the target item. - /// - Throws: ``KeychainError/underlying(_:)`` if access control creation fails. - /// - Throws: ``KeychainError/duplicateItem`` if an item with the same key already exists. - /// - Throws: ``KeychainError/osStatus(_:)`` for unexpected system errors. - public func insert(_ value: Data, by account: Account) throws(KeychainError) { + /// - value: The raw data to store. + /// - account: The account identifier conforming to `KeychainAccountProtocol`. + /// + /// - Throws: ``KeychainError/unexpectedError(_:)`` if access control creation fails. + /// - Throws: ``KeychainError/unexpectedCode(_:)`` if adding the new item to the keychain fails. + /// - Throws: Any error thrown by ``delete(_:)`` if the existing item cannot be removed. + public func set(_ value: Data, for account: Account) throws(KeychainError) { + try delete(account) + var error: Unmanaged? let access = SecAccessControlCreateWithFlags( nil, account.protection, account.accessFlags, &error ) guard let access else { - let error = error?.takeRetainedValue() - throw .underlying(error as? NSError) + throw KeychainError.unexpectedError(error?.takeUnretainedValue()) } var query: [CFString: Any] = [ @@ -122,22 +138,19 @@ public final class KeychainStorage< query[kSecAttrAccessGroup] = service?.accessGroup query[kSecUseAuthenticationContext] = context - switch SecItemAdd(query as CFDictionary, nil) { - case errSecSuccess: - return - case errSecDuplicateItem: - throw .duplicateItem - case let status: - throw .osStatus(status) + let status = SecItemAdd(query as CFDictionary, nil) + guard status == noErr else { + throw KeychainError.unexpectedCode(status) } } - /// Deletes the item for the given account. + /// Deletes the keychain item associated with the specified account. /// - /// - Parameter account: The account descriptor identifying the item to remove. - /// - Throws: ``KeychainError/authenticationFailed`` if user authentication fails. - /// - Throws: ``KeychainError/osStatus(_:)`` for unexpected system errors. - public func delete(by account: Account) throws(KeychainError) { + /// If no item exists for the given account, this method completes silently without error. + /// + /// - Parameter account: The account identifier conforming to `KeychainAccountProtocol`. + /// - Throws: ``KeychainError/unexpectedCode(_:)`` if the deletion fails with an unexpected OSStatus. + public func delete(_ account: Account) throws(KeychainError) { var query: [CFString: Any] = [ kSecClass: kSecClassGenericPassword, kSecAttrAccount: account.identifier, @@ -149,45 +162,9 @@ public final class KeychainStorage< query[kSecAttrAccessGroup] = service?.accessGroup query[kSecUseAuthenticationContext] = context - switch SecItemDelete(query as CFDictionary) { - case errSecSuccess, errSecItemNotFound: - return - case errSecAuthFailed, errSecInteractionNotAllowed, errSecUserCanceled: - throw .authenticationFailed - case let status: - throw .osStatus(status) - } - } - - /// Checks whether an item exists for the given account. - /// - /// - Parameter account: The account descriptor identifying the stored item. - /// - Returns: `true` if the item exists; otherwise, `false`. - /// - Throws: ``KeychainError/osStatus(_:)`` for unexpected system errors. - public func exists(by account: Account) throws(KeychainError) -> Bool { - var query: [CFString: Any] = [ - kSecClass: kSecClassGenericPassword, - kSecAttrAccount: account.identifier, - kSecAttrSynchronizable: account.synchronizable, - kSecUseDataProtectionKeychain: true, - kSecMatchLimit: kSecMatchLimitOne, - kSecReturnData: false - ] - - let context = LAContext() - context.interactionNotAllowed = true - - query[kSecAttrService] = service?.identifier - query[kSecAttrAccessGroup] = service?.accessGroup - query[kSecUseAuthenticationContext] = context - - switch SecItemCopyMatching(query as CFDictionary, nil) { - case errSecSuccess, errSecAuthFailed, errSecInteractionNotAllowed: - return true - case errSecItemNotFound: - return false - case let status: - throw .osStatus(status) + let status = SecItemDelete(query as CFDictionary) + guard status == errSecSuccess || status == errSecItemNotFound else { + throw KeychainError.unexpectedCode(status) } } } diff --git a/Sources/KeychainKit/Enums/KeychainError.swift b/Sources/KeychainKit/Enums/KeychainError.swift index aa48d18..fb416fd 100644 --- a/Sources/KeychainKit/Enums/KeychainError.swift +++ b/Sources/KeychainKit/Enums/KeychainError.swift @@ -1,44 +1,15 @@ import Foundation -/// An error that represents a keychain operation failure. -/// -/// Each case corresponds to a specific system or data error encountered while performing keychain -/// operations. -public enum KeychainError: Error, Equatable { - /// Authentication was required but failed or was canceled. +/// Errors that can occur during Keychain operations. +public enum KeychainError: Error { + /// Authentication failed, e.g., due to biometric or passcode denial. case authenticationFailed - - /// An item with the same key already exists in the keychain. - case duplicateItem - - /// The stored or retrieved data has an invalid format. - case invalidData - - /// An unexpected system status code was returned. - /// - /// - Parameter status: The underlying `OSStatus` value. - case osStatus(OSStatus) - - /// A lower-level error occurred during encoding, decoding, or other processing. - /// - /// - Parameter error: The underlying Foundation error, if available. - case underlying(NSError?) - - /// A localized, human-readable description of the error. - public var localizedDescription: String { - switch self { - case .authenticationFailed: - return .Error.authenticationFailed - case .duplicateItem: - return .Error.duplicateItem - case .invalidData: - return .Error.invalidData - case .osStatus(let status): - let message = SecCopyErrorMessageString(status, nil) - return .Error.osStatus(message as? String ?? "") - case .underlying(let error): - let message = error?.localizedDescription - return .Error.underlying(message ?? "") - } - } + /// No item found matching the query. + case itemNotFound + /// Unexpected or corrupted data found in Keychain item. + case unexpectedData + /// An unexpected OSStatus error code returned by Keychain API. + case unexpectedCode(OSStatus) + /// A generic unexpected error, with optional underlying error info. + case unexpectedError(Error?) } diff --git a/Sources/KeychainKit/Extensions/String+Error.swift b/Sources/KeychainKit/Extensions/String+Error.swift deleted file mode 100644 index 687d7db..0000000 --- a/Sources/KeychainKit/Extensions/String+Error.swift +++ /dev/null @@ -1,15 +0,0 @@ -import Foundation -import Localizable - -extension String { - @Localizable(bundle: .module) - enum Error { - private enum Strings { - case authenticationFailed - case duplicateItem - case invalidData - case osStatus(String) - case underlying(String) - } - } -} diff --git a/Sources/KeychainKit/Protocols/KeychainAccountProtocol.swift b/Sources/KeychainKit/Protocols/KeychainAccountProtocol.swift index 67b1da1..0201eb4 100644 --- a/Sources/KeychainKit/Protocols/KeychainAccountProtocol.swift +++ b/Sources/KeychainKit/Protocols/KeychainAccountProtocol.swift @@ -1,53 +1,47 @@ import Foundation -/// A type that describes a keychain account configuration for secure item storage and access. +/// A protocol that defines the required properties for a keychain account descriptor. /// -/// Conforming types define metadata that determines how the keychain protects, authenticates, and -/// optionally synchronizes specific items. -/// -/// ## Topics -/// -/// ### Properties -/// -/// - ``identifier`` -/// - ``protection`` -/// - ``accessFlags`` -/// - ``synchronizable`` -public protocol KeychainAccountProtocol: Sendable { - /// A unique string that identifies the keychain account. +/// Types conforming to this protocol provide metadata for configuring secure storage +/// and access behavior for keychain items. +public protocol KeychainAccountProtocol { + /// A unique string used to identify the keychain account. var identifier: String { get } - /// The keychain data protection level assigned to the account. + /// The keychain data protection level for the account. /// - /// Defaults to `kSecAttrAccessibleAfterFirstUnlock`. You can override this to use another - /// accessibility option, such as `kSecAttrAccessibleWhenUnlocked` or - /// `kSecAttrAccessibleWhenPasscodeSetThisDeviceOnly`. + /// Defaults to `kSecAttrAccessibleAfterFirstUnlock`. You may override it to use other + /// accessibility levels, such as `kSecAttrAccessibleWhenUnlocked` + /// or `kSecAttrAccessibleWhenPasscodeSetThisDeviceOnly`. var protection: CFString { get } - /// The access control flags defining additional authentication requirements. + /// The access control flags used to define authentication requirements. /// - /// Defaults to an empty set (`[]`). Override this to enforce constraints like `.userPresence`, - /// `.biometryAny`, or `.devicePasscode`. + /// Defaults to `[]` (no additional access control). Can be overridden to specify + /// constraints such as `.userPresence`, `.biometryAny`, or `.devicePasscode`. var accessFlags: SecAccessControlCreateFlags { get } - /// Indicates whether the item is synchronized through iCloud Keychain. + /// Whether the item should be marked as synchronizable via iCloud Keychain. /// - /// Defaults to `false`. Set this to `true` if the item should be available across all devices - /// associated with the same iCloud account. + /// Defaults to `false`. Set to `true` if the item should sync across devices. var synchronizable: Bool { get } } public extension KeychainAccountProtocol { + /// Default value for `protection`: accessible after first unlock. var protection: CFString { kSecAttrAccessibleAfterFirstUnlock } + /// Default value for `accessFlags`: no access control constraints. var accessFlags: SecAccessControlCreateFlags { [] } + /// Default value for `synchronizable`: not synchronized across devices. var synchronizable: Bool { false } } public extension KeychainAccountProtocol where Self: RawRepresentable, Self.RawValue == String { - /// A unique string that identifies the keychain account. + /// Provides a default `identifier` implementation for `RawRepresentable` types + /// whose `RawValue` is `String`. /// - /// Derived from the instance’s raw string value. + /// The `identifier` is derived from the raw string value. var identifier: String { rawValue } } diff --git a/Sources/KeychainKit/Protocols/KeychainServiceProtocol.swift b/Sources/KeychainKit/Protocols/KeychainServiceProtocol.swift index ab1af9a..ae30994 100644 --- a/Sources/KeychainKit/Protocols/KeychainServiceProtocol.swift +++ b/Sources/KeychainKit/Protocols/KeychainServiceProtocol.swift @@ -1,33 +1,28 @@ import Foundation -/// A type that describes a keychain service used to group and identify stored items. +/// A protocol that defines the required properties for a keychain service descriptor. /// -/// Conforming types define a unique service identifier and may optionally specify an access group -/// for sharing keychain data between multiple apps or extensions. -/// -/// ## Topics -/// -/// ### Properties -/// -/// - ``identifier`` -/// - ``accessGroup`` -public protocol KeychainServiceProtocol: Sendable { - /// A unique string that identifies the keychain service. +/// Types conforming to this protocol provide an identifier used to distinguish stored items +/// and may optionally specify an access group to enable keychain sharing between apps. +public protocol KeychainServiceProtocol { + /// A unique string used to identify the keychain service. var identifier: String { get } - - /// An optional keychain access group identifier that enables shared access between apps. + + /// An optional keychain access group identifier to support shared access between apps. /// - /// Defaults to `nil`, meaning no access group is specified. + /// The default implementation returns `nil`, indicating no access group is specified. var accessGroup: String? { get } } public extension KeychainServiceProtocol { + /// The default implementation returns `nil`, indicating that no access group is specified. var accessGroup: String? { nil } } public extension KeychainServiceProtocol where Self: RawRepresentable, Self.RawValue == String { - /// A unique string that identifies the keychain service. + /// Provides a default `identifier` implementation for `RawRepresentable` types + /// whose `RawValue` is `String`. /// - /// Derived from the instance’s raw string value. + /// The `identifier` is derived from the raw string value. var identifier: String { rawValue } } diff --git a/Sources/KeychainKit/Protocols/KeychainStorageProtocol.swift b/Sources/KeychainKit/Protocols/KeychainStorageProtocol.swift index bf22753..1f7c27a 100644 --- a/Sources/KeychainKit/Protocols/KeychainStorageProtocol.swift +++ b/Sources/KeychainKit/Protocols/KeychainStorageProtocol.swift @@ -1,9 +1,13 @@ import Foundation -/// A type that provides access to data stored in the keychain. +/// A protocol that defines a type-safe interface for storing and retrieving values +/// in the system keychain. /// -/// Conforming types define how items are encoded, saved, and accessed securely, using account and -/// service descriptors to identify individual entries. +/// This protocol provides generic support for `Data`, `String`, `UUID`, and `Codable` types. +/// It allows configuring the associated account and service context for each operation. +/// +/// Types conforming to this protocol must specify concrete types for `Account` +/// and `Service`, which describe keychain item identity and service grouping. /// /// ## Topics /// @@ -16,170 +20,205 @@ import Foundation /// /// - ``service`` /// -/// ### Retrieving Items +/// ### Retrieving Values /// -/// - ``get(by:)->Data?`` -/// - ``get(by:)->String?`` -/// - ``get(by:)->UUID?`` -/// - ``get(by:decoder:)`` +/// - ``get(_:)-2gcee`` +/// - ``get(_:)-23z7h`` +/// - ``get(_:)-4xbe6`` +/// - ``get(_:decoder:)`` /// -/// ### Inserting Items +/// ### Storing Values /// -/// - ``insert(_:by:)-(Data,_)`` -/// - ``insert(_:by:)-(String,_)`` -/// - ``insert(_:by:)-(UUID,_)`` -/// - ``insert(_:by:encoder:)`` +/// - ``set(_:for:)-21dla`` +/// - ``set(_:for:)-6nzkf`` +/// - ``set(_:for:)-2smpc`` +/// - ``set(_:for:encoder:)`` /// -/// ### Deleting Items +/// ### Deleting Values /// -/// - ``delete(by:)`` -/// -/// ### Checking Existence -/// -/// - ``exists(by:)`` -public protocol KeychainStorageProtocol: Sendable { - // MARK: - Types - - /// A type that describes a keychain account used to identify stored items. +/// - ``delete(_:)`` +public protocol KeychainStorageProtocol { + /// A type that describes a keychain account and its security configuration. associatedtype Account: KeychainAccountProtocol - /// A type that describes a keychain service used to group stored items. + /// A type that identifies a keychain service context (e.g., app or subsystem). associatedtype Service: KeychainServiceProtocol - // MARK: - Properties - - /// The keychain service associated with this storage instance. + /// The service associated with this keychain storage instance. + /// + /// This value is used as the `kSecAttrService` when interacting with the keychain. + /// If `nil`, the default service behavior is used. var service: Service? { get } - // MARK: - Methods - - /// Retrieves raw data for the given account. + /// Retrieves the value stored in the keychain for the specified account as raw `Data`. /// - /// - Parameter account: The account descriptor identifying the stored item. - /// - Returns: The stored data, or `nil` if no item exists. - /// - Throws: ``KeychainError`` if the operation fails. - func get(by account: Account) throws(KeychainError) -> Data? + /// - Parameter account: The keychain account whose value should be retrieved. + /// - Returns: The data associated with the given account. + /// - Throws: An error if the item is not found, access is denied, or another keychain error occurs. + func get(_ account: Account) throws(KeychainError) -> Data - /// Inserts raw data for the given account. + /// Retrieves the value stored in the keychain for the specified account as a UTF-8 string. + /// + /// - Parameter account: The keychain account whose value should be retrieved. + /// - Returns: A string decoded from the stored data using UTF-8 encoding. + /// - Throws: An error if the item is not found, the data is not valid UTF-8, + /// or a keychain access error occurs. + func get(_ account: Account) throws(KeychainError) -> String + + /// Retrieves the value stored in the keychain for the specified account as a `UUID`. + /// + /// - Parameter account: The keychain account whose value should be retrieved. + /// - Returns: A UUID decoded from a 16-byte binary representation stored in the keychain. + /// - Throws: An error if the item is not found, the data is not exactly 16 bytes, + /// or a keychain access error occurs. + func get(_ account: Account) throws(KeychainError) -> UUID + + /// Retrieves and decodes a value of type `T` stored in the keychain for the specified account. /// /// - Parameters: - /// - value: The data to store. - /// - account: The account descriptor identifying the target item. - /// - Throws: ``KeychainError`` if the operation fails. - func insert(_ value: Data, by account: Account) throws(KeychainError) + /// - account: The keychain account whose value should be retrieved. + /// - decoder: The `JSONDecoder` instance used to decode the stored data. + /// - Returns: A decoded instance of type `T`. + /// - Throws: An error if the item is not found, decoding fails, or a keychain access error occurs. + func get(_ account: Account, decoder: JSONDecoder) throws(KeychainError) -> T - /// Deletes the item for the given account. - /// - /// - Parameter account: The account descriptor identifying the item to remove. - /// - Throws: ``KeychainError`` if the operation fails. - func delete(by account: Account) throws(KeychainError) - - /// Checks whether an item exists for the given account. - /// - /// - Parameter account: The account descriptor identifying the stored item. - /// - Returns: `true` if the item exists; otherwise, `false`. - /// - Throws: ``KeychainError`` if the check fails. - func exists(by account: Account) throws(KeychainError) -> Bool -} - -// MARK: - Get Extension - -public extension KeychainStorageProtocol { - /// Retrieves a UTF-8 string for the given account. - /// - /// - Parameter account: The account descriptor identifying the stored item. - /// - Returns: The decoded string, or `nil` if no item exists. - /// - Throws: ``KeychainError`` if retrieval fails. - /// - Throws: ``KeychainError/invalidData`` if the stored data cannot be decoded as UTF-8. - func get(by account: Account) throws(KeychainError) -> String? { - guard let data = try get(by: account) else { return nil } - guard let string = String(data: data, encoding: .utf8) else { - throw .invalidData - } - return string - } - - /// Retrieves a UUID for the given account. - /// - /// - Parameter account: The account descriptor identifying the stored item. - /// - Returns: The decoded UUID, or `nil` if no item exists. - /// - Throws: ``KeychainError`` if retrieval fails. - /// - Throws: ``KeychainError/invalidData`` if the stored value is not a valid UUID string. - func get(by account: Account) throws(KeychainError) -> UUID? { - guard let string: String = try get(by: account) else { return nil } - guard let uuid = UUID(uuidString: string) else { - throw .invalidData - } - return uuid - } - - /// Retrieves and decodes a `Decodable` value for the given account. + /// Stores raw `Data` in the keychain for the specified account. /// /// - Parameters: - /// - account: The account descriptor identifying the stored item. - /// - decoder: The JSON decoder used to decode the stored data. - /// - Returns: The decoded value, or `nil` if no item exists. - /// - Throws: ``KeychainError`` if retrieval fails. - /// - Throws: ``KeychainError/underlying(_:)`` if JSON decoding fails. - func get( - by account: Account, - decoder: JSONDecoder = .init() - ) throws(KeychainError) -> T? { - guard let data = try get(by: account) else { return nil } - do { - return try decoder.decode(T.self, from: data) - } catch { - throw .underlying(error as NSError) - } - } -} - -// MARK: - Set Extension - -public extension KeychainStorageProtocol { - /// Inserts a UTF-8 string for the given account. + /// - value: The data to store in the keychain. + /// - account: The keychain account under which the data will be saved. + /// - Throws: An error if storing the data fails. + func set(_ value: Data, for account: Account) throws(KeychainError) + + /// Stores a UTF-8 encoded `String` in the keychain for the specified account. /// /// - Parameters: - /// - value: The string to store. - /// - account: The account descriptor identifying the target item. - /// - Throws: ``KeychainError`` if the operation fails. - /// - Throws: ``KeychainError/invalidData`` if the string cannot be encoded as UTF-8. - func insert(_ value: String, by account: Account) throws(KeychainError) { - guard let data = value.data(using: .utf8) else { - throw .invalidData - } - try insert(data, by: account) - } + /// - value: The string to store in the keychain. + /// - account: The keychain account under which the string will be saved. + /// - Throws: An error if storing the string fails. + func set(_ value: String, for account: Account) throws(KeychainError) - /// Inserts a UUID for the given account. + /// Stores a `UUID` in the keychain for the specified account. /// /// - Parameters: - /// - value: The UUID to store. - /// - account: The account descriptor identifying the target item. - /// - Throws: ``KeychainError`` if the operation fails. - func insert(_ value: UUID, by account: Account) throws(KeychainError) { - try insert(value.uuidString, by: account) - } + /// - value: The UUID to store in the keychain (stored in 16-byte binary format). + /// - account: The keychain account under which the UUID will be saved. + /// - Throws: An error if storing the UUID fails. + func set(_ value: UUID, for account: Account) throws(KeychainError) - /// Encodes and inserts an `Encodable` value for the given account. + /// Encodes and stores a value of type `T` in the keychain for the specified account. /// /// - Parameters: /// - value: The value to encode and store. - /// - account: The account descriptor identifying the target item. - /// - encoder: The JSON encoder used to encode the value. - /// - Throws: ``KeychainError`` if the operation fails. - /// - Throws: ``KeychainError/underlying(_:)`` if JSON encoding fails. - func insert( + /// - account: The keychain account under which the encoded data will be saved. + /// - encoder: The `JSONEncoder` used to encode the value. + /// - Throws: An error if encoding or storing the value fails. + func set(_ value: T, for account: Account, encoder: JSONEncoder) throws(KeychainError) + + /// Deletes the keychain item associated with the specified account. + /// + /// - Parameter account: The keychain account whose stored value should be deleted. + /// - Note: If the item does not exist, the method completes silently without error. + /// - Throws: An error only if the item exists but removal fails. + func delete(_ account: Account) throws(KeychainError) +} + +public extension KeychainStorageProtocol { + /// Retrieves a UTF-8 encoded string stored in the keychain for the specified account. + /// + /// - Parameter account: The account identifier used to locate the stored value. + /// - Returns: A string decoded from the keychain data using UTF-8 encoding. + /// - Throws: ``KeychainError/unexpectedData`` if the data cannot be decoded as UTF-8. + /// - Throws: Any error thrown by ``KeychainStorageProtocol/get(_:)-2gcee`` + /// if reading the raw data fails. + func get(_ account: Account) throws(KeychainError) -> String { + guard let value = String(data: try get(account), encoding: .utf8) else { + throw KeychainError.unexpectedData + } + return value + } + + /// Retrieves a `UUID` stored in the keychain for the specified account. + /// + /// - Parameter account: The account identifier used to locate the stored value. + /// - Returns: A UUID decoded from the keychain string. + /// - Throws: ``KeychainError/unexpectedData`` if the stored string is missing or invalid. + /// - Throws: Any error thrown by ``KeychainStorageProtocol/get(_:)-23z7h`` + /// if reading the string from the keychain fails. + func get(_ account: Account) throws(KeychainError) -> UUID { + guard let value = UUID(uuidString: try get(account)) else { + throw KeychainError.unexpectedData + } + return value + } + + /// Retrieves a value of type `T` stored in the keychain and decodes it from JSON using the given decoder. + /// + /// - Parameters: + /// - account: The account identifier used to locate the stored value. + /// - decoder: The `JSONDecoder` to use for decoding. Defaults to a new instance. + /// - Returns: A decoded instance of type `T`. + /// - Throws: ``KeychainError/unexpectedError(_:)`` if the data cannot be decoded into the specified type. + /// - Throws: Any error thrown by ``KeychainStorageProtocol/get(_:)-2gcee`` if reading the raw data fails. + func get( + _ account: Account, + decoder: JSONDecoder = .init() + ) throws(KeychainError) -> T { + let value: Data = try get(account) + do { + return try decoder.decode(T.self, from: value) + } catch { + throw KeychainError.unexpectedError(error) + } + } + + /// Stores a UTF-8 encoded string in the keychain for the specified account. + /// + /// - Parameters: + /// - value: The string to store. + /// - account: The account identifier used as the key for storing the value. + /// - Throws: ``KeychainError/unexpectedData`` if the string cannot be encoded as UTF-8. + /// - Throws: Any error thrown by ``KeychainStorageProtocol/set(_:for:)-21dla`` + /// if saving the data fails. + func set(_ value: String, for account: Account) throws(KeychainError) { + guard let data = value.data(using: .utf8) else { + throw KeychainError.unexpectedData + } + try set(data, for: account) + } + + /// Stores a `UUID` value as a UTF-8 encoded string in the keychain for the specified account. + /// + /// - Parameters: + /// - value: The UUID to store. + /// - account: The account identifier used as the key for storing the value. + /// - Throws: Any error thrown by ``KeychainStorageProtocol/set(_:for:)-6nzkf`` + /// if saving the data fails. + func set(_ value: UUID, for account: Account) throws(KeychainError) { + try set(value.uuidString, for: account) + } + + /// Stores an `Encodable` value in the keychain as JSON-encoded data for the specified account. + /// + /// - Parameters: + /// - value: The value to encode and store. + /// - account: The account identifier used as the key for storing the value. + /// - encoder: The JSON encoder to use (default is a new instance). + /// - Throws: ``KeychainError/unexpectedError(_:)`` if encoding the value fails. + /// - Throws: Any error thrown by ``KeychainStorageProtocol/set(_:for:)-21dla`` + /// if saving the data fails. + func set( _ value: T, - by account: Account, + for account: Account, encoder: JSONEncoder = .init() ) throws(KeychainError) { - let data: Data do { - data = try encoder.encode(value) + let data = try encoder.encode(value) + try set(data, for: account) + } catch let error as KeychainError { + throw error } catch { - throw .underlying(error as NSError) + throw KeychainError.unexpectedError(error) } - try insert(data, by: account) } } diff --git a/Sources/KeychainKit/Resources/Localizable.xcstrings b/Sources/KeychainKit/Resources/Localizable.xcstrings deleted file mode 100644 index 44446a7..0000000 --- a/Sources/KeychainKit/Resources/Localizable.xcstrings +++ /dev/null @@ -1,56 +0,0 @@ -{ - "sourceLanguage" : "en", - "strings" : { - "Error.authenticationFailed" : { - "localizations" : { - "en" : { - "stringUnit" : { - "state" : "translated", - "value" : "Authentication failed" - } - } - } - }, - "Error.duplicateItem" : { - "localizations" : { - "en" : { - "stringUnit" : { - "state" : "translated", - "value" : "Item already exists" - } - } - } - }, - "Error.invalidData" : { - "localizations" : { - "en" : { - "stringUnit" : { - "state" : "translated", - "value" : "Stored item contains invalid or unexpected data" - } - } - } - }, - "Error.osStatus %@" : { - "localizations" : { - "en" : { - "stringUnit" : { - "state" : "translated", - "value" : "Unexpected Keychain status: %@" - } - } - } - }, - "Error.underlying %@" : { - "localizations" : { - "en" : { - "stringUnit" : { - "state" : "translated", - "value" : "Unexpected error while working with Keychain: %@" - } - } - } - } - }, - "version" : "1.0" -} \ No newline at end of file