Keychain access in Xcode 9

I'm developing a framework and it stores some critical information in the keychain. With Xcode 8.3 (8E3004b), all my unit tests pass but with Xcode 9 (9A235), they don't, I get OSStatus(errSecParam).

Here's a function I wrote specifically to illustrate the error I get. It always returns true in Xcode 8.3 and always false in Xcode 9.

  func set() -> Bool {
    var attributes = [
      String(kSecClass): kSecClassGenericPassword,
      String(kSecAttrService): "service",
      String(kSecAttrAccount): "item.key",
    ] as [String : Any]
    attributes[String(kSecValueData)] = "pouet".data(using: .utf8, allowLossyConversion: false)
    SecItemDelete(attributes as CFDictionary)
    let status = SecItemAdd(attributes as CFDictionary, nil)

    return status == noErr
  }

I tried your code in a simple Single View App project with Xcode 9 GM seed, and I always get true.


Haven't you simplified your code too much? Or are there any specific condition to reproduce your issue?

While I can’t explain the Xcode 9 change, your code is problematic for two reasons:

  • In the keychain API there’s a distinction between query dictionaries and update dictionaries:

    • A query dictionary describes a search. It’s used for

      SecItemCopyMatching
      ,
      SecItemDelete
      and the first parameter of
      SecItemUpdate
      . It should only contain keys you want to search on.
    • An update dictionary describes a change. It’s used for the second parameter for

      SecItemUpdate
      and, if you squint the right way, as the only dictionary parameter for
      SecItemAdd
      . It contains all the keys you want to change.

    Your code is passing the same dictionary to both

    SecItemDelete
    and
    SecItemAdd
    , which doesn’t make any sense; at best
    SecItemDelete
    will only find and delete the item if the item’s value (
    kSecValueData
    ) is the one you specify.
  • Your overall approach (always delete and re-add) is not something we recommend. Rather, we recommend an algorithm like this:

    on store()
        find your item
        if found
            update that item
        else
            add a new item
        end if
    end store

    Pasted in below is an example of this approach written in modern Swift.

    Note This looks way more complex than it actually is because there’s a lot of doc comments and I split the code out into separate functions to make a pedagogic point. Without those the code size literally halves (from 120 to 60 lines).

I recommend that you watch WWDC 2013 Session 709 Protecting Secrets with the Keychain, which describes the correct conceptual basis for thinking about the keychain.

Finally, the other subtlety in the keychain API is item uniqueness. For each class of keychain item (

kSecClass
) there are a certain set of attributes that, combined, must be unique across the keychain. You can learn about these attributes in the reference docs for
errSecDuplicateItem
. In your case you’re already using the critical attributes appropriate for your class (for
kSecClassGenericPassword
they are
kSecAttrService
and
kSecAttrAccount
) but failing to understand this point is a common problem.

Share and Enjoy

Quinn “The Eskimo!”
Apple Developer Relations, Developer Technical Support, Core OS/Hardware

let myEmail = "eskimo" + "1" + "@apple.com"
import Foundation

/// Utility routines for working with the keychain.

enum KeychainUtilities {

    /// Returns the value of a generic password keychain item.
    ///
    /// - Parameters:
    ///   - service: The service name for the item.
    ///   - account: The account for the item.
    /// - Returns: The value of the item.
    /// - Throws: Any error returned by the Security framework.

    static func load(service: String, account: String) throws -> Data {
        var copyResult: CFTypeRef? = nil
        let err = SecItemCopyMatching([
            kSecClass: kSecClassGenericPassword,
            kSecAttrService: service,
            kSecAttrAccount: account,
            kSecReturnData: true
        ] as NSDictionary, &copyResult)
        switch err {
            case errSecSuccess:
                return copyResult as! Data
            default:
                try throwOSStatus(err)
                // `throwOSStatus(_:)` only returns in the `errSecSuccess` case.  We know we're
                // not in that case but the compiler can't figure that out so we have to add
                // a `fatalError()` otherwise we get a warning that we don't return a value
                // in all branches of the switch.
                fatalError()
        }
    }

    /// Stores a value to a generic password keychain item.
    ///
    /// This delegates the work to two helper routines depending on whether the item already
    /// exists in the keychain or not.
    ///
    /// - Parameters:
    ///   - service: The service name for the item.
    ///   - account: The account for the item.
    ///   - password: The desired password.
    /// - Throws: Any error returned by the Security framework.

