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. This post presents an example implementation using Keychain Services for a storing and getting passwords:
- Save Data To Keychain
a. SecItemAdd
b. Saving Keychain Data Example - Update Data In Keychain
a. SecItemUpdate
b. Updating Keychain Data Example - Read Data From Keychain
a. SecItemCopyMatching
b. Get Keychain Data Example - Delete Data In Keychain
a. SecItemDelete
b. Keychain Delete Example - Sync Keychain With iCloud
Throughout this post documented code samples will be provided from an example implementation using Keychain Services, the KeychainInterface
class. KeychainInterface
will handle various Keychain errors that are important to understand:
class KeychainInterface {
enum KeychainError: Error {
// Attempted read for an item that does not exist.
case itemNotFound
// Attempted save to override an existing item.
// Use update instead of save to update existing items
case duplicateItem
// A read of an item in any format other than Data
case invalidItemFormat
// Any operation result status than errSecSuccess
case unexpectedStatus(OSStatus)
}
}
Save Data To Keychain
SecItemAdd
SecItemAdd
is used to save new items to Keychain. An item is uniquely identified by query, a CFDictionary
that specifies the item's:
a. Service, kSecAttrService
, a string to identify a set of Keychain Items like “com.my-app.bundle-id”
b. Account, kSetAttrAccount
, a string to identify a Keychain Item within a specific service, like “username@email.com”
c. Class, kSecClass
, a type of secure data to store in a Keychain Item, like kSecClassGenericPassword
The second argument result
is an UnsafeMutablePointer
to any return value specified by query. Often no return data is expected and nil
can be passed for result.
func SecItemAdd(
_ attributes: CFDictionary,
_ result: UnsafeMutablePointer<CFTypeRef?>?
) -> OSStatus
Saving Keychain Data Example
static func save(password: Data, service: String, account: String) throws {
let query: [String: AnyObject] = [
// kSecAttrService, kSecAttrAccount, and kSecClass
// uniquely identify the item to save in Keychain
kSecAttrService as String: service as AnyObject,
kSecAttrAccount as String: account as AnyObject,
kSecClass as String: kSecClassGenericPassword,
// kSecValueData is the item value to save
kSecValueData as String: password as AnyObject
]
// SecItemAdd attempts to add the item identified by
// the query to keychain
let status = SecItemAdd(
query as CFDictionary,
nil
)
// errSecDuplicateItem is a special case where the
// item identified by the query already exists. Throw
// duplicateItem so the client can determine whether
// or not to handle this as an error
if status == errSecDuplicateItem {
throw KeychainError.duplicateItem
}
// Any status other than errSecSuccess indicates the
// save operation failed.
guard status == errSecSuccess else {
throw KeychainError.unexpectedStatus(status)
}
}
Update Data In Keychain
SecItemUpdate
SecItemUpdate
is used to override existing data in Keychain. Like SecItemAdd
, the Keychain item to override is identified by query
. Unlike SecItemAdd
, SecItemUpdate
expects the new value of the Keychain item to be passed inside a different argument attributesToUpdate
using the same key, kSecValueData
.
func SecItemUpdate(
_ query: CFDictionary,
_ attributesToUpdate: CFDictionary
) -> OSStatus
Updating Keychain Data Example
static func update(password: Data, service: String, account: String) throws {
let query: [String: AnyObject] = [
// kSecAttrService, kSecAttrAccount, and kSecClass
// uniquely identify the item to update in Keychain
kSecAttrService as String: service as AnyObject,
kSecAttrAccount as String: account as AnyObject,
kSecClass as String: kSecClassGenericPassword
]
// attributes is passed to SecItemUpdate with
// kSecValueData as the updated item value
let attributes: [String: AnyObject] = [
kSecValueData as String: password as AnyObject
]
// SecItemUpdate attempts to update the item identified
// by query, overriding the previous value
let status = SecItemUpdate(
query as CFDictionary,
attributes as CFDictionary
)
// errSecItemNotFound is a special status indicating the
// item to update does not exist. Throw itemNotFound so
// the client can determine whether or not to handle
// this as an error
guard status != errSecItemNotFound else {
throw KeychainError.itemNotFound
}
// Any status other than errSecSuccess indicates the
// update operation failed.
guard status == errSecSuccess else {
throw KeychainError.unexpectedStatus(status)
}
}
Read Data From Keychain
SecItemCopyMatching
Like SecItemAdd
, SecItemCopyMatching
method has an UnsafeMutablePointer
argument as well as a query
argument. Data read by SecItemCopyMatching
will be copied into the UnsafeMutablePointer
result
for access by your macOS and iOS app.
In this implementation, SecItemCopyMatching
is expected to copy the data of the stored item in Keychain. This is implemented by setting kSecReturnData
to kCFBooleanTrue
in the query.
func SecItemCopyMatching(
_ query: CFDictionary,
_ result: UnsafeMutablePointer<CFTypeRef?>?
) -> OSStatus
Get Keychain Data Example
static func readPassword(service: String, account: String) throws -> Data {
let query: [String: AnyObject] = [
// kSecAttrService, kSecAttrAccount, and kSecClass
// uniquely identify the item to read in Keychain
kSecAttrService as String: service as AnyObject,
kSecAttrAccount as String: account as AnyObject,
kSecClass as String: kSecClassGenericPassword,
// kSecMatchLimitOne indicates keychain should read
// only the most recent item matching this query
kSecMatchLimit as String: kSecMatchLimitOne,
// kSecReturnData is set to kCFBooleanTrue in order
// to retrieve the data for the item
kSecReturnData as String: kCFBooleanTrue
]
// SecItemCopyMatching will attempt to copy the item
// identified by query to the reference itemCopy
var itemCopy: AnyObject?
let status = SecItemCopyMatching(
query as CFDictionary,
&itemCopy
)
// errSecItemNotFound is a special status indicating the
// read item does not exist. Throw itemNotFound so the
// client can determine whether or not to handle
// this case
guard status != errSecItemNotFound else {
throw KeychainError.itemNotFound
}
// Any status other than errSecSuccess indicates the
// read operation failed.
guard status == errSecSuccess else {
throw KeychainError.unexpectedStatus(status)
}
// This implementation of KeychainInterface requires all
// items to be saved and read as Data. Otherwise,
// invalidItemFormat is thrown
guard let password = itemCopy as? Data else {
throw KeychainError.invalidItemFormat
}
return password
}
Delete Data In Keychain
SecItemDelete
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 SecItemDelete(
_ query: CFDictionary
) -> OSStatus
Keychain Delete Example
static func deletePassword(service: String, account: String) throws {
let query: [String: AnyObject] = [
// kSecAttrService, kSecAttrAccount, and kSecClass
// uniquely identify the item to delete in Keychain
kSecAttrService as String: service as AnyObject,
kSecAttrAccount as String: account as AnyObject,
kSecClass as String: kSecClassGenericPassword
]
// SecItemDelete attempts to perform a delete operation
// for the item identified by query. The status indicates
// if the operation succeeded or failed.
let status = SecItemDelete(query as CFDictionary)
// Any status other than errSecSuccess indicates the
// delete operation failed.
guard status == errSecSuccess else {
throw KeychainError.unexpectedStatus(status)
}
}
Sync Keychain With iCloud
It is possible to automatically sync a user’s Keychain data with that user’s iCloud, if iCloud Keychain Synchronization is enabled. To do so, set kSecAttrSynchronizable
to kCFBooleanTrue
when constructing a Keychain Services query for all Keychain operations:
query[kSecAttrSynchronizable as String] = kCFBooleanTrue
Why not use KeychainAccess, KeychainSwift, or other Cocoapods to access Keychain?
Many apps do not require complex Keychain Services behavior beyond what Apple-provided methods like SecItemAdd
, SecItemUpdate
, SecItemCopyMatching
, and SecItemDelete
. By using Apple-provided methods to work with Keychain Services and storing secure data, a developer can:
- simplify the codebase by removing third-party dependencies like KeychainAccess and KeychainSwift
- reduce the binary size of the application
- reduce risk associated with relying on third-party dependencies
Getting And Saving Secure Data From Keychain Services in Swift on iOS and macOS
That’s it! By understanding SecItemAdd
, SecItemUpdate
, SecItemCopyMatching
, and SecItemDelete
you can implement Keychain Services to save sensitive and private data in your macOS and iOS application.