通过KeyChain保存和检索值

32

我想将一个整数存储起来,并使用KeyChain检索它。

这是我如何保存它:

func SaveNumberOfImagesTaken()
    {
        let key = "IMAGE_TAKEN"
        var taken = 10
        let data = NSKeyedArchiver.archivedDataWithRootObject(taken)
        let query : [String:AnyObject] = [
            kSecClass as String : kSecClassGenericPassword,
            kSecAttrAccount as String : key,
            kSecValueData as String : data
        ]
        let status : OSStatus = SecItemAdd(query as CFDictionaryRef, nil)

    }

这是我尝试检索它的方式:

func CheckIfKeyChainValueExitss() -> AnyObject? {
    var key = "IMAGE_TAKEN"
    let query : [String:AnyObject] = [
        kSecClass as String       : kSecClassGenericPassword,
        kSecAttrAccount as String : key,
        kSecReturnData as String  : kCFBooleanTrue,
        kSecMatchLimit as String  : kSecMatchLimitOne ]

    var dataTypeRef :Unmanaged<AnyObject>?

    let status: OSStatus = SecItemCopyMatching(query, &dataTypeRef)

    if let op = dataTypeRef?.toOpaque() {
        let data = Unmanaged<NSData>.fromOpaque(op).takeUnretainedValue()
        if let string: AnyObject? =  NSKeyedUnarchiver.unarchiveObjectWithData(data) as? AnyObject? {
            if key == "IMAGE_TAKEN"
            {
                return string as! String!

            }
            else if string == nil
            {
                return nil
            }
        }
    }
    return nil

}

我遇到了以下错误:

无法将类型为 '__NSCFNumber' 的值转换为 'NSString'

我尝试更改变量但没有成功。


我是保存错了还是检索错了?我有点困惑,哈哈 @Paulw11 - Roi Mulia
这个可插拔的钥匙链包装器可能会给你一些灵感... https://github.com/ashleymills/Keychain.swift - Ashley Mills
@AshleyMills 请看下面我的回答。 - Roi Mulia
7个回答

64

我已经更新了Eric的Swift 5版本:

class KeyChain {

    class func save(key: String, data: Data) -> OSStatus {
        let query = [
            kSecClass as String       : kSecClassGenericPassword as String,
            kSecAttrAccount as String : key,
            kSecValueData as String   : data ] as [String : Any]

        SecItemDelete(query as CFDictionary)

        return SecItemAdd(query as CFDictionary, nil)
    }

    class func load(key: String) -> Data? {
        let query = [
            kSecClass as String       : kSecClassGenericPassword,
            kSecAttrAccount as String : key,
            kSecReturnData as String  : kCFBooleanTrue!,
            kSecMatchLimit as String  : kSecMatchLimitOne ] as [String : Any]

        var dataTypeRef: AnyObject? = nil

        let status: OSStatus = SecItemCopyMatching(query as CFDictionary, &dataTypeRef)

        if status == noErr {
            return dataTypeRef as! Data?
        } else {
            return nil
        }
    }

    class func createUniqueID() -> String {
        let uuid: CFUUID = CFUUIDCreate(nil)
        let cfStr: CFString = CFUUIDCreateString(nil, uuid)

        let swiftString: String = cfStr as String
        return swiftString
    }
}

extension Data {

    init<T>(from value: T) {
        var value = value
        self.init(buffer: UnsafeBufferPointer(start: &value, count: 1))
    }

    func to<T>(type: T.Type) -> T {
        return self.withUnsafeBytes { $0.load(as: T.self) }
    }
}

我已经为Swift 3更新了Eric的版本:

class KeyChain {

    class func save(key: String, data: Data) -> OSStatus {
        let query = [
            kSecClass as String       : kSecClassGenericPassword as String,
            kSecAttrAccount as String : key,
            kSecValueData as String   : data ] as [String : Any]

        SecItemDelete(query as CFDictionary)

        return SecItemAdd(query as CFDictionary, nil)
    }

