2 * Copyright (c) 2017 Apple Inc. All Rights Reserved.
4 * @APPLE_LICENSE_HEADER_START@
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
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.
21 * @APPLE_LICENSE_HEADER_END@
26 #import <CloudKit/CloudKit.h>
27 #import <XCTest/XCTest.h>
28 #import <OCMock/OCMock.h>
31 #include <Security/SecItemPriv.h>
33 #import "keychain/ckks/tests/CloudKitMockXCTest.h"
34 #import "keychain/ckks/tests/CloudKitKeychainSyncingMockXCTest.h"
35 #import "keychain/ckks/CKKS.h"
36 #import "keychain/ckks/CKKSKeychainView.h"
37 #import "keychain/ckks/CKKSCurrentKeyPointer.h"
38 #import "keychain/ckks/CKKSItemEncrypter.h"
39 #import "keychain/ckks/CKKSKey.h"
40 #import "keychain/ckks/CKKSOutgoingQueueEntry.h"
41 #import "keychain/ckks/CKKSIncomingQueueEntry.h"
42 #import "keychain/ckks/CKKSSynchronizeOperation.h"
43 #import "keychain/ckks/CKKSViewManager.h"
44 #import "keychain/ckks/CKKSZoneStateEntry.h"
45 #import "keychain/ckks/CKKSManifest.h"
47 #import "keychain/ckks/tests/MockCloudKit.h"
49 #pragma clang diagnostic push
50 #pragma clang diagnostic ignored "-Wdeprecated-declarations"
51 #include <Security/SecureObjectSync/SOSCloudCircle.h>
52 #include <Security/SecureObjectSync/SOSAccountPriv.h>
53 #include <Security/SecureObjectSync/SOSAccount.h>
54 #include <Security/SecureObjectSync/SOSInternal.h>
55 #include <Security/SecureObjectSync/SOSFullPeerInfo.h>
56 #pragma clang diagnostic pop
58 #include <Security/SecKey.h>
59 #include <Security/SecKeyPriv.h>
60 #pragma clang diagnostic pop
62 @interface CloudKitKeychainSyncingSOSIntegrationTests : CloudKitKeychainSyncingMockXCTest
64 @property CKRecordZoneID* engramZoneID;
65 @property CKKSKeychainView* engramView;
66 @property FakeCKZone* engramZone;
67 @property (readonly) ZoneKeys* engramZoneKeys;
69 @property CKRecordZoneID* manateeZoneID;
70 @property CKKSKeychainView* manateeView;
71 @property FakeCKZone* manateeZone;
72 @property (readonly) ZoneKeys* manateeZoneKeys;
74 @property CKRecordZoneID* autoUnlockZoneID;
75 @property CKKSKeychainView* autoUnlockView;
76 @property FakeCKZone* autoUnlockZone;
77 @property (readonly) ZoneKeys* autoUnlockZoneKeys;
79 @property CKRecordZoneID* healthZoneID;
80 @property CKKSKeychainView* healthView;
81 @property FakeCKZone* healthZone;
82 @property (readonly) ZoneKeys* healthZoneKeys;
84 @property CKRecordZoneID* applepayZoneID;
85 @property CKKSKeychainView* applepayView;
86 @property FakeCKZone* applepayZone;
87 @property (readonly) ZoneKeys* applepayZoneKeys;
89 @property CKRecordZoneID* homeZoneID;
90 @property CKKSKeychainView* homeView;
91 @property FakeCKZone* homeZone;
92 @property (readonly) ZoneKeys* homeZoneKeys;
96 @implementation CloudKitKeychainSyncingSOSIntegrationTests
99 SecCKKSResetSyncing();
105 (void)[CKKSManifest shouldSyncManifests]; // initialize.
106 SecCKKSSetSyncManifests(false);
107 SecCKKSSetEnforceManifests(false);
110 SecCKKSTestSetDisableSOS(false);
112 // Wait for the ViewManager to be brought up
113 XCTAssertEqual(0, [self.injectedManager.completedSecCKKSInitialize wait:20*NSEC_PER_SEC], "No timeout waiting for SecCKKSInitialize");
115 self.engramZoneID = [[CKRecordZoneID alloc] initWithZoneName:@"Engram" ownerName:CKCurrentUserDefaultName];
116 self.engramZone = [[FakeCKZone alloc] initZone: self.engramZoneID];
117 self.zones[self.engramZoneID] = self.engramZone;
118 self.engramView = [[CKKSViewManager manager] findView:@"Engram"];
119 [self.ckksViews addObject:self.engramView];
120 XCTAssertNotNil(self.engramView, "CKKSViewManager created the Engram view");
121 [self.ckksZones addObject:self.engramZoneID];
123 self.manateeZoneID = [[CKRecordZoneID alloc] initWithZoneName:@"Manatee" ownerName:CKCurrentUserDefaultName];
124 self.manateeZone = [[FakeCKZone alloc] initZone: self.manateeZoneID];
125 self.zones[self.manateeZoneID] = self.manateeZone;
126 self.manateeView = [[CKKSViewManager manager] findView:@"Manatee"];
127 [self.ckksViews addObject:self.manateeView];
128 XCTAssertNotNil(self.manateeView, "CKKSViewManager created the Manatee view");
129 [self.ckksZones addObject:self.manateeZoneID];
131 self.autoUnlockZoneID = [[CKRecordZoneID alloc] initWithZoneName:@"AutoUnlock" ownerName:CKCurrentUserDefaultName];
132 self.autoUnlockZone = [[FakeCKZone alloc] initZone: self.autoUnlockZoneID];
133 self.zones[self.autoUnlockZoneID] = self.autoUnlockZone;
134 self.autoUnlockView = [[CKKSViewManager manager] findView:@"AutoUnlock"];
135 [self.ckksViews addObject:self.autoUnlockView];
136 XCTAssertNotNil(self.autoUnlockView, "CKKSViewManager created the AutoUnlock view");
137 [self.ckksZones addObject:self.autoUnlockZoneID];
139 self.healthZoneID = [[CKRecordZoneID alloc] initWithZoneName:@"Health" ownerName:CKCurrentUserDefaultName];
140 self.healthZone = [[FakeCKZone alloc] initZone: self.healthZoneID];
141 self.zones[self.healthZoneID] = self.healthZone;
142 self.healthView = [[CKKSViewManager manager] findView:@"Health"];
143 [self.ckksViews addObject:self.healthView];
144 XCTAssertNotNil(self.healthView, "CKKSViewManager created the Health view");
145 [self.ckksZones addObject:self.healthZoneID];
147 self.applepayZoneID = [[CKRecordZoneID alloc] initWithZoneName:@"ApplePay" ownerName:CKCurrentUserDefaultName];
148 self.applepayZone = [[FakeCKZone alloc] initZone: self.healthZoneID];
149 self.zones[self.applepayZoneID] = self.applepayZone;
150 self.applepayView = [[CKKSViewManager manager] findView:@"ApplePay"];
151 [self.ckksViews addObject:self.applepayView];
152 XCTAssertNotNil(self.applepayView, "CKKSViewManager created the ApplePay view");
153 [self.ckksZones addObject:self.applepayZoneID];
155 self.homeZoneID = [[CKRecordZoneID alloc] initWithZoneName:@"Home" ownerName:CKCurrentUserDefaultName];
156 self.homeZone = [[FakeCKZone alloc] initZone: self.healthZoneID];
157 self.zones[self.homeZoneID] = self.homeZone;
158 self.homeView = [[CKKSViewManager manager] findView:@"Home"];
159 XCTAssertNotNil(self.homeView, "CKKSViewManager created the Home view");
160 [self.ckksZones addObject:self.homeZoneID];
164 SecCKKSTestSetDisableSOS(true);
166 SecCKKSResetSyncing();
170 // If the test didn't already do this, allow each zone to spin up
171 self.accountStatus = CKAccountStatusNoAccount;
172 [self startCKKSSubsystem];
174 [self.engramView halt];
175 [self.engramView waitUntilAllOperationsAreFinished];
176 self.engramView = nil;
178 [self.manateeView halt];
179 [self.manateeView waitUntilAllOperationsAreFinished];
180 self.manateeView = nil;
182 [self.autoUnlockView halt];
183 [self.autoUnlockView waitUntilAllOperationsAreFinished];
184 self.autoUnlockView = nil;
186 [self.healthView halt];
187 [self.healthView waitUntilAllOperationsAreFinished];
188 self.healthView = nil;
190 [self.applepayView halt];
191 [self.applepayView waitUntilAllOperationsAreFinished];
192 self.applepayView = nil;
194 [self.homeView halt];
195 [self.homeView waitUntilAllOperationsAreFinished];
201 - (ZoneKeys*)engramZoneKeys {
202 return self.keys[self.engramZoneID];
205 - (ZoneKeys*)manateeZoneKeys {
206 return self.keys[self.manateeZoneID];
209 -(void)saveFakeKeyHierarchiesToLocalDatabase {
210 for(CKRecordZoneID* zoneID in self.ckksZones) {
211 [self createAndSaveFakeKeyHierarchy: zoneID];
215 -(void)testAllViewsMakeNewKeyHierarchies {
216 // Test starts with nothing anywhere
218 // Due to our new cross-zone fetch system, CKKS should only issue one fetch for all zones
219 // Since the tests can sometimes be slow, slow down the fetcher to normal speed
220 [self.injectedManager.zoneChangeFetcher.fetchScheduler changeDelays:2*NSEC_PER_SEC continuingDelay:30*NSEC_PER_SEC];
221 self.silentFetchesAllowed = false;
222 [self expectCKFetch];
224 [self startCKKSSubsystem];
226 // All zones should upload a key hierarchy
227 for(CKRecordZoneID* zoneID in self.ckksZones) {
228 [self expectCKModifyKeyRecords:3 currentKeyPointerRecords:3 tlkShareRecords:1 zoneID:zoneID];
230 OCMVerifyAllWithDelay(self.mockDatabase, 20);
232 for(CKKSKeychainView* view in self.ckksViews) {
233 XCTAssertEqual(0, [view.keyHierarchyConditions[SecCKKSZoneKeyStateReady] wait:20*NSEC_PER_SEC], "Key state should enter 'ready' for view %@", view);
237 -(void)testAllViewsAcceptExistingKeyHierarchies {
238 for(CKRecordZoneID* zoneID in self.ckksZones) {
239 [self putFakeKeyHierarchyInCloudKit:zoneID];
240 [self saveTLKMaterialToKeychain:zoneID];
241 [self expectCKKSTLKSelfShareUpload:zoneID];
244 [self.injectedManager.zoneChangeFetcher.fetchScheduler changeDelays:2*NSEC_PER_SEC continuingDelay:30*NSEC_PER_SEC];
245 self.silentFetchesAllowed = false;
246 [self expectCKFetch];
248 [self startCKKSSubsystem];
249 OCMVerifyAllWithDelay(self.mockDatabase, 20);
251 for(CKKSKeychainView* view in self.ckksViews) {
252 XCTAssertEqual(0, [view.keyHierarchyConditions[SecCKKSZoneKeyStateReady] wait:20*NSEC_PER_SEC], "Key state should enter 'ready' for view %@", view);
256 -(void)testAddEngramManateeItems {
257 [self saveFakeKeyHierarchiesToLocalDatabase]; // Make life easy for this test.
259 [self startCKKSSubsystem];
261 XCTestExpectation* engramChanged = [self expectChangeForView:self.engramZoneID.zoneName];
262 XCTestExpectation* pcsChanged = [self expectChangeForView:@"PCS"];
263 XCTestExpectation* manateeChanged = [self expectChangeForView:self.manateeZoneID.zoneName];
265 // We expect a single record to be uploaded to the engram view.
266 [self expectCKModifyItemRecords: 1 currentKeyPointerRecords: 1 zoneID:self.engramZoneID];
267 [self addGenericPassword: @"data" account: @"account-delete-me-engram" viewHint:(NSString*) kSecAttrViewHintEngram];
269 OCMVerifyAllWithDelay(self.mockDatabase, 20);
270 [self waitForExpectations:@[engramChanged] timeout:1];
271 [self waitForExpectations:@[pcsChanged] timeout:1];
273 pcsChanged = [self expectChangeForView:@"PCS"];
275 // We expect a single record to be uploaded to the manatee view.
276 [self expectCKModifyItemRecords: 1 currentKeyPointerRecords: 1 zoneID:self.manateeZoneID];
277 [self addGenericPassword: @"data" account: @"account-delete-me-manatee" viewHint:(NSString*) kSecAttrViewHintManatee];
279 OCMVerifyAllWithDelay(self.mockDatabase, 20);
280 [self waitForExpectations:@[manateeChanged] timeout:1];
281 [self waitForExpectations:@[pcsChanged] timeout:1];
284 -(void)testAddAutoUnlockItems {
285 [self saveFakeKeyHierarchiesToLocalDatabase]; // Make life easy for this test.
287 [self startCKKSSubsystem];
289 XCTestExpectation* autoUnlockChanged = [self expectChangeForView:self.autoUnlockZoneID.zoneName];
290 // AutoUnlock is NOT is PCS view, so it should not send the fake 'PCS' view notification
291 XCTestExpectation* pcsChanged = [self expectChangeForView:@"PCS"];
292 pcsChanged.inverted = YES;
294 // We expect a single record to be uploaded to the AutoUnlock view.
295 [self expectCKModifyItemRecords: 1 currentKeyPointerRecords: 1 zoneID:self.autoUnlockZoneID];
296 [self addGenericPassword: @"data" account: @"account-delete-me-autounlock" viewHint:(NSString*) kSecAttrViewHintAutoUnlock];
298 OCMVerifyAllWithDelay(self.mockDatabase, 20);
299 [self waitForExpectations:@[autoUnlockChanged] timeout:1];
300 [self waitForExpectations:@[pcsChanged] timeout:0.2];
303 -(void)testAddHealthItems {
304 [self saveFakeKeyHierarchiesToLocalDatabase]; // Make life easy for this test.
306 [self startCKKSSubsystem];
308 XCTestExpectation* healthChanged = [self expectChangeForView:self.healthZoneID.zoneName];
309 // Health is NOT is PCS view, so it should not send the fake 'PCS' view notification
310 XCTestExpectation* pcsChanged = [self expectChangeForView:@"PCS"];
311 pcsChanged.inverted = YES;
313 // We expect a single record to be uploaded to the Health view.
314 [self expectCKModifyItemRecords: 1 currentKeyPointerRecords: 1 zoneID:self.healthZoneID];
315 [self addGenericPassword: @"data" account: @"account-delete-me-autounlock" viewHint:(NSString*) kSecAttrViewHintHealth];
317 OCMVerifyAllWithDelay(self.mockDatabase, 20);
318 [self waitForExpectations:@[healthChanged] timeout:1];
319 [self waitForExpectations:@[pcsChanged] timeout:0.2];
322 -(void)testAddApplePayItems {
323 [self saveFakeKeyHierarchiesToLocalDatabase]; // Make life easy for this test.
325 [self startCKKSSubsystem];
327 XCTestExpectation* applepayChanged = [self expectChangeForView:self.applepayZoneID.zoneName];
328 // ApplePay is NOT is PCS view, so it should not send the fake 'PCS' view notification
329 XCTestExpectation* pcsChanged = [self expectChangeForView:@"PCS"];
330 pcsChanged.inverted = YES;
332 // We expect a single record to be uploaded to the ApplePay view.
333 [self expectCKModifyItemRecords: 1 currentKeyPointerRecords: 1 zoneID:self.applepayZoneID];
334 [self addGenericPassword: @"data" account: @"account-delete-me-autounlock" viewHint:(NSString*) kSecAttrViewHintApplePay];
336 OCMVerifyAllWithDelay(self.mockDatabase, 20);
337 [self waitForExpectations:@[applepayChanged] timeout:1];
338 [self waitForExpectations:@[pcsChanged] timeout:0.2];
341 -(void)testAddHomeItems {
342 [self saveFakeKeyHierarchiesToLocalDatabase]; // Make life easy for this test.
344 [self startCKKSSubsystem];
346 XCTestExpectation* homeChanged = [self expectChangeForView:self.homeZoneID.zoneName];
347 // Home is NOT a PCS view, so it should not send the fake 'PCS' view notification
348 XCTestExpectation* pcsChanged = [self expectChangeForView:@"PCS"];
349 pcsChanged.inverted = YES;
351 // We expect a single record to be uploaded to the ApplePay view.
352 [self expectCKModifyItemRecords: 1 currentKeyPointerRecords: 1 zoneID:self.homeZoneID];
353 [self addGenericPassword: @"data" account: @"account-delete-me-autounlock" viewHint:(NSString*) kSecAttrViewHintHome];
355 OCMVerifyAllWithDelay(self.mockDatabase, 20);
356 [self waitForExpectations:@[homeChanged] timeout:1];
357 [self waitForExpectations:@[pcsChanged] timeout:0.2];
360 -(void)testAddOtherViewHintItem {
361 [self saveFakeKeyHierarchiesToLocalDatabase]; // Make life easy for this test.
363 [self startCKKSSubsystem];
365 // We expect no uploads to CKKS.
366 [self addGenericPassword: @"data" account: @"account-delete-me-no-viewhint"];
367 [self addGenericPassword: @"data" account: @"account-delete-me-password" viewHint:(NSString*) kSOSViewAutofillPasswords];
370 OCMVerifyAllWithDelay(self.mockDatabase, 20);
373 - (void)testReceiveItemInView {
374 [self saveFakeKeyHierarchiesToLocalDatabase]; // Make life easy for this test.
375 [self startCKKSSubsystem];
377 for(CKRecordZoneID* zoneID in self.ckksZones) {
378 [self expectCKKSTLKSelfShareUpload:zoneID];
381 [self waitForKeyHierarchyReadinesses];
383 [self findGenericPassword:@"account-delete-me" expecting:errSecItemNotFound];
385 CKRecord* ckr = [self createFakeRecord:self.engramZoneID recordName:@"7B598D31-F9C5-481E-98AC-5A507ACB2D85"];
386 [self.engramZone addToZone: ckr];
388 XCTestExpectation* engramChanged = [self expectChangeForView:self.engramZoneID.zoneName];
389 XCTestExpectation* pcsChanged = [self expectChangeForView:@"PCS"];
391 // Trigger a notification (with hilariously fake data)
392 [self.engramView notifyZoneChange:nil];
394 [self.engramView waitForFetchAndIncomingQueueProcessing];
395 [self findGenericPassword:@"account-delete-me" expecting:errSecSuccess];
397 [self waitForExpectations:@[engramChanged] timeout:1];
398 [self waitForExpectations:@[pcsChanged] timeout:1];
401 - (void)testFindManateePiggyTLKs {
402 [self saveFakeKeyHierarchyToLocalDatabase:self.manateeZoneID];
403 [self saveTLKMaterialToKeychain:self.manateeZoneID];
405 NSDictionary* piggyTLKs = [self SOSPiggyBackCopyFromKeychain];
407 [self deleteTLKMaterialFromKeychain:self.manateeZoneID];
409 [self SOSPiggyBackAddToKeychain:piggyTLKs];
411 NSError* error = nil;
412 [self.manateeZoneKeys.tlk loadKeyMaterialFromKeychain:&error];
413 XCTAssertNil(error, "No error loading tlk from piggy contents");
416 - (void)testFindPiggyTLKs {
417 [self putFakeKeyHierachiesInCloudKit];
418 [self putFakeDeviceStatusesInCloudKit];
419 [self saveTLKsToKeychain];
421 NSDictionary* piggyTLKs = [self SOSPiggyBackCopyFromKeychain];
423 [self deleteTLKMaterialsFromKeychain];
425 [self SOSPiggyBackAddToKeychain:piggyTLKs];
427 NSError* error = nil;
428 [self.manateeZoneKeys.tlk loadKeyMaterialFromKeychain:&error];
429 XCTAssertNil(error, "No error loading manatee tlk from piggy contents");
431 [self.engramZoneKeys.tlk loadKeyMaterialFromKeychain:&error];
432 XCTAssertNil(error, "No error loading engram tlk from piggy contents");
434 [self.autoUnlockZoneKeys.tlk loadKeyMaterialFromKeychain:&error];
435 XCTAssertNil(error, "No error loading AutoUnlock tlk from piggy contents");
437 [self.healthZoneKeys.tlk loadKeyMaterialFromKeychain:&error];
438 XCTAssertNil(error, "No error loading Health tlk from piggy contents");
440 [self.applepayZoneKeys.tlk loadKeyMaterialFromKeychain:&error];
441 XCTAssertNil(error, "No error loading ApplePay tlk from piggy contents");
443 [self.homeZoneKeys.tlk loadKeyMaterialFromKeychain:&error];
444 XCTAssertNil(error, "No error loading Home tlk from piggy contents");
447 -(NSString*)fileForStorage
449 static dispatch_once_t onceToken;
450 static NSString *tempPath = NULL;
451 dispatch_once(&onceToken, ^{
452 tempPath = [[[[NSFileManager defaultManager] temporaryDirectory] URLByAppendingPathComponent:@"PiggyPacket"] path];
458 -(void)testPiggybackingData{
459 [self putFakeKeyHierachiesInCloudKit];
460 [self saveTLKsToKeychain];
462 for(CKRecordZoneID* zoneID in self.ckksZones) {
463 [self expectCKKSTLKSelfShareUpload:zoneID];
465 [self startCKKSSubsystem];
467 [self waitForKeyHierarchyReadinesses];
469 OCMVerifyAllWithDelay(self.mockDatabase, 20);
472 * Pull data from keychain and view manager
475 NSDictionary* piggydata = [self SOSPiggyBackCopyFromKeychain];
476 NSArray<NSData *>* icloudidentities = piggydata[@"idents"];
477 NSArray<NSDictionary *>* tlks = piggydata[@"tlk"];
479 XCTAssertEqual([tlks count], [[self.injectedManager viewList] count], "TLKs not same as views");
481 XCTAssertNotNil(tlks, "tlks not set");
482 XCTAssertNotEqual([tlks count], (NSUInteger)0, "0 tlks");
483 XCTAssertNotNil(icloudidentities, "idents not set");
484 XCTAssertNotEqual([icloudidentities count], (NSUInteger)0, "0 icloudidentities");
486 NSData *initial = SOSPiggyCreateInitialSyncData(icloudidentities, tlks);
488 XCTAssertNotNil(initial, "Initial not set");
489 BOOL writeStatus = [initial writeToFile:[self fileForStorage] options:NSDataWritingAtomic error: nil];
490 XCTAssertTrue(writeStatus, "had trouble writing to disk");
491 XCTAssertNotEqual((int)[initial length], 0, "initial sync data is greater than 0");
494 * Check that they make it accross
497 const uint8_t* der = [initial bytes];
498 const uint8_t *der_end = der + [initial length];
500 NSDictionary *result = SOSPiggyCopyInitialSyncData(&der, der_end);
501 XCTAssertNotNil(result, "Initial not set");
502 NSArray *copiedTLKs = result[@"tlks"];
503 XCTAssertNotNil(copiedTLKs, "tlks not set");
504 XCTAssertEqual([copiedTLKs count], 5u, "piggybacking should have gotten 5 TLKs across (but we have more than that elsewhere)");
506 NSArray *copiediCloudidentities = result[@"idents"];
507 XCTAssertNotNil(copiediCloudidentities, "idents not set");
508 XCTAssertEqual([copiediCloudidentities count], [icloudidentities count], "ident count not same");
511 -(void)testVerifyTLKSorting {
512 char key[32*2] = {0};
513 NSArray<NSDictionary *> *tlks = @[
515 @"acct" : @"11111111",
516 @"srvr" : @"Manatee",
517 @"v_Data" : [NSData dataWithBytes:key length:sizeof(key)],
521 @"acct" : @"55555555",
523 @"v_Data" : [NSData dataWithBytes:key length:sizeof(key)],
526 @"acct" : @"22222222",
528 @"v_Data" : [NSData dataWithBytes:key length:sizeof(key)],
532 @"acct" : @"44444444",
533 @"srvr" : @"Manatee",
534 @"v_Data" : [NSData dataWithBytes:key length:sizeof(key)],
537 @"acct" : @"33333333",
539 @"v_Data" : [NSData dataWithBytes:key length:sizeof(key)],
543 @"acct" : @"66666666",
545 @"v_Data" : [NSData dataWithBytes:key length:sizeof(key)],
550 NSArray<NSDictionary *>* sortedTLKs = SOSAccountSortTLKS(tlks);
551 XCTAssertNotNil(sortedTLKs, "sortedTLKs not set");
553 // Home gets sorted into the middle, as the other Health and Manatee TLKs aren't 'authoritative'
554 NSArray<NSString *> *expectedOrder = @[ @"11111111", @"22222222", @"33333333", @"66666666", @"44444444", @"55555555"];
555 [sortedTLKs enumerateObjectsUsingBlock:^(NSDictionary *tlk, NSUInteger idx, BOOL * _Nonnull stop) {
556 NSString *uuid = tlk[@"acct"];
557 XCTAssertEqualObjects(uuid, expectedOrder[idx], "wrong order");
562 - (void)testAcceptExistingPiggyKeyHierarchy {
563 // Test starts with no keys in CKKS database, but one in our fake CloudKit.
564 // Test also begins with the TLK having arrived in the local keychain (via SOS)
565 [self putFakeKeyHierachiesInCloudKit];
566 [self saveTLKsToKeychain];
567 NSDictionary* piggyTLKS = [self SOSPiggyBackCopyFromKeychain];
568 [self SOSPiggyBackAddToKeychain:piggyTLKS];
569 [self deleteTLKMaterialsFromKeychain];
571 // The CKKS subsystem should write a TLK Share for each view
572 for(CKRecordZoneID* zoneID in self.ckksZones) {
573 [self expectCKKSTLKSelfShareUpload:zoneID];
576 // Spin up CKKS subsystem.
577 [self startCKKSSubsystem];
579 [self.manateeView waitForKeyHierarchyReadiness];
581 OCMVerifyAllWithDelay(self.mockDatabase, 20);
583 // Verify that there are three local keys, and three local current key records
584 __weak __typeof(self) weakSelf = self;
585 [self.manateeView dispatchSync: ^bool{
586 __strong __typeof(weakSelf) strongSelf = weakSelf;
587 XCTAssertNotNil(strongSelf, "self exists");
589 NSError* error = nil;
591 NSArray<CKKSKey*>* keys = [CKKSKey localKeys:strongSelf.manateeZoneID error:&error];
592 XCTAssertNil(error, "no error fetching keys");
593 XCTAssertEqual(keys.count, 3u, "Three keys in local database");
595 NSArray<CKKSCurrentKeyPointer*>* currentkeys = [CKKSCurrentKeyPointer all:strongSelf.manateeZoneID error:&error];
596 XCTAssertNil(error, "no error fetching current keys");
597 XCTAssertEqual(currentkeys.count, 3u, "Three current key pointers in local database");
599 // Ensure that the manatee syncable TLK is created from a piggy
600 NSDictionary* query = @{
601 (id)kSecClass : (id)kSecClassInternetPassword,
602 (id)kSecAttrNoLegacy : @YES,
603 (id)kSecAttrAccessGroup : @"com.apple.security.ckks",
604 (id)kSecAttrDescription: SecCKKSKeyClassTLK,
605 (id)kSecAttrAccount: strongSelf.manateeZoneKeys.tlk.uuid,
606 (id)kSecAttrServer: strongSelf.manateeZoneID.zoneName,
607 (id)kSecAttrSynchronizable: @YES,
608 (id)kSecReturnAttributes: @YES,
609 (id)kSecReturnData: @YES,
611 CFTypeRef result = nil;
612 XCTAssertEqual(errSecSuccess, SecItemCopyMatching((__bridge CFDictionaryRef)query, &result), "Found a syncable TLK");
613 XCTAssertNotNil((__bridge id) result, "Received a result from SecItemCopyMatching");
614 CFReleaseNull(result);
620 - (void)putFakeDeviceStatusesInCloudKit {
621 [self putFakeDeviceStatusInCloudKit: self.engramZoneID];
622 [self putFakeDeviceStatusInCloudKit: self.manateeZoneID];
623 [self putFakeDeviceStatusInCloudKit: self.autoUnlockZoneID];
624 [self putFakeDeviceStatusInCloudKit: self.healthZoneID];
625 [self putFakeDeviceStatusInCloudKit: self.applepayZoneID];
626 [self putFakeDeviceStatusInCloudKit: self.homeZoneID];
629 -(void)putFakeKeyHierachiesInCloudKit{
630 [self putFakeKeyHierarchyInCloudKit: self.engramZoneID];
631 [self putFakeKeyHierarchyInCloudKit: self.manateeZoneID];
632 [self putFakeKeyHierarchyInCloudKit: self.autoUnlockZoneID];
633 [self putFakeKeyHierarchyInCloudKit: self.healthZoneID];
634 [self putFakeKeyHierarchyInCloudKit: self.applepayZoneID];
635 [self putFakeKeyHierarchyInCloudKit: self.homeZoneID];
637 -(void)saveTLKsToKeychain{
638 [self saveTLKMaterialToKeychain:self.engramZoneID];
639 [self saveTLKMaterialToKeychain:self.manateeZoneID];
640 [self saveTLKMaterialToKeychain:self.autoUnlockZoneID];
641 [self saveTLKMaterialToKeychain:self.healthZoneID];
642 [self saveTLKMaterialToKeychain:self.applepayZoneID];
643 [self saveTLKMaterialToKeychain:self.homeZoneID];
645 -(void)deleteTLKMaterialsFromKeychain{
646 [self deleteTLKMaterialFromKeychain: self.engramZoneID];
647 [self deleteTLKMaterialFromKeychain: self.manateeZoneID];
648 [self deleteTLKMaterialFromKeychain: self.autoUnlockZoneID];
649 [self deleteTLKMaterialFromKeychain: self.healthZoneID];
650 [self deleteTLKMaterialFromKeychain: self.applepayZoneID];
651 [self deleteTLKMaterialFromKeychain: self.homeZoneID];
654 -(void)waitForKeyHierarchyReadinesses {
655 [self.manateeView waitForKeyHierarchyReadiness];
656 [self.engramView waitForKeyHierarchyReadiness];
657 [self.autoUnlockView waitForKeyHierarchyReadiness];
658 [self.healthView waitForKeyHierarchyReadiness];
659 [self.applepayView waitForKeyHierarchyReadiness];
660 [self.homeView waitForKeyHierarchyReadiness];
663 -(void)testAcceptExistingAndUsePiggyKeyHierarchy {
664 // Test starts with nothing in database, but one in our fake CloudKit.
665 [self putFakeKeyHierachiesInCloudKit];
666 [self putFakeDeviceStatusesInCloudKit];
667 [self saveTLKsToKeychain];
668 NSDictionary* piggyData = [self SOSPiggyBackCopyFromKeychain];
669 [self deleteTLKMaterialsFromKeychain];
671 // Spin up CKKS subsystem.
672 [self startCKKSSubsystem];
674 // The CKKS subsystem should not try to write anything to the CloudKit database.
675 XCTAssertEqual(0, [self.manateeView.keyHierarchyConditions[SecCKKSZoneKeyStateWaitForTLK] wait:20*NSEC_PER_SEC], "CKKS entered waitfortlk");
677 OCMVerifyAllWithDelay(self.mockDatabase, 20);
679 // Now, save the TLKs to the keychain (to simulate them coming in later via piggybacking).
680 for(CKRecordZoneID* zoneID in self.ckksZones) {
681 [self expectCKKSTLKSelfShareUpload:zoneID];
684 [self SOSPiggyBackAddToKeychain:piggyData];
685 [self waitForKeyHierarchyReadinesses];
687 // We expect a single record to be uploaded for each key class
688 [self expectCKModifyItemRecords: 1 currentKeyPointerRecords: 1 zoneID:self.manateeZoneID checkItem: [self checkClassCBlock:self.manateeZoneID message:@"Object was encrypted under class C key in hierarchy"]];
689 [self addGenericPassword: @"data" account: @"account-delete-me-manatee" viewHint:(id)kSecAttrViewHintManatee];
691 OCMVerifyAllWithDelay(self.mockDatabase, 20);
693 [self expectCKModifyItemRecords: 1 currentKeyPointerRecords: 1 zoneID:self.manateeZoneID checkItem: [self checkClassABlock:self.manateeZoneID message:@"Object was encrypted under class A key in hierarchy"]];
695 [self addGenericPassword:@"asdf"
696 account:@"account-class-A"
697 viewHint:(id)kSecAttrViewHintManatee
698 access:(id)kSecAttrAccessibleWhenUnlocked
699 expecting:errSecSuccess
700 message:@"Adding class A item"];
701 OCMVerifyAllWithDelay(self.mockDatabase, 20);