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,004
  • 19
  • 69
  • 93
  • 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,073
  • 4
  • 33
  • 32
  • 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
  • 906
  • 6
  • 14
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,409
  • 7
  • 35
  • 57
steve0hh
  • 627
  • 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,162
  • 1
  • 11
  • 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,320
  • 2
  • 95
  • 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