使用 Swift 向 iOS 钥匙串添加项目和查询

Posted

技术标签:

【中文标题】使用 Swift 向 iOS 钥匙串添加项目和查询【英文标题】:Adding Items to and Querying the iOS Keychain with Swift 【发布时间】:2014-08-11 00:51:01 【问题描述】:

我无法将所有可用于从ios Keychain 添加数据和查询数据的Objective C 代码示例转换为Swift。我正在尝试对字符串(访问令牌)进行基本存储并将其读回。我已经查看了有关 Stack Overflow 的其他一些问题,但我无法让它发挥作用。我试图从各种来源拼凑出一个解决方案。

编辑 1: 我尝试了更基本的设置,因为我认为我的 self.defaultKeychainQuery 可能把事情搞砸了。我已将以下代码更新为最新版本。

编辑 2: 搞定了。我没有正确地将数据值添加到保存查询中。我需要将字符串转换为 NSData。我已将以下代码更新为最新的工作版本。

编辑 3: 正如 Xerxes 在下面指出的那样,由于字典的某些问题,此代码不适用于高于 Beta 1 的 Xcode 版本。如果您知道解决此问题的方法,请告诉我。

更新:我把它变成了keychain library written in Swift called Locksmith。


保存

class func save(service: NSString, data: NSString) 
  var dataFromString: NSData = data.dataUsingEncoding(NSUTF8StringEncoding, allowLossyConversion: false)
  // Instantiate a new default keychain query
  var keychainQuery: NSMutableDictionary = NSMutableDictionary(objects: [kSecClassGenericPassword, service, userAccount, dataFromString], forKeys: [kSecClass, kSecAttrService, kSecAttrAccount, kSecValueData])

  // Delete any existing items
  SecItemDelete(keychainQuery as CFDictionaryRef)

  // Add the new keychain item
  var status: OSStatus = SecItemAdd(keychainQuery as CFDictionaryRef, nil)

  // Check that it worked ok
  println("Saving status code is: \(status)")

加载

  class func load(service: NSString) -> AnyObject? 
    // Instantiate a new default keychain query
    // Tell the query to return a result
    // Limit our results to one item
    var keychainQuery: NSMutableDictionary = NSMutableDictionary(objects: [kSecClassGenericPassword, service, userAccount, kCFBooleanTrue, kSecMatchLimitOne], forKeys: [kSecClass, kSecAttrService, kSecAttrAccount, kSecReturnData, kSecMatchLimit])



    // I'm not too sure what's happening here...
    var dataTypeRef :Unmanaged<AnyObject>?

    // Search for the keychain items
    let status: OSStatus = SecItemCopyMatching(keychainQuery, &dataTypeRef)


    println("Loading status code is: \(status)")

    // I'm not too sure what's happening here...
    let opaque = dataTypeRef?.toOpaque()

    if let op = opaque? 
      let retrievedData = Unmanaged<NSData>.fromOpaque(op).takeUnretainedValue()
      println("Retrieved the following data from the keychain: \(retrievedData)")
      var str = NSString(data: retrievedData, encoding: NSUTF8StringEncoding)
      println("The decoded string is \(str)")
     else 
      println("Nothing was retrieved from the keychain.")
    

    return nil
  

用法(视图控制器)

KeychainService.saveToken("sometoken")
KeychainService.loadToken()

使用这些便捷方法

class func saveToken(token: NSString) 
    self.save("service", data: token)
  

class func loadToken() 
    var token = self.load("service")
    if let t = token 
      println("The token is: \(t)")
    
  

这导致控制台中的输出:

Saving status code is: 0
Loading status code is: 0
Retrieved the following data from the keychain: <736f6d65 746f6b65 6e>
The decoded string is sometoken

非常感谢您的帮助。我不太确定一旦获得 dataTypeRef 后如何处理它,或者它是否有上面代码给出的任何数据。

【问题讨论】:

我仍在等待完成这项工作,但没有比这更明智的了。事实上,在最新版本的 Xcode 中,使用“NSDictionary”时我会收到一条错误消息。真的不应该这么沮丧!祝你好运......我会密切关注。 @Darren 我想我已经成功了。查看上面的编辑版本。我没有将 NSString 输入转换为正确的 NSData,也没有将实际数据值添加到 keychainQuery。更改此设置后,我不得不更改 load 方法来解码 NSData 响应。让我知道这是否对您有帮助:) 您使用的是哪个版本的 Xcode?我已经在 Beta-2 中尝试过您的代码,并且从keychainQuery 的定义中不断收到以下错误消息Could not find an overload for 'init' that accepts the supplied arguments。自从我改用 beta-2 后,这才开始发生在我身上。感谢您的回复。 我想我可能是第一个测试版(版本 6.0 (6A215l))。您是否尝试过使用任何其他可用方法设置键/值对?可能只是NSMutableDictionary(objects:keys:),特别是那是错误的。 matt - 你打算用这个新图书馆制作一个 Cocoapod 吗?我很想尝试一下,我真的不想用子模块导入它:) 【参考方案1】:

