diff --git a/.gitignore b/.gitignore index 67356a1..330d167 100644 --- a/.gitignore +++ b/.gitignore @@ -1,10 +1,18 @@ -## General -.DS_Store -.swiftpm -.build/ +# Xcode +# +# gitignore contributors: remember to update Global/Xcode.gitignore, Objective-C.gitignore & Swift.gitignore -## Various settings -Package.resolved +## 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 *.pbxuser !default.pbxuser *.mode1v3 @@ -13,5 +21,70 @@ Package.resolved !default.mode2v3 *.perspectivev3 !default.perspectivev3 -xcuserdata/ -*.xcuserdatad/ + +## 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/ 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/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 deleted file mode 100644 index 273919b..0000000 --- a/Package.swift +++ /dev/null @@ -1,28 +0,0 @@ -// 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", - defaultLocalization: "en", - platforms: [.macOS(.v12), .iOS(.v15)], - products: [ - .library(name: "KeychainKit", targets: ["KeychainKit"]) - ], - dependencies: [ - .package(url: "https://github.com/angd-dev/localizable.git", from: "1.0.0"), - .package(url: "https://github.com/apple/swift-docc-plugin", from: "1.0.0") - ], - targets: [ - .target( - name: "KeychainKit", - dependencies: [ - .product(name: "Localizable", package: "localizable") - ], - resources: [ - .process("Resources/Localizable.xcstrings") - ] - ) - ] -) diff --git a/README.md b/README.md index 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/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 diff --git a/keychain-kit.xcodeproj/project.pbxproj b/keychain-kit.xcodeproj/project.pbxproj new file mode 100644 index 0000000..0705aa5 --- /dev/null +++ b/keychain-kit.xcodeproj/project.pbxproj @@ -0,0 +1,362 @@ +// !$*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.0; + 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.0; + 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.xcodeproj/project.xcworkspace/contents.xcworkspacedata b/keychain-kit.xcodeproj/project.xcworkspace/contents.xcworkspacedata new file mode 100644 index 0000000..2cbedfd --- /dev/null +++ b/keychain-kit.xcodeproj/project.xcworkspace/contents.xcworkspacedata @@ -0,0 +1,7 @@ + + + + + diff --git a/keychain-kit.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist b/keychain-kit.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist new file mode 100644 index 0000000..18d9810 --- /dev/null +++ b/keychain-kit.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist @@ -0,0 +1,8 @@ + + + + + IDEDidComputeMac32BitWarning + + + diff --git a/keychain-kit/Sources/Keychain.swift b/keychain-kit/Sources/Keychain.swift new file mode 100644 index 0000000..da0291f --- /dev/null +++ b/keychain-kit/Sources/Keychain.swift @@ -0,0 +1,102 @@ +// +// Keychain.swift +// keychain-kit +// +// Created by Aleksey Zgurskiy on 02.02.2020. +// Copyright © 2020 mr.noone. All rights reserved. +// + +import Foundation + +public struct Keychain { + 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, + 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/keychain-kit/Support Files/Config.xcconfig b/keychain-kit/Support Files/Config.xcconfig new file mode 100644 index 0000000..ae8eefd --- /dev/null +++ b/keychain-kit/Support Files/Config.xcconfig @@ -0,0 +1,13 @@ +// +// 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 new file mode 100644 index 0000000..a30dcfa --- /dev/null +++ b/keychain-kit/Support Files/Debug.xcconfig @@ -0,0 +1,12 @@ +// +// 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 new file mode 100644 index 0000000..c0701c6 --- /dev/null +++ b/keychain-kit/Support Files/Info.plist @@ -0,0 +1,22 @@ + + + + + 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 new file mode 100644 index 0000000..f8da04d --- /dev/null +++ b/keychain-kit/Support Files/Release.xcconfig @@ -0,0 +1,12 @@ +// +// 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 new file mode 100644 index 0000000..bae651a --- /dev/null +++ b/keychain-kit/Support Files/keychain_kit.h @@ -0,0 +1,19 @@ +// +// 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 + +