]> git.saurik.com Git - apple/security.git/blob - secdxctests/CDKeychainTests.m
Security-58286.240.4.tar.gz
[apple/security.git] / secdxctests / CDKeychainTests.m
1 /*
2 * Copyright (c) 2017 Apple Inc. All Rights Reserved.
3 *
4 * @APPLE_LICENSE_HEADER_START@
5 *
6 * This file contains Original Code and/or Modifications of Original Code
7 * as defined in and that are subject to the Apple Public Source License
8 * Version 2.0 (the 'License'). You may not use this file except in
9 * compliance with the License. Please obtain a copy of the License at
10 * http://www.opensource.apple.com/apsl/ and read it before using this
11 * file.
12 *
13 * The Original Code and all software distributed under the License are
14 * distributed on an 'AS IS' basis, WITHOUT WARRANTY OF ANY KIND, EITHER
15 * EXPRESS OR IMPLIED, AND APPLE HEREBY DISCLAIMS ALL SUCH WARRANTIES,
16 * INCLUDING WITHOUT LIMITATION, ANY WARRANTIES OF MERCHANTABILITY,
17 * FITNESS FOR A PARTICULAR PURPOSE, QUIET ENJOYMENT OR NON-INFRINGEMENT.
18 * Please see the License for the specific language governing rights and
19 * limitations under the License.
20 *
21 * @APPLE_LICENSE_HEADER_END@
22 */
23
24 #import "SecCDKeychain.h"
25 #import "CKKS.h"
26 #import "spi.h"
27 #import "SecItemServer.h"
28 #import "SecdTestKeychainUtilities.h"
29 #import "server_security_helpers.h"
30 #import "SecDbKeychainItemV7.h"
31 #import "KeychainXCTest.h"
32 #import "SecCDKeychainManagedItem+CoreDataClass.h"
33 #import "SecFileLocations.h"
34 #import <SecurityFoundation/SFEncryptionOperation.h>
35 #import <SecurityFoundation/SFKeychain.h>
36 #import <XCTest/XCTest.h>
37 #import <OCMock/OCMock.h>
38
39 #if USE_KEYSTORE
40
41 @interface SecCDKeychain (UnitTestingRedeclarations)
42
43 @property (readonly, getter=_queue) dispatch_queue_t queue;
44
45 - (void)_registerItemTypeForTesting:(SecCDKeychainItemType*)itemType;
46
47 - (void)performOnManagedObjectQueue:(void (^)(NSManagedObjectContext* context, NSError* error))block;
48 - (void)performOnManagedObjectQueueAndWait:(void (^)(NSManagedObjectContext* context, NSError* error))block;
49 - (SecCDKeychainManagedItem*)fetchManagedItemForPersistentID:(NSUUID*)persistentID withManagedObjectContext:(NSManagedObjectContext*)managedObjectContext error:(NSError**)error;
50
51 - (NSData*)_onQueueGetDatabaseKeyDataWithError:(NSError**)error;
52 - (void)_onQueueDropClassAPersistentStore;
53
54 @end
55
56 @interface SecCDKeychainItem (UnitTestingRedeclarations)
57
58 - (instancetype)initWithManagedItem:(SecCDKeychainManagedItem*)managedItem keychain:(SecCDKeychain*)keychain error:(NSError**)error;
59
60 @end
61
62 @interface TestItemType : SecCDKeychainItemType
63 @end
64
65 @implementation TestItemType
66
67 + (instancetype)itemType
68 {
69 return [[self alloc] _initWithName:@"TestItem" version:1 primaryKeys:nil syncableKeys:nil];
70 }
71
72 @end
73
74 @interface CDKeychainTests : KeychainXCTest
75 @end
76
77 @implementation CDKeychainTests {
78 SecCDKeychain* _keychain;
79 SFKeychainServerFakeConnection* _connection;
80 }
81
82 - (void)setUp
83 {
84 [super setUp];
85
86 NSURL* persistentStoreURL = (__bridge_transfer NSURL*)SecCopyURLForFileInKeychainDirectory((__bridge CFStringRef)@"CDKeychain");
87 NSBundle* resourcesBundle = [NSBundle bundleWithPath:@"/System/Library/Keychain/KeychainResources.bundle"];
88 NSURL* managedObjectModelURL = [resourcesBundle URLForResource:@"KeychainModel" withExtension:@"momd"];
89 _keychain = [[SecCDKeychain alloc] initWithStorageURL:persistentStoreURL modelURL:managedObjectModelURL encryptDatabase:false];
90 _connection = [[SFKeychainServerFakeConnection alloc] init];
91
92 self.keychainPartialMock = OCMPartialMock(_keychain);
93 [[[[self.keychainPartialMock stub] andCall:@selector(getDatabaseKeyDataithError:) onObject:self] ignoringNonObjectArgs] _onQueueGetDatabaseKeyDataWithError:NULL];
94
95 [_keychain _registerItemTypeForTesting:[TestItemType itemType]];
96 }
97
98 - (void)tearDown
99 {
100 [self.keychainPartialMock stopMocking];
101 self.keychainPartialMock = nil;
102
103 [super tearDown];
104 }
105
106 - (NSArray<SecCDKeychainLookupTuple*>*)lookupTuplesForAttributes:(NSDictionary*)attributes
107 {
108 NSMutableArray* lookupTuples = [[NSMutableArray alloc] initWithCapacity:attributes.count];
109 [attributes enumerateKeysAndObjectsUsingBlock:^(NSString* key, id value, BOOL* stop) {
110 [lookupTuples addObject:[SecCDKeychainLookupTuple lookupTupleWithKey:key value:value]];
111 }];
112 return lookupTuples;
113 }
114
115 - (void)testInsertAndRetrieveItemWithPersistentID
116 {
117 SecCDKeychainAccessControlEntity* owner = [SecCDKeychainAccessControlEntity accessControlEntityWithType:SecCDKeychainAccessControlEntityTypeAccessGroup stringRepresentation:@"com.apple.token"];
118
119 NSUUID* persistentID = [NSUUID UUID];
120 SecCDKeychainItem* item = [[SecCDKeychainItem alloc] initItemType:[TestItemType itemType] withPersistentID:persistentID attributes:@{@"key" : @"value"} lookupAttributes:nil secrets:@{@"data" : @"MySecret"} owner:owner keyclass:key_class_ak];
121
122 XCTestExpectation* insertExpectation = [self expectationWithDescription:@"insert item"];
123 [_keychain insertItems:@[item] withConnection:_connection completionHandler:^(bool success, NSError* error) {
124 XCTAssertTrue(success, @"should have been able to insert item");
125 XCTAssertNil(error, @"should not have gotten an error inserting item");
126 [insertExpectation fulfill];
127 }];
128 [self waitForExpectations:@[insertExpectation] timeout:5.0];
129
130 XCTestExpectation* fetchExpectation = [self expectationWithDescription:@"fetch item"];
131 [_keychain fetchItemForPersistentID:persistentID withConnection:_connection completionHandler:^(SecCDKeychainItem* fetchedItem, NSError* error) {
132 XCTAssertNotNil(fetchedItem, @"should have been able to fetch item");
133 XCTAssertNil(error, @"should not have gotten an error fetching item");
134 XCTAssertEqualObjects(fetchedItem, item, @"fetched item should match inserted item");
135 [fetchExpectation fulfill];
136 }];
137 [self waitForExpectations:@[fetchExpectation] timeout:5.0];
138 }
139
140 - (SecCDKeychainItem*)fullItemForItemMetadata:(SecCDKeychainItemMetadata*)metadata
141 {
142 dispatch_semaphore_t semaphore = dispatch_semaphore_create(0);
143 __block SecCDKeychainItem* result = nil;
144 [metadata fetchFullItemWithKeychain:_keychain withConnection:_connection completionHandler:^(SecCDKeychainItem* item, NSError* error) {
145 result = item;
146 dispatch_semaphore_signal(semaphore);
147 }];
148 dispatch_semaphore_wait(semaphore, dispatch_time(DISPATCH_TIME_NOW, 5 * NSEC_PER_SEC));
149 return result;
150 }
151
152 - (void)testLookupItemByAttribute
153 {
154 NSUUID* firstItemID = [NSUUID UUID];
155 NSUUID* secondItemID = [NSUUID UUID];
156 NSDictionary* firstItemAttributes = @{@"attribute1" : @"gotIt", @"sharedAttribute" : @"first"};
157 NSDictionary* secondItemAttributes = @{@"attribute2" : @"gotIt", @"sharedAttribute" : @"second"};
158 NSArray* firstItemLookupAttributes = [self lookupTuplesForAttributes:firstItemAttributes];
159 NSArray* secondItemLookupAttributes = [self lookupTuplesForAttributes:secondItemAttributes];
160
161 SecCDKeychainAccessControlEntity* owner = [SecCDKeychainAccessControlEntity accessControlEntityWithType:SecCDKeychainAccessControlEntityTypeAccessGroup stringRepresentation:@"com.apple.token"];
162 SecCDKeychainItem* firstItem = [[SecCDKeychainItem alloc] initItemType:[TestItemType itemType] withPersistentID:firstItemID attributes:firstItemAttributes lookupAttributes:firstItemLookupAttributes secrets:@{@"data" : @"I'm the first"} owner:owner keyclass:key_class_ak];
163 SecCDKeychainItem* secondItem = [[SecCDKeychainItem alloc] initItemType:[TestItemType itemType] withPersistentID:secondItemID attributes:secondItemAttributes lookupAttributes:secondItemLookupAttributes secrets:@{@"data" : @"I'm the second"} owner:owner keyclass:key_class_ak];
164
165 XCTestExpectation* insertExpection = [self expectationWithDescription:@"insert items"];
166 [_keychain insertItems:@[firstItem, secondItem] withConnection:_connection completionHandler:^(bool success, NSError* error) {
167 XCTAssertTrue(success, @"failed to successfully insert items with error: %@", error);
168 XCTAssertNil(error, @"encountered error inserting items: %@", error);
169 [insertExpection fulfill];
170 }];
171 [self waitForExpectations:@[insertExpection] timeout:5.0];
172
173 XCTestExpectation* fetchExpectation = [self expectationWithDescription:@"fetch items"];
174 [_keychain fetchItemsWithValue:@"gotIt" forLookupKey:@"attribute1" ofType:SecCDKeychainLookupValueTypeString withConnection:_connection completionHandler:^(NSArray<SecCDKeychainItemMetadata*>* items, NSError* error) {
175 XCTAssertEqual(items.count, (unsigned long)1, @"did not get the expected number of fetched items; expected 1, but got %d", (int)items.count);
176 SecCDKeychainItem* fetchedItem = [self fullItemForItemMetadata:items.firstObject];
177 XCTAssertNotNil(fetchedItem, @"failed to fetch item we just inserted with error: %@", error);
178 XCTAssertNil(error, @"encountered error fetching item: %@", error);
179 XCTAssertEqualObjects(fetchedItem, firstItem, @"fetched item does not match the item we expected to fetch");
180 [fetchExpectation fulfill];
181 }];
182
183 XCTestExpectation* secondFetchExpectation = [self expectationWithDescription:@"second fetch"];
184 [_keychain fetchItemsWithValue:@"first" forLookupKey:@"sharedAttribute" ofType:SecCDKeychainLookupValueTypeString withConnection:_connection completionHandler:^(NSArray<SecCDKeychainItemMetadata*>* items, NSError* error) {
185 XCTAssertEqual(items.count, (unsigned long)1, @"did not get the expected number of fetched items; expected 1, but got %d", (int)items.count);
186 SecCDKeychainItem* fetchedItem = [self fullItemForItemMetadata:items.firstObject];
187 XCTAssertNotNil(fetchedItem, @"failed to fetch item we just inserted with error: %@", error);
188 XCTAssertNil(error, @"encountered error fetching item: %@", error);
189 XCTAssertEqualObjects(fetchedItem, firstItem, @"fetched item does not match the item we expected to fetch");
190 [secondFetchExpectation fulfill];
191 }];
192 [self waitForExpectations:@[fetchExpectation, secondFetchExpectation] timeout:5.0];
193 }
194
195 - (void)testLookupMultipleItems
196 {
197 NSUUID* firstItemID = [NSUUID UUID];
198 NSUUID* secondItemID = [NSUUID UUID];
199 NSUUID* thirdUUID = [NSUUID UUID];
200 NSDictionary* firstItemAttributes = @{@"attribute1" : @"gotIt", @"sharedAttribute" : @"bothHaveThis"};
201 NSDictionary* secondItemAttributes = @{@"attribute2" : @"gotIt", @"sharedAttribute" : @"bothHaveThis"};
202 NSDictionary* thirdItemAttributes = @{@"attribute3" : @"gotIt", @"sharedAttribute" : @"somethingDifferent"};
203 NSArray* firstItemLookupAttributes = [self lookupTuplesForAttributes:firstItemAttributes];
204 NSArray* secondItemLookupAttributes = [self lookupTuplesForAttributes:secondItemAttributes];
205 NSArray* thirdItemLookupAttributes = [self lookupTuplesForAttributes:thirdItemAttributes];
206
207 SecCDKeychainAccessControlEntity* owner = [SecCDKeychainAccessControlEntity accessControlEntityWithType:SecCDKeychainAccessControlEntityTypeAccessGroup stringRepresentation:@"com.apple.token"];
208 SecCDKeychainItem* firstItem = [[SecCDKeychainItem alloc] initItemType:[TestItemType itemType] withPersistentID:firstItemID attributes:firstItemAttributes lookupAttributes:firstItemLookupAttributes secrets:@{@"data" : @"I'm the first"} owner:owner keyclass:key_class_ak];
209 SecCDKeychainItem* secondItem = [[SecCDKeychainItem alloc] initItemType:[TestItemType itemType] withPersistentID:secondItemID attributes:secondItemAttributes lookupAttributes:secondItemLookupAttributes secrets:@{@"data" : @"I'm the second"} owner:owner keyclass:key_class_ak];
210 SecCDKeychainItem* thirdItem = [[SecCDKeychainItem alloc] initItemType:[TestItemType itemType] withPersistentID:thirdUUID attributes:thirdItemAttributes lookupAttributes:thirdItemLookupAttributes secrets:@{@"data" : @"I'm the third"} owner:owner keyclass:key_class_ak];
211
212 XCTestExpectation* insertExpectation = [self expectationWithDescription:@"insert items"];
213 [_keychain insertItems:@[firstItem, secondItem] withConnection:_connection completionHandler:^(bool success, NSError* error) {
214 XCTAssertTrue(success, @"failed to successfully insert items with error: %@", error);
215 XCTAssertNil(error, @"encountered error inserting items: %@", error);
216 [insertExpectation fulfill];
217 }];
218 [self waitForExpectations:@[insertExpectation] timeout:5.0];
219
220 XCTestExpectation* fetchExpectation = [self expectationWithDescription:@"fetch items"];
221 [_keychain fetchItemsWithValue:@"bothHaveThis" forLookupKey:@"sharedAttribute" ofType:SecCDKeychainLookupValueTypeString withConnection:_connection completionHandler:^(NSArray<SecCDKeychainItemMetadata*>* items, NSError* error) {
222 XCTAssertEqual(items.count, (unsigned long)2, @"did not get the expected number of fetched items; expected 2, but got %d", (int)items.count);
223 XCTAssertNil(error, @"encountered error fetching item: %@", error);
224 if (items.count >= 2) {
225 SecCDKeychainItem* firstFetchedObject = [self fullItemForItemMetadata:items[0]];
226 SecCDKeychainItem* secondFetchedObject = [self fullItemForItemMetadata:items[1]];
227 XCTAssertTrue([firstFetchedObject isEqual:firstItem] || [firstFetchedObject isEqual:secondItem], @"first fetched object does not match either of the items we expected to fetch");
228 XCTAssertTrue([secondFetchedObject isEqual:firstItem] || [secondFetchedObject isEqual:secondItem], @"second fetched object does not match either of the items we expected to fetch");
229 XCTAssertFalse([firstFetchedObject isEqual:secondFetchedObject], @"the objects we got back from our query are unexpectedly equal to each other");
230 XCTAssertFalse([items containsObject:thirdItem.metadata], @"found an unexpected item in our fetch results");
231 }
232 [fetchExpectation fulfill];
233 }];
234 [self waitForExpectations:@[fetchExpectation] timeout:5.0];
235 }
236
237 - (void)testDuplicates
238 {
239 // TODO: test some duplicate-rejection scenarios, including the case where no primary keys are explicitly defined
240 }
241
242 - (void)testAccessGroups
243 {
244 [_connection setFakeAccessGroups:[NSArray arrayWithObject:@"TestAccessGroup"]];
245
246 // first we'll try inserting in an access group that should be rejected
247 SecCDKeychainAccessControlEntity* badOwner = [SecCDKeychainAccessControlEntity accessControlEntityWithType:SecCDKeychainAccessControlEntityTypeAccessGroup stringRepresentation:@"BadAccessGroup"];
248 NSUUID* persistentID = [NSUUID UUID];
249 SecCDKeychainItem* badItem = [[SecCDKeychainItem alloc] initItemType:[TestItemType itemType] withPersistentID:persistentID attributes:@{@"key" : @"value"} lookupAttributes:nil secrets:@{@"data" : @"MySecret"} owner:badOwner keyclass:key_class_ak];
250
251 XCTestExpectation* badAccessGroupInsertExpectation = [self expectationWithDescription:@"items insert with bad access group"];
252 [_keychain insertItems:@[badItem] withConnection:_connection completionHandler:^(bool success, NSError* error) {
253 XCTAssertFalse(success, @"should have rejected insert of item for a bad access group");
254 XCTAssertNotNil(error, @"should have encountered an error for inserting with bad access group");
255 XCTAssertEqualObjects(error.domain, SFKeychainErrorDomain, @"should have gotten an error in the SFKeychainErrorDomain");
256 XCTAssertEqual(error.code, SFKeychainErrorInvalidAccessGroup, @"should have gotten the SFKeychainErrorInvalidAccessGroup error code");
257 [badAccessGroupInsertExpectation fulfill];
258 }];
259 [self waitForExpectations:@[badAccessGroupInsertExpectation] timeout:5.0];
260
261 // ok, now try an insertion that should succeed
262 SecCDKeychainAccessControlEntity* goodOwner = [SecCDKeychainAccessControlEntity accessControlEntityWithType:SecCDKeychainAccessControlEntityTypeAccessGroup stringRepresentation:@"TestAccessGroup"];
263 SecCDKeychainItem* item = [[SecCDKeychainItem alloc] initItemType:[TestItemType itemType] withPersistentID:persistentID attributes:@{@"key" : @"value"} lookupAttributes:nil secrets:@{@"data" : @"MySecret"} owner:goodOwner keyclass:key_class_ak];
264
265 XCTestExpectation* insertExpectation = [self expectationWithDescription:@"items inserted"];
266 [_keychain insertItems:@[item] withConnection:_connection completionHandler:^(bool success, NSError* error) {
267 XCTAssertTrue(success, @"should have been able to insert item");
268 XCTAssertNil(error, @"should not have gotten an error inserting item");
269 [insertExpectation fulfill];
270 }];
271 [self waitForExpectations:@[insertExpectation] timeout:5.0];
272
273 XCTestExpectation* fetchExpectation = [self expectationWithDescription:@"fetch item"];
274 [_keychain fetchItemForPersistentID:persistentID withConnection:_connection completionHandler:^(SecCDKeychainItem* fetchedItem, NSError* error) {
275 XCTAssertNotNil(fetchedItem, @"should have been able to fetch item");
276 XCTAssertNil(error, @"should not have gotten error fetching item");
277 XCTAssertEqualObjects(fetchedItem, item, @"fetched item should match the inserted item");
278 [fetchExpectation fulfill];
279 }];
280 [self waitForExpectations:@[fetchExpectation] timeout:5.0];
281
282 // now change my access group and see if I get the expected error trying to fetch the item
283 [_connection setFakeAccessGroups:[NSArray arrayWithObject:@"NotYourAccessGroup"]];
284
285 XCTestExpectation* badFetchExpectation = [self expectationWithDescription:@"fetch item with bad access group"];
286 [_keychain fetchItemForPersistentID:persistentID withConnection:_connection completionHandler:^(SecCDKeychainItem* fetchedItem, NSError* error) {
287 XCTAssertNil(fetchedItem, @"should not have gotten item when fetching with a bad access group");
288 XCTAssertNotNil(error, @"should have gotten an error fetching item");
289 XCTAssertEqualObjects(error.domain, SFKeychainErrorDomain, @"should have gotten an error in the SFKeychainErrorDomain");
290 XCTAssertEqual(error.code, SFKeychainErrorItemNotFound, @"should have gotten the SFKeychainErrorItemNotFound error code");
291 [badFetchExpectation fulfill];
292 }];
293 [self waitForExpectations:@[badFetchExpectation] timeout:5.0];
294
295 // now try to delete the thing to confirm we cannot
296
297 XCTestExpectation* badDeleteExpectation = [self expectationWithDescription:@"delete item with bad access group"];
298 [_keychain deleteItemWithPersistentID:persistentID withConnection:_connection completionHandler:^(bool success, NSError* error) {
299 XCTAssertFalse(success, @"should not have succeeded at deleting item we don't have access to");
300 XCTAssertNotNil(error, @"should have gotten an error deleting item we don't have access to");
301 XCTAssertEqualObjects(error.domain, SFKeychainErrorDomain, @"should have gotten an error in the SFKeychainErrorDomain");
302 XCTAssertEqual(error.code, SFKeychainErrorItemNotFound, @"should have gotten the SFKeychainErrorItemNotFound error code");
303 [badDeleteExpectation fulfill];
304 }];
305 [self waitForExpectations:@[badDeleteExpectation] timeout:5.0];
306
307 // switch to the good access group to make sure it's still there
308 [_connection setFakeAccessGroups:[NSArray arrayWithObject:@"TestAccessGroup"]];
309
310 XCTestExpectation* fetchAgainExpecation = [self expectationWithDescription:@"fetch item again"];
311 [_keychain fetchItemForPersistentID:persistentID withConnection:_connection completionHandler:^(SecCDKeychainItem* fetchedItem, NSError* error) {
312 XCTAssertNotNil(fetchedItem, @"should have been able to fetch item");
313 XCTAssertNil(error, @"should not have gotten error fetching item");
314 XCTAssertEqualObjects(fetchedItem, item, @"fetched item should match the inserted item");
315 [fetchAgainExpecation fulfill];
316 }];
317 [self waitForExpectations:@[fetchAgainExpecation] timeout:5.0];
318
319 // and finally delete with proper permissions
320
321 XCTestExpectation* deleteExpectation = [self expectationWithDescription:@"delete item"];
322 [_keychain deleteItemWithPersistentID:persistentID withConnection:_connection completionHandler:^(bool success, NSError* error) {
323 XCTAssertTrue(success, @"should have succeeded deleting item");
324 XCTAssertNil(error, @"should not have gotten error deleting item: %@", error);
325 [deleteExpectation fulfill];
326 }];
327 [self waitForExpectations:@[deleteExpectation] timeout:5.0];
328
329 XCTestExpectation* postDeleteFetchExpectation = [self expectationWithDescription:@"fetch item after delete"];
330 [_keychain fetchItemForPersistentID:persistentID withConnection:_connection completionHandler:^(SecCDKeychainItem* fetchedItem, NSError* error) {
331 XCTAssertNil(fetchedItem, @"should not have gotten item when fetching with a bad access group");
332 XCTAssertNotNil(error, @"should have gotten error for fetch of deleted item");
333 XCTAssertEqualObjects(error.domain, SFKeychainErrorDomain, @"should have gotten an error in the SFKeychainErrorDomain");
334 XCTAssertEqual(error.code, SFKeychainErrorItemNotFound, @"should have gotten the SFKeychainErrorItemNotFound error code");
335 [postDeleteFetchExpectation fulfill];
336 }];
337 [self waitForExpectations:@[postDeleteFetchExpectation] timeout:5.0];
338 }
339
340 - (void)testDifferentOwnersWithSimilarItems
341 {
342 [_connection setFakeAccessGroups:[NSArray arrayWithObject:@"FirstAccessGroup"]];
343 SecCDKeychainAccessControlEntity* firstOwner = [SecCDKeychainAccessControlEntity accessControlEntityWithType:SecCDKeychainAccessControlEntityTypeAccessGroup stringRepresentation:@"FirstAccessGroup"];
344
345 NSUUID* firstPersistentID = [NSUUID UUID];
346 SecCDKeychainItem* firstItem = [[SecCDKeychainItem alloc] initItemType:[TestItemType itemType] withPersistentID:firstPersistentID attributes:@{@"key" : @"value"} lookupAttributes:nil secrets:@{@"data" : @"MyFirstSecret"} owner:firstOwner keyclass:key_class_ak];
347
348 XCTestExpectation* firstInsertExpectation = [self expectationWithDescription:@"insert first item"];
349 [_keychain insertItems:@[firstItem] withConnection:_connection completionHandler:^(bool success, NSError* error) {
350 XCTAssertTrue(success, @"should have been able to insert item");
351 XCTAssertNil(error, @"should not have gotten an error inserting item");
352 [firstInsertExpectation fulfill];
353 }];
354 [self waitForExpectations:@[firstInsertExpectation] timeout:5.0];
355
356 [_connection setFakeAccessGroups:[NSArray arrayWithObject:@"SecondAccessGroup"]];
357 SecCDKeychainAccessControlEntity* secondOwner = [SecCDKeychainAccessControlEntity accessControlEntityWithType:SecCDKeychainAccessControlEntityTypeAccessGroup stringRepresentation:@"SecondAccessGroup"];
358
359 NSUUID* secondPersistentID = [NSUUID UUID];
360 SecCDKeychainItem* secondItem = [[SecCDKeychainItem alloc] initItemType:[TestItemType itemType] withPersistentID:secondPersistentID attributes:@{@"key" : @"value"} lookupAttributes:nil secrets:@{@"data" : @"MySecondSecret"} owner:secondOwner keyclass:key_class_ak];
361
362 XCTestExpectation* secondInsertExpectation = [self expectationWithDescription:@"insert item"];
363 [_keychain insertItems:@[secondItem] withConnection:_connection completionHandler:^(bool success, NSError* error) {
364 XCTAssertTrue(success, @"should have been able to insert item");
365 XCTAssertNil(error, @"should not have gotten an error inserting item");
366 [secondInsertExpectation fulfill];
367 }];
368 [self waitForExpectations:@[secondInsertExpectation] timeout:5.0];
369
370 // now try fetching the first and second items
371 // we should have access to the second but not the first
372
373 XCTestExpectation* firstFetchExpectation = [self expectationWithDescription:@"fetch first item"];
374 [_keychain fetchItemForPersistentID:firstPersistentID withConnection:_connection completionHandler:^(SecCDKeychainItem* fetchedItem, NSError* error) {
375 XCTAssertNil(fetchedItem, @"should not have been able to fetch item from first access group");
376 XCTAssertNotNil(error, @"should have gotten an error fetching item");
377 XCTAssertEqualObjects(error.domain, SFKeychainErrorDomain, @"should have gotten an error in the SFKeychainErrorDomain");
378 XCTAssertEqual(error.code, SFKeychainErrorItemNotFound, @"should have gotten the SFKeychainErrorItemNotFound error code");
379 [firstFetchExpectation fulfill];
380 }];
381 [self waitForExpectations:@[firstFetchExpectation] timeout:5.0];
382
383 XCTestExpectation* secondFetchExpectation = [self expectationWithDescription:@"fetch second item"];
384 [_keychain fetchItemForPersistentID:secondPersistentID withConnection:_connection completionHandler:^(SecCDKeychainItem* fetchedItem, NSError* error) {
385 XCTAssertNotNil(fetchedItem, @"should have been able to fetch item");
386 XCTAssertNil(error, @"should not have gotten an error fetching item");
387 XCTAssertEqualObjects(fetchedItem, secondItem, @"fetched item should match inserted item");
388 [secondFetchExpectation fulfill];
389 }];
390 [self waitForExpectations:@[secondFetchExpectation] timeout:5.0];
391
392 // finally, do a search; make sure it returns exactly one result
393 XCTestExpectation* searchExpectation = [self expectationWithDescription:@"search items"];
394 [_keychain fetchItemsWithValue:@"value" forLookupKey:@"key" ofType:SecCDKeychainLookupValueTypeString withConnection:_connection completionHandler:^(NSArray<SecCDKeychainItemMetadata*>* items, NSError* error) {
395 XCTAssertEqual(items.count, (unsigned long)1, @"should have gotten 1 search result, but got %d", (int)items.count);
396 SecCDKeychainItem* fetchedItem = [self fullItemForItemMetadata:items.firstObject];
397 XCTAssertNotNil(fetchedItem, @"should have been able to fetch full item");
398 XCTAssertNil(error, @"should not have gotten error looking up item");
399 XCTAssertEqualObjects(fetchedItem, secondItem, @"item we looked up should match the one we inserted");
400 [searchExpectation fulfill];
401 }];
402 [self waitForExpectations:@[searchExpectation] timeout:5.0];
403 }
404
405 - (void)testTamperedMetadata
406 {
407 SecCDKeychainAccessControlEntity* owner = [SecCDKeychainAccessControlEntity accessControlEntityWithType:SecCDKeychainAccessControlEntityTypeAccessGroup stringRepresentation:@"com.apple.token"];
408
409 NSUUID* persistentID = [NSUUID UUID];
410 SecCDKeychainItem* item = [[SecCDKeychainItem alloc] initItemType:[TestItemType itemType] withPersistentID:persistentID attributes:@{@"key" : @"value"} lookupAttributes:nil secrets:@{@"data" : @"MyFirstSecret"} owner:owner keyclass:key_class_ak];
411
412 XCTestExpectation* insertExpectation = [self expectationWithDescription:@"insert item"];
413 [_keychain insertItems:@[item] withConnection:_connection completionHandler:^(bool success, NSError* error) {
414 XCTAssertTrue(success, @"should have been able to insert item");
415 XCTAssertNil(error, @"should not have gotten an error inserting item");
416 [insertExpectation fulfill];
417 }];
418 [self waitForExpectations:@[insertExpectation] timeout:5.0];
419
420 XCTestExpectation* tamperExpectation = [self expectationWithDescription:@"tamper with item"];
421 [_keychain performOnManagedObjectQueue:^(NSManagedObjectContext* managedObjectContext, NSError* managedObjectError) {
422 XCTAssertNotNil(managedObjectContext, @"should have gotten a managed object to perform work with");
423 XCTAssertNil(managedObjectError, @"should not get error performing a block on the managed object context");
424
425 NSError* error = nil;
426 SecCDKeychainManagedItem* managedItem = [self->_keychain fetchManagedItemForPersistentID:persistentID withManagedObjectContext:managedObjectContext error:&error];
427 XCTAssertNotNil(managedItem, @"should have been able to fetch the managed item we just inserted");
428 XCTAssertNil(error, @"should not have gotten an error fetching the managed item");
429
430 // ok, now let's do something nefarious, like change the metadata
431 NSDictionary* fakeMetadata = @{@"key" : @"differentValue"};
432 managedItem.metadata = [NSPropertyListSerialization dataWithPropertyList:fakeMetadata format:NSPropertyListXMLFormat_v1_0 options:9 error:&error];
433 XCTAssertNotNil(managedItem.metadata, @"should have been able to write modified metadata");
434 XCTAssertNil(error, @"should not have gotten an error writing modified metadata");
435
436 SecCDKeychainItem* brokenItem = [[SecCDKeychainItem alloc] initWithManagedItem:managedItem keychain:self->_keychain error:&error];
437 XCTAssertNil(brokenItem, @"should not have been able to create item with tampered metadata");
438 XCTAssertNotNil(error, @"should have gotten an error attempting to create an item with tampered metadata");
439 XCTAssertEqualObjects(error.domain, SFKeychainErrorDomain, @"should have gotten an error in the SFKeychainErrorDomain");
440 XCTAssertEqual(error.code, SFKeychainErrorItemDecryptionFailed, @"should have gotten the SFKeychainErrorItemDecryptionFailed error");
441
442 [tamperExpectation fulfill];
443 }];
444 [self waitForExpectations:@[tamperExpectation] timeout:5.0];
445 }
446
447 @end
448
449 // these tests do not wish to have a keychain already setup
450 @interface CDKeychainSetupTests : KeychainXCTest
451 @end
452
453 @implementation CDKeychainSetupTests {
454 SFKeychainServerFakeConnection* _connection;
455 }
456
457 - (void)setUp
458 {
459 [super setUp];
460
461 _connection = [[SFKeychainServerFakeConnection alloc] init];
462 }
463
464 - (void)testKeychainLocking
465 {
466 self.lockState = LockStateLockedAndDisallowAKS;
467
468 NSURL* persistentStoreURL = (__bridge_transfer NSURL*)SecCopyURLForFileInKeychainDirectory((__bridge CFStringRef)@"CDKeychain");
469 NSBundle* resourcesBundle = [NSBundle bundleWithPath:@"/System/Library/Keychain/KeychainResources.bundle"];
470 NSURL* managedObjectModelURL = [resourcesBundle URLForResource:@"KeychainModel" withExtension:@"momd"];
471 SecCDKeychain* keychain = [[SecCDKeychain alloc] initWithStorageURL:persistentStoreURL modelURL:managedObjectModelURL encryptDatabase:false];
472 XCTAssertNotNil(keychain, @"should have been able to create a keychain instance");
473
474 self.keychainPartialMock = OCMPartialMock(keychain);
475 [[[[self.keychainPartialMock stub] andCall:@selector(getDatabaseKeyDataithError:) onObject:self] ignoringNonObjectArgs] _onQueueGetDatabaseKeyDataWithError:NULL];
476
477 SecCDKeychainAccessControlEntity* owner = [SecCDKeychainAccessControlEntity accessControlEntityWithType:SecCDKeychainAccessControlEntityTypeAccessGroup stringRepresentation:@"com.apple.token"];
478
479 NSUUID* persistentID = [NSUUID UUID];
480 SecCDKeychainItem* item = [[SecCDKeychainItem alloc] initItemType:[TestItemType itemType] withPersistentID:persistentID attributes:@{@"key" : @"value"} lookupAttributes:nil secrets:@{@"data" : @"MySecret"} owner:owner keyclass:key_class_ak];
481
482 XCTestExpectation* insertExpectation = [self expectationWithDescription:@"insert item"];
483 [keychain insertItems:@[item] withConnection:_connection completionHandler:^(bool success, NSError* error) {
484 XCTAssertFalse(success, @"should not be able to insert item while locked");
485 XCTAssertNotNil(error, @"should get error inserting item while locked");
486
487 // in the future, check for proper error
488 // // <rdar://problem/38972671> add SFKeychainErrorDeviceLocked
489
490 [insertExpectation fulfill];
491 }];
492 [self waitForExpectations:@[insertExpectation] timeout:5.0];
493
494 self.lockState = LockStateUnlocked;
495
496 [keychain _registerItemTypeForTesting:[TestItemType itemType]];
497
498 XCTestExpectation* insertAgainExpectation = [self expectationWithDescription:@"insert item again"];
499 [keychain insertItems:@[item] withConnection:_connection completionHandler:^(bool success, NSError* error) {
500 XCTAssertTrue(success, @"should be able to insert item after unlock");
501 XCTAssertNil(error, @"should not get error inserting after unlock");
502 [insertAgainExpectation fulfill];
503 }];
504 [self waitForExpectations:@[insertAgainExpectation] timeout:5.0];
505
506 XCTestExpectation* fetchExpectation = [self expectationWithDescription:@"fetch item"];
507 [keychain fetchItemForPersistentID:persistentID withConnection:_connection completionHandler:^(SecCDKeychainItem* fetchedItem, NSError* error) {
508 XCTAssertNotNil(fetchedItem, @"should have been able to fetch item");
509 XCTAssertNil(error, @"should not have gotten an error fetching item");
510 XCTAssertEqualObjects(fetchedItem, item, @"fetched item should match inserted item");
511 [fetchExpectation fulfill];
512 }];
513 [self waitForExpectations:@[fetchExpectation] timeout:5.0];
514
515 self.lockState = LockStateLockedAndDisallowAKS;
516 dispatch_sync(keychain.queue, ^{
517 [keychain _onQueueDropClassAPersistentStore];
518 });
519
520 XCTestExpectation* fetchWhileLockedExpectation = [self expectationWithDescription:@"fetch item while locked"];
521 [keychain fetchItemForPersistentID:persistentID withConnection:_connection completionHandler:^(SecCDKeychainItem* fetchedItem, NSError* error) {
522 XCTAssertNil(fetchedItem, @"should not be able to fetch item while locked");
523 XCTAssertNotNil(error, @"should get an error fetching item while locked");
524
525 // in the future, check for proper error
526 // // <rdar://problem/38972671> add SFKeychainErrorDeviceLocked
527
528 [fetchWhileLockedExpectation fulfill];
529 }];
530 [self waitForExpectations:@[fetchWhileLockedExpectation] timeout:5.0];
531 }
532
533 @end
534
535 #endif