Storing Objects in keychain

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)]
}
  1. 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.
  2. The kSecAttrAccount is unique to kSecClassGenericPassword and acts as the primary key field. To ensure that the value is unique simply use the string representation of the json data.
  3. the kSecReturnPersistentRef key is used to indicate whether or not a persistent ref needs to be created when executing the query. kCFBooleanTrue simply means true as we do want to have a persistent ref returned.
  4. 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.