Keychain Services API Tutorial for Passwords in Swift
In this Keychain tutorial for Swift on iOS, you’ll learn how to interact with the C language API to securely store passwords in the iOS Keychain. By Lorenzo Boaro.
Sign up/Sign in
With a free Kodeco account you can download source code, track your progress, bookmark, personalise your learner profile and more!
Create accountAlready a member of Kodeco? Sign in
Sign up/Sign in
With a free Kodeco account you can download source code, track your progress, bookmark, personalise your learner profile and more!
Create accountAlready a member of Kodeco? Sign in
Contents
Keychain Services API Tutorial for Passwords in Swift
30 mins
Implementing Wrapper’s API
Open SecureStore.swift and add the following implementation inside setValue(_:for:)
:
// 1
guard let encodedPassword = value.data(using: .utf8) else {
throw SecureStoreError.string2DataConversionError
}
// 2
var query = secureStoreQueryable.query
query[String(kSecAttrAccount)] = userAccount
// 3
var status = SecItemCopyMatching(query as CFDictionary, nil)
switch status {
// 4
case errSecSuccess:
var attributesToUpdate: [String: Any] = [:]
attributesToUpdate[String(kSecValueData)] = encodedPassword
status = SecItemUpdate(query as CFDictionary,
attributesToUpdate as CFDictionary)
if status != errSecSuccess {
throw error(from: status)
}
// 5
case errSecItemNotFound:
query[String(kSecValueData)] = encodedPassword
status = SecItemAdd(query as CFDictionary, nil)
if status != errSecSuccess {
throw error(from: status)
}
default:
throw error(from: status)
}
This method, as the name implies, allows storing a new password for a specific account. If it cannot update or add a password, it throws a SecureStoreError.unhandledError
, which specifies a localized description for it.
Here’s what your code does:
- Check if it can encode the value to store into a
Data
type. If that’s not possible, it throws a conversion error. - Ask the
secureStoreQueryable
instance for the query to execute and append the account you’re looking for. - Return the keychain item that matches the query.
- If the query succeeds, it means a password for that account already exists. In this case, you replace the existing password’s value using
SecItemUpdate(_:_:)
. - If it cannot find an item, the password for that account does not exist yet. You add the item by invoking
SecItemAdd(_:_:)
.
The Keychain Services API uses Core Foundation types. To make the compiler happy, you must convert from Core Foundation types to Swift types and vice versa.
In the first case, since each key’s attribute is of type CFString
, its usage as a key in a query dictionary requires a cast to String
. However, the conversion from [String: Any]
to CFDictionary
enables you to invoke the C functions.
Now it’s time to retrieve your password. Scroll below the method you’ve just implemented and replace the implementation of getValue(for:)
with the following:
// 1
var query = secureStoreQueryable.query
query[String(kSecMatchLimit)] = kSecMatchLimitOne
query[String(kSecReturnAttributes)] = kCFBooleanTrue
query[String(kSecReturnData)] = kCFBooleanTrue
query[String(kSecAttrAccount)] = userAccount
// 2
var queryResult: AnyObject?
let status = withUnsafeMutablePointer(to: &queryResult) {
SecItemCopyMatching(query as CFDictionary, $0)
}
switch status {
// 3
case errSecSuccess:
guard
let queriedItem = queryResult as? [String: Any],
let passwordData = queriedItem[String(kSecValueData)] as? Data,
let password = String(data: passwordData, encoding: .utf8)
else {
throw SecureStoreError.data2StringConversionError
}
return password
// 4
case errSecItemNotFound:
return nil
default:
throw error(from: status)
}
Given a specific account, this method retrieves the password associated with it. Again, if something goes wrong with the request, the code throws a SecureStoreError.unhandledError
.
Here’s what’s happening with the code you’ve just added:
- Ask
secureStoreQueryable
for the query to execute. Besides adding the account you’re interested in, this enriches the query with other attributes and their related values. In particular, you’re asking it to return a single result, to return all the attributes associated with that specific item and to give you back the unencrypted data as a result. - Use
SecItemCopyMatching(_:_:)
to perform the search. On completion,queryResult
will contain a reference to the found item, if available.withUnsafeMutablePointer(to:_:)
gives you access to anUnsafeMutablePointer
that you can use and modify inside the closure to store the result. - If the query succeeds, it means that it found an item. Since the result is represented by a dictionary that contains all the attributes you’ve asked for, you need to extract the data first and then decode it into a
Data
type. - If an item is not found, return a
nil
value.
Adding or retrieving passwords for an account is not enough. You need to integrate a way to remove passwords as well.
Find removeValue(for:)
and add this implementation:
var query = secureStoreQueryable.query
query[String(kSecAttrAccount)] = userAccount
let status = SecItemDelete(query as CFDictionary)
guard status == errSecSuccess || status == errSecItemNotFound else {
throw error(from: status)
}
To remove a password, you perform SecItemDelete(_:)
specifying the account you’re looking for. If you successfully deleted the password or if no item was found, your job is done and you bail out. Otherwise, you throw an unhandled error in order to let the user know something went wrong.
But what if you want to remove all the passwords associated with a specific service? Your next step is to implement the final code for achieving this.
Find removeAllValues()
and add the following code within its brackets:
let query = secureStoreQueryable.query
let status = SecItemDelete(query as CFDictionary)
guard status == errSecSuccess || status == errSecItemNotFound else {
throw error(from: status)
}
As you’ll notice, this method is similar to the previous one except for the query passed to the SecItemDelete(_:)
function. In this case, you remove passwords independently from the user account.
Finally, build the framework to verify everything compiles correctly.
Connecting the Dots
All the work you’ve done so far enriches your wrapper with add, update, delete and retrieve capabilities. As is, you must create the wrapper with an instance of some type that conforms to SecureStoreQueryable
.
Since your very first goal was to deal both with generic and internet passwords, your next step is to create two different configurations that a consumer can create and inject into your wrapper.
First, examine how to compose a query for generic passwords.
Open SecureStoreQueryable.swift and add the following code below the SecureStoreQueryable
definition:
public struct GenericPasswordQueryable {
let service: String
let accessGroup: String?
init(service: String, accessGroup: String? = nil) {
self.service = service
self.accessGroup = accessGroup
}
}
GenericPasswordQueryable
is a simple struct
that accepts a service and an access group as String
parameters.
Next, add the following extension below the GenericPasswordQueryable
definition:
extension GenericPasswordQueryable: SecureStoreQueryable {
public var query: [String: Any] {
var query: [String: Any] = [:]
query[String(kSecClass)] = kSecClassGenericPassword
query[String(kSecAttrService)] = service
// Access group if target environment is not simulator
#if !targetEnvironment(simulator)
if let accessGroup = accessGroup {
query[String(kSecAttrAccessGroup)] = accessGroup
}
#endif
return query
}
}
To conform to SecureStoreQueryable
protocol, you must implement query
as a property. The query represents the way your wrapper is able to perform the chosen functionality.
The composed query has specific keys and values:
- The item class, represented by the key
kSecClass
, has the valuekSecClassGenericPassword
since you’re dealing with generic passwords. This is how keychain infers that the data is secret and requires encryption. -
kSecAttrService
is set to theservice
parameter value that is injected with a new instance ofGenericPasswordQueryable
. - Finally, if your code is not running on a simulator, you also set
kSecAttrAccessGroup
key to the providedaccessGroup
value. This lets you share items among different apps with the same access group.
Next, build the framework to ensure that everything works correctly.
kSecClassGenericPassword
, the primary key is the combination of kSecAttrAccount
and kSecAttrService
. In other words, the tuple allows you to uniquely identify a generic password in the Keychain.Your shiny new wrapper is not complete yet! The next step is to integrate the functionalities allowing consumers to interact with internet passwords as well.
Scroll to the end of SecureStoreQueryable.swift and add the following:
public struct InternetPasswordQueryable {
let server: String
let port: Int
let path: String
let securityDomain: String
let internetProtocol: InternetProtocol
let internetAuthenticationType: InternetAuthenticationType
}
InternetPasswordQueryable
is a struct
that helps you manipulate Internet Passwords within your applications Keychain.
Before conforming to SecureStoreQueryable
, take a moment to understand how your API will work in this case.
If users want to deal with internet passwords, they create a new instance of InternetPasswordQueryable
where internetProtocol
and internetAuthenticationType
properties are bound to specific domains.
Next, add the following to below your InternetPasswordQueryable
implementation:
extension InternetPasswordQueryable: SecureStoreQueryable {
public var query: [String: Any] {
var query: [String: Any] = [:]
query[String(kSecClass)] = kSecClassInternetPassword
query[String(kSecAttrPort)] = port
query[String(kSecAttrServer)] = server
query[String(kSecAttrSecurityDomain)] = securityDomain
query[String(kSecAttrPath)] = path
query[String(kSecAttrProtocol)] = internetProtocol.rawValue
query[String(kSecAttrAuthenticationType)] = internetAuthenticationType.rawValue
return query
}
}
As seen in the generic passwords case, the query has specific keys and values:
- The item class, represented by the key
kSecClass
, has the valuekSecClassInternetPassword
, since you’re now interacting with internet passwords. -
kSecAttrPort
is set to theport
parameter. -
kSecAttrServer
is set to theserver
parameter. -
kSecAttrSecurityDomain
is set to thesecurityDomain
parameter. -
kSecAttrPath
is set to thepath
parameter. -
kSecAttrProtocol
is bound to therawValue
of theinternetProtocol
parameter. - Finally,
kSecAttrAuthenticationType
is bound to therawValue
of theinternetAuthenticationType
parameter.
Again, build to see if Xcode compiles correctly.
kSecClassInternetPassword
, the primary key is the combination of kSecAttrAccount
, kSecAttrSecurityDomain
, kSecAttrServer
, kSecAttrProtocol
, kSecAttrAuthenticationType
, kSecAttrPort
and kSecAttrPath
. In other words, those values allow you to uniquely identify an internet password in the Keychain.Now it’s time to see the result of all your hard work. But wait! Since you’re not creating an app that runs on a simulator, how are you going to verify it?
Here’s where unit tests come to the rescue.