Persistance des données iOS - KeyChain

Lorsque nous développons des applications iOS, nous avons souvent besoin de stocker localement des données sensibles (mot de passe, accessToken, secretKey, etc.). Pour les programmeurs débutants, la première chose qui vient à l'esprit peut être d'utiliser UserDefaults. Cependant, comme nous le savons tous, l'utiliser UserDefaultspour stocker des informations sensibles est simplement une idée de bas niveau. Étant donné que les données que nous stockons habituellement UserDefaultsne sont pas codées, cela est très dangereux.

Afin de stocker en toute sécurité des informations sensibles localement, nous devons utiliser KeyChainles services fournis par Apple. Ce framework est assez ancien, donc quand on le lira plus tard, on aura l'impression que l'API qu'il fournit n'est pas aussi rapide que le framework actuel.

Dans cet article, je vais vous montrer comment créer une classe auxiliaire keyChain à usage général adaptée à la fois à iOS et à MacOS pour effectuer des opérations d'ajout, de suppression, de modification et de requête sur les données. commençons! ! !

Enregistrer les données dans KeyChain

final class KeyChainHelper {
    static let standard = KeyChainHelper()
    private init(){}
}
复制代码

Il faut utiliser SecItemAdd(_:_:)la méthode intelligemment, cette méthode va recevoir un CFDictionarytype d'objet requête.

L'idée est de créer un objet de requête contenant des paires clé-valeur des données les plus importantes que nous souhaitons stocker. Ensuite, transmettez l'objet de requête dans SecItemAdd(_:_:)la méthode pour effectuer l'opération de sauvegarde.

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)")
    }
}
复制代码

En regardant l'extrait de code ci-dessus, l'objet de requête se compose de 4 paires clé-valeur :

  • kSecValueData: Cette clé représente que les données ont été stockées dans le keyChain
  • kSecClass: Cette clé représente que les données ont été stockées dans le keyChain. Nous définissons sa valeur sur kSecClassGenericPassword, ce qui signifie que les données que nous sauvegardons sont un élément de mot de passe général
  • kSecAttrServiceet kSecAttrAccount: Lorsqu'il kSecClassest réglé sur kSecClassGenericPassword, les touches kSecAttrServiceet kSecAttrAccountsont nécessaires. La valeur correspondant à ces deux clés sera utilisée comme clé clé des données sauvegardées, en d'autres termes, nous les utiliserons pour lire la valeur sauvegardée du keyChain.

对于kSecAttrServicekSecAttrAccount所对应的值的定义并没有什么难的。推荐使用字符串。例如:如果我们想存储Facebook的accesToken,我们需要将kSecAttrService设置成”access-token“,将kSecAttrAccount设置成”facebook“

创建完query对象之后,我们可以调用SecItemAdd(_:_:)方法来保存数据到keyChain。SecItemAdd(_:_:)方法会返回一个OSStatus来代表存储状态。如果我们得到的是errSecSuccess状态,则意味着数据已经被成功保存到keyChain中

下面是save(_:service:account:)方法的使用

let accessToken = "dummy-access-token"
let data = Data(accessToken.utf8)
KeychainHelper.standard.save(data, service: "access-token", account: "facebook")
复制代码

keyChain不能在playground中使用,所以,上述代码必须写在Controller中。

更新KeyChain中已有的数据

现在我们有了save(_:service:account:)方法,让我们用相同的kSecAttrServicekSecAttrAccount所对应的值来存储其他token

let accessToken = "another-dummy-access-token"
let data = Data(accessToken.utf8)
KeychainHelper.standard.save(data, service: "access-token", account: "facebook")
复制代码

这时候,我们就无法将accessToken保存到keyChain中了。同时,我们会得到一个Error: -25299的报错。该错误码代表的是存储失败。因为我们所使用的keys已经存在于keyChain当中了。

为了解决这个问题,我们需要检查这个错误码(相当于errSecDuplicateItem),然后使用SecItemUpdate(_:_:)方法来更新keyChain。一起看看并更新我们前述的save(_:service:account:)方法吧:

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)
    }
}
复制代码

跟保存操作相似的是,我们需要先创建一个query对象,这个对象包含kSecAttrServicekSecAttrAccount。但是这次,我们将会创建另外一个包含kSecValueData的字典,并将它传给SecItemUpdate(_:_:)方法。

这样的话,我们就可以让save(_:service:account:)方法来更新keyChain中已有的数据了。

从KeyChain中读取数据

从keyChain中读取数据的方式和保存的方式非常相似。我们首先要做的是创建一个query对象,然后调用一个keyChain方法:

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)
}
复制代码

跟之前一样,我们需要设置query对象的kSecAttrService and kSecAttrAccount的值。在这之前,我们需要为query对象添加一个新的键kSecReturnData,其值为true,代表的是我们希望query返回对应项的数据。

之后,我们将利用 SecItemCopyMatching(_:_:) 方法并通过引用传入 AnyObject 类型的result对象。SecItemCopyMatching(_:_:)方法同样返回一个OSStatus类型的值,代表读取操作状态。但是如果读取失败了,这里我们不做任何校验,并返回nil

让keyChain支持读取的操作就这么多了,看一下他是怎么工作的吧

let data = KeychainHelper.standard.read(service: "access-token", account: "facebook")!
let accessToken = String(data: data, encoding: .utf8)!
print(accessToken)
复制代码

从KeyChain中删除数据

如果没有删除操作,我们的KeyChainHelper类并不算完成。一起看看下面的代码片段吧

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

如果你全程都在看的话,上述代码可能对你来说非常熟悉,那是相当的”自解释“了,需要注意的是,这里我们使用了SecItemDelete(_:)方法来删除KeyChain中的数据了。

创建一个通用的KeyChainHelper 类

存储

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)")
    }
}
复制代码

读取

func read<T>(service: String, account: String, type: T.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
    }
}
复制代码

使用

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 = "domain.com"
let service = "token"

// Save `auth` to keychain
KeychainHelper.standard.save(auth, service: service, account: account)

// Read `auth` from keychain
let result = KeychainHelper.standard.read(service: service,
                                          account: account,
                                          type: Auth.self)!

print(result.accessToken)   // Output: "dummy-access-token"
print(result.refreshToken)  // Output: "dummy-refresh-token"
复制代码

Guess you like

Origin juejin.im/post/7195486949526732857