    class func load(key: String) -> Data? {
        let query = [
            kSecClass as String       : kSecClassGenericPassword,
            kSecAttrAccount as String : key,
            kSecReturnData as String  : kCFBooleanTrue,
            kSecMatchLimit as String  : kSecMatchLimitOne ] as [String : Any]

        var dataTypeRef: AnyObject? = nil

        let status: OSStatus = SecItemCopyMatching(query as CFDictionary, &dataTypeRef)

        if status == noErr {
            return dataTypeRef as! Data?
        } else {
            return nil
        }
    }

    class func createUniqueID() -> String {
        let uuid: CFUUID = CFUUIDCreate(nil)
        let cfStr: CFString = CFUUIDCreateString(nil, uuid)

        let swiftString: String = cfStr as String
        return swiftString
    }
}

extension Data {

    init<T>(from value: T) {
        var value = value
        self.init(buffer: UnsafeBufferPointer(start: &value, count: 1))
    }

    func to<T>(type: T.Type) -> T {
        return self.withUnsafeBytes { $0.pointee }
    }
}

示例用法:

let int: Int = 555
let data = Data(from: int)
let status = KeyChain.save(key: "MyNumber", data: data)
print("status: ", status)

if let receivedData = KeyChain.load(key: "MyNumber") {
    let result = receivedData.to(type: Int.self)
    print("result: ", result)
}

使用此方法存储/检索字符串值时出现错误。保存字符串时没有错误,但在接收它时,receivedData 不为 nil(至少具有 24 的 .count),但是 receivedData.to(type: String.self) 返回 nil。有什么想法吗? - ConfusionTowers
1
在Swift 5中,针对行“return self.withUnsafeBytes { $0.pointee }”出现警告“withUnsafeBytes已弃用:请改用withUnsafeBytes <R>(_:(UnsafeRawBufferPointer)throws-> R)rethrows-> R”,我该如何解决?谢谢。 - Jim B
3
这种方法可以应用在先保存数据再后加载的情况下,但如果你是先加载再保存(例如):第一次检查钥匙串里的值时没有找到,那么程序就会崩溃。 - aalesano
4
在这一行代码发生崩溃: return self.withUnsafeBytes { $0.pointee } - Muhammad
我建议注意,对于 kSecClassGenericPassword 类,只设置了 kSecAttrAccount。但是,这个类的主键是由 kSecAttrAccountkSecAttrService 的组合构成的,因此添加 kSecAttrService 也是一个好主意。更多信息请参见:https://stackoverflow.com/a/57926586/1322703 - Alexander Bekert
显示剩余7条评论

6

我刚刚使用了外部资源等,并制作了一个好用的助手: 享受它吧!

 class func save(key: String, data: NSData) {
        let query = [
            kSecClass as String       : kSecClassGenericPassword as String,
            kSecAttrAccount as String : key,
            kSecValueData as String   : data ]

        SecItemDelete(query as CFDictionaryRef)

        let status: OSStatus = SecItemAdd(query as CFDictionaryRef, nil)

    }

    class func load(key: String) -> NSData? {
        let query = [
            kSecClass as String       : kSecClassGenericPassword,
            kSecAttrAccount as String : key,
            kSecReturnData as String  : kCFBooleanTrue,
            kSecMatchLimit as String  : kSecMatchLimitOne ]

        var dataTypeRef :Unmanaged<AnyObject>?

        let status: OSStatus = SecItemCopyMatching(query, &dataTypeRef)

        if status == noErr {
            return (dataTypeRef!.takeRetainedValue() as! NSData)
        } else {
            return nil
        }


    }

    class func stringToNSDATA(string : String)->NSData
    {
        let _Data = (string as NSString).dataUsingEncoding(NSUTF8StringEncoding)
        return _Data!

    }


    class func NSDATAtoString(data: NSData)->String
    {
        var returned_string : String = NSString(data: data, encoding: NSUTF8StringEncoding)! as String
        return returned_string
    }

    class func intToNSDATA(r_Integer : Int)->NSData
    {

            var SavedInt: Int = r_Integer
            let _Data = NSData(bytes: &SavedInt, length: sizeof(Int))
        return _Data

    }
    class func NSDATAtoInteger(_Data : NSData) -> Int
    {
            var RecievedValue : Int = 0
            _Data.getBytes(&RecievedValue, length: sizeof(Int))
            return RecievedValue

    }
    class func CreateUniqueID() -> String
    {
        var uuid: CFUUIDRef = CFUUIDCreate(nil)
        var cfStr:CFString = CFUUIDCreateString(nil, uuid)

        var nsTypeString = cfStr as NSString
        var swiftString:String = nsTypeString as String
        return swiftString
    }

    //EXAMPLES
