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>
29 #import <Foundation/NSKeyedArchiver_Private.h>
31 #import "keychain/ckks/tests/CloudKitMockXCTest.h"
32 #import "keychain/ckks/tests/CloudKitKeychainSyncingMockXCTest.h"
33 #import "keychain/ckks/CKKS.h"
34 #import "keychain/ckks/CKKSFixups.h"
35 #import "keychain/ckks/CKKSZoneStateEntry.h"
36 #import "keychain/ckks/CKKSViewManager.h"
37 #import "keychain/ckks/CKKSCurrentItemPointer.h"
38 #import "keychain/ckks/CKKSIncomingQueueOperation.h"
40 #import "keychain/ckks/tests/MockCloudKit.h"
41 #import "keychain/ckks/tests/CKKSTests.h"
42 #import "keychain/ckks/tests/CKKSTests+API.h"
45 @interface CloudKitKeychainSyncingFixupTests : CloudKitKeychainSyncingTestsBase
48 @implementation CloudKitKeychainSyncingFixupTests
50 - (void)testNoFixupOnInitialStart {
51 id mockFixups = OCMClassMock([CKKSFixups class]);
52 OCMReject([[[mockFixups stub] ignoringNonObjectArgs] fixup:0 for:[OCMArg any]]);
54 [self expectCKModifyKeyRecords: 3 currentKeyPointerRecords: 3 tlkShareRecords:1 zoneID:self.keychainZoneID];
55 [self startCKKSSubsystem];
57 [self.keychainView waitForKeyHierarchyReadiness];
58 [self waitForCKModifications];
59 OCMVerifyAllWithDelay(self.mockDatabase, 8);
62 [mockFixups stopMocking];
65 - (void)testImmediateRestartUsesLatestFixup {
66 id mockFixups = OCMClassMock([CKKSFixups class]);
67 OCMExpect([mockFixups fixup:CKKSCurrentFixupNumber for:[OCMArg any]]);
69 // Test starts with nothing in database. We expect some sort of TLK/key hierarchy upload.
70 [self expectCKModifyKeyRecords: 3 currentKeyPointerRecords: 3 tlkShareRecords:1 zoneID:self.keychainZoneID];
71 [self startCKKSSubsystem];
73 [self.keychainView waitForKeyHierarchyReadiness];
74 [self waitForCKModifications];
75 OCMVerifyAllWithDelay(self.mockDatabase, 8);
77 // Tear down the CKKS object
78 [self.keychainView halt];
80 self.keychainView = [[CKKSViewManager manager] restartZone:self.keychainZoneID.zoneName];
81 [self.keychainView waitForKeyHierarchyReadiness];
82 OCMVerifyAllWithDelay(self.mockDatabase, 8);
85 [mockFixups stopMocking];
88 - (void)testFixupRefetchAllCurrentItemPointers {
89 // Due to <rdar://problem/34916549> CKKS: current item pointer CKRecord resurrection,
90 // CKKS needs to refetch all current item pointers if it restarts and hasn't yet.
92 // Test starts with no keys in database. We expect some sort of TLK/key hierarchy upload.
93 [self expectCKModifyKeyRecords:3 currentKeyPointerRecords:3 tlkShareRecords:1 zoneID:self.keychainZoneID];
94 [self startCKKSSubsystem];
96 [self.keychainView waitForKeyHierarchyReadiness];
97 [self waitForCKModifications];
98 OCMVerifyAllWithDelay(self.mockDatabase, 8);
100 // Add some current item pointers. They don't necessarily need to point to anything...
102 CKKSCurrentItemPointer* cip = [[CKKSCurrentItemPointer alloc] initForIdentifier:@"com.apple.security.ckks-pcsservice"
103 currentItemUUID:@"DD7C2F9B-B22D-3B90-C299-E3B48174BFA3"
104 state:SecCKKSProcessedStateRemote
105 zoneID:self.keychainZoneID
106 encodedCKRecord:nil];
107 [self.keychainZone addToZone: [cip CKRecordWithZoneID:self.keychainZoneID]];
108 CKRecordID* currentPointerRecordID = [[CKRecordID alloc] initWithRecordName: @"com.apple.security.ckks-pcsservice" zoneID:self.keychainZoneID];
109 CKRecord* currentPointerRecord = self.keychainZone.currentDatabase[currentPointerRecordID];
110 XCTAssertNotNil(currentPointerRecord, "Found record in CloudKit at expected UUID");
112 CKKSCurrentItemPointer* cip2 = [[CKKSCurrentItemPointer alloc] initForIdentifier:@"com.apple.security.ckks-pcsservice2"
113 currentItemUUID:@"3AB8E78D-75AF-CFEF-F833-FA3E3E90978A"
114 state:SecCKKSProcessedStateRemote
115 zoneID:self.keychainZoneID
116 encodedCKRecord:nil];
117 [self.keychainZone addToZone: [cip2 CKRecordWithZoneID:self.keychainZoneID]];
118 CKRecordID* currentPointerRecordID2 = [[CKRecordID alloc] initWithRecordName: @"com.apple.security.ckks-pcsservice2" zoneID:self.keychainZoneID];
119 CKRecord* currentPointerRecord2 = self.keychainZone.currentDatabase[currentPointerRecordID2];
120 XCTAssertNotNil(currentPointerRecord2, "Found record in CloudKit at expected UUID");
122 [self.keychainView notifyZoneChange:nil];
123 [self.keychainView waitForFetchAndIncomingQueueProcessing];
125 // Tear down the CKKS object
126 [self.keychainView halt];
128 [self.keychainView dispatchSync: ^bool {
129 // Edit the zone state entry to have no fixups
130 NSError* error = nil;
131 CKKSZoneStateEntry* ckse = [CKKSZoneStateEntry fromDatabase:self.keychainZoneID.zoneName error:&error];
133 XCTAssertNil(error, "no error pulling ckse from database");
134 XCTAssertNotNil(ckse, "received a ckse");
136 ckse.lastFixup = CKKSFixupNever;
137 [ckse saveToDatabase: &error];
138 XCTAssertNil(error, "no error saving to database");
140 // And add a garbage CIP
141 CKKSCurrentItemPointer* cip3 = [[CKKSCurrentItemPointer alloc] initForIdentifier:@"garbage"
142 currentItemUUID:@"3AB8E78D-75AF-CFEF-F833-FA3E3E90978A"
143 state:SecCKKSProcessedStateLocal
144 zoneID:self.keychainZoneID
145 encodedCKRecord:nil];
146 cip3.storedCKRecord = [cip3 CKRecordWithZoneID:self.keychainZoneID];
147 XCTAssertEqual(cip3.identifier, @"garbage", "Identifier is what we thought it was");
148 [cip3 saveToDatabase:&error];
149 XCTAssertNil(error, "no error saving to database");
153 self.silentFetchesAllowed = false;
154 [self expectCKFetchByRecordID];
155 [self expectCKFetchByQuery]; // and one for the TLKShare fixup
157 // Change one of the CIPs while CKKS is offline
158 cip2.currentItemUUID = @"changed-by-cloudkit";
159 [self.keychainZone addToZone: [cip2 CKRecordWithZoneID:self.keychainZoneID]];
161 // Bring CKKS back up
162 self.keychainView = [[CKKSViewManager manager] restartZone:self.keychainZoneID.zoneName];
163 [self.keychainView waitForKeyHierarchyReadiness];
165 [self.keychainView waitForOperationsOfClass:[CKKSIncomingQueueOperation class]];
166 OCMVerifyAllWithDelay(self.mockDatabase, 8);
168 [self.keychainView dispatchSync: ^bool {
169 // The zone state entry should be up the most recent fixup level
170 NSError* error = nil;
171 CKKSZoneStateEntry* ckse = [CKKSZoneStateEntry fromDatabase:self.keychainZoneID.zoneName error:&error];
172 XCTAssertNil(error, "no error pulling ckse from database");
173 XCTAssertNotNil(ckse, "received a ckse");
174 XCTAssertEqual(ckse.lastFixup, CKKSCurrentFixupNumber, "CKSE should have the current fixup number stored");
176 // The garbage CIP should be gone, and CKKS should have caught up to the CIP change
177 NSArray<CKKSCurrentItemPointer*>* allCIPs = [CKKSCurrentItemPointer allInZone:self.keychainZoneID error:&error];
178 XCTAssertNil(error, "no error loading all CIPs from database");
180 XCTestExpectation* foundCIP2 = [self expectationWithDescription: @"found CIP2"];
181 for(CKKSCurrentItemPointer* loaded in allCIPs) {
182 if([loaded.identifier isEqualToString: cip2.identifier]) {
184 XCTAssertEqualObjects(loaded.currentItemUUID, @"changed-by-cloudkit", "Fixup should have fixed UUID to new value, not pre-shutdown value");
186 XCTAssertNotEqualObjects(loaded.identifier, @"garbage", "Garbage CIP shouldn't exist anymore");
189 [self waitForExpectations:@[foundCIP2] timeout:0.1];
194 - (void)setFixupNumber:(CKKSFixup)newFixup ckks:(CKKSKeychainView*)ckks {
195 [ckks dispatchSync: ^bool {
196 // Edit the zone state entry to have no fixups
197 NSError* error = nil;
198 CKKSZoneStateEntry* ckse = [CKKSZoneStateEntry fromDatabase:ckks.zoneID.zoneName error:&error];
200 XCTAssertNil(error, "no error pulling ckse from database");
201 XCTAssertNotNil(ckse, "received a ckse");
203 ckse.lastFixup = newFixup;
204 [ckse saveToDatabase: &error];
205 XCTAssertNil(error, "no error saving to database");
210 - (void)testFixupFetchAllTLKShareRecords {
211 // In <rdar://problem/34901306> CKKSTLK: TLKShare CloudKit upload/download on TLK change, trust set addition,
212 // we added the TLKShare CKRecord type. Upgrading devices must fetch all such records when they come online for the first time.
214 // Test starts with nothing in database. We expect some sort of TLK/key hierarchy upload.
215 // Note that this already does TLK sharing, and so technically doesn't need to do the fixup, but we'll fix that later.
216 [self expectCKModifyKeyRecords:3 currentKeyPointerRecords:3 tlkShareRecords:1 zoneID:self.keychainZoneID];
217 [self startCKKSSubsystem];
219 [self.keychainView waitForKeyHierarchyReadiness];
220 [self waitForCKModifications];
221 OCMVerifyAllWithDelay(self.mockDatabase, 8);
223 // Tear down the CKKS object
224 [self.keychainView halt];
225 [self setFixupNumber:CKKSFixupRefetchCurrentItemPointers ckks:self.keychainView];
227 // Also, create a TLK share record that CKKS should find
228 // Make another share, but from an untrusted peer to some other peer. local shouldn't necessarily care.
229 NSError* error = nil;
231 CKKSSOSSelfPeer* remotePeer1 = [[CKKSSOSSelfPeer alloc] initWithSOSPeerID:@"remote-peer1"
232 encryptionKey:[[SFECKeyPair alloc] initRandomKeyPairWithSpecifier:[[SFECKeySpecifier alloc] initWithCurve:SFEllipticCurveNistp384]]
233 signingKey:[[SFECKeyPair alloc] initRandomKeyPairWithSpecifier:[[SFECKeySpecifier alloc] initWithCurve:SFEllipticCurveNistp384]]];
235 CKKSTLKShare* share = [CKKSTLKShare share:self.keychainZoneKeys.tlk
237 to:self.currentSelfPeer
241 XCTAssertNil(error, "Should have been no error sharing a CKKSKey");
242 XCTAssertNotNil(share, "Should be able to create a share");
244 CKRecord* shareCKRecord = [share CKRecordWithZoneID: self.keychainZoneID];
245 XCTAssertNotNil(shareCKRecord, "Should have been able to create a CKRecord");
246 [self.keychainZone addToZone:shareCKRecord];
249 self.silentFetchesAllowed = false;
250 [self expectCKFetchByQuery];
252 // We want to ensure that the key hierarchy state machine doesn't progress past fixup until we let this go
253 [self holdCloudKitFetches];
255 self.keychainView = [[CKKSViewManager manager] restartZone:self.keychainZoneID.zoneName];
257 XCTAssertEqual(0, [self.keychainView.keyHierarchyConditions[SecCKKSZoneKeyStateWaitForFixupOperation] wait:8*NSEC_PER_SEC], "Key state should become waitforfixup");
258 [self releaseCloudKitFetchHold];
259 XCTAssertEqual(0, [self.keychainView.keyHierarchyConditions[SecCKKSZoneKeyStateReady] wait:8*NSEC_PER_SEC], "Key state should become ready");
261 OCMVerifyAllWithDelay(self.mockDatabase, 8);
263 [self.keychainView.lastFixupOperation waitUntilFinished];
264 XCTAssertNil(self.keychainView.lastFixupOperation.error, "Shouldn't have been any error performing fixup");
266 // and check that the share made it
267 [self.keychainView dispatchSync:^bool {
268 NSError* blockerror = nil;
269 CKKSTLKShare* localshare = [CKKSTLKShare tryFromDatabaseFromCKRecordID:shareCKRecord.recordID error:&blockerror];
270 XCTAssertNil(blockerror, "Shouldn't error finding new TLKShare record in database");
271 XCTAssertNotNil(localshare, "Should be able to find a new TLKShare record in database");
276 - (void)testFixupLocalReload {
277 // In <rdar://problem/35540228> Server Generated CloudKit "Manatee Identity Lost"
278 // items could be deleted from the local keychain after CKKS believed they were already synced, and therefore wouldn't resync
280 [self expectCKModifyKeyRecords:3 currentKeyPointerRecords:3 tlkShareRecords:1 zoneID:self.keychainZoneID];
281 [self startCKKSSubsystem];
283 XCTAssertEqual(0, [self.keychainView.keyHierarchyConditions[SecCKKSZoneKeyStateReady] wait:8*NSEC_PER_SEC], @"Key state should become 'ready'");
284 OCMVerifyAllWithDelay(self.mockDatabase, 8);
285 [self waitForCKModifications];
287 [self addGenericPassword: @"data" account: @"first"];
288 [self expectCKModifyItemRecords:1 currentKeyPointerRecords:1 zoneID:self.keychainZoneID];
289 OCMVerifyAllWithDelay(self.mockDatabase, 8);
290 [self waitForCKModifications];
292 // Add another record to mock up early CKKS record saving
293 __block CKRecordID* secondRecordID = nil;
294 CKKSCondition* secondRecordIDFilled = [[CKKSCondition alloc] init];
295 [self addGenericPassword: @"data" account: @"second"];
296 [self expectCKModifyItemRecords:1 currentKeyPointerRecords:1 zoneID:self.keychainZoneID checkItem:^BOOL(CKRecord * _Nonnull record) {
297 secondRecordID = record.recordID;
298 [secondRecordIDFilled fulfill];
301 OCMVerifyAllWithDelay(self.mockDatabase, 8);
302 [self waitForCKModifications];
303 XCTAssertNotNil(secondRecordID, "Should have filled in secondRecordID");
304 XCTAssertEqual(0, [secondRecordIDFilled wait:8*NSEC_PER_SEC], "Should have filled in secondRecordID within enough time");
306 // Tear down the CKKS object
307 [self.keychainView halt];
308 [self setFixupNumber:CKKSFixupFetchTLKShares ckks:self.keychainView];
310 // Delete items from keychain
311 [self deleteGenericPassword:@"first"];
313 // Corrupt the second item's CKMirror entry to only contain system fields in the CKRecord portion (to emulate early CKKS behavior)
314 [self.keychainView dispatchSync:^bool {
315 NSError* error = nil;
316 CKKSMirrorEntry* ckme = [CKKSMirrorEntry fromDatabase:secondRecordID.recordName zoneID:self.keychainZoneID error:&error];
317 XCTAssertNil(error, "Should have no error pulling second CKKSMirrorEntry from database");
319 NSKeyedArchiver *archiver = [[NSKeyedArchiver alloc] initRequiringSecureCoding:YES];
320 [ckme.item.storedCKRecord encodeSystemFieldsWithCoder:archiver];
321 ckme.item.encodedCKRecord = archiver.encodedData;
323 [ckme saveToDatabase:&error];
324 XCTAssertNil(error, "No error saving system-fielded CKME back to database");
328 // Now, restart CKKS, but place a hold on the fixup operation
329 self.silentFetchesAllowed = false;
330 self.accountStatus = CKAccountStatusCouldNotDetermine;
331 [self.accountStateTracker notifyCircleStatusChangeAndWaitForSignal];
333 self.keychainView = [[CKKSViewManager manager] restartZone:self.keychainZoneID.zoneName];
334 self.keychainView.holdFixupOperation = [CKKSResultOperation named:@"hold-fixup" withBlock:^{}];
336 self.accountStatus = CKAccountStatusAvailable;
337 [self.accountStateTracker notifyCircleStatusChangeAndWaitForSignal];
339 XCTAssertEqual(0, [self.keychainView.keyHierarchyConditions[SecCKKSZoneKeyStateWaitForFixupOperation] wait:8*NSEC_PER_SEC], "Key state should become waitforfixup");
340 [self.operationQueue addOperation: self.keychainView.holdFixupOperation];
341 XCTAssertEqual(0, [self.keychainView.keyHierarchyConditions[SecCKKSZoneKeyStateReady] wait:8*NSEC_PER_SEC], "Key state should become ready");
343 [self.keychainView.lastFixupOperation waitUntilFinished];
344 XCTAssertNil(self.keychainView.lastFixupOperation.error, "Shouldn't have been any error performing fixup");
346 [self.keychainView waitForOperationsOfClass:[CKKSIncomingQueueOperation class]];
348 // And the item should be back!
349 [self checkGenericPassword: @"data" account: @"first"];