😜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 askSecClassGenericPassword
indicating that the data we are saving is a generic password item.kSecAttrService
andkSecAttrAccount
: These 2 keys are mandatory whenkSecClass
is set tokSecClassGenericPassword
. 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 setkSecAttrService
as “password-token” andkSecAttrAccount
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 theSecItemAdd(_:_:)
method to save the data to the keychain. TheSecItemAdd(_:_:)
method will then return anOSStatus
that indicates the status of the save operation. If we get theerrSecSuccess
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
andkSecAttrAccount
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 previoussave(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 ofkSecAttrService
andkSecAttrAccount
. But this time, we will have to create another dictionary that consists ofkSecValueData
and feed it to theSecItemUpdate(_:_:)
method.SecItemUpdate
expects the new value of the Keychain item to be passed inside a different argumentattributesToUpdate
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?