//    
//    //Save And Parse Int


//    var Int_Data = KeyChain.intToNSDATA(555)
//    KeyChain.save("MAMA", data: Int_Data)
//    var RecievedDataAfterSave = KeyChain.load("MAMA")
//    var NSDataTooInt = KeyChain.NSDATAtoInteger(RecievedDataAfterSave!)
//    println(NSDataTooInt)
//    
//    
//    //Save And Parse String


//    var string_Data = KeyChain.stringToNSDATA("MANIAK")
//    KeyChain.save("ZAHAL", data: string_Data)
//    var RecievedDataStringAfterSave = KeyChain.load("ZAHAL")
//    var NSDATAtoString = KeyChain.NSDATAtoString(RecievedDataStringAfterSave!)
//    println(NSDATAtoString)

6

这是 Sazzad Hissain Khan的答案 重写版,适用于iOS,没有使用非Swifty NS前缀属性,代码更加清晰。

import Security

class KeychainService {
    class func updatePassword(service: String, account: String, data: String) {
        guard let dataFromString = data.data(using: .utf8, allowLossyConversion: false) else {
            return
        }

        let status = SecItemUpdate(modifierQuery(service: service, account: account), [kSecValueData: dataFromString] as CFDictionary)

        checkError(status)
    }

    class func removePassword(service: String, account: String) {
        let status = SecItemDelete(modifierQuery(service: service, account: account))

        checkError(status)
    }

    class func savePassword(service: String, account: String, data: String) {
        guard let dataFromString = data.data(using: .utf8, allowLossyConversion: false) else {
            return
        }

        let keychainQuery: [CFString: Any] = [kSecClass: kSecClassGenericPassword,
                                              kSecAttrService: service,
                                              kSecAttrAccount: account,
                                              kSecValueData: dataFromString]

        let status = SecItemAdd(keychainQuery as CFDictionary, nil)

        checkError(status)
    }

    class func loadPassword(service: String, account: String) -> String? {
        var dataTypeRef: CFTypeRef?

        let status = SecItemCopyMatching(modifierQuery(service: service, account: account), &dataTypeRef)

        if status == errSecSuccess,
            let retrievedData = dataTypeRef as? Data {
            return String(data: retrievedData, encoding: .utf8)
        } else {
            checkError(status)

            return nil
        }
    }

    fileprivate static func modifierQuery(service: String, account: String) -> CFDictionary {
        let keychainQuery: [CFString: Any] = [kSecClass: kSecClassGenericPassword,
                                              kSecAttrService: service,
                                              kSecAttrAccount: account,
                                              kSecReturnData: kCFBooleanTrue]

        return keychainQuery as CFDictionary
    }

    fileprivate static func checkError(_ status: OSStatus) {
        if status != errSecSuccess {
            if #available(iOS 11.3, *),
            let err = SecCopyErrorMessageString(status, nil) {
                print("Operation failed: \(err)")
            } else {
                print("Operation failed: \(status). Check the error message through https://osstatus.com.")
            }
        }
    }
}

钥匙串中存储的数据是否有可能受到影响?我的意思是操作系统更新或其他情况下会不会受到影响? - Mitesh Dobareeya

5

Roi Mulia的答案非常好,这里是一份对Swift 2进行了一些微小调整的版本:

class KeyChain {
    class func save(key: String, data: NSData) -> OSStatus {
        let query = [
            kSecClass as String       : kSecClassGenericPassword as String,
            kSecAttrAccount as String : key,
            kSecValueData as String   : data ]

