MyKeychain.m   [plain text]


//
//  MyKeychain.m
//  KCSync
//
//  Created by John Hurley on 10/3/12.
//  Copyright (c) 2012 john. All rights reserved.
//

#import "MyKeychain.h"
#import <Security/Security.h>
#import <Security/SecItemPriv.h>
#import <utilities/debugging.h>

const NSString *kItemPasswordKey = @"ItemPasswordKey";
const NSString *kItemAccountKey = @"ItemAccountKey";
const NSString *kItemNameKey = @"ItemNameKey";

#define KCSCOPE "mykeychain"

// secdebug(KCSCOPE,

@implementation MyKeychain

static MyKeychain *sharedInstance = nil;

+ (MyKeychain *) sharedInstance
{
    if (!sharedInstance)
		sharedInstance = [[self alloc] init];

    return sharedInstance;
}

// Translate status messages into return strings
- (NSString *) fetchStatus : (OSStatus) status
{
    switch (status)
    {
    case 0:
        return(@"Success!");
    case errSecNotAvailable:
        return(@"No trust results are available.!");
    case errSecItemNotFound:
        return(@"The item cannot be found.");
    case errSecParam:
        return(@"Parameter error.");
    case errSecAllocate:
        return(@"Memory allocation error. Failed to allocate memory.");
    case errSecInteractionNotAllowed:
        return(@"User interaction is not allowed.");
    case errSecUnimplemented:
        return(@"Function is not implemented");
    case errSecDuplicateItem:
        return(@"The item already exists.");
    case errSecDecode:
        return(@"Unable to decode the provided data.");

    default:
		return([NSString stringWithFormat:@"Function returned: %ld", (long)status]);
        break;
    }
    return @"can't happen...";
}

#define	ACCOUNT	@"Keychain Sync Test Account"
#define	SERVICE	@"Keychain Sync Test Service"
#define PWKEY	@"Keychain Sync Test Password Data"

// Return a base dictionary
- (NSMutableDictionary *) baseDictionary
{
	NSMutableDictionary *md = [[NSMutableDictionary alloc] init];
	
	// Password identification keys
	NSData *identifier = [PWKEY dataUsingEncoding:NSUTF8StringEncoding];
	[md setObject:identifier forKey:(__bridge id)kSecAttrGeneric];
	[md setObject:ACCOUNT forKey:(__bridge id)kSecAttrAccount];
    [md setObject:SERVICE forKey:(__bridge id)kSecAttrService];
	[md setObject:(__bridge id)kSecClassGenericPassword forKey:(__bridge id)kSecClass];
    [md setObject:(__bridge id)kCFBooleanTrue forKey:(__bridge id)kSecAttrSynchronizable];
    
	return md;
}

// Return a keychain-style dictionary populated with the password
- (NSMutableDictionary *) buildDictForPassword:(NSString *) password
{
	NSMutableDictionary *passwordDict = [self baseDictionary];
	
	// Add the password
	NSData *passwordData = [password dataUsingEncoding:NSUTF8StringEncoding];
    [passwordDict setObject:passwordData forKey:(__bridge id)kSecValueData]; // password 
	
	return passwordDict;
}

// Build a search query based
- (NSMutableDictionary *) buildSearchQuery
{
	NSMutableDictionary *genericPasswordQuery = [self baseDictionary];
	
	// Add the search constraints
	[genericPasswordQuery setObject:(__bridge id)kSecMatchLimitOne forKey:(__bridge id)kSecMatchLimit];
	[genericPasswordQuery setObject:(__bridge id)kCFBooleanTrue
							 forKey:(__bridge id)kSecReturnAttributes];
	[genericPasswordQuery setObject:(__bridge id)kCFBooleanTrue
							 forKey:(__bridge id)kSecReturnData];
	
	return genericPasswordQuery;
}

// retrieve data dictionary from the keychain
- (NSMutableArray *) fetchDictionaryWithQuery:(NSMutableDictionary *)query
{
	NSMutableDictionary *genericPasswordQuery = query;
	
//  secerror("Query: %@", query);
    CFTypeRef cfresult = nil;
	OSStatus status = SecItemCopyMatching((__bridge CFDictionaryRef)genericPasswordQuery, &cfresult);

//  secerror( "FETCH: %s\n", [[self fetchStatus:status] UTF8String]);

	if (status == errSecItemNotFound)
        return NULL;
    
    if (CFGetTypeID(cfresult) == CFArrayGetTypeID())
    {
        NSMutableArray *result = [NSMutableArray arrayWithCapacity:0];
        [result addObjectsFromArray:CFBridgingRelease(cfresult)];
        return result;
    }
    
    // If it is a single result, embed it in an array because callers expect it
    if (CFGetTypeID(cfresult) == CFDictionaryGetTypeID())
        return [NSMutableArray arrayWithObject:CFBridgingRelease(cfresult)];

	return NULL;
}	

