FIRAuthKeychainServicesTests.m 12 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313
  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 "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. /** @class FIRAuthKeychainTests
  75. @brief Tests for @c FIRAuthKeychainTests .
  76. */
  77. @interface FIRAuthKeychainTests : XCTestCase
  78. @end
  79. @implementation FIRAuthKeychainTests
  80. /** @fn testReadNonexisting
  81. @brief Tests reading non-existing keychain item.
  82. */
  83. - (void)testReadNonexisting {
  84. [self setPassword:nil account:accountFromKey(kKey) service:kService];
  85. [self setPassword:nil account:kKey service:nil]; // legacy form
  86. FIRAuthKeychainServices *keychain = [[FIRAuthKeychainServices alloc] initWithService:kService];
  87. NSError *error = fakeError();
  88. XCTAssertNil([keychain dataForKey:kKey error:&error]);
  89. XCTAssertNil(error);
  90. }
  91. /** @fn testReadExisting
  92. @brief Tests reading existing keychain item.
  93. */
  94. - (void)testReadExisting {
  95. [self setPassword:kData account:accountFromKey(kKey) service:kService];
  96. FIRAuthKeychainServices *keychain = [[FIRAuthKeychainServices alloc] initWithService:kService];
  97. NSError *error = fakeError();
  98. XCTAssertEqualObjects([keychain dataForKey:kKey error:&error], dataFromString(kData));
  99. XCTAssertNil(error);
  100. [self deletePasswordWithAccount:accountFromKey(kKey) service:kService];
  101. }
  102. /** @fn testNotReadOtherService
  103. @brief Tests not reading keychain item belonging to other service.
  104. */
  105. - (void)testNotReadOtherService {
  106. [self setPassword:nil account:accountFromKey(kKey) service:kService];
  107. [self setPassword:kData account:accountFromKey(kKey) service:kOtherService];
  108. FIRAuthKeychainServices *keychain = [[FIRAuthKeychainServices alloc] initWithService:kService];
  109. NSError *error = fakeError();
  110. XCTAssertNil([keychain dataForKey:kKey error:&error]);
  111. XCTAssertNil(error);
  112. [self deletePasswordWithAccount:accountFromKey(kKey) service:kOtherService];
  113. }
  114. /** @fn testWriteNonexisting
  115. @brief Tests writing new keychain item.
  116. */
  117. - (void)testWriteNonexisting {
  118. [self setPassword:nil account:accountFromKey(kKey) service:kService];
  119. FIRAuthKeychainServices *keychain = [[FIRAuthKeychainServices alloc] initWithService:kService];
  120. XCTAssertTrue([keychain setData:dataFromString(kData) forKey:kKey error:NULL]);
  121. XCTAssertEqualObjects([self passwordWithAccount:accountFromKey(kKey) service:kService], kData);
  122. [self deletePasswordWithAccount:accountFromKey(kKey) service:kService];
  123. }
  124. /** @fn testWriteExisting
  125. @brief Tests overwriting existing keychain item.
  126. */
  127. - (void)testWriteExisting {
  128. [self setPassword:kData account:accountFromKey(kKey) service:kService];
  129. FIRAuthKeychainServices *keychain = [[FIRAuthKeychainServices alloc] initWithService:kService];
  130. XCTAssertTrue([keychain setData:dataFromString(kOtherData) forKey:kKey error:NULL]);
  131. XCTAssertEqualObjects([self passwordWithAccount:accountFromKey(kKey) service:kService],
  132. kOtherData);
  133. [self deletePasswordWithAccount:accountFromKey(kKey) service:kService];
  134. }
  135. /** @fn testDeleteNonexisting
  136. @brief Tests deleting non-existing keychain item.
  137. */
  138. - (void)testDeleteNonexisting {
  139. [self setPassword:nil account:accountFromKey(kKey) service:kService];
  140. FIRAuthKeychainServices *keychain = [[FIRAuthKeychainServices alloc] initWithService:kService];
  141. XCTAssertTrue([keychain removeDataForKey:kKey error:NULL]);
  142. XCTAssertNil([self passwordWithAccount:accountFromKey(kKey) service:kService]);
  143. }
  144. /** @fn testDeleteExisting
  145. @brief Tests deleting existing keychain item.
  146. */
  147. - (void)testDeleteExisting {
  148. [self setPassword:kData account:accountFromKey(kKey) service:kService];
  149. FIRAuthKeychainServices *keychain = [[FIRAuthKeychainServices alloc] initWithService:kService];
  150. XCTAssertTrue([keychain removeDataForKey:kKey error:NULL]);
  151. XCTAssertNil([self passwordWithAccount:accountFromKey(kKey) service:kService]);
  152. }
  153. /** @fn testReadLegacy
  154. @brief Tests reading legacy keychain item.
  155. */
  156. - (void)testReadLegacy {
  157. [self setPassword:nil account:accountFromKey(kKey) service:kService];
  158. [self setPassword:kData account:kKey service:nil]; // legacy form
  159. FIRAuthKeychainServices *keychain = [[FIRAuthKeychainServices alloc] initWithService:kService];
  160. NSError *error = fakeError();
  161. XCTAssertEqualObjects([keychain dataForKey:kKey error:&error], dataFromString(kData));
  162. XCTAssertNil(error);
  163. // Legacy item should have been moved to current form.
  164. XCTAssertEqualObjects([self passwordWithAccount:accountFromKey(kKey) service:kService], kData);
  165. XCTAssertNil([self passwordWithAccount:kKey service:nil]);
  166. [self deletePasswordWithAccount:accountFromKey(kKey) service:kService];
  167. }
  168. /** @fn testNotReadLegacy
  169. @brief Tests not reading legacy keychain item because current keychain item exists.
  170. */
  171. - (void)testNotReadLegacy {
  172. [self setPassword:kData account:accountFromKey(kKey) service:kService];
  173. [self setPassword:kOtherData account:kKey service:nil]; // legacy form
  174. FIRAuthKeychainServices *keychain = [[FIRAuthKeychainServices alloc] initWithService:kService];
  175. NSError *error = fakeError();
  176. XCTAssertEqualObjects([keychain dataForKey:kKey error:&error], dataFromString(kData));
  177. XCTAssertNil(error);
  178. // Legacy item should have leave untouched.
  179. XCTAssertEqualObjects([self passwordWithAccount:accountFromKey(kKey) service:kService], kData);
  180. XCTAssertEqualObjects([self passwordWithAccount:kKey service:nil], kOtherData);
  181. [self deletePasswordWithAccount:accountFromKey(kKey) service:kService];
  182. [self deletePasswordWithAccount:kKey service:nil];
  183. }
  184. /** @fn testRemoveLegacy
  185. @brief Tests removing keychain item also removes legacy keychain item.
  186. */
  187. - (void)testRemoveLegacy {
  188. [self setPassword:kData account:accountFromKey(kKey) service:kService];
  189. [self setPassword:kOtherData account:kKey service:nil]; // legacy form
  190. FIRAuthKeychainServices *keychain = [[FIRAuthKeychainServices alloc] initWithService:kService];
  191. XCTAssertTrue([keychain removeDataForKey:kKey error:NULL]);
  192. XCTAssertNil([self passwordWithAccount:accountFromKey(kKey) service:kService]);
  193. XCTAssertNil([self passwordWithAccount:kKey service:nil]);
  194. }
  195. /** @fn testNullErrorParameter
  196. @brief Tests that 'NULL' can be safely passed in.
  197. */
  198. - (void)testNullErrorParameter {
  199. FIRAuthKeychainServices *keychain = [[FIRAuthKeychainServices alloc] initWithService:kService];
  200. [keychain dataForKey:kKey error:NULL];
  201. [keychain setData:dataFromString(kData) forKey:kKey error:NULL];
  202. [keychain removeDataForKey:kKey error:NULL];
  203. }
  204. #pragma mark - Helpers
  205. /** @fn passwordWithAccount:service:
  206. @brief Reads a generic password string from the keychain.
  207. @param account The account attribute of the keychain item.
  208. @param service The service attribute of the keychain item, if provided.
  209. @return The generic password string, if the keychain item exists.
  210. */
  211. - (nullable NSString *)passwordWithAccount:(nonnull NSString *)account
  212. service:(nullable NSString *)service {
  213. NSMutableDictionary *query = [@{
  214. (__bridge id)kSecReturnData : @YES,
  215. (__bridge id)kSecClass : (__bridge id)kSecClassGenericPassword,
  216. (__bridge id)kSecAttrAccount : account,
  217. } mutableCopy];
  218. if (service) {
  219. query[(__bridge id)kSecAttrService] = service;
  220. }
  221. CFDataRef result;
  222. OSStatus status = SecItemCopyMatching((__bridge CFDictionaryRef)query, (CFTypeRef *)&result);
  223. if (status == errSecItemNotFound) {
  224. return nil;
  225. }
  226. XCTAssertEqual(status, errSecSuccess);
  227. return stringFromData((__bridge NSData *)(result));
  228. }
  229. /** @fn addPassword:account:service:
  230. @brief Adds a generic password string to the keychain.
  231. @param password The value attribute for the password to write to the keychain item.
  232. @param account The account attribute of the keychain item.
  233. @param service The service attribute of the keychain item, if provided.
  234. */
  235. - (void)addPassword:(nonnull NSString *)password
  236. account:(nonnull NSString *)account
  237. service:(nullable NSString *)service {
  238. NSMutableDictionary *query = [@{
  239. (__bridge id)kSecValueData : dataFromString(password),
  240. (__bridge id)kSecClass : (__bridge id)kSecClassGenericPassword,
  241. (__bridge id)kSecAttrAccount : account,
  242. } mutableCopy];
  243. if (service) {
  244. query[(__bridge id)kSecAttrService] = service;
  245. }
  246. OSStatus status = SecItemAdd((__bridge CFDictionaryRef)query, NULL);
  247. XCTAssertEqual(status, errSecSuccess);
  248. }
  249. /** @fn deletePasswordWithAccount:service:
  250. @brief Deletes a generic password string from the keychain.
  251. @param account The account attribute of the keychain item.
  252. @param service The service attribute of the keychain item, if provided.
  253. */
  254. - (void)deletePasswordWithAccount:(nonnull NSString *)account service:(nullable NSString *)service {
  255. NSMutableDictionary *query = [@{
  256. (__bridge id)kSecClass : (__bridge id)kSecClassGenericPassword,
  257. (__bridge id)kSecAttrAccount : account,
  258. } mutableCopy];
  259. if (service) {
  260. query[(__bridge id)kSecAttrService] = service;
  261. }
  262. OSStatus status = SecItemDelete((__bridge CFDictionaryRef)query);
  263. XCTAssertEqual(status, errSecSuccess);
  264. }
  265. /** @fn setPasswordWithString:account:service:
  266. @brief Sets a generic password string to the keychain.
  267. @param password The value attribute of the keychain item, if provided, or nil to delete the
  268. existing password if any.
  269. @param account The account attribute of the keychain item.
  270. @param service The service attribute of the keychain item, if provided.
  271. */
  272. - (void)setPassword:(nullable NSString *)password
  273. account:(nonnull NSString *)account
  274. service:(nullable NSString *)service {
  275. if ([self passwordWithAccount:account service:service]) {
  276. [self deletePasswordWithAccount:account service:service];
  277. }
  278. if (password) {
  279. [self addPassword:password account:account service:service];
  280. }
  281. }
  282. @end