2 * Copyright (c) 2020 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>
30 #import "keychain/categories/NSError+UsefulConstructors.h"
31 #import "keychain/ckks/CKKS.h"
32 #import "keychain/ckks/CKKSIncomingQueueEntry.h"
33 #import "keychain/ckks/CKKSOutgoingQueueEntry.h"
34 #import "keychain/ckks/CloudKitCategories.h"
35 #import "keychain/ckks/tests/CKKSTests.h"
36 #import "keychain/ckks/tests/CloudKitKeychainSyncingMockXCTest.h"
37 #import "keychain/ckks/tests/CloudKitMockXCTest.h"
38 #import "keychain/ckks/tests/MockCloudKit.h"
40 @interface CloudKitKeychainSyncingItemSyncChoiceTests : CloudKitKeychainSyncingTestsBase
41 @property CKKSSOSSelfPeer* remotePeer1;
44 @implementation CloudKitKeychainSyncingItemSyncChoiceTests
46 - (size_t)outgoingQueueSize:(CKKSKeychainView*)view {
47 __block size_t result = 0;
49 [view dispatchSyncWithReadOnlySQLTransaction:^{
50 NSError* zoneError = nil;
51 NSArray<CKKSOutgoingQueueEntry*>* entries = [CKKSOutgoingQueueEntry all:view.zoneID error:&zoneError];
52 XCTAssertNil(zoneError, "should be no error fetching all OQEs");
54 result = (size_t)entries.count;
59 - (size_t)incomingQueueSize:(CKKSKeychainView*)view {
60 __block size_t result = 0;
62 [view dispatchSyncWithReadOnlySQLTransaction:^{
63 NSError* zoneError = nil;
64 NSArray<CKKSIncomingQueueEntry*>* entries = [CKKSIncomingQueueEntry all:view.zoneID error:&zoneError];
65 XCTAssertNil(zoneError, "should be no error fetching all IQEs");
67 result = (size_t)entries.count;
75 self.remotePeer1 = [[CKKSSOSSelfPeer alloc] initWithSOSPeerID:@"remote-peer1"
76 encryptionKey:[[SFECKeyPair alloc] initRandomKeyPairWithSpecifier:[[SFECKeySpecifier alloc] initWithCurve:SFEllipticCurveNistp384]]
77 signingKey:[[SFECKeyPair alloc] initRandomKeyPairWithSpecifier:[[SFECKeySpecifier alloc] initWithCurve:SFEllipticCurveNistp384]]
78 viewList:self.managedViewList];
81 - (void)testAddItemToPausedView {
82 [self createAndSaveFakeKeyHierarchy: self.keychainZoneID]; // Make life easy for this test.
84 [self startCKKSSubsystem];
85 XCTAssertEqual(0, [self.keychainView.keyHierarchyConditions[SecCKKSZoneKeyStateReady] wait:20*NSEC_PER_SEC], @"Key state should have arrived at ready");
87 [self.keychainView setCurrentSyncingPolicy:[self viewSortingPolicyForManagedViewListWithUserControllableViews:[NSSet setWithObject:self.keychainView.zoneName]
88 syncUserControllableViews:TPPBPeerStableInfo_UserControllableViewStatus_DISABLED]
91 [self addGenericPassword:@"data" account:@"account-delete-me"];
92 [self.keychainView waitForOperationsOfClass:[CKKSOutgoingQueueOperation class]];
93 XCTAssertEqual(1, [self outgoingQueueSize:self.keychainView], "There should be one pending item in the outgoing queue");
96 [self addGenericPassword:@"data" account:@"account-delete-me-2"];
97 [self.keychainView waitForOperationsOfClass:[CKKSOutgoingQueueOperation class]];
98 XCTAssertEqual(2, [self outgoingQueueSize:self.keychainView], "There should be two pending item in the outgoing queue");
100 // When syncing is enabled, these items should sync
101 [self expectCKModifyItemRecords:2 currentKeyPointerRecords:1 zoneID:self.keychainZoneID];
103 [self.keychainView setCurrentSyncingPolicy:[self viewSortingPolicyForManagedViewListWithUserControllableViews:[NSSet setWithObject:self.keychainView.zoneName]
104 syncUserControllableViews:TPPBPeerStableInfo_UserControllableViewStatus_ENABLED]
106 OCMVerifyAllWithDelay(self.mockDatabase, 20);
109 - (void)testReceiveItemToPausedView {
110 [self createAndSaveFakeKeyHierarchy: self.keychainZoneID]; // Make life easy for this test.
112 [self startCKKSSubsystem];
113 XCTAssertEqual(0, [self.keychainView.keyHierarchyConditions[SecCKKSZoneKeyStateReady] wait:20*NSEC_PER_SEC], @"Key state should have arrived at ready");
115 [self.keychainView setCurrentSyncingPolicy:[self viewSortingPolicyForManagedViewListWithUserControllableViews:[NSSet setWithObject:self.keychainView.zoneName]
116 syncUserControllableViews:TPPBPeerStableInfo_UserControllableViewStatus_DISABLED]
119 [self findGenericPassword: @"account0" expecting:errSecItemNotFound];
121 [self.keychainZone addToZone:[self createFakeRecord:self.keychainZoneID recordName:@"7B598D31-F9C5-481E-98AC-5A507ACB2D00" withAccount:@"account0"]];
122 [self.injectedManager.zoneChangeFetcher notifyZoneChange:nil];
123 [self.keychainView waitForFetchAndIncomingQueueProcessing];
124 XCTAssertEqual(1, [self incomingQueueSize:self.keychainView], "There should be one pending item in the incoming queue");
126 [self.keychainZone addToZone:[self createFakeRecord:self.keychainZoneID recordName:@"7B598D31-F9C5-481E-0000-5A507ACB2D00" withAccount:@"account1"]];
127 [self.injectedManager.zoneChangeFetcher notifyZoneChange:nil];
128 [self.keychainView waitForFetchAndIncomingQueueProcessing];
129 XCTAssertEqual(2, [self incomingQueueSize:self.keychainView], "There should be two pending item in the incoming queue");
131 [self findGenericPassword:@"account0" expecting:errSecItemNotFound];
132 [self findGenericPassword:@"account1" expecting:errSecItemNotFound];
134 // When syncing is enabled, these items should sync
135 [self.keychainView setCurrentSyncingPolicy:[self viewSortingPolicyForManagedViewListWithUserControllableViews:[NSSet setWithObject:self.keychainView.zoneName]
136 syncUserControllableViews:TPPBPeerStableInfo_UserControllableViewStatus_ENABLED]
139 [self.keychainView waitForOperationsOfClass:[CKKSIncomingQueueOperation class]];
140 [self findGenericPassword:@"account0" expecting:errSecSuccess];
141 [self findGenericPassword:@"account1" expecting:errSecSuccess];
144 - (void)testAcceptKeyHierarchyWhilePaused {
145 [self.keychainView setCurrentSyncingPolicy:[self viewSortingPolicyForManagedViewListWithUserControllableViews:[NSSet setWithObject:self.keychainView.zoneName]
146 syncUserControllableViews:TPPBPeerStableInfo_UserControllableViewStatus_DISABLED]
149 [self putFakeKeyHierarchyInCloudKit:self.keychainZoneID];
150 [self saveTLKMaterialToKeychain:self.keychainZoneID];
151 [self expectCKKSTLKSelfShareUpload:self.keychainZoneID];
153 [self startCKKSSubsystem];
155 XCTAssertEqual(0, [self.keychainView.keyHierarchyConditions[SecCKKSZoneKeyStateReady] wait:20*NSEC_PER_SEC], "Key state should have become ready");
158 - (void)testUploadSelfTLKShare {
159 [self.keychainView setCurrentSyncingPolicy:[self viewSortingPolicyForManagedViewListWithUserControllableViews:[NSSet setWithObject:self.keychainView.zoneName]
160 syncUserControllableViews:TPPBPeerStableInfo_UserControllableViewStatus_DISABLED]
163 // Test starts with no keys in CKKS database, but one in our fake CloudKit.
164 [self putFakeKeyHierarchyInCloudKit:self.keychainZoneID];
166 // Test also starts with the TLK shared to all trusted peers from peer1
167 [self.mockSOSAdapter.trustedPeers addObject:self.remotePeer1];
168 [self putTLKSharesInCloudKit:self.keychainZoneKeys.tlk from:self.remotePeer1 zoneID:self.keychainZoneID];
170 // The CKKS subsystem should accept the keys, and share the TLK back to itself
171 [self expectCKModifyKeyRecords:0 currentKeyPointerRecords:0 tlkShareRecords:1 zoneID:self.keychainZoneID];
172 [self startCKKSSubsystem];
173 XCTAssertEqual(0, [self.keychainView.keyHierarchyConditions[SecCKKSZoneKeyStateReady] wait:20*NSEC_PER_SEC], "Key state should become ready");
175 OCMVerifyAllWithDelay(self.mockDatabase, 20);
178 - (void)testSendNewTLKSharesOnTrustSetAddition {
179 [self.keychainView setCurrentSyncingPolicy:[self viewSortingPolicyForManagedViewListWithUserControllableViews:[NSSet setWithObject:self.keychainView.zoneName]
180 syncUserControllableViews:TPPBPeerStableInfo_UserControllableViewStatus_DISABLED]
183 // step 1: add a new peer; we should share the TLK with them
184 // start with no trusted peers
185 [self.mockSOSAdapter.trustedPeers removeAllObjects];
187 [self startCKKSSubsystem];
188 [self performOctagonTLKUpload:self.ckksViews];
190 [self expectCKModifyKeyRecords:0 currentKeyPointerRecords:0 tlkShareRecords:1 zoneID:self.keychainZoneID];
191 [self.mockSOSAdapter.trustedPeers addObject:self.remotePeer1];
192 [self.mockSOSAdapter sendTrustedPeerSetChangedUpdate];
194 OCMVerifyAllWithDelay(self.mockDatabase, 20);
195 [self waitForCKModifications];
197 // and just double-check that no syncing is occurring
198 [self addGenericPassword:@"data" account:@"account-delete-me"];
199 [self.keychainView waitForOperationsOfClass:[CKKSOutgoingQueueOperation class]];
200 XCTAssertEqual(1, [self outgoingQueueSize:self.keychainView], "There should be one pending item in the outgoing queue");
203 - (void)testAddAndNotifyOnSyncDuringPausedOperation {
204 [self createAndSaveFakeKeyHierarchy:self.keychainZoneID];
205 [self startCKKSSubsystem];
207 XCTAssertEqual(0, [self.keychainView.keyHierarchyConditions[SecCKKSZoneKeyStateReady] wait:20*NSEC_PER_SEC], "Key state should have become ready");
208 OCMVerifyAllWithDelay(self.mockDatabase, 20);
210 [self.keychainView setCurrentSyncingPolicy:[self viewSortingPolicyForManagedViewListWithUserControllableViews:[NSSet setWithObject:self.keychainView.zoneName]
211 syncUserControllableViews:TPPBPeerStableInfo_UserControllableViewStatus_DISABLED]
214 NSMutableDictionary* query = [@{
215 (id)kSecClass : (id)kSecClassGenericPassword,
216 (id)kSecAttrAccessGroup : @"com.apple.security.ckks",
217 (id)kSecAttrAccessible: (id)kSecAttrAccessibleAfterFirstUnlock,
218 (id)kSecAttrAccount : @"testaccount",
219 (id)kSecAttrSynchronizable : (id)kCFBooleanTrue,
220 (id)kSecAttrSyncViewHint : self.keychainView.zoneName,
221 (id)kSecValueData : (id) [@"asdf" dataUsingEncoding:NSUTF8StringEncoding],
224 XCTestExpectation* blockExpectation = [self expectationWithDescription: @"callback occurs"];
226 XCTAssertEqual(errSecSuccess, _SecItemAddAndNotifyOnSync((__bridge CFDictionaryRef) query, NULL, ^(bool didSync, CFErrorRef cferror) {
227 XCTAssertFalse(didSync, "Item did not sync");
229 NSError* error = (__bridge NSError*)cferror;
230 XCTAssertNotNil(error, "Error syncing item");
231 XCTAssertEqual(error.domain, CKKSErrorDomain, "Error domain was CKKSErrorDomain");
232 XCTAssertEqual(error.code, CKKSErrorViewIsPaused, "Error code is 'view is paused'");
234 [blockExpectation fulfill];
235 }), @"_SecItemAddAndNotifyOnSync succeeded");
237 OCMVerifyAllWithDelay(self.mockDatabase, 10);
239 [self waitForExpectationsWithTimeout:5.0 handler:nil];