Home Using Apple's Keychain Services to Store Sensitive Data in Swift
Post
Cancel

Using Apple's Keychain Services to Store Sensitive Data in Swift

Apples framework for Securely storing small chunks of data on behalf of the user. The gist is you query the Keychain store with a bunch of key value pair attributes and get back a status code with your data.

Need for Security 🔐

In the world of mobile engineering, keeping sensitive data secure is a big deal. And guess what? Apple’s got this cool framework called Keychain for iOS and macOS apps that’s been around forever, but it still does the job really well. It’s like the OG solution for safeguarding small pieces of sensitive data like passwords, auth tokens, or even credit card info.

Keychain: The Age-Old Framework ⏳

Even though the Keychain framework has been around since iOS 2.0, yea, it’s really that old lol, it’s still a solid choice for enforcing strong encryption and protection against unauthorized access to sensitive data. I think its longevity is a testament to its reliability and effectiveness. But this longevity is also what makes it a bit of a pain to work with due to its aging interface; it’s not as swifty as some of the other Apple frameworks we are used to working with.

With that said, let’s dive into how to work with it and demystify some of the challenges posed by its aging interface. So, we can use this Keychain magic! 🚀 to secure our app’s sensitive data.

Understanding Keychain’s Key Functions and Queries

To interact with the Keychain Service, we primarily need to use four functions from the Security Framework:

  • SecItemAdd
  • SecItemUpdate
  • SecItemCopyMatching and
  • SecItemDelete

Each function plays a specific role in creating, reading, updating, and deleting keychain data items. To interface with these different methods, we need to put together a query dictionary, that will contain the different attributes about the data that we are about to store, delete or update. Usually the query dictionary let’s us look up items, specify encryption policies and return the data when appropriate. The gist is you query the keychain store and get back a status code with your data.

Let’s break down these functions and their query parameters.

SecItemAdd: Storing Data in Keychain

The purpose of the SecItemAdd function is to add a new data item into the keychain store.

This is what we need to use whenever we want to store a new data in the keychain framework, and the main parameters it takes are

Query Parameters:

  • kSecClass: The class of the item (e.g., kSecClassGenericPassword for passwords or kSecClassInternetPassword for internet passwords).
  • kSecAttrAccount: A way for us to uniquely identify the item in Keychain Store. Usually a unique id or user-defined account name associated with the data item. Think of it as the lookup key for the data.
  • kSecValueData: The sensitive data to be stored.
  • kSecAttrAccessible: Defines when the data item can be accessed (e.g., after the first unlock).
  • kSecAttrAccessControl: Defines access controls for the data item (optional).

As the saying goes, a practical example is worth a thousand explanations. So, here’s an example of our attempt to store a user password in the keychain store:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
import Foundation
import Security

class KeychainManager {
    
    enum KeychainError: Error {
        case duplicateItem
        case unknown(OSStatus)
    }
    
    static func storePassword() throws {
        let passwordData = "mySecretPassword".data(using: .utf8)!
        let query: [String: Any] = [
            kSecClass as String: kSecClassGenericPassword,
            kSecAttrAccount as String: "[email protected]",
            kSecValueData as String: passwordData,
            kSecAttrAccessible as String: kSecAttrAccessibleWhenUnlocked
        ]

        // adding the item to keychain here
        let status = SecItemAdd(query as CFDictionary, nil)
        
        if status == errSecDuplicateItem {
            throw KeychainError.duplicateItem
        } else if status != errSecSuccess {
            throw KeychainError.unknown(status)
        }
    }
}


What’s going on

After setting up the query dictionary, we attempt to add the item to the Keychain using SecItemAdd method. If the data item already exists keychain will return a (errSecDuplicateItem) status, therefore we throw a KeychainError.duplicateItem error. For any other unknown error, we throw a KeychainError.unknown with the OSStatus value to provide more information about the specific error.

The kSecClass is basically how we tell the Keychain framework what kind of data we want to store, and here we are saying we want to store a generic password, and the key to specifying that is the kSecClassGenericPassword.

The kSecAttrAccount this is how we specify the unique identifier for the data we’re putting into the keychain vault. In our case since we are storing a password for a user account, the kSecAttrAccount will be the user’s email address or username.

