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.

4.7 (21) · 2 Reviews

Download materials
Save for later
Share
You are currently viewing page 2 of 3 of this article. Click here to view the first page.

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:

  1. Check if it can encode the value to store into a Data type. If that’s not possible, it throws a conversion error.
  2. Ask the secureStoreQueryable instance for the query to execute and append the account you’re looking for.
  3. Return the keychain item that matches the query.
  4. 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(_:_:).
  5. 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:

  1. 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.
  2. 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 an UnsafeMutablePointer that you can use and modify inside the closure to store the result.
  3. 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.
  4. 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 value kSecClassGenericPassword since you’re dealing with generic passwords. This is how keychain infers that the data is secret and requires encryption.
  • kSecAttrService is set to the service parameter value that is injected with a new instance of GenericPasswordQueryable.
  • Finally, if your code is not running on a simulator, you also set kSecAttrAccessGroup key to the provided accessGroup value. This lets you share items among different apps with the same access group.

Next, build the framework to ensure that everything works correctly.

Note: For a keychain item of class 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 value kSecClassInternetPassword, since you’re now interacting with internet passwords.
  • kSecAttrPort is set to the port parameter.
  • kSecAttrServer is set to the server parameter.
  • kSecAttrSecurityDomain is set to the securityDomain parameter.
  • kSecAttrPath is set to the path parameter.
  • kSecAttrProtocol is bound to the rawValue of the internetProtocol parameter.
  • Finally, kSecAttrAuthenticationType is bound to the rawValue of the internetAuthenticationType parameter.

Again, build to see if Xcode compiles correctly.

Note: For a keychain item of class 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.