SFHFKeychainUtils.m 15 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434
  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. #import "SFHFKeychainUtils.h"
  30. #import <Security/Security.h>
  31. static NSString *SFHFKeychainUtilsErrorDomain = @"SFHFKeychainUtilsErrorDomain";
  32. #if __IPHONE_OS_VERSION_MIN_REQUIRED < 30000 && TARGET_IPHONE_SIMULATOR
  33. @interface SFHFKeychainUtils (PrivateMethods)
  34. + (SecKeychainItemRef) getKeychainItemReferenceForUsername: (NSString *) username andServiceName: (NSString *) serviceName error: (NSError **) error;
  35. @end
  36. #endif
  37. @implementation SFHFKeychainUtils
  38. #if __IPHONE_OS_VERSION_MIN_REQUIRED < 30000 && TARGET_IPHONE_SIMULATOR
  39. + (NSString *) getPasswordForUsername: (NSString *) username andServiceName: (NSString *) serviceName error: (NSError **) error {
  40. if (!username || !serviceName) {
  41. *error = [NSError errorWithDomain: SFHFKeychainUtilsErrorDomain code: -2000 userInfo: nil];
  42. return nil;
  43. }
  44. SecKeychainItemRef item = [SFHFKeychainUtils getKeychainItemReferenceForUsername: username andServiceName: serviceName error: error];
  45. if (*error || !item) {
  46. return nil;
  47. }
  48. // from Advanced Mac OS X Programming, ch. 16
  49. UInt32 length;
  50. char *password;
  51. SecKeychainAttribute attributes[8];
  52. SecKeychainAttributeList list;
  53. attributes[0].tag = kSecAccountItemAttr;
  54. attributes[1].tag = kSecDescriptionItemAttr;
  55. attributes[2].tag = kSecLabelItemAttr;
  56. attributes[3].tag = kSecModDateItemAttr;
  57. list.count = 4;
  58. list.attr = attributes;
  59. OSStatus status = SecKeychainItemCopyContent(item, NULL, &list, &length, (void **)&password);
  60. if (status != noErr) {
  61. *error = [NSError errorWithDomain: SFHFKeychainUtilsErrorDomain code: status userInfo: nil];
  62. return nil;
  63. }
  64. NSString *passwordString = nil;
  65. if (password != NULL) {
  66. char passwordBuffer[1024];
  67. if (length > 1023) {
  68. length = 1023;
  69. }
  70. strncpy(passwordBuffer, password, length);
  71. passwordBuffer[length] = '\0';
  72. passwordString = [NSString stringWithCString:passwordBuffer];
  73. }
  74. SecKeychainItemFreeContent(&list, password);
  75. CFRelease(item);
  76. return passwordString;
  77. }
  78. + (void) storeUsername: (NSString *) username andPassword: (NSString *) password forServiceName: (NSString *) serviceName updateExisting: (BOOL) updateExisting error: (NSError **) error {
  79. if (!username || !password || !serviceName) {
  80. *error = [NSError errorWithDomain: SFHFKeychainUtilsErrorDomain code: -2000 userInfo: nil];
  81. return;
  82. }
  83. OSStatus status = noErr;
  84. SecKeychainItemRef item = [SFHFKeychainUtils getKeychainItemReferenceForUsername: username andServiceName: serviceName error: error];
  85. if (*error && [*error code] != noErr) {
  86. return;
  87. }
  88. *error = nil;
  89. if (item) {
  90. status = SecKeychainItemModifyAttributesAndData(item,
  91. NULL,
  92. strlen([password UTF8String]),
  93. [password UTF8String]);
  94. CFRelease(item);
  95. }
  96. else {
  97. status = SecKeychainAddGenericPassword(NULL,
  98. strlen([serviceName UTF8String]),
  99. [serviceName UTF8String],
  100. strlen([username UTF8String]),
  101. [username UTF8String],
  102. strlen([password UTF8String]),
  103. [password UTF8String],
  104. NULL);
  105. }
  106. if (status != noErr) {
  107. *error = [NSError errorWithDomain: SFHFKeychainUtilsErrorDomain code: status userInfo: nil];
  108. }
  109. }
  110. + (void) deleteItemForUsername: (NSString *) username andServiceName: (NSString *) serviceName error: (NSError **) error {
  111. if (!username || !serviceName) {
  112. *error = [NSError errorWithDomain: SFHFKeychainUtilsErrorDomain code: 2000 userInfo: nil];
  113. return;
  114. }
  115. *error = nil;
  116. SecKeychainItemRef item = [SFHFKeychainUtils getKeychainItemReferenceForUsername: username andServiceName: serviceName error: error];
  117. if (*error && [*error code] != noErr) {
  118. return;
  119. }
  120. OSStatus status;
  121. if (item) {
  122. status = SecKeychainItemDelete(item);
  123. CFRelease(item);
  124. }
  125. if (status != noErr) {
  126. *error = [NSError errorWithDomain: SFHFKeychainUtilsErrorDomain code: status userInfo: nil];
  127. }
  128. }
  129. + (SecKeychainItemRef) getKeychainItemReferenceForUsername: (NSString *) username andServiceName: (NSString *) serviceName error: (NSError **) error {
  130. if (!username || !serviceName) {
  131. *error = [NSError errorWithDomain: SFHFKeychainUtilsErrorDomain code: -2000 userInfo: nil];
  132. return nil;
  133. }
  134. *error = nil;
  135. SecKeychainItemRef item;
  136. OSStatus status = SecKeychainFindGenericPassword(NULL,
  137. strlen([serviceName UTF8String]),
  138. [serviceName UTF8String],
  139. strlen([username UTF8String]),
  140. [username UTF8String],
  141. NULL,
  142. NULL,
  143. &item);
  144. if (status != noErr) {
  145. if (status != errSecItemNotFound) {
  146. *error = [NSError errorWithDomain: SFHFKeychainUtilsErrorDomain code: status userInfo: nil];
  147. }
  148. return nil;
  149. }
  150. return item;
  151. }
  152. #else
  153. + (NSString *) getPasswordForUsername: (NSString *) username andServiceName: (NSString *) serviceName error: (NSError **) error {
  154. if (!username || !serviceName) {
  155. if (error != nil) {
  156. *error = [NSError errorWithDomain: SFHFKeychainUtilsErrorDomain code: -2000 userInfo: nil];
  157. }
  158. return nil;
  159. }
  160. if (error != nil) {
  161. *error = nil;
  162. }
  163. // Set up a query dictionary with the base query attributes: item type (generic), username, and service
  164. NSArray *keys = [[NSArray alloc] initWithObjects: (__bridge NSString *) kSecClass, kSecAttrAccount, kSecAttrService, nil];
  165. NSArray *objects = [[NSArray alloc] initWithObjects: (__bridge NSString *) kSecClassGenericPassword, username, serviceName, nil];
  166. NSMutableDictionary *query = [[NSMutableDictionary alloc] initWithObjects: objects forKeys: keys];
  167. // First do a query for attributes, in case we already have a Keychain item with no password data set.
  168. // One likely way such an incorrect item could have come about is due to the previous (incorrect)
  169. // version of this code (which set the password as a generic attribute instead of password data).
  170. NSDictionary *attributeResult = NULL;
  171. NSMutableDictionary *attributeQuery = [query mutableCopy];
  172. [attributeQuery setObject: (id) kCFBooleanTrue forKey:(__bridge id) kSecReturnAttributes];
  173. OSStatus status = SecItemCopyMatching((__bridge CFDictionaryRef) attributeQuery, (void*) &attributeResult);
  174. if (status != noErr) {
  175. // No existing item found--simply return nil for the password
  176. if (error != nil && status != errSecItemNotFound) {
  177. //Only return an error if a real exception happened--not simply for "not found."
  178. *error = [NSError errorWithDomain: SFHFKeychainUtilsErrorDomain code: status userInfo: nil];
  179. }
  180. return nil;
  181. }
  182. // We have an existing item, now query for the password data associated with it.
  183. NSData *resultData = nil;
  184. NSMutableDictionary *passwordQuery = [query mutableCopy];
  185. [passwordQuery setObject: (id) kCFBooleanTrue forKey: (__bridge id) kSecReturnData];
  186. status = SecItemCopyMatching((__bridge CFDictionaryRef) passwordQuery, (void*) &resultData);
  187. if (status != noErr) {
  188. if (status == errSecItemNotFound) {
  189. // We found attributes for the item previously, but no password now, so return a special error.
  190. // Users of this API will probably want to detect this error and prompt the user to
  191. // re-enter their credentials. When you attempt to store the re-entered credentials
  192. // using storeUsername:andPassword:forServiceName:updateExisting:error
  193. // the old, incorrect entry will be deleted and a new one with a properly encrypted
  194. // password will be added.
  195. if (error != nil) {
  196. *error = [NSError errorWithDomain: SFHFKeychainUtilsErrorDomain code: -1999 userInfo: nil];
  197. }
  198. }
  199. else {
  200. // Something else went wrong. Simply return the normal Keychain API error code.
  201. if (error != nil) {
  202. *error = [NSError errorWithDomain: SFHFKeychainUtilsErrorDomain code: status userInfo: nil];
  203. }
  204. }
  205. return nil;
  206. }
  207. NSString *password = nil;
  208. if (resultData) {
  209. password = [[NSString alloc] initWithData: resultData encoding: NSUTF8StringEncoding];
  210. }
  211. else {
  212. // There is an existing item, but we weren't able to get password data for it for some reason,
  213. // Possibly as a result of an item being incorrectly entered by the previous code.
  214. // Set the -1999 error so the code above us can prompt the user again.
  215. if (error != nil) {
  216. *error = [NSError errorWithDomain: SFHFKeychainUtilsErrorDomain code: -1999 userInfo: nil];
  217. }
  218. }
  219. return password;
  220. }
  221. + (BOOL) storeUsername: (NSString *) username andPassword: (NSString *) password forServiceName: (NSString *) serviceName updateExisting: (BOOL) updateExisting error: (NSError **) error
  222. {
  223. if (!username || !password || !serviceName)
  224. {
  225. if (error != nil)
  226. {
  227. *error = [NSError errorWithDomain: SFHFKeychainUtilsErrorDomain code: -2000 userInfo: nil];
  228. }
  229. return NO;
  230. }
  231. // See if we already have a password entered for these credentials.
  232. NSError *getError = nil;
  233. NSString *existingPassword = [SFHFKeychainUtils getPasswordForUsername: username andServiceName: serviceName error:&getError];
  234. if ([getError code] == -1999)
  235. {
  236. // There is an existing entry without a password properly stored (possibly as a result of the previous incorrect version of this code.
  237. // Delete the existing item before moving on entering a correct one.
  238. getError = nil;
  239. [self deleteItemForUsername: username andServiceName: serviceName error: &getError];
  240. if ([getError code] != noErr)
  241. {
  242. if (error != nil)
  243. {
  244. *error = getError;
  245. }
  246. return NO;
  247. }
  248. }
  249. else if ([getError code] != noErr)
  250. {
  251. if (error != nil)
  252. {
  253. *error = getError;
  254. }
  255. return NO;
  256. }
  257. if (error != nil)
  258. {
  259. *error = nil;
  260. }
  261. OSStatus status = noErr;
  262. if (existingPassword)
  263. {
  264. // We have an existing, properly entered item with a password.
  265. // Update the existing item.
  266. if (![existingPassword isEqualToString:password] && updateExisting)
  267. {
  268. //Only update if we're allowed to update existing. If not, simply do nothing.
  269. NSArray *keys = [[NSArray alloc] initWithObjects: (__bridge NSString *) kSecClass,
  270. kSecAttrService,
  271. kSecAttrLabel,
  272. kSecAttrAccount,
  273. nil];
  274. NSArray *objects = [[NSArray alloc] initWithObjects: (__bridge NSString *) kSecClassGenericPassword,
  275. serviceName,
  276. serviceName,
  277. username,
  278. nil];
  279. NSDictionary *query = [[NSDictionary alloc] initWithObjects: objects forKeys: keys];
  280. status = SecItemUpdate((__bridge CFDictionaryRef) query, (__bridge CFDictionaryRef) [NSDictionary dictionaryWithObject: [password dataUsingEncoding: NSUTF8StringEncoding] forKey: (__bridge NSString *) kSecValueData]);
  281. }
  282. }
  283. else
  284. {
  285. // No existing entry (or an existing, improperly entered, and therefore now
  286. // deleted, entry). Create a new entry.
  287. NSArray *keys = [[NSArray alloc] initWithObjects: (__bridge NSString *) kSecClass,
  288. kSecAttrService,
  289. kSecAttrLabel,
  290. kSecAttrAccount,
  291. kSecValueData,
  292. nil];
  293. NSArray *objects = [[NSArray alloc] initWithObjects: (__bridge NSString *) kSecClassGenericPassword,
  294. serviceName,
  295. serviceName,
  296. username,
  297. [password dataUsingEncoding: NSUTF8StringEncoding],
  298. nil];
  299. NSDictionary *query = [[NSDictionary alloc] initWithObjects: objects forKeys: keys];
  300. status = SecItemAdd((__bridge CFDictionaryRef) query, NULL);
  301. }
  302. if (status != noErr)
  303. {
  304. // Something went wrong with adding the new item. Return the Keychain error code.
  305. if (error != nil) {
  306. *error = [NSError errorWithDomain: SFHFKeychainUtilsErrorDomain code: status userInfo: nil];
  307. }
  308. return NO;
  309. }
  310. return YES;
  311. }
  312. + (BOOL) deleteItemForUsername: (NSString *) username andServiceName: (NSString *) serviceName error: (NSError **) error
  313. {
  314. if (!username || !serviceName)
  315. {
  316. if (error != nil)
  317. {
  318. *error = [NSError errorWithDomain: SFHFKeychainUtilsErrorDomain code: -2000 userInfo: nil];
  319. }
  320. return NO;
  321. }
  322. if (error != nil)
  323. {
  324. *error = nil;
  325. }
  326. NSArray *keys = [[NSArray alloc] initWithObjects: (__bridge NSString *) kSecClass, kSecAttrAccount, kSecAttrService, kSecReturnAttributes, nil];
  327. NSArray *objects = [[NSArray alloc] initWithObjects: (__bridge NSString *) kSecClassGenericPassword, username, serviceName, kCFBooleanTrue, nil];
  328. NSDictionary *query = [[NSDictionary alloc] initWithObjects: objects forKeys: keys];
  329. OSStatus status = SecItemDelete((__bridge CFDictionaryRef) query);
  330. if (status != noErr)
  331. {
  332. if (error != nil) {
  333. *error = [NSError errorWithDomain: SFHFKeychainUtilsErrorDomain code: status userInfo: nil];
  334. }
  335. return NO;
  336. }
  337. return YES;
  338. }
  339. #endif
  340. @end