Issue #934
There are a few keychain wrappers around but for simple needs, you can write it yourself
Here is a basic implementation. I use actor to go with async/await, and a struct KeychainError
to contain status code in case we want to deal with error cases.
accessGroup
is to define kSecAttrAccessGroup to share keychain across your apps
public actor Keychain {
public struct KeychainError: Error {
let status: OSStatus
}
let service: String
let accessGroup: String?
public init(
service: String,
accessGroup: String? = nil
) {
self.service = service
self.accessGroup = accessGroup
}
}
Since we need some common query parameters across few methods, I usually use helper method. We use kSecClassGenericPassword
class so we set key to kSecAttrAccount
func baseQuery(key: String) -> [CFString: Any] {
var query: [CFString: Any] = [:]
query[kSecClass] = kSecClassGenericPassword
query[kSecAttrService] = service
query[kSecAttrAccount] = key
if let accessGroup {
query[kSecAttrAccessGroup] = accessGroup
}
return query
}
Below is how to get and set Data to keychain
func get(key: String) throws -> Data {
var query = baseQuery(key: key)
query[kSecMatchLimit] = kSecMatchLimitOne
query[kSecReturnAttributes] = kCFBooleanTrue
query[kSecReturnData] = kCFBooleanTrue
var obj: AnyObject?
let status = SecItemCopyMatching(query as CFDictionary, &obj)
if status == errSecSuccess,
let json = obj as? [CFString: AnyObject],
let data = json[kSecValueData] as? Data {
return data
} else {
throw KeychainError(status: status)
}
}
func set(key: String, data: Data) throws {
do {
_ = try get(key: key)
try update(key: key, data: data)
} catch let error as KeychainError {
if error.status == errSecItemNotFound {
try add(key: key, data: data)
}
}
}
func delete(key: String) throws {
let query = baseQuery(key: key)
let status = SecItemDelete(query as CFDictionary)
if status != errSecSuccess {
throw KeychainError(status: status)
}
}
private func update(key: String, data: Data) throws {
let query = baseQuery(key: key)
let updates: [CFString: Any] = [
kSecValueData: data
]
let status = SecItemUpdate(query as CFDictionary, updates as CFDictionary)
if status != errSecSuccess {
throw KeychainError(status: status)
}
}
private func add(key: String, data: Data) throws {
var query = baseQuery(key: key)
query[kSecValueData] = data
let status = SecItemAdd(query as CFDictionary, nil)
if status != errSecSuccess {
throw KeychainError(status: status)
}
}
If there is no error, then OSStatus
will be errSecSuccess
which has value 0
There are some other query attributes like
- kSecAttrAccessible to set security attribute, like kSecAttrAccessibleWhenUnlocked