Search code examples
iosobjective-cxcodekeyboardkeychain

Using keychain in a keyboard target


I am using a framework in my keyboard that is using keychain to store UUID. Unfortunately my keyboard crashes while trying to write that value here:

2015-09-10 12:34:26.232 Keyboard[15504:3505665] *** Assertion failure in -[LUIKeychain writeToKeychain], /Projects/iOS/LUIFramework/LUIFramework/LUIKeychain.m:249
2015-09-10 12:34:26.233 Keyboard[15504:3505665] *** Terminating app due to uncaught exception 'NSInternalInconsistencyException', reason: 'Couldn't add the Keychain Item.'

The framework is mine and should work on all apple platforms so the good news is that I can fix it ... any ideas where to start?

Edit: Requested source code:

I am using it to store UUID here:

#import "LUIUUID.h"
#import "LUIKeychain.h"


#define kLUIUUIDKey                                 @"LUIUUIDKey"
#define kLUISessionUUIDKey                          @"LUISessionUUIDKey"


@implementation LUIUUID


+ (NSString *)UUID {
    LUIKeychain *keychain = [[LUIKeychain alloc] initWithIdentifier:kLUIUUIDKey accessGroup:nil];
    NSString *uuid = [keychain objectForKey:(__bridge id)kSecAttrAccount];
    if (!uuid || uuid.length == 0) {
        uuid = [[NSUUID UUID] UUIDString];
        [keychain setObject:uuid forKey:(__bridge id)(kSecAttrAccount)];
    }
    return uuid;
}


@end

And this is the library:

#import "LUIKeychain.h"
#import <Security/Security.h>

/*

 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 LUIKeychain ()

@property (nonatomic, strong) NSMutableDictionary *genericPasswordQuery;
@property (nonatomic, strong) NSMutableDictionary *keychainItemData;
/*
 The decision behind the following two methods (secItemFormatToDictionary and dictionaryToSecItemFormat) was
 to encapsulate the transition between what the detail view controller was expecting (NSString *)and what the
 Keychain API expects as a validly constructed container class.
 */
- (NSMutableDictionary *)secItemFormatToDictionary:(NSDictionary *)dictionaryToConvert;
- (NSMutableDictionary *)dictionaryToSecItemFormat:(NSDictionary *)dictionaryToConvert;

// Updates the item in the keychain, or adds it if it doesn't exist.
- (void)writeToKeychain;

@end


@implementation LUIKeychain


- (instancetype)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.
        _genericPasswordQuery = [[NSMutableDictionary alloc] init];

        [_genericPasswordQuery setObject:(__bridge id)kSecClassGenericPassword forKey:(__bridge id)kSecClass];
        [_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
            [_genericPasswordQuery setObject:accessGroup forKey:(__bridge id)kSecAttrAccessGroup];
#endif
        }

        // Use the proper search constants, return only the attributes of the first match.
        [_genericPasswordQuery setObject:(__bridge id)kSecMatchLimitOne forKey:(__bridge id)kSecMatchLimit];
        [_genericPasswordQuery setObject:(id)kCFBooleanTrue forKey:(__bridge id)kSecReturnAttributes];

        NSDictionary *tempQuery = [NSDictionary dictionaryWithDictionary:_genericPasswordQuery];

        NSMutableDictionary *dictionary = nil;
        CFTypeRef outDictionary = (__bridge CFTypeRef)dictionary;

        if (! SecItemCopyMatching((__bridge CFDictionaryRef)tempQuery, &outDictionary) == noErr) {
            // Stick these default values into keychain item if nothing found.
            [self resetKeychainItem];

            // Add the generic attribute and the keychain access group.
            [_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
                [_keychainItemData setObject:accessGroup forKey:(__bridge id)kSecAttrAccessGroup];
#endif
            }
        }
        else {
            // load the saved data from Keychain.
            _keychainItemData = [self secItemFormatToDictionary:(__bridge NSDictionary *)(outDictionary)];
        }

        //[outDictionary release];
    }

    return self;
}

