FLEXKeychainQuery.m 9.3 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317
  1. //
  2. // SSKeychainQuery.m
  3. // SSKeychain
  4. //
  5. // Created by Caleb Davenport on 3/19/13.
  6. // Copyright (c) 2013-2014 Sam Soffes. All rights reserved.
  7. //
  8. #import "FLEXKeychainQuery.h"
  9. #import "FLEXKeychain.h"
  10. @implementation FLEXKeychainQuery
  11. @synthesize account = _account;
  12. @synthesize service = _service;
  13. @synthesize label = _label;
  14. @synthesize passwordData = _passwordData;
  15. #ifdef SSKEYCHAIN_ACCESS_GROUP_AVAILABLE
  16. @synthesize accessGroup = _accessGroup;
  17. #endif
  18. #ifdef SSKEYCHAIN_SYNCHRONIZATION_AVAILABLE
  19. @synthesize synchronizationMode = _synchronizationMode;
  20. #endif
  21. #pragma mark - Public
  22. - (BOOL)save:(NSError *__autoreleasing *)error {
  23. OSStatus status = SSKeychainErrorBadArguments;
  24. if (!self.service || !self.account || !self.passwordData) {
  25. if (error) {
  26. *error = [[self class] errorWithCode:status];
  27. }
  28. return NO;
  29. }
  30. NSMutableDictionary *query = nil;
  31. NSMutableDictionary * searchQuery = [self query];
  32. status = SecItemCopyMatching((__bridge CFDictionaryRef)searchQuery, nil);
  33. if (status == errSecSuccess) {//item already exists, update it!
  34. query = [[NSMutableDictionary alloc]init];
  35. [query setObject:self.passwordData forKey:(__bridge id)kSecValueData];
  36. #if __IPHONE_4_0 && TARGET_OS_IPHONE
  37. CFTypeRef accessibilityType = [FLEXKeychain accessibilityType];
  38. if (accessibilityType) {
  39. [query setObject:(__bridge id)accessibilityType forKey:(__bridge id)kSecAttrAccessible];
  40. }
  41. #endif
  42. status = SecItemUpdate((__bridge CFDictionaryRef)(searchQuery), (__bridge CFDictionaryRef)(query));
  43. }else if(status == errSecItemNotFound){//item not found, create it!
  44. query = [self query];
  45. if (self.label) {
  46. [query setObject:self.label forKey:(__bridge id)kSecAttrLabel];
  47. }
  48. [query setObject:self.passwordData forKey:(__bridge id)kSecValueData];
  49. #if __IPHONE_4_0 && TARGET_OS_IPHONE
  50. CFTypeRef accessibilityType = [FLEXKeychain accessibilityType];
  51. if (accessibilityType) {
  52. [query setObject:(__bridge id)accessibilityType forKey:(__bridge id)kSecAttrAccessible];
  53. }
  54. #endif
  55. status = SecItemAdd((__bridge CFDictionaryRef)query, NULL);
  56. }
  57. if (status != errSecSuccess && error != NULL) {
  58. *error = [[self class] errorWithCode:status];
  59. }
  60. return (status == errSecSuccess);}
  61. - (BOOL)deleteItem:(NSError *__autoreleasing *)error {
  62. OSStatus status = SSKeychainErrorBadArguments;
  63. if (!self.service || !self.account) {
  64. if (error) {
  65. *error = [[self class] errorWithCode:status];
  66. }
  67. return NO;
  68. }
  69. NSMutableDictionary *query = [self query];
  70. #if TARGET_OS_IPHONE
  71. status = SecItemDelete((__bridge CFDictionaryRef)query);
  72. #else
  73. // On Mac OS, SecItemDelete will not delete a key created in a different
  74. // app, nor in a different version of the same app.
  75. //
  76. // To replicate the issue, save a password, change to the code and
  77. // rebuild the app, and then attempt to delete that password.
  78. //
  79. // This was true in OS X 10.6 and probably later versions as well.
  80. //
  81. // Work around it by using SecItemCopyMatching and SecKeychainItemDelete.
  82. CFTypeRef result = NULL;
  83. [query setObject:@YES forKey:(__bridge id)kSecReturnRef];
  84. status = SecItemCopyMatching((__bridge CFDictionaryRef)query, &result);
  85. if (status == errSecSuccess) {
  86. status = SecKeychainItemDelete((SecKeychainItemRef)result);
  87. CFRelease(result);
  88. }
  89. #endif
  90. if (status != errSecSuccess && error != NULL) {
  91. *error = [[self class] errorWithCode:status];
  92. }
  93. return (status == errSecSuccess);
  94. }
  95. - (NSArray *)fetchAll:(NSError *__autoreleasing *)error {
  96. NSMutableDictionary *query = [self query];
  97. [query setObject:@YES forKey:(__bridge id)kSecReturnAttributes];
  98. [query setObject:(__bridge id)kSecMatchLimitAll forKey:(__bridge id)kSecMatchLimit];
  99. #if __IPHONE_4_0 && TARGET_OS_IPHONE
  100. CFTypeRef accessibilityType = [FLEXKeychain accessibilityType];
  101. if (accessibilityType) {
  102. [query setObject:(__bridge id)accessibilityType forKey:(__bridge id)kSecAttrAccessible];
  103. }
  104. #endif
  105. CFTypeRef result = NULL;
  106. OSStatus status = SecItemCopyMatching((__bridge CFDictionaryRef)query, &result);
  107. if (status != errSecSuccess && error != NULL) {
  108. *error = [[self class] errorWithCode:status];
  109. return nil;
  110. }
  111. return (__bridge_transfer NSArray *)result;
  112. }
  113. - (BOOL)fetch:(NSError *__autoreleasing *)error {
  114. OSStatus status = SSKeychainErrorBadArguments;
  115. if (!self.service || !self.account) {
  116. if (error) {
  117. *error = [[self class] errorWithCode:status];
  118. }
  119. return NO;
  120. }
  121. CFTypeRef result = NULL;
  122. NSMutableDictionary *query = [self query];
  123. [query setObject:@YES forKey:(__bridge id)kSecReturnData];
  124. [query setObject:(__bridge id)kSecMatchLimitOne forKey:(__bridge id)kSecMatchLimit];
  125. status = SecItemCopyMatching((__bridge CFDictionaryRef)query, &result);
  126. if (status != errSecSuccess) {
  127. if (error) {
  128. *error = [[self class] errorWithCode:status];
  129. }
  130. return NO;
  131. }
  132. self.passwordData = (__bridge_transfer NSData *)result;
  133. return YES;
  134. }
  135. #pragma mark - Accessors
  136. - (void)setPasswordObject:(id<NSCoding>)object {
  137. self.passwordData = [NSKeyedArchiver archivedDataWithRootObject:object];
  138. }
  139. - (id<NSCoding>)passwordObject {
  140. if ([self.passwordData length]) {
  141. return [NSKeyedUnarchiver unarchiveObjectWithData:self.passwordData];
  142. }
  143. return nil;
  144. }
  145. - (void)setPassword:(NSString *)password {
  146. self.passwordData = [password dataUsingEncoding:NSUTF8StringEncoding];
  147. }
  148. - (NSString *)password {
  149. if ([self.passwordData length]) {
  150. return [[NSString alloc] initWithData:self.passwordData encoding:NSUTF8StringEncoding];
  151. }
  152. return nil;
  153. }
  154. #pragma mark - Synchronization Status
  155. #ifdef SSKEYCHAIN_SYNCHRONIZATION_AVAILABLE
  156. + (BOOL)isSynchronizationAvailable {
  157. #if TARGET_OS_IPHONE
  158. // Apple suggested way to check for 7.0 at runtime
  159. // https://developer.apple.com/library/ios/documentation/userexperience/conceptual/transitionguide/SupportingEarlieriOS.html
  160. return floor(NSFoundationVersionNumber) > NSFoundationVersionNumber_iOS_6_1;
  161. #else
  162. return floor(NSFoundationVersionNumber) > NSFoundationVersionNumber10_8_4;
  163. #endif
  164. }
  165. #endif
  166. #pragma mark - Private
  167. - (NSMutableDictionary *)query {
  168. NSMutableDictionary *dictionary = [NSMutableDictionary dictionaryWithCapacity:3];
  169. [dictionary setObject:(__bridge id)kSecClassGenericPassword forKey:(__bridge id)kSecClass];
  170. if (self.service) {
  171. [dictionary setObject:self.service forKey:(__bridge id)kSecAttrService];
  172. }
  173. if (self.account) {
  174. [dictionary setObject:self.account forKey:(__bridge id)kSecAttrAccount];
  175. }
  176. #ifdef SSKEYCHAIN_ACCESS_GROUP_AVAILABLE
  177. #if !TARGET_IPHONE_SIMULATOR
  178. if (self.accessGroup) {
  179. [dictionary setObject:self.accessGroup forKey:(__bridge id)kSecAttrAccessGroup];
  180. }
  181. #endif
  182. #endif
  183. #ifdef SSKEYCHAIN_SYNCHRONIZATION_AVAILABLE
  184. if ([[self class] isSynchronizationAvailable]) {
  185. id value;
  186. switch (self.synchronizationMode) {
  187. case SSKeychainQuerySynchronizationModeNo: {
  188. value = @NO;
  189. break;
  190. }
  191. case SSKeychainQuerySynchronizationModeYes: {
  192. value = @YES;
  193. break;
  194. }
  195. case SSKeychainQuerySynchronizationModeAny: {
  196. value = (__bridge id)(kSecAttrSynchronizableAny);
  197. break;
  198. }
  199. }
  200. [dictionary setObject:value forKey:(__bridge id)(kSecAttrSynchronizable)];
  201. }
  202. #endif
  203. return dictionary;
  204. }
  205. + (NSError *)errorWithCode:(OSStatus) code {
  206. static dispatch_once_t onceToken;
  207. static NSBundle *resourcesBundle = nil;
  208. dispatch_once(&onceToken, ^{
  209. NSURL *url = [[NSBundle bundleForClass:[self class]] URLForResource:@"SSKeychain" withExtension:@"bundle"];
  210. resourcesBundle = [NSBundle bundleWithURL:url];
  211. });
  212. NSString *message = nil;
  213. switch (code) {
  214. case errSecSuccess: return nil;
  215. case SSKeychainErrorBadArguments: message = NSLocalizedStringFromTableInBundle(@"SSKeychainErrorBadArguments", @"SSKeychain", resourcesBundle, nil); break;
  216. #if TARGET_OS_IPHONE
  217. case errSecUnimplemented: {
  218. message = NSLocalizedStringFromTableInBundle(@"errSecUnimplemented", @"SSKeychain", resourcesBundle, nil);
  219. break;
  220. }
  221. case errSecParam: {
  222. message = NSLocalizedStringFromTableInBundle(@"errSecParam", @"SSKeychain", resourcesBundle, nil);
  223. break;
  224. }
  225. case errSecAllocate: {
  226. message = NSLocalizedStringFromTableInBundle(@"errSecAllocate", @"SSKeychain", resourcesBundle, nil);
  227. break;
  228. }
  229. case errSecNotAvailable: {
  230. message = NSLocalizedStringFromTableInBundle(@"errSecNotAvailable", @"SSKeychain", resourcesBundle, nil);
  231. break;
  232. }
  233. case errSecDuplicateItem: {
  234. message = NSLocalizedStringFromTableInBundle(@"errSecDuplicateItem", @"SSKeychain", resourcesBundle, nil);
  235. break;
  236. }
  237. case errSecItemNotFound: {
  238. message = NSLocalizedStringFromTableInBundle(@"errSecItemNotFound", @"SSKeychain", resourcesBundle, nil);
  239. break;
  240. }
  241. case errSecInteractionNotAllowed: {
  242. message = NSLocalizedStringFromTableInBundle(@"errSecInteractionNotAllowed", @"SSKeychain", resourcesBundle, nil);
  243. break;
  244. }
  245. case errSecDecode: {
  246. message = NSLocalizedStringFromTableInBundle(@"errSecDecode", @"SSKeychain", resourcesBundle, nil);
  247. break;
  248. }
  249. case errSecAuthFailed: {
  250. message = NSLocalizedStringFromTableInBundle(@"errSecAuthFailed", @"SSKeychain", resourcesBundle, nil);
  251. break;
  252. }
  253. default: {
  254. message = NSLocalizedStringFromTableInBundle(@"errSecDefault", @"SSKeychain", resourcesBundle, nil);
  255. }
  256. #else
  257. default:
  258. message = (__bridge_transfer NSString *)SecCopyErrorMessageString(code, NULL);
  259. #endif
  260. }
  261. NSDictionary *userInfo = nil;
  262. if (message) {
  263. userInfo = @{ NSLocalizedDescriptionKey : message };
  264. }
  265. return [NSError errorWithDomain:kSSKeychainErrorDomain code:code userInfo:userInfo];
  266. }
  267. @end