diff --git a/KeychainKit.xcodeproj/KeychainKit_Info.plist b/KeychainKit.xcodeproj/KeychainKit_Info.plist
deleted file mode 100644
index 57ada9f..0000000
--- a/KeychainKit.xcodeproj/KeychainKit_Info.plist
+++ /dev/null
@@ -1,25 +0,0 @@
-
-
-
- CFBundleDevelopmentRegion
- en
- CFBundleExecutable
- $(EXECUTABLE_NAME)
- CFBundleIdentifier
- $(PRODUCT_BUNDLE_IDENTIFIER)
- CFBundleInfoDictionaryVersion
- 6.0
- CFBundleName
- $(PRODUCT_NAME)
- CFBundlePackageType
- FMWK
- CFBundleShortVersionString
- 1.0
- CFBundleSignature
- ????
- CFBundleVersion
- $(CURRENT_PROJECT_VERSION)
- NSPrincipalClass
-
-
-
diff --git a/KeychainKit.xcodeproj/project.pbxproj b/KeychainKit.xcodeproj/project.pbxproj
deleted file mode 100644
index 2b99a90..0000000
--- a/KeychainKit.xcodeproj/project.pbxproj
+++ /dev/null
@@ -1,383 +0,0 @@
-// !$*UTF8*$!
-{
- archiveVersion = "1";
- objectVersion = "46";
- objects = {
- "KeychainKit::KeychainKit" = {
- isa = "PBXNativeTarget";
- buildConfigurationList = "OBJ_16";
- buildPhases = (
- "OBJ_19",
- "OBJ_21"
- );
- dependencies = (
- );
- name = "KeychainKit";
- productName = "KeychainKit";
- productReference = "KeychainKit::KeychainKit::Product";
- productType = "com.apple.product-type.framework";
- };
- "KeychainKit::KeychainKit::Product" = {
- isa = "PBXFileReference";
- path = "KeychainKit.framework";
- sourceTree = "BUILT_PRODUCTS_DIR";
- };
- "KeychainKit::SwiftPMPackageDescription" = {
- isa = "PBXNativeTarget";
- buildConfigurationList = "OBJ_23";
- buildPhases = (
- "OBJ_26"
- );
- dependencies = (
- );
- name = "KeychainKitPackageDescription";
- productName = "KeychainKitPackageDescription";
- productType = "com.apple.product-type.framework";
- };
- "OBJ_1" = {
- isa = "PBXProject";
- attributes = {
- LastSwiftMigration = "9999";
- LastUpgradeCheck = "9999";
- };
- buildConfigurationList = "OBJ_2";
- compatibilityVersion = "Xcode 3.2";
- developmentRegion = "en";
- hasScannedForEncodings = "0";
- knownRegions = (
- "en"
- );
- mainGroup = "OBJ_5";
- productRefGroup = "OBJ_11";
- projectDirPath = ".";
- targets = (
- "KeychainKit::KeychainKit",
- "KeychainKit::SwiftPMPackageDescription"
- );
- };
- "OBJ_10" = {
- isa = "PBXGroup";
- children = (
- );
- name = "Tests";
- path = "";
- sourceTree = "SOURCE_ROOT";
- };
- "OBJ_11" = {
- isa = "PBXGroup";
- children = (
- "KeychainKit::KeychainKit::Product"
- );
- name = "Products";
- path = "";
- sourceTree = "BUILT_PRODUCTS_DIR";
- };
- "OBJ_13" = {
- isa = "PBXFileReference";
- path = "LICENSE";
- sourceTree = "";
- };
- "OBJ_14" = {
- isa = "PBXFileReference";
- path = "README.md";
- sourceTree = "";
- };
- "OBJ_16" = {
- isa = "XCConfigurationList";
- buildConfigurations = (
- "OBJ_17",
- "OBJ_18"
- );
- defaultConfigurationIsVisible = "0";
- defaultConfigurationName = "Release";
- };
- "OBJ_17" = {
- isa = "XCBuildConfiguration";
- buildSettings = {
- ENABLE_TESTABILITY = "YES";
- FRAMEWORK_SEARCH_PATHS = (
- "$(inherited)",
- "$(PLATFORM_DIR)/Developer/Library/Frameworks"
- );
- HEADER_SEARCH_PATHS = (
- "$(inherited)"
- );
- INFOPLIST_FILE = "KeychainKit.xcodeproj/KeychainKit_Info.plist";
- IPHONEOS_DEPLOYMENT_TARGET = "8.0";
- LD_RUNPATH_SEARCH_PATHS = (
- "$(inherited)",
- "$(TOOLCHAIN_DIR)/usr/lib/swift/macosx"
- );
- MACOSX_DEPLOYMENT_TARGET = "10.10";
- OTHER_CFLAGS = (
- "$(inherited)"
- );
- OTHER_LDFLAGS = (
- "$(inherited)"
- );
- OTHER_SWIFT_FLAGS = (
- "$(inherited)"
- );
- PRODUCT_BUNDLE_IDENTIFIER = "KeychainKit";
- PRODUCT_MODULE_NAME = "$(TARGET_NAME:c99extidentifier)";
- PRODUCT_NAME = "$(TARGET_NAME:c99extidentifier)";
- SKIP_INSTALL = "YES";
- SWIFT_ACTIVE_COMPILATION_CONDITIONS = (
- "$(inherited)"
- );
- SWIFT_VERSION = "5.0";
- TARGET_NAME = "KeychainKit";
- TVOS_DEPLOYMENT_TARGET = "9.0";
- WATCHOS_DEPLOYMENT_TARGET = "2.0";
- };
- name = "Debug";
- };
- "OBJ_18" = {
- isa = "XCBuildConfiguration";
- buildSettings = {
- ENABLE_TESTABILITY = "YES";
- FRAMEWORK_SEARCH_PATHS = (
- "$(inherited)",
- "$(PLATFORM_DIR)/Developer/Library/Frameworks"
- );
- HEADER_SEARCH_PATHS = (
- "$(inherited)"
- );
- INFOPLIST_FILE = "KeychainKit.xcodeproj/KeychainKit_Info.plist";
- IPHONEOS_DEPLOYMENT_TARGET = "8.0";
- LD_RUNPATH_SEARCH_PATHS = (
- "$(inherited)",
- "$(TOOLCHAIN_DIR)/usr/lib/swift/macosx"
- );
- MACOSX_DEPLOYMENT_TARGET = "10.10";
- OTHER_CFLAGS = (
- "$(inherited)"
- );
- OTHER_LDFLAGS = (
- "$(inherited)"
- );
- OTHER_SWIFT_FLAGS = (
- "$(inherited)"
- );
- PRODUCT_BUNDLE_IDENTIFIER = "KeychainKit";
- PRODUCT_MODULE_NAME = "$(TARGET_NAME:c99extidentifier)";
- PRODUCT_NAME = "$(TARGET_NAME:c99extidentifier)";
- SKIP_INSTALL = "YES";
- SWIFT_ACTIVE_COMPILATION_CONDITIONS = (
- "$(inherited)"
- );
- SWIFT_VERSION = "5.0";
- TARGET_NAME = "KeychainKit";
- TVOS_DEPLOYMENT_TARGET = "9.0";
- WATCHOS_DEPLOYMENT_TARGET = "2.0";
- };
- name = "Release";
- };
- "OBJ_19" = {
- isa = "PBXSourcesBuildPhase";
- files = (
- "OBJ_20"
- );
- };
- "OBJ_2" = {
- isa = "XCConfigurationList";
- buildConfigurations = (
- "OBJ_3",
- "OBJ_4"
- );
- defaultConfigurationIsVisible = "0";
- defaultConfigurationName = "Release";
- };
- "OBJ_20" = {
- isa = "PBXBuildFile";
- fileRef = "OBJ_9";
- };
- "OBJ_21" = {
- isa = "PBXFrameworksBuildPhase";
- files = (
- );
- };
- "OBJ_23" = {
- isa = "XCConfigurationList";
- buildConfigurations = (
- "OBJ_24",
- "OBJ_25"
- );
- defaultConfigurationIsVisible = "0";
- defaultConfigurationName = "Release";
- };
- "OBJ_24" = {
- isa = "XCBuildConfiguration";
- buildSettings = {
- LD = "/usr/bin/true";
- OTHER_SWIFT_FLAGS = (
- "-swift-version",
- "5",
- "-I",
- "$(TOOLCHAIN_DIR)/usr/lib/swift/pm/4_2",
- "-target",
- "x86_64-apple-macosx10.10",
- "-sdk",
- "/Users/mr.noone/Applications/Xcode.app/Contents/Developer/Platforms/MacOSX.platform/Developer/SDKs/MacOSX10.15.sdk",
- "-package-description-version",
- "5.2.0"
- );
- SWIFT_VERSION = "5.0";
- };
- name = "Debug";
- };
- "OBJ_25" = {
- isa = "XCBuildConfiguration";
- buildSettings = {
- LD = "/usr/bin/true";
- OTHER_SWIFT_FLAGS = (
- "-swift-version",
- "5",
- "-I",
- "$(TOOLCHAIN_DIR)/usr/lib/swift/pm/4_2",
- "-target",
- "x86_64-apple-macosx10.10",
- "-sdk",
- "/Users/mr.noone/Applications/Xcode.app/Contents/Developer/Platforms/MacOSX.platform/Developer/SDKs/MacOSX10.15.sdk",
- "-package-description-version",
- "5.2.0"
- );
- SWIFT_VERSION = "5.0";
- };
- name = "Release";
- };
- "OBJ_26" = {
- isa = "PBXSourcesBuildPhase";
- files = (
- "OBJ_27"
- );
- };
- "OBJ_27" = {
- isa = "PBXBuildFile";
- fileRef = "OBJ_6";
- };
- "OBJ_3" = {
- isa = "XCBuildConfiguration";
- buildSettings = {
- CLANG_ENABLE_OBJC_ARC = "YES";
- COMBINE_HIDPI_IMAGES = "YES";
- COPY_PHASE_STRIP = "NO";
- DEBUG_INFORMATION_FORMAT = "dwarf";
- DYLIB_INSTALL_NAME_BASE = "@rpath";
- ENABLE_NS_ASSERTIONS = "YES";
- GCC_OPTIMIZATION_LEVEL = "0";
- GCC_PREPROCESSOR_DEFINITIONS = (
- "$(inherited)",
- "SWIFT_PACKAGE=1",
- "DEBUG=1"
- );
- MACOSX_DEPLOYMENT_TARGET = "10.10";
- ONLY_ACTIVE_ARCH = "YES";
- OTHER_SWIFT_FLAGS = (
- "$(inherited)",
- "-DXcode"
- );
- PRODUCT_NAME = "$(TARGET_NAME)";
- SDKROOT = "macosx";
- SUPPORTED_PLATFORMS = (
- "macosx",
- "iphoneos",
- "iphonesimulator",
- "appletvos",
- "appletvsimulator",
- "watchos",
- "watchsimulator"
- );
- SWIFT_ACTIVE_COMPILATION_CONDITIONS = (
- "$(inherited)",
- "SWIFT_PACKAGE",
- "DEBUG"
- );
- SWIFT_OPTIMIZATION_LEVEL = "-Onone";
- USE_HEADERMAP = "NO";
- };
- name = "Debug";
- };
- "OBJ_4" = {
- isa = "XCBuildConfiguration";
- buildSettings = {
- CLANG_ENABLE_OBJC_ARC = "YES";
- COMBINE_HIDPI_IMAGES = "YES";
- COPY_PHASE_STRIP = "YES";
- DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym";
- DYLIB_INSTALL_NAME_BASE = "@rpath";
- GCC_OPTIMIZATION_LEVEL = "s";
- GCC_PREPROCESSOR_DEFINITIONS = (
- "$(inherited)",
- "SWIFT_PACKAGE=1"
- );
- MACOSX_DEPLOYMENT_TARGET = "10.10";
- OTHER_SWIFT_FLAGS = (
- "$(inherited)",
- "-DXcode"
- );
- PRODUCT_NAME = "$(TARGET_NAME)";
- SDKROOT = "macosx";
- SUPPORTED_PLATFORMS = (
- "macosx",
- "iphoneos",
- "iphonesimulator",
- "appletvos",
- "appletvsimulator",
- "watchos",
- "watchsimulator"
- );
- SWIFT_ACTIVE_COMPILATION_CONDITIONS = (
- "$(inherited)",
- "SWIFT_PACKAGE"
- );
- SWIFT_OPTIMIZATION_LEVEL = "-Owholemodule";
- USE_HEADERMAP = "NO";
- };
- name = "Release";
- };
- "OBJ_5" = {
- isa = "PBXGroup";
- children = (
- "OBJ_6",
- "OBJ_7",
- "OBJ_10",
- "OBJ_11",
- "OBJ_13",
- "OBJ_14"
- );
- path = "";
- sourceTree = "";
- };
- "OBJ_6" = {
- isa = "PBXFileReference";
- explicitFileType = "sourcecode.swift";
- path = "Package.swift";
- sourceTree = "";
- };
- "OBJ_7" = {
- isa = "PBXGroup";
- children = (
- "OBJ_8"
- );
- name = "Sources";
- path = "";
- sourceTree = "SOURCE_ROOT";
- };
- "OBJ_8" = {
- isa = "PBXGroup";
- children = (
- "OBJ_9"
- );
- name = "KeychainKit";
- path = "Sources/KeychainKit";
- sourceTree = "SOURCE_ROOT";
- };
- "OBJ_9" = {
- isa = "PBXFileReference";
- path = "Keychain.swift";
- sourceTree = "";
- };
- };
- rootObject = "OBJ_1";
-}
diff --git a/KeychainKit.xcodeproj/project.xcworkspace/contents.xcworkspacedata b/KeychainKit.xcodeproj/project.xcworkspace/contents.xcworkspacedata
deleted file mode 100644
index fe1aa71..0000000
--- a/KeychainKit.xcodeproj/project.xcworkspace/contents.xcworkspacedata
+++ /dev/null
@@ -1,7 +0,0 @@
-
-
-
-
-
\ No newline at end of file
diff --git a/KeychainKit.xcodeproj/project.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings b/KeychainKit.xcodeproj/project.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings
deleted file mode 100644
index a72dc2b..0000000
--- a/KeychainKit.xcodeproj/project.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings
+++ /dev/null
@@ -1,8 +0,0 @@
-
-
-
-
- IDEWorkspaceSharedSettings_AutocreateContextsIfNeeded
-
-
-
\ No newline at end of file
diff --git a/KeychainKit.xcodeproj/xcshareddata/xcschemes/KeychainKit-Package.xcscheme b/KeychainKit.xcodeproj/xcshareddata/xcschemes/KeychainKit-Package.xcscheme
deleted file mode 100644
index 45c3ade..0000000
--- a/KeychainKit.xcodeproj/xcshareddata/xcschemes/KeychainKit-Package.xcscheme
+++ /dev/null
@@ -1,24 +0,0 @@
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
diff --git a/LICENSE b/LICENSE
index 7365fe7..b033d92 100644
--- a/LICENSE
+++ b/LICENSE
@@ -1,6 +1,6 @@
MIT License
-Copyright (c) 2020 Aleksey Zgurskiy
+Copyright (c) 2025 ANGD Dev
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
diff --git a/Package.swift b/Package.swift
index e11cace..c5dc016 100644
--- a/Package.swift
+++ b/Package.swift
@@ -1,16 +1,15 @@
-// swift-tools-version:5.2
+// 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",
- platforms: [.iOS(.v8)],
- products: [
- .library(name: "KeychainKit", targets: ["KeychainKit"]),
- ],
- dependencies: [],
- targets: [
- .target(name: "KeychainKit", dependencies: [])
- ]
+ name: "KeychainKit",
+ platforms: [.macOS(.v10_15), .iOS(.v13)],
+ products: [
+ .library(name: "KeychainKit", targets: ["KeychainKit"]),
+ ],
+ targets: [
+ .target(name: "KeychainKit")
+ ]
)
diff --git a/README.md b/README.md
index f8af6be..ad1492a 100644
--- a/README.md
+++ b/README.md
@@ -1 +1,60 @@
-# keychain-kit
\ No newline at end of file
+# KeychainKit
+
+KeychainKit is a type-safe, easy-to-use wrapper around Apple’s Keychain service that supports storing, retrieving, and deleting data with optional local authentication.
+
+## Overview
+
+This library enables working with Keychain without losing control over security settings while simplifying type-safe access to data types like `Data`, `String`, `UUID`, and any `Codable` types.
+
+It supports optional authentication via `LAContext`, allowing integration with Face ID, Touch ID, or device passcode.
+
+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).
+
+### Adding to an Xcode Project
+
+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., `2.0.0`).
+5. Add the library to your target module.
+
+### Adding to Package.swift
+
+If you are using Swift Package Manager with a `Package.swift` file, add the dependency like this:
+
+```swift
+// swift-tools-version: 5.10
+import PackageDescription
+
+let package = Package(
+ name: "YourProject",
+ dependencies: [
+ .package(url: "https://github.com/angd-dev/keychain-kit.git", from: "2.0.0")
+ ],
+ targets: [
+ .target(
+ name: "YourTarget",
+ dependencies: [
+ .product(name: "KeychainKit", package: "keychain-kit")
+ ]
+ )
+ ]
+)
+```
+
+## Additional Resources
+
+For more information and usage examples, see the [documentation](https://docs.angd.dev/?package=keychain-kit&version=2.0.0).
+
+## License
+
+This project is licensed under the MIT License. See the `LICENSE` file for details.
diff --git a/Sources/KeychainKit/Classes/KeychainStorage.swift b/Sources/KeychainKit/Classes/KeychainStorage.swift
new file mode 100644
index 0000000..8529d7e
--- /dev/null
+++ b/Sources/KeychainKit/Classes/KeychainStorage.swift
@@ -0,0 +1,272 @@
+import Foundation
+import LocalAuthentication
+import Security
+
+/// A type-safe storage abstraction over the Keychain service.
+///
+/// Supports storing, retrieving, and deleting generic data associated with
+/// accounts and services, with optional local authentication context support.
+///
+/// ## Topics
+///
+/// ### Initializers
+///
+/// - ``init(service:context:)``
+///
+/// ### Instance Properties
+///
+/// - ``service``
+/// - ``context``
+///
+/// ### Retrieving Values
+///
+/// - ``get(_:)-5u61a``
+/// - ``get(_:)-502rt``
+/// - ``get(_:)-63a3x``
+/// - ``get(_:decoder:)``
+///
+/// ### Storing Values
+///
+/// - ``set(_:for:)-7053g``
+/// - ``set(_:for:)-99s6o``
+/// - ``set(_:for:)-2e1p6``
+/// - ``set(_:for:encoder:)``
+///
+/// ### Deleting Values
+///
+/// - ``delete(_:)``
+public final class KeychainStorage<
+ Account: KeychainAccountProtocol,
+ Service: KeychainServiceProtocol
+>: KeychainStorageProtocol {
+ // MARK: - Properties
+
+ /// The service metadata associated with this Keychain storage instance.
+ public let service: Service?
+
+ /// An optional local authentication context used for biometric or passcode protection.
+ public let context: LAContext?
+
+ // MARK: - Inits
+
+ /// Creates a new `KeychainStorage` instance with the given service and authentication context.
+ ///
+ /// - Parameters:
+ /// - 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
+ }
+
+ // MARK: - Methods
+
+ /// Retrieves raw `Data` stored in Keychain for the specified account.
+ ///
+ /// - Parameter account: The account identifier conforming to `KeychainAccountProtocol`.
+ /// - Returns: The raw data associated with the given account.
+ /// - Throws: ``KeychainError/itemNotFound`` when no keychain item matches the query.
+ /// - Throws: ``KeychainError/authenticationFailed`` if biometric or device authentication fails.
+ /// - Throws: ``KeychainError/unexpectedData`` if the stored data is missing or corrupted.
+ /// - Throws: ``KeychainError/unexpectedCode(_:)`` for any other OSStatus error returned by the Keychain API.
+ 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
+ ]
+
+ query[kSecAttrService] = service?.identifier
+ query[kSecAttrAccessGroup] = service?.accessGroup
+ query[kSecUseAuthenticationContext] = context
+
+ var queryResult: AnyObject?
+ let status = withUnsafeMutablePointer(to: &queryResult) {
+ SecItemCopyMatching(query as CFDictionary, UnsafeMutablePointer($0))
+ }
+
+ switch status {
+ case errSecSuccess:
+ guard
+ let item = queryResult as? [CFString : AnyObject],
+ let data = item[kSecValueData] as? Data
+ else { throw KeychainError.unexpectedData }
+ return data
+ case errSecItemNotFound:
+ throw KeychainError.itemNotFound
+ case errSecAuthFailed:
+ throw KeychainError.authenticationFailed
+ default:
+ throw KeychainError.unexpectedCode(status)
+ }
+ }
+
+ /// Retrieves a UTF-8 encoded string stored in Keychain for the specified account.
+ ///
+ /// - Parameter account: The account identifier conforming to `KeychainAccountProtocol`.
+ /// - Returns: The stored string value associated with the account.
+ /// - Throws: ``KeychainError/itemNotFound`` when no keychain item matches the query.
+ /// - Throws: ``KeychainError/authenticationFailed`` if biometric or device authentication fails.
+ /// - Throws: ``KeychainError/unexpectedData`` if the stored data cannot be decoded as UTF-8.
+ /// - Throws: ``KeychainError/unexpectedCode(_:)`` for any other OSStatus error returned by the Keychain API.
+ public 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 Keychain for the specified account.
+ ///
+ /// - Parameter account: The account identifier conforming to `KeychainAccountProtocol`.
+ /// - Returns: The stored UUID value associated with the account.
+ /// - Throws: ``KeychainError/itemNotFound`` when no keychain item matches the query.
+ /// - Throws: ``KeychainError/authenticationFailed`` if biometric or device authentication fails.
+ /// - Throws: ``KeychainError/unexpectedData`` if the stored string is missing or is not a valid UUID.
+ /// - Throws: ``KeychainError/unexpectedCode(_:)`` for any other OSStatus error returned by the Keychain API.
+ public 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 Keychain, decoded from JSON using the provided decoder.
+ ///
+ /// - Parameters:
+ /// - account: The account identifier conforming to `KeychainAccountProtocol`.
+ /// - decoder: The `JSONDecoder` instance used to decode the data (default is a new instance).
+ /// - Returns: The decoded value of type `T`.
+ /// - Throws: ``KeychainError/itemNotFound`` when no keychain item matches the query.
+ /// - Throws: ``KeychainError/authenticationFailed`` if biometric or device authentication fails.
+ /// - Throws: ``KeychainError/unexpectedData`` if the stored data is missing or corrupted.
+ /// - Throws: ``KeychainError/unexpectedCode(_:)`` for any OSStatus error returned by the Keychain API.
+ /// - Throws: ``KeychainError/unexpectedError(_:)`` if decoding the data into `T` fails.
+ public 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 raw `Data` in the Keychain for the specified account, replacing any existing value.
+ ///
+ /// - Parameters:
+ /// - value: The raw data to store in the Keychain.
+ /// - account: The account identifier conforming to `KeychainAccountProtocol`.
+ /// - Throws: ``KeychainError/unexpectedError(_:)`` if access control creation fails.
+ /// - Throws: ``KeychainError/unexpectedCode(_:)`` if adding the item to the Keychain fails.
+ /// - Throws: Any error thrown by ``delete(_:)`` if the previous value 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 {
+ throw KeychainError.unexpectedError(error?.takeUnretainedValue())
+ }
+
+ var query: [CFString: Any] = [
+ kSecClass: kSecClassGenericPassword,
+ kSecAttrAccount: account.identifier,
+ kSecAttrSynchronizable: account.synchronizable,
+ kSecUseDataProtectionKeychain: true,
+ kSecAttrAccessControl: access,
+ kSecValueData: value
+ ]
+
+ query[kSecAttrService] = service?.identifier
+ query[kSecAttrAccessGroup] = service?.accessGroup
+ query[kSecUseAuthenticationContext] = context
+
+ let status = SecItemAdd(query as CFDictionary, nil)
+ guard status == noErr else {
+ throw KeychainError.unexpectedCode(status)
+ }
+ }
+
+ /// Stores a UTF-8 encoded string in the Keychain for the specified account.
+ ///
+ /// - Parameters:
+ /// - value: The string value to store.
+ /// - account: The account identifier conforming to `KeychainAccountProtocol`.
+ /// - Throws: ``KeychainError/unexpectedError(_:)`` if access control creation fails.
+ /// - Throws: ``KeychainError/unexpectedCode(_:)`` if adding the item to the Keychain fails.
+ /// - Throws: Any error thrown by ``set(_:for:)-7053g`` if encoding or insertion fails.
+ public func set(_ value: String, for account: Account) throws(KeychainError) {
+ try set(value.data(using: .utf8)!, for: account)
+ }
+
+ /// Stores a `UUID` value as a string in the Keychain for the specified account.
+ ///
+ /// - Parameters:
+ /// - value: The UUID value to store.
+ /// - account: The account identifier conforming to `KeychainAccountProtocol`.
+ /// - Throws: ``KeychainError/unexpectedError(_:)`` if access control creation fails.
+ /// - Throws: ``KeychainError/unexpectedCode(_:)`` if adding the item to the Keychain fails.
+ /// - Throws: Any error thrown by ``set(_:for:)-7053g`` if encoding or insertion fails.
+ public 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 conforming to `KeychainAccountProtocol`.
+ /// - encoder: The `JSONEncoder` to use for encoding the value (default is a new instance).
+ /// - Throws: ``KeychainError/unexpectedError(_:)`` if encoding fails.
+ /// - Throws: ``KeychainError/unexpectedError(_:)`` if access control creation fails.
+ /// - Throws: ``KeychainError/unexpectedCode(_:)`` if adding the item to the Keychain fails.
+ /// - Throws: Any error thrown by ``set(_:for:)-7053g`` if insertion fails.
+ public func set(
+ _ value: T,
+ for account: Account,
+ encoder: JSONEncoder = .init()
+ ) throws(KeychainError) {
+ do {
+ let data = try encoder.encode(value)
+ try set(data, for: account)
+ } catch let error as KeychainError {
+ throw error
+ } catch {
+ throw KeychainError.unexpectedError(error)
+ }
+ }
+
+ /// Deletes the item associated with the specified account from the Keychain.
+ ///
+ /// If no item exists for the given account, the method does nothing and does not throw an error.
+ ///
+ /// - Parameter account: The account identifier conforming to `KeychainAccountProtocol`.
+ /// - Throws: ``KeychainError/unexpectedCode(_:)`` if deletion fails with an unexpected OSStatus.
+ public func delete(_ account: Account) throws(KeychainError) {
+ var query: [CFString: Any] = [
+ kSecClass: kSecClassGenericPassword,
+ kSecAttrAccount: account.identifier,
+ kSecAttrSynchronizable: account.synchronizable,
+ kSecUseDataProtectionKeychain: true
+ ]
+
+ query[kSecAttrService] = service?.identifier
+ query[kSecAttrAccessGroup] = service?.accessGroup
+ query[kSecUseAuthenticationContext] = context
+
+ 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
new file mode 100644
index 0000000..fb416fd
--- /dev/null
+++ b/Sources/KeychainKit/Enums/KeychainError.swift
@@ -0,0 +1,15 @@
+import Foundation
+
+/// Errors that can occur during Keychain operations.
+public enum KeychainError: Error {
+ /// Authentication failed, e.g., due to biometric or passcode denial.
+ case authenticationFailed
+ /// 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/Keychain.swift b/Sources/KeychainKit/Keychain.swift
deleted file mode 100644
index 5df6fea..0000000
--- a/Sources/KeychainKit/Keychain.swift
+++ /dev/null
@@ -1,117 +0,0 @@
-//
-// Keychain.swift
-// keychain-kit
-//
-// Created by Aleksey Zgurskiy on 02.02.2020.
-// Copyright © 2020 mr.noone. All rights reserved.
-//
-
-import Foundation
-
-public protocol KeychainProtocol {
- func get(_ key: String) throws -> Data
- func get(_ key: String) throws -> String
- func get(_ key: String) throws -> UUID
- func get(_ key: String, decoder: JSONDecoder) throws -> T where T: Decodable
-
- func set(_ data: Data, for key: String) throws
- func set(_ value: String, for key: String) throws
- func set(_ uuid: UUID, for key: String) throws
- func set(_ value: T, for key: String, encoder: JSONEncoder) throws where T: Encodable
-
- func delete(_ key: String) throws
-}
-
-public struct Keychain: KeychainProtocol {
- public enum Error: Swift.Error {
- case noData
- case unexpectedData
- case unexpected(code: OSStatus)
- }
-
- // MARK: - Inits
-
- public init() {}
-
- // MARK: - Public methods
-
- public func get(_ key: String) throws -> Data {
- let query: [CFString : AnyObject] = [
- kSecClass : kSecClassGenericPassword,
- kSecAttrAccount : key as AnyObject,
- kSecMatchLimit : kSecMatchLimitOne,
- kSecReturnAttributes : kCFBooleanTrue,
- kSecReturnData : kCFBooleanTrue
- ]
-
- var queryResult: AnyObject?
- let status = withUnsafeMutablePointer(to: &queryResult) {
- SecItemCopyMatching(query as CFDictionary, UnsafeMutablePointer($0))
- }
-
- guard status != errSecItemNotFound else { throw Error.noData }
- guard status == noErr else { throw Error.unexpected(code: status) }
-
- guard
- let item = queryResult as? [CFString : AnyObject],
- let data = item[kSecValueData] as? Data
- else { throw Error.noData }
-
- return data
- }
-
- public func get(_ key: String) throws -> String {
- guard let value = String(data: try get(key), encoding: .utf8) else {
- throw Error.unexpectedData
- }
- return value
- }
-
- public func get(_ key: String) throws -> UUID {
- guard let value = UUID(uuidString: try get(key)) else {
- throw Error.unexpectedData
- }
- return value
- }
-
- public func get(_ key: String, decoder: JSONDecoder = JSONDecoder()) throws -> T where T: Decodable {
- return try decoder.decode(T.self, from: get(key))
- }
-
- public func set(_ data: Data, for key: String) throws {
- try delete(key)
-
- let query: [CFString : AnyObject] = [
- kSecClass : kSecClassGenericPassword,
- kSecAttrAccount : key as AnyObject,
- kSecValueData : data as AnyObject
- ]
-
- let status = SecItemAdd(query as CFDictionary, nil)
- guard status == noErr else { throw Error.unexpected(code: status) }
- }
-
- public func set(_ value: String, for key: String) throws {
- try set(value.data(using: .utf8)!, for: key)
- }
-
- public func set(_ uuid: UUID, for key: String) throws {
- try set(uuid.uuidString, for: key)
- }
-
- public func set(_ value: T, for key: String, encoder: JSONEncoder = JSONEncoder()) throws where T: Encodable {
- try set(encoder.encode(value), for: key)
- }
-
- public func delete(_ key: String) throws {
- let query: [CFString : AnyObject] = [
- kSecClass : kSecClassGenericPassword,
- kSecAttrAccount : key as AnyObject
- ]
-
- let status = SecItemDelete(query as CFDictionary)
- guard status == noErr || status == errSecItemNotFound else {
- throw Error.unexpected(code: status)
- }
- }
-}
diff --git a/Sources/KeychainKit/Protocols/KeychainAccountProtocol.swift b/Sources/KeychainKit/Protocols/KeychainAccountProtocol.swift
new file mode 100644
index 0000000..0201eb4
--- /dev/null
+++ b/Sources/KeychainKit/Protocols/KeychainAccountProtocol.swift
@@ -0,0 +1,47 @@
+import Foundation
+
+/// A protocol that defines the required properties for a keychain account descriptor.
+///
+/// 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 for the account.
+ ///
+ /// 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 used to define authentication requirements.
+ ///
+ /// Defaults to `[]` (no additional access control). Can be overridden to specify
+ /// constraints such as `.userPresence`, `.biometryAny`, or `.devicePasscode`.
+ var accessFlags: SecAccessControlCreateFlags { get }
+
+ /// Whether the item should be marked as synchronizable via iCloud Keychain.
+ ///
+ /// 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 {
+ /// Provides a default `identifier` implementation for `RawRepresentable` types
+ /// whose `RawValue` is `String`.
+ ///
+ /// 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
new file mode 100644
index 0000000..ae30994
--- /dev/null
+++ b/Sources/KeychainKit/Protocols/KeychainServiceProtocol.swift
@@ -0,0 +1,28 @@
+import Foundation
+
+/// A protocol that defines the required properties for a keychain service descriptor.
+///
+/// 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 to support shared access between apps.
+ ///
+ /// 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 {
+ /// Provides a default `identifier` implementation for `RawRepresentable` types
+ /// whose `RawValue` is `String`.
+ ///
+ /// 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
new file mode 100644
index 0000000..943ec6b
--- /dev/null
+++ b/Sources/KeychainKit/Protocols/KeychainStorageProtocol.swift
@@ -0,0 +1,124 @@
+import Foundation
+
+/// A protocol that defines a type-safe interface for storing and retrieving values
+/// in the system keychain.
+///
+/// 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
+///
+/// ### Associated Types
+///
+/// - ``Account``
+/// - ``Service``
+///
+/// ### Instance Properties
+///
+/// - ``service``
+///
+/// ### Retrieving Values
+///
+/// - ``get(_:)-2gcee``
+/// - ``get(_:)-23z7h``
+/// - ``get(_:)-4xbe6``
+/// - ``get(_:decoder:)``
+///
+/// ### Storing Values
+///
+/// - ``set(_:for:)-21dla``
+/// - ``set(_:for:)-6nzkf``
+/// - ``set(_:for:)-2smpc``
+/// - ``set(_:for:encoder:)``
+///
+/// ### Deleting Values
+///
+/// - ``delete(_:)``
+public protocol KeychainStorageProtocol {
+ /// A type that describes a keychain account and its security configuration.
+ associatedtype Account: KeychainAccountProtocol
+
+ /// A type that identifies a keychain service context (e.g., app or subsystem).
+ associatedtype Service: KeychainServiceProtocol
+
+ /// 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 }
+
+ /// Retrieves the value stored in the keychain for the specified account as raw `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
+
+ /// 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:
+ /// - 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
+
+ /// Stores raw `Data` in the keychain for the specified account.
+ ///
+ /// - Parameters:
+ /// - 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 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)
+
+ /// Stores a `UUID` in the keychain for the specified account.
+ ///
+ /// - Parameters:
+ /// - 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 stores a value of type `T` in the keychain for the specified account.
+ ///
+ /// - Parameters:
+ /// - value: The value to encode and store.
+ /// - 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)
+}