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, but not update any keys
65 [self expectCKModifyKeyRecords: 0 currentKeyPointerRecords: 1 tlkShareRecords: 0 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, 8);
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, but not update any keys
103 [self expectCKModifyKeyRecords: 0 currentKeyPointerRecords: 1 tlkShareRecords: 0 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, 8);
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];
126 CKReference* oldClassCKey = self.keychainZone.currentDatabase[self.keychainZoneKeys.currentClassCPointer.storedCKRecord.recordID][SecCKRecordParentKeyRefKey];
128 // Spin up CKKS subsystem.
129 [self startCKKSSubsystem];
132 [self expectCKModifyItemRecords: 1 currentKeyPointerRecords: 1 zoneID:self.keychainZoneID
133 checkItem: [self checkClassCBlock:self.keychainZoneID message:@"Object was encrypted under class C key in hierarchy"]];
134 [self addGenericPassword: @"data" account: @"account-delete-me"];
135 OCMVerifyAllWithDelay(self.mockDatabase, 8);
137 // Break the key hierarchy
138 [self rollFakeKeyHierarchyInCloudKit:self.keychainZoneID];
139 [self saveTLKMaterialToKeychain:self.keychainZoneID];
141 CKReference* newClassCKey = self.keychainZone.currentDatabase[self.keychainZoneKeys.currentClassCPointer.storedCKRecord.recordID][SecCKRecordParentKeyRefKey];
142 self.keychainZone.currentDatabase[self.keychainZoneKeys.currentClassCPointer.storedCKRecord.recordID][SecCKRecordParentKeyRefKey] = oldClassCKey;
143 self.keychainZoneKeys.currentClassCPointer.currentKeyUUID = oldClassCKey.recordID.recordName;
145 [self.keychainView notifyZoneChange:nil];
147 // CKKS should then fix the pointers, but not update any keys
148 [self expectCKModifyKeyRecords: 0 currentKeyPointerRecords: 1 tlkShareRecords: 0 zoneID:self.keychainZoneID];
149 [self.keychainView notifyZoneChange:nil];
150 OCMVerifyAllWithDelay(self.mockDatabase, 8);
152 // And then upload the item as usual
153 [self expectCKModifyItemRecords: 1 currentKeyPointerRecords: 1 zoneID:self.keychainZoneID
154 checkItem: [self checkClassCBlock:self.keychainZoneID message:@"Object was encrypted under class C key in hierarchy"]];
155 [self addGenericPassword: @"data" account: @"account-delete-me-2"];
156 OCMVerifyAllWithDelay(self.mockDatabase, 8);
158 [self waitForCKModifications];
159 XCTAssertEqualObjects(newClassCKey, self.keychainZone.currentDatabase[self.keychainZoneKeys.currentClassCPointer.storedCKRecord.recordID][SecCKRecordParentKeyRefKey], "CKKS should have fixed up the broken class C key reference");
162 - (void)testRecoverFromWrongClassCCurrentKeyPointersOnNotificationFixRejected {
163 // The current key pointers in cloudkit should always point directly under the top TLK.
165 // Test starts with a good key hierarchy in our fake CloudKit, and the TLK already arrived.
166 [self putFakeKeyHierarchyInCloudKit:self.keychainZoneID];
167 [self saveTLKMaterialToKeychain:self.keychainZoneID];
169 CKRecordID* classCPointerRecordID = self.keychainZoneKeys.currentClassCPointer.storedCKRecord.recordID;
171 CKReference* oldClassCKey = self.keychainZone.currentDatabase[classCPointerRecordID][SecCKRecordParentKeyRefKey];
173 // Spin up CKKS subsystem.
174 [self startCKKSSubsystem];
177 [self expectCKModifyItemRecords: 1 currentKeyPointerRecords: 1 zoneID:self.keychainZoneID
178 checkItem: [self checkClassCBlock:self.keychainZoneID message:@"Object was encrypted under class C key in hierarchy"]];
179 [self addGenericPassword: @"data" account: @"account-delete-me"];
180 OCMVerifyAllWithDelay(self.mockDatabase, 8);
182 // Break the key hierarchy
183 [self rollFakeKeyHierarchyInCloudKit:self.keychainZoneID];
184 [self saveTLKMaterialToKeychain:self.keychainZoneID];
186 CKReference* newClassCKey = self.keychainZone.currentDatabase[classCPointerRecordID][SecCKRecordParentKeyRefKey];
187 CKRecord* classCPointer = self.keychainZone.currentDatabase[classCPointerRecordID];
189 CKRecord* brokenClassCPointer = [classCPointer copy];
190 brokenClassCPointer[SecCKRecordParentKeyRefKey] = oldClassCKey;
191 self.keychainZoneKeys.currentClassCPointer.currentKeyUUID = oldClassCKey.recordID.recordName;
192 [self.keychainZone addCKRecordToZone:brokenClassCPointer];
194 self.silentFetchesAllowed = false;
195 [self expectCKFetchAndRunBeforeFinished: ^{
196 // Directly after CKKS fetches, we should fix up the pointers to be right again
197 self.keychainZoneKeys.currentClassCPointer.currentKeyUUID = newClassCKey.recordID.recordName;
198 [self.keychainZone addToZone: classCPointer];
199 XCTAssertEqualObjects(newClassCKey, self.keychainZone.currentDatabase[classCPointerRecordID][SecCKRecordParentKeyRefKey], "CKKS should have fixed up the broken class C key reference");
200 self.silentFetchesAllowed = true;
203 // CKKS should try to fix the pointers, but be rejected (since someone else has already fixed them)
204 // It should not try again, because someone already fixed them
205 [self expectCKAtomicModifyItemRecordsUpdateFailure:self.keychainZoneID];
206 [self.keychainView notifyZoneChange:nil];
207 OCMVerifyAllWithDelay(self.mockDatabase, 8);
209 // And then use the 'new' key as it should
210 [self expectCKModifyItemRecords: 1 currentKeyPointerRecords: 1 zoneID:self.keychainZoneID
211 checkItem: [self checkClassCBlock:self.keychainZoneID message:@"Object was encrypted under class C key in hierarchy"]];
212 [self addGenericPassword: @"data" account: @"account-delete-me-2"];
213 OCMVerifyAllWithDelay(self.mockDatabase, 8);
215 [self waitForCKModifications];
216 XCTAssertEqualObjects(newClassCKey, self.keychainZone.currentDatabase[classCPointerRecordID][SecCKRecordParentKeyRefKey], "CKKS should have fixed up the broken class C key reference");
219 - (void)testRecoverFromWrongClassCCurrentKeyPointersOnRecordWrite {
220 // The current key pointers in cloudkit should always point directly under the top TLK.
222 // Test starts with a good but rolled key hierarchy in our fake CloudKit, and the TLK already arrived.
223 [self putFakeKeyHierarchyInCloudKit:self.keychainZoneID];
224 [self saveTLKMaterialToKeychain:self.keychainZoneID];
226 CKRecordID* classCPointerRecordID = self.keychainZoneKeys.currentClassCPointer.storedCKRecord.recordID;
227 CKReference* oldClassCKey = self.keychainZone.currentDatabase[classCPointerRecordID][SecCKRecordParentKeyRefKey];
229 [self rollFakeKeyHierarchyInCloudKit:self.keychainZoneID];
230 [self saveTLKMaterialToKeychain:self.keychainZoneID];
232 // Spin up CKKS subsystem.
233 [self startCKKSSubsystem];
236 [self expectCKModifyItemRecords: 1 currentKeyPointerRecords: 1 zoneID:self.keychainZoneID
237 checkItem: [self checkClassCBlock:self.keychainZoneID message:@"Object was encrypted under class C key in hierarchy"]];
238 [self addGenericPassword: @"data" account: @"account-delete-me"];
239 OCMVerifyAllWithDelay(self.mockDatabase, 8);
241 // Now, break the class C pointer, but don't tell CKKS
242 CKReference* newClassCKey = self.keychainZone.currentDatabase[classCPointerRecordID][SecCKRecordParentKeyRefKey];
243 CKRecord* classCPointer = self.keychainZone.currentDatabase[classCPointerRecordID];
245 CKRecord* brokenClassCPointer = [classCPointer copy];
246 brokenClassCPointer[SecCKRecordParentKeyRefKey] = oldClassCKey;
247 self.keychainZoneKeys.currentClassCPointer.currentKeyUUID = oldClassCKey.recordID.recordName;
248 [self.keychainZone addCKRecordToZone:brokenClassCPointer];
250 // CKKS should receive a key hierarchy error, since it's wrong in CloudKit
251 // It should then fix the pointers and retry the upload
252 [self expectCKReceiveSyncKeyHierarchyError:self.keychainZoneID];
253 [self expectCKModifyKeyRecords: 0 currentKeyPointerRecords: 1 tlkShareRecords: 0 zoneID:self.keychainZoneID];
255 // And then use the 'new' key as it should
256 [self expectCKModifyItemRecords: 1 currentKeyPointerRecords: 1 zoneID:self.keychainZoneID
257 checkItem: [self checkClassCBlock:self.keychainZoneID message:@"Object was encrypted under class C key in hierarchy"]];
258 [self addGenericPassword: @"data" account: @"account-delete-me-2"];
259 OCMVerifyAllWithDelay(self.mockDatabase, 8);
261 [self waitForCKModifications];
262 XCTAssertEqualObjects(newClassCKey, self.keychainZone.currentDatabase[classCPointerRecordID][SecCKRecordParentKeyRefKey], "CKKS should have fixed up the broken class C key reference");