4

I'm playing around with some Mac OS X Keychain / Security APIs using Swift. So far, I've been able to successfully fetch both a username and a password from an existing keychain item (by setting both kSecReturnAttributes and kSecReturnAttributes to true).

This code has been cobbled together from several sources including StackOverflow and the Apple Dev Forums:

//
//  main.swift
//  go-osxkeychain
//

import Foundation

// Create an HTTPS Keychain item w/ this server name before running this code
let serverName = "api.myserver.com"

public func getPassword(serverName: NSString) -> NSString? {
    // Instantiate a new default keychain query.
    // Tell the query to return the password data, as well as item attributes.
    // Limit our results to one item.
    var keychainQuery = NSMutableDictionary(
        objects: [
            kSecClassInternetPassword,
            serverName,
            kSecAttrProtocolHTTPS,
            true,
            true,
            kSecMatchLimitOne
        ], forKeys: [
            kSecClass,
            kSecAttrServer,
            kSecAttrProtocol,
            kSecReturnAttributes,
            kSecReturnData,
            kSecMatchLimit
        ])

    var dataTypeRef :Unmanaged<AnyObject>?

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

    if Int(errSecSuccess) != Int(status) {
        println("error finding keychain item")
        return "ERROR"
    }

    let opaque = dataTypeRef?.toOpaque()

    if let op = opaque? {
        let retrievedDictData = Unmanaged<NSDictionary>.fromOpaque(op).takeUnretainedValue()
        let foundDict = NSDictionary(dictionary: retrievedDictData)

        // let account = foundDict[kSecAccountItemAttr as NSNumber] as String // BROKEN
        let account = foundDict["acct"] as String // WORKS
        println("ACCOUNT: \(account)")

        let foundSecret = NSString(data: foundDict[kSecValueData as NSString] as NSData, encoding: NSUTF8StringEncoding) as String
        println("PASSWORD: \(foundSecret)")

        return foundSecret
    } else {
        println("Nothing was retrieved from the keychain. Status code \(status)")
    }

    return ""
}

let contents = getPassword(serverName) as String

As far as I know, the ugliness of this code is basically unavoidable due to the nature of the Keychain APIs. When I run this code locally, I see the following output:

0
ACCOUNT: bgentry@fakedomain.com
PASSWORD: this.is.a.fake.password
Program ended with exit code: 0

That being said, one thing in particular has stumped me. The NSDictionary of keychain item attributes I get back from a successful API call (named foundDict) contains keys that correspond to SecItemAttr / FourCharCode constants, shown here. For example, the constant kSecAccountItemAttr should allow me to retrieve the user account from the keychain item. Here's its Objective-C definition:

typedef FourCharCode SecItemAttr;
enum
{
    ...
    kSecAccountItemAttr             = 'acct',
    ...
}

In Swift, I've been unable to find any way to pass in these constants by reference, instead using strings to access attributes from the dictionary:

// Runtime error, "fatal error: unexpectedly found nil while unwrapping an Optional value"
let account = foundDict[kSecAccountItemAttr] as String

// Runtime error, "fatal error: unexpectedly found nil while unwrapping an Optional value"
let account = foundDict[kSecAccountItemAttr as NSNumber] as String

// Compiler error, "type 'FourCharCode' does not conform to protocol 'NSCopying'"
let account = foundDict[kSecAccountItemAttr as FourCharCode] as String

// Compiler error, "'Int' is not convertible to 'String'"
let account = foundDict[kSecAccountItemAttr as String] as String

// This works:
let account = foundDict["acct"] as String

Obviously I'd like to avoid splattering strings all over my code for my attribute keys, and I'd also like to avoid having to redeclare these constants in my Swift code.

How can I do that?

bgentry
  • 316
  • 1
  • 2
  • 12
  • Hi, I'm trying to add an Internet password to the mac keychain with swift. I originally was trying with SecKeychainAddInternetPassword but i couldn't get it to work with the protocol and auth type constants. I'm now trying to figure it out with SecItemAdd. Its not going well! I was wondering if you have tried it. Theres really limited resources out there when you search for keychain, mac and swift. – Lightbulb1 Dec 10 '14 at 11:07
  • @Lightbulb1 sorry to say I gave up on this for now... The OS X security APIs are just such a mess :( – bgentry Dec 11 '14 at 22:21
  • Yes I have found that out over the last week. In the end i did manage to get SecItemAdd working and now i'm trying to get SecItemCopyMatching working although its not so hard once you workout how to build the query object. Wish apple just have a simple swift class but its interesting learning this stuff. If you ever need any help, I can share what i've learnt. – Lightbulb1 Dec 12 '14 at 09:47

1 Answers1

2

To set or get the account name of the keychain item, you have to use kSecAttrAccount as the key (which is an NSString) and not kSecAccountItemAttr. Example:

if let account = foundDict[kSecAttrAccount as NSString] as? NSString {
    println("ACCOUNT: \(account)")
}
Martin R
  • 529,903
  • 94
  • 1,240
  • 1,382
  • Well that was remarkably simple :) Under what circumstances would one use the `kSecAccountItemAttr` style constants rather than the `kSecAttrAccount` ones? These docs can be quite a maze sometimes. Also, is there any reason to use `as? NSString` rather than `as? String`? – bgentry Jul 29 '14 at 18:18
  • In fact it seems like this code works just fine using `String` everywhere instead of `NSString`. Wouldn't that be preferable in Swift? – bgentry Jul 29 '14 at 18:29
  • @bgentry: 1) I actually do not know, but here http://stackoverflow.com/a/8471594/1187415 is an example where `kSecAccountItemAttr` is used. - 2) `kSecAttrAccount` *is* an NSString, and foundDict *is* an NSDictionary with NSStrings as keys, so `foundDict[kSecAttrAccount as NSString]` looks more natural to me, but `foundDict[kSecAttrAccount as String]` works as well. It would be interesting if there is a difference in the generated code. - 3) The dictionary value *is* an NSString, but converting it to a Swift String is fine. - So to make it short: I cannot really tell you what is better :) – Martin R Jul 29 '14 at 21:17