- (NSMutableArray *)fetchDictionary
{
	return [self fetchDictionaryWithQuery:[self buildSearchQuery]];
}	

// create a new keychain entry
- (BOOL) createKeychainValue:(NSString *) password
{
	NSMutableDictionary *md = [self buildDictForPassword:password];
	OSStatus status = SecItemAdd((__bridge CFDictionaryRef)md, NULL);

    secerror( "CREATE: %s\n", [[self fetchStatus:status] UTF8String]);
	
	if (status == errSecSuccess) return YES; else return NO;
}

// remove a keychain entry
- (void) clearKeychain
{
	NSMutableDictionary *genericPasswordQuery = [self baseDictionary];
	
	OSStatus status = SecItemDelete((__bridge CFDictionaryRef) genericPasswordQuery);
    secerror( "DELETE: %s\n", [[self fetchStatus:status] UTF8String]);
}

// update a keychain entry
- (BOOL) updateKeychainValue:(NSString *)password
{
	NSMutableDictionary *genericPasswordQuery = [self baseDictionary];
	
	NSMutableDictionary *attributesToUpdate = [[NSMutableDictionary alloc] init];
	NSData *passwordData = [password dataUsingEncoding:NSUTF8StringEncoding];
	[attributesToUpdate setObject:passwordData forKey:(__bridge id)kSecValueData];
	
	OSStatus status = SecItemUpdate((__bridge CFDictionaryRef)genericPasswordQuery, (__bridge CFDictionaryRef)attributesToUpdate);
    secerror( "UPDATE: %s\n", [[self fetchStatus:status] UTF8String]);
	
	if (status == 0) return YES; else return NO;
}

// fetch a keychain value
- (NSString *) fetchPassword
{
    NSMutableArray *allItems = [self fetchDictionary];
    if (!allItems)
        return NULL;
    
    // This is used for a single item, so take first item in array
    NSData *passData = [allItems[0] objectForKey:(__bridge id)kSecValueData];
    if (passData)
        return [[NSString alloc] initWithData:passData encoding:NSUTF8StringEncoding];
	
    return NULL;
}

- (void)setPassword: (NSString *) thePassword
{
	if (![self createKeychainValue:thePassword]) 
		[self updateKeychainValue:thePassword];
}

- (void)setItem:(NSDictionary *)newItem
{
    OSStatus status = errSecSuccess;
    NSMutableDictionary *passwordDict = [self baseDictionary];

	// Add the password
	NSData *passwordData = [[newItem objectForKey:kItemPasswordKey] dataUsingEncoding:NSUTF8StringEncoding];
    [passwordDict setObject:passwordData forKey:(__bridge id)kSecValueData]; // password 

    [passwordDict setObject:[newItem objectForKey:kItemAccountKey] forKey:(__bridge id)kSecAttrAccount];
    [passwordDict setObject:[newItem objectForKey:kItemNameKey] forKey:(__bridge id)kSecAttrService];

    // Try to add first; if error, then try to update
	status = SecItemAdd((__bridge CFDictionaryRef)passwordDict, NULL);
    secerror( "SecItemAdd result: %@ (%ld)", [self fetchStatus:status], (long)status);
    NSLog(@"SecItemAdd result: %@ (%ld)", [self fetchStatus:status], (long)status);

    if (status)
    {
        // Try update
        // We only want to update the password data, so delete the other keys
        
//        NSArray *keysToRemove = [NSArray arrayWithObjects:kItemAccountKey, kItemNameKey, nil];
        [passwordDict removeObjectsForKeys:@[(__bridge id)kSecValueData]];

        NSDictionary *itemsToUpdate = @{ (__bridge id)kSecValueData : passwordData };
//        [genericPasswordQuery setObject:[newItem objectForKey:kItemAccountKey] forKey:(__bridge id)kSecAttrAccount];
//        [genericPasswordQuery setObject:[newItem objectForKey:kItemNameKey] forKey:(__bridge id)kSecAttrService];
        status = SecItemUpdate((__bridge CFDictionaryRef)passwordDict, (__bridge CFDictionaryRef)(itemsToUpdate));
        secerror( "SecItemUpdate result: %@ (%ld)", [self fetchStatus:status], (long)status);
        NSLog(@"SecItemUpdate result: %@ (%ld)", [self fetchStatus:status], (long)status);
    }
}

- (NSMutableArray *)fetchDictionaryAll
{
	NSMutableDictionary *query = [[NSMutableDictionary alloc] init];
	[query setObject:(__bridge id)kSecClassGenericPassword forKey:(__bridge id)kSecClass];
    [query setObject:(__bridge id)kCFBooleanTrue forKey:(__bridge id)kSecAttrSynchronizable];

	// Add the search constraints
	[query setObject:(__bridge id)kSecMatchLimitAll forKey:(__bridge id)kSecMatchLimit];
	[query setObject:(__bridge id)kCFBooleanTrue
							 forKey:(__bridge id)kSecReturnAttributes];
	[query setObject:(__bridge id)kCFBooleanTrue
							 forKey:(__bridge id)kSecReturnData];

    NSMutableArray *genericItems = [self fetchDictionaryWithQuery:query];
    
    // Now look for internet items
    [query setObject:(__bridge id)kSecClassInternetPassword forKey:(__bridge id)kSecClass];
    NSMutableArray *internetItems = [self fetchDictionaryWithQuery:query];
    if (internetItems)
        [genericItems addObjectsFromArray:internetItems];
	return genericItems;
}	

