Skip to content

Instantly share code, notes, and snippets.

@aksswami
Created August 25, 2021 07:26
Show Gist options
  • Save aksswami/660335d509ae26bc2ab9020b314dc613 to your computer and use it in GitHub Desktop.
Save aksswami/660335d509ae26bc2ab9020b314dc613 to your computer and use it in GitHub Desktop.
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
}
}
}
@aksswami
Copy link
Author

Usage:-

let secretStore = AppCoordinator.createKeychainStore()

fileprivate static func createKeychainStore() -> KeychainStore {
    let genericSecretQueryable =
        GenericPasswordQueryable(service: "SERVICE")
    return KeychainStore(keychainQueryable: genericSecretQueryable)
}
// 1. Store secrets received from API to keychainStore
  do {
      try secretStore.setValue(apiKey, for: "password.key")
  } catch let error {
      print(error.localizedDescription)
  }

// 2. Swift Computed property, retrieve from store. 
var password: String? {
    return try? secretStore.getValue(for: "password.key")
}

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment