aboutsummaryrefslogtreecommitdiffhomepage
path: root/Example/Auth/Tests/FIRAuthKeychainTests.m
blob: 374e868fef5e371c2c671c33f73923b4f9e51daf (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
/*
 * Copyright 2017 Google
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

#import <Security/Security.h>
#import <XCTest/XCTest.h>

#import "FIRAuthKeychain.h"

/** @var kAccountPrefix
    @brief The keychain account prefix assumed by the tests.
 */
static NSString *const kAccountPrefix = @"firebase_auth_1_";

/** @var kKey
    @brief The key used in tests.
 */
static NSString *const kKey = @"ACCOUNT";

/** @var kService
    @brief The keychain service used in tests.
 */
static NSString *const kService = @"SERVICE";

/** @var kOtherService
    @brief Another keychain service used in tests.
 */
static NSString *const kOtherService = @"OTHER_SERVICE";

/** @var kData
    @brief A piece of keychain data used in tests.
 */
static NSString *const kData = @"DATA";

/** @var kOtherData
    @brief Another piece of keychain data used in tests.
 */
static NSString *const kOtherData = @"OTHER_DATA";

/** @fn accountFromKey
    @brief Converts a key string to an account string.
    @param key The key string to be converted from.
    @return The account string being the conversion result.
 */
static NSString *accountFromKey(NSString *key) {
  return [kAccountPrefix stringByAppendingString:key];
}

/** @fn dataFromString
    @brief Converts a NSString to NSData.
    @param string The NSString to be converted from.
    @return The NSData being the conversion result.
 */
static NSData *dataFromString(NSString *string) {
  return [string dataUsingEncoding:NSUTF8StringEncoding];
}

/** @fn stringFromData
    @brief Converts a NSData to NSString.
    @param data The NSData to be converted from.
    @return The NSString being the conversion result.
 */
static NSString *stringFromData(NSData *data) {
  return [[NSString alloc] initWithData:data encoding:NSUTF8StringEncoding];
}

/** @fn fakeError
    @brief Creates a fake error object.
    @return a non-nil NSError instance.
 */
static NSError *fakeError() {
  return [NSError errorWithDomain:@"ERROR" code:-1 userInfo:nil];
}

/** @class FIRAuthKeychainTests
    @brief Tests for @c FIRAuthKeychainTests .
 */
@interface FIRAuthKeychainTests : XCTestCase
@end

@implementation FIRAuthKeychainTests

/** @fn testReadNonexisting
    @brief Tests reading non-existing keychain item.
 */
- (void)testReadNonexisting {
  [self setPassword:nil account:accountFromKey(kKey) service:kService];
  [self setPassword:nil account:kKey service:nil];  // legacy form
  FIRAuthKeychain *keychain = [[FIRAuthKeychain alloc] initWithService:kService];
  NSError *error = fakeError();
  XCTAssertNil([keychain dataForKey:kKey error:&error]);
  XCTAssertNil(error);
}

/** @fn testReadExisting
    @brief Tests reading existing keychain item.
 */
- (void)testReadExisting {
  [self setPassword:kData account:accountFromKey(kKey) service:kService];
  FIRAuthKeychain *keychain = [[FIRAuthKeychain alloc] initWithService:kService];
  NSError *error = fakeError();
  XCTAssertEqualObjects([keychain dataForKey:kKey error:&error], dataFromString(kData));
  XCTAssertNil(error);
  [self deletePasswordWithAccount:accountFromKey(kKey) service:kService];
}

/** @fn testNotReadOtherService
    @brief Tests not reading keychain item belonging to other service.
 */
- (void)testNotReadOtherService {
  [self setPassword:nil account:accountFromKey(kKey) service:kService];
  [self setPassword:kData account:accountFromKey(kKey) service:kOtherService];
  FIRAuthKeychain *keychain = [[FIRAuthKeychain alloc] initWithService:kService];
  NSError *error = fakeError();
  XCTAssertNil([keychain dataForKey:kKey error:&error]);
  XCTAssertNil(error);
  [self deletePasswordWithAccount:accountFromKey(kKey) service:kOtherService];
}

/** @fn testWriteNonexisting
    @brief Tests writing new keychain item.
 */
- (void)testWriteNonexisting {
  [self setPassword:nil account:accountFromKey(kKey) service:kService];
  FIRAuthKeychain *keychain = [[FIRAuthKeychain alloc] initWithService:kService];
  XCTAssertTrue([keychain setData:dataFromString(kData) forKey:kKey error:NULL]);
  XCTAssertEqualObjects([self passwordWithAccount:accountFromKey(kKey) service:kService], kData);
  [self deletePasswordWithAccount:accountFromKey(kKey) service:kService];
}

/** @fn testWriteExisting
    @brief Tests overwriting existing keychain item.
 */
- (void)testWriteExisting {
  [self setPassword:kData account:accountFromKey(kKey) service:kService];
  FIRAuthKeychain *keychain = [[FIRAuthKeychain alloc] initWithService:kService];
  XCTAssertTrue([keychain setData:dataFromString(kOtherData) forKey:kKey error:NULL]);
  XCTAssertEqualObjects([self passwordWithAccount:accountFromKey(kKey) service:kService],
                        kOtherData);
  [self deletePasswordWithAccount:accountFromKey(kKey) service:kService];
}

/** @fn testDeleteNonexisting
    @brief Tests deleting non-existing keychain item.
 */
- (void)testDeleteNonexisting {
  [self setPassword:nil account:accountFromKey(kKey) service:kService];
  FIRAuthKeychain *keychain = [[FIRAuthKeychain alloc] initWithService:kService];
  XCTAssertTrue([keychain removeDataForKey:kKey error:NULL]);
  XCTAssertNil([self passwordWithAccount:accountFromKey(kKey) service:kService]);
}

/** @fn testDeleteExisting
    @brief Tests deleting existing keychain item.
 */
- (void)testDeleteExisting {
  [self setPassword:kData account:accountFromKey(kKey) service:kService];
  FIRAuthKeychain *keychain = [[FIRAuthKeychain alloc] initWithService:kService];
  XCTAssertTrue([keychain removeDataForKey:kKey error:NULL]);
  XCTAssertNil([self passwordWithAccount:accountFromKey(kKey) service:kService]);
}

/** @fn testReadLegacy
    @brief Tests reading legacy keychain item.
 */
- (void)testReadLegacy {
  [self setPassword:nil account:accountFromKey(kKey) service:kService];
  [self setPassword:kData account:kKey service:nil];  // legacy form
  FIRAuthKeychain *keychain = [[FIRAuthKeychain alloc] initWithService:kService];
  NSError *error = fakeError();
  XCTAssertEqualObjects([keychain dataForKey:kKey error:&error], dataFromString(kData));
  XCTAssertNil(error);
  // Legacy item should have been moved to current form.
  XCTAssertEqualObjects([self passwordWithAccount:accountFromKey(kKey) service:kService], kData);
  XCTAssertNil([self passwordWithAccount:kKey service:nil]);
  [self deletePasswordWithAccount:accountFromKey(kKey) service:kService];
}

/** @fn testNotReadLegacy
    @brief Tests not reading legacy keychain item because current keychain item exists.
 */
- (void)testNotReadLegacy {
  [self setPassword:kData account:accountFromKey(kKey) service:kService];
  [self setPassword:kOtherData account:kKey service:nil];  // legacy form
  FIRAuthKeychain *keychain = [[FIRAuthKeychain alloc] initWithService:kService];
  NSError *error = fakeError();
  XCTAssertEqualObjects([keychain dataForKey:kKey error:&error], dataFromString(kData));
  XCTAssertNil(error);
  // Legacy item should have leave untouched.
  XCTAssertEqualObjects([self passwordWithAccount:accountFromKey(kKey) service:kService], kData);
  XCTAssertEqualObjects([self passwordWithAccount:kKey service:nil], kOtherData);
  [self deletePasswordWithAccount:accountFromKey(kKey) service:kService];
  [self deletePasswordWithAccount:kKey service:nil];
}

/** @fn testRemoveLegacy
    @brief Tests removing keychain item also removes legacy keychain item.
 */
- (void)testRemoveLegacy {
  [self setPassword:kData account:accountFromKey(kKey) service:kService];
  [self setPassword:kOtherData account:kKey service:nil];  // legacy form
  FIRAuthKeychain *keychain = [[FIRAuthKeychain alloc] initWithService:kService];
  XCTAssertTrue([keychain removeDataForKey:kKey error:NULL]);
  XCTAssertNil([self passwordWithAccount:accountFromKey(kKey) service:kService]);
  XCTAssertNil([self passwordWithAccount:kKey service:nil]);
}

/** @fn testNullErrorParameter
    @brief Tests that 'NULL' can be safely passed in.
 */
- (void)testNullErrorParameter {
  FIRAuthKeychain *keychain = [[FIRAuthKeychain alloc] initWithService:kService];
  [keychain dataForKey:kKey error:NULL];
  [keychain setData:dataFromString(kData) forKey:kKey error:NULL];
  [keychain removeDataForKey:kKey error:NULL];
}

#pragma mark - Helpers

/** @fn passwordWithAccount:service:
    @brief Reads a generic password string from the keychain.
    @param account The account attribute of the keychain item.
    @param service The service attribute of the keychain item, if provided.
    @return The generic password string, if the keychain item exists.
 */
- (nullable NSString *)passwordWithAccount:(nonnull NSString *)account
                                   service:(nullable NSString *)service {
  NSMutableDictionary *query = [@{
    (__bridge id)kSecReturnData : @YES,
    (__bridge id)kSecClass : (__bridge id)kSecClassGenericPassword,
    (__bridge id)kSecAttrAccount : account,
  } mutableCopy];
  if (service) {
    query[(__bridge id)kSecAttrService] = service;
  }
  CFDataRef result;
  OSStatus status = SecItemCopyMatching((__bridge CFDictionaryRef)query, (CFTypeRef *)&result);
  if (status == errSecItemNotFound) {
    return nil;
  }
  XCTAssertEqual(status, errSecSuccess);
  return stringFromData((__bridge NSData *)(result));
}

/** @fn addPassword:account:service:
    @brief Adds a generic password string to the keychain.
    @param password The value attribute for the password to write to the keychain item.
    @param account The account attribute of the keychain item.
    @param service The service attribute of the keychain item, if provided.
 */
- (void)addPassword:(nonnull NSString *)password
            account:(nonnull NSString *)account
            service:(nullable NSString *)service {
  NSMutableDictionary *query = [@{
    (__bridge id)kSecValueData : dataFromString(password),
    (__bridge id)kSecClass : (__bridge id)kSecClassGenericPassword,
    (__bridge id)kSecAttrAccount : account,
  } mutableCopy];
  if (service) {
    query[(__bridge id)kSecAttrService] = service;
  }
  OSStatus status = SecItemAdd((__bridge CFDictionaryRef)query, NULL);
  XCTAssertEqual(status, errSecSuccess);
}

/** @fn deletePasswordWithAccount:service:
    @brief Deletes a generic password string from the keychain.
    @param account The account attribute of the keychain item.
    @param service The service attribute of the keychain item, if provided.
 */
- (void)deletePasswordWithAccount:(nonnull NSString *)account
                          service:(nullable NSString *)service {
  NSMutableDictionary *query = [@{
    (__bridge id)kSecClass : (__bridge id)kSecClassGenericPassword,
    (__bridge id)kSecAttrAccount : account,
  } mutableCopy];
  if (service) {
    query[(__bridge id)kSecAttrService] = service;
  }
  OSStatus status = SecItemDelete((__bridge CFDictionaryRef)query);
  XCTAssertEqual(status, errSecSuccess);
}

/** @fn setPasswordWithString:account:service:
    @brief Sets a generic password string to the keychain.
    @param password The value attribute of the keychain item, if provided, or nil to delete the
        existing password if any.
    @param account The account attribute of the keychain item.
    @param service The service attribute of the keychain item, if provided.
 */
- (void)setPassword:(nullable NSString *)password
            account:(nonnull NSString *)account
            service:(nullable NSString *)service {
  if ([self passwordWithAccount:account service:service]) {
    [self deletePasswordWithAccount:account service:service];
  }
  if (password) {
    [self addPassword:password account:account service:service];
  }
}

@end