Files
keychain-kit/Sources/KeychainKit/Protocols/KeychainStorageProtocol.swift
2025-11-12 19:40:08 +02:00

186 lines
6.4 KiB
Swift

import Foundation
/// A type that provides access to data stored in the keychain.
///
/// Conforming types define how items are encoded, saved, and accessed securely, using account and
/// service descriptors to identify individual entries.
///
/// ## Topics
///
/// ### Associated Types
///
/// - ``Account``
/// - ``Service``
///
/// ### Instance Properties
///
/// - ``service``
///
/// ### Retrieving Items
///
/// - ``get(by:)->Data?``
/// - ``get(by:)->String?``
/// - ``get(by:)->UUID?``
/// - ``get(by:decoder:)``
///
/// ### Inserting Items
///
/// - ``insert(_:by:)-(Data,_)``
/// - ``insert(_:by:)-(String,_)``
/// - ``insert(_:by:)-(UUID,_)``
/// - ``insert(_:by:encoder:)``
///
/// ### Deleting Items
///
/// - ``delete(by:)``
///
/// ### Checking Existence
///
/// - ``exists(by:)``
public protocol KeychainStorageProtocol: Sendable {
// MARK: - Types
/// A type that describes a keychain account used to identify stored items.
associatedtype Account: KeychainAccountProtocol
/// A type that describes a keychain service used to group stored items.
associatedtype Service: KeychainServiceProtocol
// MARK: - Properties
/// The keychain service associated with this storage instance.
var service: Service? { get }
// MARK: - Methods
/// Retrieves raw data for the given account.
///
/// - 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?
/// Inserts raw data for the given 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)
/// 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.
///
/// - 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<T: Decodable>(
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.
///
/// - 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)
}
/// Inserts a UUID for the given 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)
}
/// Encodes and inserts an `Encodable` value for the given 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<T: Encodable>(
_ value: T,
by account: Account,
encoder: JSONEncoder = .init()
) throws(KeychainError) {
let data: Data
do {
data = try encoder.encode(value)
} catch {
throw .underlying(error as NSError)
}
try insert(data, by: account)
}
}