diff --git a/.gitignore b/.gitignore
index 70e6dad..67356a1 100644
--- a/.gitignore
+++ b/.gitignore
@@ -4,6 +4,7 @@
.build/
## Various settings
+Package.resolved
*.pbxuser
!default.pbxuser
*.mode1v3
diff --git a/.swift-format b/.swift-format
new file mode 100644
index 0000000..b77abe6
--- /dev/null
+++ b/.swift-format
@@ -0,0 +1,15 @@
+{
+ "fileScopedDeclarationPrivacy": {
+ "accessLevel": "private"
+ },
+ "indentBlankLines": true,
+ "indentation": {
+ "spaces": 4
+ },
+ "lineLength": 9999,
+ "maximumBlankLines": 1,
+ "multiElementCollectionTrailingCommas": false,
+ "rules": {
+ "FileScopedDeclarationPrivacy": true
+ }
+}
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..273919b 100644
--- a/Package.swift
+++ b/Package.swift
@@ -1,16 +1,28 @@
-// swift-tools-version:5.2
+// swift-tools-version: 6.0
// 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",
+ defaultLocalization: "en",
+ platforms: [.macOS(.v12), .iOS(.v15)],
+ products: [
+ .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")
+ ]
+ )
+ ]
)
diff --git a/README.md b/README.md
index f8af6be..c73e91a 100644
--- a/README.md
+++ b/README.md
@@ -1 +1,55 @@
-# 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.
+
+## 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., `3.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: "3.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=3.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..9788e58
--- /dev/null
+++ b/Sources/KeychainKit/Classes/KeychainStorage.swift
@@ -0,0 +1,193 @@
+import Foundation
+import LocalAuthentication
+import Security
+
+/// A service that provides access and management for keychain items.
+///
+/// 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.
+///
+/// ## Topics
+///
+/// ### Initializers
+///
+/// - ``init(service:context:)``
+///
+/// ### Instance Properties
+///
+/// - ``service``
+/// - ``context``
+///
+/// ### Instance Methods
+///
+/// - ``get(by:)->Data?``
+/// - ``insert(_:by:)-(Data,_)``
+/// - ``delete(by:)``
+/// - ``exists(by:)``
+public final class KeychainStorage<
+ Account: KeychainAccountProtocol,
+ Service: KeychainServiceProtocol
+>: KeychainStorageProtocol, @unchecked Sendable {
+ // MARK: - Properties
+
+ /// The service descriptor associated with this keychain storage.
+ public let service: Service?
+
+ /// The authentication context used for keychain operations.
+ public let context: LAContext?
+
+ // MARK: - Initialization
+
+ /// Creates a new keychain storage instance.
+ ///
+ /// - 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.
+ public init(service: Service?, context: LAContext?) {
+ self.service = service
+ self.context = context
+ }
+
+ // 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/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? {
+ var query: [CFString: Any] = [
+ kSecClass: kSecClassGenericPassword,
+ kSecAttrAccount: account.identifier,
+ kSecAttrSynchronizable: account.synchronizable,
+ kSecUseDataProtectionKeychain: true,
+ kSecMatchLimit: kSecMatchLimitOne,
+ kSecReturnData: true
+ ]
+
+ query[kSecAttrService] = service?.identifier
+ query[kSecAttrAccessGroup] = service?.accessGroup
+ query[kSecUseAuthenticationContext] = context
+
+ var result: AnyObject?
+
+ switch SecItemCopyMatching(query as CFDictionary, &result) {
+ case errSecSuccess:
+ if let data = result as? Data {
+ return data
+ } else {
+ throw .invalidData
+ }
+ case errSecItemNotFound:
+ return nil
+ case errSecAuthFailed, errSecInteractionNotAllowed, errSecUserCanceled:
+ throw .authenticationFailed
+ case let status:
+ throw .osStatus(status)
+ }
+ }
+
+ /// Inserts raw data for the given account.
+ ///
+ /// - 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) {
+ 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)
+ }
+
+ 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
+
+ switch SecItemAdd(query as CFDictionary, nil) {
+ case errSecSuccess:
+ return
+ case errSecDuplicateItem:
+ throw .duplicateItem
+ case let status:
+ throw .osStatus(status)
+ }
+ }
+
+ /// Deletes the item for the given 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) {
+ 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
+
+ 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)
+ }
+ }
+}
diff --git a/Sources/KeychainKit/Enums/KeychainError.swift b/Sources/KeychainKit/Enums/KeychainError.swift
new file mode 100644
index 0000000..aa48d18
--- /dev/null
+++ b/Sources/KeychainKit/Enums/KeychainError.swift
@@ -0,0 +1,44 @@
+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.
+ 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 ?? "")
+ }
+ }
+}
diff --git a/Sources/KeychainKit/Extensions/String+Error.swift b/Sources/KeychainKit/Extensions/String+Error.swift
new file mode 100644
index 0000000..687d7db
--- /dev/null
+++ b/Sources/KeychainKit/Extensions/String+Error.swift
@@ -0,0 +1,15 @@
+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/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..67b1da1
--- /dev/null
+++ b/Sources/KeychainKit/Protocols/KeychainAccountProtocol.swift
@@ -0,0 +1,53 @@
+import Foundation
+
+/// A type that describes a keychain account configuration for secure item storage and access.
+///
+/// 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.
+ var identifier: String { get }
+
+ /// The keychain data protection level assigned to the account.
+ ///
+ /// Defaults to `kSecAttrAccessibleAfterFirstUnlock`. You can override this to use another
+ /// accessibility option, such as `kSecAttrAccessibleWhenUnlocked` or
+ /// `kSecAttrAccessibleWhenPasscodeSetThisDeviceOnly`.
+ var protection: CFString { get }
+
+ /// The access control flags defining additional authentication requirements.
+ ///
+ /// Defaults to an empty set (`[]`). Override this to enforce constraints like `.userPresence`,
+ /// `.biometryAny`, or `.devicePasscode`.
+ var accessFlags: SecAccessControlCreateFlags { get }
+
+ /// Indicates whether the item is synchronized through iCloud Keychain.
+ ///
+ /// Defaults to `false`. Set this to `true` if the item should be available across all devices
+ /// associated with the same iCloud account.
+ var synchronizable: Bool { get }
+}
+
+public extension KeychainAccountProtocol {
+ var protection: CFString { kSecAttrAccessibleAfterFirstUnlock }
+
+ var accessFlags: SecAccessControlCreateFlags { [] }
+
+ var synchronizable: Bool { false }
+}
+
+public extension KeychainAccountProtocol where Self: RawRepresentable, Self.RawValue == String {
+ /// A unique string that identifies the keychain account.
+ ///
+ /// Derived from the instance’s 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..ab1af9a
--- /dev/null
+++ b/Sources/KeychainKit/Protocols/KeychainServiceProtocol.swift
@@ -0,0 +1,33 @@
+import Foundation
+
+/// A type that describes a keychain service used to group and identify stored items.
+///
+/// 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.
+ var identifier: String { get }
+
+ /// An optional keychain access group identifier that enables shared access between apps.
+ ///
+ /// Defaults to `nil`, meaning no access group is specified.
+ var accessGroup: String? { get }
+}
+
+public extension KeychainServiceProtocol {
+ var accessGroup: String? { nil }
+}
+
+public extension KeychainServiceProtocol where Self: RawRepresentable, Self.RawValue == String {
+ /// A unique string that identifies the keychain service.
+ ///
+ /// Derived from the instance’s 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..bf22753
--- /dev/null
+++ b/Sources/KeychainKit/Protocols/KeychainStorageProtocol.swift
@@ -0,0 +1,185 @@
+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(
+ 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(
+ _ 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)
+ }
+}
diff --git a/Sources/KeychainKit/Resources/Localizable.xcstrings b/Sources/KeychainKit/Resources/Localizable.xcstrings
new file mode 100644
index 0000000..44446a7
--- /dev/null
+++ b/Sources/KeychainKit/Resources/Localizable.xcstrings
@@ -0,0 +1,56 @@
+{
+ "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