5

We've created an app in swift that uses keychain. The app works fine when run on a device or in the simulator but can't access the keychain when provisioned via Testflight unless provisioned to a new device that's never had the app previously installed via Xcode 6.1.

Following is an excerpt of the keychain code:

    import UIKit
    import Security

    let serviceIdentifier = "com.ourdomain"

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

class KeychainManager {

    class func setString(value: NSString, forKey: String) {
        self.save(serviceIdentifier, key: forKey, data: value)
    }

    class func stringForKey(key: String) -> NSString? {
        var token = self.load(serviceIdentifier, key: key)

        return token
    }

    class func removeItemForKey(key: String) {
        self.save(serviceIdentifier, key: key, data: "")
    }



    class func save(service: NSString, key: String, data: NSString) {
        var dataFromString: NSData = data.dataUsingEncoding(NSUTF8StringEncoding, allowLossyConversion: false)!
        // Instantiate a new default keychain query
        var keychainQuery: NSMutableDictionary = NSMutableDictionary(objects: [kSecClassGenericPasswordValue, service, key, dataFromString], forKeys: [kSecClassValue, kSecAttrServiceValue, kSecAttrAccountValue, kSecValueDataValue])

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

        if data == "" { return }

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

    class func load(service: NSString, key: String) -> NSString? {
        // Instantiate a new default keychain query
        // Tell the query to return a result
        // Limit our results to one item
        var keychainQuery: NSMutableDictionary = NSMutableDictionary(objects: [kSecClassGenericPasswordValue, service, key, kCFBooleanTrue, kSecMatchLimitOneValue], forKeys: [kSecClassValue, kSecAttrServiceValue, kSecAttrAccountValue, kSecReturnDataValue, kSecMatchLimitValue])

        var dataTypeRef :Unmanaged<AnyObject>?

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

        let opaque = dataTypeRef?.toOpaque()

        var contentsOfKeychain: NSString?

        if let op = opaque? {
            let retrievedData = Unmanaged<NSData>.fromOpaque(op).takeUnretainedValue()

            // Convert the data retrieved from the keychain into a string
            contentsOfKeychain = NSString(data: retrievedData, encoding: NSUTF8StringEncoding)
        } else {
            return nil
        }

        return contentsOfKeychain
    }  
  }

After the app was already installed on the device via Xcode 6.1 I noticed that the "serviceIdentifier" - "com.ourdomain" was incorrect and didn't match the app's bundle identifier as required with provisioning.

I then changed the "serviceIdentifier" value to match the bundle identifier - "com.ourdomain.appname" however the app just won't work on the device when provisioned via Testflight. I'm positive this is because the device already has the keychain for the bundle id installed with the incorrect identifier but I can't fathom how to get around this to either remove the keychain when the app is removed or get the provisioning profile to use the existing keychain (with the incorrect identifier)

Any help would be greatly appreciated. Thanks in advance

Mark
  • 6,051
  • 2
  • 26
  • 25

3 Answers3

5

Use withUnsafeMutablePointer function and UnsafeMutablePointer struct to retrieving the data instead, like below:

var result: AnyObject?
var status = withUnsafeMutablePointer(&result) { SecItemCopyMatching(keychainQuery, UnsafeMutablePointer($0)) }

if status == errSecSuccess {
    if let data = result as NSData? {
        if let string = NSString(data: data, encoding: NSUTF8StringEncoding) {
            // ...
        }
    }
}

it works fine with release (Fastest optimization) build.

kishikawa katsumi
  • 10,418
  • 1
  • 41
  • 53
  • 2
    This is an actual solution to the problem! The accepted answer of disabling compiler optimisation in unacceptable in many cases. For what it's worth, I only see the original bug on 64bit devices. – alexkent Jan 07 '15 at 16:01
  • 1
    This worked for me after many hours of banging my head against a wall. Thanks. – Nick Jan 18 '15 at 07:09
  • Doesn't work, iPhone 6, 6+ and iPad Air 2 still exhibiting the problem when using `withUnsafeMutablePointer`. `-Onone` is still the only viable option for all devices until Swift 1.2 is stable –  Mar 03 '15 at 11:16
  • This works only change I had to make was `if status == noErr {` – Nick Wargnier Mar 15 '15 at 01:46
2

Ok so everything in the provisioning profile and keychain code was fine. The problem was a setting in the Swift compiler! Changing the Optimization Level for "Release" from "Fastest" to "None" seemed to resolve the issue.

enter image description here

Mark
  • 6,051
  • 2
  • 26
  • 25
  • 2
    I have the exact same issue. It looks like dataTypeRef?.toOpaque() is returning nil when you set the Optimization Level to -O. However, I don't think that setting the Optimization level to -Onone is not a solution because it will make you swift code much slower. I uwe Xcode 6.1 (6A1052d) – JorgeDeCorte Nov 04 '14 at 10:47
  • I have run into the same problem and reported this bug to Apple (bug 19003552 if that's helpful). Anyone running into this should do the same as duplicates should help in prioritizing issues. – stefanlindbohm Nov 17 '14 at 20:00
  • @JorgeDeCorte - Thanks. Maybe we need to try and explicitly unwrap the dataTypeRef... have you tried that? - I'm going to see if maybe that resolves the issue – Mark Nov 19 '14 at 10:51
  • I am having the very same issue which renders KeyChain access with Swift useless IMHO. My workaround is to do this in Objective-C as this is a Swift specific issue. I use the Lockbox pod, works well https://github.com/granoff/Lockbox – Click Ahead Nov 28 '14 at 16:47
  • This does indeed cause the functionality to work, but is not a viable solution. The "Use withUnsafeMutablePointer function and UnsafeMutablePointer" answer does indeed work and is IMO the correct answer. – Nick Jan 18 '15 at 07:08
  • I've accepted the answer "Use withUnsafeMutablePointer function and UnsafeMutablePointer"... my answer was the only solution we could find at the time and yes, i agree, its not an ideal or viable solution!! thanks @kishikawa katsumi – Mark Jan 22 '15 at 18:01
0

The change of the swift optimizer is working, but not a good way to solve the problem. As stated in the comments, this seems to be a bug in the swift compiler with the optimization.

This is not iOS only, the exact same thing happens on OSX (maybe add a tag to the question). The OSStatus is 0 (success) but the referencing pointer is nil as soon as the optimization is done.

The best workaround is to implement to keychain communication in objective-c or using a wrapper like EMKeychain (OSX only, sorry don't know an iOS Wrapper at the moment)

Axel Zehden
  • 530
  • 3
  • 17