The kSecValueData is the actual sensitive data we want to store in the keychain. And we usually store it as a Data object in Swift, as you can see by the data encoding of our passwordData constant.

With the kSecAttrAccessible key we define the accessibility of the keychain data item we are about to store. Telling keychain when the data can be accessed. Here we are using the kSecAttrAccessibleWhenUnlocked option, which tells keychain the data item can be accessed only when the device is unlocked. This is the default option by the way, so we could remove this attribute if we wanted.

Here is how we would use our storePassword method:

1
2
3
4
5
6
7
8
9
10
    do {
        try KeychainManager.storePassword()
        print("Password stored successfully.")
    } catch KeychainManager.KeychainError.duplicateItem {
        print("Duplicate item error: The password already exists in Keychain.")
    } catch KeychainManager.KeychainError.unknown(let status) {
        print("Unknown error: \(status)")
    } catch {
        print("Unexpected error: \(error)")
    }

SecItemUpdate: Updating a Data Item in Keychain

The purpose of the SecItemUpdate function is to modify an existing keychain data.

This is what we need to use whenever we want to update our keychain data item, for instance replacing an expired authToken with a new one or invalid password with a new password etc. The main parameters we need to pass are

Query Parameters:

  • Query parameters (e.g., account name) for finding the item to update.
  • Attributes to modify, such as kSecValueData for updating the stored data.

Example: Updating a stored token in the Keychain

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
import Foundation
import Security

class KeychainManager {
    
    enum KeychainError: Error {
        case itemNotFound
        case unknown(OSStatus)
    }
    
    static func updateAccessToken() throws {
        // Prepare the new access token data to be updated in the Keychain
        let newTokenData = "newAccessToken".data(using: .utf8)!
        
        // Create a query dictionary to uniquely identify the item to update in the Keychain
        let query: [String: Any] = [
            kSecClass as String: kSecClassInternetPassword, // Specifies the item class as an internet password
            kSecAttrAccount as String: "api.example.com", // The account name uniquely identifying the item
        ]
        
        // Prepare the attributes to update the item with the new access token
        let attributesToUpdate: [String: Any] = [
            kSecValueData as String: newTokenData // The new access token data to be updated
        ]
        
        // Attempt to update the item in the Keychain
        let status = SecItemUpdate(query as CFDictionary, attributesToUpdate as CFDictionary)
        
        // Check the result of the Keychain operation and throw appropriate errors if needed
        if status == errSecItemNotFound {
            throw KeychainError.itemNotFound // Throw if the item to update is not found in Keychain
        } else if status != errSecSuccess {
            throw KeychainError.unknown(status) // Throw for any other unknown error
        }
    }
}

Query explanation

  1. kSecClass as String: kSecClassInternetPassword: This key value pair in our query dict specifies that the data item being updated in the Keychain is an internet password. The internet password type is what we commonly use to store credentials related to websites or APIs.

  2. kSecAttrAccount as String: "api.example.com": This key value pair attribute represents the account name that uniquely identifies the item that we want to update in the Keychain store. In this case, it could be a URL or any other identifier specific to the data.

  3. kSecValueData as String: newTokenData: This key value pair is the new access token data to be updated in the Keychain. In the code, we convert the string “newAccessToken” to data using the UTF-8 encoding and update the item with this new data.

After setting up the query dictionary, we attempt to update the item in the Keychain using SecItemUpdate method. If the item to update is not found keychain will return a (errSecItemNotFound) status, and just like in the earlier example we will throw a KeychainError.itemNotFound error. For any other unknown error, we throw a KeychainError.unknown with the OSStatus value to provide more information about the specific error.

SecItemCopyMatching: Retrieving Data Item in Keychain

The purpose of the SecItemCopyMatching function is to retrieve an existing data item from keychain.

This is what we need to use whenever we want to read data we have previously stored in our keychain store, and the main parameters it takes are:

Query Parameters:

  • Query parameters for finding the item to retrieve.
  • A pointer to retrieve the requested data

Example: Retrieving a credit card info from the Keychain

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
import Foundation
import Security

class KeychainManager {
    
    enum KeychainError: Error {
        case itemNotFound
        case unexpectedDataFormat
        case unknown(OSStatus)
    }
    
