diff --git a/.gitignore b/.gitignore index 67356a1..70e6dad 100644 --- a/.gitignore +++ b/.gitignore @@ -4,7 +4,6 @@ .build/ ## Various settings -Package.resolved *.pbxuser !default.pbxuser *.mode1v3 diff --git a/.swift-format b/.swift-format deleted file mode 100644 index b77abe6..0000000 --- a/.swift-format +++ /dev/null @@ -1,15 +0,0 @@ -{ - "fileScopedDeclarationPrivacy": { - "accessLevel": "private" - }, - "indentBlankLines": true, - "indentation": { - "spaces": 4 - }, - "lineLength": 9999, - "maximumBlankLines": 1, - "multiElementCollectionTrailingCommas": false, - "rules": { - "FileScopedDeclarationPrivacy": true - } -} diff --git a/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/KeychainKit.xcodeproj/project.xcworkspace/contents.xcworkspacedata b/KeychainKit.xcodeproj/project.xcworkspace/contents.xcworkspacedata new file mode 100644 index 0000000..fe1aa71 --- /dev/null +++ b/KeychainKit.xcodeproj/project.xcworkspace/contents.xcworkspacedata @@ -0,0 +1,7 @@ + + + + + \ No newline at end of file diff --git a/KeychainKit.xcodeproj/project.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings b/KeychainKit.xcodeproj/project.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings new file mode 100644 index 0000000..a72dc2b --- /dev/null +++ b/KeychainKit.xcodeproj/project.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings @@ -0,0 +1,8 @@ + + + + + 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/LICENSE b/LICENSE index b033d92..7365fe7 100644 --- a/LICENSE +++ b/LICENSE @@ -1,6 +1,6 @@ MIT License -Copyright (c) 2025 ANGD Dev +Copyright (c) 2020 Aleksey Zgurskiy 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 273919b..e11cace 100644 --- a/Package.swift +++ b/Package.swift @@ -1,28 +1,16 @@ -// swift-tools-version: 6.0 +// 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", - defaultLocalization: "en", - platforms: [.macOS(.v12), .iOS(.v15)], - products: [ - .library(name: "KeychainKit", targets: ["KeychainKit"]) - ], - dependencies: [ - .package(url: "https://github.com/angd-dev/localizable.git", from: "1.0.0"), - .package(url: "https://github.com/apple/swift-docc-plugin", from: "1.0.0") - ], - targets: [ - .target( - name: "KeychainKit", - dependencies: [ - .product(name: "Localizable", package: "localizable") - ], - resources: [ - .process("Resources/Localizable.xcstrings") - ] - ) - ] + name: "KeychainKit", + platforms: [.iOS(.v8)], + products: [ + .library(name: "KeychainKit", targets: ["KeychainKit"]), + ], + dependencies: [], + targets: [ + .target(name: "KeychainKit", dependencies: []) + ] ) diff --git a/README.md b/README.md index c73e91a..f8af6be 100644 --- a/README.md +++ b/README.md @@ -1,55 +1 @@ -# KeychainKit - -KeychainKit is a type-safe, easy-to-use wrapper around Apple’s Keychain service that supports storing, retrieving, and deleting data with optional local authentication. - -## Overview - -This library enables working with Keychain without losing control over security settings while simplifying type-safe access to data types like `Data`, `String`, `UUID`, and any `Codable` types. - -It supports optional authentication via `LAContext`, allowing integration with Face ID, Touch ID, or device passcode. - -KeychainKit does not hide the complexity of Keychain operations but provides a clean API and convenient error handling via a custom `KeychainError` type. - -## Installation - -To add KeychainKit to your project, use Swift Package Manager (SPM). - -### Adding to an Xcode Project - -1. Open your project in Xcode. -2. Navigate to the `File` menu and select `Add Package Dependencies`. -3. Enter the repository URL: `https://github.com/angd-dev/keychain-kit.git` -4. Choose the version to install (e.g., `3.0.0`). -5. Add the library to your target module. - -### Adding to Package.swift - -If you are using Swift Package Manager with a `Package.swift` file, add the dependency like this: - -```swift -// swift-tools-version: 5.10 -import PackageDescription - -let package = Package( - name: "YourProject", - dependencies: [ - .package(url: "https://github.com/angd-dev/keychain-kit.git", from: "3.0.0") - ], - targets: [ - .target( - name: "YourTarget", - dependencies: [ - .product(name: "KeychainKit", package: "keychain-kit") - ] - ) - ] -) -``` - -## Additional Resources - -For more information and usage examples, see the [documentation](https://docs.angd.dev/?package=keychain-kit&version=3.0.0). - -## License - -This project is licensed under the MIT License. See the `LICENSE` file for details. +# keychain-kit \ No newline at end of file diff --git a/Sources/KeychainKit/Classes/KeychainStorage.swift b/Sources/KeychainKit/Classes/KeychainStorage.swift deleted file mode 100644 index 9788e58..0000000 --- a/Sources/KeychainKit/Classes/KeychainStorage.swift +++ /dev/null @@ -1,193 +0,0 @@ -import Foundation -import LocalAuthentication -import Security - -/// A service that provides access and management for keychain items. -/// -/// This type provides direct access to the system keychain using `Security` and -/// `LocalAuthentication` frameworks. It supports querying, inserting, deleting, and checking item -/// existence, while handling authentication contexts and access controls automatically. -/// -/// ## Topics -/// -/// ### Initializers -/// -/// - ``init(service:context:)`` -/// -/// ### Instance Properties -/// -/// - ``service`` -/// - ``context`` -/// -/// ### Instance Methods -/// -/// - ``get(by:)->Data?`` -/// - ``insert(_:by:)-(Data,_)`` -/// - ``delete(by:)`` -/// - ``exists(by:)`` -public final class KeychainStorage< - Account: KeychainAccountProtocol, - Service: KeychainServiceProtocol ->: KeychainStorageProtocol, @unchecked Sendable { - // MARK: - Properties - - /// The service descriptor associated with this keychain storage. - public let service: Service? - - /// The authentication context used for keychain operations. - public let context: LAContext? - - // MARK: - Initialization - - /// Creates a new keychain storage instance. - /// - /// - Parameters: - /// - service: The service descriptor that defines the keychain group and access settings. - /// - context: The authentication context used for secure access, or `nil` to use a default one. - public init(service: Service?, context: LAContext?) { - self.service = service - self.context = context - } - - // MARK: - Methods - - /// Retrieves raw data for the given account. - /// - /// - Parameter account: The account descriptor identifying the stored item. - /// - Returns: The stored data, or `nil` if no item exists. - /// - Throws: ``KeychainError/invalidData`` if the retrieved value cannot be cast to `Data`. - /// - Throws: ``KeychainError/authenticationFailed`` if user authentication fails. - /// - Throws: ``KeychainError/osStatus(_:)`` for unexpected system errors. - public func get(by account: Account) throws(KeychainError) -> Data? { - var query: [CFString: Any] = [ - kSecClass: kSecClassGenericPassword, - kSecAttrAccount: account.identifier, - kSecAttrSynchronizable: account.synchronizable, - kSecUseDataProtectionKeychain: true, - kSecMatchLimit: kSecMatchLimitOne, - kSecReturnData: true - ] - - query[kSecAttrService] = service?.identifier - query[kSecAttrAccessGroup] = service?.accessGroup - query[kSecUseAuthenticationContext] = context - - var result: AnyObject? - - switch SecItemCopyMatching(query as CFDictionary, &result) { - case errSecSuccess: - if let data = result as? Data { - return data - } else { - throw .invalidData - } - case errSecItemNotFound: - return nil - case errSecAuthFailed, errSecInteractionNotAllowed, errSecUserCanceled: - throw .authenticationFailed - case let status: - throw .osStatus(status) - } - } - - /// Inserts raw data for the given account. - /// - /// - Parameters: - /// - value: The data to store. - /// - account: The account descriptor identifying the target item. - /// - Throws: ``KeychainError/underlying(_:)`` if access control creation fails. - /// - Throws: ``KeychainError/duplicateItem`` if an item with the same key already exists. - /// - Throws: ``KeychainError/osStatus(_:)`` for unexpected system errors. - public func insert(_ value: Data, by account: Account) throws(KeychainError) { - var error: Unmanaged? - let access = SecAccessControlCreateWithFlags( - nil, account.protection, account.accessFlags, &error - ) - - guard let access else { - let error = error?.takeRetainedValue() - throw .underlying(error as? NSError) - } - - var query: [CFString: Any] = [ - kSecClass: kSecClassGenericPassword, - kSecAttrAccount: account.identifier, - kSecAttrSynchronizable: account.synchronizable, - kSecUseDataProtectionKeychain: true, - kSecAttrAccessControl: access, - kSecValueData: value - ] - - query[kSecAttrService] = service?.identifier - query[kSecAttrAccessGroup] = service?.accessGroup - query[kSecUseAuthenticationContext] = context - - switch SecItemAdd(query as CFDictionary, nil) { - case errSecSuccess: - return - case errSecDuplicateItem: - throw .duplicateItem - case let status: - throw .osStatus(status) - } - } - - /// Deletes the item for the given account. - /// - /// - Parameter account: The account descriptor identifying the item to remove. - /// - Throws: ``KeychainError/authenticationFailed`` if user authentication fails. - /// - Throws: ``KeychainError/osStatus(_:)`` for unexpected system errors. - public func delete(by account: Account) throws(KeychainError) { - var query: [CFString: Any] = [ - kSecClass: kSecClassGenericPassword, - kSecAttrAccount: account.identifier, - kSecAttrSynchronizable: account.synchronizable, - kSecUseDataProtectionKeychain: true - ] - - query[kSecAttrService] = service?.identifier - query[kSecAttrAccessGroup] = service?.accessGroup - query[kSecUseAuthenticationContext] = context - - switch SecItemDelete(query as CFDictionary) { - case errSecSuccess, errSecItemNotFound: - return - case errSecAuthFailed, errSecInteractionNotAllowed, errSecUserCanceled: - throw .authenticationFailed - case let status: - throw .osStatus(status) - } - } - - /// Checks whether an item exists for the given account. - /// - /// - Parameter account: The account descriptor identifying the stored item. - /// - Returns: `true` if the item exists; otherwise, `false`. - /// - Throws: ``KeychainError/osStatus(_:)`` for unexpected system errors. - public func exists(by account: Account) throws(KeychainError) -> Bool { - var query: [CFString: Any] = [ - kSecClass: kSecClassGenericPassword, - kSecAttrAccount: account.identifier, - kSecAttrSynchronizable: account.synchronizable, - kSecUseDataProtectionKeychain: true, - kSecMatchLimit: kSecMatchLimitOne, - kSecReturnData: false - ] - - let context = LAContext() - context.interactionNotAllowed = true - - query[kSecAttrService] = service?.identifier - query[kSecAttrAccessGroup] = service?.accessGroup - query[kSecUseAuthenticationContext] = context - - switch SecItemCopyMatching(query as CFDictionary, nil) { - case errSecSuccess, errSecAuthFailed, errSecInteractionNotAllowed: - return true - case errSecItemNotFound: - return false - case let status: - throw .osStatus(status) - } - } -} diff --git a/Sources/KeychainKit/Enums/KeychainError.swift b/Sources/KeychainKit/Enums/KeychainError.swift deleted file mode 100644 index aa48d18..0000000 --- a/Sources/KeychainKit/Enums/KeychainError.swift +++ /dev/null @@ -1,44 +0,0 @@ -import Foundation - -/// An error that represents a keychain operation failure. -/// -/// Each case corresponds to a specific system or data error encountered while performing keychain -/// operations. -public enum KeychainError: Error, Equatable { - /// Authentication was required but failed or was canceled. - case authenticationFailed - - /// An item with the same key already exists in the keychain. - case duplicateItem - - /// The stored or retrieved data has an invalid format. - case invalidData - - /// An unexpected system status code was returned. - /// - /// - Parameter status: The underlying `OSStatus` value. - case osStatus(OSStatus) - - /// A lower-level error occurred during encoding, decoding, or other processing. - /// - /// - Parameter error: The underlying Foundation error, if available. - case underlying(NSError?) - - /// A localized, human-readable description of the error. - public var localizedDescription: String { - switch self { - case .authenticationFailed: - return .Error.authenticationFailed - case .duplicateItem: - return .Error.duplicateItem - case .invalidData: - return .Error.invalidData - case .osStatus(let status): - let message = SecCopyErrorMessageString(status, nil) - return .Error.osStatus(message as? String ?? "") - case .underlying(let error): - let message = error?.localizedDescription - return .Error.underlying(message ?? "") - } - } -} diff --git a/Sources/KeychainKit/Extensions/String+Error.swift b/Sources/KeychainKit/Extensions/String+Error.swift deleted file mode 100644 index 687d7db..0000000 --- a/Sources/KeychainKit/Extensions/String+Error.swift +++ /dev/null @@ -1,15 +0,0 @@ -import Foundation -import Localizable - -extension String { - @Localizable(bundle: .module) - enum Error { - private enum Strings { - case authenticationFailed - case duplicateItem - case invalidData - case osStatus(String) - case underlying(String) - } - } -} diff --git a/Sources/KeychainKit/Keychain.swift b/Sources/KeychainKit/Keychain.swift new file mode 100644 index 0000000..5df6fea --- /dev/null +++ b/Sources/KeychainKit/Keychain.swift @@ -0,0 +1,117 @@ +// +// 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 deleted file mode 100644 index 67b1da1..0000000 --- a/Sources/KeychainKit/Protocols/KeychainAccountProtocol.swift +++ /dev/null @@ -1,53 +0,0 @@ -import Foundation - -/// A type that describes a keychain account configuration for secure item storage and access. -/// -/// Conforming types define metadata that determines how the keychain protects, authenticates, and -/// optionally synchronizes specific items. -/// -/// ## Topics -/// -/// ### Properties -/// -/// - ``identifier`` -/// - ``protection`` -/// - ``accessFlags`` -/// - ``synchronizable`` -public protocol KeychainAccountProtocol: Sendable { - /// A unique string that identifies the keychain account. - var identifier: String { get } - - /// The keychain data protection level assigned to the account. - /// - /// Defaults to `kSecAttrAccessibleAfterFirstUnlock`. You can override this to use another - /// accessibility option, such as `kSecAttrAccessibleWhenUnlocked` or - /// `kSecAttrAccessibleWhenPasscodeSetThisDeviceOnly`. - var protection: CFString { get } - - /// The access control flags defining additional authentication requirements. - /// - /// Defaults to an empty set (`[]`). Override this to enforce constraints like `.userPresence`, - /// `.biometryAny`, or `.devicePasscode`. - var accessFlags: SecAccessControlCreateFlags { get } - - /// Indicates whether the item is synchronized through iCloud Keychain. - /// - /// Defaults to `false`. Set this to `true` if the item should be available across all devices - /// associated with the same iCloud account. - var synchronizable: Bool { get } -} - -public extension KeychainAccountProtocol { - var protection: CFString { kSecAttrAccessibleAfterFirstUnlock } - - var accessFlags: SecAccessControlCreateFlags { [] } - - var synchronizable: Bool { false } -} - -public extension KeychainAccountProtocol where Self: RawRepresentable, Self.RawValue == String { - /// A unique string that identifies the keychain account. - /// - /// Derived from the instance’s raw string value. - var identifier: String { rawValue } -} diff --git a/Sources/KeychainKit/Protocols/KeychainServiceProtocol.swift b/Sources/KeychainKit/Protocols/KeychainServiceProtocol.swift deleted file mode 100644 index ab1af9a..0000000 --- a/Sources/KeychainKit/Protocols/KeychainServiceProtocol.swift +++ /dev/null @@ -1,33 +0,0 @@ -import Foundation - -/// A type that describes a keychain service used to group and identify stored items. -/// -/// Conforming types define a unique service identifier and may optionally specify an access group -/// for sharing keychain data between multiple apps or extensions. -/// -/// ## Topics -/// -/// ### Properties -/// -/// - ``identifier`` -/// - ``accessGroup`` -public protocol KeychainServiceProtocol: Sendable { - /// A unique string that identifies the keychain service. - var identifier: String { get } - - /// An optional keychain access group identifier that enables shared access between apps. - /// - /// Defaults to `nil`, meaning no access group is specified. - var accessGroup: String? { get } -} - -public extension KeychainServiceProtocol { - var accessGroup: String? { nil } -} - -public extension KeychainServiceProtocol where Self: RawRepresentable, Self.RawValue == String { - /// A unique string that identifies the keychain service. - /// - /// Derived from the instance’s raw string value. - var identifier: String { rawValue } -} diff --git a/Sources/KeychainKit/Protocols/KeychainStorageProtocol.swift b/Sources/KeychainKit/Protocols/KeychainStorageProtocol.swift deleted file mode 100644 index bf22753..0000000 --- a/Sources/KeychainKit/Protocols/KeychainStorageProtocol.swift +++ /dev/null @@ -1,185 +0,0 @@ -import Foundation - -/// A type that provides access to data stored in the keychain. -/// -/// Conforming types define how items are encoded, saved, and accessed securely, using account and -/// service descriptors to identify individual entries. -/// -/// ## Topics -/// -/// ### Associated Types -/// -/// - ``Account`` -/// - ``Service`` -/// -/// ### Instance Properties -/// -/// - ``service`` -/// -/// ### Retrieving Items -/// -/// - ``get(by:)->Data?`` -/// - ``get(by:)->String?`` -/// - ``get(by:)->UUID?`` -/// - ``get(by:decoder:)`` -/// -/// ### Inserting Items -/// -/// - ``insert(_:by:)-(Data,_)`` -/// - ``insert(_:by:)-(String,_)`` -/// - ``insert(_:by:)-(UUID,_)`` -/// - ``insert(_:by:encoder:)`` -/// -/// ### Deleting Items -/// -/// - ``delete(by:)`` -/// -/// ### Checking Existence -/// -/// - ``exists(by:)`` -public protocol KeychainStorageProtocol: Sendable { - // MARK: - Types - - /// A type that describes a keychain account used to identify stored items. - associatedtype Account: KeychainAccountProtocol - - /// A type that describes a keychain service used to group stored items. - associatedtype Service: KeychainServiceProtocol - - // MARK: - Properties - - /// The keychain service associated with this storage instance. - var service: Service? { get } - - // MARK: - Methods - - /// Retrieves raw data for the given account. - /// - /// - Parameter account: The account descriptor identifying the stored item. - /// - Returns: The stored data, or `nil` if no item exists. - /// - Throws: ``KeychainError`` if the operation fails. - func get(by account: Account) throws(KeychainError) -> Data? - - /// Inserts raw data for the given account. - /// - /// - Parameters: - /// - value: The data to store. - /// - account: The account descriptor identifying the target item. - /// - Throws: ``KeychainError`` if the operation fails. - func insert(_ value: Data, by account: Account) throws(KeychainError) - - /// Deletes the item for the given account. - /// - /// - Parameter account: The account descriptor identifying the item to remove. - /// - Throws: ``KeychainError`` if the operation fails. - func delete(by account: Account) throws(KeychainError) - - /// Checks whether an item exists for the given account. - /// - /// - Parameter account: The account descriptor identifying the stored item. - /// - Returns: `true` if the item exists; otherwise, `false`. - /// - Throws: ``KeychainError`` if the check fails. - func exists(by account: Account) throws(KeychainError) -> Bool -} - -// MARK: - Get Extension - -public extension KeychainStorageProtocol { - /// Retrieves a UTF-8 string for the given account. - /// - /// - Parameter account: The account descriptor identifying the stored item. - /// - Returns: The decoded string, or `nil` if no item exists. - /// - Throws: ``KeychainError`` if retrieval fails. - /// - Throws: ``KeychainError/invalidData`` if the stored data cannot be decoded as UTF-8. - func get(by account: Account) throws(KeychainError) -> String? { - guard let data = try get(by: account) else { return nil } - guard let string = String(data: data, encoding: .utf8) else { - throw .invalidData - } - return string - } - - /// Retrieves a UUID for the given account. - /// - /// - Parameter account: The account descriptor identifying the stored item. - /// - Returns: The decoded UUID, or `nil` if no item exists. - /// - Throws: ``KeychainError`` if retrieval fails. - /// - Throws: ``KeychainError/invalidData`` if the stored value is not a valid UUID string. - func get(by account: Account) throws(KeychainError) -> UUID? { - guard let string: String = try get(by: account) else { return nil } - guard let uuid = UUID(uuidString: string) else { - throw .invalidData - } - return uuid - } - - /// Retrieves and decodes a `Decodable` value for the given account. - /// - /// - Parameters: - /// - account: The account descriptor identifying the stored item. - /// - decoder: The JSON decoder used to decode the stored data. - /// - Returns: The decoded value, or `nil` if no item exists. - /// - Throws: ``KeychainError`` if retrieval fails. - /// - Throws: ``KeychainError/underlying(_:)`` if JSON decoding fails. - func get( - by account: Account, - decoder: JSONDecoder = .init() - ) throws(KeychainError) -> T? { - guard let data = try get(by: account) else { return nil } - do { - return try decoder.decode(T.self, from: data) - } catch { - throw .underlying(error as NSError) - } - } -} - -// MARK: - Set Extension - -public extension KeychainStorageProtocol { - /// Inserts a UTF-8 string for the given account. - /// - /// - Parameters: - /// - value: The string to store. - /// - account: The account descriptor identifying the target item. - /// - Throws: ``KeychainError`` if the operation fails. - /// - Throws: ``KeychainError/invalidData`` if the string cannot be encoded as UTF-8. - func insert(_ value: String, by account: Account) throws(KeychainError) { - guard let data = value.data(using: .utf8) else { - throw .invalidData - } - try insert(data, by: account) - } - - /// Inserts a UUID for the given account. - /// - /// - Parameters: - /// - value: The UUID to store. - /// - account: The account descriptor identifying the target item. - /// - Throws: ``KeychainError`` if the operation fails. - func insert(_ value: UUID, by account: Account) throws(KeychainError) { - try insert(value.uuidString, by: account) - } - - /// Encodes and inserts an `Encodable` value for the given account. - /// - /// - Parameters: - /// - value: The value to encode and store. - /// - account: The account descriptor identifying the target item. - /// - encoder: The JSON encoder used to encode the value. - /// - Throws: ``KeychainError`` if the operation fails. - /// - Throws: ``KeychainError/underlying(_:)`` if JSON encoding fails. - func insert( - _ value: T, - by account: Account, - encoder: JSONEncoder = .init() - ) throws(KeychainError) { - let data: Data - do { - data = try encoder.encode(value) - } catch { - throw .underlying(error as NSError) - } - try insert(data, by: account) - } -} diff --git a/Sources/KeychainKit/Resources/Localizable.xcstrings b/Sources/KeychainKit/Resources/Localizable.xcstrings deleted file mode 100644 index 44446a7..0000000 --- a/Sources/KeychainKit/Resources/Localizable.xcstrings +++ /dev/null @@ -1,56 +0,0 @@ -{ - "sourceLanguage" : "en", - "strings" : { - "Error.authenticationFailed" : { - "localizations" : { - "en" : { - "stringUnit" : { - "state" : "translated", - "value" : "Authentication failed" - } - } - } - }, - "Error.duplicateItem" : { - "localizations" : { - "en" : { - "stringUnit" : { - "state" : "translated", - "value" : "Item already exists" - } - } - } - }, - "Error.invalidData" : { - "localizations" : { - "en" : { - "stringUnit" : { - "state" : "translated", - "value" : "Stored item contains invalid or unexpected data" - } - } - } - }, - "Error.osStatus %@" : { - "localizations" : { - "en" : { - "stringUnit" : { - "state" : "translated", - "value" : "Unexpected Keychain status: %@" - } - } - } - }, - "Error.underlying %@" : { - "localizations" : { - "en" : { - "stringUnit" : { - "state" : "translated", - "value" : "Unexpected error while working with Keychain: %@" - } - } - } - } - }, - "version" : "1.0" -} \ No newline at end of file