// MARK: ----- Full routines -----

// Return a base dictionary
- (NSMutableDictionary *) baseDictionaryFull:(NSString *)account service:(NSString *)service
{
	NSMutableDictionary *md = [[NSMutableDictionary alloc] init];
	
	// Password identification keys
	NSData *identifier = [PWKEY dataUsingEncoding:NSUTF8StringEncoding];
	[md setObject:identifier forKey:(__bridge id)kSecAttrGeneric];
	[md setObject:account forKey:(__bridge id)kSecAttrAccount];
    [md setObject:service forKey:(__bridge id)kSecAttrService];
	[md setObject:(__bridge id)kSecClassGenericPassword forKey:(__bridge id)kSecClass];
    [md setObject:(__bridge id)kCFBooleanTrue forKey:(__bridge id)kSecAttrSynchronizable];

//	return [md autorelease];
	return md;
}

- (BOOL) createKeychainValueFull:(NSString *)account service:(NSString *)service password:(NSString *)password
{
	NSMutableDictionary *md = [self buildDictForPasswordFull:account service:service password:password];
	OSStatus status = SecItemAdd((__bridge CFDictionaryRef)md, NULL);
    secerror( "CREATE: %s\n", [[self fetchStatus:status] UTF8String]);
	
	if (status == errSecSuccess) return YES; else return NO;
}

- (BOOL) updateKeychainValueFull:(NSString *)account service:(NSString *)service password:(NSString *)password
{
	NSMutableDictionary *genericPasswordQuery = [self baseDictionaryFull:account service:service];
	
	NSMutableDictionary *attributesToUpdate = [[NSMutableDictionary alloc] init];
	NSData *passwordData = [password dataUsingEncoding:NSUTF8StringEncoding];
	[attributesToUpdate setObject:passwordData forKey:(__bridge id)kSecValueData];
	
	OSStatus status = SecItemUpdate((__bridge CFDictionaryRef)genericPasswordQuery, (__bridge CFDictionaryRef)attributesToUpdate);
    secerror( "UPDATE: %s\n", [[self fetchStatus:status] UTF8String]);
	
	if (status == 0) return YES; else return NO;
}

- (void)setPasswordFull:(NSString *)account service:(NSString *)service password:(NSString *) thePassword
{
    secerror( "setPasswordFull account: %@, service: %@, password: %@", account, service, thePassword);
	if (![self createKeychainValueFull:account service:service password:thePassword])
		[self updateKeychainValueFull:account service:service password:thePassword];
}

// Return a keychain-style dictionary populated with the password
- (NSMutableDictionary *) buildDictForPasswordFull:(NSString *)account service:(NSString *)service password:(NSString *)password
{
	NSMutableDictionary *passwordDict = [self baseDictionaryFull:account service:service];
	
	// Add the password
	NSData *passwordData = [password dataUsingEncoding:NSUTF8StringEncoding];
    [passwordDict setObject:passwordData forKey:(__bridge id)kSecValueData]; // password 
	
	return passwordDict;
}

- (void)clearAllKeychainItems
{
    CFIndex ix, top;
    OSStatus status = errSecSuccess;
    
    NSArray *allItems = (NSArray *)[[MyKeychain sharedInstance] fetchDictionaryAll];
    top = [allItems count];
    secerror( "Deleting %ld items", (long)top);
    
    for (ix=0; ix<top && !status; ix++)
    {
        NSDictionary *item = [allItems objectAtIndex:ix];
        NSMutableDictionary *query = [[NSMutableDictionary alloc] init];
        
        NSData *identifier = [PWKEY dataUsingEncoding:NSUTF8StringEncoding];
        [query setObject:identifier forKey:(__bridge id)kSecAttrGeneric];
        [query setObject:(__bridge id)kSecClassGenericPassword forKey:(__bridge id)kSecClass];
        [query setObject:(__bridge id)kCFBooleanTrue forKey:(__bridge id)kSecAttrSynchronizable];
        
        [query setObject:[item objectForKey:(__bridge id)(kSecAttrAccount)] forKey:(__bridge id)kSecAttrAccount];
        [query setObject:[item objectForKey:(__bridge id)(kSecAttrService)] forKey:(__bridge id)kSecAttrService];
        
        status = SecItemDelete((__bridge CFDictionaryRef) query);
        secerror( "DELETE: %s\n", [[self fetchStatus:status] UTF8String]);
    }
}


@end