Adding Items to and Querying the iOS Keychain with Swift
Asked Answered
L

5

21

I'm having trouble converting all of the Objective C code samples that are available for adding data and querying data from the iOS Keychain into Swift. I'm trying to do a basic storage of a string (an access token) and reading it back. I've had a look at some of the other questions on Stack Overflow, but I can't quite get it to work. I've tried to piece together a solution from the various sources.

Edit 1: I tried with a more basic setup, because I thought my self.defaultKeychainQuery might have been messing things up. I've updated the code below to the latest version.

Edit 2: Got it working. I wasn't adding the data value to the save query properly. I needed to convert the string to NSData. I've updated the code below to the most recent working version.

Edit 3: As Xerxes points out below, this code doesn't work with Xcode versions higher than Beta 1 because of some issue with Dictionaries. If you know of a fix for this, please let me know.

Update: I turned this into a keychain library written in Swift called Locksmith.


Save

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)")
}

Load

  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
  }

Usage (view controller)

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

which uses these convenience methods

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)")
    }
  }

This leads to the output in the console:

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

Thanks a lot for your help. I'm not too sure what to do with dataTypeRef once I've got it, or if it has any data given the code above.

Leid answered 20/6, 2014 at 9:39 Comment(9)
I'm still waiting to get this work and none the wiser. In fact, with the latest version of Xcode, I'm getting an error message with the use of 'NSDictionary'. It really shouldn't be this frustrating! Good luck ... I'll be watching closely.Becket
@Becket I think I've gotten it working. Check out the edited version above. I wasn't converting the NSString input into the proper NSData, and I hadn't added the actual data value to the keychainQuery. After changing this, I then had to change the load method to decode the NSData response. Let me know if that helps you :)Leid
Which version of Xcode are you using? I've tried your code in Beta-2, and I keep getting the following error message Could not find an overload for 'init' that accepts the supplied arguments from the definition of keychainQuery. This has only started happening to me since I changed to beta-2. Thanks for any reply.Becket
I think I might be on the first beta (Version 6.0 (6A215l)). Have you tried setting the key/value pairs using any of the other available methods? Might just be NSMutableDictionary(objects:keys:) specifically that's buggy.Leid
matt - Are you planning on making a Cocoapod out of this new Library? I'd love to try it out, I really don't want to import it with a submodule :)Vitek
@JamesArmstead Yeah, I think I will once Cocoapods hits 1.0 and adds Swift dependency support. Feel free to open an issue on the Github repo if you want it sooner though :)Leid
Re: "// I'm not too sure what's happening here..." I just brought this code up with a Swift guru at WWDC. What's happening is that you're taking the Unmanaged pointer and extracting a COpaquePointer? which is essentially an optional void*. This lets you actually compare the pointer value, to make sure it's not NULL. If Sec failed to return anything, dataTypeRef wouldn't actually be nil, but the wrapped pointer would be null.Acidophil
@Leid Can you confirm that using your library to store usernames and passwords will pass the Apple Store encryption standards?Were
@Leid do you think you could help me with this question?: #38212306Hounding
H
7

In order to get this to work, you will need to retrieve the retained values of the keychain constants and store then first like so:

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

You can then reference the values in the NSMutableDictionary like so:

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

I wrote a blog post about it at: http://rshelby.com/2014/08/using-swift-to-save-and-query-ios-keychain-in-xcode-beta-4/

Hope this helps!

rshelby

Heise answered 10/8, 2014 at 14:29 Comment(8)
Thanks very much. The class compiles without ant errors. But I'm not sure how to use it. Can you include an example please? And maybe a little explanation on what the methods in the class (saveToken, load etc) are and what they're supposed to do?Raceme
Thanks for your help! I've updated my original blog post with these changes and a couple of other minor things. @Raceme my post should have a simple example usage. Let me know if it works for you :)Leid
@matt Hi, I was following that example. In the viewDidLoad method, I save a value like this - KeychainService.saveToken("Some value") and in another button press, I retrieve it like this - label.text = KeychainService.loadToken(). But I get the following error - Nothing was retrieved from the keychain. Status code -25300Raceme
@Raceme in private class save(...), right at the end of the function, add println("Saved to the keychain. Status code \(status)"). What's the output now? The status code -25300 indicates that the item couldn't be found in the keychain (reference—scroll to end)Leid
@matt I think I must have missed something. I copied the updated code from your blogpost and tried the same code and it worked. I have one last question. How do you save multiple values? Because there is no such thing like key value pairs in this, right? How do I save different values and retrieve them separately?Raceme
@Raceme I think you'd need to use a different value for the key kSecAttrAccountValue wherever needed. This is just a string, so you'd modify load and save to accept the arguments, use the arguments, etc.Leid
Use .takeUnretainedValue() because these are system constants and you should not be adjusting their retain counts.Hydrophilous
takeUnretainedValue() is not supported in xcode 6.1.1Amrita
U
5

I wrote a demo app and helper functions for this simple task: writing/reading a text string for a given key in 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")
Unity answered 5/2, 2015 at 22:27 Comment(0)
J
3

For Swift users

Single line code to add/retrieve/update fields in Keychain:
https://github.com/jrendel/SwiftKeychainWrapper

Usage

Add a string value to keychain:

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

Retrieve a string value from keychain:

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

Remove a string value from keychain:

let removeSuccessful: Bool = KeychainWrapper.removeObjectForKey("myKey")
Jennings answered 15/2, 2016 at 3:42 Comment(0)
M
3

My interpretation on how to add, get, delete passwords (for those who are lazy to use libraries presented in this thread):

// 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)")

The result codes and other useful info can be found in the official documentation: https://developer.apple.com/library/ios/documentation/Security/Reference/keychainservices/

Millham answered 23/4, 2016 at 19:11 Comment(0)
L
1

I think I've worked out the solution. I've edited my post above to include the code that works (at least for me). I've also blogged about it here: using the iOS Keychain with Swift (example code).

Update 11 Aug: I've updated the code in the blog post based on rshelby's comments. Take a look.

Update: I turned this into a keychain library written in Swift called Locksmith.


Save

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)")
}

Load

  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
  }

Usage (view controller)

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

which uses these convenience methods

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)")
    }
  }

This leads to the output in the console:

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
Leid answered 20/6, 2014 at 22:59 Comment(3)
hi , I am getting the following error when using your code. #24454308Hutton
I get an error saying Extra argument 'objects' in call in 2 places. This line var keychainQuery: NSMutableDictionary = NSMutableDictionary(objects: [kSecClassGenericPassword, service, userAccount, dataFromString], forKeys: [kSecClass, kSecAttrService, kSecAttrAccount, kSecValueData]) in the save function and this line, var keychainQuery: NSMutableDictionary = NSMutableDictionary(objects: [kSecClassGenericPassword, service, userAccount, kCFBooleanTrue, kSecMatchLimitOne], forKeys: [kSecClass, kSecAttrService, kSecAttrAccount, kSecReturnData, kSecMatchLimit]) in the load function.Raceme
@Raceme I've been having a lot of trouble with this code depending on which version of the beta I'm on, so I'm not sure how to fix it sorry. In the meantime, I'm using Sam Soffes' SSKeychain to access the keychain.Leid

© 2022 - 2024 — McMap. All rights reserved.