FIRAuthKeychainServicesTests.m 13 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344
  1. /*
  2. * Copyright 2017 Google
  3. *
  4. * Licensed under the Apache License, Version 2.0 (the "License");
  5. * you may not use this file except in compliance with the License.
  6. * You may obtain a copy of the License at
  7. *
  8. * http://www.apache.org/licenses/LICENSE-2.0
  9. *
  10. * Unless required by applicable law or agreed to in writing, software
  11. * distributed under the License is distributed on an "AS IS" BASIS,
  12. * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  13. * See the License for the specific language governing permissions and
  14. * limitations under the License.
  15. */
  16. #import <Security/Security.h>
  17. #import <XCTest/XCTest.h>
  18. #import "FirebaseAuth/Sources/Storage/FIRAuthKeychainServices.h"
  19. /** @var kAccountPrefix
  20. @brief The keychain account prefix assumed by the tests.
  21. */
  22. static NSString *const kAccountPrefix = @"firebase_auth_1_";
  23. /** @var kKey
  24. @brief The key used in tests.
  25. */
  26. static NSString *const kKey = @"ACCOUNT";
  27. /** @var kService
  28. @brief The keychain service used in tests.
  29. */
  30. static NSString *const kService = @"SERVICE";
  31. /** @var kOtherService
  32. @brief Another keychain service used in tests.
  33. */
  34. static NSString *const kOtherService = @"OTHER_SERVICE";
  35. /** @var kData
  36. @brief A piece of keychain data used in tests.
  37. */
  38. static NSString *const kData = @"DATA";
  39. /** @var kOtherData
  40. @brief Another piece of keychain data used in tests.
  41. */
  42. static NSString *const kOtherData = @"OTHER_DATA";
  43. /** @fn accountFromKey
  44. @brief Converts a key string to an account string.
  45. @param key The key string to be converted from.
  46. @return The account string being the conversion result.
  47. */
  48. static NSString *accountFromKey(NSString *key) {
  49. return [kAccountPrefix stringByAppendingString:key];
  50. }
  51. /** @fn dataFromString
  52. @brief Converts a NSString to NSData.
  53. @param string The NSString to be converted from.
  54. @return The NSData being the conversion result.
  55. */
  56. static NSData *dataFromString(NSString *string) {
  57. return [string dataUsingEncoding:NSUTF8StringEncoding];
  58. }
  59. /** @fn stringFromData
  60. @brief Converts a NSData to NSString.
  61. @param data The NSData to be converted from.
  62. @return The NSString being the conversion result.
  63. */
  64. static NSString *stringFromData(NSData *data) {
  65. return [[NSString alloc] initWithData:data encoding:NSUTF8StringEncoding];
  66. }
  67. /** @fn fakeError
  68. @brief Creates a fake error object.
  69. @return a non-nil NSError instance.
  70. */
  71. static NSError *fakeError() {
  72. return [NSError errorWithDomain:@"ERROR" code:-1 userInfo:nil];
  73. }
  74. @interface FIRAuthKeychainServices ()
  75. // Exposed for testing.
  76. - (nullable NSData *)itemWithQuery:(NSDictionary *)query error:(NSError **_Nullable)error;
  77. @end
  78. /** @class FIRAuthKeychainTests
  79. @brief Tests for @c FIRAuthKeychainTests .
  80. */
  81. @interface FIRAuthKeychainTests : XCTestCase
  82. @end
  83. @implementation FIRAuthKeychainTests
  84. /** @fn testReadNonexisting
  85. @brief Tests reading non-existing keychain item.
  86. */
  87. - (void)testReadNonexisting {
  88. [self setPassword:nil account:accountFromKey(kKey) service:kService];
  89. [self setPassword:nil account:kKey service:nil]; // legacy form
  90. FIRAuthKeychainServices *keychain = [[FIRAuthKeychainServices alloc] initWithService:kService];
  91. NSError *error = fakeError();
  92. XCTAssertNil([keychain dataForKey:kKey error:&error]);
  93. XCTAssertNil(error);
  94. }
  95. /** @fn testReadExisting
  96. @brief Tests reading existing keychain item.
  97. */
  98. - (void)testReadExisting {
  99. [self setPassword:kData account:accountFromKey(kKey) service:kService];
  100. FIRAuthKeychainServices *keychain = [[FIRAuthKeychainServices alloc] initWithService:kService];
  101. NSError *error = fakeError();
  102. XCTAssertEqualObjects([keychain dataForKey:kKey error:&error], dataFromString(kData));
  103. XCTAssertNil(error);
  104. [self deletePasswordWithAccount:accountFromKey(kKey) service:kService];
  105. }
  106. /** @fn testReadMultiple
  107. @brief Tests reading multiple items from keychain returns only the first item.
  108. */
  109. - (void)testReadMultiple {
  110. [self addPassword:kData account:accountFromKey(kKey) service:kService];
  111. [self addPassword:kOtherData account:accountFromKey(kKey) service:kOtherService];
  112. FIRAuthKeychainServices *keychain = [[FIRAuthKeychainServices alloc] initWithService:kService];
  113. NSString *queriedAccount = accountFromKey(kKey);
  114. NSDictionary *query = @{
  115. (__bridge id)kSecClass : (__bridge id)kSecClassGenericPassword,
  116. (__bridge id)kSecAttrAccount : queriedAccount,
  117. };
  118. NSError *error = fakeError();
  119. // Keychain on macOS returns items in a different order than keychain on iOS,
  120. // so test that the returned object is one of any of the added objects.
  121. NSData *queriedData = [keychain itemWithQuery:query error:&error];
  122. BOOL isValidKeychainItem =
  123. [@[ dataFromString(kData), dataFromString(kOtherData) ] containsObject:queriedData];
  124. XCTAssertTrue(isValidKeychainItem);
  125. XCTAssertNil(error);
  126. [self deletePasswordWithAccount:accountFromKey(kKey) service:kService];
  127. [self deletePasswordWithAccount:accountFromKey(kKey) service:kOtherService];
  128. }
  129. /** @fn testNotReadOtherService
  130. @brief Tests not reading keychain item belonging to other service.
  131. */
  132. - (void)testNotReadOtherService {
  133. [self setPassword:nil account:accountFromKey(kKey) service:kService];
  134. [self setPassword:kData account:accountFromKey(kKey) service:kOtherService];
  135. FIRAuthKeychainServices *keychain = [[FIRAuthKeychainServices alloc] initWithService:kService];
  136. NSError *error = fakeError();
  137. XCTAssertNil([keychain dataForKey:kKey error:&error]);
  138. XCTAssertNil(error);
  139. [self deletePasswordWithAccount:accountFromKey(kKey) service:kOtherService];
  140. }
  141. /** @fn testWriteNonexisting
  142. @brief Tests writing new keychain item.
  143. */
  144. - (void)testWriteNonexisting {
  145. [self setPassword:nil account:accountFromKey(kKey) service:kService];
  146. FIRAuthKeychainServices *keychain = [[FIRAuthKeychainServices alloc] initWithService:kService];
  147. XCTAssertTrue([keychain setData:dataFromString(kData) forKey:kKey error:NULL]);
  148. XCTAssertEqualObjects([self passwordWithAccount:accountFromKey(kKey) service:kService], kData);
  149. [self deletePasswordWithAccount:accountFromKey(kKey) service:kService];
  150. }
  151. /** @fn testWriteExisting
  152. @brief Tests overwriting existing keychain item.
  153. */
  154. - (void)testWriteExisting {
  155. [self setPassword:kData account:accountFromKey(kKey) service:kService];
  156. FIRAuthKeychainServices *keychain = [[FIRAuthKeychainServices alloc] initWithService:kService];
  157. XCTAssertTrue([keychain setData:dataFromString(kOtherData) forKey:kKey error:NULL]);
  158. XCTAssertEqualObjects([self passwordWithAccount:accountFromKey(kKey) service:kService],
  159. kOtherData);
  160. [self deletePasswordWithAccount:accountFromKey(kKey) service:kService];
  161. }
  162. /** @fn testDeleteNonexisting
  163. @brief Tests deleting non-existing keychain item.
  164. */
  165. - (void)testDeleteNonexisting {
  166. [self setPassword:nil account:accountFromKey(kKey) service:kService];
  167. FIRAuthKeychainServices *keychain = [[FIRAuthKeychainServices alloc] initWithService:kService];
  168. XCTAssertTrue([keychain removeDataForKey:kKey error:NULL]);
  169. XCTAssertNil([self passwordWithAccount:accountFromKey(kKey) service:kService]);
  170. }
  171. /** @fn testDeleteExisting
  172. @brief Tests deleting existing keychain item.
  173. */
  174. - (void)testDeleteExisting {
  175. [self setPassword:kData account:accountFromKey(kKey) service:kService];
  176. FIRAuthKeychainServices *keychain = [[FIRAuthKeychainServices alloc] initWithService:kService];
  177. XCTAssertTrue([keychain removeDataForKey:kKey error:NULL]);
  178. XCTAssertNil([self passwordWithAccount:accountFromKey(kKey) service:kService]);
  179. }
  180. /** @fn testReadLegacy
  181. @brief Tests reading legacy keychain item.
  182. */
  183. - (void)testReadLegacy {
  184. [self setPassword:nil account:accountFromKey(kKey) service:kService];
  185. [self setPassword:kData account:kKey service:nil]; // legacy form
  186. FIRAuthKeychainServices *keychain = [[FIRAuthKeychainServices alloc] initWithService:kService];
  187. NSError *error = fakeError();
  188. XCTAssertEqualObjects([keychain dataForKey:kKey error:&error], dataFromString(kData));
  189. XCTAssertNil(error);
  190. // Legacy item should have been moved to current form.
  191. XCTAssertEqualObjects([self passwordWithAccount:accountFromKey(kKey) service:kService], kData);
  192. XCTAssertNil([self passwordWithAccount:kKey service:nil]);
  193. [self deletePasswordWithAccount:accountFromKey(kKey) service:kService];
  194. }
  195. /** @fn testNotReadLegacy
  196. @brief Tests not reading legacy keychain item because current keychain item exists.
  197. */
  198. - (void)testNotReadLegacy {
  199. [self setPassword:kData account:accountFromKey(kKey) service:kService];
  200. [self setPassword:kOtherData account:kKey service:nil]; // legacy form
  201. FIRAuthKeychainServices *keychain = [[FIRAuthKeychainServices alloc] initWithService:kService];
  202. NSError *error = fakeError();
  203. XCTAssertEqualObjects([keychain dataForKey:kKey error:&error], dataFromString(kData));
  204. XCTAssertNil(error);
  205. // Legacy item should have leave untouched.
  206. XCTAssertEqualObjects([self passwordWithAccount:accountFromKey(kKey) service:kService], kData);
  207. XCTAssertEqualObjects([self passwordWithAccount:kKey service:nil], kOtherData);
  208. [self deletePasswordWithAccount:accountFromKey(kKey) service:kService];
  209. [self deletePasswordWithAccount:kKey service:nil];
  210. }
  211. /** @fn testRemoveLegacy
  212. @brief Tests removing keychain item also removes legacy keychain item.
  213. */
  214. - (void)testRemoveLegacy {
  215. [self setPassword:kData account:accountFromKey(kKey) service:kService];
  216. [self setPassword:kOtherData account:kKey service:nil]; // legacy form
  217. FIRAuthKeychainServices *keychain = [[FIRAuthKeychainServices alloc] initWithService:kService];
  218. XCTAssertTrue([keychain removeDataForKey:kKey error:NULL]);
  219. XCTAssertNil([self passwordWithAccount:accountFromKey(kKey) service:kService]);
  220. XCTAssertNil([self passwordWithAccount:kKey service:nil]);
  221. }
  222. /** @fn testNullErrorParameter
  223. @brief Tests that 'NULL' can be safely passed in.
  224. */
  225. - (void)testNullErrorParameter {
  226. FIRAuthKeychainServices *keychain = [[FIRAuthKeychainServices alloc] initWithService:kService];
  227. [keychain dataForKey:kKey error:NULL];
  228. [keychain setData:dataFromString(kData) forKey:kKey error:NULL];
  229. [keychain removeDataForKey:kKey error:NULL];
  230. }
  231. #pragma mark - Helpers
  232. /** @fn passwordWithAccount:service:
  233. @brief Reads a generic password string from the keychain.
  234. @param account The account attribute of the keychain item.
  235. @param service The service attribute of the keychain item, if provided.
  236. @return The generic password string, if the keychain item exists.
  237. */
  238. - (nullable NSString *)passwordWithAccount:(nonnull NSString *)account
  239. service:(nullable NSString *)service {
  240. NSMutableDictionary *query = [@{
  241. (__bridge id)kSecReturnData : @YES,
  242. (__bridge id)kSecClass : (__bridge id)kSecClassGenericPassword,
  243. (__bridge id)kSecAttrAccount : account,
  244. } mutableCopy];
  245. if (service) {
  246. query[(__bridge id)kSecAttrService] = service;
  247. }
  248. CFDataRef result;
  249. OSStatus status = SecItemCopyMatching((__bridge CFDictionaryRef)query, (CFTypeRef *)&result);
  250. if (status == errSecItemNotFound) {
  251. return nil;
  252. }
  253. XCTAssertEqual(status, errSecSuccess);
  254. return stringFromData((__bridge NSData *)(result));
  255. }
  256. /** @fn addPassword:account:service:
  257. @brief Adds a generic password string to the keychain.
  258. @param password The value attribute for the password to write to the keychain item.
  259. @param account The account attribute of the keychain item.
  260. @param service The service attribute of the keychain item, if provided.
  261. */
  262. - (void)addPassword:(nonnull NSString *)password
  263. account:(nonnull NSString *)account
  264. service:(nullable NSString *)service {
  265. NSMutableDictionary *query = [@{
  266. (__bridge id)kSecValueData : dataFromString(password),
  267. (__bridge id)kSecClass : (__bridge id)kSecClassGenericPassword,
  268. (__bridge id)kSecAttrAccount : account,
  269. } mutableCopy];
  270. if (service) {
  271. query[(__bridge id)kSecAttrService] = service;
  272. }
  273. OSStatus status = SecItemAdd((__bridge CFDictionaryRef)query, NULL);
  274. XCTAssertEqual(status, errSecSuccess);
  275. }
  276. /** @fn deletePasswordWithAccount:service:
  277. @brief Deletes a generic password string from the keychain.
  278. @param account The account attribute of the keychain item.
  279. @param service The service attribute of the keychain item, if provided.
  280. */
  281. - (void)deletePasswordWithAccount:(nonnull NSString *)account service:(nullable NSString *)service {
  282. NSMutableDictionary *query = [@{
  283. (__bridge id)kSecClass : (__bridge id)kSecClassGenericPassword,
  284. (__bridge id)kSecAttrAccount : account,
  285. } mutableCopy];
  286. if (service) {
  287. query[(__bridge id)kSecAttrService] = service;
  288. }
  289. OSStatus status = SecItemDelete((__bridge CFDictionaryRef)query);
  290. XCTAssertEqual(status, errSecSuccess);
  291. }
  292. /** @fn setPasswordWithString:account:service:
  293. @brief Sets a generic password string to the keychain.
  294. @param password The value attribute of the keychain item, if provided, or nil to delete the
  295. existing password if any.
  296. @param account The account attribute of the keychain item.
  297. @param service The service attribute of the keychain item, if provided.
  298. */
  299. - (void)setPassword:(nullable NSString *)password
  300. account:(nonnull NSString *)account
  301. service:(nullable NSString *)service {
  302. if ([self passwordWithAccount:account service:service]) {
  303. [self deletePasswordWithAccount:account service:service];
  304. }
  305. if (password) {
  306. [self addPassword:password account:account service:service];
  307. }
  308. }
  309. @end