Get the Username(s) stored in Keychain, using only the ServiceName? OR: Where are you supposed to store the Username?
Asked Answered
K

3

5

So the OS X Keychain has three pieces of information:

  • ServiceName (the name of my app)
  • Username
  • Password

I obviously always know the ServiceName. Is there a way to find any saved Username(s) for that ServiceName? (Finding the password is easy once you know the Username.)

I would much prefer to use a nice Cocoa wrapper such as EMKeychain to do this. But EMKeychain requires the UserName to get any keychain item!

+ (EMGenericKeychainItem *)genericKeychainItemForService:(NSString *)serviceNameString withUsername:(NSString *)usernameString;

How are you expected to fully utilize saving credentials in the Keychain, if you need the Username to find the credentials? Is the best practice to save the Username in the .plist file or something?

Kriss answered 9/12, 2011 at 20:19 Comment(0)
D
6

SecKeychainFindGenericPassword only returns a single keychain item. To find all generic passwords for a specific service, you need to run a query on the keychain. There are several ways to do this, based on what version of OS X you target.

If you need to run on 10.5 or below, you'll need to use SecKeychainSearchCreateFromAttributes. It's a rather horrible API. Here is a rough cut of a method that returns a dictionary mapping usernames to passwords.

- (NSDictionary *)genericPasswordsWithService:(NSString *)service {
    OSStatus status;

    // Construct a query.
    const char *utf8Service = [service UTF8String];
    SecKeychainAttribute attr = { .tag = kSecServiceItemAttr, 
                                  .length = strlen(utf8Service), 
                                  .data = (void *)utf8Service };
    SecKeychainAttribute attrList = { .count = 1, .attr = &attr };
    SecKeychainSearchRef *search = NULL;
    status = SecKeychainSearchCreateFromAttributes(NULL, kSecGenericPasswordItemClass, &attrList, &search);
    if (status) {
        report(status);
        return nil;
    }

    // Enumerate results.
    NSMutableDictionary *result = [NSMutableDictionary dictionary];
    while (1) {
        SecKeychainItemRef item = NULL;
        status = SecKeychainSearchCopyNext(search, &item);
        if (status)
            break;

        // Find 'account' attribute and password value.
        UInt32 tag = kSecAccountItemAttr;
        UInt32 format = CSSM_DB_ATTRIBUTE_FORMAT_STRING;
        SecKeychainAttributeInfo info = { .count = 1, .tag = &tag, .format = &format };
        SecKeychainAttributeList *attrList = NULL;
        UInt32 length = 0;
        void *data = NULL;
        status = SecKeychainItemCopyAttributesAndData(item, &info, NULL, &attrList, &length, &data);
        if (status) {
            CFRelease(item);
            continue;
        }

        NSAssert(attrList->count == 1 && attrList->attr[0].tag == kSecAccountItemAttr, @"SecKeychainItemCopyAttributesAndData is messing with us");
        NSString *account = [[[NSString alloc] initWithBytes:attrList->attr[0].data length:attrList->attr[0].length encoding:NSUTF8StringEncoding] autorelease];
        NSString *password = [[[NSString alloc] initWithBytes:data length:length encoding:NSUTF8StringEncoding] autorelease];
        [result setObject:password forKey:account];

        SecKeychainItemFreeAttributesAndData(attrList, data);
        CFRelease(item);
    }
    CFRelease(search);
    return result;
}

For 10.6 and later, you can use the somewhat less inconvenient SecItemCopyMatching API:

- (NSDictionary *)genericPasswordsWithService:(NSString *)service {
    NSDictionary *query = [NSDictionary dictionaryWithObjectsAndKeys:
                           kSecClassGenericPassword, kSecClass,
                           (id)kCFBooleanTrue, kSecReturnData,
                           (id)kCFBooleanTrue, kSecReturnAttributes,
                           kSecMatchLimitAll, kSecMatchLimit,
                           service, kSecAttrService,
                           nil];
    NSArray *itemDicts = nil;
    OSStatus status = SecItemCopyMatching((CFDictionaryRef)q, (CFTypeRef *)&itemDicts);
    if (status) {
        report(status);
        return nil;
    }
    NSMutableDictionary *result = [NSMutableDictionary dictionary];
    for (NSDictionary *itemDict in itemDicts) {
        NSData *data = [itemDict objectForKey:kSecValueData];
        NSString *password = [[[NSString alloc] initWithData:data encoding:NSUTF8StringEncoding] autorelease];
        NSString *account = [itemDict objectForKey:kSecAttrAccount];
        [result setObject:password forKey:account];
    }
    [itemDicts release];
    return result;
}

For 10.7 or later, you can use my wonderful LKKeychain framework (PLUG!). It doesn't support building attribute-based queries, but you can simply list all passwords and filter out the ones you don't need.

- (NSDictionary *)genericPasswordsWithService:(NSString *)service {
    LKKCKeychain *keychain = [LKKCKeychain defaultKeychain];
    NSMutableDictionary *result = [NSMutableDictionary dictionary];
    for (LKKCGenericPassword *item in [keychain genericPasswords]) {
        if ([service isEqualToString:item.service]) {
            [result setObject:item.password forKey:item.account];
        }
    }
    return result;
}

(I didn't try running, or even compiling any of the above code samples; sorry for any typos.)

Daze answered 12/12, 2011 at 13:42 Comment(1)
Looks like LKKeychain has been renamed to LKSecurityMccool
T
2

You don't need the username. You do with EMKeychain, but that's an artificial distinction that that class imposes; the underlying Keychain Services function does not require a username to find a keychain item.

When using SecKeychainFindGenericPassword directly, pass 0 and NULL for the username parameters. It will return a keychain item that exists on that service.

However, that will return only one item. If the user has multiple keychain items on the same service, you won't know that, or which one you got (the documentation says it returns the “first” matching item, with no specification of what it considers “first”). If you want any and all items for that service, you should create a search and use that.

Thumbstall answered 9/12, 2011 at 21:31 Comment(3)
>*It will return a keychain item that exists on that service.* Where and in what form is this keychain item stored? In the SecKeychainFindGenericPassword you get passwordData - On return, a pointer to a buffer that holds the password data and itemRef - On return, a pointer to the item object of the generic password... but nothing similar for the username field? How do I get the username when using that method?Kriss
Er, I suppose SecKeychainItemRef *itemRef is what I need to use. But how do I pull the username/password/service out of that?Kriss
@cksubs developer.apple.com/library/mac/documentation/Security/…Thumbstall
M
0

Generic passwords have a unique key of the service name and the username. Thus, to fetch a single generic keychain entry, you will need to provide both. However, you can iterate over all generic keychain entries for your given service using the SecKeychainFindGenericPassword function.

(Disclaimer: I don't know anything about doing this in EMKeychain.)

Military answered 9/12, 2011 at 21:17 Comment(2)
That isn't true. The username is not required.Thumbstall
Also, how exactly do you “iterate over all generic keychain entries for your given service using the SecKeychainFindGenericPassword function”? The documentation for that function says it returns the first matching entry.Thumbstall

© 2022 - 2024 — McMap. All rights reserved.