//
//
SFHFKeychainUtils.h
//
//
Created by Buzz Andersen on 10/20/08.
//
Based partly on code by Jonathan Wight, Jon Crosby, and Mike Malone.
//
Copyright 2008 Sci-Fi Hi-Fi. All rights reserved.
//
//
Permission is hereby granted, free of charge, to any person
//
obtaining a copy of this software and associated documentation
//
files (the "Software"), to deal in the Software without
//
restriction, including without limitation the rights to use,
//
copy, modify, merge, publish, distribute, sublicense, and/or sell
//
copies of the Software, and to permit persons to whom the
//
Software is furnished to do so, subject to the following
//
conditions:
//
//
The above copyright notice and this permission notice shall be
//
included in all copies or substantial portions of the Software.
//
//
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
//
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES
//
OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
//
NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
//
HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
//
WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
//
FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
//
OTHER DEALINGS IN THE SOFTWARE.
//
#import <UIKit/UIKit.h>
@interface SFHFKeychainUtils : NSObject {
}
+ (NSString *) getPasswordForUsername: (NSString *) username andServiceName: (NSString *) serviceName error: (NSError **) error;
+ (BOOL) storeUsername: (NSString *) username andPassword: (NSString *) password forServiceName: (NSString *) serviceName updateExisting: (BOOL) updateExisting error: (NSError **) error;
+ (BOOL) deleteItemForUsername: (NSString *) username andServiceName: (NSString *) serviceName error: (NSError **) error;
@end
//
//
SFHFKeychainUtils.m
//
//
Created by Buzz Andersen on 10/20/08.
//
Based partly on code by Jonathan Wight, Jon Crosby, and Mike Malone.
//
Copyright 2008 Sci-Fi Hi-Fi. All rights reserved.
//
//
Permission is hereby granted, free of charge, to any person
//
obtaining a copy of this software and associated documentation
//
files (the "Software"), to deal in the Software without
//
restriction, including without limitation the rights to use,
//
copy, modify, merge, publish, distribute, sublicense, and/or sell
//
copies of the Software, and to permit persons to whom the
//
Software is furnished to do so, subject to the following
//
conditions:
//
//
The above copyright notice and this permission notice shall be
//
included in all copies or substantial portions of the Software.
//
//
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
//
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES
//
OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
//
NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
//
HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
//
WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
//
FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
//
OTHER DEALINGS IN THE SOFTWARE.
//
#import
"
SFHFKeychainUtils.h
"
#import <Security/Security.h>
static NSString *SFHFKeychainUtilsErrorDomain =
@"
SFHFKeychainUtilsErrorDomain
";
#if __IPHONE_OS_VERSION_MIN_REQUIRED < 30000 && TARGET_IPHONE_SIMULATOR
@interface SFHFKeychainUtils (PrivateMethods)
+ (SecKeychainItemRef) getKeychainItemReferenceForUsername: (NSString *) username andServiceName: (NSString *) serviceName error: (NSError **) error;
@end
#endif
@implementation SFHFKeychainUtils
#if __IPHONE_OS_VERSION_MIN_REQUIRED < 30000 && TARGET_IPHONE_SIMULATOR
+ (NSString *) getPasswordForUsername: (NSString *) username andServiceName: (NSString *) serviceName error: (NSError **) error {
if (!username || !serviceName) {
*error = [NSError errorWithDomain: SFHFKeychainUtilsErrorDomain code: -
2000 userInfo: nil];
return nil;
}
SecKeychainItemRef item = [SFHFKeychainUtils getKeychainItemReferenceForUsername: username andServiceName: serviceName error: error];
if (*error || !item) {
return nil;
}
//
from Advanced Mac OS X Programming, ch. 16
UInt32 length;
char *password;
SecKeychainAttribute attributes[
8];
SecKeychainAttributeList list;
attributes[
0].tag = kSecAccountItemAttr;
attributes[
1].tag = kSecDescriptionItemAttr;
attributes[
2].tag = kSecLabelItemAttr;
attributes[
3].tag = kSecModDateItemAttr;
list.count =
4;
list.attr = attributes;
OSStatus status = SecKeychainItemCopyContent(item, NULL, &list, &length, (
void **)&password);
if (status != noErr) {
*error = [NSError errorWithDomain: SFHFKeychainUtilsErrorDomain code: status userInfo: nil];
return nil;
}
NSString *passwordString = nil;
if (password != NULL) {
char passwordBuffer[
1024];
if (length >
1023) {
length =
1023;
}
strncpy(passwordBuffer, password, length);
passwordBuffer[length] =
'
\0
';
passwordString = [NSString stringWithCString:passwordBuffer];
}
SecKeychainItemFreeContent(&list, password);
CFRelease(item);
return passwordString;
}
+ (
void) storeUsername: (NSString *) username andPassword: (NSString *) password forServiceName: (NSString *) serviceName updateExisting: (BOOL) updateExisting error: (NSError **) error {
if (!username || !password || !serviceName) {
*error = [NSError errorWithDomain: SFHFKeychainUtilsErrorDomain code: -
2000 userInfo: nil];
return;
}
OSStatus status = noErr;
SecKeychainItemRef item = [SFHFKeychainUtils getKeychainItemReferenceForUsername: username andServiceName: serviceName error: error];
if (*error && [*error code] != noErr) {
return;
}
*error = nil;
if (item) {
status = SecKeychainItemModifyAttributesAndData(item,
NULL,
strlen([password UTF8String]),
[password UTF8String]);
CFRelease(item);
}
else {
status = SecKeychainAddGenericPassword(NULL,
strlen([serviceName UTF8String]),
[serviceName UTF8String],
strlen([username UTF8String]),
[username UTF8String],
strlen([password UTF8String]),
[password UTF8String],
NULL);
}
if (status != noErr) {
*error = [NSError errorWithDomain: SFHFKeychainUtilsErrorDomain code: status userInfo: nil];
}
}
+ (
void) deleteItemForUsername: (NSString *) username andServiceName: (NSString *) serviceName error: (NSError **) error {
if (!username || !serviceName) {
*error = [NSError errorWithDomain: SFHFKeychainUtilsErrorDomain code:
2000 userInfo: nil];
return;
}
*error = nil;
SecKeychainItemRef item = [SFHFKeychainUtils getKeychainItemReferenceForUsername: username andServiceName: serviceName error: error];
if (*error && [*error code] != noErr) {
return;
}
OSStatus status;
if (item) {
status = SecKeychainItemDelete(item);
CFRelease(item);
}
if (status != noErr) {
*error = [NSError errorWithDomain: SFHFKeychainUtilsErrorDomain code: status userInfo: nil];
}
}
+ (SecKeychainItemRef) getKeychainItemReferenceForUsername: (NSString *) username andServiceName: (NSString *) serviceName error: (NSError **) error {
if (!username || !serviceName) {
*error = [NSError errorWithDomain: SFHFKeychainUtilsErrorDomain code: -
2000 userInfo: nil];
return nil;
}
*error = nil;
SecKeychainItemRef item;
OSStatus status = SecKeychainFindGenericPassword(NULL,
strlen([serviceName UTF8String]),
[serviceName UTF8String],
strlen([username UTF8String]),
[username UTF8String],
NULL,
NULL,
&item);
if (status != noErr) {
if (status != errSecItemNotFound) {
*error = [NSError errorWithDomain: SFHFKeychainUtilsErrorDomain code: status userInfo: nil];
}
return nil;
}
return item;
}
#else
+ (NSString *) getPasswordForUsername: (NSString *) username andServiceName: (NSString *) serviceName error: (NSError **) error {
if (!username || !serviceName) {
if (error != nil) {
*error = [NSError errorWithDomain: SFHFKeychainUtilsErrorDomain code: -
2000 userInfo: nil];
}
return nil;
}
if (error != nil) {
*error = nil;
}
//
Set up a query dictionary with the base query attributes: item type (generic), username, and service
NSArray *keys = [[[NSArray alloc] initWithObjects: (NSString *) kSecClass, kSecAttrAccount, kSecAttrService, nil] autorelease];
NSArray *objects = [[[NSArray alloc] initWithObjects: (NSString *) kSecClassGenericPassword, username, serviceName, nil] autorelease];
NSMutableDictionary *query = [[[NSMutableDictionary alloc] initWithObjects: objects forKeys: keys] autorelease];
//
First do a query for attributes, in case we already have a Keychain item with no password data set.
//
One likely way such an incorrect item could have come about is due to the previous (incorrect)
//
version of this code (which set the password as a generic attribute instead of password data).
NSDictionary *attributeResult = NULL;
NSMutableDictionary *attributeQuery = [query mutableCopy];
[attributeQuery setObject: (
id) kCFBooleanTrue forKey:(
id) kSecReturnAttributes];
OSStatus status = SecItemCopyMatching((CFDictionaryRef) attributeQuery, (CFTypeRef *) &attributeResult);
[attributeResult release];
[attributeQuery release];
if (status != noErr) {
//
No existing item found--simply return nil for the password
if (error != nil && status != errSecItemNotFound) {
//
Only return an error if a real exception happened--not simply for "not found."
*error = [NSError errorWithDomain: SFHFKeychainUtilsErrorDomain code: status userInfo: nil];
}
return nil;
}
//
We have an existing item, now query for the password data associated with it.
NSData *resultData = nil;
NSMutableDictionary *passwordQuery = [query mutableCopy];
[passwordQuery setObject: (
id) kCFBooleanTrue forKey: (
id) kSecReturnData];
status = SecItemCopyMatching((CFDictionaryRef) passwordQuery, (CFTypeRef *) &resultData);
[resultData autorelease];
[passwordQuery release];
if (status != noErr) {
if (status == errSecItemNotFound) {
//
We found attributes for the item previously, but no password now, so return a special error.
//
Users of this API will probably want to detect this error and prompt the user to
//
re-enter their credentials. When you attempt to store the re-entered credentials
//
using storeUsername:andPassword:forServiceName:updateExisting:error
//
the old, incorrect entry will be deleted and a new one with a properly encrypted
//
password will be added.
if (error != nil) {
*error = [NSError errorWithDomain: SFHFKeychainUtilsErrorDomain code: -
1999 userInfo: nil];
}
}
else {
//
Something else went wrong. Simply return the normal Keychain API error code.
if (error != nil) {
*error = [NSError errorWithDomain: SFHFKeychainUtilsErrorDomain code: status userInfo: nil];
}
}
return nil;
}
NSString *password = nil;
if (resultData) {
password = [[NSString alloc] initWithData: resultData encoding: NSUTF8StringEncoding];
}
else {
//
There is an existing item, but we weren't able to get password data for it for some reason,
//
Possibly as a result of an item being incorrectly entered by the previous code.
//
Set the -1999 error so the code above us can prompt the user again.
if (error != nil) {
*error = [NSError errorWithDomain: SFHFKeychainUtilsErrorDomain code: -
1999 userInfo: nil];
}
}
return [password autorelease];
}
+ (BOOL) storeUsername: (NSString *) username andPassword: (NSString *) password forServiceName: (NSString *) serviceName updateExisting: (BOOL) updateExisting error: (NSError **) error
{
if (!username || !password || !serviceName)
{
if (error != nil)
{
*error = [NSError errorWithDomain: SFHFKeychainUtilsErrorDomain code: -
2000 userInfo: nil];
}
return NO;
}
//
See if we already have a password entered for these credentials.
NSError *getError = nil;
NSString *existingPassword = [SFHFKeychainUtils getPasswordForUsername: username andServiceName: serviceName error:&getError];
if ([getError code] == -
1999)
{
//
There is an existing entry without a password properly stored (possibly as a result of the previous incorrect version of this code.
//
Delete the existing item before moving on entering a correct one.
getError = nil;
[self deleteItemForUsername: username andServiceName: serviceName error: &getError];
if ([getError code] != noErr)
{
if (error != nil)
{
*error = getError;
}
return NO;
}
}
else
if ([getError code] != noErr)
{
if (error != nil)
{
*error = getError;
}
return NO;
}
if (error != nil)
{
*error = nil;
}
OSStatus status = noErr;
if (existingPassword)
{
//
We have an existing, properly entered item with a password.
//
Update the existing item.
if (![existingPassword isEqualToString:password] && updateExisting)
{
//
Only update if we're allowed to update existing. If not, simply do nothing.
NSArray *keys = [[[NSArray alloc] initWithObjects: (NSString *) kSecClass,
kSecAttrService,
kSecAttrLabel,
kSecAttrAccount,
nil] autorelease];
NSArray *objects = [[[NSArray alloc] initWithObjects: (NSString *) kSecClassGenericPassword,
serviceName,
serviceName,
username,
nil] autorelease];
NSDictionary *query = [[[NSDictionary alloc] initWithObjects: objects forKeys: keys] autorelease];
status = SecItemUpdate((CFDictionaryRef) query, (CFDictionaryRef) [NSDictionary dictionaryWithObject: [password dataUsingEncoding: NSUTF8StringEncoding] forKey: (NSString *) kSecValueData]);
}
}
else
{
//
No existing entry (or an existing, improperly entered, and therefore now
//
deleted, entry). Create a new entry.
NSArray *keys = [[[NSArray alloc] initWithObjects: (NSString *) kSecClass,
kSecAttrService,
kSecAttrLabel,
kSecAttrAccount,
kSecValueData,
nil] autorelease];
NSArray *objects = [[[NSArray alloc] initWithObjects: (NSString *) kSecClassGenericPassword,
serviceName,
serviceName,
username,
[password dataUsingEncoding: NSUTF8StringEncoding],
nil] autorelease];
NSDictionary *query = [[[NSDictionary alloc] initWithObjects: objects forKeys: keys] autorelease];
status = SecItemAdd((CFDictionaryRef) query, NULL);
}
if (status != noErr)
{
//
Something went wrong with adding the new item. Return the Keychain error code.
if (error != nil) {
*error = [NSError errorWithDomain: SFHFKeychainUtilsErrorDomain code: status userInfo: nil];
}
return NO;
}
return YES;
}
+ (BOOL) deleteItemForUsername: (NSString *) username andServiceName: (NSString *) serviceName error: (NSError **) error
{
if (!username || !serviceName)
{
if (error != nil)
{
*error = [NSError errorWithDomain: SFHFKeychainUtilsErrorDomain code: -
2000 userInfo: nil];
}
return NO;
}
if (error != nil)
{
*error = nil;
}
NSArray *keys = [[[NSArray alloc] initWithObjects: (NSString *) kSecClass, kSecAttrAccount, kSecAttrService, kSecReturnAttributes, nil] autorelease];
NSArray *objects = [[[NSArray alloc] initWithObjects: (NSString *) kSecClassGenericPassword, username, serviceName, kCFBooleanTrue, nil] autorelease];
NSDictionary *query = [[[NSDictionary alloc] initWithObjects: objects forKeys: keys] autorelease];
OSStatus status = SecItemDelete((CFDictionaryRef) query);
if (status != noErr)
{
if (error != nil) {
*error = [NSError errorWithDomain: SFHFKeychainUtilsErrorDomain code: status userInfo: nil];
}
return NO;
}
return YES;
}
#endif
@end