6

I want to store user credentials in the keychain. I found this: http://developer.apple.com/library/ios/#samplecode/GenericKeychain/Listings/Classes_KeychainItemWrapper_m.html#//apple_ref/doc/uid/DTS40007797-Classes_KeychainItemWrapper_m-DontLinkElementID_10

I added the KeychainItemWrapper.h/.m to my project. Unfortunately it doesn't compile. I'm targeting iOS 5, guessing that's the problem.

For example, this line:

    [genericPasswordQuery setObject:identifier forKey:(id)kSecAttrGeneric];

gives me this error:

cast of C pointer type 'CFTypeRef' (aka 'const void *') to Objective-C pointer type 'id' requires a bridged cast

I tried the "fix its", but it just introduced different errors.

Suggestions on how to proceed? I find it odd this wrapper isn't built into the SDK in the first place. Is there an new API/example for iOS 5? I couldn't find one. And is iOS 5 really still under NDA?

phil swenson
  • 8,564
  • 20
  • 74
  • 99
  • It would be helpful to everyone if you could change the accepted answer from one that is clearly incorrect. There is a gist of the [ARC Version of this code here](https://gist.github.com/1170641). – PassKit Sep 03 '13 at 01:48

5 Answers5

25

Turning ARC off is a short-sighted answer. I included an ARC-compatible version of the KeychainWrapper below.

I got it from this project.

Note: Experts on the matter (see comments below) think this is a better implementation: https://gist.github.com/1170641

Also, note that KeyChain credentials persist after your app is deleted. If you're using this for token authentication, you may consider NSUserDefaults instead. See this post for more info.

//File: KeychainWrapper.h
#import <UIKit/UIKit.h>

@interface KeychainWrapper : NSObject {}

+ (NSString *) getPasswordForUsername: (NSString *) username andServiceName: (NSString *) serviceName error: (NSError **) error;
+ (BOOL) storeUsername: (NSString *) username andPassword: (NSString *) password forServiceName: (NSString *) serviceName updateExisting: (BOOL) updateExisting error: (NSError **) error;
+ (BOOL) deleteItemForUsername: (NSString *) username andServiceName: (NSString *) serviceName error: (NSError **) error;

@end

and the implementation:

//File: KeychainWrapper.m
#import "KeychainWrapper.h"
#import <Security/Security.h>


static NSString *KeychainWrapperErrorDomain = @"KeychainWrapperErrorDomain";

#if __IPHONE_OS_VERSION_MIN_REQUIRED < 30000 && TARGET_IPHONE_SIMULATOR
@interface KeychainWrapper (PrivateMethods)
+ (SecKeychainItemRef) getKeychainItemReferenceForUsername: (NSString *) username andServiceName: (NSString *) serviceName error: (NSError **) error;
@end
#endif

@implementation KeychainWrapper

#if __IPHONE_OS_VERSION_MIN_REQUIRED < 30000 && TARGET_IPHONE_SIMULATOR

+ (NSString *) getPasswordForUsername: (NSString *) username andServiceName: (NSString *) serviceName error: (NSError **) error {
    if (!username || !serviceName) {
        *error = [NSError errorWithDomain: KeychainWrapperErrorDomain code: -2000 userInfo: nil];
        return nil;
    }

    SecKeychainItemRef item = [KeychainWrapper getKeychainItemReferenceForUsername: username andServiceName: serviceName error: error];

    if (*error || !item) {
        return nil;
    }

    // from Advanced Mac OS X Programming, ch. 16
    UInt32 length;
    char *password;
    SecKeychainAttribute attributes[8];
    SecKeychainAttributeList list;

    attributes[0].tag = kSecAccountItemAttr;
    attributes[1].tag = kSecDescriptionItemAttr;
    attributes[2].tag = kSecLabelItemAttr;
    attributes[3].tag = kSecModDateItemAttr;

    list.count = 4;
    list.attr = attributes;

    OSStatus status = SecKeychainItemCopyContent(item, NULL, &list, &length, (void **)&password);

    if (status != noErr) {
        *error = [NSError errorWithDomain: KeychainWrapperErrorDomain code: status userInfo: nil];
        return nil;
    }

    NSString *passwordString = nil;

    if (password != NULL) {
        char passwordBuffer[1024];

        if (length > 1023) {
            length = 1023;
        }
        strncpy(passwordBuffer, password, length);

        passwordBuffer[length] = '\0';
        passwordString = [NSString stringWithCString:passwordBuffer];
    }

    SecKeychainItemFreeContent(&list, password);

    CFRelease(item);

    return passwordString;
}

+ (void) storeUsername: (NSString *) username andPassword: (NSString *) password forServiceName: (NSString *) serviceName updateExisting: (BOOL) updateExisting error: (NSError **) error { 
    if (!username || !password || !serviceName) {
        *error = [NSError errorWithDomain: KeychainWrapperErrorDomain code: -2000 userInfo: nil];
        return;
    }

    OSStatus status = noErr;

    SecKeychainItemRef item = [KeychainWrapper getKeychainItemReferenceForUsername: username andServiceName: serviceName error: error];

    if (*error && [*error code] != noErr) {
        return;
    }

    *error = nil;

    // null means it's in the default keychain
    if (item) {
        status = SecKeychainItemModifyAttributesAndData(item,
                                                        NULL,
                                                        strlen([password UTF8String]),
                                                        [password UTF8String]);

        CFRelease(item);
    }
    else {
        status = SecKeychainAddGenericPassword(NULL,                                     
                                               strlen([serviceName UTF8String]), 
                                               [serviceName UTF8String],
                                               strlen([username UTF8String]),                        
                                               [username UTF8String],
                                               strlen([password UTF8String]),
                                               [password UTF8String],
                                               NULL);
    }

    if (status != noErr) {
        *error = [NSError errorWithDomain: KeychainWrapperErrorDomain code: status userInfo: nil];
    }
}

+ (void) deleteItemForUsername: (NSString *) username andServiceName: (NSString *) serviceName error: (NSError **) error {
    if (!username || !serviceName) {
        *error = [NSError errorWithDomain: KeychainWrapperErrorDomain code: 2000 userInfo: nil];
        return;
    }

    *error = nil;

    SecKeychainItemRef item = [KeychainWrapper getKeychainItemReferenceForUsername: username andServiceName: serviceName error: error];

    if (*error && [*error code] != noErr) {
        return;
    }

    OSStatus status;

    if (item) {
        status = SecKeychainItemDelete(item);

        CFRelease(item);
    }

    if (status != noErr) {
        *error = [NSError errorWithDomain: KeychainWrapperErrorDomain code: status userInfo: nil];
    }
}

+ (SecKeychainItemRef) getKeychainItemReferenceForUsername: (NSString *) username andServiceName: (NSString *) serviceName error: (NSError **) error {
    if (!username || !serviceName) {
        *error = [NSError errorWithDomain: KeychainWrapperErrorDomain code: -2000 userInfo: nil];
        return nil;
    }

    *error = nil;

    SecKeychainItemRef item;

    OSStatus status = SecKeychainFindGenericPassword(NULL,
                                                     strlen([serviceName UTF8String]),
                                                     [serviceName UTF8String],
                                                     strlen([username UTF8String]),
                                                     [username UTF8String],
                                                     NULL,
                                                     NULL,
                                                     &item);

    if (status != noErr) {
        if (status != errSecItemNotFound) {
            *error = [NSError errorWithDomain: KeychainWrapperErrorDomain code: status userInfo: nil];
        }

        return nil;     
    }

    return item;
}

#else

+ (NSString *) getPasswordForUsername: (NSString *) username andServiceName: (NSString *) serviceName error: (NSError **) error {
    if (!username || !serviceName) {
        if (error != nil) {
            *error = [NSError errorWithDomain: KeychainWrapperErrorDomain code: -2000 userInfo: nil];
        }
        return nil;
    }

    if (error != nil) {
        *error = nil;
    }

    // Set up a query dictionary with the base query attributes: item type (generic), username, and service

    NSArray *keys = [[NSArray alloc] initWithObjects: (__bridge_transfer NSString *) kSecClass, kSecAttrAccount, kSecAttrService, nil];
    NSArray *objects = [[NSArray alloc] initWithObjects: (__bridge_transfer NSString *) kSecClassGenericPassword, username, serviceName, nil];

    NSMutableDictionary *query = [[NSMutableDictionary alloc] initWithObjects: objects forKeys: keys];

    // First do a query for attributes, in case we already have a Keychain item with no password data set.
    // One likely way such an incorrect item could have come about is due to the previous (incorrect)
    // version of this code (which set the password as a generic attribute instead of password data).

    NSMutableDictionary *attributeQuery = [query mutableCopy];
    [attributeQuery setObject: (id) kCFBooleanTrue forKey:(__bridge_transfer id) kSecReturnAttributes];
    CFTypeRef attrResult = NULL;
    OSStatus status = SecItemCopyMatching((__bridge_retained CFDictionaryRef) attributeQuery, &attrResult);
    //NSDictionary *attributeResult = (__bridge_transfer NSDictionary *)attrResult;

    if (status != noErr) {
        // No existing item found--simply return nil for the password
        if (error != nil && status != errSecItemNotFound) {
            //Only return an error if a real exception happened--not simply for "not found."
            *error = [NSError errorWithDomain: KeychainWrapperErrorDomain code: status userInfo: nil];
        }

        return nil;
    }

    // We have an existing item, now query for the password data associated with it.

    NSMutableDictionary *passwordQuery = [query mutableCopy];
    [passwordQuery setObject: (id) kCFBooleanTrue forKey: (__bridge_transfer id) kSecReturnData];
    CFTypeRef resData = NULL;
    status = SecItemCopyMatching((__bridge_retained CFDictionaryRef) passwordQuery, (CFTypeRef *) &resData);
    NSData *resultData = (__bridge_transfer NSData *)resData;

    if (status != noErr) {
        if (status == errSecItemNotFound) {
            // We found attributes for the item previously, but no password now, so return a special error.
            // Users of this API will probably want to detect this error and prompt the user to
            // re-enter their credentials.  When you attempt to store the re-entered credentials
            // using storeUsername:andPassword:forServiceName:updateExisting:error
            // the old, incorrect entry will be deleted and a new one with a properly encrypted
            // password will be added.
            if (error != nil) {
                *error = [NSError errorWithDomain: KeychainWrapperErrorDomain code: -1999 userInfo: nil];
            }
        }
        else {
            // Something else went wrong. Simply return the normal Keychain API error code.
            if (error != nil) {
                *error = [NSError errorWithDomain: KeychainWrapperErrorDomain code: status userInfo: nil];
            }
        }

        return nil;
    }

    NSString *password = nil;   

    if (resultData) {
        password = [[NSString alloc] initWithData: resultData encoding: NSUTF8StringEncoding];
    }
    else {
        // There is an existing item, but we weren't able to get password data for it for some reason,
        // Possibly as a result of an item being incorrectly entered by the previous code.
        // Set the -1999 error so the code above us can prompt the user again.
        if (error != nil) {
            *error = [NSError errorWithDomain: KeychainWrapperErrorDomain code: -1999 userInfo: nil];
        }
    }

    return password;
}

+ (BOOL) storeUsername: (NSString *) username andPassword: (NSString *) password forServiceName: (NSString *) serviceName updateExisting: (BOOL) updateExisting error: (NSError **) error 
{       
    if (!username || !password || !serviceName) 
    {
        if (error != nil) 
        {
            *error = [NSError errorWithDomain: KeychainWrapperErrorDomain code: -2000 userInfo: nil];
        }
        return NO;
    }

    // See if we already have a password entered for these credentials.
    NSError *getError = nil;
    NSString *existingPassword = [KeychainWrapper getPasswordForUsername: username andServiceName: serviceName error:&getError];

    if ([getError code] == -1999) 
    {
        // There is an existing entry without a password properly stored (possibly as a result of the previous incorrect version of this code.
        // Delete the existing item before moving on entering a correct one.

        getError = nil;

        [self deleteItemForUsername: username andServiceName: serviceName error: &getError];

        if ([getError code] != noErr) 
        {
            if (error != nil) 
            {
                *error = getError;
            }
            return NO;
        }
    }
    else if ([getError code] != noErr) 
    {
        if (error != nil) 
        {
            *error = getError;
        }
        return NO;
    }

    if (error != nil) 
    {
        *error = nil;
    }

    OSStatus status = noErr;

    if (existingPassword) 
    {
        // We have an existing, properly entered item with a password.
        // Update the existing item.

        if (![existingPassword isEqualToString:password] && updateExisting) 
        {
            //Only update if we're allowed to update existing.  If not, simply do nothing.

            NSArray *keys = [[NSArray alloc] initWithObjects: (__bridge_transfer NSString *) kSecClass, 
                             kSecAttrService, 
                             kSecAttrLabel, 
                             kSecAttrAccount, 
                             nil];

            NSArray *objects = [[NSArray alloc] initWithObjects: (__bridge_transfer NSString *) kSecClassGenericPassword, 
                                serviceName,
                                serviceName,
                                username,
                                nil];

            NSDictionary *query = [[NSDictionary alloc] initWithObjects: objects forKeys: keys];            

            status = SecItemUpdate((__bridge_retained CFDictionaryRef) query, (__bridge_retained CFDictionaryRef) [NSDictionary dictionaryWithObject: [password dataUsingEncoding: NSUTF8StringEncoding] forKey: (__bridge_transfer NSString *) kSecValueData]);
        }
    }
    else 
    {
        // No existing entry (or an existing, improperly entered, and therefore now
        // deleted, entry).  Create a new entry.

        NSArray *keys = [[NSArray alloc] initWithObjects: (__bridge_transfer NSString *) kSecClass, 
                         kSecAttrService, 
                         kSecAttrLabel, 
                         kSecAttrAccount, 
                         kSecValueData, 
                         nil];

        NSArray *objects = [[NSArray alloc] initWithObjects: (__bridge_transfer NSString *) kSecClassGenericPassword, 
                            serviceName,
                            serviceName,
                            username,
                            [password dataUsingEncoding: NSUTF8StringEncoding],
                            nil];

        NSDictionary *query = [[NSDictionary alloc] initWithObjects: objects forKeys: keys];            

        status = SecItemAdd((__bridge_retained CFDictionaryRef) query, NULL);
    }

    if (error != nil && status != noErr) 
    {
        // Something went wrong with adding the new item. Return the Keychain error code.
        *error = [NSError errorWithDomain: KeychainWrapperErrorDomain code: status userInfo: nil];

        return NO;
    }

    return YES;
}

+ (BOOL) deleteItemForUsername: (NSString *) username andServiceName: (NSString *) serviceName error: (NSError **) error 
{
    if (!username || !serviceName) 
    {
        if (error != nil) 
        {
            *error = [NSError errorWithDomain: KeychainWrapperErrorDomain code: -2000 userInfo: nil];
        }
        return NO;
    }

    if (error != nil) 
    {
        *error = nil;
    }

    NSArray *keys = [[NSArray alloc] initWithObjects: (__bridge_transfer NSString *) kSecClass, kSecAttrAccount, kSecAttrService, kSecReturnAttributes, nil];
    NSArray *objects = [[NSArray alloc] initWithObjects: (__bridge_transfer NSString *) kSecClassGenericPassword, username, serviceName, kCFBooleanTrue, nil];

    NSDictionary *query = [[NSDictionary alloc] initWithObjects: objects forKeys: keys];

    OSStatus status = SecItemDelete((__bridge_retained CFDictionaryRef) query);

    if (error != nil && status != noErr) 
    {
        *error = [NSError errorWithDomain: KeychainWrapperErrorDomain code: status userInfo: nil];      

        return NO;
    }

    return YES;
}

#endif

@end
Community
  • 1
  • 1
Flaviu
  • 6,240
  • 4
  • 35
  • 33
  • 5
    Save credentials in UserDefault? What a bad advice, always use KeyChain ! – Thomas Decaux Apr 12 '12 at 12:03
  • 1
    I think storing a temporary token in UserDefaults is fine. Not the username and password of course. – Flaviu Apr 14 '12 at 01:28
  • Don't take this personally, but please don't use this code snippet. `__bridge_transfer` will decrement the retain count on CFRefs. You are using that on a system defined constant which could lead to trouble. Also I have a big problem with the statement that turning off ARC is short-sighted. One of the main reasons to selectively disable ARC is because you are doing excessive NS <-> CF conversions. When working with the keychain, if you use code that hasn't been vetted you not only right memory leaks, memory crashes but _also_ potential security issues. – amattn May 10 '12 at 19:55
  • @amattn I'm sorry. Your understanding of the iOS platform is clearly higher than mine. How would you go about making an ARC-compatible version of the KeychainWrapper? – Flaviu May 11 '12 at 01:22
  • @Flaviu After a short code review, the one that Ahmed Khalaf links to looks correctly implemented: https://gist.github.com/1170641 – amattn May 11 '12 at 10:57
  • @amattn I've added a note in the post above so people can decide for themselves. Thanks for letting me know. – Flaviu May 17 '12 at 01:23
  • Is there an advantage in doing it this way rather than the suggestion by @steve0hh below? – sherlock Apr 10 '13 at 04:15
12

Here's an ARCified version that had successfully compiled for me. Just remember to link the Security Framework with your target.

Hope this helps!

Community
  • 1
  • 1
pxlshpr
  • 986
  • 6
  • 15
4

Any references to CF classes must be paired with a "__bridge" statement to cast between Objective-C and Core Foundation Classes

Try this :

[genericPasswordQuery setObject:identifier forKey:(__bridge id) kSecAttrGeneric];
Asciiom
  • 9,867
  • 7
  • 38
  • 57
steve0hh
  • 637
  • 6
  • 15
0

Change your code to the one below:

[genericPasswordQuery setObject:identifier forKey:(__bridge id)kSecAttrGeneric];

Source: https://developer.apple.com/library/ios/documentation/Security/Conceptual/keychainServConcepts/iPhoneTasks/iPhoneTasks.html

haifacarina
  • 1,212
  • 1
  • 12
  • 18
-27

Your problem isn't iOS 5, its ARC. I'd recommend turning off ARC for those files, or your entire project even.

Joshua Weinberg
  • 28,598
  • 2
  • 97
  • 90
  • 2
    +3/-17 still selected as answer... Even I cant flag it, "Technically incorrect answer should not be flagged" – Anoop Vaidya Mar 20 '13 at 03:32
  • 1
    I haven't edited it just because I find it hilarious at this point. Check the date people, it wasn't worth converting to ARC at the time. – Joshua Weinberg Mar 20 '13 at 15:39
  • 1
    I am sure, even you wont be feeling good with +3/-18. either you could have updated the answer, or requested to delete. That is my personal view. – Anoop Vaidya Mar 20 '13 at 15:40