    static func store(service: String, account: String, password: Data) throws {
        var copyResult: CFTypeRef? = nil
        let err = SecItemCopyMatching([
            kSecClass: kSecClassGenericPassword,
            kSecAttrService: service,
            kSecAttrAccount: account,
            kSecReturnData: true
        ] as NSDictionary, &copyResult)
        switch err {
            case errSecSuccess:
                let oldPassword = copyResult as! Data
                if oldPassword != password {
                    try self.storeByUpdating(service: service, account: account, password: password)
                }
            case errSecItemNotFound:
                try self.storeByAdding(service: service, account: account, password: password)
            default:
                try throwOSStatus(err)
        }
    }

    /// Stores a value to a generic password keychain item.
    ///
    /// This private routine is called to update an existing keychain item.
    ///
    /// - Parameters:
    ///   - service: The service name for the item.
    ///   - account: The account for the item.
    ///   - password: The desired password.
    /// - Throws: Any error returned by the Security framework.

    private static func storeByUpdating(service: String, account: String, password: Data) throws {
        let err = SecItemUpdate([
            kSecClass: kSecClassGenericPassword,
            kSecAttrService: service,
            kSecAttrAccount: account,
        ] as NSDictionary, [
            kSecValueData: password
        ] as NSDictionary)
        try throwOSStatus(err)
    }

    /// Stores a value to a generic password keychain item.
    ///
    /// This private routine is called to add the keychain item.
    ///
    /// - Parameters:
    ///   - service: The service name for the item.
    ///   - account: The account for the item.
    ///   - password: The desired password.
    /// - Throws: Any error returned by the Security framework.

    private static func storeByAdding(service: String, account: String, password: Data) throws {
        let err = SecItemAdd([
            kSecClass: kSecClassGenericPassword,
            kSecAttrService: service,
            kSecAttrAccount: account,
            kSecValueData: password,
        ] as NSDictionary, nil)
        try throwOSStatus(err)
    }

    /// Throws an error if a Security framework call has failed.
    ///
    /// - Parameter err: The error to check.

    private static func throwOSStatus(_ err: OSStatus) throws {
        guard err == errSecSuccess else {
            throw NSError(domain: NSOSStatusErrorDomain, code: Int(err), userInfo: nil)
        }
    }
}

You're right in a Simple View App this code works in Xcode 8.3 and Xcode 9. The problem is when I use it in a framework project.


Xcode 9:

https://www.dropbox.com/s/8ozomgu2vov7uhy/Xcode_9.jpg?dl=0


Xcode 8:

https://www.dropbox.com/s/c5791qwzd4rxbxe/Xcode_8.jpg?dl=0

Thank you for your answer, I understand and already tried what you advise, but with at the end the same result (SecItemAdd returns status == errSecParam). I don't understand why running the same code on Xcode 8.3 and Xcode 9 returns different results. Moreover the code posted earlier works OK in a simple view app.



    private func createKeychainQuery(withKey key: String) -> NSMutableDictionary {
        let result = NSMutableDictionary()
        result.setValue(kSecClassGenericPassword, forKey: kSecClass as String)
        result.setValue(key, forKey: kSecAttrService as String)
        result.setValue(kSecAttrAccessibleAlwaysThisDeviceOnly, forKey: kSecAttrAccessible as String)
        return result
    }


    private func save(string: String?, forKey key: String) {
         let query = createKeychainQuery(withKey: key) 
         let objectData: Data? = string?.data(using: .utf8, allowLossyConversion: false)


         if SecItemCopyMatching(query, nil) == noErr {
             if let dictData = objectData {
                 let status = SecItemUpdate(query, NSDictionary(dictionary: [kSecValueData: dictData]))
             } else {
                 let status = SecItemDelete(query)
             }
         } else {
             if let dictData = objectData {
                 query.setValue(dictData, forKey: kSecValueData as String)
                 let status = SecItemAdd(query, nil)
             }
         }
     }

Does this code only fail when you’re running it in the unit test? That is, if you call the same framework code from your app, does it work? If it fails in the unit test but works in the app, you’ll probably need to switch your unit test to run as an app test rather than a library test. You can read more about this in this post.

Share and Enjoy

Quinn “The Eskimo!”
Apple Developer Relations, Developer Technical Support, Core OS/Hardware

let myEmail = "eskimo" + "1" + "@apple.com"

Yes it's right, the unit tests fail only in the library context. When I test an app using this library, it works. I'll try what you suggest.

The solution is to add a host application to the framework test target.

