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