FreeRDP
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 
33 static 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