Created
August 25, 2021 07:26
-
-
Save aksswami/660335d509ae26bc2ab9020b314dc613 to your computer and use it in GitHub Desktop.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
mport Foundation | |
import Security | |
public struct KeychainStore { | |
let keychainQueryable: KeychainQueryable | |
public init(keychainQueryable: KeychainQueryable) { | |
self.keychainQueryable = keychainQueryable | |
} | |
/// Set value to provided key. If already exists updates the value, otherwise create new. | |
/// - Parameters: | |
/// - value: secret value which needs to be store. Example:- Password, APIKey etc. | |
/// - userAccount: UserAccount or Key to store this secret against. This should be unique under this service for later retrieval. | |
/// - Throws: KeychainError | |
public func setValue(_ value: String, for userAccount: String) throws { | |
let exist = try checkExists(for: userAccount) | |
if exist { | |
try updateValue(value, for: userAccount) | |
} else { | |
try insertValue(value, for: userAccount) | |
} | |
} | |
/// Update value for given Key/userAccount | |
/// - Parameters: | |
/// - value: Value to be updated | |
/// - userAccount: UserAccount or Key to store this secret against. | |
/// - Throws: KeychainError | |
internal func updateValue(_ value: String, for userAccount: String) throws { | |
guard let encodedPassword = value.data(using: .utf8) else { | |
throw KeychainError.dataEncodingError | |
} | |
var query = keychainQueryable.query | |
query[String(kSecAttrAccount)] = userAccount | |
var attributesToUpdate: [String: Any] = [:] | |
attributesToUpdate[String(kSecValueData)] = encodedPassword | |
let status = SecItemUpdate(query as CFDictionary, | |
attributesToUpdate as CFDictionary) | |
if status != errSecSuccess { | |
throw KeychainError.updateError(status: status, account: userAccount) | |
} | |
} | |
/// Insert new value for given Key/userAccount | |
/// - Parameters: | |
/// - value: Value to be updated | |
/// - userAccount: UserAccount or Key to store this secret against. | |
/// - Throws: KeychainError | |
internal func insertValue(_ value: String, for userAccount: String) throws { | |
guard let encodedPassword = value.data(using: .utf8) else { | |
throw KeychainError.dataEncodingError | |
} | |
var query = keychainQueryable.query | |
query[String(kSecAttrAccount)] = userAccount | |
query[String(kSecValueData)] = encodedPassword | |
let status = SecItemAdd(query as CFDictionary, nil) | |
if status != errSecSuccess { | |
throw KeychainError.createError(status: status, account: userAccount) | |
} | |
} | |
/// Check if value ke | |
/// - Parameters: | |
/// - userAccount: UserAccount or Key to store this secret against. | |
/// - Throws: KeychainError | |
/// - Returns: If exists return true, otherwise false. | |
internal func checkExists(for userAccount: String) throws -> Bool { | |
var query = keychainQueryable.query | |
query[String(kSecAttrAccount)] = userAccount | |
let status = SecItemCopyMatching(query as CFDictionary, nil) | |
switch status { | |
case errSecSuccess: | |
return true | |
case errSecItemNotFound: | |
return false | |
default: | |
throw KeychainError.unhandledError(status: status, account: userAccount) | |
} | |
} | |
/// Retrieve stored secret/value for specified userAccount/key. Throws error if not found. | |
/// - Parameter userAccount: UserAccount or Key to store this secret against. | |
/// - Throws: KeychainError | |
/// - Returns: If exists return stored secret. | |
public func getValue(for userAccount: String) throws -> String? { | |
var query = keychainQueryable.query | |
query[String(kSecMatchLimit)] = kSecMatchLimitOne | |
query[String(kSecReturnAttributes)] = kCFBooleanTrue | |
query[String(kSecReturnData)] = kCFBooleanTrue | |
query[String(kSecAttrAccount)] = userAccount | |
var queryResult: AnyObject? | |
let status = withUnsafeMutablePointer(to: &queryResult) { | |
SecItemCopyMatching(query as CFDictionary, $0) | |
} | |
switch status { | |
case errSecSuccess: | |
guard | |
let queriedItem = queryResult as? [String: Any], | |
let passwordData = queriedItem[String(kSecValueData)] as? Data, | |
let password = String(data: passwordData, encoding: .utf8) | |
else { | |
throw KeychainError.stringEncodingError | |
} | |
return password | |
case errSecItemNotFound: | |
return nil | |
default: | |
throw KeychainError.unhandledError(status: status, account: userAccount) | |
} | |
} | |
/// Remove stored secret/value for specified userAccount/key | |
/// - Parameter userAccount: UserAccount or Key to store this secret against. | |
/// - Throws: KeychainError | |
public func removeValue(for userAccount: String) throws { | |
var query = keychainQueryable.query | |
query[String(kSecAttrAccount)] = userAccount | |
let status = SecItemDelete(query as CFDictionary) | |
guard status == errSecSuccess || status == errSecItemNotFound else { | |
throw KeychainError.unhandledError(status: status, account: userAccount) | |
} | |
} | |
/// Removes all stores secret items from Keychain assiciated with given service | |
/// - Throws: KeychainError | |
public func removeAllValues() throws { | |
let query = keychainQueryable.query | |
let status = SecItemDelete(query as CFDictionary) | |
guard status == errSecSuccess || status == errSecItemNotFound else { | |
throw KeychainError.unhandledError(status: status, account: nil) | |
} | |
} | |
fileprivate static func error(from status: OSStatus) -> String { | |
if #available(iOS 11.3, *) { | |
return SecCopyErrorMessageString(status, nil) as String? ?? NSLocalizedString("Unexpected Error", comment: "") | |
} else { | |
return status.description | |
} | |
} | |
} | |
public protocol KeychainQueryable { | |
var query: [String: Any] { get } | |
} | |
public struct GenericPasswordQueryable { | |
let service: String | |
let accessGroup: String? | |
init(service: String, accessGroup: String? = nil) { | |
self.service = service | |
self.accessGroup = accessGroup | |
} | |
} | |
extension GenericPasswordQueryable: KeychainQueryable { | |
public var query: [String: Any] { | |
var query: [String: Any] = [:] | |
query[String(kSecClass)] = kSecClassGenericPassword | |
query[String(kSecAttrService)] = service | |
#if !targetEnvironment(simulator) | |
if let accessGroup = accessGroup { | |
query[String(kSecAttrAccessGroup)] = accessGroup | |
} | |
#endif | |
return query | |
} | |
} | |
/// Error enum which can be expected from KeychainStore | |
public enum KeychainError: Error { | |
case stringEncodingError | |
case dataEncodingError | |
case updateError(status: OSStatus, account: String?) | |
case createError(status: OSStatus, account: String?) | |
case unhandledError(status: OSStatus, account: String?) | |
} | |
extension KeychainError: LocalizedError { | |
public var errorDescription: String? { | |
switch self { | |
case .stringEncodingError: | |
return NSLocalizedString("Corrupted data, failed to encode given data to string.", comment: "") | |
case .dataEncodingError: | |
return NSLocalizedString("Corrupted string, failed to encode give string to data.", comment: "") | |
case .updateError(let status, let account), | |
.createError(let status, let account), | |
.unhandledError(let status, let account): | |
var message = KeychainStore.error(from: status) | |
if let key = account { | |
message = message + " for account \(key)" | |
} | |
return message | |
} | |
} | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Usage:-