Apples framework for Securely storing small chunks of data on behalf of the user. The gist is you query the Keychain store with a bunch of key value pair attributes and get back a status code with your data.
Need for Security 🔐
In the world of mobile engineering, keeping sensitive data secure is a big deal. And guess what? Apple’s got this cool framework called Keychain
for iOS and macOS apps that’s been around forever, but it still does the job really well. It’s like the OG solution for safeguarding small pieces of sensitive data like passwords, auth tokens, or even credit card info.
Keychain: The Age-Old Framework ⏳
Even though the Keychain framework has been around since iOS 2.0, yea, it’s really that old lol, it’s still a solid choice for enforcing strong encryption and protection against unauthorized access to sensitive data. I think its longevity is a testament to its reliability and effectiveness. But this longevity is also what makes it a bit of a pain to work with due to its aging interface; it’s not as swifty as some of the other Apple frameworks we are used to working with.
With that said, let’s dive into how to work with it and demystify some of the challenges posed by its aging interface. So, we can use this Keychain magic! 🚀 to secure our app’s sensitive data.
Understanding Keychain’s Key Functions and Queries
To interact with the Keychain Service, we primarily need to use four functions from the Security
Framework:
SecItemAdd
SecItemUpdate
SecItemCopyMatching
andSecItemDelete
Each function plays a specific role in creating, reading, updating, and deleting keychain data items. To interface with these different methods, we need to put together a query dictionary, that will contain the different attributes about the data that we are about to store, delete or update. Usually the query dictionary let’s us look up items, specify encryption policies and return the data when appropriate. The gist is you query the keychain store and get back a status code with your data.
Let’s break down these functions and their query parameters.
SecItemAdd: Storing Data in Keychain
The purpose of the SecItemAdd
function is to add a new data item into the keychain store.
This is what we need to use whenever we want to store a new data in the keychain framework, and the main parameters it takes are
Query Parameters:
kSecClass
: The class of the item (e.g., kSecClassGenericPassword for passwords or kSecClassInternetPassword for internet passwords).kSecAttrAccount
: A way for us to uniquely identify the item in Keychain Store. Usually a unique id or user-defined account name associated with the data item. Think of it as the lookup key for the data.kSecValueData
: The sensitive data to be stored.kSecAttrAccessible
: Defines when the data item can be accessed (e.g., after the first unlock).kSecAttrAccessControl
: Defines access controls for the data item (optional).
As the saying goes, a practical example is worth a thousand explanations. So, here’s an example of our attempt to store a user password in the keychain store:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
import Foundation
import Security
class KeychainManager {
enum KeychainError: Error {
case duplicateItem
case unknown(OSStatus)
}
static func storePassword() throws {
let passwordData = "mySecretPassword".data(using: .utf8)!
let query: [String: Any] = [
kSecClass as String: kSecClassGenericPassword,
kSecAttrAccount as String: "[email protected]",
kSecValueData as String: passwordData,
kSecAttrAccessible as String: kSecAttrAccessibleWhenUnlocked
]
// adding the item to keychain here
let status = SecItemAdd(query as CFDictionary, nil)
if status == errSecDuplicateItem {
throw KeychainError.duplicateItem
} else if status != errSecSuccess {
throw KeychainError.unknown(status)
}
}
}
What’s going on
After setting up the query dictionary, we attempt to add the item to the Keychain using SecItemAdd
method. If the data item already exists keychain will return a (errSecDuplicateItem)
status, therefore we throw a KeychainError.duplicateItem
error. For any other unknown error, we throw a KeychainError.unknown
with the OSStatus
value to provide more information about the specific error.
The kSecClass
is basically how we tell the Keychain framework what kind of data we want to store, and here we are saying we want to store a generic password, and the key to specifying that is the kSecClassGenericPassword
.
The kSecAttrAccount
this is how we specify the unique identifier for the data we’re putting into the keychain vault. In our case since we are storing a password for a user account, the kSecAttrAccount
will be the user’s email address or username.
The kSecValueData
is the actual sensitive data we want to store in the keychain. And we usually store it as a Data object in Swift, as you can see by the data encoding of our passwordData
constant.
With the kSecAttrAccessible
key we define the accessibility of the keychain data item we are about to store. Telling keychain when the data can be accessed. Here we are using the kSecAttrAccessibleWhenUnlocked
option, which tells keychain the data item can be accessed only when the device is unlocked. This is the default option by the way, so we could remove this attribute if we wanted.
Here is how we would use our storePassword
method:
1
2
3
4
5
6
7
8
9
10
do {
try KeychainManager.storePassword()
print("Password stored successfully.")
} catch KeychainManager.KeychainError.duplicateItem {
print("Duplicate item error: The password already exists in Keychain.")
} catch KeychainManager.KeychainError.unknown(let status) {
print("Unknown error: \(status)")
} catch {
print("Unexpected error: \(error)")
}
SecItemUpdate: Updating a Data Item in Keychain
The purpose of the SecItemUpdate
function is to modify an existing keychain data.
This is what we need to use whenever we want to update our keychain data item, for instance replacing an expired authToken with a new one or invalid password with a new password etc. The main parameters we need to pass are
Query Parameters:
- Query parameters (e.g., account name) for finding the item to update.
- Attributes to modify, such as
kSecValueData
for updating the stored data.
Example: Updating a stored token in the Keychain
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
import Foundation
import Security
class KeychainManager {
enum KeychainError: Error {
case itemNotFound
case unknown(OSStatus)
}
static func updateAccessToken() throws {
// Prepare the new access token data to be updated in the Keychain
let newTokenData = "newAccessToken".data(using: .utf8)!
// Create a query dictionary to uniquely identify the item to update in the Keychain
let query: [String: Any] = [
kSecClass as String: kSecClassInternetPassword, // Specifies the item class as an internet password
kSecAttrAccount as String: "api.example.com", // The account name uniquely identifying the item
]
// Prepare the attributes to update the item with the new access token
let attributesToUpdate: [String: Any] = [
kSecValueData as String: newTokenData // The new access token data to be updated
]
// Attempt to update the item in the Keychain
let status = SecItemUpdate(query as CFDictionary, attributesToUpdate as CFDictionary)
// Check the result of the Keychain operation and throw appropriate errors if needed
if status == errSecItemNotFound {
throw KeychainError.itemNotFound // Throw if the item to update is not found in Keychain
} else if status != errSecSuccess {
throw KeychainError.unknown(status) // Throw for any other unknown error
}
}
}
Query explanation
-
kSecClass as String: kSecClassInternetPassword
: This key value pair in our query dict specifies that the data item being updated in the Keychain is an internet password. The internet password type is what we commonly use to store credentials related to websites or APIs. -
kSecAttrAccount as String: "api.example.com"
: This key value pair attribute represents the account name that uniquely identifies the item that we want to update in the Keychain store. In this case, it could be a URL or any other identifier specific to the data. -
kSecValueData as String: newTokenData
: This key value pair is the new access token data to be updated in the Keychain. In the code, we convert the string “newAccessToken” to data using the UTF-8 encoding and update the item with this new data.
After setting up the query dictionary, we attempt to update the item in the Keychain using SecItemUpdate
method. If the item to update is not found keychain will return a (errSecItemNotFound)
status, and just like in the earlier example we will throw a KeychainError.itemNotFound
error. For any other unknown error, we throw a KeychainError.unknown with the OSStatus value to provide more information about the specific error.
SecItemCopyMatching: Retrieving Data Item in Keychain
The purpose of the SecItemCopyMatching
function is to retrieve an existing data item from keychain.
This is what we need to use whenever we want to read data we have previously stored in our keychain store, and the main parameters it takes are:
Query Parameters:
- Query parameters for finding the item to retrieve.
- A pointer to retrieve the requested data
Example: Retrieving a credit card info from the Keychain
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
import Foundation
import Security
class KeychainManager {
enum KeychainError: Error {
case itemNotFound
case unexpectedDataFormat
case unknown(OSStatus)
}
static func retrieveCreditCardData() throws -> Data {
// Create a query dictionary to retrieve the credit card data from the Keychain
let query: [String: Any] = [
kSecClass as String: kSecClassGenericPassword, // Specifies the item class as a generic password
kSecAttrAccount as String: "chasebankcreditcard123", // The account name identifying the credit card item
kSecReturnData as String: true, // Specifies that the item data should be returned
kSecMatchLimit as String: kSecMatchLimitOne // Specifies that only one item should be returned
]
// Attempt to retrieve the credit card data from the Keychain
var item: CFTypeRef?
let status = SecItemCopyMatching(query as CFDictionary, &item)
// Check the result of the Keychain operation and throw appropriate errors if needed
if status == errSecItemNotFound {
throw KeychainError.itemNotFound // Throw if the credit card item is not found in Keychain
} else if status != errSecSuccess {
throw KeychainError.unknown(status) // Throw for any other unknown error
}
// Ensure the retrieved item data is in the expected format
guard let data = item as? Data else {
throw KeychainError.unexpectedDataFormat // Throw if the retrieved data is not in the expected format
}
return data
}
}
Query explanation
From our prior examples I think most of the query attributes are pretty self explanatory except these two:
-
kSecReturnData as String: true
: This query attribute tells keychain that the data item (credit card data) should be returned as part of the query result. -
kSecMatchLimit as String: kSecMatchLimitOne
: This query attribute help us limit the number of data items returned by the query to one. In this case, we expect only one credit card item to be retrieved.
Our retrieveCreditCardData
method in action:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
import Foundation
// Assuming the KeychainManager class is defined as above
do {
let creditCardData = try KeychainManager.retrieveCreditCardData()
// Process the retrieved credit card data
// For example, you can convert the data to a string or decode it as needed
if let creditCardInfo = String(data: creditCardData, encoding: .utf8) {
print("Credit Card Data: \(creditCardInfo)")
} else {
print("Failed to convert credit card data to a string.")
}
} catch KeychainManager.KeychainError.itemNotFound {
print("Credit card item not found in Keychain.")
} catch KeychainManager.KeychainError.unexpectedDataFormat {
print("Unexpected data format retrieved from Keychain.")
} catch KeychainManager.KeychainError.unknown(let status) {
print("Unknown error occurred: \(status)")
} catch {
print("Unexpected error: \(error)")
}
SecItemDelete: Deleting Data from Keychain
The purpose of the SecItemDelete
function is to delete an existing data from the keychain store.
This is what we need to use whenever we want to delete data we have previously stored in our keychain store, and the main parameters it takes are:
Query Parameters:
- Query parameters for finding the item to delete.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
import Foundation
import Security
class KeychainManager {
enum KeychainError: Error {
case itemNotFound
case unknown(OSStatus)
}
static func deletePassword() throws {
// Create a query dictionary to identify the password item to delete from the Keychain
let query: [String: Any] = [
kSecClass as String: kSecClassGenericPassword, // Specifies the item class as a generic password
kSecAttrAccount as String: "[email protected]", // The account name identifying the password data item
]
// Attempt to delete the password item from the Keychain
let status = SecItemDelete(query as CFDictionary)
// Check the result of the Keychain operation and throw appropriate errors if needed
if status == errSecItemNotFound {
throw KeychainError.itemNotFound // Throw if the password item is not found in Keychain
} else if status != errSecSuccess {
throw KeychainError.unknown(status) // Throw for any other unknown error
}
}
}
Using our deletePassword
method:
1
2
3
4
5
6
7
8
9
10
do {
try KeychainManager.deletePassword()
print("Password deleted successfully.")
} catch KeychainManager.KeychainError.itemNotFound {
print("Password item not found in Keychain.")
} catch KeychainManager.KeychainError.unknown(let status) {
print("Unknown error occurred: \(status)")
} catch {
print("Unexpected error: \(error)")
}
Conclusion
Despite it’s age and the challenges associated with it’s aging interface, the Keychain framework remains a powerful tool for securely storing sensitive data. Hopefully by helping you understand its core functions, you will be able to build robust keychain stores that protect your user’s sensitive data. Thanks for reading!