Adding item to keychain using Swift
Asked Answered
F

7

13

I'm trying to add an item to the iOS keychain using Swift but can't figure out how to type cast properly. From WWDC 2013 session 709, given the following Objective-C code:

NSData *secret = [@"top secret" dataWithEncoding:NSUTF8StringEncoding];
NSDictionary *query = @{
    (id)kSecClass: (id)kSecClassGenericPassword,
    (id)kSecAttrService: @"myservice",
    (id)kSecAttrAccount: @"account name here",
    (id)kSecValueData: secret,
};

OSStatus = SecItemAdd((CFDictionaryRef)query, NULL);

Attempting to do it in Swift as follows:

var secret: NSData = "Top Secret".dataUsingEncoding(NSUTF8StringEncoding, allowLossyConversion: false)
var query: NSDictionary = [
    kSecClass: kSecClassGenericPassword,
    kSecAttrService: "MyService",
    kSecAttrAccount: "Some account",
    kSecValueData: secret
]

yields the error "Cannot convert the expression's type 'Dictionary' to 'DictionaryLiteralConvertible'.

Another approach I took was to use Swift and the - setObject:forKey: method on a Dictionary to add kSecClassGenericPassword with the key kSecClass.

In Objective-C:

NSMutableDictionary *searchDictionary = [NSMutableDictionary dictionary];
[searchDictionary setObject:(id)kSecClassGenericPassword forKey:(id)kSecClass];

In the Objective-C code, the CFTypeRef of the various keychain item class keys are bridged over using id. In the Swift documentation it's mentioned that Swift imports id as AnyObject. However when I attempted to downcast kSecClass as AnyObject for the method, I get the error that "Type 'AnyObject' does not conform to NSCopying.

Any help, whether it's a direct answer or some guidance about how to interact with Core Foundation types would be appreciated.

EDIT 2

This solution is no longer valid as of Xcode 6 Beta 2. If you are using Beta 1 the code below may work.

var secret: NSData = "Top Secret".dataUsingEncoding(NSUTF8StringEncoding, allowLossyConversion: false)
let query = NSDictionary(objects: [kSecClassGenericPassword, "MyService", "Some account", secret], forKeys: [kSecClass,kSecAttrService, kSecAttrAccount, kSecValueData])

OSStatus status = SecItemAdd(query as CFDictionaryRef, NULL)

To use Keychain Item Attribute keys as dictionary keys you have to unwrap them by using either takeRetainedValue or takeUnretainedValue (as appropriate). Then you can cast them to NSCopying. This is because they are CFTypeRefs in the header, which aren't all copyable.

As of Xcode 6 Beta 2 however, this causes Xcode to crash.

Fazeli answered 9/6, 2014 at 3:29 Comment(5)
Hi. Could you possibly let me know how the below is the accepted answer? I have the same code as you shown above, but no matter what I do, I cannot read or add an item to the KeyChain. Can you update the code with the solution as to how you made it work specifically to save the item in the Keychain? Thanks in advance.Carolynncarolynne
@Darren. I edited my question to include the answer. I accepted the answer below before the second edit was made so I haven't tried to make it work by down casting the literal.Fazeli
@PasanPremaratne Hi, I am trying to do the same thing but it does not work in Beta 2, have you found a solution yet? thanksAraxes
@Xerxes Not yet but I filed rdar 17395972Fazeli
Normally I wouldn't just drop a link like this, but because there's been so many changes through the betas and to 8.1, I made a library called Locksmith for querying the Keychain in Swift.Farmelo
G
7

In the xcode 6.0.1 you must do this!!

let kSecClassValue = NSString(format: kSecClass)
let kSecAttrAccountValue = NSString(format: kSecAttrAccount)
let kSecValueDataValue = NSString(format: kSecValueData)
let kSecClassGenericPasswordValue = NSString(format: kSecClassGenericPassword)
let kSecAttrServiceValue = NSString(format: kSecAttrService)
let kSecMatchLimitValue = NSString(format: kSecMatchLimit)
let kSecReturnDataValue = NSString(format: kSecReturnData)
let kSecMatchLimitOneValue = NSString(format: kSecMatchLimitOne)
Gabbard answered 10/10, 2014 at 22:43 Comment(4)
Can you please explain your answer ?Salomie
Call let kSecClassValue = kSecClass.takeRetainedValue () as NSString will generate an error because the class kSecClass no method takeRetainedValue () and therefore constant declaration kSekslassValue should look like this as I have described aboveGabbard
In answer will be betterSalomie
You shouldn't use NSString(format: ...) for this. It is risky providing your variable as a format. String(...) will work fine.Hunchback
T
13

You simply need to downcast the literal:

let dict = ["hi": "Pasan"] as NSDictionary

Now dict is an NSDictionary. To make a mutable one, it's very similar to Objective-C:

let mDict = dict.mutableCopy() as NSMutableDictionary
mDict["hola"] = "Ben"
Transcontinental answered 9/6, 2014 at 3:52 Comment(3)
Why I have to downcast the literal can you please explain. Because I think we don't have to cast a variable when we declare and assign a variable at same place.Comanche
This still isn't working for me. I get the same (or a variation of the error message) when using a form of the swift code in the question example.Carolynncarolynne
Hi guys, I am trying to do the same thing but it does not work in Beta 2, have you found a solution yet? thanksAraxes
G
7

In the xcode 6.0.1 you must do this!!

