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:

  1. Save Data To Keychain
    a. SecItemAdd
    b. Saving Keychain Data Example
  2. Update Data In Keychain
    a. SecItemUpdate
    b. Updating Keychain Data Example
  3. Read Data From Keychain
    a. SecItemCopyMatching
    b. Get Keychain Data Example
  4. Delete Data In Keychain
    a. SecItemDelete
    b. Keychain Delete Example
  5. 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.