]> git.saurik.com Git - apple/security.git/blob - keychain/ckks/tests/CKKSCloudKitTests.m
Security-58286.200.222.tar.gz
[apple/security.git] / keychain / ckks / tests / CKKSCloudKitTests.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 <XCTest/XCTest.h>
25 #import <CloudKit/CloudKit.h>
26 #import <CloudKit/CloudKit_Private.h>
27
28 #import <Security/SecureObjectSync/SOSViews.h>
29 #import <utilities/SecFileLocations.h>
30 #import <securityd/SecItemServer.h>
31 #if NO_SERVER
32 #include <securityd/spi.h>
33 #endif
34
35 #import "keychain/ckks/CKKS.h"
36 #import "keychain/ckks/CKKSViewManager.h"
37 #import "keychain/ckks/CKKSKeychainView.h"
38 #import "keychain/ckks/CKKSCurrentKeyPointer.h"
39 #import "keychain/ckks/CKKSMirrorEntry.h"
40 #import "keychain/ckks/CKKSItemEncrypter.h"
41 #import "keychain/ckks/CloudKitCategories.h"
42 #import "keychain/categories/NSError+UsefulConstructors.h"
43 #import "keychain/ckks/tests/MockCloudKit.h"
44
45 @interface CKKSCloudKitTests : XCTestCase
46
47 @property NSOperationQueue *operationQueue;
48 @property CKContainer *container;
49 @property CKDatabase *database;
50 @property CKKSKeychainView *kcv;
51 @property NSString *zoneName;
52 @property CKRecordZoneID *zoneID;
53 @property NSDictionary *remoteItems;
54 @property NSInteger queueTimeout;
55
56 @end
57
58 // TODO: item modification should up gencount, check this
59
60 @implementation CKKSCloudKitTests
61
62 #if OCTAGON
63
64 #pragma mark Setup
65
66 + (void)setUp {
67 SecCKKSResetSyncing();
68 SecCKKSTestsEnable();
69 SecCKKSSetReduceRateLimiting(true);
70 [super setUp];
71
72 #if NO_SERVER
73 securityd_init_local_spi();
74 #endif
75 }
76
77 - (void)setUp {
78 self.remoteItems = nil;
79 self.queueTimeout = 900; // CloudKit can be *very* slow, and some tests upload a lot of items indeed
80 NSString *containerName = [NSString stringWithFormat:@"com.apple.test.p01.B.com.apple.security.keychain.%@", [[NSUUID new] UUIDString]];
81 self.container = [CKContainer containerWithIdentifier:containerName];
82
83 SecCKKSTestResetFlags();
84 SecCKKSTestSetDisableSOS(true);
85
86 self.operationQueue = [NSOperationQueue new];
87
88 CKKSViewManager* manager = [[CKKSViewManager alloc] initWithContainerName:containerName
89 usePCS:SecCKKSContainerUsePCS
90 fetchRecordZoneChangesOperationClass:[CKFetchRecordZoneChangesOperation class]
91 fetchRecordsOperationClass:[CKFetchRecordsOperation class]
92 queryOperationClass:[CKQueryOperation class]
93 modifySubscriptionsOperationClass:[CKModifySubscriptionsOperation class]
94 modifyRecordZonesOperationClass:[CKModifyRecordZonesOperation class]
95 apsConnectionClass:[APSConnection class]
96 nsnotificationCenterClass:[NSNotificationCenter class]
97 notifierClass:[FakeCKKSNotifier class]];
98 [CKKSViewManager resetManager:false setTo:manager];
99
100 // Make a new fake keychain
101 NSString* smallName = [self.name componentsSeparatedByString:@" "][1];
102 smallName = [smallName stringByReplacingOccurrencesOfString:@"]" withString:@""];
103
104 NSString* tmp_dir = [NSString stringWithFormat: @"/tmp/%@.%X", smallName, arc4random()];
105 [[NSFileManager defaultManager] createDirectoryAtPath:[NSString stringWithFormat: @"%@/Library/Keychains", tmp_dir] withIntermediateDirectories:YES attributes:nil error:NULL];
106
107 SetCustomHomeURLString((__bridge CFStringRef) tmp_dir);
108 SecKeychainDbReset(NULL);
109 // Actually load the database.
110 kc_with_dbt(true, NULL, ^bool (SecDbConnectionRef dbt) { return false; });
111
112 self.zoneName = @"keychain";
113 self.zoneID = [[CKRecordZoneID alloc] initWithZoneName:self.zoneName ownerName:CKCurrentUserDefaultName];
114 self.kcv = [[CKKSViewManager manager] findOrCreateView:@"keychain"];
115 }
116
117 - (void)tearDown {
118 self.remoteItems = nil;
119 [[CKKSViewManager manager] clearView:@"keychain"];
120 SecCKKSTestResetFlags();
121 }
122
123 + (void)tearDown {
124 SecCKKSResetSyncing();
125 }
126
127 #pragma mark Helpers
128
129 - (BOOL)waitForEmptyOutgoingQueue:(CKKSKeychainView *)view {
130 [view processOutgoingQueue:[CKOperationGroup CKKSGroupWithName:@"waitForEmptyOutgoingQueue"]];
131 NSInteger secondsToWait = self.queueTimeout;
132 while (true) {
133 if ([view outgoingQueueEmpty:nil]) {
134 return YES;
135 }
136 [NSThread sleepForTimeInterval:1];
137 if (--secondsToWait % 60 == 0) {
138 long minutesWaited = (self.queueTimeout - secondsToWait)/60;
139 NSLog(@"Waiting %ld minute%@ for empty outgoingQueue", minutesWaited, minutesWaited > 1 ? @"s" : @"");
140 }
141 if (secondsToWait <= 0) {
142 XCTFail(@"Timed out waiting for '%@' OutgoingQueue to become empty", view);
143 NSLog(@"Giving up waiting for empty outgoingQueue");
144 return NO;
145 }
146
147 }
148 }
149
150 - (void)startCKKSSubsystem {
151 // TODO: we removed this mechanism, but haven't tested to see if these tests still succeed
152 }
153
154 - (NSMutableDictionary *)fetchLocalItems {
155 NSDictionary *query = @{(id)kSecClass : (id)kSecClassGenericPassword,
156 (id)kSecAttrSynchronizable : (id)kCFBooleanTrue,
157 (id)kSecReturnAttributes : (id)kCFBooleanTrue,
158 (id)kSecReturnData : (id)kCFBooleanTrue,
159 (id)kSecMatchLimit : (id)kSecMatchLimitAll,
160 };
161 CFTypeRef cfresults;
162 OSStatus status = SecItemCopyMatching((__bridge CFDictionaryRef) query, &cfresults);
163 XCTAssert(status == errSecSuccess || status == errSecItemNotFound, @"retrieved zero or more local items");
164 if (status == errSecItemNotFound) {
165 return [NSMutableDictionary new];
166 } else if (status != errSecSuccess) {
167 return nil;
168 }
169
170 NSArray *results = CFBridgingRelease(cfresults);
171
172 NSMutableDictionary *ret = [NSMutableDictionary new];
173 for (NSMutableDictionary *item in results) {
174 ret[item[@"UUID"]] = item;
175 }
176
177 return ret;
178 }
179
180 - (NSMutableDictionary *)fetchRemoteItems {
181 CKFetchRecordZoneChangesConfiguration *options = [CKFetchRecordZoneChangesConfiguration new];
182 options.previousServerChangeToken = nil;
183
184 CKFetchRecordZoneChangesOperation *op = [[CKFetchRecordZoneChangesOperation alloc] initWithRecordZoneIDs:@[self.zoneID] configurationsByRecordZoneID:@{self.zoneID : options}];
185 op.configuration.automaticallyRetryNetworkFailures = NO;
186 op.configuration.discretionaryNetworkBehavior = CKOperationDiscretionaryNetworkBehaviorNonDiscretionary;
187 op.configuration.container = self.container;
188
189 __block NSMutableDictionary *data = [NSMutableDictionary new];
190 __block NSUInteger synckeys = 0;
191 __block NSUInteger currkeys = 0;
192 op.recordChangedBlock = ^(CKRecord *record) {
193 if ([record.recordType isEqualToString:SecCKRecordItemType]) {
194 data[record.recordID.recordName] = [self decryptRecord:record];
195 } else if ([record.recordType isEqualToString:SecCKRecordIntermediateKeyType]) {
196 synckeys += 1;
197 } else if ([record.recordType isEqualToString:SecCKRecordCurrentKeyType]) {
198 currkeys += 1;
199 } else {
200 XCTFail(@"Encountered unexpected item %@", record);
201 }
202 };
203
204 dispatch_semaphore_t sema = dispatch_semaphore_create(0);
205 op.recordZoneFetchCompletionBlock = ^(CKRecordZoneID * _Nonnull recordZoneID,
206 CKServerChangeToken * _Nullable serverChangeToken,
207 NSData * _Nullable clientChangeTokenData,
208 BOOL moreComing,
209 NSError * _Nullable recordZoneError) {
210 XCTAssertNil(recordZoneError, @"No error in recordZoneFetchCompletionBlock");
211 if (!moreComing) {
212 dispatch_semaphore_signal(sema);
213 }
214 };
215
216 [op start];
217 dispatch_semaphore_wait(sema, DISPATCH_TIME_FOREVER);
218
219 // These are new, fresh zones for each test. There should not be old keys yet.
220 if (synckeys != 3) {XCTFail(@"Unexpected number of synckeys: %lu", (unsigned long)synckeys);}
221 if (currkeys != 3) {XCTFail(@"Unexpected number of current keys: %lu", (unsigned long)currkeys);}
222
223 self.remoteItems = data;
224 return data;
225 }
226
227 - (BOOL)compareLocalItem:(NSDictionary *)lhs remote:(NSDictionary *)rhs {
228 if ([lhs[@"cdat"] compare: rhs[@"cdat"]] != NSOrderedSame) {XCTFail(@"Creation date differs"); return NO;}
229 if ([lhs[@"mdat"] compare: rhs[@"mdat"]] != NSOrderedSame) {XCTFail(@"Modification date differs"); return NO;}
230 if (![lhs[@"agrp"] isEqualToString:rhs[@"agrp"]]) {XCTFail(@"Access group differs"); return NO;}
231 if (![lhs[@"acct"] isEqualToString:rhs[@"acct"]]) {XCTFail(@"Account differs"); return NO;}
232 if (![lhs[@"v_Data"] isEqualToData:rhs[@"v_Data"]]) {XCTFail(@"Data differs"); return NO;}
233 // class for lhs is already genp due to copymatching query
234 if (![rhs[@"class"] isEqualToString:@"genp"]) {XCTFail(@"Class not genp for remote item"); return NO;}
235 return YES;
236 }
237
238 - (BOOL)compareLocalKeychainWithCloudKitState {
239 BOOL success = YES;
240 NSMutableDictionary *localItems = [self fetchLocalItems];
241 if (localItems == nil) {XCTFail(@"Received nil for localItems"); return NO;}
242 NSMutableDictionary *remoteItems = [self fetchRemoteItems];
243 if (remoteItems == nil) {XCTFail(@"Received nil for remoteItems"); return NO;}
244
245 for (NSString *uuid in localItems.allKeys) {
246 if (remoteItems[uuid] == nil) {
247 XCTFail(@"account %@ item %@ not present in remote", localItems[uuid][@"acct"], localItems[uuid]);
248 success = NO;
249 continue;
250 }
251 if (![self compareLocalItem:localItems[uuid] remote:remoteItems[uuid]]) {
252 XCTFail(@"local item %@ matches remote item %@", localItems[uuid], remoteItems[uuid]);
253 success = NO;
254 }
255 [remoteItems removeObjectForKey:uuid];
256 }
257 if ([remoteItems count]) {
258 XCTFail(@"No remote items present not found in local, %@", remoteItems);
259 return NO;
260 }
261 return success;
262 }
263
264 - (BOOL)updateGenericPassword:(NSString *)password account:(NSString *)account {
265 NSDictionary *query = @{(id)kSecClass : (id)kSecClassGenericPassword,
266 (id)kSecAttrAccessGroup : @"com.apple.security.ckks",
267 (id)kSecAttrAccessible: (id)kSecAttrAccessibleAfterFirstUnlock,
268 (id)kSecAttrAccount : account,
269 (id)kSecAttrSynchronizable : (id)kCFBooleanTrue,
270 };
271 NSDictionary *newpasswd = @{(id)kSecValueData : (id) [password dataUsingEncoding:NSUTF8StringEncoding]};
272
273 return errSecSuccess == SecItemUpdate((__bridge CFDictionaryRef)query, (__bridge CFDictionaryRef)newpasswd);
274 }
275
276 - (BOOL)uploadRecords:(NSArray<CKRecord*>*)records {
277 CKModifyRecordsOperation *op = [[CKModifyRecordsOperation alloc] initWithRecordsToSave:records recordIDsToDelete:nil];
278 op.configuration.automaticallyRetryNetworkFailures = NO;
279 op.configuration.discretionaryNetworkBehavior = CKOperationDiscretionaryNetworkBehaviorNonDiscretionary;
280 op.configuration.container = self.container;
281
282 dispatch_semaphore_t sema = dispatch_semaphore_create(0);
283 __block BOOL result = NO;
284 op.modifyRecordsCompletionBlock = ^(NSArray<CKRecord *> *savedRecords,
285 NSArray<CKRecordID *> *deletedRecordIDs,
286 NSError *operationError) {
287 XCTAssertNil(operationError, @"No error uploading records, %@", operationError);
288 if (operationError == nil) {
289 result = YES;
290 }
291 dispatch_semaphore_signal(sema);
292 };
293
294 [op start];
295 dispatch_semaphore_wait(sema, DISPATCH_TIME_FOREVER);
296
297 return result;
298 }
299
300 #pragma mark Helpers Adapted from MockXCTest
301
302 - (CKRecord*)createFakeRecord: (CKRecordZoneID*)zoneID recordName:(NSString*)recordName {
303 return [self createFakeRecord: zoneID recordName:recordName withAccount: nil];
304 }
305
306 - (CKRecord*)createFakeRecord: (CKRecordZoneID*)zoneID recordName:(NSString*)recordName withAccount: (NSString*) account {
307 NSError* error = nil;
308
309 /* Basically: @{
310 @"acct" : @"account-delete-me",
311 @"agrp" : @"com.apple.security.sos",
312 @"cdat" : @"2016-12-21 03:33:25 +0000",
313 @"class" : @"genp",
314 @"mdat" : @"2016-12-21 03:33:25 +0000",
315 @"musr" : [[NSData alloc] init],
316 @"pdmn" : @"ak",
317 @"sha1" : [[NSData alloc] initWithBase64EncodedString: @"C3VWONaOIj8YgJjk/xwku4By1CY=" options:0],
318 @"svce" : @"",
319 @"tomb" : [NSNumber numberWithInt: 0],
320 @"v_Data" : [@"data" dataUsingEncoding: NSUTF8StringEncoding],
321 };
322 TODO: this should be binary encoded instead of expanded, but the plist encoder should handle it fine */
323 NSData* itemdata = [[NSData alloc] initWithBase64EncodedString:@"PD94bWwgdmVyc2lvbj0iMS4wIiBlbmNvZGluZz0iVVRGLTgiPz4KPCFET0NUWVBFIHBsaXN0IFBVQkxJQyAiLS8vQXBwbGUvL0RURCBQTElTVCAxLjAvL0VOIiAiaHR0cDovL3d3dy5hcHBsZS5jb20vRFREcy9Qcm9wZXJ0eUxpc3QtMS4wLmR0ZCI+CjxwbGlzdCB2ZXJzaW9uPSIxLjAiPgo8ZGljdD4KCTxrZXk+YWNjdDwva2V5PgoJPHN0cmluZz5hY2NvdW50LWRlbGV0ZS1tZTwvc3RyaW5nPgoJPGtleT5hZ3JwPC9rZXk+Cgk8c3RyaW5nPmNvbS5hcHBsZS5zZWN1cml0eS5zb3M8L3N0cmluZz4KCTxrZXk+Y2RhdDwva2V5PgoJPGRhdGU+MjAxNi0xMi0yMVQwMzozMzoyNVo8L2RhdGU+Cgk8a2V5PmNsYXNzPC9rZXk+Cgk8c3RyaW5nPmdlbnA8L3N0cmluZz4KCTxrZXk+bWRhdDwva2V5PgoJPGRhdGU+MjAxNi0xMi0yMVQwMzozMzoyNVo8L2RhdGU+Cgk8a2V5Pm11c3I8L2tleT4KCTxkYXRhPgoJPC9kYXRhPgoJPGtleT5wZG1uPC9rZXk+Cgk8c3RyaW5nPmFrPC9zdHJpbmc+Cgk8a2V5PnNoYTE8L2tleT4KCTxkYXRhPgoJQzNWV09OYU9JajhZZ0pqay94d2t1NEJ5MUNZPQoJPC9kYXRhPgoJPGtleT5zdmNlPC9rZXk+Cgk8c3RyaW5nPjwvc3RyaW5nPgoJPGtleT50b21iPC9rZXk+Cgk8aW50ZWdlcj4wPC9pbnRlZ2VyPgoJPGtleT52X0RhdGE8L2tleT4KCTxkYXRhPgoJWkdGMFlRPT0KCTwvZGF0YT4KPC9kaWN0Pgo8L3BsaXN0Pgo=" options:0];
324 NSMutableDictionary * item = [[NSPropertyListSerialization propertyListWithData:itemdata
325 options:0
326 format:nil
327 error:&error] mutableCopy];
328 // Fix up dictionary
329 item[@"agrp"] = @"com.apple.security.ckks";
330
331 XCTAssertNil(error, "interpreted data as item");
332
333 if(account) {
334 [item setObject: account forKey: (__bridge id) kSecAttrAccount];
335 }
336
337 CKRecordID* ckrid = [[CKRecordID alloc] initWithRecordName:recordName zoneID:zoneID];
338 return [self newRecord: ckrid withNewItemData: item];
339 }
340
341 - (CKRecord*)newRecord: (CKRecordID*) recordID withNewItemData:(NSDictionary*) dictionary {
342 NSError* error = nil;
343 CKKSKey* classCKey = [CKKSKey currentKeyForClass:SecCKKSKeyClassC zoneID:recordID.zoneID error:&error];
344 XCTAssertNotNil(classCKey, "Have class C key for zone");
345
346 CKKSItem* cipheritem = [CKKSItemEncrypter encryptCKKSItem:[[CKKSItem alloc] initWithUUID:recordID.recordName
347 parentKeyUUID:classCKey.uuid
348 zoneID:recordID.zoneID]
349 dataDictionary:dictionary
350 updatingCKKSItem:nil
351 parentkey:classCKey
352 error:&error];
353
354 CKKSOutgoingQueueEntry* ciphertext = [[CKKSOutgoingQueueEntry alloc] initWithCKKSItem:cipheritem
355 action:SecCKKSActionAdd
356 state:SecCKKSStateNew
357 waitUntil:nil
358 accessGroup:@"unused in this function"];
359 XCTAssertNil(error, "encrypted item with class c key");
360
361 CKRecord* ckr = [ciphertext.item CKRecordWithZoneID: recordID.zoneID];
362 XCTAssertNotNil(ckr, "Created a CKRecord");
363 return ckr;
364 }
365
366 - (NSDictionary*)decryptRecord: (CKRecord*) record {
367 CKKSMirrorEntry* ckme = [[CKKSMirrorEntry alloc] initWithCKRecord: record];
368
369 NSError* error = nil;
370
371 NSDictionary* ret = [CKKSItemEncrypter decryptItemToDictionary:ckme.item error:&error];
372 XCTAssertNil(error);
373 XCTAssertNotNil(ret);
374 return ret;
375 }
376
377 - (BOOL)addMultiplePasswords:(NSString *)password account:(NSString *)account amount:(NSUInteger)amount {
378 while (amount > 0) {
379 if (![self addGenericPassword:password account:[NSString stringWithFormat:@"%@%03lu", account, amount]]) {
380 return NO;
381 }
382 amount -= 1;
383 }
384 return YES;
385 }
386
387 - (BOOL)deleteMultiplePasswords:(NSString *)account amount:(NSUInteger)amount {
388 while (amount > 0) {
389 if (![self deleteGenericPassword:[NSString stringWithFormat:@"%@%03lu", account, amount]]) {
390 return NO;
391 }
392 amount -= 1;
393 }
394 return YES;
395 }
396
397 - (BOOL)addGenericPassword: (NSString*) password account: (NSString*) account viewHint: (NSString*) viewHint expecting: (OSStatus) status message: (NSString*) message {
398 NSMutableDictionary* query = [@{
399 (id)kSecClass : (id)kSecClassGenericPassword,
400 (id)kSecAttrAccessGroup : @"com.apple.security.ckks",
401 (id)kSecAttrAccessible: (id)kSecAttrAccessibleAfterFirstUnlock,
402 (id)kSecAttrAccount : account,
403 (id)kSecAttrSynchronizable : (id)kCFBooleanTrue,
404 (id)kSecValueData : (id) [password dataUsingEncoding:NSUTF8StringEncoding],
405 } mutableCopy];
406
407 if(viewHint) {
408 query[(id)kSecAttrSyncViewHint] = viewHint;
409 }
410
411 return status == SecItemAdd((__bridge CFDictionaryRef) query, NULL);
412 }
413
414 - (BOOL)addGenericPassword: (NSString*) password account: (NSString*) account expecting: (OSStatus) status message: (NSString*) message {
415 return [self addGenericPassword:password account:account viewHint:nil expecting:errSecSuccess message:message];
416 }
417
418 - (BOOL)addGenericPassword: (NSString*) password account: (NSString*) account {
419 return [self addGenericPassword:password account:account viewHint:nil expecting:errSecSuccess message:@"Add item to keychain"];
420 }
421
422 - (BOOL)addGenericPassword: (NSString*) password account: (NSString*) account viewHint:(NSString*)viewHint {
423 return [self addGenericPassword:password account:account viewHint:viewHint expecting:errSecSuccess message:@"Add item to keychain with a viewhint"];
424 }
425
426 - (BOOL)deleteGenericPassword: (NSString*) account {
427 NSDictionary* query = @{(id)kSecClass : (id)kSecClassGenericPassword,
428 (id)kSecAttrAccount : account,
429 (id)kSecAttrSynchronizable : (id)kCFBooleanTrue,};
430
431 return errSecSuccess == SecItemDelete((__bridge CFDictionaryRef) query);
432 }
433
434 - (BOOL)findGenericPassword: (NSString*) account expecting: (OSStatus) status {
435 NSDictionary *query = @{(id)kSecClass : (id)kSecClassGenericPassword,
436 (id)kSecAttrAccessGroup : @"com.apple.security.ckks",
437 (id)kSecAttrAccount : account,
438 (id)kSecAttrSynchronizable : (id)kCFBooleanTrue,
439 (id)kSecMatchLimit : (id)kSecMatchLimitOne,};
440
441 return status == SecItemCopyMatching((__bridge CFDictionaryRef) query, NULL);
442 }
443
444 - (void)checkGenericPassword: (NSString*) password account: (NSString*) account {
445 NSDictionary *query = @{(id)kSecClass : (id)kSecClassGenericPassword,
446 (id)kSecAttrAccessGroup : @"com.apple.security.ckks",
447 (id)kSecAttrAccount : account,
448 (id)kSecAttrSynchronizable : (id)kCFBooleanTrue,
449 (id)kSecMatchLimit : (id)kSecMatchLimitOne,
450 (id)kSecReturnData : (id)kCFBooleanTrue,
451 };
452 CFTypeRef result = NULL;
453
454 XCTAssertEqual(errSecSuccess, SecItemCopyMatching((__bridge CFDictionaryRef) query, &result), "Finding item %@", account);
455 XCTAssertNotNil((__bridge id)result, "Received an item");
456
457 NSString* storedPassword = [[NSString alloc] initWithData: (__bridge NSData*) result encoding: NSUTF8StringEncoding];
458 XCTAssertNotNil(storedPassword, "Password parsed as a password");
459
460 XCTAssertEqualObjects(storedPassword, password, "Stored password matches received password");
461 }
462
463 #pragma mark Tests
464
465 - (void)testAddDelete {
466 [self startCKKSSubsystem];
467 [self.kcv waitForKeyHierarchyReadiness];
468
469 XCTAssert([self addGenericPassword:@"data" account:@"ck-test-adddelete"], @"Added single item");
470
471 [self waitForEmptyOutgoingQueue:self.kcv];
472 XCTAssert([self compareLocalKeychainWithCloudKitState], @"testAdd: states match after add");
473
474 XCTAssert([self deleteGenericPassword:@"ck-test-adddelete"], @"Deleted single item");
475
476 [self waitForEmptyOutgoingQueue:self.kcv];
477 XCTAssert([self compareLocalKeychainWithCloudKitState], @"testAdd: states match after delete");
478 }
479
480 - (void)testAddModDelete {
481 [self startCKKSSubsystem];
482 [self.kcv waitForKeyHierarchyReadiness];
483
484 [self addGenericPassword:@"data" account:@"ck-test-addmoddelete"];
485
486 [self waitForEmptyOutgoingQueue:self.kcv];
487 XCTAssert([self compareLocalKeychainWithCloudKitState], @"testAddMod: states match after add");
488
489 [self updateGenericPassword:@"otherdata" account:@"ck-test-addmoddelete"];
490
491 [self waitForEmptyOutgoingQueue:self.kcv];
492 XCTAssert([self compareLocalKeychainWithCloudKitState], @"testAddMod: states match after mod");
493
494 [self deleteGenericPassword:@"ck-test-addmoddelete"];
495
496 [self waitForEmptyOutgoingQueue:self.kcv];
497 XCTAssert([self compareLocalKeychainWithCloudKitState], @"testAddMod: states match after del");
498 }
499
500 - (void)testAddModDeleteImmediate {
501 [self startCKKSSubsystem];
502 [self.kcv waitForKeyHierarchyReadiness];
503
504 XCTAssert([self addGenericPassword:@"data" account:@"ck-test-addmoddeleteimmediate"], @"Added item");
505 XCTAssert([self updateGenericPassword:@"otherdata" account:@"ck-test-addmoddeleteimmediate"], @"Modified item");
506 XCTAssert([self deleteGenericPassword:@"ck-test-addmoddeleteimmediate"], @"Deleted item");
507
508 [self waitForEmptyOutgoingQueue:self.kcv];
509 [self.kcv waitForFetchAndIncomingQueueProcessing];
510 XCTAssert([self compareLocalKeychainWithCloudKitState], @"testAddMod: states match after immediate add/mod/delete");
511 }
512
513 - (void)testReceive {
514 [self startCKKSSubsystem];
515 [self.kcv waitForKeyHierarchyReadiness];
516
517 [self findGenericPassword:@"ck-test-receive" expecting:errSecItemNotFound];
518 XCTAssert([self compareLocalKeychainWithCloudKitState], @"testReceive: states match before receive");
519
520 CKRecord *record = [self createFakeRecord:self.zoneID recordName:@"b6050e4d-e7b7-4e4e-b318-825cacc34722" withAccount:@"ck-test-receive"];
521 [self uploadRecords:@[record]];
522
523 [self.kcv notifyZoneChange:nil];
524 [self.kcv waitForFetchAndIncomingQueueProcessing];
525
526 [self findGenericPassword:@"ck-test-receive" expecting:errSecSuccess];
527 XCTAssert([self compareLocalKeychainWithCloudKitState], @"testReceive: states match after receive");
528 }
529
530 - (void)testReceiveColliding {
531 [self startCKKSSubsystem];
532 [self.kcv waitForKeyHierarchyReadiness];
533
534 XCTAssert([self findGenericPassword:@"ck-test-receivecolliding" expecting:errSecItemNotFound], @"test item not yet in keychain");
535
536 // Conflicting items! This test does not care how conflict gets resolved, just that state is consistent after syncing
537 CKRecord *r1 = [self createFakeRecord:self.zoneID recordName:@"97576447-c6b8-47fe-8f00-64f5da49d538" withAccount:@"ck-test-receivecolliding"];
538 CKRecord *r2 = [self createFakeRecord:self.zoneID recordName:@"c6b86447-9757-47fe-8f00-64f5da49d538" withAccount:@"ck-test-receivecolliding"];
539 [self uploadRecords:@[r1,r2]];
540
541 // poke CKKS since we won't have a real CK notification
542 [self.kcv notifyZoneChange:nil];
543 [self.kcv waitForFetchAndIncomingQueueProcessing];
544 [self waitForEmptyOutgoingQueue:self.kcv];
545
546 XCTAssert([self findGenericPassword:@"ck-test-receivecolliding" expecting:errSecSuccess], @"Item present after download");
547 // This will also flag an issue if the two conflicting items persist
548 XCTAssert([self compareLocalKeychainWithCloudKitState], @"Back in sync after processing incoming changes");
549 }
550
551 - (void)testAddMultipleDeleteAll {
552 [self startCKKSSubsystem];
553 [self.kcv waitForKeyHierarchyReadiness];
554
555 XCTAssert([self addMultiplePasswords:@"data" account:@"ck-test-addmultipledeleteall" amount:5]);
556
557 [self waitForEmptyOutgoingQueue:self.kcv];
558 XCTAssert([self compareLocalKeychainWithCloudKitState], @"testAddMultipleDeleteAll: states match after adds");
559
560 XCTAssert([self deleteMultiplePasswords:@"ck-test-addmultipledeleteall" amount:3]);
561
562 [self waitForEmptyOutgoingQueue:self.kcv];
563 XCTAssert([self compareLocalKeychainWithCloudKitState], @"testAddMultipleDeleteAll: states match after deletes");
564
565 XCTAssert([self deleteGenericPassword:@"ck-test-addmultipledeleteall005"]);
566 XCTAssert([self deleteGenericPassword:@"ck-test-addmultipledeleteall004"]);
567
568 [self waitForEmptyOutgoingQueue:self.kcv];
569 XCTAssert([self compareLocalKeychainWithCloudKitState], @"testAddMultipleDeleteAll: states match after deletes");
570 }
571
572 - (void)testAddLotsOfItems {
573 [ self startCKKSSubsystem];
574 [self.kcv waitForKeyHierarchyReadiness];
575
576 XCTAssert([self addMultiplePasswords:@"data" account:@"ck-test-addlotsofitems" amount:250], @"Added a truckload of items");
577
578 XCTAssert([self waitForEmptyOutgoingQueue:self.kcv], @"Completed upload within %ld seconds", (long)self.queueTimeout);
579 XCTAssert([self compareLocalKeychainWithCloudKitState], @"testAddLotsOfItems: states match after adding tons of items");
580
581 XCTAssert([self deleteMultiplePasswords:@"ck-test-addlotsofitems" amount:250], @"Got rid of a truckload of items");
582 XCTAssert([self waitForEmptyOutgoingQueue:self.kcv], @"Completed deletions within %ld seconds",(long)self.queueTimeout);
583
584 XCTAssert([self compareLocalKeychainWithCloudKitState], @"testAddLotsOfItems: states match after removing tons of items again");
585 }
586
587 #endif
588
589 @end