为了让它工作,你需要检索钥匙串常量的保留值,然后首先像这样存储:

let kSecClassValue = kSecClass.takeRetainedValue() as NSString
let kSecAttrAccountValue = kSecAttrAccount.takeRetainedValue() as NSString
let kSecValueDataValue = kSecValueData.takeRetainedValue() as NSString
let kSecClassGenericPasswordValue = kSecClassGenericPassword.takeRetainedValue() as NSString
let kSecAttrServiceValue = kSecAttrService.takeRetainedValue() as NSString
let kSecMatchLimitValue = kSecMatchLimit.takeRetainedValue() as NSString
let kSecReturnDataValue = kSecReturnData.takeRetainedValue() as NSString
let kSecMatchLimitOneValue = kSecMatchLimitOne.takeRetainedValue() as NSString

然后您可以像这样引用 NSMutableDictionary 中的值:

var keychainQuery: NSMutableDictionary = NSMutableDictionary(objects: [kSecClassGenericPasswordValue, service, userAccount, kCFBooleanTrue, kSecMatchLimitOneValue], forKeys: [kSecClassValue, kSecAttrServiceValue, kSecAttrAccountValue, kSecReturnDataValue, kSecMatchLimitValue])

我写了一篇关于它的博客文章: http://rshelby.com/2014/08/using-swift-to-save-and-query-ios-keychain-in-xcode-beta-4/

希望这会有所帮助!

谢尔比

【讨论】:

非常感谢。该类编译没有 ant 错误。但我不确定如何使用它。你能举个例子吗?或许可以稍微解释一下类中的方法(saveTokenload 等)是什么以及它们应该做什么? 感谢您的帮助!我已经更新了my original blog post 的这些更改和其他一些小事情。 @Isuru 我的帖子应该有一个简单的示例用法。让我知道它是否适合你:) @matt 嗨,我按照那个例子。在viewDidLoad 方法中,我保存一个像这样的值-KeychainService.saveToken("Some value"),然后在另一个按钮按下时,我像这样检索它-label.text = KeychainService.loadToken()。但我收到以下错误 - 没有从钥匙串中检索到任何内容。状态码-25300 @Isuru 在private class save(...),就在函数的最后,添加println("Saved to the keychain. Status code \(status)")。现在有什么输出?状态码 -25300 表示在钥匙串中找不到该项目 (reference—scroll to end) @Isuru 我认为您需要在需要时为键 kSecAttrAccountValue 使用不同的值。这只是一个字符串,因此您需要修改 loadsave 以接受参数、使用参数等。【参考方案2】:

我为这个简单的任务编写了一个演示应用程序和辅助函数:为 Keychain 中的给定键写入/读取文本字符串。

https://github.com/marketplacer/keychain-swift

let keychain = KeychainSwift()
keychain.set("hello world", forKey: "my key")
keychain.get("my key")
keychain.delete("my key")

【讨论】:

【参考方案3】:

我对如何添加、获取、删除密码的解释(对于那些懒惰使用此线程中提供的库的人):

// Saving password associated with the login and service
let userAccount = "user's login"
let service = "service name"
let passwordData: NSData = self.textfield_password.text!.dataUsingEncoding(NSUTF8StringEncoding, allowLossyConversion: false)!

let keychainQuery: [NSString: NSObject] = [
        kSecClass: kSecClassGenericPassword,
        kSecAttrAccount: userAccount,
        kSecAttrService: service,
        kSecValueData: passwordData]    

SecItemDelete(keychainQuery as CFDictionaryRef) //Deletes the item just in case it already exists
let keychain_save_status: OSStatus = SecItemAdd(keychainQuery as CFDictionaryRef, nil)
print("Keychain saving code is: \(keychain_save_status)")

...

// Getting the password associated with the login and service
let userAccount = "user's login"
let service = "service name"
let keychainQuery: [NSString: NSObject] = [
    kSecClass: kSecClassGenericPassword,
    kSecAttrService: service,
    kSecAttrAccount: userAccount,
    kSecReturnData: kCFBooleanTrue,
    kSecMatchLimit: kSecMatchLimitOne]

var rawResult: AnyObject?
let keychain_get_status: OSStatus = SecItemCopyMatching(keychainQuery, &rawResult)
print("Keychain getting code is: \(keychain_get_status)")

if (keychain_get_status == errSecSuccess) 
    let retrievedData = rawResult as? NSData
    let pass = NSString(data: retrievedData!, encoding: NSUTF8StringEncoding)
    print("Username: \(userAccount), password: \(pass!)")
    // Do your work with the retrieved password here
 else 
    print("No login data found in Keychain.")

...

//Deleting user's credentials from Keychain. Password is optional for the query when you delete, in most cases you won't know it after all.
let userAccount = "user's login"
let service = "service name"

let keychainQuery: [NSString: NSObject] = [
        kSecClass: kSecClassGenericPassword,
        kSecAttrAccount: userAccount,
        kSecAttrService: service]
let keychain_delete_status: OSStatus = SecItemDelete(keychainQuery as CFDictionaryRef)
print("Keychain deleting code is: \(keychain_delete_status)")

结果代码和其他有用信息可以在官方文档中找到:https://developer.apple.com/library/ios/documentation/Security/Reference/keychainservices/

【讨论】:

【参考方案4】:

对于 Swift 用户

在钥匙串中添加/检索/更新字段的单行代码:https://github.com/jrendel/SwiftKeychainWrapper

用法

将字符串值添加到钥匙串:

let saveSuccessful: Bool = KeychainWrapper.setString("Some String", forKey: "myKey")  

从钥匙串中检索字符串值:

let retrievedString: String? = KeychainWrapper.stringForKey("myKey")

从钥匙串中移除一个字符串值:

let removeSuccessful: Bool = KeychainWrapper.removeObjectForKey("myKey")

【讨论】:

【参考方案5】:

我想我已经找到了解决方案。我在上面编辑了我的帖子以包含有效的代码(至少对我而言)。我也在这里写过博客:using the iOS Keychain with Swift (example code)。

8 月 11 日更新:我已根据 rshelby 的 cmets 更新了博客文章中的代码。看看吧。

更新:我把它变成了keychain library written in Swift called Locksmith。


保存

class func save(service: NSString, data: NSString) 
  var dataFromString: NSData = data.dataUsingEncoding(NSUTF8StringEncoding, allowLossyConversion: false)
  // Instantiate a new default keychain query
  var keychainQuery: NSMutableDictionary = NSMutableDictionary(objects: [kSecClassGenericPassword, service, userAccount, dataFromString], forKeys: [kSecClass, kSecAttrService, kSecAttrAccount, kSecValueData])

  // Delete any existing items
  SecItemDelete(keychainQuery as CFDictionaryRef)

  // Add the new keychain item
  var status: OSStatus = SecItemAdd(keychainQuery as CFDictionaryRef, nil)

  // Check that it worked ok
  println("Saving status code is: \(status)")

加载

  class func load(service: NSString) -> AnyObject? 
    // Instantiate a new default keychain query
    // Tell the query to return a result
    // Limit our results to one item
    var keychainQuery: NSMutableDictionary = NSMutableDictionary(objects: [kSecClassGenericPassword, service, userAccount, kCFBooleanTrue, kSecMatchLimitOne], forKeys: [kSecClass, kSecAttrService, kSecAttrAccount, kSecReturnData, kSecMatchLimit])



    // I'm not too sure what's happening here...
    var dataTypeRef :Unmanaged<AnyObject>?

    // Search for the keychain items
    let status: OSStatus = SecItemCopyMatching(keychainQuery, &dataTypeRef)


    println("Loading status code is: \(status)")

    // I'm not too sure what's happening here...
    let opaque = dataTypeRef?.toOpaque()

    if let op = opaque? 
      let retrievedData = Unmanaged<NSData>.fromOpaque(op).takeUnretainedValue()
      println("Retrieved the following data from the keychain: \(retrievedData)")
      var str = NSString(data: retrievedData, encoding: NSUTF8StringEncoding)
      println("The decoded string is \(str)")
     else 
      println("Nothing was retrieved from the keychain.")
    

    return nil
  

用法(视图控制器)

KeychainService.saveToken("sometoken")
KeychainService.loadToken()

使用这些便捷方法

class func saveToken(token: NSString) 
    self.save("service", data: token)
  

class func loadToken() 
    var token = self.load("service")
    if let t = token 
      println("The token is: \(t)")
    
  

这导致控制台中的输出:

Saving status code is: 0
Loading status code is: 0
Retrieved the following data from the keychain: <736f6d65 746f6b65 6e>
The decoded string is sometoken

【讨论】:

嗨,使用您的代码时出现以下错误。 ***.com/questions/24453808/… 我在 2 个地方收到错误提示 Extra argument 'objects' in callsave 函数中的这一行 var keychainQuery: NSMutableDictionary = NSMutableDictionary(objects: [kSecClassGenericPassword, service, userAccount, dataFromString], forKeys: [kSecClass, kSecAttrService, kSecAttrAccount, kSecValueData]) 和加载函数中的这一行 var keychainQuery: NSMutableDictionary = NSMutableDictionary(objects: [kSecClassGenericPassword, service, userAccount, kCFBooleanTrue, kSecMatchLimitOne], forKeys: [kSecClass, kSecAttrService, kSecAttrAccount, kSecReturnData, kSecMatchLimit]) @Isuru 我在这段代码上遇到了很多麻烦,具体取决于我使用的 beta 版本,所以我不知道如何解决它抱歉。同时,我正在使用 Sam Soffes 的 SSKeychain 来访问钥匙串。

以上是关于使用 Swift 向 iOS 钥匙串添加项目和查询的主要内容,如果未能解决你的问题,请参考以下文章

无法添加到钥匙串 iOS

Xamarin.iOS VSTS 无法将临时钥匙串添加到钥匙串搜索路径

是否可以更新钥匙串项目的 kSecAttrAccessible 值?

“访问钥匙串时出错”

在钥匙串 Swift 中存储整数

将钥匙添加到钥匙链项目