- (void)setObject:(id)inObject forKey:(id)key {
    if (inObject == nil) return;
    id currentObject = [_keychainItemData objectForKey:key];
    if (![currentObject isEqual:inObject]) {
        [_keychainItemData setObject:inObject forKey:key];
        [self writeToKeychain];
    }
}

- (id)objectForKey:(id)key {
    return [_keychainItemData objectForKey:key];
}

- (void)resetKeychainItem {
    OSStatus junk = noErr;
    if (!_keychainItemData) {
        _keychainItemData = [[NSMutableDictionary alloc] init];
    }
    else if (_keychainItemData) {
        NSMutableDictionary *tempDictionary = [self dictionaryToSecItemFormat:_keychainItemData];
        junk = SecItemDelete((__bridge CFDictionaryRef)tempDictionary);
        NSAssert( junk == noErr || junk == errSecItemNotFound, @"Problem deleting current dictionary." );
    }

    // Default attributes for keychain item.
    [_keychainItemData setObject:@"" forKey:(__bridge id)kSecAttrAccount];
    [_keychainItemData setObject:@"" forKey:(__bridge id)kSecAttrLabel];
    [_keychainItemData setObject:@"" forKey:(__bridge id)kSecAttrDescription];

    // Default data for keychain item.
    [_keychainItemData setObject:@"" forKey:(__bridge id)kSecValueData];
}

- (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 {
    // 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:(id)kCFBooleanTrue forKey:(__bridge id)kSecReturnData];
    [returnDictionary setObject:(__bridge id)kSecClassGenericPassword forKey:(__bridge id)kSecClass];

    // Acquire the password data from the attributes.
    NSData *pData = NULL;
    CFTypeRef passwordData = (__bridge CFTypeRef)pData;
    OSStatus val = SecItemCopyMatching((__bridge CFDictionaryRef)returnDictionary, &passwordData);
    if (val == noErr) {
        // Remove the search, class, and identifier key/value, we don't need them anymore.
        [returnDictionary removeObjectForKey:(__bridge id)kSecReturnData];

        NSData *resultData = CFBridgingRelease(passwordData);
        // Add the password to the dictionary, converting from NSData to NSString.
        //NSString *password = [[[NSString alloc] initWithBytes:[passwordData bytes] length:[passwordData length]
        //encoding:NSUTF8StringEncoding] autorelease];
        NSString *password = [[NSString alloc] initWithData:resultData encoding:NSUTF8StringEncoding];
        [returnDictionary setObject:password forKey:(__bridge id)kSecValueData];
    }
    else {
        // Don't do anything if nothing is found.
        // NSAssert(NO, @"Serious error, no matching item found in the keychain.\n");
    }

    //[passwordData release];

    return returnDictionary;
}

- (void)writeToKeychain {
    NSDictionary *attr = NULL;
    CFTypeRef attributes = (__bridge CFTypeRef)attr;
    NSMutableDictionary *updateItem = NULL;
    OSStatus result;

    if (SecItemCopyMatching((__bridge CFDictionaryRef)_genericPasswordQuery, (CFTypeRef *)&attributes) == noErr) {
        // 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:[_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:_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.

        result = SecItemUpdate((__bridge CFDictionaryRef)updateItem, (__bridge CFDictionaryRef)tempCheck);
        NSAssert( result == noErr, @"Couldn't update the Keychain Item." );
    }
    else {
        // No previous item found; add the new one.
        result = SecItemAdd((__bridge CFDictionaryRef)[self dictionaryToSecItemFormat:_keychainItemData], NULL);
        NSAssert( result == noErr, @"Couldn't add the Keychain Item." );
    }
}


@end

Solution

  • Ok, I have had the full access disabled on the keyboard.

    I have solved the problem by putting a try/catch on the write to keychain method and by using:

    - (BOOL)isFullAccessGranted {
        return !![UIPasteboard generalPasteboard];
    }
    

    To check if the keyboard has the full access or not. If not, I display a message.