Created
May 31, 2021 08:57
-
-
Save lahariganti/baf503502340f1bb3b6cf2de37b4df08 to your computer and use it in GitHub Desktop.
This file contains hidden or 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
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