SoFunction
Updated on 2025-04-12

iOS data persistence KeyChain data operation detailed explanation

text

When we develop iOS applications, we often need to store sensitive data (password, accessToken, secretKey, etc.) locally. For beginner programmers, the first thing that comes to mind may be the useUserDefaults. However, it is well known thatUserDefaultsIt is simply a low idea to store sensitive information. Because we usually store itUserDefaultsThe data in it is not encoded, which is very insecure.

In order to safely store sensitive information locally, we should use the Apple providedKeyChainServe. This framework is quite old, so when we read it later, we will feel that the API it provides is not as fast as the current framework.

In this article, you will show you how to create a general keyChain auxiliary class that is suitable for iOS and MacOS, and add, delete, modify and check data. Let's get started! ! !

Save data to KeyChain

final class KeyChainHelper {
    static let standard = KeyChainHelper()
    private init(){}
}

We must use it ingeniouslySecItemAdd(_:_:)Method, this method will receive aCFDictionaryA query object of type.

The idea is to create a query object that contains the most important data key-value pairs we want to store. Then, pass the query object intoSecItemAdd(_:_:)The method is used to perform the save operation.

func save(_ data: Data, service: String, account: String) {
    // Create query
    let query = [
        kSecValueData: data,
        kSecClass: kSecClassGenericPassword,
        kSecAttrService: service,
        kSecAttrAccount: account,
    ] as CFDictionary
    // Add data in query to keychain
    let status = SecItemAdd(query, nil)
    if status != errSecSuccess {
        // Print out the error
        print("Error: (status)")
    }
}

Looking back at the above code snippet, the query object consists of 4 key-value pairs:

  • kSecValueData: This key means that the data has been stored in the keyChain
  • kSecClass: This key means that the data has been stored in the keyChain. We set its value askSecClassGenericPassword, This means that the data we save is a common password item
  • kSecAttrServiceandkSecAttrAccount: whenkSecClassSet tokSecClassGenericPasswordWhenkSecAttrServiceandkSecAttrAccountThese two keys are necessary. The values ​​corresponding to these two keys will serve as the key keys of the saved data, in other words, we will use them to read the saved value from the keyChain.

forkSecAttrServiceandkSecAttrAccountThe definition of the corresponding value is not difficult. It is recommended to use strings. For example: If we want to store Facebook's accessToken, we need tokSecAttrServiceSet to "access-token" andkSecAttrAccountSet to "facebook"

After creating the query object, we can call itSecItemAdd(_:_:)Method to save data to keyChain.SecItemAdd(_:_:)The method will return aOSStatusto represent storage status. If what we get iserrSecSuccessstate means that the data has been successfully saved to keyChain

Below issave(_:service:account:)Use of methods

let accessToken = "dummy-access-token"
let data = Data(accessToken.utf8)
(data, service: "access-token", account: "facebook")

keyChain cannot be used in playground, so the above code must be written in the Controller.

Update existing data in KeyChain

Now we havesave(_:service:account:)Method, let's use the samekSecAttrServiceandkSecAttrAccountThe corresponding value is used to store other tokens

let accessToken = "another-dummy-access-token"
let data = Data(accessToken.utf8)
(data, service: "access-token", account: "facebook")

At this time, we cannot save the accessToken into the keyChain. At the same time, we will get aError: -25299Report an error. This error code represents storage failure. Because the keys we use already exist in keyChain.

To solve this problem, we need to check this error code (equivalent toerrSecDuplicateItem), then useSecItemUpdate(_:_:)Method to update keyChain. Let's take a look and update our aforementionedsave(_:service:account:)Method:

func save(_ data: Data, service: String, account: String) {
    // ... ...
    // ... ...
    if status == errSecDuplicateItem {
        // Item already exist, thus update it.
        let query = [
            kSecAttrService: service,
            kSecAttrAccount: account,
            kSecClass: kSecClassGenericPassword,
        ] as CFDictionary
        let attributesToUpdate = [kSecValueData: data] as CFDictionary
        // Update existing item
        SecItemUpdate(query, attributesToUpdate)
    }
}

Similar to the save operation, we need to create a query object first, which containskSecAttrServiceandkSecAttrAccount. But this time, we will create another one containingkSecValueDatadictionary and pass it toSecItemUpdate(_:_:)method.

In this way, we can makesave(_:service:account:)Method to update the existing data in keyChain.

Read data from KeyChain

The way data is read from keyChain is very similar to how it is saved. The first thing we need to do is create a query object and then call a keyChain method:

func read(service: String, account: String) -> Data? {
    let query = [
        kSecAttrService: service,
        kSecAttrAccount: account,
        kSecClass: kSecClassGenericPassword,
        kSecReturnData: true
    ] as CFDictionary
    var result: AnyObject?
    SecItemCopyMatching(query, &result)
    return (result as? Data)
}

As before, we need to set the query objectkSecAttrService and kSecAttrAccountvalue. Before this, we need to add a new key to the query objectkSecReturnData, its value istrue, which means that we want query to return the data of the corresponding item.

After that, we will useSecItemCopyMatching(_:_:)Method and pass in by referenceAnyObjectType ofresultObject.SecItemCopyMatching(_:_:)The method also returns an OSStatus value, representing the state of the read operation. But if the read fails, we do not do any checking here and returnnil

There are so many operations to enable keyChain to support reading. Let's see how it works.

let data = (service: "access-token", account: "facebook")!
let accessToken = String(data: data, encoding: .utf8)!
print(accessToken)

Delete data from KeyChain

If there is no delete operation, our KeyChainHelper class is not completed. Let's take a look at the following code snippet

func delete(service: String, account: String) {
    let query = [
        kSecAttrService: service,
        kSecAttrAccount: account,
        kSecClass: kSecClassGenericPassword,
        ] as CFDictionary
    // Delete item from keychain
    SecItemDelete(query)
}

If you are watching the whole process, the above code may be very familiar to you, which is quite "self-explanatory". It should be noted that we use it hereSecItemDelete(_:)Method to delete the data in KeyChain.

Create a common KeyChainHelper class

storage

func save<T>(_ item: T, service: String, account: String) where T : Codable {
    do {
        // Encode as JSON data and save in keychain
        let data = try JSONEncoder().encode(item)
        save(data, service: service, account: account)
    } catch {
        assertionFailure("Fail to encode item for keychain: (error)")
    }
}

Read

func read<T>(service: String, account: String, type: ) -> T? where T : Codable {
    // Read item data from keychain
    guard let data = read(service: service, account: account) else {
        return nil
    }
    // Decode JSON data to object
    do {
        let item = try JSONDecoder().decode(type, from: data)
        return item
    } catch {
        assertionFailure("Fail to decode item for keychain: \(error)")
        return nil
    }
}

use

struct Auth: Codable {
    let accessToken: String
    let refreshToken: String
}
// Create an object to save
let auth = Auth(accessToken: "dummy-access-token",
                 refreshToken: "dummy-refresh-token")
let account = ""
let service = "token"
// Save `auth` to keychain
(auth, service: service, account: account)
// Read `auth` from keychain
let result = (service: service,
                                          account: account,
                                          type: )!
print()   // Output: "dummy-access-token"
print()  // Output: "dummy-refresh-token"

The above is the detailed content of iOS data persistence KeyChain. For more information about iOS data persistence KeyChain, please follow my other related articles!