        SecItemDelete(query as CFDictionaryRef)

        return SecItemAdd(query as CFDictionaryRef, nil)

    }

    class func load(key: String) -> NSData? {
        let query = [
            kSecClass as String       : kSecClassGenericPassword,
            kSecAttrAccount as String : key,
            kSecReturnData as String  : kCFBooleanTrue,
            kSecMatchLimit as String  : kSecMatchLimitOne ]

        var dataTypeRef:AnyObject? = nil

        let status: OSStatus = SecItemCopyMatching(query, &dataTypeRef)

        if status == noErr {
            return (dataTypeRef! as! NSData)
        } else {
            return nil
        }


    }

    class func stringToNSDATA(string : String)->NSData
    {
        let _Data = (string as NSString).dataUsingEncoding(NSUTF8StringEncoding)
        return _Data!

    }


    class func NSDATAtoString(data: NSData)->String
    {
        let returned_string : String = NSString(data: data, encoding: NSUTF8StringEncoding)! as String
        return returned_string
    }

    class func intToNSDATA(r_Integer : Int)->NSData
    {

        var SavedInt: Int = r_Integer
        let _Data = NSData(bytes: &SavedInt, length: sizeof(Int))
        return _Data

    }
    class func NSDATAtoInteger(_Data : NSData) -> Int
    {
        var RecievedValue : Int = 0
        _Data.getBytes(&RecievedValue, length: sizeof(Int))
        return RecievedValue

    }
    class func CreateUniqueID() -> String
    {
        let uuid: CFUUIDRef = CFUUIDCreate(nil)
        let cfStr:CFString = CFUUIDCreateString(nil, uuid)

        let nsTypeString = cfStr as NSString
        let swiftString:String = nsTypeString as String
        return swiftString
    }
}

示例用法:

let data = KeyChain.intToNSDATA(555)
let status = KeyChain.save("MyNumber", data: data)
print(status)

if let receivedData = KeyChain.load("MyNumber") {
    let result = KeyChain.NSDATAtoInteger(receivedData)
    print(result)
}

钥匙串中存储的数据是否有可能受到影响?我的意思是操作系统更新等是否会影响它? - Mitesh Dobareeya

4

我试图让它尽可能简单。

fileprivate class KeychainService {

  static func updatePassword(_ password: String, serviceKey: String) {
    guard let dataFromString = password.data(using: .utf8) else { return }

    let keychainQuery: [CFString : Any] = [kSecClass: kSecClassGenericPassword,
                                           kSecAttrService: serviceKey,
                                           kSecValueData: dataFromString]
    SecItemDelete(keychainQuery as CFDictionary)
    SecItemAdd(keychainQuery as CFDictionary, nil)
  }

  static func removePassword(serviceKey: String) {

    let keychainQuery: [CFString : Any] = [kSecClass: kSecClassGenericPassword,
                                           kSecAttrService: serviceKey]

    SecItemDelete(keychainQuery as CFDictionary)
  }

  static func loadPassword(serviceKey: String) -> String? {
    let keychainQuery: [CFString : Any] = [kSecClass : kSecClassGenericPassword,
                                           kSecAttrService : serviceKey,
                                           kSecReturnData: kCFBooleanTrue,
                                           kSecMatchLimitOne: kSecMatchLimitOne]

    var dataTypeRef: AnyObject?
    SecItemCopyMatching(keychainQuery as CFDictionary, &dataTypeRef)
    guard let retrievedData = dataTypeRef as? Data else { return nil }

    return String(data: retrievedData, encoding: .utf8)
  }

  static func flush()  {
    let secItemClasses =  [kSecClassGenericPassword]
    for itemClass in secItemClasses {
      let spec: NSDictionary = [kSecClass: itemClass]
      SecItemDelete(spec)
    }
  }
}

如何使用这个? - Yogesh Patel
1
KeychainService.updatePassword("password", serviceKey: "key"); let password = KeychainService.loadPassword("key"); - Igor Kovryzhkin

