Keychain is normally used to simply store credentials. This snippet illustrates a workaround to store entire objects in the keychain and supply them with a persistent ref to have a direct reference to individual items.
1. Make your object Codable
Simply make sure that your object is codable such as the following example:
public struct CustomObject: Codable {
var label: String
var count: Int
}
2. Encoding and decoding
Keychain supports storing data so, to store the object it will be converted to json data. A generic function can be used to encode items:
For enconding:
func convertItemToJson<O: Encodable>(_ item: O) -> Data {
let encoder = JSONEncoder()
let jsonData = try! encoder.encode(item)
return jsonData
}
For decoding the following can be used:
func decodeFromJson(_ itemData: Data) -> CustomObject? {
let jsonDecoder = JSONDecoder()
let decodedObject = try? jsonDecoder.decode(CustomObject.self, from: itemData)
return decodedObject
}
3. CRUD
Make sure to import Security
.
Create
For adding items to the keychain, the SecItemAdd
function is used. To call it, a query needs to be supplied, which exists out of multiple key/value pairs:
internal class func addItemsQuery(for item: CustomObject) -> [String: Any] {
return [kSecClass as String: kSecClassGenericPassword,
kSecAttrAccount as String: String(data: convertItemToJson(item), encoding: .utf8)!,
kSecReturnPersistentRef as String: kCFBooleanTrue,
kSecValueData as String: convertItemToJson(item)]
}
- When adding an item to the keychain, you need to specify its class which handles as a category. You can read up about the different types of classes and their uses here.
- The
kSecAttrAccount
is unique tokSecClassGenericPassword
and acts as the primary key field. To ensure that the value is unique simply use the string representation of the json data. - the
kSecReturnPersistentRef
key is used to indicate whether or not a persistent ref needs to be created when executing the query.kCFBooleanTrue
simply meanstrue
as we do want to have a persistent ref returned. kSecValueData
will be where the object will be stored.
Finally, to execute the query:
class func addItem(_ item: CustomObject) throws {
var persistentRef: AnyObject?
let status = SecItemAdd(addItemsQuery(for: item) as CFDictionary, &persistentRef)
guard status == errSecSuccess else {
throw fatalError("stopping code execution due to errorStatus: \(status)")
}
if let referencedData = persistentRef as? Data {
print("Store your persistenceReference somewhere")
}
}
Read
To retrieve all items from the keychain (limited to your application)
internal class func getAllItemsQuery() -> [String: Any] {
return [kSecClass as String: kSecClassGenericPassword,
kSecMatchLimit as String: kSecMatchLimitAll,
kSecReturnPersistentRef as String: kCFBooleanTrue,
kSecReturnData as String: kCFBooleanTrue]
}
class func getAllItems() throws -> [CustomObject] {
var item_list: AnyObject?
let status = SecItemCopyMatching(getAllItemsQuery() as CFDictionary, &item_list)
if status == errSecItemNotFound {
return []
}
guard status == errSecSuccess , let returnedItems = item_list as? [NSDictionary] else {
throw fatalError("stopping code execution due to errorStatus: \(status)")
}
var object_list = [CustomObject]()
for item in returnedItems {
if let jsonData = item[kSecValueData] as? Data {
if let decodedObject = decodeFromJson(jsonData) {
if let persistentRef = item[kSecValuePersistentRef] as? Data {
object_list.append(decodedObject)
}
}
}
}
return object_list
}
To retrieve a specific item from the keychain with help of the persistent ref: Different params can be applied to filter the query, an overview can be found here.
internal class func getItemByRef(with ref: NSData) -> [String: Any] {
return [kSecClass as String: kSecClassGenericPassword,
kSecValuePersistentRef as String: ref,
kSecMatchLimit as String: kSecMatchLimitOne,
kSecReturnData as String: kCFBooleanTrue,
kSecReturnPersistentRef as String: kCFBooleanTrue]
}
class func findItem(by ref: NSData) -> CustomObject? {
let query = getItemByRef(with: ref) as CFDictionary
var item: CFTypeRef?
let status = SecItemCopyMatching(query, &item)
if status == errSecItemNotFound {
return nil
}
guard status == errSecSuccess, let foundItem = item as? [String: Any],
let jsonData = foundItem[kSecValueData as String] as? Data else {
throw fatalError("stopping code execution")
}
return decodeFromJson(jsonData)
}
Update
internal class func getSpecificItemQuery(for ref: NSData) -> [String: Any] {
return [kSecClass as String: kSecClassGenericPassword,
kSecValuePersistentRef as String: ref]
}
class func updateItem(_ ref: NSData, with updatedItem: CustomObject) throws {
let query = getSpecificItemQuery(for: ref) as CFDictionary
let updateRequest = [kSecValueData: convertItemToJson(updatedItem)] as CFDictionary
let status = SecItemUpdate(query, updateRequest)
guard status == errSecSuccess else {
throw fatalError("stopping code execution due to errorStatus: \(status)")
}
}
Delete
class func deleteItem(_ ref: NSData) throws {
let status = SecItemDelete(getSpecificItemQuery(for: ref) as CFDictionary)
guard status == errSecSuccess else {
throw fatalError("stopping code execution due to errorStatus: \(status)")
}
}
To delete all items from the keychain (limited to the current app) call the same function as above with the following query:
internal class func deleteAllItemsQuery() -> [String: Any] {
return [kSecClass as String: kSecClassGenericPassword]
}
An important note that items MUST be deleted from the keychain in order for them to actually be gone. ‘Simply’ deleting the app from the device will not discard the keychain items.