Saving Email/Password to Keychain in iOS
Asked Answered
ios
C

3

55

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);
    }
}
Cholon answered 9/3, 2011 at 15:20 Comment(1)
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. github.com/jeremangnr/JNKeychainMis
S
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
Shanika answered 9/3, 2011 at 20:34 Comment(12)
@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.Shanika
@Shanika 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? thxExpecting
May want to use kSecAttrAccessibleWhenUnlockedThisDeviceOnly (as opposed to kSecAttrAccessibleAfterFirstUnlock) in an example that may be copied by lots of folks.Mulligatawny
@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.Shanika
This answer needs updating for ARCIm
I submitted an edit for ARC. It's tested and it works, but I don't know if it's correct.Hyponitrite
Also note that the keychain is effectively cleartext on jailbroken devices, beware.Hibernia
@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.)Kerrikerrie
@Hyponitrite 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.Fiorenze
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?Termitarium
I've created a modified version of this with support for setting the account: gist.github.com/btjones/10287581Radiative
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)Douglass
H
38

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
Hanforrd answered 24/11, 2013 at 8:14 Comment(0)
D
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.

Delvecchio answered 25/4, 2021 at 13:3 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.