1

以下是如何保存和检索一个结构体 User 的示例,这是一个相当常见的用例:

import Security
import UIKit

class KeyChain {
    struct User {
        let identifier: Int64
        let password: String
    }

    private static let service = "MyService"

    static func save(user: User) -> Bool {
        let identifier = Data(from: user.identifier)
        let password = user.password.data(using: .utf8)!
        let query = [kSecClass as String : kSecClassGenericPassword as String,
                     kSecAttrService as String : service,
                     kSecAttrAccount as String : identifier,
                     kSecValueData as String : password]
            as [String : Any]

        let deleteStatus = SecItemDelete(query as CFDictionary)

        if deleteStatus == noErr || deleteStatus == errSecItemNotFound {
            return SecItemAdd(query as CFDictionary, nil) == noErr
        }

        return false
    }

    static func retrieveUser() -> User? {
        let query = [kSecClass as String : kSecClassGenericPassword,
                     kSecAttrService as String : service,
                     kSecReturnAttributes as String : kCFBooleanTrue!,
                     kSecReturnData as String: kCFBooleanTrue!]
            as [String : Any]

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

        if status == noErr,
            let dict = result as? [String: Any],
            let passwordData = dict[String(kSecValueData)] as? Data,
            let password = String(data: passwordData, encoding: .utf8),
            let identifier = (dict[String(kSecAttrAccount)] as? Data)?.to(type: Int64.self) {

            return User(identifier: identifier, password: password)
        } else {
            return nil
        }
    }
}

private extension Data {
    init<T>(from value: T) {
        var value = value

        self.init(buffer: UnsafeBufferPointer(start: &value, count: 1))
    }

    func to<T>(type: T.Type) -> T {
        withUnsafeBytes { $0.load(as: T.self) }
    }
}

我没有看到解决方案中使用任何数据扩展方法。它们是必需的吗?如果是,具体用于什么? - Benzy
这个在uitextfields中的实现是什么?我想在viewdidload()中接收我的用户信息,这样它就已经存在了。 - FrosyFeet456

0

你在存储一个数字,而不是一个字符串,所以你得到的是一个 NSNumber,而不是一个字符串。这个异常非常清晰明了 - 你不能将 NSNumber 强制转换为 String - 你可以使用 stringValue() 方法来获取 NSNumber 的字符串表示形式。

if let op = dataTypeRef?.toOpaque() {
    let data = Unmanaged<NSData>.fromOpaque(op).takeUnretainedValue()
    if let string: AnyObject? =  NSKeyedUnarchiver.unarchiveObjectWithData(data) as? AnyObject? {
        if key == "IMAGE_TAKEN"
        {
            return string.stringValue() as! String!
        }
        else if string == nil
        {
                return nil
        }
    }
}

因为我假设你需要一个字符串。它已经是NSNumber了。返回int可能更有意义,这种情况下您将使用intValue() - 但您需要更改函数签名。理论上,您可以省略as!String!,因为stringValue()始终返回字符串。 - Paulw11
没有代码在我面前,你可能需要先将 string 强制转换为 NSNumber,以防止编译器抱怨它不知道 AnyObject?stringValue - Paulw11
最终我想要实现的是存储整数。在需要时检索它,将数量添加到值上并再次存储。我之前发布的代码中,我是否正确地保存/存储了它?问题出在检索方法还是两者都有问题?这个密钥链真的让我困惑,哈哈,抱歉。谢谢! - Roi Mulia
你想把这些数据存储在钥匙串中的原因是什么?对于简单的整数,NSUserDefaults更简单。钥匙串最适合需要保密的密码等内容。此外,还有很多包装库可以使钥匙串的使用更加容易。 - Paulw11
我知道。对于你的第一个问题- 我需要存储无法删除的数据。Nsuserdefult非常好,但如果用户删除了应用程序,那就呵呵了。2. 我正在努力学习,等我弄清楚了这个问题,我会制作自己的包装器 :) 当然也会分享出来! - Roi Mulia
显示剩余12条评论

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接