let kSecClassValue = NSString(format: kSecClass)
let kSecAttrAccountValue = NSString(format: kSecAttrAccount)
let kSecValueDataValue = NSString(format: kSecValueData)
let kSecClassGenericPasswordValue = NSString(format: kSecClassGenericPassword)
let kSecAttrServiceValue = NSString(format: kSecAttrService)
let kSecMatchLimitValue = NSString(format: kSecMatchLimit)
let kSecReturnDataValue = NSString(format: kSecReturnData)
let kSecMatchLimitOneValue = NSString(format: kSecMatchLimitOne)
Gabbard answered 10/10, 2014 at 22:43 Comment(4)
Can you please explain your answer ?Salomie
Call let kSecClassValue = kSecClass.takeRetainedValue () as NSString will generate an error because the class kSecClass no method takeRetainedValue () and therefore constant declaration kSekslassValue should look like this as I have described aboveGabbard
In answer will be betterSalomie
You shouldn't use NSString(format: ...) for this. It is risky providing your variable as a format. String(...) will work fine.Hunchback
P
6

Perhaps things have improved since. On Xcode 7 beta 4, no casting seems to be necessary except when dealing with the result AnyObject?. Specifically, the following seems to work:

var query : [NSString : AnyObject] = [
    kSecClass : kSecClassGenericPassword,
    kSecAttrService : "MyAwesomeService",
    kSecReturnAttributes : true,   // return dictionary in result parameter
    kSecReturnData : true          // include the password value
]
var result : AnyObject?
let err = SecItemCopyMatching(query, &result)
if (err == errSecSuccess) {
    // on success cast the result to a dictionary and extract the
    // username and password from the dictionary.
    if let result = result as ? [NSString : AnyObject],
       let username = result[kSecAttrAccount] as? String,
       let passdata = result[kSecValueData] as? NSData,
       let password = NSString(data:passdata, encoding:NSUTF8StringEncoding) as? String {
        return (username, password)
    }
} else if (status == errSecItemNotFound) {
    return nil;
} else {
    // probably a program error,
    // print and lookup err code (e.g., -50 = bad parameter)
}

To add a key if it was missing:

var query : [NSString : AnyObject] = [
    kSecClass : kSecClassGenericPassword,
    kSecAttrService : "MyAwesomeService",
    kSecAttrLabel : "MyAwesomeService Password",
    kSecAttrAccount : username,
    kSecValueData : password.dataUsingEncoding(NSUTF8StringEncoding)!
]
let result = SecItemAdd(query, nil)
// check that result is errSecSuccess, etc...

A few things to point out: your initial problem might have been that str.dataUsingEncoding returns an Optional. Adding '!' or better yet, using an if let to handle nil return, would likely make your code work. Printing out the error code and looking it up in the docs will help a lot in isolating the problem (I was getting err -50 = bad parameter, until I noticed a problem with my kSecClass, nothing to do with data types or casts!).

Permanent answered 27/7, 2015 at 5:27 Comment(1)
This helped me out and was I was looking for. Not a fan of unnecessarily using dependencies. Thanks!Whippersnapper
S
4

This seemed to work fine or at least compiler didn't have kittens - UNTESTED beyond that

        var secret: NSData = "Top Secret".dataUsingEncoding(NSUTF8StringEncoding, allowLossyConversion: false)
        var array1 = NSArray(objects:"\(kSecClassGenericPassword)", "MyService", "Some account", secret)
        var array2 = NSArray(objects:"\(kSecClass)","\(kSecAttrService)", "\(kSecAttrAccount)","\(kSecValueData)")
        let query = NSDictionary(objects: array1, forKeys: array2)
        println(query)
        let status  = SecItemAdd(query as CFDictionaryRef, nil)

Seems to work fine in Beta 2

Smoothtongued answered 8/7, 2014 at 10:6 Comment(0)
C
1

In order to get this to work, you need to access the retained values of the keychain constants instead. For example:

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 MSMutableDictionary 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

Custer answered 10/8, 2014 at 14:33 Comment(0)
D
1

Swift 3

let kSecClassKey = String(kSecClass)
let kSecAttrAccountKey = String(kSecAttrAccount)
let kSecValueDataKey = String(kSecValueData)
let kSecClassGenericPasswordKey = String(kSecClassGenericPassword)
let kSecAttrServiceKey = String(kSecAttrService)
let kSecMatchLimitKey = String(kSecMatchLimit)
let kSecReturnDataKey = String(kSecReturnData)
let kSecMatchLimitOneKey = String(kSecMatchLimitOne)

you can also do it inside the dictionary itself alá:

var query: [String: Any] = [
    String(kSecClass): kSecClassGenericPassword,
    String(kSecAttrService): "MyService",
    String(kSecAttrAccount): "Some account",
    String(kSecValueData): secret
]

however, this is more expensive for the compiler, even more so since you're probably using the query in multiple places.

Diminution answered 26/12, 2016 at 16:52 Comment(0)
A
0

more convenient to use the cocoa pods SSKeychain

+ (NSArray *)allAccounts;
+ (NSArray *)accountsForService:(NSString *)serviceName;
+ (NSString *)passwordForService:(NSString *)serviceName account:(NSString *)account;
+ (BOOL)deletePasswordForService:(NSString *)serviceName account:(NSString *)account;
+ (BOOL)setPassword:(NSString *)password forService:(NSString *)serviceName account:(NSString *)account;
Alcohol answered 8/1, 2015 at 4:28 Comment(2)
I'm aware of SSKeychain (in fact one of the answers provided here is by the author himself) but I was looking for a pure Swift implementation rather than having to use bridging headersFazeli
@pasan was asking for a pure Swift implementation. Its also quite obvious he wanted to learn how to do it himself.Whippersnapper

© 2022 - 2024 — McMap. All rights reserved.