56

I'm very new to iOS development so forgive me if this is a newbie question. I have a simple authentication mechanism for my app that takes a user's email address and password. I also have a switch that says 'Remember me'. If the user toggles that switch on, I'd like to preserve their email/password so those fields can be auto-populated in the future.

I've gotten this to work with saving to a plist file but I know that's not the best idea since the password is unencrypted. I found some sample code for saving to the keychain, but to be honest, I'm a little lost. For the function below, I'm not sure how to call it and how to modify it to save the email address as well.

I'm guessing to call it would be: saveString(@"passwordgoeshere");

Thank you for any help!!!

+ (void)saveString:(NSString *)inputString forKey:(NSString *)account {

    NSAssert(account != nil, @"Invalid account");
    NSAssert(inputString != nil, @"Invalid string");

    NSMutableDictionary *query = [NSMutableDictionary dictionary];

    [query setObject:(id)kSecClassGenericPassword forKey:(id)kSecClass];
    [query setObject:account forKey:(id)kSecAttrAccount];
    [query setObject:(id)kSecAttrAccessibleWhenUnlocked forKey:(id)kSecAttrAccessible];

    OSStatus error = SecItemCopyMatching((CFDictionaryRef)query, NULL);
    if (error == errSecSuccess) {
        // do update
        NSDictionary *attributesToUpdate = [NSDictionary dictionaryWithObject:[inputString dataUsingEncoding:NSUTF8StringEncoding] 
                                                                      forKey:(id)kSecValueData];

        error = SecItemUpdate((CFDictionaryRef)query, (CFDictionaryRef)attributesToUpdate);
        NSAssert1(error == errSecSuccess, @"SecItemUpdate failed: %d", error);
    } else if (error == errSecItemNotFound) {
        // do add
        [query setObject:[inputString dataUsingEncoding:NSUTF8StringEncoding] forKey:(id)kSecValueData];

        error = SecItemAdd((CFDictionaryRef)query, NULL);
        NSAssert1(error == errSecSuccess, @"SecItemAdd failed: %d", error);
    } else {
        NSAssert1(NO, @"SecItemCopyMatching failed: %d", error);
    }
}
Nate
  • 31,017
  • 13
  • 83
  • 207
Jason
  • 577
  • 2
  • 6
  • 8
  • 5
    I fixed @Anomie's code to work with ARC and put it on Github (I linked to this answer and mentioned your user in both files, but if you want further attribution please let me know). I also changed the formatting a bit and made the method names a little more generic. https://github.com/jeremangnr/JNKeychain – jere May 13 '13 at 15:37

3 Answers3

101

I've written a simple wrapper that allows saving of any NSCoding-compliant object to the keychain. You could, for example, store your email and password in an NSDictionary and store the NSDictionary to the keychain using this class.

SimpleKeychain.h

#import <Foundation/Foundation.h>

@class SimpleKeychainUserPass;

@interface SimpleKeychain : NSObject

+ (void)save:(NSString *)service data:(id)data;
+ (id)load:(NSString *)service;
+ (void)delete:(NSString *)service;

@end

SimpleKeychain.m

#import "SimpleKeychain.h"

@implementation SimpleKeychain

+ (NSMutableDictionary *)getKeychainQuery:(NSString *)service {
    return [NSMutableDictionary dictionaryWithObjectsAndKeys:
            (id)kSecClassGenericPassword, (id)kSecClass,
            service, (id)kSecAttrService,
            service, (id)kSecAttrAccount,
            (id)kSecAttrAccessibleAfterFirstUnlock, (id)kSecAttrAccessible,
            nil];
}

+ (void)save:(NSString *)service data:(id)data {
    NSMutableDictionary *keychainQuery = [self getKeychainQuery:service];
    SecItemDelete((CFDictionaryRef)keychainQuery);
    [keychainQuery setObject:[NSKeyedArchiver archivedDataWithRootObject:data] forKey:(id)kSecValueData];
    SecItemAdd((CFDictionaryRef)keychainQuery, NULL);
}

+ (id)load:(NSString *)service {
    id ret = nil;
    NSMutableDictionary *keychainQuery = [self getKeychainQuery:service];
    [keychainQuery setObject:(id)kCFBooleanTrue forKey:(id)kSecReturnData];
    [keychainQuery setObject:(id)kSecMatchLimitOne forKey:(id)kSecMatchLimit];
    CFDataRef keyData = NULL;
    if (SecItemCopyMatching((CFDictionaryRef)keychainQuery, (CFTypeRef *)&keyData) == noErr) {
        @try {
            ret = [NSKeyedUnarchiver unarchiveObjectWithData:(NSData *)keyData];
        }
        @catch (NSException *e) {
            NSLog(@"Unarchive of %@ failed: %@", service, e);
        }
        @finally {}
    }
    if (keyData) CFRelease(keyData);
    return ret;
}

+ (void)delete:(NSString *)service {
    NSMutableDictionary *keychainQuery = [self getKeychainQuery:service];
    SecItemDelete((CFDictionaryRef)keychainQuery);
}

