From 060469201081e33be916ea9fa16fbca1a56909a6 Mon Sep 17 00:00:00 2001 From: Aleksey Zgurskiy Date: Sun, 8 Mar 2020 17:42:22 +0200 Subject: [PATCH 01/10] Add keychain protocol --- keychain-kit/Sources/Keychain.swift | 16 +++++++++++++++- 1 file changed, 15 insertions(+), 1 deletion(-) diff --git a/keychain-kit/Sources/Keychain.swift b/keychain-kit/Sources/Keychain.swift index 4b2038b..5df6fea 100644 --- a/keychain-kit/Sources/Keychain.swift +++ b/keychain-kit/Sources/Keychain.swift @@ -8,7 +8,21 @@ import Foundation -public struct Keychain { +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 From d9814c2cc87b935c4b30105bfef2ab3b9fe00824 Mon Sep 17 00:00:00 2001 From: Aleksey Zgurskiy Date: Sun, 8 Mar 2020 17:43:09 +0200 Subject: [PATCH 02/10] Update version --- keychain-kit.xcodeproj/project.pbxproj | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/keychain-kit.xcodeproj/project.pbxproj b/keychain-kit.xcodeproj/project.pbxproj index 9a70958..4c558d0 100644 --- a/keychain-kit.xcodeproj/project.pbxproj +++ b/keychain-kit.xcodeproj/project.pbxproj @@ -302,7 +302,7 @@ "@executable_path/Frameworks", "@loader_path/Frameworks", ); - MARKETING_VERSION = 1.1.1; + MARKETING_VERSION = 1.1.2; PRODUCT_BUNDLE_IDENTIFIER = "com.mr-noone.keychain-kit"; SKIP_INSTALL = YES; SWIFT_OPTIMIZATION_LEVEL = "-Onone"; @@ -327,7 +327,7 @@ "@executable_path/Frameworks", "@loader_path/Frameworks", ); - MARKETING_VERSION = 1.1.1; + MARKETING_VERSION = 1.1.2; PRODUCT_BUNDLE_IDENTIFIER = "com.mr-noone.keychain-kit"; SKIP_INSTALL = YES; SWIFT_VERSION = 5.0; From 54446ff3141ffedf270c2868aaf47ded5db85db4 Mon Sep 17 00:00:00 2001 From: Aleksey Zgurskiy Date: Fri, 7 Aug 2020 16:47:58 +0300 Subject: [PATCH 03/10] Make swift pm project --- .gitignore | 88 +--- KeychainKit.xcodeproj/KeychainKit_Info.plist | 25 ++ KeychainKit.xcodeproj/project.pbxproj | 383 ++++++++++++++++++ .../contents.xcworkspacedata | 4 +- .../xcshareddata/WorkspaceSettings.xcsettings | 6 +- .../xcschemes/KeychainKit-Package.xcscheme | 24 ++ Package.swift | 16 + .../KeychainKit}/Keychain.swift | 0 keychain-kit.xcodeproj/project.pbxproj | 362 ----------------- keychain-kit/Support Files/Config.xcconfig | 13 - keychain-kit/Support Files/Debug.xcconfig | 12 - keychain-kit/Support Files/Info.plist | 22 - keychain-kit/Support Files/Release.xcconfig | 12 - keychain-kit/Support Files/keychain_kit.h | 19 - 14 files changed, 460 insertions(+), 526 deletions(-) create mode 100644 KeychainKit.xcodeproj/KeychainKit_Info.plist create mode 100644 KeychainKit.xcodeproj/project.pbxproj rename {keychain-kit.xcodeproj => KeychainKit.xcodeproj}/project.xcworkspace/contents.xcworkspacedata (61%) rename keychain-kit.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist => KeychainKit.xcodeproj/project.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings (66%) create mode 100644 KeychainKit.xcodeproj/xcshareddata/xcschemes/KeychainKit-Package.xcscheme create mode 100644 Package.swift rename {keychain-kit/Sources => Sources/KeychainKit}/Keychain.swift (100%) delete mode 100644 keychain-kit.xcodeproj/project.pbxproj delete mode 100644 keychain-kit/Support Files/Config.xcconfig delete mode 100644 keychain-kit/Support Files/Debug.xcconfig delete mode 100644 keychain-kit/Support Files/Info.plist delete mode 100644 keychain-kit/Support Files/Release.xcconfig delete mode 100644 keychain-kit/Support Files/keychain_kit.h diff --git a/.gitignore b/.gitignore index 330d167..70e6dad 100644 --- a/.gitignore +++ b/.gitignore @@ -1,18 +1,9 @@ -# Xcode -# -# gitignore contributors: remember to update Global/Xcode.gitignore, Objective-C.gitignore & Swift.gitignore +## General +.DS_Store +.swiftpm +.build/ -## User settings -xcuserdata/ - -## compatibility with Xcode 8 and earlier (ignoring not required starting Xcode 9) -*.xcscmblueprint -*.xccheckout - -## compatibility with Xcode 3 and earlier (ignoring not required starting Xcode 4) -build/ -DerivedData/ -*.moved-aside +## Various settings *.pbxuser !default.pbxuser *.mode1v3 @@ -21,70 +12,5 @@ DerivedData/ !default.mode2v3 *.perspectivev3 !default.perspectivev3 - -## Obj-C/Swift specific -*.hmap - -## App packaging -*.ipa -*.dSYM.zip -*.dSYM - -## Playgrounds -timeline.xctimeline -playground.xcworkspace - -# Swift Package Manager -# -# Add this line if you want to avoid checking in source code from Swift Package Manager dependencies. -# Packages/ -# Package.pins -# Package.resolved -# *.xcodeproj -# -# Xcode automatically generates this directory with a .xcworkspacedata file and xcuserdata -# hence it is not needed unless you have added a package configuration file to your project -# .swiftpm - -.build/ - -# CocoaPods -# -# We recommend against adding the Pods directory to your .gitignore. However -# you should judge for yourself, the pros and cons are mentioned at: -# https://guides.cocoapods.org/using/using-cocoapods.html#should-i-check-the-pods-directory-into-source-control -# -# Pods/ -# -# Add this line if you want to avoid checking in source code from the Xcode workspace -# *.xcworkspace - -# Carthage -# -# Add this line if you want to avoid checking in source code from Carthage dependencies. -# Carthage/Checkouts - -Carthage/Build/ - -# Accio dependency management -Dependencies/ -.accio/ - -# fastlane -# -# It is recommended to not store the screenshots in the git repo. -# Instead, use fastlane to re-generate the screenshots whenever they are needed. -# For more information about the recommended setup visit: -# https://docs.fastlane.tools/best-practices/source-control/#source-control - -fastlane/report.xml -fastlane/Preview.html -fastlane/screenshots/**/*.png -fastlane/test_output - -# Code Injection -# -# After new code Injection tools there's a generated folder /iOSInjectionProject -# https://github.com/johnno1962/injectionforxcode - -iOSInjectionProject/ +xcuserdata/ +*.xcuserdatad/ diff --git a/KeychainKit.xcodeproj/KeychainKit_Info.plist b/KeychainKit.xcodeproj/KeychainKit_Info.plist new file mode 100644 index 0000000..57ada9f --- /dev/null +++ b/KeychainKit.xcodeproj/KeychainKit_Info.plist @@ -0,0 +1,25 @@ + + + + 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 new file mode 100644 index 0000000..2b99a90 --- /dev/null +++ b/KeychainKit.xcodeproj/project.pbxproj @@ -0,0 +1,383 @@ +// !$*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/keychain-kit.xcodeproj/project.xcworkspace/contents.xcworkspacedata b/KeychainKit.xcodeproj/project.xcworkspace/contents.xcworkspacedata similarity index 61% rename from keychain-kit.xcodeproj/project.xcworkspace/contents.xcworkspacedata rename to KeychainKit.xcodeproj/project.xcworkspace/contents.xcworkspacedata index 2cbedfd..fe1aa71 100644 --- a/keychain-kit.xcodeproj/project.xcworkspace/contents.xcworkspacedata +++ b/KeychainKit.xcodeproj/project.xcworkspace/contents.xcworkspacedata @@ -2,6 +2,6 @@ + location = "self:"> - + \ No newline at end of file diff --git a/keychain-kit.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist b/KeychainKit.xcodeproj/project.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings similarity index 66% rename from keychain-kit.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist rename to KeychainKit.xcodeproj/project.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings index 18d9810..a72dc2b 100644 --- a/keychain-kit.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist +++ b/KeychainKit.xcodeproj/project.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings @@ -2,7 +2,7 @@ - IDEDidComputeMac32BitWarning - + 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 new file mode 100644 index 0000000..45c3ade --- /dev/null +++ b/KeychainKit.xcodeproj/xcshareddata/xcschemes/KeychainKit-Package.xcscheme @@ -0,0 +1,24 @@ + + + + + + + + + + + + + + + diff --git a/Package.swift b/Package.swift new file mode 100644 index 0000000..e11cace --- /dev/null +++ b/Package.swift @@ -0,0 +1,16 @@ +// swift-tools-version:5.2 +// 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: []) + ] +) diff --git a/keychain-kit/Sources/Keychain.swift b/Sources/KeychainKit/Keychain.swift similarity index 100% rename from keychain-kit/Sources/Keychain.swift rename to Sources/KeychainKit/Keychain.swift diff --git a/keychain-kit.xcodeproj/project.pbxproj b/keychain-kit.xcodeproj/project.pbxproj deleted file mode 100644 index 4c558d0..0000000 --- a/keychain-kit.xcodeproj/project.pbxproj +++ /dev/null @@ -1,362 +0,0 @@ -// !$*UTF8*$! -{ - archiveVersion = 1; - classes = { - }; - objectVersion = 50; - objects = { - -/* Begin PBXBuildFile section */ - 4B08E1FC23E73380003504E1 /* keychain_kit.h in Headers */ = {isa = PBXBuildFile; fileRef = 4B08E1FA23E73380003504E1 /* keychain_kit.h */; settings = {ATTRIBUTES = (Public, ); }; }; - 4B08E20823E73CAF003504E1 /* Keychain.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4B08E20723E73CAF003504E1 /* Keychain.swift */; }; -/* End PBXBuildFile section */ - -/* Begin PBXFileReference section */ - 4B08E1F723E73380003504E1 /* KeychainKit.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = KeychainKit.framework; sourceTree = BUILT_PRODUCTS_DIR; }; - 4B08E1FA23E73380003504E1 /* keychain_kit.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = keychain_kit.h; sourceTree = ""; }; - 4B08E1FB23E73380003504E1 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; - 4B08E20323E733CD003504E1 /* Config.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = Config.xcconfig; sourceTree = ""; }; - 4B08E20423E733F3003504E1 /* Debug.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = Debug.xcconfig; sourceTree = ""; }; - 4B08E20523E733F9003504E1 /* Release.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = Release.xcconfig; sourceTree = ""; }; - 4B08E20723E73CAF003504E1 /* Keychain.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Keychain.swift; sourceTree = ""; }; -/* End PBXFileReference section */ - -/* Begin PBXFrameworksBuildPhase section */ - 4B08E1F423E73380003504E1 /* Frameworks */ = { - isa = PBXFrameworksBuildPhase; - buildActionMask = 2147483647; - files = ( - ); - runOnlyForDeploymentPostprocessing = 0; - }; -/* End PBXFrameworksBuildPhase section */ - -/* Begin PBXGroup section */ - 4B08E1ED23E7337F003504E1 = { - isa = PBXGroup; - children = ( - 4B08E1F923E73380003504E1 /* keychain-kit */, - 4B08E1F823E73380003504E1 /* Products */, - ); - sourceTree = ""; - }; - 4B08E1F823E73380003504E1 /* Products */ = { - isa = PBXGroup; - children = ( - 4B08E1F723E73380003504E1 /* KeychainKit.framework */, - ); - name = Products; - sourceTree = ""; - }; - 4B08E1F923E73380003504E1 /* keychain-kit */ = { - isa = PBXGroup; - children = ( - 4B08E20623E73C92003504E1 /* Sources */, - 4B08E20223E733BF003504E1 /* Support Files */, - ); - path = "keychain-kit"; - sourceTree = ""; - }; - 4B08E20223E733BF003504E1 /* Support Files */ = { - isa = PBXGroup; - children = ( - 4B08E1FA23E73380003504E1 /* keychain_kit.h */, - 4B08E1FB23E73380003504E1 /* Info.plist */, - 4B08E20323E733CD003504E1 /* Config.xcconfig */, - 4B08E20423E733F3003504E1 /* Debug.xcconfig */, - 4B08E20523E733F9003504E1 /* Release.xcconfig */, - ); - path = "Support Files"; - sourceTree = ""; - }; - 4B08E20623E73C92003504E1 /* Sources */ = { - isa = PBXGroup; - children = ( - 4B08E20723E73CAF003504E1 /* Keychain.swift */, - ); - path = Sources; - sourceTree = ""; - }; -/* End PBXGroup section */ - -/* Begin PBXHeadersBuildPhase section */ - 4B08E1F223E73380003504E1 /* Headers */ = { - isa = PBXHeadersBuildPhase; - buildActionMask = 2147483647; - files = ( - 4B08E1FC23E73380003504E1 /* keychain_kit.h in Headers */, - ); - runOnlyForDeploymentPostprocessing = 0; - }; -/* End PBXHeadersBuildPhase section */ - -/* Begin PBXNativeTarget section */ - 4B08E1F623E73380003504E1 /* keychain-kit */ = { - isa = PBXNativeTarget; - buildConfigurationList = 4B08E1FF23E73380003504E1 /* Build configuration list for PBXNativeTarget "keychain-kit" */; - buildPhases = ( - 4B08E1F223E73380003504E1 /* Headers */, - 4B08E1F323E73380003504E1 /* Sources */, - 4B08E1F423E73380003504E1 /* Frameworks */, - 4B08E1F523E73380003504E1 /* Resources */, - ); - buildRules = ( - ); - dependencies = ( - ); - name = "keychain-kit"; - productName = "keychain-kit"; - productReference = 4B08E1F723E73380003504E1 /* KeychainKit.framework */; - productType = "com.apple.product-type.framework"; - }; -/* End PBXNativeTarget section */ - -/* Begin PBXProject section */ - 4B08E1EE23E73380003504E1 /* Project object */ = { - isa = PBXProject; - attributes = { - LastUpgradeCheck = 1130; - ORGANIZATIONNAME = mr.noone; - TargetAttributes = { - 4B08E1F623E73380003504E1 = { - CreatedOnToolsVersion = 11.3.1; - LastSwiftMigration = 1130; - }; - }; - }; - buildConfigurationList = 4B08E1F123E73380003504E1 /* Build configuration list for PBXProject "keychain-kit" */; - compatibilityVersion = "Xcode 9.3"; - developmentRegion = en; - hasScannedForEncodings = 0; - knownRegions = ( - en, - Base, - ); - mainGroup = 4B08E1ED23E7337F003504E1; - productRefGroup = 4B08E1F823E73380003504E1 /* Products */; - projectDirPath = ""; - projectRoot = ""; - targets = ( - 4B08E1F623E73380003504E1 /* keychain-kit */, - ); - }; -/* End PBXProject section */ - -/* Begin PBXResourcesBuildPhase section */ - 4B08E1F523E73380003504E1 /* Resources */ = { - isa = PBXResourcesBuildPhase; - buildActionMask = 2147483647; - files = ( - ); - runOnlyForDeploymentPostprocessing = 0; - }; -/* End PBXResourcesBuildPhase section */ - -/* Begin PBXSourcesBuildPhase section */ - 4B08E1F323E73380003504E1 /* Sources */ = { - isa = PBXSourcesBuildPhase; - buildActionMask = 2147483647; - files = ( - 4B08E20823E73CAF003504E1 /* Keychain.swift in Sources */, - ); - runOnlyForDeploymentPostprocessing = 0; - }; -/* End PBXSourcesBuildPhase section */ - -/* Begin XCBuildConfiguration section */ - 4B08E1FD23E73380003504E1 /* Debug */ = { - isa = XCBuildConfiguration; - baseConfigurationReference = 4B08E20423E733F3003504E1 /* Debug.xcconfig */; - buildSettings = { - ALWAYS_SEARCH_USER_PATHS = NO; - CLANG_ANALYZER_NONNULL = YES; - CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; - CLANG_CXX_LANGUAGE_STANDARD = "gnu++14"; - CLANG_CXX_LIBRARY = "libc++"; - CLANG_ENABLE_MODULES = YES; - CLANG_ENABLE_OBJC_ARC = YES; - CLANG_ENABLE_OBJC_WEAK = YES; - CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; - CLANG_WARN_BOOL_CONVERSION = YES; - CLANG_WARN_COMMA = YES; - CLANG_WARN_CONSTANT_CONVERSION = YES; - CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; - CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; - CLANG_WARN_DOCUMENTATION_COMMENTS = YES; - CLANG_WARN_EMPTY_BODY = YES; - CLANG_WARN_ENUM_CONVERSION = YES; - CLANG_WARN_INFINITE_RECURSION = YES; - CLANG_WARN_INT_CONVERSION = YES; - CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; - CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; - CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; - CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; - CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; - CLANG_WARN_STRICT_PROTOTYPES = YES; - CLANG_WARN_SUSPICIOUS_MOVE = YES; - CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; - CLANG_WARN_UNREACHABLE_CODE = YES; - CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; - COPY_PHASE_STRIP = NO; - CURRENT_PROJECT_VERSION = 1; - DEBUG_INFORMATION_FORMAT = dwarf; - ENABLE_STRICT_OBJC_MSGSEND = YES; - ENABLE_TESTABILITY = YES; - GCC_C_LANGUAGE_STANDARD = gnu11; - GCC_DYNAMIC_NO_PIC = NO; - GCC_NO_COMMON_BLOCKS = YES; - GCC_OPTIMIZATION_LEVEL = 0; - GCC_PREPROCESSOR_DEFINITIONS = ( - "DEBUG=1", - "$(inherited)", - ); - GCC_WARN_64_TO_32_BIT_CONVERSION = YES; - GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; - GCC_WARN_UNDECLARED_SELECTOR = YES; - GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; - GCC_WARN_UNUSED_FUNCTION = YES; - GCC_WARN_UNUSED_VARIABLE = YES; - IPHONEOS_DEPLOYMENT_TARGET = 8.0; - MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE; - MTL_FAST_MATH = YES; - ONLY_ACTIVE_ARCH = YES; - SDKROOT = iphoneos; - SWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG; - SWIFT_OPTIMIZATION_LEVEL = "-Onone"; - VERSIONING_SYSTEM = "apple-generic"; - VERSION_INFO_PREFIX = ""; - }; - name = Debug; - }; - 4B08E1FE23E73380003504E1 /* Release */ = { - isa = XCBuildConfiguration; - baseConfigurationReference = 4B08E20523E733F9003504E1 /* Release.xcconfig */; - buildSettings = { - ALWAYS_SEARCH_USER_PATHS = NO; - CLANG_ANALYZER_NONNULL = YES; - CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; - CLANG_CXX_LANGUAGE_STANDARD = "gnu++14"; - CLANG_CXX_LIBRARY = "libc++"; - CLANG_ENABLE_MODULES = YES; - CLANG_ENABLE_OBJC_ARC = YES; - CLANG_ENABLE_OBJC_WEAK = YES; - CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; - CLANG_WARN_BOOL_CONVERSION = YES; - CLANG_WARN_COMMA = YES; - CLANG_WARN_CONSTANT_CONVERSION = YES; - CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; - CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; - CLANG_WARN_DOCUMENTATION_COMMENTS = YES; - CLANG_WARN_EMPTY_BODY = YES; - CLANG_WARN_ENUM_CONVERSION = YES; - CLANG_WARN_INFINITE_RECURSION = YES; - CLANG_WARN_INT_CONVERSION = YES; - CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; - CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; - CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; - CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; - CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; - CLANG_WARN_STRICT_PROTOTYPES = YES; - CLANG_WARN_SUSPICIOUS_MOVE = YES; - CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; - CLANG_WARN_UNREACHABLE_CODE = YES; - CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; - COPY_PHASE_STRIP = NO; - CURRENT_PROJECT_VERSION = 1; - DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; - ENABLE_NS_ASSERTIONS = NO; - ENABLE_STRICT_OBJC_MSGSEND = YES; - GCC_C_LANGUAGE_STANDARD = gnu11; - GCC_NO_COMMON_BLOCKS = YES; - GCC_WARN_64_TO_32_BIT_CONVERSION = YES; - GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; - GCC_WARN_UNDECLARED_SELECTOR = YES; - GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; - GCC_WARN_UNUSED_FUNCTION = YES; - GCC_WARN_UNUSED_VARIABLE = YES; - IPHONEOS_DEPLOYMENT_TARGET = 8.0; - MTL_ENABLE_DEBUG_INFO = NO; - MTL_FAST_MATH = YES; - SDKROOT = iphoneos; - SWIFT_COMPILATION_MODE = wholemodule; - SWIFT_OPTIMIZATION_LEVEL = "-O"; - VALIDATE_PRODUCT = YES; - VERSIONING_SYSTEM = "apple-generic"; - VERSION_INFO_PREFIX = ""; - }; - name = Release; - }; - 4B08E20023E73380003504E1 /* Debug */ = { - isa = XCBuildConfiguration; - buildSettings = { - CLANG_ENABLE_MODULES = YES; - CODE_SIGN_STYLE = Automatic; - DEFINES_MODULE = YES; - DEVELOPMENT_TEAM = 84Z2AMFMF3; - DYLIB_COMPATIBILITY_VERSION = 1; - DYLIB_CURRENT_VERSION = 1; - DYLIB_INSTALL_NAME_BASE = "@rpath"; - INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks"; - LD_RUNPATH_SEARCH_PATHS = ( - "$(inherited)", - "@executable_path/Frameworks", - "@loader_path/Frameworks", - ); - MARKETING_VERSION = 1.1.2; - PRODUCT_BUNDLE_IDENTIFIER = "com.mr-noone.keychain-kit"; - SKIP_INSTALL = YES; - SWIFT_OPTIMIZATION_LEVEL = "-Onone"; - SWIFT_VERSION = 5.0; - TARGETED_DEVICE_FAMILY = "1,2"; - }; - name = Debug; - }; - 4B08E20123E73380003504E1 /* Release */ = { - isa = XCBuildConfiguration; - buildSettings = { - CLANG_ENABLE_MODULES = YES; - CODE_SIGN_STYLE = Automatic; - DEFINES_MODULE = YES; - DEVELOPMENT_TEAM = 84Z2AMFMF3; - DYLIB_COMPATIBILITY_VERSION = 1; - DYLIB_CURRENT_VERSION = 1; - DYLIB_INSTALL_NAME_BASE = "@rpath"; - INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks"; - LD_RUNPATH_SEARCH_PATHS = ( - "$(inherited)", - "@executable_path/Frameworks", - "@loader_path/Frameworks", - ); - MARKETING_VERSION = 1.1.2; - PRODUCT_BUNDLE_IDENTIFIER = "com.mr-noone.keychain-kit"; - SKIP_INSTALL = YES; - SWIFT_VERSION = 5.0; - TARGETED_DEVICE_FAMILY = "1,2"; - }; - name = Release; - }; -/* End XCBuildConfiguration section */ - -/* Begin XCConfigurationList section */ - 4B08E1F123E73380003504E1 /* Build configuration list for PBXProject "keychain-kit" */ = { - isa = XCConfigurationList; - buildConfigurations = ( - 4B08E1FD23E73380003504E1 /* Debug */, - 4B08E1FE23E73380003504E1 /* Release */, - ); - defaultConfigurationIsVisible = 0; - defaultConfigurationName = Release; - }; - 4B08E1FF23E73380003504E1 /* Build configuration list for PBXNativeTarget "keychain-kit" */ = { - isa = XCConfigurationList; - buildConfigurations = ( - 4B08E20023E73380003504E1 /* Debug */, - 4B08E20123E73380003504E1 /* Release */, - ); - defaultConfigurationIsVisible = 0; - defaultConfigurationName = Release; - }; -/* End XCConfigurationList section */ - }; - rootObject = 4B08E1EE23E73380003504E1 /* Project object */; -} diff --git a/keychain-kit/Support Files/Config.xcconfig b/keychain-kit/Support Files/Config.xcconfig deleted file mode 100644 index ae8eefd..0000000 --- a/keychain-kit/Support Files/Config.xcconfig +++ /dev/null @@ -1,13 +0,0 @@ -// -// Config.xcconfig -// keychain-kit -// -// Created by Aleksey Zgurskiy on 02.02.2020. -// Copyright © 2020 mr.noone. All rights reserved. -// - -// Configuration settings file format documentation can be found at: -// https://help.apple.com/xcode/#/dev745c5c974 - -INFOPLIST_FILE = keychain-kit/Support Files/Info.plist -PRODUCT_NAME = KeychainKit // $(TARGET_NAME:c99extidentifier) diff --git a/keychain-kit/Support Files/Debug.xcconfig b/keychain-kit/Support Files/Debug.xcconfig deleted file mode 100644 index a30dcfa..0000000 --- a/keychain-kit/Support Files/Debug.xcconfig +++ /dev/null @@ -1,12 +0,0 @@ -// -// Debug.xcconfig -// keychain-kit -// -// Created by Aleksey Zgurskiy on 02.02.2020. -// Copyright © 2020 mr.noone. All rights reserved. -// - -// Configuration settings file format documentation can be found at: -// https://help.apple.com/xcode/#/dev745c5c974 - -#include "Config.xcconfig" diff --git a/keychain-kit/Support Files/Info.plist b/keychain-kit/Support Files/Info.plist deleted file mode 100644 index c0701c6..0000000 --- a/keychain-kit/Support Files/Info.plist +++ /dev/null @@ -1,22 +0,0 @@ - - - - - CFBundleDevelopmentRegion - $(DEVELOPMENT_LANGUAGE) - CFBundleExecutable - $(EXECUTABLE_NAME) - CFBundleIdentifier - $(PRODUCT_BUNDLE_IDENTIFIER) - CFBundleInfoDictionaryVersion - 6.0 - CFBundleName - $(PRODUCT_NAME) - CFBundlePackageType - $(PRODUCT_BUNDLE_PACKAGE_TYPE) - CFBundleShortVersionString - $(MARKETING_VERSION) - CFBundleVersion - $(CURRENT_PROJECT_VERSION) - - diff --git a/keychain-kit/Support Files/Release.xcconfig b/keychain-kit/Support Files/Release.xcconfig deleted file mode 100644 index f8da04d..0000000 --- a/keychain-kit/Support Files/Release.xcconfig +++ /dev/null @@ -1,12 +0,0 @@ -// -// Release.xcconfig -// keychain-kit -// -// Created by Aleksey Zgurskiy on 02.02.2020. -// Copyright © 2020 mr.noone. All rights reserved. -// - -// Configuration settings file format documentation can be found at: -// https://help.apple.com/xcode/#/dev745c5c974 - -#include "Config.xcconfig" diff --git a/keychain-kit/Support Files/keychain_kit.h b/keychain-kit/Support Files/keychain_kit.h deleted file mode 100644 index bae651a..0000000 --- a/keychain-kit/Support Files/keychain_kit.h +++ /dev/null @@ -1,19 +0,0 @@ -// -// keychain_kit.h -// keychain-kit -// -// Created by Aleksey Zgurskiy on 02.02.2020. -// Copyright © 2020 mr.noone. All rights reserved. -// - -#import - -//! Project version number for keychain_kit. -FOUNDATION_EXPORT double keychain_kitVersionNumber; - -//! Project version string for keychain_kit. -FOUNDATION_EXPORT const unsigned char keychain_kitVersionString[]; - -// In this header, you should import all the public headers of your framework using statements like #import - - From bb3d349993c7c1284726405570d5165232fd9892 Mon Sep 17 00:00:00 2001 From: Oleksii Zghurskyi Date: Mon, 7 Jul 2025 18:30:06 +0300 Subject: [PATCH 04/10] Improved type safety, local auth support, and better error handling --- KeychainKit.xcodeproj/KeychainKit_Info.plist | 25 -- KeychainKit.xcodeproj/project.pbxproj | 383 ------------------ .../contents.xcworkspacedata | 7 - .../xcshareddata/WorkspaceSettings.xcsettings | 8 - .../xcschemes/KeychainKit-Package.xcscheme | 24 -- LICENSE | 2 +- Package.swift | 19 +- README.md | 61 ++- .../KeychainKit/Classes/KeychainStorage.swift | 272 +++++++++++++ Sources/KeychainKit/Enums/KeychainError.swift | 15 + Sources/KeychainKit/Keychain.swift | 117 ------ .../Protocols/KeychainAccountProtocol.swift | 47 +++ .../Protocols/KeychainServiceProtocol.swift | 28 ++ .../Protocols/KeychainStorageProtocol.swift | 124 ++++++ 14 files changed, 556 insertions(+), 576 deletions(-) delete mode 100644 KeychainKit.xcodeproj/KeychainKit_Info.plist delete mode 100644 KeychainKit.xcodeproj/project.pbxproj delete mode 100644 KeychainKit.xcodeproj/project.xcworkspace/contents.xcworkspacedata delete mode 100644 KeychainKit.xcodeproj/project.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings delete mode 100644 KeychainKit.xcodeproj/xcshareddata/xcschemes/KeychainKit-Package.xcscheme create mode 100644 Sources/KeychainKit/Classes/KeychainStorage.swift create mode 100644 Sources/KeychainKit/Enums/KeychainError.swift delete mode 100644 Sources/KeychainKit/Keychain.swift create mode 100644 Sources/KeychainKit/Protocols/KeychainAccountProtocol.swift create mode 100644 Sources/KeychainKit/Protocols/KeychainServiceProtocol.swift create mode 100644 Sources/KeychainKit/Protocols/KeychainStorageProtocol.swift 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) +} From d9eaa9df3f0a3a2db227b2344b138042b92134c9 Mon Sep 17 00:00:00 2001 From: Oleksii Zghurskyi Date: Tue, 8 Jul 2025 21:24:45 +0300 Subject: [PATCH 05/10] Add docc plugin --- Package.swift | 3 +++ 1 file changed, 3 insertions(+) diff --git a/Package.swift b/Package.swift index c5dc016..88859d3 100644 --- a/Package.swift +++ b/Package.swift @@ -9,6 +9,9 @@ let package = Package( products: [ .library(name: "KeychainKit", targets: ["KeychainKit"]), ], + dependencies: [ + .package(url: "https://github.com/apple/swift-docc-plugin", from: "1.0.0") + ], targets: [ .target(name: "KeychainKit") ] From bd4d2133a279fa1c9877f72428b896cda533f0f2 Mon Sep 17 00:00:00 2001 From: Oleksii Zghurskyi Date: Tue, 22 Jul 2025 21:42:13 +0300 Subject: [PATCH 06/10] Move default implementations of keychain get/set methods into protocol extension --- .gitignore | 1 + .../KeychainKit/Classes/KeychainStorage.swift | 140 +++--------------- .../Protocols/KeychainStorageProtocol.swift | 100 +++++++++++++ 3 files changed, 120 insertions(+), 121 deletions(-) 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/Sources/KeychainKit/Classes/KeychainStorage.swift b/Sources/KeychainKit/Classes/KeychainStorage.swift index 8529d7e..687abba 100644 --- a/Sources/KeychainKit/Classes/KeychainStorage.swift +++ b/Sources/KeychainKit/Classes/KeychainStorage.swift @@ -20,17 +20,11 @@ import Security /// /// ### Retrieving Values /// -/// - ``get(_:)-5u61a`` -/// - ``get(_:)-502rt`` -/// - ``get(_:)-63a3x`` -/// - ``get(_:decoder:)`` +/// - ``get(_:)`` /// /// ### Storing Values /// -/// - ``set(_:for:)-7053g`` -/// - ``set(_:for:)-99s6o`` -/// - ``set(_:for:)-2e1p6`` -/// - ``set(_:for:encoder:)`` +/// - ``set(_:for:)`` /// /// ### Deleting Values /// @@ -61,14 +55,15 @@ public final class KeychainStorage< // MARK: - Methods - /// Retrieves raw `Data` stored in Keychain for the specified account. + /// Retrieves raw `Data` stored in the 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. + /// - 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 stored data is missing or corrupted. - /// - Throws: ``KeychainError/unexpectedCode(_:)`` for any other OSStatus error returned by the Keychain API. + /// - 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, @@ -105,67 +100,19 @@ public final class KeychainStorage< } } - /// Retrieves a UTF-8 encoded string stored in Keychain for the specified account. + /// Stores raw `Data` in the keychain for the specified account, replacing any existing value. /// - /// - 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. + /// 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 raw data to store. /// - 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. + /// - 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) @@ -197,61 +144,12 @@ public final class KeychainStorage< } } - /// Stores a UTF-8 encoded string in the Keychain for the specified account. + /// Deletes the keychain item associated with 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. + /// 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 deletion fails with an unexpected OSStatus. + /// - Throws: ``KeychainError/unexpectedCode(_:)`` if the deletion fails with an unexpected OSStatus. public func delete(_ account: Account) throws(KeychainError) { var query: [CFString: Any] = [ kSecClass: kSecClassGenericPassword, diff --git a/Sources/KeychainKit/Protocols/KeychainStorageProtocol.swift b/Sources/KeychainKit/Protocols/KeychainStorageProtocol.swift index 943ec6b..1f7c27a 100644 --- a/Sources/KeychainKit/Protocols/KeychainStorageProtocol.swift +++ b/Sources/KeychainKit/Protocols/KeychainStorageProtocol.swift @@ -122,3 +122,103 @@ public protocol KeychainStorageProtocol { /// - 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, + 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) + } + } +} From e8bb90bbb9ca7f69b5f2d205de1529912e1aae58 Mon Sep 17 00:00:00 2001 From: Oleksii Zghurskyi Date: Tue, 22 Jul 2025 21:44:17 +0300 Subject: [PATCH 07/10] Update readme --- README.md | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index ad1492a..1888a5c 100644 --- a/README.md +++ b/README.md @@ -24,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., `2.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 @@ -38,7 +38,7 @@ import PackageDescription let package = Package( name: "YourProject", dependencies: [ - .package(url: "https://github.com/angd-dev/keychain-kit.git", from: "2.0.0") + .package(url: "https://github.com/angd-dev/keychain-kit.git", from: "2.1.0") ], targets: [ .target( @@ -53,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=2.0.0). +For more information and usage examples, see the [documentation](https://docs.angd.dev/?package=keychain-kit&version=2.1.0). ## License From e1a610b19fa459eb4725a6621a055635136569e4 Mon Sep 17 00:00:00 2001 From: Oleksii Zghurskyi Date: Sun, 27 Jul 2025 15:48:52 +0300 Subject: [PATCH 08/10] Add Equatable protocol for KeychainError --- Sources/KeychainKit/Enums/KeychainError.swift | 28 ++++++++++++++++++- 1 file changed, 27 insertions(+), 1 deletion(-) diff --git a/Sources/KeychainKit/Enums/KeychainError.swift b/Sources/KeychainKit/Enums/KeychainError.swift index fb416fd..0753cfd 100644 --- a/Sources/KeychainKit/Enums/KeychainError.swift +++ b/Sources/KeychainKit/Enums/KeychainError.swift @@ -1,7 +1,7 @@ import Foundation /// Errors that can occur during Keychain operations. -public enum KeychainError: Error { +public enum KeychainError: Error, Equatable { /// Authentication failed, e.g., due to biometric or passcode denial. case authenticationFailed /// No item found matching the query. @@ -12,4 +12,30 @@ public enum KeychainError: Error { case unexpectedCode(OSStatus) /// A generic unexpected error, with optional underlying error info. case unexpectedError(Error?) + + /// Compares two `KeychainError` values for equality. + /// + /// - Parameters: + /// - lhs: The first `KeychainError` to compare. + /// - rhs: The second `KeychainError` to compare. + /// - Returns: `true` if both errors are of the same case and represent the same error details. + /// + /// For `.unexpectedError`, the comparison is based on the underlying `NSError` identity, + /// which includes domain and error code. + public static func == (lhs: Self, rhs: Self) -> Bool { + switch (lhs, rhs) { + case (.authenticationFailed, .authenticationFailed): + true + case (.itemNotFound, .itemNotFound): + true + case (.unexpectedData, .unexpectedData): + true + case (.unexpectedCode(let lCode), .unexpectedCode(let rCode)): + lCode == rCode + case (.unexpectedError(let lErr), .unexpectedError(let rErr)): + lErr as NSError? == rErr as NSError? + default: + false + } + } } From e0af36f9a65391cab9c3ea079297420096a02a84 Mon Sep 17 00:00:00 2001 From: Oleksii Zghurskyi Date: Sun, 17 Aug 2025 20:28:17 +0300 Subject: [PATCH 09/10] Version 3 --- .swift-format | 15 ++ Package.swift | 10 +- README.md | 11 +- .../KeychainKit/Classes/KeychainStorage.swift | 167 +++++++----- Sources/KeychainKit/Enums/KeychainError.swift | 65 ++--- .../KeychainKit/Extensions/String+Error.swift | 15 ++ .../Protocols/KeychainAccountProtocol.swift | 46 ++-- .../Protocols/KeychainServiceProtocol.swift | 29 +- .../Protocols/KeychainStorageProtocol.swift | 255 ++++++++---------- .../Resources/Localizable.xcstrings | 56 ++++ 10 files changed, 376 insertions(+), 293 deletions(-) create mode 100644 .swift-format create mode 100644 Sources/KeychainKit/Extensions/String+Error.swift create mode 100644 Sources/KeychainKit/Resources/Localizable.xcstrings 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/Package.swift b/Package.swift index 88859d3..8a35735 100644 --- a/Package.swift +++ b/Package.swift @@ -1,18 +1,22 @@ -// swift-tools-version: 5.10 +// 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: [.macOS(.v10_15), .iOS(.v13)], + 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") + .target(name: "KeychainKit", dependencies: [ + .product(name: "Localizable", package: "localizable") + ]) ] ) diff --git a/README.md b/README.md index 1888a5c..c73e91a 100644 --- a/README.md +++ b/README.md @@ -10,11 +10,6 @@ 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). @@ -24,7 +19,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., `2.1.0`). +4. Choose the version to install (e.g., `3.0.0`). 5. Add the library to your target module. ### Adding to Package.swift @@ -38,7 +33,7 @@ import PackageDescription let package = Package( name: "YourProject", dependencies: [ - .package(url: "https://github.com/angd-dev/keychain-kit.git", from: "2.1.0") + .package(url: "https://github.com/angd-dev/keychain-kit.git", from: "3.0.0") ], targets: [ .target( @@ -53,7 +48,7 @@ let package = Package( ## Additional Resources -For more information and usage examples, see the [documentation](https://docs.angd.dev/?package=keychain-kit&version=2.1.0). +For more information and usage examples, see the [documentation](https://docs.angd.dev/?package=keychain-kit&version=3.0.0). ## License diff --git a/Sources/KeychainKit/Classes/KeychainStorage.swift b/Sources/KeychainKit/Classes/KeychainStorage.swift index 687abba..9788e58 100644 --- a/Sources/KeychainKit/Classes/KeychainStorage.swift +++ b/Sources/KeychainKit/Classes/KeychainStorage.swift @@ -2,10 +2,11 @@ import Foundation import LocalAuthentication import Security -/// A type-safe storage abstraction over the Keychain service. +/// A service that provides access and management for keychain items. /// -/// Supports storing, retrieving, and deleting generic data associated with -/// accounts and services, with optional local authentication context support. +/// 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 /// @@ -18,36 +19,31 @@ import Security /// - ``service`` /// - ``context`` /// -/// ### Retrieving Values +/// ### Instance Methods /// -/// - ``get(_:)`` -/// -/// ### Storing Values -/// -/// - ``set(_:for:)`` -/// -/// ### Deleting Values -/// -/// - ``delete(_:)`` +/// - ``get(by:)->Data?`` +/// - ``insert(_:by:)-(Data,_)`` +/// - ``delete(by:)`` +/// - ``exists(by:)`` public final class KeychainStorage< Account: KeychainAccountProtocol, Service: KeychainServiceProtocol ->: KeychainStorageProtocol { +>: KeychainStorageProtocol, @unchecked Sendable { // MARK: - Properties - /// The service metadata associated with this Keychain storage instance. + /// The service descriptor associated with this keychain storage. public let service: Service? - /// An optional local authentication context used for biometric or passcode protection. + /// The authentication context used for keychain operations. public let context: LAContext? - // MARK: - Inits + // MARK: - Initialization - /// Creates a new `KeychainStorage` instance with the given service and authentication context. + /// Creates a new keychain storage instance. /// /// - Parameters: - /// - service: An optional `Service` instance representing the keychain service metadata. - /// - context: An optional `LAContext` instance for authentication protection. + /// - 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 @@ -55,23 +51,20 @@ public final class KeychainStorage< // MARK: - Methods - /// Retrieves raw `Data` stored in the keychain for the specified account. + /// Retrieves raw data for the given account. /// - /// - 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 { + /// - 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, - kSecReturnAttributes: true, kSecReturnData: true ] @@ -79,50 +72,41 @@ public final class KeychainStorage< query[kSecAttrAccessGroup] = service?.accessGroup query[kSecUseAuthenticationContext] = context - var queryResult: AnyObject? - let status = withUnsafeMutablePointer(to: &queryResult) { - SecItemCopyMatching(query as CFDictionary, UnsafeMutablePointer($0)) - } + var result: AnyObject? - switch status { + switch SecItemCopyMatching(query as CFDictionary, &result) { case errSecSuccess: - guard - let item = queryResult as? [CFString : AnyObject], - let data = item[kSecValueData] as? Data - else { throw KeychainError.unexpectedData } - return data + if let data = result as? Data { + return data + } else { + throw .invalidData + } case errSecItemNotFound: - throw KeychainError.itemNotFound - case errSecAuthFailed: - throw KeychainError.authenticationFailed - default: - throw KeychainError.unexpectedCode(status) + return nil + case errSecAuthFailed, errSecInteractionNotAllowed, errSecUserCanceled: + throw .authenticationFailed + case let status: + throw .osStatus(status) } } - /// 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. + /// Inserts raw data for the given account. /// /// - Parameters: - /// - 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) - + /// - 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 { - throw KeychainError.unexpectedError(error?.takeUnretainedValue()) + let error = error?.takeRetainedValue() + throw .underlying(error as? NSError) } var query: [CFString: Any] = [ @@ -138,19 +122,22 @@ public final class KeychainStorage< query[kSecAttrAccessGroup] = service?.accessGroup query[kSecUseAuthenticationContext] = context - let status = SecItemAdd(query as CFDictionary, nil) - guard status == noErr else { - throw KeychainError.unexpectedCode(status) + switch SecItemAdd(query as CFDictionary, nil) { + case errSecSuccess: + return + case errSecDuplicateItem: + throw .duplicateItem + case let status: + throw .osStatus(status) } } - /// Deletes the keychain item associated with the specified account. + /// Deletes the item for the given account. /// - /// 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) { + /// - 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, @@ -162,9 +149,45 @@ public final class KeychainStorage< query[kSecAttrAccessGroup] = service?.accessGroup query[kSecUseAuthenticationContext] = context - let status = SecItemDelete(query as CFDictionary) - guard status == errSecSuccess || status == errSecItemNotFound else { - throw KeychainError.unexpectedCode(status) + 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 index 0753cfd..aa48d18 100644 --- a/Sources/KeychainKit/Enums/KeychainError.swift +++ b/Sources/KeychainKit/Enums/KeychainError.swift @@ -1,41 +1,44 @@ import Foundation -/// Errors that can occur during Keychain operations. +/// 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 failed, e.g., due to biometric or passcode denial. + /// Authentication was required but failed or was canceled. 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?) - /// Compares two `KeychainError` values for equality. + /// 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. /// - /// - Parameters: - /// - lhs: The first `KeychainError` to compare. - /// - rhs: The second `KeychainError` to compare. - /// - Returns: `true` if both errors are of the same case and represent the same error details. + /// - Parameter status: The underlying `OSStatus` value. + case osStatus(OSStatus) + + /// A lower-level error occurred during encoding, decoding, or other processing. /// - /// For `.unexpectedError`, the comparison is based on the underlying `NSError` identity, - /// which includes domain and error code. - public static func == (lhs: Self, rhs: Self) -> Bool { - switch (lhs, rhs) { - case (.authenticationFailed, .authenticationFailed): - true - case (.itemNotFound, .itemNotFound): - true - case (.unexpectedData, .unexpectedData): - true - case (.unexpectedCode(let lCode), .unexpectedCode(let rCode)): - lCode == rCode - case (.unexpectedError(let lErr), .unexpectedError(let rErr)): - lErr as NSError? == rErr as NSError? - default: - false + /// - 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/Protocols/KeychainAccountProtocol.swift b/Sources/KeychainKit/Protocols/KeychainAccountProtocol.swift index 0201eb4..67b1da1 100644 --- a/Sources/KeychainKit/Protocols/KeychainAccountProtocol.swift +++ b/Sources/KeychainKit/Protocols/KeychainAccountProtocol.swift @@ -1,47 +1,53 @@ import Foundation -/// A protocol that defines the required properties for a keychain account descriptor. +/// A type that describes a keychain account configuration for secure item storage and access. /// -/// 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. +/// 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 for the account. + /// The keychain data protection level assigned to the account. /// - /// Defaults to `kSecAttrAccessibleAfterFirstUnlock`. You may override it to use other - /// accessibility levels, such as `kSecAttrAccessibleWhenUnlocked` - /// or `kSecAttrAccessibleWhenPasscodeSetThisDeviceOnly`. + /// 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 used to define authentication requirements. + /// The access control flags defining additional authentication requirements. /// - /// Defaults to `[]` (no additional access control). Can be overridden to specify - /// constraints such as `.userPresence`, `.biometryAny`, or `.devicePasscode`. + /// Defaults to an empty set (`[]`). Override this to enforce constraints like `.userPresence`, + /// `.biometryAny`, or `.devicePasscode`. var accessFlags: SecAccessControlCreateFlags { get } - /// Whether the item should be marked as synchronizable via iCloud Keychain. + /// Indicates whether the item is synchronized through iCloud Keychain. /// - /// Defaults to `false`. Set to `true` if the item should sync across devices. + /// 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 { - /// 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`. + /// A unique string that identifies the keychain account. /// - /// The `identifier` is derived from the raw string value. + /// 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 index ae30994..ab1af9a 100644 --- a/Sources/KeychainKit/Protocols/KeychainServiceProtocol.swift +++ b/Sources/KeychainKit/Protocols/KeychainServiceProtocol.swift @@ -1,28 +1,33 @@ import Foundation -/// A protocol that defines the required properties for a keychain service descriptor. +/// A type that describes a keychain service used to group and identify stored items. /// -/// 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. +/// 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 to support shared access between apps. + + /// An optional keychain access group identifier that enables shared access between apps. /// - /// The default implementation returns `nil`, indicating no access group is specified. + /// Defaults to `nil`, meaning 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`. + /// A unique string that identifies the keychain service. /// - /// The `identifier` is derived from the raw string value. + /// 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 index 1f7c27a..bf22753 100644 --- a/Sources/KeychainKit/Protocols/KeychainStorageProtocol.swift +++ b/Sources/KeychainKit/Protocols/KeychainStorageProtocol.swift @@ -1,13 +1,9 @@ import Foundation -/// A protocol that defines a type-safe interface for storing and retrieving values -/// in the system keychain. +/// A type that provides access to data stored in the 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. +/// Conforming types define how items are encoded, saved, and accessed securely, using account and +/// service descriptors to identify individual entries. /// /// ## Topics /// @@ -20,205 +16,170 @@ import Foundation /// /// - ``service`` /// -/// ### Retrieving Values +/// ### Retrieving Items /// -/// - ``get(_:)-2gcee`` -/// - ``get(_:)-23z7h`` -/// - ``get(_:)-4xbe6`` -/// - ``get(_:decoder:)`` +/// - ``get(by:)->Data?`` +/// - ``get(by:)->String?`` +/// - ``get(by:)->UUID?`` +/// - ``get(by:decoder:)`` /// -/// ### Storing Values +/// ### Inserting Items /// -/// - ``set(_:for:)-21dla`` -/// - ``set(_:for:)-6nzkf`` -/// - ``set(_:for:)-2smpc`` -/// - ``set(_:for:encoder:)`` +/// - ``insert(_:by:)-(Data,_)`` +/// - ``insert(_:by:)-(String,_)`` +/// - ``insert(_:by:)-(UUID,_)`` +/// - ``insert(_:by:encoder:)`` /// -/// ### Deleting Values +/// ### Deleting Items /// -/// - ``delete(_:)`` -public protocol KeychainStorageProtocol { - /// A type that describes a keychain account and its security configuration. +/// - ``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 identifies a keychain service context (e.g., app or subsystem). + /// A type that describes a keychain service used to group stored items. 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. + // MARK: - Properties + + /// The keychain service associated with this storage instance. 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 + // MARK: - Methods - /// Retrieves the value stored in the keychain for the specified account as a UTF-8 string. + /// Retrieves raw data for the given account. /// - /// - 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 + /// - 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? - /// 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. + /// Inserts raw data for the given 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 + /// - 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) - /// Stores raw `Data` in the keychain for the specified account. + /// Deletes the item for the given 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) + /// - Parameter account: The account descriptor identifying the item to remove. + /// - Throws: ``KeychainError`` if the operation fails. + func delete(by account: Account) throws(KeychainError) - /// Stores a UTF-8 encoded `String` in the keychain for the specified account. + /// Checks whether an item exists for the given 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) + /// - 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 encoded string stored in the keychain for the specified account. + /// Retrieves a UTF-8 string for the given 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 + /// - 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 value + return string } - /// Retrieves a `UUID` stored in the keychain for the specified account. + /// Retrieves a UUID for the given 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 + /// - 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 value + return uuid } - /// Retrieves a value of type `T` stored in the keychain and decodes it from JSON using the given decoder. + /// Retrieves and decodes a `Decodable` value for the given account. /// /// - 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. + /// - 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( - _ account: Account, + by account: Account, decoder: JSONDecoder = .init() - ) throws(KeychainError) -> T { - let value: Data = try get(account) + ) throws(KeychainError) -> T? { + guard let data = try get(by: account) else { return nil } do { - return try decoder.decode(T.self, from: value) + return try decoder.decode(T.self, from: data) } catch { - throw KeychainError.unexpectedError(error) + throw .underlying(error as NSError) } } - - /// Stores a UTF-8 encoded string in the keychain for the specified account. +} + +// MARK: - Set Extension + +public extension KeychainStorageProtocol { + /// Inserts a UTF-8 string for the given 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) { + /// - 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 KeychainError.unexpectedData + throw .invalidData } - try set(data, for: account) + try insert(data, by: account) } - /// Stores a `UUID` value as a UTF-8 encoded string in the keychain for the specified account. + /// Inserts a UUID for the given 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) + /// - 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) } - /// Stores an `Encodable` value in the keychain as JSON-encoded data for the specified account. + /// Encodes and inserts an `Encodable` value for the given 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( + /// - 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, - for account: Account, + by account: Account, encoder: JSONEncoder = .init() ) throws(KeychainError) { + let data: Data do { - let data = try encoder.encode(value) - try set(data, for: account) - } catch let error as KeychainError { - throw error + data = try encoder.encode(value) } catch { - throw KeychainError.unexpectedError(error) + 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 From daec0db688c82edb9c246dbb36b797da1c33a94c Mon Sep 17 00:00:00 2001 From: Oleksii Zghurskyi Date: Wed, 17 Sep 2025 19:33:01 +0300 Subject: [PATCH 10/10] Fix resources --- Package.swift | 14 ++++++++++---- 1 file changed, 10 insertions(+), 4 deletions(-) diff --git a/Package.swift b/Package.swift index 8a35735..273919b 100644 --- a/Package.swift +++ b/Package.swift @@ -8,15 +8,21 @@ let package = Package( defaultLocalization: "en", platforms: [.macOS(.v12), .iOS(.v15)], 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") - ]) + .target( + name: "KeychainKit", + dependencies: [ + .product(name: "Localizable", package: "localizable") + ], + resources: [ + .process("Resources/Localizable.xcstrings") + ] + ) ] )