ios - 将 accessToken 存储在 iOS 钥匙串(keychain)中

标签 ios swift keychain

我正在寻找在iOS钥匙串(keychain)中存储/​​加载accessToken和refreshToken的简单方法。

到目前为止我已经做到了:

    enum Key: String {
        case accessToken = "some.keys.accessToken"
        case refreshToken = "some.keys.refreshToken"
    
        fileprivate var tag: Data {
            rawValue.data(using: .utf8)!
        }
    }

    enum KeychainError: Error {
        case storeFailed
        case loadFailed
    }
    
    func store(key: Key, value: String) throws {
        let addQuery: [String: Any] = [
            kSecClass as String: kSecClassKey,
            kSecAttrApplicationTag as String: key.tag,
            kSecValueRef as String: value
        ]
        let status = SecItemAdd(addQuery as CFDictionary, nil)
        guard status == errSecSuccess else {
            print("Store key: '\(key.rawValue)' in Keychain failed with status: \(status.description)")
            throw KeychainError.storeFailed
        }
    }
    
    func load(key: Key) throws -> String? {
        let getQuery: [String: Any] = [
            kSecClass as String: kSecClassKey,
            kSecAttrApplicationTag as String: key.tag,
            kSecAttrKeyType as String: kSecAttrKeyTypeRSA,
            kSecReturnRef as String: true
        ]
        
        var item: CFTypeRef?
        let status = SecItemCopyMatching(getQuery as CFDictionary, &item)
        guard status == errSecSuccess else { 
            print("Load key: '\(key.rawValue)' in Keychain failed with status: \(status.description)")
            throw KeychainError.loadFailed 
        }
        return item as? String
    }

但是失败并显示消息:

运行store时:

Store key: 'some.keys.accessToken' in Keychain failed with status: -50

运行加载时:

Load key: 'some.keys.accessToken' in Keychain failed with status: -25300

我在这里做错了什么?

最佳答案

根据Apple的建议,您应该使用kSecClassGenericPassword类来安全地存储任意数据,即 token 。要正确执行此操作,您需要在 kSecAttrAccount 键下存储一个 String 标识符,并在下存储一个安全值的 Data 表示形式kSecValueData 键。您可以通过执行以下操作轻松将字符串值转换为 Data(假设 token 包含 UTF8 数据)。

tokenString.data(using: .utf8)
// or
Data(tokenString.utf8)

首先有一些不错的东西。

/// Errors that can be thrown when the Keychain is queried.
enum KeychainError: LocalizedError {
    /// The requested item was not found in the Keychain.
    case itemNotFound
    /// Attempted to save an item that already exists.
    /// Update the item instead.
    case duplicateItem
    /// The operation resulted in an unexpected status.
    case unexpectedStatus(OSStatus)
}

/// A service that can be used to group the tokens
/// as the kSecAttrAccount should be unique.
let service = "com.bundle.stuff.token-service"

将 token 插入钥匙串(keychain)。

func insertToken(_ token: Data, identifier: String, service: String = service) throws {
    let attributes = [
        kSecClass: kSecClassGenericPassword,
        kSecAttrService: service,
        kSecAttrAccount: identifier,
        kSecValueData: token
    ] as CFDictionary

    let status = SecItemAdd(attributes, nil)
    guard status == errSecSuccess else {
        if status == errSecDuplicateItem {
            throw KeychainError.duplicateItem
        }
        throw KeychainError.unexpectedStatus(status)
    }
}

检索将按如下方式完成。

func getToken(identifier: String, service: String = service) throws -> String {
    let query = [
        kSecClass: kSecClassGenericPassword,
        kSecAttrService: service,
        kSecAttrAccount: identifier,
        kSecMatchLimit: kSecMatchLimitOne,
        kSecReturnData: true
    ] as CFDictionary

    var result: AnyObject?
    let status = SecItemCopyMatching(query, &result)

    guard status == errSecSuccess else {
        if status == errSecItemNotFound {
            // Technically could make the return optional and return nil here
            // depending on how you like this to be taken care of
            throw KeychainError.itemNotFound
        }
        throw KeychainError.unexpectedStatus(status)
    }
    // Lots of bang operators here, due to the nature of Keychain functionality.
    // You could work with more guards/if let or others.
    return String(data: result as! Data, encoding: .utf8)!
}

请注意,如前所述,通用密码具有某些规范,我认为最重要的规范是 kSecAttrAccount 标志对于您存储的每个 token 必须是唯一的。您不能存储同一标识符的访问 token A 和访问 token B。这将导致触发 .duplicateItem 错误。

我还想指出 OSStatus website对于获取有关错误代码的更多信息非常有用。除了网站之外,还有 SecCopyErrorMessageString(OSStatus, UnsafeMutableRawPointer?) 函数可以获取有关错误代码的更多信息。

现在这从技术上回答了你的问题,但下面我添加了一些更好的东西。 Update 会更新现有项目的 token 值,请在调用 update! 之前确保该项目存在。 Upsert 在 token 尚不存在时插入 token ,如果存在,则会更新 token 值。删除将从钥匙串(keychain)中删除 token 值。

func updateToken(_ token: Data, identifier: String, service: String = service) throws {
    let query = [
        kSecClass: kSecClassGenericPassword,
        kSecAttrService: service,
        kSecAttrAccount: identifier
    ] as CFDictionary

    let attributes = [
        kSecValueData: token
    ] as CFDictionary

    let status = SecItemUpdate(query, attributes)
    guard status == errSecSuccess else {
        if status == errSecItemNotFound {
            throw KeychainError.itemNotFound
        }
        throw KeychainError.unexpectedStatus(status)
    }
}

func upsertToken(_ token: Data, identifier: String, service: String = service) throws {
    do {
        _ = try getToken(identifier: identifier, service: service)
        try updateToken(token, identifier: identifier, service: service)
    } catch KeychainError.itemNotFound {
        try insertToken(token, identifier: identifier, service: service)
    }
}

func deleteToken(identifier: String, service: String = service) throws {
    let query = [
        kSecClass: kSecClassGenericPassword,
        kSecAttrService: service,
        kSecAttrAccount: identifier
    ] as CFDictionary

    let status = SecItemDelete(query)
    guard status == errSecSuccess || status == errSecItemNotFound else {
        throw KeychainError.unexpectedStatus(status)
    }
}

关于ios - 将 accessToken 存储在 iOS 钥匙串(keychain)中,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/68209016/

相关文章:

ios - Swift/iOS - 带有更多信息的 UIButton Sender

jquery - iOS Safari 调试控制台 - 如何获取错误的行号?

ios - Swift 闭包中的 nil AutoreleasingUnsafeMutablePointer 有何意义?

ios - 以编程方式锁定 iPhone (>iOS7)

ios - Realm swift : Database won't be updated after backup

macos - SecItemCopyMatching 无法读取 iCloud 钥匙串(keychain)

php - 使用 PHP 以编程方式访问钥匙串(keychain)中的 iOS 证书

ios - 自定义钥匙串(keychain)警报操作

objective-c - 这个方法在哪里调用 - (void)glkView :(GLKView *)view drawInRect:(CGRect)rect

iOS 谷歌街景 API