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: [email protected]
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?
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)")
}