@end
Anomie
  • 92,546
  • 13
  • 126
  • 145
  • @abe: Try it and see? According to the documentation all the same functions are available, but it's possible `getKeychainQuery:` needs to have extra parameters in the dictionary. – Anomie Jul 07 '11 at 01:52
  • @Anomie i gave it a try and found it errors on values, one of which is kSecClassGenericPassword but i checked the documentation it appears to be support in foundation framework i also included security framework any thoughts on y it might error? thx – abe Jul 07 '11 at 06:10
  • May want to use kSecAttrAccessibleWhenUnlockedThisDeviceOnly (as opposed to kSecAttrAccessibleAfterFirstUnlock) in an example that may be copied by lots of folks. – Daniel May 25 '12 at 20:39
  • @Daniel: It all depends on whether you need to use the encrypted data when your app is running in the background, and whether you want to keep the encrypted data if the person upgrades their phone. – Anomie May 26 '12 at 13:36
  • 5
    This answer needs updating for ARC – Aran Mulholland Jan 10 '13 at 04:38
  • I submitted an edit for ARC. It's tested and it works, but I don't know if it's correct. – Minthos Apr 24 '13 at 11:55
  • Also note that [the keychain is effectively cleartext](https://github.com/ptoomey3/Keychain-Dumper) on jailbroken devices, beware. –  May 13 '13 at 15:42
  • @Aran, That code looks ARC-ready now. It's using CFRelease, which is still required even in ARC (Memory management of CF objects is still up to you.) – Duncan C Sep 12 '13 at 18:30
  • @Minthos I just saw this thread. I believe you have to give your own answer with your own improvement as a new answer to this question. Even if its just an improvement to the existing answer. That could have been the reason why they rejected the edit. – Houman Nov 03 '13 at 11:50
  • 1
    I hate to be the one to ask. But how would I implement the ability to store the username,password after using Simplekeychain? I can't use [Simplekeychain setObject:Password.text forKey:Password.text]; So whats an alternative? – f00d Jan 17 '14 at 17:21
  • 1
    I've created a modified version of this with support for setting the account: https://gist.github.com/btjones/10287581 – Brandon Apr 09 '14 at 16:12
  • 1
    `SecItemDelete` should never be used in a password store function. Either you want to add a new password entry (then there is nothing to delete) or you want to update an existing one, then you shall __update__ it and not delete it and then add a new one (as the system does not recognize that as an update, it will treat the item as new - all access list modifications and user customizations of that item will be lost - also it will move back from whatever keychain the system hat put it back to the default keychain) – Mecki Oct 29 '14 at 13:59
39

ARC ready code:

KeychainUserPass.h

#import <Foundation/Foundation.h>

@interface KeychainUserPass : NSObject

+ (void)save:(NSString *)service data:(id)data;
+ (id)load:(NSString *)service;
+ (void)delete:(NSString *)service;

@end

KeychainUserPass.m

#import "KeychainUserPass.h"

@implementation KeychainUserPass

+ (NSMutableDictionary *)getKeychainQuery:(NSString *)service {
    return [NSMutableDictionary dictionaryWithObjectsAndKeys:
            (__bridge id)kSecClassGenericPassword, (__bridge id)kSecClass,
            service, (__bridge id)kSecAttrService,
            service, (__bridge id)kSecAttrAccount,
            (__bridge id)kSecAttrAccessibleAfterFirstUnlock, (__bridge id)kSecAttrAccessible,
            nil];
}

+ (void)save:(NSString *)service data:(id)data {
    NSMutableDictionary *keychainQuery = [self getKeychainQuery:service];
    SecItemDelete((__bridge CFDictionaryRef)keychainQuery);
    [keychainQuery setObject:[NSKeyedArchiver archivedDataWithRootObject:data] forKey:(__bridge id)kSecValueData];
    SecItemAdd((__bridge CFDictionaryRef)keychainQuery, NULL);
}

+ (id)load:(NSString *)service {
    id ret = nil;
    NSMutableDictionary *keychainQuery = [self getKeychainQuery:service];
    [keychainQuery setObject:(id)kCFBooleanTrue forKey:(__bridge id)kSecReturnData];
    [keychainQuery setObject:(__bridge id)kSecMatchLimitOne forKey:(__bridge id)kSecMatchLimit];
    CFDataRef keyData = NULL;
    if (SecItemCopyMatching((__bridge CFDictionaryRef)keychainQuery, (CFTypeRef *)&keyData) == noErr) {
        @try {
            ret = [NSKeyedUnarchiver unarchiveObjectWithData:(__bridge NSData *)keyData];
        }
        @catch (NSException *e) {
            NSLog(@"Unarchive of %@ failed: %@", service, e);
        }
        @finally {}
    }
    if (keyData) CFRelease(keyData);
    return ret;
}

+ (void)delete:(NSString *)service {
    NSMutableDictionary *keychainQuery = [self getKeychainQuery:service];
    SecItemDelete((__bridge CFDictionaryRef)keychainQuery);
}

@end
Allan Spreys
  • 5,287
  • 5
  • 39
  • 44
0

I don’t think your suggested method is a good way to do a 'Remember me' function. I believe a better way is to simply not log that user out from its account on the server. Store a cookie on the client side with a hashed value in it, and send that with every server call. You should always do that with every server call anyway, as opposed to sending locally stored passwords. Don’t even store them in local variables.

If the user wants to store the password on its Keychain, that’s a totally different and independent user task than ‘Remember me’. I’m afraid you have confused these two use cases.

Rul
  • 1