    static func retrieveCreditCardData() throws -> Data {
        // Create a query dictionary to retrieve the credit card data from the Keychain
        let query: [String: Any] = [
            kSecClass as String: kSecClassGenericPassword, // Specifies the item class as a generic password
            kSecAttrAccount as String: "chasebankcreditcard123", // The account name identifying the credit card item
            kSecReturnData as String: true, // Specifies that the item data should be returned
            kSecMatchLimit as String: kSecMatchLimitOne // Specifies that only one item should be returned
        ]
        
        // Attempt to retrieve the credit card data from the Keychain
        var item: CFTypeRef?
        let status = SecItemCopyMatching(query as CFDictionary, &item)
        
        // Check the result of the Keychain operation and throw appropriate errors if needed
        if status == errSecItemNotFound {
            throw KeychainError.itemNotFound // Throw if the credit card item is not found in Keychain
        } else if status != errSecSuccess {
            throw KeychainError.unknown(status) // Throw for any other unknown error
        }
        
        // Ensure the retrieved item data is in the expected format
        guard let data = item as? Data else {
            throw KeychainError.unexpectedDataFormat // Throw if the retrieved data is not in the expected format
        }
        
        return data
    }
}

Query explanation

From our prior examples I think most of the query attributes are pretty self explanatory except these two:

  1. kSecReturnData as String: true: This query attribute tells keychain that the data item (credit card data) should be returned as part of the query result.

  2. kSecMatchLimit as String: kSecMatchLimitOne: This query attribute help us limit the number of data items returned by the query to one. In this case, we expect only one credit card item to be retrieved.

Our retrieveCreditCardData method in action:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
import Foundation

// Assuming the KeychainManager class is defined as above
    do {
        let creditCardData = try KeychainManager.retrieveCreditCardData()
        // Process the retrieved credit card data
        // For example, you can convert the data to a string or decode it as needed
        if let creditCardInfo = String(data: creditCardData, encoding: .utf8) {
            print("Credit Card Data: \(creditCardInfo)")
        } else {
            print("Failed to convert credit card data to a string.")
        }
    } catch KeychainManager.KeychainError.itemNotFound {
        print("Credit card item not found in Keychain.")
    } catch KeychainManager.KeychainError.unexpectedDataFormat {
        print("Unexpected data format retrieved from Keychain.")
    } catch KeychainManager.KeychainError.unknown(let status) {
        print("Unknown error occurred: \(status)")
    } catch {
        print("Unexpected error: \(error)")
    }

SecItemDelete: Deleting Data from Keychain

The purpose of the SecItemDelete function is to delete an existing data from the keychain store.

This is what we need to use whenever we want to delete data we have previously stored in our keychain store, and the main parameters it takes are:

Query Parameters:

  • Query parameters for finding the item to delete.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
import Foundation
import Security

class KeychainManager {
    
    enum KeychainError: Error {
        case itemNotFound
        case unknown(OSStatus)
    }
    
    static func deletePassword() throws {
        // Create a query dictionary to identify the password item to delete from the Keychain
        let query: [String: Any] = [
            kSecClass as String: kSecClassGenericPassword, // Specifies the item class as a generic password
            kSecAttrAccount as String: "[email protected]", // The account name identifying the password data item
        ]
        
        // Attempt to delete the password item from the Keychain
        let status = SecItemDelete(query as CFDictionary)
        
        // Check the result of the Keychain operation and throw appropriate errors if needed
        if status == errSecItemNotFound {
            throw KeychainError.itemNotFound // Throw if the password item is not found in Keychain
        } else if status != errSecSuccess {
            throw KeychainError.unknown(status) // Throw for any other unknown error
        }
    }
}

Using our deletePassword method:

1
2
3
4
5
6
7
8
9
10
do {
    try KeychainManager.deletePassword()
    print("Password deleted successfully.")
} catch KeychainManager.KeychainError.itemNotFound {
    print("Password item not found in Keychain.")
} catch KeychainManager.KeychainError.unknown(let status) {
    print("Unknown error occurred: \(status)")
} catch {
    print("Unexpected error: \(error)")
}

Conclusion

Despite it’s age and the challenges associated with it’s aging interface, the Keychain framework remains a powerful tool for securely storing sensitive data. Hopefully by helping you understand its core functions, you will be able to build robust keychain stores that protect your user’s sensitive data. Thanks for reading!

This post is licensed under CC BY 4.0 by the author.