This article is an entirely rewritten version of an old tutorial I wrote years ago titled “Using the iOS Keychain”. Originally written in Objective-C, the old version has been archived but it is accessible here.
The Keychain is the place where you would store sensitive data. As secure as iOS currently is, the keychain is the right place to store passwords, authentication tokens, and other sensitive data. You should not store this kind of data in UserDefaults
, even if iOS has made it harder to access that data for normal users in the latest versions.
In this article we will explore how to use the iOS keychain (which is also applicable to iPadOS, watchOS, and even tvOS) using Swift. The APIs are similar to the ones used in macOS, but the way both systems work with their keychain is different enough to consider them separate. The keychain APIs are very old and as such we will be written some “ugly” Swift code to get everything to work, although these days it’s much easier to do the bridging to Core Foundation and back. With that said, using the keychain isn’t too hard, and you should use it if you find yourself needing to store sensitive data.
Basic Keychain Concepts
Before we write some code, we need to get some terminology down. Keep these concepts in mind as you read through this article:
- Keychain: The keychain is a secure and encrypted storage place for sensitive data. You can think of it is a database of sensitive information.
- Keychain Item: This is a registry in the keychain.
- Item Class: You can think of a class as a template of information you want to store. The keychain offers classes for different common credentials, such as username/password pairs, a certificate, a generic password, and more.
Please note that the keychain is tied to the developer provisioning profile used to sign the app and its bundle ID. If either of these change, the data becomes inaccessible.
Common Keychain Operations
With that basic terminology out of the way, we can start doing basic operations: Adding new items to the keychain, searching for specific items, updating items, and deleting items.
First things first, the keychain services are part of the Security
framework, so don’t forget to add that import:
# import Security
Adding Items to the Keychain
Adding Items
To add items to the keychain, you use the SecItemAdd
function. Again, these APIs are very old, so you may be surprised by what they take as arguments and what they return. The first parameter is a dictionary known as a query. The query specifies the data the keychain item will hold along with parameters we can use to find it - more on that later. Being an old (and low level) API, this argument is actually a CFDictionary
.
At the very least, the keychain item should have:
- The item class, specified with the
kSecClass
key. - The actual data you want to hold, whether it is a plain password, or anything else. This should be stored as
Data
(or if you prefer,CFData
). The key iskSecValueData
.
You can specify optional attributes and optional return types as well. We will see these in a bit.
The second parameter to this function is a UnsafeMutablePointer<CFTypeRef>?
which may contain the data created. This is an unsafe generic pointer. This is low level stuff, but I promise it will make sense later.
This parameter makes more sense when are retrieving items from the keychain, but if you wanted to return the item you just created to check its data or do anything else with it, you can use this parameter to return it after creating it.
The function itself returns an OSStatus
. OSStatus
tells us the status of the operation we just performed, and you will see it in all the keychain APIs. If it returns 0
(or errSecSuccess
), the operation finished successfully and our operation finished without an issue. In the case of SecItemAdd
, that will mean our new item was added to the keychain.
To create a keychain item, this is the minimum code you would need:
let keychainItemQuery = [
kSecValueData: "Pullip2020".data(using: .utf8)!,
kSecClass: kSecClassGenericPassword
] as CFDictionary
let status = SecItemAdd(keychainItemQuery, nil)
print("Operation finished with status: \(status)")
This will work fine, and it will print 0
for the status which is what we would expect (Yes, OSStatus
is not bridged to Swift yet, so you won’t get the actual enum case name errSecSuccess
). The problem with this code is that searching for this item later will be a bit harder. We will explore why when we explore how to retrieve items, but for now, try to add more context to your new items. We can do this by adding optional attributes. All the attributes we can use are also dictionary keys that have the kSecAttr
prefix.
Different classes have different attributes that make the keys unique. When using kSecClassInternetPassword
, we can specify a username and the domain name of the website. Note that we are passing nil
to the second argument, because we aren’t interested in returning the newly created item just yet.
let keychainItem = [
kSecValueData: "Pullip2020".data(using: .utf8)!,
kSecAttrAccount: "andyibanez",
kSecAttrServer: "pullipstyle.com",
kSecClass: kSecClassInternetPassword
] as CFDictionary
let status = SecItemAdd(keychainItem, nil)
print("Operation finished with status: \(status)")
Retrieving Newly-Added Records.
SecItemAdd
has a second argument we can use to return the newly created item if we want to use it instantly. This parameter is called result
, and you will probably specify nil
here most of the time, but it’s good to know that this function can return data after adding items, in case you ever need it.
When we create a new item, we can specify four different return types that will be filled in the second argument. Remember this parameter is of type UnsafeMutablePointer<CFTypeRef>?
, so it can result a pointer that can point to pretty much anything. This is a big mess, so we will explain the different return types before we move on. All the return types are specified with a key in the query that has the prefix kSecReturn
.
kSecReturnRef
: When this is set totrue
,result
will point to either aSecKeychainItem
,SecKey
,SecCertificate
,SecIdentity
, orCFData
, depending on thekSecClass
specified in the query. I couldn’t get this to return anything on iOS.kSecReturnPersistentRef
: When this is set totrue
,result
will containCFData
which you can use to persist on disk or pass to different processes.kSecReturnData
: When this is set totrue
, this will return the actual sensitive data stored in the keychain item. The sensitive data will vary on the item class, but if your query contains akSecValueData
key, it will return that.kSecReturnAttributes
: This will return all the attributes used to create the item in aCFDictionary
.
So… Yes, it is a mess. The API is very old, and result
can be pretty much anything. This API is not really friendly with Swift’s type safety, so we have to do a lot of casting when working with keychain services.
CFTypeRef
is bridged to AnyObject
, so you can forget about CFTypeRef
and use AnyObject
everywhere instead.
Here is an example of a query that returns the attributes, using kSecReturnAttributes
:
let keychainItem = [
kSecValueData: "Pullip2020".data(using: .utf8)!,
kSecAttrAccount: "andyibanez",
kSecAttrServer: "pullipstyle.com",
kSecClass: kSecClassInternetPassword,
kSecReturnAttributes: true
] as CFDictionary
var ref: AnyObject?
let status = SecItemAdd(keychainItem, &ref)
let result = ref as! NSDictionary
print("Operation finished with status: \(status)")
print("Returned attributes:")
result.forEach { key, value in
print("\(key): \(value)")
}
This will print:
Operation finished with status: 0
Returned attributes:
acct: andyibanez
atyp:
sha1: {length = 20, bytes = 0x589a101265fbb5cd7b596657d2109c13450533a1}
path:
sdmn:
pdmn: ak
mdat: 2020-05-24 19:33:51 +0000
sync: 0
cdat: 2020-05-24 19:33:51 +0000
ptcl: 0
srvr: pullipstyle.com
agrp: 7X7FABXK4C.com.andyibanez.keychain
port: 0
You will never use these keys directly. Always use the proper kSecAttr
key to get your data:
print("Website: \(result[kSecAttrServer] ?? "")") // Website: pullipstyle.com
You may have noticed the actual password is missing. To get the password, we need to specify the kSecReturnData
key instead. kSecReturnData
returns a CFData
, and kSecReturnAttributes
returns a dictionary, so they are incompatible types to begin with.
To retrieve the password itself:
let keychainItem = [
kSecValueData: "Pullip2020".data(using: .utf8)!,
kSecAttrAccount: "andyibanez",
kSecAttrServer: "pullipstyle.com",
kSecClass: kSecClassInternetPassword,
kSecReturnData: true
] as CFDictionary
var ref: AnyObject?
let status = SecItemAdd(keychainItem, &ref)
let result = ref as! Data
print("Operation finished with status: \(status)")
let password = String(data: result, encoding: .utf8)!
print("Password: \(password)")
Password: Pullip2020
… But, you can actually return both at the same time! Even though using kSecReturnData
and kSecReturnAttributes
return different types, if you specify both keys at the same time and set them to true, SecItemAdd
will return a CFDictionary
that contains the attributes and the password. So, if you are aiming for consistency, and you always want result
to have the same data type, if you specify more than one return key, you will always get a dictionary back.
let keychainItem = [
kSecValueData: "Pullip2020".data(using: .utf8)!,
kSecAttrAccount: "andyibanez",
kSecAttrServer: "pullipstyle.com",
kSecClass: kSecClassInternetPassword,
kSecReturnData: true,
kSecReturnAttributes: true
] as CFDictionary
var ref: AnyObject?
let status = SecItemAdd(keychainItem, &ref)
let result = ref as! NSDictionary
print("Operation finished with status: \(status)")
print("Username: \(result[kSecAttrAccount] ?? "")")
let passwordData = result[kSecValueData] as! Data
let passwordString = String(data: passwordData, encoding: .utf8)
print("Password: \(passwordString ?? "")")
Operation finished with status: 0
Username: andyibanez
Password: Pullip2020
Retrieving Items from the Keychain
In this section we will explore how to write queries that retrieve data from the keychain. If you want to work along, I have written this small piece of code that will populate the keychain with different but similar entries:
let usernames = ["andyibanez", "alice", "eileen", "blackberry"]
usernames.forEach { username in
let keychainItem = [
kSecValueData: "\(username)-Pullip2020".data(using: .utf8)!,
kSecAttrAccount: username,
kSecAttrServer: "pullipstyle.com",
kSecClass: kSecClassInternetPassword,
kSecReturnData: true,
kSecReturnAttributes: true
] as CFDictionary
var ref: AnyObject?
let status = SecItemAdd(keychainItem, &ref)
let result = ref as! NSDictionary
print("Operation finished with status: \(status)")
print("Username: \(result[kSecAttrAccount] ?? "")")
let passwordData = result[kSecValueData] as! Data
let passwordString = String(data: passwordData, encoding: .utf8)
print("Password: \(passwordString ?? "")")
}
To query the keychain and retrieve the items, we use the SecItemCopyMatching
function. The two parameters it takes are actually the same ones as SecItemAdd
. The way these two functions is the same: You specify a query, and get something in return, including nil. it returns a OSStatus
like SecItemAdd
.
While SecItemCopyMatching
has an optional second parameter, you probably want to specify it most of the time, otherwise you will never get any data back when working with it.
You write your query the same way you did when creating items, but there’s a few things we need to keep in mind. First, the queries can be as specific or as generic as you want. Second, you can force the keychain to return one or more items at once.
Once again this API is messy, because the result
can once again return more than one type. The conditions are the same as when returning data from SecItemAdd
, but there is one more type SecItemCopyMatching
can return. If you specify the key kSecMatchLimit
and give it a value bigger than 1
, you will get a CFArray
of CFDictionary
.
Let’s see this in action. You know you have many items that have pullipstyle.com
as their kSecAttrServer
. Despite that, the following query will only return one:
let query = [
kSecClass: kSecClassInternetPassword,
kSecAttrServer: "pullipstyle.com",
kSecReturnAttributes: true,
kSecReturnData: true
] as CFDictionary
var result: AnyObject?
let status = SecItemCopyMatching(query, &result)
print("Operation finished with status: \(status)")
let dic = result as! NSDictionary
let username = dic[kSecAttrAccount] ?? ""
let passwordData = dic[kSecValueData] as! Data
let password = String(data: passwordData, encoding: .utf8)!
print("Username: \(username)")
print("Password: \(password)")
If the query doesn’t find any items to return, result
will be nil, so make sure you check for that.
To return more than one, we will specify kSecMatchLimit
to a high enough number to return all the entries:
let query = [
kSecClass: kSecClassInternetPassword,
kSecAttrServer: "pullipstyle.com",
kSecReturnAttributes: true,
kSecReturnData: true,
kSecMatchLimit: 5
] as CFDictionary
var result: AnyObject?
let status = SecItemCopyMatching(query, &result)
print("Operation finished with status: \(status)")
let array = result as! [NSDictionary]
array.forEach { dic in
let username = dic[kSecAttrAccount] ?? ""
let passwordData = dic[kSecValueData] as! Data
let password = String(data: passwordData, encoding: .utf8)!
print("Username: \(username)")
print("Password: \(password)")
}
Now the code has been adapted to work with an array instead of a dictionary, and we can now get all the entries that match the query.
One annoying detail of specifying kSecMatchLimit
is that when your query produces zero results, result
will again be nil
instead of an empty array, so keep that in mind.
The more attribute keys you add, the more specific your queries become.
let query = [
kSecClass: kSecClassInternetPassword,
kSecAttrServer: "pullipstyle.com",
kSecAttrAccount: "andyibanez",
kSecReturnAttributes: true,
kSecReturnData: true,
kSecMatchLimit: 5
] as CFDictionary
var result: AnyObject?
let status = SecItemCopyMatching(query, &result)
print("Operation finished with status: \(status)")
let array = result as! [NSDictionary]
array.forEach { dic in
let username = dic[kSecAttrAccount] ?? ""
let passwordData = dic[kSecValueData] as! Data
let password = String(data: passwordData, encoding: .utf8)!
print("Username: \(username)")
print("Password: \(password)")
}
The above code will match both the server (pullipstyle.com
) and the username (andyibanez
). This returns one item only in our specific case. Luckily since kSecMatchLimit
is bigger than one, we still get an array and not a dictionary.
Operation finished with status: 0
Username: andyibanez
Password: andyibanez-Pullip2020
Updating Items
To update items in the keychain, you use the SecItemUpdate
function which also returns an OSStatus
. Unlike SecItemAdd
and SecItemCopyMatching
, this function takes two CFDictionaries
as its arguments. The first one is the query which you know and love. The second one contains the attributes you want to update and their new values.
When specifying the query, you really need to be careful with how specific it is. If it isn’t specific enough, you may end up updating multiple items at once when you didn’t intend to.
let query = [
kSecClass: kSecClassInternetPassword,
kSecAttrServer: "pullipstyle.com",
] as CFDictionary
let updateFields = [
kSecValueData: "newPassword".data(using: .utf8)!
] as CFDictionary
let status = SecItemUpdate(query, updateFields)
print("Operation finished with status: \(status)")
This will successfully modify the password of all kSecClass
es whose kSecAttrServer
is pullipstyle.com
. It will update ALL the entries, because our query is too general and it matches many entries. Always try to make the query more specific to avoid these problems, especially if your keychain stores a lot of sensitive data.
In this case it would better to also specify the username (kSecAttrAccount
) of the user we want to update:
let query = [
kSecClass: kSecClassInternetPassword,
kSecAttrServer: "pullipstylew.com",
kSecAttrAccount: "andyibanez"
] as CFDictionary
let updateFields = [
kSecValueData: "newPassword".data(using: .utf8)!
] as CFDictionary
let status = SecItemUpdate(query, updateFields)
print("Operation finished with status: \(status)")
Deleting Items
To delete an item, use the SecItemDelete
function. This function only takes one parameter, the query, and returns an OSStatus
.
Just like when we are updating items, make sure your query is very specific so you only delete what you intend to delete. If you don’t you may delete extra entries accidentally.
let query = [
kSecClass: kSecClassInternetPassword,
kSecAttrServer: "pullipstyle.com",
kSecAttrAccount: "andyibanez.com"
] as CFDictionary
SecItemDelete(query)
Conclusion
Using the keychain to store sensitive data is the way to go. While it is a low-level API, the bridging to Swift has become more bearable in the last few years. You generally won’t worry too much about the bridging types, but it’s worth keeping into amount that the operations that return data back can return different data types.
We explored the basic usage of the keychain services APIs. We learend how to add, search, update, and delete entries, but the keychain can actually do quite a lot more than just that. Hopefully this article helped you get started to write more secure storage for sensitive data in your own apps.