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>
30 #import "keychain/ckks/tests/CloudKitMockXCTest.h"
31 #import "keychain/ckks/tests/CloudKitKeychainSyncingMockXCTest.h"
32 #import "keychain/ckks/CKKS.h"
33 #import "keychain/ckks/CKKSCurrentKeyPointer.h"
34 #import "keychain/ckks/CKKSKey.h"
35 #import "keychain/ckks/CKKSOutgoingQueueEntry.h"
36 #import "keychain/ckks/CKKSIncomingQueueEntry.h"
38 #import "keychain/ckks/tests/MockCloudKit.h"
39 #import "keychain/ckks/tests/CKKSTests.h"
41 @interface CloudKitKeychainSyncingServerValidationRecoveryTests : CloudKitKeychainSyncingTestsBase
44 @implementation CloudKitKeychainSyncingServerValidationRecoveryTests
46 /* Tests for CKKSServerUnexpectedSyncKeyInChain */
48 - (void)testRecoverFromWrongClassACurrentKeyPointersOnStartup {
49 // The current key pointers in cloudkit should always point directly under the top TLK.
51 // Test starts with a broken key hierarchy in our fake CloudKit, and the TLK already arrived.
52 [self putFakeKeyHierarchyInCloudKit:self.keychainZoneID];
53 [self saveTLKMaterialToKeychain:self.keychainZoneID];
55 CKReference* oldClassAKey = self.keychainZone.currentDatabase[self.keychainZoneKeys.currentClassAPointer.storedCKRecord.recordID][SecCKRecordParentKeyRefKey];
56 [self rollFakeKeyHierarchyInCloudKit:self.keychainZoneID];
57 [self saveTLKMaterialToKeychain:self.keychainZoneID];
58 CKReference* newClassAKey = self.keychainZone.currentDatabase[self.keychainZoneKeys.currentClassAPointer.storedCKRecord.recordID][SecCKRecordParentKeyRefKey];
60 // Break the reference
61 self.keychainZone.currentDatabase[self.keychainZoneKeys.currentClassAPointer.storedCKRecord.recordID][SecCKRecordParentKeyRefKey] = oldClassAKey;
62 self.keychainZoneKeys.currentClassAPointer.currentKeyUUID = oldClassAKey.recordID.recordName;
64 // CKKS should then fix the pointers and give itself a TLK share, but not update any keys
65 [self expectCKModifyKeyRecords: 0 currentKeyPointerRecords: 1 tlkShareRecords: 1 zoneID:self.keychainZoneID];
67 // And then upload the record as normal
68 [self expectCKModifyItemRecords: 1 currentKeyPointerRecords: 1 zoneID:self.keychainZoneID
69 checkItem: [self checkClassABlock:self.keychainZoneID message:@"Object was encrypted under class A key in hierarchy"]];
70 [self addGenericPassword:@"asdf"
71 account:@"account-class-A"
73 access:(id)kSecAttrAccessibleWhenUnlocked
74 expecting:errSecSuccess
75 message:@"Adding class A item"];
77 // Spin up CKKS subsystem.
78 [self startCKKSSubsystem];
80 OCMVerifyAllWithDelay(self.mockDatabase, 20);
82 [self waitForCKModifications];
83 XCTAssertEqualObjects(newClassAKey, self.keychainZone.currentDatabase[self.keychainZoneKeys.currentClassAPointer.storedCKRecord.recordID][SecCKRecordParentKeyRefKey], "CKKS should have fixed up the broken class A key reference");
86 - (void)testRecoverFromWrongClassCCurrentKeyPointersOnStartup {
87 // The current key pointers in cloudkit should always point directly under the top TLK.
89 // Test starts with a broken key hierarchy in our fake CloudKit, and the TLK already arrived.
90 [self putFakeKeyHierarchyInCloudKit:self.keychainZoneID];
91 [self saveTLKMaterialToKeychain:self.keychainZoneID];
93 CKReference* oldClassCKey = self.keychainZone.currentDatabase[self.keychainZoneKeys.currentClassCPointer.storedCKRecord.recordID][SecCKRecordParentKeyRefKey];
94 [self rollFakeKeyHierarchyInCloudKit:self.keychainZoneID];
95 [self saveTLKMaterialToKeychain:self.keychainZoneID];
96 CKReference* newClassCKey = self.keychainZone.currentDatabase[self.keychainZoneKeys.currentClassCPointer.storedCKRecord.recordID][SecCKRecordParentKeyRefKey];
98 // Break the reference
99 self.keychainZone.currentDatabase[self.keychainZoneKeys.currentClassCPointer.storedCKRecord.recordID][SecCKRecordParentKeyRefKey] = oldClassCKey;
100 self.keychainZoneKeys.currentClassCPointer.currentKeyUUID = oldClassCKey.recordID.recordName;
102 // CKKS should then fix the pointers and its TLK shares, but not update any keys
103 [self expectCKModifyKeyRecords:0 currentKeyPointerRecords:1 tlkShareRecords:1 zoneID:self.keychainZoneID];
105 // And then upload the record as normal
106 [self expectCKModifyItemRecords: 1 currentKeyPointerRecords: 1 zoneID:self.keychainZoneID
107 checkItem: [self checkClassCBlock:self.keychainZoneID message:@"Object was encrypted under class C key in hierarchy"]];
108 [self addGenericPassword: @"data" account: @"account-delete-me"];
110 // Spin up CKKS subsystem.
111 [self startCKKSSubsystem];
113 OCMVerifyAllWithDelay(self.mockDatabase, 20);
115 [self waitForCKModifications];
116 XCTAssertEqualObjects(newClassCKey, self.keychainZone.currentDatabase[self.keychainZoneKeys.currentClassCPointer.storedCKRecord.recordID][SecCKRecordParentKeyRefKey], "CKKS should have fixed up the broken class C key reference");
119 - (void)testRecoverFromWrongClassCCurrentKeyPointersOnNotification {
120 // The current key pointers in cloudkit should always point directly under the top TLK.
122 // Test starts with a good key hierarchy in our fake CloudKit, and the TLK already arrived.
123 [self putFakeKeyHierarchyInCloudKit:self.keychainZoneID];
124 [self saveTLKMaterialToKeychain:self.keychainZoneID];
125 [self expectCKKSTLKSelfShareUpload:self.keychainZoneID];
127 CKReference* oldClassCKey = self.keychainZone.currentDatabase[self.keychainZoneKeys.currentClassCPointer.storedCKRecord.recordID][SecCKRecordParentKeyRefKey];
129 // Spin up CKKS subsystem.
130 [self startCKKSSubsystem];
133 [self expectCKModifyItemRecords: 1 currentKeyPointerRecords: 1 zoneID:self.keychainZoneID
134 checkItem: [self checkClassCBlock:self.keychainZoneID message:@"Object was encrypted under class C key in hierarchy"]];
135 [self addGenericPassword: @"data" account: @"account-delete-me"];
136 OCMVerifyAllWithDelay(self.mockDatabase, 20);
138 // Break the key hierarchy
139 [self rollFakeKeyHierarchyInCloudKit:self.keychainZoneID];
140 [self saveTLKMaterialToKeychain:self.keychainZoneID];
142 CKReference* newClassCKey = self.keychainZone.currentDatabase[self.keychainZoneKeys.currentClassCPointer.storedCKRecord.recordID][SecCKRecordParentKeyRefKey];
143 self.keychainZone.currentDatabase[self.keychainZoneKeys.currentClassCPointer.storedCKRecord.recordID][SecCKRecordParentKeyRefKey] = oldClassCKey;
144 self.keychainZoneKeys.currentClassCPointer.currentKeyUUID = oldClassCKey.recordID.recordName;
146 [self.injectedManager.zoneChangeFetcher notifyZoneChange:nil];
148 // CKKS should then fix the pointers and give itself a new TLK share record, but not update any keys
149 [self expectCKModifyKeyRecords:0 currentKeyPointerRecords:1 tlkShareRecords:1 zoneID:self.keychainZoneID];
150 [self.injectedManager.zoneChangeFetcher notifyZoneChange:nil];
151 OCMVerifyAllWithDelay(self.mockDatabase, 20);
153 // And then upload the item as usual
154 [self expectCKModifyItemRecords: 1 currentKeyPointerRecords: 1 zoneID:self.keychainZoneID
155 checkItem: [self checkClassCBlock:self.keychainZoneID message:@"Object was encrypted under class C key in hierarchy"]];
156 [self addGenericPassword: @"data" account: @"account-delete-me-2"];
157 OCMVerifyAllWithDelay(self.mockDatabase, 20);
159 [self waitForCKModifications];
160 XCTAssertEqualObjects(newClassCKey, self.keychainZone.currentDatabase[self.keychainZoneKeys.currentClassCPointer.storedCKRecord.recordID][SecCKRecordParentKeyRefKey], "CKKS should have fixed up the broken class C key reference");
163 - (void)testRecoverFromWrongClassCCurrentKeyPointersOnNotificationFixRejected {
164 // The current key pointers in cloudkit should always point directly under the top TLK.
166 // Test starts with a good key hierarchy in our fake CloudKit, and the TLK already arrived.
167 [self putFakeKeyHierarchyInCloudKit:self.keychainZoneID];
168 [self saveTLKMaterialToKeychain:self.keychainZoneID];
169 [self expectCKKSTLKSelfShareUpload:self.keychainZoneID];
171 CKRecordID* classCPointerRecordID = self.keychainZoneKeys.currentClassCPointer.storedCKRecord.recordID;
173 CKReference* oldClassCKey = self.keychainZone.currentDatabase[classCPointerRecordID][SecCKRecordParentKeyRefKey];
175 // Spin up CKKS subsystem.
176 [self startCKKSSubsystem];
179 [self expectCKModifyItemRecords: 1 currentKeyPointerRecords: 1 zoneID:self.keychainZoneID
180 checkItem: [self checkClassCBlock:self.keychainZoneID message:@"Object was encrypted under class C key in hierarchy"]];
181 [self addGenericPassword: @"data" account: @"account-delete-me"];
182 OCMVerifyAllWithDelay(self.mockDatabase, 20);
184 XCTAssertEqual(0, [self.keychainView.keyHierarchyConditions[SecCKKSZoneKeyStateReady] wait:20*NSEC_PER_SEC], @"Key state should have arrived at ready");
185 [self waitForCKModifications];
187 // Break the key hierarchy. The TLK will arrive via SOS later in this test.
188 [self rollFakeKeyHierarchyInCloudKit:self.keychainZoneID];
190 CKReference* newClassCKey = self.keychainZone.currentDatabase[classCPointerRecordID][SecCKRecordParentKeyRefKey];
191 CKRecord* classCPointer = self.keychainZone.currentDatabase[classCPointerRecordID];
193 CKRecord* brokenClassCPointer = [classCPointer copy];
194 brokenClassCPointer[SecCKRecordParentKeyRefKey] = oldClassCKey;
195 self.keychainZoneKeys.currentClassCPointer.currentKeyUUID = oldClassCKey.recordID.recordName;
196 [self.keychainZone addCKRecordToZone:brokenClassCPointer];
198 self.silentFetchesAllowed = false;
199 [self expectCKFetchAndRunBeforeFinished: ^{
200 // Directly after CKKS fetches, we should fix up the pointers to be right again
201 self.keychainZoneKeys.currentClassCPointer.currentKeyUUID = newClassCKey.recordID.recordName;
202 [self.keychainZone addToZone: classCPointer];
203 XCTAssertEqualObjects(newClassCKey, self.keychainZone.currentDatabase[classCPointerRecordID][SecCKRecordParentKeyRefKey], "CKKS should have fixed up the broken class C key reference");
204 self.silentFetchesAllowed = true;
207 // CKKS should try to fix the pointers, but be rejected (since someone else has already fixed them)
208 // It should not try to modify the pointers again, but it should give itself the new TLK
209 [self expectCKAtomicModifyItemRecordsUpdateFailure:self.keychainZoneID];
210 [self expectCKKSTLKSelfShareUpload:self.keychainZoneID];
212 [self saveTLKMaterialToKeychain:self.keychainZoneID];
213 [self.injectedManager.zoneChangeFetcher notifyZoneChange:nil];
214 OCMVerifyAllWithDelay(self.mockDatabase, 20);
216 // And then use the 'new' key as it should
217 [self expectCKModifyItemRecords: 1 currentKeyPointerRecords: 1 zoneID:self.keychainZoneID
218 checkItem: [self checkClassCBlock:self.keychainZoneID message:@"Object was encrypted under class C key in hierarchy"]];
219 [self addGenericPassword: @"data" account: @"account-delete-me-2"];
220 OCMVerifyAllWithDelay(self.mockDatabase, 20);
222 [self waitForCKModifications];
223 XCTAssertEqualObjects(newClassCKey, self.keychainZone.currentDatabase[classCPointerRecordID][SecCKRecordParentKeyRefKey], "CKKS should have fixed up the broken class C key reference");
226 - (void)testRecoverFromWrongClassCCurrentKeyPointersOnRecordWrite {
227 // The current key pointers in cloudkit should always point directly under the top TLK.
229 // Test starts with a good but rolled key hierarchy in our fake CloudKit, and the TLK already arrived.
230 [self putFakeKeyHierarchyInCloudKit:self.keychainZoneID];
231 [self saveTLKMaterialToKeychain:self.keychainZoneID];
233 CKRecordID* classCPointerRecordID = self.keychainZoneKeys.currentClassCPointer.storedCKRecord.recordID;
234 CKReference* oldClassCKey = self.keychainZone.currentDatabase[classCPointerRecordID][SecCKRecordParentKeyRefKey];
236 [self rollFakeKeyHierarchyInCloudKit:self.keychainZoneID];
237 [self expectCKKSTLKSelfShareUpload:self.keychainZoneID];
238 [self saveTLKMaterialToKeychain:self.keychainZoneID];
240 // Spin up CKKS subsystem.
241 [self startCKKSSubsystem];
244 [self expectCKModifyItemRecords: 1 currentKeyPointerRecords: 1 zoneID:self.keychainZoneID
245 checkItem: [self checkClassCBlock:self.keychainZoneID message:@"Object was encrypted under class C key in hierarchy"]];
246 [self addGenericPassword: @"data" account: @"account-delete-me"];
247 OCMVerifyAllWithDelay(self.mockDatabase, 20);
248 [self waitForCKModifications];
250 // Now, break the class C pointer, but don't tell CKKS
251 CKReference* newClassCKey = self.keychainZone.currentDatabase[classCPointerRecordID][SecCKRecordParentKeyRefKey];
252 CKRecord* classCPointer = self.keychainZone.currentDatabase[classCPointerRecordID];
254 CKRecord* brokenClassCPointer = [classCPointer copy];
255 brokenClassCPointer[SecCKRecordParentKeyRefKey] = oldClassCKey;
256 self.keychainZoneKeys.currentClassCPointer.currentKeyUUID = oldClassCKey.recordID.recordName;
257 [self.keychainZone addCKRecordToZone:brokenClassCPointer];
259 // CKKS should receive a key hierarchy error, since it's wrong in CloudKit
260 // It should then fix the pointers and retry the upload
261 [self expectCKReceiveSyncKeyHierarchyError:self.keychainZoneID];
262 [self expectCKModifyKeyRecords: 0 currentKeyPointerRecords: 1 tlkShareRecords: 0 zoneID:self.keychainZoneID];
264 // And then use the 'new' key as it should
265 [self expectCKModifyItemRecords: 1 currentKeyPointerRecords: 1 zoneID:self.keychainZoneID
266 checkItem: [self checkClassCBlock:self.keychainZoneID message:@"Object was encrypted under class C key in hierarchy"]];
267 [self addGenericPassword: @"data" account: @"account-delete-me-2"];
268 OCMVerifyAllWithDelay(self.mockDatabase, 20);
270 [self waitForCKModifications];
271 XCTAssertEqualObjects(newClassCKey, self.keychainZone.currentDatabase[classCPointerRecordID][SecCKRecordParentKeyRefKey], "CKKS should have fixed up the broken class C key reference");