Apparantly the keychain access requires to be in the context of an application in order to be able to work.


This can be done in the following way:

1. Add a new single view application target to your project

2. Go to the test target and select the general tab.

3. Under Host Application - select the application target that you've added.

4. Test should run fine now.

Hi,I have the same problem.But I was wondering, Why is Xcode 8.3 run fine without Host Application,Xcode 9 is failure?And I don't like run the unit test with the Host Application.Will Xcode fix or re-add this in later version?

Thanks!

But I was wondering, Why is Xcode 8.3 run fine without Host Application, Xcode 9 is failure?

Are you testing on the same version of iOS in both cases?

And I don't like run the unit test with the Host Application.Will Xcode fix or re-add this in later version?

I can’t comment on The Future™ but, as always, if something is giving you grief you should file a bug report explain what you’d like to see changed.

Please post your bug number, just for the record.

Share and Enjoy

Quinn “The Eskimo!”
Apple Developer Relations, Developer Technical Support, Core OS/Hardware

let myEmail = "eskimo" + "1" + "@apple.com"

Thank you for your answer!

Below is my code and my error condition.

Target : my project

File: Keychain.swift:

Code:


private let SecMatchLimit: String! = kSecMatchLimit as String

private let SecReturnData: String! = kSecReturnData as String

private let SecValueData: String! = kSecValueData as String

private let SecClass: String! = kSecClass as String

private let SecAttrGeneric: String! = kSecAttrGeneric as String

private let SecAttrAccount: String! = kSecAttrAccount as String


class Keychain {

@discardableResult

static func save(key: String, data: Data) -> Bool {

let query = [

SecClass: kSecClassGenericPassword as String,

SecAttrAccount: key,

SecValueData: data ] as [String : Any]

SecItemDelete(query as CFDictionary)

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

if status == errSecSuccess {

return true

} else if status == errSecDuplicateItem {

return update(data, forKey: key)

} else {

return false

}

}

}


Target : myprojectTests

File: KeychainTests.swift:

Code:

class KeychainTests: XCTestCase {

func testSaveData() {

let key = "foo"

guard let data = "data".data(using: .utf8) else {

fatalError()

}

let result = Keychain.save(key: key, data: data)

XCTAssertTrue(result, "key chain save data failure")

let saveData = Keychain.loadData(key: key)

XCTAssertNotNil(saveData, "saveData is nil")

guard let sd = saveData else {

fatalError("Keyhain load failure")

}

XCTAssertEqual(sd, data, "save data is not equal to load data")

guard let updateData = "data2".data(using: .utf8) else {

fatalError()

}

Keychain.save(key: key, data: updateData)

let updateDataFromKeychain = Keychain.loadData(key: key)

XCTAssertNotNil(updateDataFromKeychain, "updateDataFromKeychain is nil")

guard let uData = updateDataFromKeychain else { fatalError("Keychain load failure") }

XCTAssertEqual(uData, updateData, "update data is not equal to load update data")

}

}

```


The test function was passed in the Xcode8.3 and the unit test target's Host Application is none.I can use xctool run-tests it without simulator.

And Xcode9 run test with Host Application of none, Nothing else has been touched.The unit test is always failure, and the SecItemAdd function is always failure, the error OSStatus is -50.I have to change Host Application to my project , it can be run pass.It is my problem, I like to use xctool to test my project unit test without simulator, so I hope Xcode can be re-add the Keychain unit test when Host Application is none.This is just my personal opinion.


Thanks again!

Will file a bug here too. I really thought that after dealing with literally years of keychain errors (-34018) in simulator, that we would finally be done with this.


Uggh. 😢

Thanks for posting this...my day was about to fall off the cliff, now it is not 🙂

I was working around this previously in framework targets that used keychain APIs by adding an empty app target to run the tests. Since moving to Xcode 9.1 I now also have to move framework targets that depend on those frameworks to use an empty app.

So this has regressed further since Xcode 8.2. Are logical tests (without a host target) not officially supported for framework targets anymore? Should Xcode just add the host target automatically then?


I'll file the bug again, against 9.1 now, and one for the new scenario when I have time.

I like to use xctool to test my project unit test without simulator, so I hope Xcode can be re-add the Keychain unit test when Host Application is none.

My recommendations are:

Share and Enjoy

Quinn “The Eskimo!”
Apple Developer Relations, Developer Technical Support, Core OS/Hardware

let myEmail = "eskimo" + "1" + "@apple.com"
Keychain access in Xcode 9
 
 
Q