Skip to content

Instantly share code, notes, and snippets.

@lahariganti
Created May 31, 2021 08:57
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save lahariganti/baf503502340f1bb3b6cf2de37b4df08 to your computer and use it in GitHub Desktop.
Save lahariganti/baf503502340f1bb3b6cf2de37b4df08 to your computer and use it in GitHub Desktop.
import Foundation
struct KeychainWrapperError: Error {
var message: String?
var type: KeychainErrorType
enum KeychainErrorType {
case badData
case servicesError
case itemNotFound
case unableToConvertToString
}
// `OSStatus`: can assume one of the values listed in Item Return Result Keys
// https://developer.apple.com/documentation/security/1542001-security_framework_result_codes
init(status: OSStatus, type: KeychainErrorType) {
self.type = type
if let errorMessage = SecCopyErrorMessageString(status, nil) {
self.message = String(errorMessage)
} else {
self.message = "Status Code: \(status)"
}
}
init(type: KeychainErrorType) {
self.type = type
}
init(message: String, type: KeychainErrorType) {
self.message = message
self.type = type
}
}
final class KeychainWrapper {
func storeTokenFor(account: String, service: String, token: String) throws {
if token.isEmpty {
try deleteTokenFor(account: account, service: service)
return
}
guard let tokenData = token.data(using: .utf8) else {
print("Error converting value to data.")
throw KeychainWrapperError(type: .badData)
}
// The query is a dictionary that maps a String to an Any object, depending on the attribute.
// This pattern is common when calling C-based APIs from Swift. For each attribute, you supply the defined global
// constant beginning with kSec. In each case, you cast the constant to a String (it’s a CFString really), and you follow it with the value for that attribute.
let query: [String: Any] = [kSecClass as String: kSecClassGenericPassword,
kSecAttrAccount as String: account,
kSecAttrService as String: service,
kSecValueData as String: tokenData]
// Asks Keychain Services to add information to the keychain
// You cast the query to the expected CFDictionary type.
// C APIs often use the return value to show the result of a function. Here the value has type OSStatus.
let status = SecItemAdd(query as CFDictionary, nil)
switch status {
case errSecSuccess:
break
case errSecDuplicateItem:
try updateTokenFor(account: account, service: service, token: token)
default:
throw KeychainWrapperError(status: status, type: .servicesError)
}
}
func getTokenFor(account: String, service: String) throws -> String {
let query: [String: Any] = [kSecClass as String: kSecClassGenericPassword,
kSecAttrAccount as String: account,
kSecAttrService as String: service,
kSecMatchLimit as String: kSecMatchLimitOne,
kSecReturnAttributes as String: true,
kSecReturnData as String: true]
var item: CFTypeRef?
// The C function will update the memory at that location with a new value
let status = SecItemCopyMatching(query as CFDictionary, &item)
guard status != errSecItemNotFound else {
throw KeychainWrapperError(type: .itemNotFound)
}
guard status == errSecSuccess else {
throw KeychainWrapperError(status: status, type: .servicesError)
}
// Cast the returned CFTypeRef to a dictionary
guard
let existingItem = item as? [String: Any],
let valueData = existingItem[kSecValueData as String] as? Data,
let value = String(data: valueData, encoding: .utf8)
else {
throw KeychainWrapperError(type: .unableToConvertToString)
}
return value
}
func updateTokenFor(account: String, service: String, token: String) throws {
guard let tokenData = token.data(using: .utf8) else {
print("Error converting value to data.")
return
}
let query: [String: Any] = [
kSecClass as String: kSecClassGenericPassword,
kSecAttrAccount as String: account,
kSecAttrService as String: service
]
let attributes: [String: Any] = [
kSecValueData as String: tokenData
]
let status = SecItemUpdate(query as CFDictionary, attributes as CFDictionary)
guard status != errSecItemNotFound else {
throw KeychainWrapperError(
message: "Matching Item Not Found",
type: .itemNotFound)
}
guard status == errSecSuccess else {
throw KeychainWrapperError(status: status, type: .servicesError)
}
}
func deleteTokenFor(account: String, service: String) throws {
let query: [String: Any] = [
kSecClass as String: kSecClassGenericPassword,
kSecAttrAccount as String: account,
kSecAttrService as String: service
]
let status = SecItemDelete(query as CFDictionary)
guard status == errSecSuccess || status == errSecItemNotFound else {
throw KeychainWrapperError(status: status, type: .servicesError)
}
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment