Skip to content

Instantly share code, notes, and snippets.

@levigroker
Forked from dhoerl/KeychainItemWrapper.h
Created September 7, 2012 22:54
Show Gist options
  • Save levigroker/3670472 to your computer and use it in GitHub Desktop.
Save levigroker/3670472 to your computer and use it in GitHub Desktop.
KeychainItem
/*
File: KeychainItem.h
Abstract:
Objective-C wrapper for accessing a single keychain item.
KeychainItem by Levi Brown is licensed under a Creative Commons Attribution 3.0 Unported License.
To view a copy of this license, visit http://creativecommons.org/licenses/by/3.0/
Based on a work at https://developer.apple.com/library/ios/#samplecode/GenericKeychain
Some ARC conversion modifications were made based on: https://gist.github.com/1170641
Under the terms of this license, the above attribution and license statements are to remain
intact in any derrivitive works or distributions.
*/
#import <Foundation/Foundation.h>
#import <Security/Security.h>
//Error domain
extern NSString* const kKeychainItemErrorDomain;
//The possible error codes
typedef enum {
KeychainItemErrorSuccess = errSecSuccess, //0 No error.
KeychainItemErrorUnimplemented = errSecUnimplemented, //-4 Function or operation not implemented.
KeychainItemErrorParam = errSecParam, //-50 One or more parameters passed to the function were not valid.
KeychainItemErrorAllocate = errSecAllocate, //-108 Failed to allocate memory.
KeychainItemErrorNotAvailable = errSecNotAvailable, //–25291 No trust results are available.
KeychainItemErrorAuthFailed = errSecAuthFailed, //–25293 Authorization/Authentication failed.
KeychainItemErrorDuplicateItem = errSecDuplicateItem, //25299 The item already exists.
KeychainItemErrorItemNotFound = errSecItemNotFound, //–25300 The item cannot be found.
KeychainItemErrorInteractionNotAllowed = errSecInteractionNotAllowed, //–25308 Interaction with the Security Server is not allowed.
KeychainItemErrorDecode = errSecDecode //-26275 Unable to decode the provided data.
} KeychainItemErrorCode;
//The KeychainItem class is a representation of a single item in the keychain and can be used to set or fetch
//an associated value (and metadata) for the keychain item.
@interface KeychainItem : NSObject
//Convenience creators
+ (KeychainItem *)keychainItemWithIdentifier:(NSString *)identifier;
+ (KeychainItem *)keychainItemWithIdentifier:(NSString *)identifier accessGroup:(NSString *)accessGroup;
// Designated initializer.
- (id)initWithIdentifier:(NSString *)identifier accessGroup:(NSString *)accessGroup;
//Gets the kSecValueData value and returns it as a string
- (NSString *)secureValue;
//Sets the kSecValueData from the given string, potentially returning an error should one occur while interfacing with the keychain.
- (NSError *)setSecureValue:(NSString *)secureValue;
//Lower level API
- (id)objectForSecKey:(CFTypeRef)key;
- (NSError *)setObject:(id)inObject forSecKey:(CFTypeRef)key;
// Initializes and resets the default generic keychain item data.
//NOTE: If ignoreErrors is YES then if an error occurs while trying to delete the data from the keychain,
//initialization will occur anyway, potentially leaving this object's local data out of sync with the keychain.
- (NSError *)resetIgnoringErrors:(BOOL)ignoreErrors;
@end
/*
File: KeychainItem.m
Abstract:
Objective-C wrapper for accessing a single keychain item.
KeychainItem by Levi Brown is licensed under a Creative Commons Attribution 3.0 Unported License.
To view a copy of this license, visit http://creativecommons.org/licenses/by/3.0/
Based on a work at https://developer.apple.com/library/ios/#samplecode/GenericKeychain
Some ARC conversion modifications were made based on: https://gist.github.com/1170641
Under the terms of this license, the above attribution and license statements are to remain
intact in any derrivitive works or distributions.
*/
#import "KeychainItem.h"
NSString* const kKeychainItemErrorDomain = @"KeychainItem";
/*
These are the default constants and their respective types,
available for the kSecClassGenericPassword Keychain Item class:
kSecAttrAccessGroup - CFStringRef
kSecAttrCreationDate - CFDateRef
kSecAttrModificationDate - CFDateRef
kSecAttrDescription - CFStringRef
kSecAttrComment - CFStringRef
kSecAttrCreator - CFNumberRef
kSecAttrType - CFNumberRef
kSecAttrLabel - CFStringRef
kSecAttrIsInvisible - CFBooleanRef
kSecAttrIsNegative - CFBooleanRef
kSecAttrAccount - CFStringRef
kSecAttrService - CFStringRef
kSecAttrGeneric - CFDataRef
See the header file Security/SecItem.h for more details.
*/
@interface KeychainItem ()
@property (nonatomic, retain) NSMutableDictionary *keychainItemData; // The actual keychain item data backing store.
@property (nonatomic, retain) NSMutableDictionary *genericPasswordQuery; // A placeholder for the generic keychain item query used to locate the item.
@end
@implementation KeychainItem
#pragma mark - Initialization
+ (KeychainItem *)keychainItemWithIdentifier:(NSString *)identifier
{
return [KeychainItem keychainItemWithIdentifier:identifier accessGroup:nil];
}
+ (KeychainItem *)keychainItemWithIdentifier:(NSString *)identifier accessGroup:(NSString *)accessGroup
{
KeychainItem *item = [[KeychainItem alloc] initWithIdentifier:identifier accessGroup:accessGroup];
return item;
}
- (id)initWithIdentifier:(NSString *)identifier accessGroup:(NSString *)accessGroup;
{
if (self = [super init])
{
// Begin Keychain search setup. The genericPasswordQuery leverages the special user
// defined attribute kSecAttrGeneric to distinguish itself between other generic Keychain
// items which may be included by the same application.
self.genericPasswordQuery = [[NSMutableDictionary alloc] init];
[self.genericPasswordQuery setObject:(__bridge id)(kSecClassGenericPassword) forKey:(__bridge id)kSecClass];
[self.genericPasswordQuery setObject:identifier forKey:(__bridge id)kSecAttrGeneric];
// The keychain access group attribute determines if this item can be shared
// amongst multiple apps whose code signing entitlements contain the same keychain access group.
if (accessGroup != nil)
{
#if TARGET_IPHONE_SIMULATOR
// Ignore the access group if running on the iPhone simulator.
//
// Apps that are built for the simulator aren't signed, so there's no keychain access group
// for the simulator to check. This means that all apps can see all keychain items when run
// on the simulator.
//
// If a SecItem contains an access group attribute, SecItemAdd and SecItemUpdate on the
// simulator will return -25243 (errSecNoAccessForItem).
#else
[self.genericPasswordQuery setObject:accessGroup forKey:(__bridge id)kSecAttrAccessGroup];
#endif
}
// Use the proper search constants, return only the attributes of the first match.
[self.genericPasswordQuery setObject:(__bridge id)kSecMatchLimitOne forKey:(__bridge id)kSecMatchLimit];
[self.genericPasswordQuery setObject:(__bridge id)kCFBooleanTrue forKey:(__bridge id)kSecReturnAttributes];
NSDictionary *tempQuery = [NSDictionary dictionaryWithDictionary:self.genericPasswordQuery];
CFMutableDictionaryRef outDictionary = NULL;
if (SecItemCopyMatching((__bridge CFDictionaryRef)tempQuery, (CFTypeRef *)&outDictionary) == errSecSuccess)
{
// load the saved data from Keychain.
self.keychainItemData = [self secItemFormatToDictionary:(__bridge NSDictionary *)outDictionary error:nil];
}
else
{
// Stick these default values into keychain item if nothing found.
[self resetIgnoringErrors:YES];
// Add the generic attribute and the keychain access group.
[self.keychainItemData setObject:identifier forKey:(__bridge id)kSecAttrGeneric];
if (accessGroup != nil)
{
#if TARGET_IPHONE_SIMULATOR
// Ignore the access group if running on the iPhone simulator.
//
// Apps that are built for the simulator aren't signed, so there's no keychain access group
// for the simulator to check. This means that all apps can see all keychain items when run
// on the simulator.
//
// If a SecItem contains an access group attribute, SecItemAdd and SecItemUpdate on the
// simulator will return -25243 (errSecNoAccessForItem).
#else
[self.keychainItemData setObject:accessGroup forKey:(__bridge id)kSecAttrAccessGroup];
#endif
}
}
if (outDictionary)
{
CFRelease(outDictionary);
}
}
return self;
}
#pragma mark - API
//Gets the kSecValueData value and returns it as a string
- (NSString *)secureValue
{
NSString *retVal = nil;
id obj = [self objectForSecKey:kSecValueData];
retVal = [obj description];
return retVal;
}
//Sets the kSecValueData from the given string, potentially returning an error should one occur while interfacing with the keychain.
- (NSError *)setSecureValue:(NSString *)secureValue
{
NSError *error = [self setObject:secureValue forSecKey:kSecValueData];
return error;
}
- (NSError *)setObject:(id)inObject forSecKey:(CFTypeRef)key;
{
NSError *error = nil;
if (inObject)
{
id currentObject = [self.keychainItemData objectForKey:(__bridge id)(key)];
if (![currentObject isEqual:inObject])
{
[self.keychainItemData setObject:inObject forKey:(__bridge id)(key)];
error = [self writeToKeychain];
}
}
return error;
}
- (id)objectForSecKey:(CFTypeRef)key;
{
return [self.keychainItemData objectForKey:(__bridge id)(key)];
}
//NOTE: If ignoreErrors is YES then if an error occurs while trying to delete the data from the keychain, initialization will occur anyway, potentially leaving this object's local data out of sync with the keychain.
- (NSError *)resetIgnoringErrors:(BOOL)ignoreErrors
{
NSError *error = nil;
if (self.keychainItemData)
{
NSMutableDictionary *tempDictionary = [self dictionaryToSecItemFormat:self.keychainItemData];
OSStatus status = SecItemDelete((__bridge CFDictionaryRef)tempDictionary);
if (status == errSecSuccess || status == errSecItemNotFound)
{
NSMutableDictionary *userInfo = [NSMutableDictionary dictionaryWithCapacity:1];
[userInfo setObject:NSLocalizedString(@"Unable to delete item from keychain.", nil) forKey:NSLocalizedDescriptionKey];
error = [NSError errorWithDomain:kKeychainItemErrorDomain code:status userInfo:userInfo];
}
}
if (ignoreErrors || !error)
{
self.keychainItemData = [NSMutableDictionary dictionary];
// Default attributes for keychain item.
[self.keychainItemData setObject:@"" forKey:(__bridge id)kSecAttrAccount];
[self.keychainItemData setObject:@"" forKey:(__bridge id)kSecAttrLabel];
[self.keychainItemData setObject:@"" forKey:(__bridge id)kSecAttrDescription];
// Default data for keychain item.
[self.keychainItemData setObject:@"" forKey:(__bridge id)kSecValueData];
}
return error;
}
#pragma mark - Implementation
- (NSMutableDictionary *)dictionaryToSecItemFormat:(NSDictionary *)dictionaryToConvert
{
// The assumption is that this method will be called with a properly populated dictionary
// containing all the right key/value pairs for a SecItem.
// Create a dictionary to return populated with the attributes and data.
NSMutableDictionary *returnDictionary = [NSMutableDictionary dictionaryWithDictionary:dictionaryToConvert];
// Add the Generic Password keychain item class attribute.
[returnDictionary setObject:(__bridge id)kSecClassGenericPassword forKey:(__bridge id)kSecClass];
// Convert the NSString to NSData to meet the requirements for the value type kSecValueData.
// This is where to store sensitive data that should be encrypted.
NSString *passwordString = [dictionaryToConvert objectForKey:(__bridge id)kSecValueData];
[returnDictionary setObject:[passwordString dataUsingEncoding:NSUTF8StringEncoding] forKey:(__bridge id)kSecValueData];
return returnDictionary;
}
- (NSMutableDictionary *)secItemFormatToDictionary:(NSDictionary *)dictionaryToConvert error:(NSError **)error
{
// The assumption is that this method will be called with a properly populated dictionary
// containing all the right key/value pairs for the UI element.
// Create a dictionary to return populated with the attributes and data.
NSMutableDictionary *returnDictionary = [NSMutableDictionary dictionaryWithDictionary:dictionaryToConvert];
// Add the proper search key and class attribute.
[returnDictionary setObject:(__bridge id)kCFBooleanTrue forKey:(__bridge id)kSecReturnData];
[returnDictionary setObject:(__bridge id)kSecClassGenericPassword forKey:(__bridge id)kSecClass];
// Acquire the password data from the attributes.
CFDataRef passwordData = NULL;
OSStatus status = SecItemCopyMatching((__bridge CFDictionaryRef)returnDictionary, (CFTypeRef *)&passwordData);
if (status == errSecSuccess)
{
// Remove the search, class, and identifier key/value, we don't need them anymore.
[returnDictionary removeObjectForKey:(__bridge id)kSecReturnData];
// Add the password to the dictionary, converting from NSData to NSString.
NSString *password = [[NSString alloc] initWithBytes:[(__bridge NSData *)passwordData bytes] length:[(__bridge NSData *)passwordData length]
encoding:NSUTF8StringEncoding];
[returnDictionary setObject:password forKey:(__bridge id)kSecValueData];
}
else
{
returnDictionary = nil;
if (error)
{
NSMutableDictionary *userInfo = [NSMutableDictionary dictionaryWithCapacity:1];
[userInfo setObject:NSLocalizedString(@"No matching item found in the keychain.", nil) forKey:NSLocalizedDescriptionKey];
*error = [NSError errorWithDomain:kKeychainItemErrorDomain code:status userInfo:userInfo];
}
}
if (passwordData)
{
CFRelease(passwordData);
}
return returnDictionary;
}
- (NSError *)writeToKeychain
{
NSError *error = nil;
CFDictionaryRef attributes = NULL;
NSMutableDictionary *updateItem = nil;
OSStatus status = SecItemCopyMatching((__bridge CFDictionaryRef)self.genericPasswordQuery, (CFTypeRef *)&attributes);
if (status == errSecSuccess)
{
// First we need the attributes from the Keychain.
updateItem = [NSMutableDictionary dictionaryWithDictionary:(__bridge NSDictionary *)attributes];
// Second we need to add the appropriate search key/values.
[updateItem setObject:[self.genericPasswordQuery objectForKey:(__bridge id)kSecClass] forKey:(__bridge id)kSecClass];
// Lastly, we need to set up the updated attribute list being careful to remove the class.
NSMutableDictionary *tempCheck = [self dictionaryToSecItemFormat:self.keychainItemData];
[tempCheck removeObjectForKey:(__bridge id)kSecClass];
#if TARGET_IPHONE_SIMULATOR
// Remove the access group if running on the iPhone simulator.
//
// Apps that are built for the simulator aren't signed, so there's no keychain access group
// for the simulator to check. This means that all apps can see all keychain items when run
// on the simulator.
//
// If a SecItem contains an access group attribute, SecItemAdd and SecItemUpdate on the
// simulator will return -25243 (errSecNoAccessForItem).
//
// The access group attribute will be included in items returned by SecItemCopyMatching,
// which is why we need to remove it before updating the item.
[tempCheck removeObjectForKey:(__bridge id)kSecAttrAccessGroup];
#endif
// An implicit assumption is that you can only update a single item at a time.
status = SecItemUpdate((__bridge CFDictionaryRef)updateItem, (__bridge CFDictionaryRef)tempCheck);
if (status != errSecSuccess)
{
NSMutableDictionary *userInfo = [NSMutableDictionary dictionaryWithCapacity:1];
[userInfo setObject:NSLocalizedString(@"Could not update the keychain item.", nil) forKey:NSLocalizedDescriptionKey];
error = [NSError errorWithDomain:kKeychainItemErrorDomain code:status userInfo:userInfo];
}
}
else if (status == errSecItemNotFound)
{
// No previous item found; add the new one.
status = SecItemAdd((__bridge CFDictionaryRef)[self dictionaryToSecItemFormat:self.keychainItemData], NULL);
NSMutableDictionary *userInfo = [NSMutableDictionary dictionaryWithCapacity:1];
[userInfo setObject:NSLocalizedString(@"Could not add the keychain item.", nil) forKey:NSLocalizedDescriptionKey];
error = [NSError errorWithDomain:kKeychainItemErrorDomain code:status userInfo:userInfo];
}
else
{
NSMutableDictionary *userInfo = [NSMutableDictionary dictionaryWithCapacity:1];
[userInfo setObject:NSLocalizedString(@"Unable to write to the keychain.", nil) forKey:NSLocalizedDescriptionKey];
error = [NSError errorWithDomain:kKeychainItemErrorDomain code:status userInfo:userInfo];
}
if (attributes)
{
CFRelease(attributes);
}
return error;
}
@end
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment