4 // Created by Buzz Andersen on 10/20/08.
5 // Based partly on code by Jonathan Wight, Jon Crosby, and Mike Malone.
6 // Copyright 2008 Sci-Fi Hi-Fi. All rights reserved.
8 // Permission is hereby granted, free of charge, to any person
9 // obtaining a copy of this software and associated documentation
10 // files (the "Software"), to deal in the Software without
11 // restriction, including without limitation the rights to use,
12 // copy, modify, merge, publish, distribute, sublicense, and/or sell
13 // copies of the Software, and to permit persons to whom the
14 // Software is furnished to do so, subject to the following
17 // The above copyright notice and this permission notice shall be
18 // included in all copies or substantial portions of the Software.
20 // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
21 // EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES
22 // OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
23 // NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
24 // HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
25 // WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
26 // FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
27 // OTHER DEALINGS IN THE SOFTWARE.
30 #import "SFHFKeychainUtils.h"
31 #import <Security/Security.h>
33 static NSString *SFHFKeychainUtilsErrorDomain = @"SFHFKeychainUtilsErrorDomain";
35 #if __IPHONE_OS_VERSION_MIN_REQUIRED < 30000 && TARGET_IPHONE_SIMULATOR
36 @interface SFHFKeychainUtils (PrivateMethods)
37 + (SecKeychainItemRef) getKeychainItemReferenceForUsername: (NSString *) username andServiceName: (NSString *) serviceName error: (NSError **) error;
41 @implementation SFHFKeychainUtils
43 #if __IPHONE_OS_VERSION_MIN_REQUIRED < 30000 && TARGET_IPHONE_SIMULATOR
45 + (NSString *) getPasswordForUsername: (NSString *) username andServiceName: (NSString *) serviceName error: (NSError **) error {
46 if (!username || !serviceName) {
47 *error = [NSError errorWithDomain: SFHFKeychainUtilsErrorDomain code: -2000 userInfo: nil];
51 SecKeychainItemRef item = [SFHFKeychainUtils getKeychainItemReferenceForUsername: username andServiceName: serviceName error: error];
53 if (*error || !item) {
57 // from Advanced Mac OS X Programming, ch. 16
60 SecKeychainAttribute attributes[8];
61 SecKeychainAttributeList list;
63 attributes[0].tag = kSecAccountItemAttr;
64 attributes[1].tag = kSecDescriptionItemAttr;
65 attributes[2].tag = kSecLabelItemAttr;
66 attributes[3].tag = kSecModDateItemAttr;
69 list.attr = attributes;
71 OSStatus status = SecKeychainItemCopyContent(item, NULL, &list, &length, (void **)&password);
73 if (status != noErr) {
74 *error = [NSError errorWithDomain: SFHFKeychainUtilsErrorDomain code: status userInfo: nil];
78 NSString *passwordString = nil;
80 if (password != NULL) {
81 char passwordBuffer[1024];
86 strncpy(passwordBuffer, password, length);
88 passwordBuffer[length] = '\0';
89 passwordString = [NSString stringWithCString:passwordBuffer];
92 SecKeychainItemFreeContent(&list, password);
96 return passwordString;
99 + (void) storeUsername: (NSString *) username andPassword: (NSString *) password forServiceName: (NSString *) serviceName updateExisting: (BOOL) updateExisting error: (NSError **) error {
100 if (!username || !password || !serviceName) {
101 *error = [NSError errorWithDomain: SFHFKeychainUtilsErrorDomain code: -2000 userInfo: nil];
105 OSStatus status = noErr;
107 SecKeychainItemRef item = [SFHFKeychainUtils getKeychainItemReferenceForUsername: username andServiceName: serviceName error: error];
109 if (*error && [*error code] != noErr) {
116 status = SecKeychainItemModifyAttributesAndData(item,
118 strlen([password UTF8String]),
119 [password UTF8String]);
124 status = SecKeychainAddGenericPassword(NULL,
125 strlen([serviceName UTF8String]),
126 [serviceName UTF8String],
127 strlen([username UTF8String]),
128 [username UTF8String],
129 strlen([password UTF8String]),
130 [password UTF8String],
134 if (status != noErr) {
135 *error = [NSError errorWithDomain: SFHFKeychainUtilsErrorDomain code: status userInfo: nil];
139 + (void) deleteItemForUsername: (NSString *) username andServiceName: (NSString *) serviceName error: (NSError **) error {
140 if (!username || !serviceName) {
141 *error = [NSError errorWithDomain: SFHFKeychainUtilsErrorDomain code: 2000 userInfo: nil];
147 SecKeychainItemRef item = [SFHFKeychainUtils getKeychainItemReferenceForUsername: username andServiceName: serviceName error: error];
149 if (*error && [*error code] != noErr) {
156 status = SecKeychainItemDelete(item);
161 if (status != noErr) {
162 *error = [NSError errorWithDomain: SFHFKeychainUtilsErrorDomain code: status userInfo: nil];
166 + (SecKeychainItemRef) getKeychainItemReferenceForUsername: (NSString *) username andServiceName: (NSString *) serviceName error: (NSError **) error {
167 if (!username || !serviceName) {
168 *error = [NSError errorWithDomain: SFHFKeychainUtilsErrorDomain code: -2000 userInfo: nil];
174 SecKeychainItemRef item;
176 OSStatus status = SecKeychainFindGenericPassword(NULL,
177 strlen([serviceName UTF8String]),
178 [serviceName UTF8String],
179 strlen([username UTF8String]),
180 [username UTF8String],
185 if (status != noErr) {
186 if (status != errSecItemNotFound) {
187 *error = [NSError errorWithDomain: SFHFKeychainUtilsErrorDomain code: status userInfo: nil];
198 + (NSString *) getPasswordForUsername: (NSString *) username andServiceName: (NSString *) serviceName error: (NSError **) error {
199 if (!username || !serviceName) {
200 if (error) *error = [NSError errorWithDomain: SFHFKeychainUtilsErrorDomain code: -2000 userInfo: nil];
204 if (error) *error = nil;
206 // Set up a query dictionary with the base query attributes: item type (generic), username, and service
208 NSArray *keys = [[[NSArray alloc] initWithObjects: (NSString *) kSecClass, kSecAttrAccount, kSecAttrService, nil] autorelease];
209 NSArray *objects = [[[NSArray alloc] initWithObjects: (NSString *) kSecClassGenericPassword, username, serviceName, nil] autorelease];
211 NSMutableDictionary *query = [[[NSMutableDictionary alloc] initWithObjects: objects forKeys: keys] autorelease];
213 // First do a query for attributes, in case we already have a Keychain item with no password data set.
214 // One likely way such an incorrect item could have come about is due to the previous (incorrect)
215 // version of this code (which set the password as a generic attribute instead of password data).
217 NSDictionary *attributeResult = NULL;
218 NSMutableDictionary *attributeQuery = [query mutableCopy];
219 [attributeQuery setObject: (id) kCFBooleanTrue forKey:(id) kSecReturnAttributes];
220 OSStatus status = SecItemCopyMatching((CFDictionaryRef) attributeQuery, (CFTypeRef *) &attributeResult);
222 [attributeResult release];
223 [attributeQuery release];
225 if (status != noErr) {
226 // No existing item found--simply return nil for the password
227 if (status != errSecItemNotFound) {
228 //Only return an error if a real exception happened--not simply for "not found."
229 if (error) *error = [NSError errorWithDomain: SFHFKeychainUtilsErrorDomain code: status userInfo: nil];
235 // We have an existing item, now query for the password data associated with it.
237 NSData *resultData = nil;
238 NSMutableDictionary *passwordQuery = [query mutableCopy];
239 [passwordQuery setObject: (id) kCFBooleanTrue forKey: (id) kSecReturnData];
241 status = SecItemCopyMatching((CFDictionaryRef) passwordQuery, (CFTypeRef *) &resultData);
243 [resultData autorelease];
244 [passwordQuery release];
246 if (status != noErr) {
247 if (status == errSecItemNotFound) {
248 // We found attributes for the item previously, but no password now, so return a special error.
249 // Users of this API will probably want to detect this error and prompt the user to
250 // re-enter their credentials. When you attempt to store the re-entered credentials
251 // using storeUsername:andPassword:forServiceName:updateExisting:error
252 // the old, incorrect entry will be deleted and a new one with a properly encrypted
253 // password will be added.
254 if (error) *error = [NSError errorWithDomain: SFHFKeychainUtilsErrorDomain code: -1999 userInfo: nil];
257 // Something else went wrong. Simply return the normal Keychain API error code.
258 if (error) *error = [NSError errorWithDomain: SFHFKeychainUtilsErrorDomain code: status userInfo: nil];
264 NSString *password = nil;
267 password = [[NSString alloc] initWithData: resultData encoding: NSUTF8StringEncoding];
270 // There is an existing item, but we weren't able to get password data for it for some reason,
271 // Possibly as a result of an item being incorrectly entered by the previous code.
272 // Set the -1999 error so the code above us can prompt the user again.
273 if (error) *error = [NSError errorWithDomain: SFHFKeychainUtilsErrorDomain code: -1999 userInfo: nil];
276 return [password autorelease];
279 + (BOOL) storeUsername: (NSString *) username andPassword: (NSString *) password forServiceName: (NSString *) serviceName updateExisting: (BOOL) updateExisting error: (NSError **) error {
280 if (!username || !password || !serviceName) {
281 if (error) *error = [NSError errorWithDomain: SFHFKeychainUtilsErrorDomain code: -2000 userInfo: nil];
285 // See if we already have a password entered for these credentials.
286 NSError *checkPasswordError = nil;
287 NSString *existingPassword = [SFHFKeychainUtils getPasswordForUsername: username andServiceName: serviceName error: &checkPasswordError];
289 if ([checkPasswordError code] == -1999) {
290 // There is an existing entry without a password properly stored (possibly as a result of the previous incorrect version of this code.
291 // Delete the existing item before moving on entering a correct one.
293 NSError *deleteError = nil;
295 [self deleteItemForUsername: username andServiceName: serviceName error: &deleteError];
297 if ([deleteError code] != noErr) {
298 if (error) *error = deleteError;
302 else if ([checkPasswordError code] != noErr) {
303 if (error) *error = checkPasswordError;
307 if (error) *error = nil;
309 OSStatus status = noErr;
311 if (existingPassword) {
312 // We have an existing, properly entered item with a password.
313 // Update the existing item.
315 if (![existingPassword isEqualToString:password] && updateExisting) {
316 //Only update if we're allowed to update existing. If not, simply do nothing.
318 NSArray *keys = [[[NSArray alloc] initWithObjects: (NSString *) kSecClass,
324 NSArray *objects = [[[NSArray alloc] initWithObjects: (NSString *) kSecClassGenericPassword,
330 NSDictionary *query = [[[NSDictionary alloc] initWithObjects: objects forKeys: keys] autorelease];
332 status = SecItemUpdate((CFDictionaryRef) query, (CFDictionaryRef) [NSDictionary dictionaryWithObject: [password dataUsingEncoding: NSUTF8StringEncoding] forKey: (NSString *) kSecValueData]);
336 // No existing entry (or an existing, improperly entered, and therefore now
337 // deleted, entry). Create a new entry.
339 NSArray *keys = [[[NSArray alloc] initWithObjects: (NSString *) kSecClass,
346 NSArray *objects = [[[NSArray alloc] initWithObjects: (NSString *) kSecClassGenericPassword,
350 [password dataUsingEncoding: NSUTF8StringEncoding],
353 NSDictionary *query = [[[NSDictionary alloc] initWithObjects: objects forKeys: keys] autorelease];
355 status = SecItemAdd((CFDictionaryRef) query, NULL);
358 if (status != noErr) {
359 // Something went wrong with adding the new item. Return the Keychain error code.
360 if (error) *error = [NSError errorWithDomain: SFHFKeychainUtilsErrorDomain code: status userInfo: nil];
366 + (BOOL) deleteItemForUsername: (NSString *) username andServiceName: (NSString *) serviceName error: (NSError **) error {
367 if (!username || !serviceName) {
368 if (error) *error = [NSError errorWithDomain: SFHFKeychainUtilsErrorDomain code: -2000 userInfo: nil];
372 if (error) *error = nil;
374 NSArray *keys = [[[NSArray alloc] initWithObjects: (NSString *) kSecClass, kSecAttrAccount, kSecAttrService, kSecReturnAttributes, nil] autorelease];
375 NSArray *objects = [[[NSArray alloc] initWithObjects: (NSString *) kSecClassGenericPassword, username, serviceName, kCFBooleanTrue, nil] autorelease];
377 NSDictionary *query = [[[NSDictionary alloc] initWithObjects: objects forKeys: keys] autorelease];
379 OSStatus status = SecItemDelete((CFDictionaryRef) query);
381 if (status != noErr) {
382 if (error) *error = [NSError errorWithDomain: SFHFKeychainUtilsErrorDomain code: status userInfo: nil];