FreeRDP
All Data Structures Namespaces Files Functions Variables Typedefs Enumerations Enumerator Modules Pages
SFHFKeychainUtils.m
1//
2// SFHFKeychainUtils.m
3//
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.
7//
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
15// conditions:
16//
17// The above copyright notice and this permission notice shall be
18// included in all copies or substantial portions of the Software.
19//
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.
28//
29
30#import "SFHFKeychainUtils.h"
31#import <Security/Security.h>
32
33static NSString *SFHFKeychainUtilsErrorDomain = @"SFHFKeychainUtilsErrorDomain";
34
35#if __IPHONE_OS_VERSION_MIN_REQUIRED < 30000 && TARGET_IPHONE_SIMULATOR
36@interface SFHFKeychainUtils (PrivateMethods)
37+ (SecKeychainItemRef)getKeychainItemReferenceForUsername:(NSString *)username
38 andServerName:(NSString *)serverName
39 error:(NSError **)error;
40@end
41#endif
42
43@implementation SFHFKeychainUtils
44
45#if __IPHONE_OS_VERSION_MIN_REQUIRED < 30000 && TARGET_IPHONE_SIMULATOR
46
47+ (NSString *)getPasswordForUsername:(NSString *)username
48 andServerName:(NSString *)serverName
49 error:(NSError **)error
50{
51 if (!username || !serviceName)
52 {
53 *error = [NSError errorWithDomain:SFHFKeychainUtilsErrorDomain code:-2000 userInfo:nil];
54 return nil;
55 }
56
57 SecKeychainItemRef item = [SFHFKeychainUtils getKeychainItemReferenceForUsername:username
58 andServerName:serverName
59 error:error];
60
61 if (*error || !item)
62 {
63 return nil;
64 }
65
66 // from Advanced Mac OS X Programming, ch. 16
67 UInt32 length;
68 char *password;
69 SecKeychainAttribute attributes[8];
70 SecKeychainAttributeList list;
71
72 attributes[0].tag = kSecAccountItemAttr;
73 attributes[1].tag = kSecDescriptionItemAttr;
74 attributes[2].tag = kSecLabelItemAttr;
75 attributes[3].tag = kSecModDateItemAttr;
76
77 list.count = 4;
78 list.attr = attributes;
79
80 OSStatus status = SecKeychainItemCopyContent(item, NULL, &list, &length, (void **)&password);
81
82 if (status != noErr)
83 {
84 *error = [NSError errorWithDomain:SFHFKeychainUtilsErrorDomain code:status userInfo:nil];
85 return nil;
86 }
87
88 NSString *passwordString = nil;
89
90 if (password != NULL)
91 {
92 char passwordBuffer[1024];
93
94 if (length > 1023)
95 {
96 length = 1023;
97 }
98 strncpy(passwordBuffer, password, length);
99
100 passwordBuffer[length] = '\0';
101 passwordString = [NSString stringWithCString:passwordBuffer encoding:NSUTF8StringEncoding];
102 }
103
104 SecKeychainItemFreeContent(&list, password);
105
106 CFRelease(item);
107
108 return passwordString;
109}
110
111+ (void)storeUsername:(NSString *)username
112 andPassword:(NSString *)password
113 forServerName:(NSString *)serverName
114 updateExisting:(BOOL)updateExisting
115 error:(NSError **)error
116{
117 if (!username || !password || !serverName)
118 {
119 *error = [NSError errorWithDomain:SFHFKeychainUtilsErrorDomain code:-2000 userInfo:nil];
120 return;
121 }
122
123 OSStatus status = noErr;
124
125 SecKeychainItemRef item = [SFHFKeychainUtils getKeychainItemReferenceForUsername:username
126 andServerName:serverName
127 error:error];
128
129 if (*error && [*error code] != noErr)
130 {
131 return;
132 }
133
134 *error = nil;
135
136 if (item)
137 {
138 status = SecKeychainItemModifyAttributesAndData(item, NULL, strlen([password UTF8String]),
139 [password UTF8String]);
140
141 CFRelease(item);
142 }
143 else
144 {
145 status = SecKeychainAddGenericPassword(
146 NULL, strlen([serverName UTF8String]), [serverName UTF8String],
147 strlen([username UTF8String]), [username UTF8String], strlen([password UTF8String]),
148 [password UTF8String], NULL);
149 }
150
151 if (status != noErr)
152 {
153 *error = [NSError errorWithDomain:SFHFKeychainUtilsErrorDomain code:status userInfo:nil];
154 }
155}
156
157+ (void)deleteItemForUsername:(NSString *)username
158 andServerName:(NSString *)serverName
159 error:(NSError **)error
160{
161 if (!username || !serverName)
162 {
163 *error = [NSError errorWithDomain:SFHFKeychainUtilsErrorDomain code:2000 userInfo:nil];
164 return;
165 }
166
167 *error = nil;
168
169 SecKeychainItemRef item = [SFHFKeychainUtils getKeychainItemReferenceForUsername:username
170 andServerName:serverName
171 error:error];
172
173 if (*error && [*error code] != noErr)
174 {
175 return;
176 }
177
178 OSStatus status;
179
180 if (item)
181 {
182 status = SecKeychainItemDelete(item);
183
184 CFRelease(item);
185 }
186
187 if (status != noErr)
188 {
189 *error = [NSError errorWithDomain:SFHFKeychainUtilsErrorDomain code:status userInfo:nil];
190 }
191}
192
193+ (SecKeychainItemRef)getKeychainItemReferenceForUsername:(NSString *)username
194 andServerName:(NSString *)serverName
195 error:(NSError **)error
196{
197 if (!username || !serverName)
198 {
199 *error = [NSError errorWithDomain:SFHFKeychainUtilsErrorDomain code:-2000 userInfo:nil];
200 return nil;
201 }
202
203 *error = nil;
204
205 SecKeychainItemRef item;
206
207 OSStatus status = SecKeychainFindGenericPassword(
208 NULL, strlen([serverName UTF8String]), [serverName UTF8String],
209 strlen([username UTF8String]), [username UTF8String], NULL, NULL, &item);
210
211 if (status != noErr)
212 {
213 if (status != errSecItemNotFound)
214 {
215 *error = [NSError errorWithDomain:SFHFKeychainUtilsErrorDomain
216 code:status
217 userInfo:nil];
218 }
219
220 return nil;
221 }
222
223 return item;
224}
225
226#else
227
228+ (NSString *)getPasswordForUsername:(NSString *)username
229 andServerName:(NSString *)serverName
230 error:(NSError **)error
231{
232 if (!username || !serverName)
233 {
234 if (error != nil)
235 {
236 *error = [NSError errorWithDomain:SFHFKeychainUtilsErrorDomain code:-2000 userInfo:nil];
237 }
238 return nil;
239 }
240
241 if (error != nil)
242 {
243 *error = nil;
244 }
245
246 // Set up a query dictionary with the base query attributes: item type (generic), username, and
247 // service
248
249 NSArray *keys = [[[NSArray alloc]
250 initWithObjects:(NSString *)kSecClass, kSecAttrAccount, kSecAttrService, nil] autorelease];
251 NSArray *objects = [[[NSArray alloc] initWithObjects:(NSString *)kSecClassGenericPassword,
252 username, serverName, nil] autorelease];
253
254 NSMutableDictionary *query = [[[NSMutableDictionary alloc] initWithObjects:objects
255 forKeys:keys] autorelease];
256
257 // First do a query for attributes, in case we already have a Keychain item with no password
258 // data set. One likely way such an incorrect item could have come about is due to the previous
259 // (incorrect) version of this code (which set the password as a generic attribute instead of
260 // password data).
261
262 NSDictionary *attributeResult = NULL;
263 NSMutableDictionary *attributeQuery = [query mutableCopy];
264 [attributeQuery setObject:(id)kCFBooleanTrue forKey:(id)kSecReturnAttributes];
265 OSStatus status =
266 SecItemCopyMatching((CFDictionaryRef)attributeQuery, (CFTypeRef *)&attributeResult);
267
268 [attributeResult release];
269 [attributeQuery release];
270
271 if (status != noErr)
272 {
273 // No existing item found--simply return nil for the password
274 if (error != nil && status != errSecItemNotFound)
275 {
276 // Only return an error if a real exception happened--not simply for "not found."
277 *error = [NSError errorWithDomain:SFHFKeychainUtilsErrorDomain
278 code:status
279 userInfo:nil];
280 }
281
282 return nil;
283 }
284
285 // We have an existing item, now query for the password data associated with it.
286
287 NSData *resultData = nil;
288 NSMutableDictionary *passwordQuery = [query mutableCopy];
289 [passwordQuery setObject:(id)kCFBooleanTrue forKey:(id)kSecReturnData];
290
291 status = SecItemCopyMatching((CFDictionaryRef)passwordQuery, (CFTypeRef *)&resultData);
292
293 [resultData autorelease];
294 [passwordQuery release];
295
296 if (status != noErr)
297 {
298 if (status == errSecItemNotFound)
299 {
300 // We found attributes for the item previously, but no password now, so return a special
301 // error. Users of this API will probably want to detect this error and prompt the user
302 // to re-enter their credentials. When you attempt to store the re-entered credentials
303 // using storeUsername:andPassword:forServiceName:updateExisting:error
304 // the old, incorrect entry will be deleted and a new one with a properly encrypted
305 // password will be added.
306 if (error != nil)
307 {
308 *error = [NSError errorWithDomain:SFHFKeychainUtilsErrorDomain
309 code:-1999
310 userInfo:nil];
311 }
312 }
313 else
314 {
315 // Something else went wrong. Simply return the normal Keychain API error code.
316 if (error != nil)
317 {
318 *error = [NSError errorWithDomain:SFHFKeychainUtilsErrorDomain
319 code:status
320 userInfo:nil];
321 }
322 }
323
324 return nil;
325 }
326
327 NSString *password = nil;
328
329 if (resultData)
330 {
331 password = [[NSString alloc] initWithData:resultData encoding:NSUTF8StringEncoding];
332 }
333 else
334 {
335 // There is an existing item, but we weren't able to get password data for it for some
336 // reason, Possibly as a result of an item being incorrectly entered by the previous code.
337 // Set the -1999 error so the code above us can prompt the user again.
338 if (error != nil)
339 {
340 *error = [NSError errorWithDomain:SFHFKeychainUtilsErrorDomain code:-1999 userInfo:nil];
341 }
342 }
343
344 return [password autorelease];
345}
346
347+ (BOOL)storeUsername:(NSString *)username
348 andPassword:(NSString *)password
349 forServerName:(NSString *)serverName
350 updateExisting:(BOOL)updateExisting
351 error:(NSError **)error
352{
353 if (!username || !password || !serverName)
354 {
355 if (error != nil)
356 {
357 *error = [NSError errorWithDomain:SFHFKeychainUtilsErrorDomain code:-2000 userInfo:nil];
358 }
359 return NO;
360 }
361
362 // See if we already have a password entered for these credentials.
363 NSError *getError = nil;
364 NSString *existingPassword = [SFHFKeychainUtils getPasswordForUsername:username
365 andServerName:serverName
366 error:&getError];
367
368 if ([getError code] == -1999)
369 {
370 // There is an existing entry without a password properly stored (possibly as a result of
371 // the previous incorrect version of this code. Delete the existing item before moving on
372 // entering a correct one.
373
374 getError = nil;
375
376 [self deleteItemForUsername:username andServerName:serverName error:&getError];
377
378 if ([getError code] != noErr)
379 {
380 if (error != nil)
381 {
382 *error = getError;
383 }
384 return NO;
385 }
386 }
387 else if ([getError code] != noErr)
388 {
389 if (error != nil)
390 {
391 *error = getError;
392 }
393 return NO;
394 }
395
396 if (error != nil)
397 {
398 *error = nil;
399 }
400
401 OSStatus status = noErr;
402
403 if (existingPassword)
404 {
405 // We have an existing, properly entered item with a password.
406 // Update the existing item.
407
408 if (![existingPassword isEqualToString:password] && updateExisting)
409 {
410 // Only update if we're allowed to update existing. If not, simply do nothing.
411
412 NSArray *keys =
413 [[[NSArray alloc] initWithObjects:(NSString *)kSecClass, kSecAttrService,
414 kSecAttrLabel, kSecAttrAccount, nil] autorelease];
415
416 NSArray *objects =
417 [[[NSArray alloc] initWithObjects:(NSString *)kSecClassGenericPassword, serverName,
418 serverName, username, nil] autorelease];
419
420 NSDictionary *query = [[[NSDictionary alloc] initWithObjects:objects
421 forKeys:keys] autorelease];
422
423 status = SecItemUpdate(
424 (CFDictionaryRef)query,
425 (CFDictionaryRef)[NSDictionary
426 dictionaryWithObject:[password dataUsingEncoding:NSUTF8StringEncoding]
427 forKey:(NSString *)kSecValueData]);
428 }
429 }
430 else
431 {
432 // No existing entry (or an existing, improperly entered, and therefore now
433 // deleted, entry). Create a new entry.
434
435 NSArray *keys =
436 [[[NSArray alloc] initWithObjects:(NSString *)kSecClass, kSecAttrService, kSecAttrLabel,
437 kSecAttrAccount, kSecValueData, nil] autorelease];
438
439 NSArray *objects = [[[NSArray alloc]
440 initWithObjects:(NSString *)kSecClassGenericPassword, serverName, serverName, username,
441 [password dataUsingEncoding:NSUTF8StringEncoding], nil] autorelease];
442
443 NSDictionary *query = [[[NSDictionary alloc] initWithObjects:objects
444 forKeys:keys] autorelease];
445
446 status = SecItemAdd((CFDictionaryRef)query, NULL);
447 }
448
449 if (error != nil && status != noErr)
450 {
451 // Something went wrong with adding the new item. Return the Keychain error code.
452 *error = [NSError errorWithDomain:SFHFKeychainUtilsErrorDomain code:status userInfo:nil];
453
454 return NO;
455 }
456
457 return YES;
458}
459
460+ (BOOL)deleteItemForUsername:(NSString *)username
461 andServerName:(NSString *)serverName
462 error:(NSError **)error
463{
464 if (!username || !serverName)
465 {
466 if (error != nil)
467 {
468 *error = [NSError errorWithDomain:SFHFKeychainUtilsErrorDomain code:-2000 userInfo:nil];
469 }
470 return NO;
471 }
472
473 if (error != nil)
474 {
475 *error = nil;
476 }
477
478 NSArray *keys =
479 [[[NSArray alloc] initWithObjects:(NSString *)kSecClass, kSecAttrAccount, kSecAttrService,
480 kSecReturnAttributes, nil] autorelease];
481 NSArray *objects =
482 [[[NSArray alloc] initWithObjects:(NSString *)kSecClassGenericPassword, username,
483 serverName, kCFBooleanTrue, nil] autorelease];
484
485 NSDictionary *query = [[[NSDictionary alloc] initWithObjects:objects forKeys:keys] autorelease];
486
487 OSStatus status = SecItemDelete((CFDictionaryRef)query);
488
489 if (error != nil && status != noErr)
490 {
491 *error = [NSError errorWithDomain:SFHFKeychainUtilsErrorDomain code:status userInfo:nil];
492
493 return NO;
494 }
495
496 return YES;
497}
498
499#endif
500
501@end