😜KeyChain

As perviously explained, UserDefaults saves data into a plist file. User can get access to Library/Preferences folder of their iPhone and read or modify the UserDefaults plist data easily (eg: user can change the boolean value of “isPremiumUser” from false to true, or change the value of trails left).

Other than in-app purchase status, you shouldn’t keep user passwords or API keys in UserDefaults for the same reason. This is where KeyChain comes in to save us. For sensitive information, such as a username or an access token, I strongly recommend using the keychain.

Keychain Services is a secure storage interface for macOS and iOS best used for small pieces of private data like passwords, cookies, and authentication tokens.

From Apple documentation, the KeyChain services API helps you solve this problem by giving your app a mechanism to store small bits of user data in an encrypted database called a keychain. When you securely remember the password for them, you free the user to choose a complicated one.

Most of the Keychain services API provided by Apple are written in C language and its APIs are not as Swifty as other modern frameworks by Apple. Now, let’s get start to implement a helper class that saves, updates, reads and deletes data using the keychain service.

Let’s make the KeychainHelper a helper class as a singleton class, that manages our keychain services.

class KeychainHelper {
    static let shared = KeychainHelper()
    private init() {}
    
    // Class implementation here...
}

Saving Data To Keychain

SecItemAdd is used to save new items to Keychain. An item is uniquely identified by query, a CFDictionary that specifies the item's:

  • kSecValueData: A key that represents the data being saved to the keychain.

  • kSecClass: A key that represents the type of data being saved. Here we set its value as kSecClassGenericPassword indicating that the data we are saving is a generic password item.

  • kSecAttrService and kSecAttrAccount: These 2 keys are mandatory when kSecClass is set to kSecClassGenericPassword. The values for both of these keys will act as the primary key for the data being saved. In other words, we will use them to retrieve the saved data from the keychain later on. For example, if we are saving the Facebook access token, we can set kSecAttrService as “password-token” and kSecAttrAccount as “abc@gmail.com“.

func save(data: Data, service: String, account: String) {
    let query = [
        kSecValueData: data,
        kSecClass: kSecClassGenericPassword,
        kSecAttrService: service,
        kSecAttrAccount: account,
    ] as CFDictionary
    
    let status = SecItemAdd(query, nil)
    
    if status != errSecSuccess {
        // Print out the error
        print("Error: \(status)")
    }
}

Here in above method save(data:service:account), we first create the query, then we call the SecItemAdd(_:_:) method to save the data to the keychain. The SecItemAdd(_:_:) method will then return an OSStatus that indicates the status of the save operation. If we get the errSecSuccess status, it means that the data has been successfully saved to the keychain.

Here’s how to use the save(data:service:account) method we just created:

let passwordToken = "Password@123"
let data = Data(passwordToken.utf8)
KeychainHelper.shared.save(data: data, service: "password-token", account: "abc@gmail.com")

Updating Existing Data in Keychain

If we try to save another token using the same kSecAttrService and kSecAttrAccount value, we won’t be able to save the access token to the keychain, and will get error because keys that we use already exist in the keychain. Let’s update our previous save(data:service:account) method.

func save(data: Data, service: String, account: String) {
    // ... ...

    if status == errSecDuplicateItem {
        // Item already exist, thus update it.
        let query = [
            kSecAttrService: service,
            kSecAttrAccount: account,
            kSecClass: kSecClassGenericPassword,
        ] as CFDictionary

        let attributesToUpdate = [kSecValueData: data] as CFDictionary

        // Update existing item
        SecItemUpdate(query, attributesToUpdate)
    }
}

SecItemUpdate is used to override existing data in Keychain. Similar to the save operation, we must first create a query object that consists of kSecAttrService and kSecAttrAccount. But this time, we will have to create another dictionary that consists of kSecValueData and feed it to the SecItemUpdate(_:_:) method. SecItemUpdate expects the new value of the Keychain item to be passed inside a different argument attributesToUpdate using the same key, kSecValueData.

Reading Data from Keychain

The way to read data from the keychain is very similar to how we save data to it. We first create a query object and then we use SecItemCopyMatching method to get the data from keychain.

func read(service: String, account: String) -> Data? {
    let query = [
        kSecAttrService: service,
        kSecAttrAccount: account,
        kSecClass: kSecClassGenericPassword,
        kSecReturnData: true
    ] as CFDictionary
    
    var itemCopy: AnyObject?
    let status = SecItemCopyMatching(query, &itemCopy)
    
    if status == errSecItemNotFound {
        print("Error: ItemNotFound")
        return nil
    }
    if status != errSecSuccess {
        print("Error: UnexpectedError")
        return nil
    }
    let password = itemCopy as? Data
    return password
}

Let’s use the read(service:account) method we just created:

let data = KeychainHelper.shared.read(service: "password-token", account: "abc@gmail.com")!
let password = String(data: data, encoding: .utf8)!
print(password)

Delete Data In Keychain

Like the other Keychain methods, SecItemDelete takes in a query and returns an OSStatus. Keychain will delete permanently associate data with the items matching the query.

func delete(service: String, account: String) {
    let query = [
        kSecAttrService: service,
        kSecAttrAccount: account,
        kSecClass: kSecClassGenericPassword,
        ] as CFDictionary
    
    let status = SecItemDelete(query)
    if status == errSecSuccess {
        print("Delete Successful")
    }
}

This is all about how we can use KeyChain Services to sensitive information instead in UserDefaults. Here, we have implemented all the necessary functionalities of the keychain helper class but it only supports reading and writing items of type Data. We can extend our helper class to create a generic save method that accepts any object with data type.

Last updated

Was this helpful?