]> git.saurik.com Git - apple/security.git/blob - keychain/ckks/tests/CKKSTests.m
Security-59754.41.1.tar.gz
[apple/security.git] / keychain / ckks / tests / CKKSTests.m
1 /*
2 * Copyright (c) 2016 Apple Inc. All Rights Reserved.
3 *
4 * @APPLE_LICENSE_HEADER_START@
5 *
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
11 * file.
12 *
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.
20 *
21 * @APPLE_LICENSE_HEADER_END@
22 */
23
24 #if OCTAGON
25
26 #import "CKKSTests.h"
27 #import <CloudKit/CloudKit.h>
28 #import <Foundation/NSXPCConnection_Private.h>
29 #import <XCTest/XCTest.h>
30 #import <OCMock/OCMock.h>
31
32 #include <Security/SecItemPriv.h>
33 #include "keychain/securityd/SecItemDb.h"
34 #include "keychain/securityd/SecItemServer.h"
35 #include <utilities/SecFileLocations.h>
36 #include "keychain/SecureObjectSync/SOSInternal.h"
37
38 #import "keychain/ckks/tests/CloudKitMockXCTest.h"
39 #import "keychain/ckks/tests/CloudKitKeychainSyncingMockXCTest.h"
40 #import "keychain/ckks/CKKS.h"
41 #import "keychain/ckks/CKKSControlProtocol.h"
42 #import "keychain/ckks/CKKSCurrentKeyPointer.h"
43 #import "keychain/ckks/CKKSItemEncrypter.h"
44 #import "keychain/ckks/CKKSKey.h"
45 #import "keychain/ckks/CKKSOutgoingQueueEntry.h"
46 #import "keychain/ckks/CKKSIncomingQueueEntry.h"
47 #import "keychain/ckks/CKKSStates.h"
48 #import "keychain/ckks/CKKSSynchronizeOperation.h"
49 #import "keychain/ckks/CKKSViewManager.h"
50 #import "keychain/ckks/CKKSZoneStateEntry.h"
51 #import "keychain/ckks/CKKSManifest.h"
52 #import "keychain/ckks/CKKSAnalytics.h"
53 #import "keychain/ckks/CKKSZoneChangeFetcher.h"
54 #import "keychain/categories/NSError+UsefulConstructors.h"
55 #import "keychain/ckks/CKKSPeer.h"
56
57 #import "keychain/ckks/tests/MockCloudKit.h"
58
59 #import "keychain/ckks/tests/CKKSTests.h"
60 #import "keychain/ot/ObjCImprovements.h"
61 #import <utilities/SecCoreAnalytics.h>
62
63 // break abstraction
64 @interface CKKSLockStateTracker ()
65 @property (nullable) NSDate* lastUnlockedTime;
66 @end
67
68 @implementation CloudKitKeychainSyncingTests
69
70 #pragma mark - Tests
71
72 - (void)testBringupToKeyStateReady {
73 [self createAndSaveFakeKeyHierarchy: self.keychainZoneID]; // Make life easy for this test.
74 [self startCKKSSubsystem];
75
76 XCTAssertEqual(0, [self.keychainView.keyHierarchyConditions[SecCKKSZoneKeyStateReady] wait:20*NSEC_PER_SEC], @"Key state should have arrived at ready");
77 }
78
79 - (void)testAddItem {
80 [self createAndSaveFakeKeyHierarchy: self.keychainZoneID]; // Make life easy for this test.
81
82 // We expect a single record to be uploaded.
83 [self expectCKModifyItemRecords: 1 currentKeyPointerRecords: 1 zoneID:self.keychainZoneID];
84
85 [self startCKKSSubsystem];
86 XCTAssertEqual(0, [self.keychainView.keyHierarchyConditions[SecCKKSZoneKeyStateReady] wait:20*NSEC_PER_SEC], @"Key state should have arrived at ready");
87
88 [self addGenericPassword: @"data" account: @"account-delete-me"];
89
90 OCMVerifyAllWithDelay(self.mockDatabase, 20);
91 }
92
93 - (void)testActiveTLKs {
94 [self createAndSaveFakeKeyHierarchy: self.keychainZoneID]; // Make life easy for this test.
95
96 // We expect a single record to be uploaded.
97 [self expectCKModifyItemRecords: 1 currentKeyPointerRecords: 1 zoneID:self.keychainZoneID];
98
99 [self startCKKSSubsystem];
100 [self addGenericPassword: @"data" account: @"account-delete-me"];
101
102 OCMVerifyAllWithDelay(self.mockDatabase, 20);
103
104 NSError* localError = nil;
105 NSArray<CKKSKeychainBackedKey*>* tlks = [[CKKSViewManager manager] currentTLKsFilteredByPolicy:NO error:&localError];
106 XCTAssertNil(localError, "Should have no error fetching current TLKs");
107
108 XCTAssertEqual([tlks count], (NSUInteger)1, "Should have one TLK");
109 XCTAssertEqualObjects(tlks[0].zoneID.zoneName, @"keychain", "should have a TLK for keychain");
110
111 XCTAssertEqualObjects(tlks[0].uuid, self.keychainZoneKeys.tlk.uuid, "should have the TLK matching cloudkit");
112 }
113
114 - (void)testActiveTLKsWhenMissing {
115 [self putFakeKeyHierarchyInCloudKit:self.keychainZoneID];
116 [self putFakeDeviceStatusInCloudKit:self.keychainZoneID];
117
118 [self startCKKSSubsystem];
119 XCTAssertEqual(0, [self.keychainView.keyHierarchyConditions[SecCKKSZoneKeyStateWaitForTLK] wait:20*NSEC_PER_SEC], @"Key state should have arrived at waitfortlk");
120
121 NSError* localError = nil;
122 NSArray<CKKSKeychainBackedKey*>* tlks = [[CKKSViewManager manager] currentTLKsFilteredByPolicy:NO error:&localError];
123 XCTAssertNil(localError, "Should have no error fetching current TLKs");
124
125 XCTAssertEqual([tlks count], (NSUInteger)0, "Should have zero TLKs");
126 }
127
128 - (void)testActiveTLKsWhenLocked {
129 [self createAndSaveFakeKeyHierarchy: self.keychainZoneID];
130
131 [self startCKKSSubsystem];
132 XCTAssertEqual(0, [self.keychainView.keyHierarchyConditions[SecCKKSZoneKeyStateReady] wait:20*NSEC_PER_SEC], @"Key state should have arrived at ready");
133
134 self.aksLockState = true;
135 [self.lockStateTracker recheck];
136
137 NSError* localError = nil;
138 NSArray<CKKSKeychainBackedKey*>* tlks = [[CKKSViewManager manager] currentTLKsFilteredByPolicy:NO error:&localError];
139 XCTAssertNotNil(localError, "Should have an error fetching current TLKs");
140
141 XCTAssertEqual([tlks count], (NSUInteger)0, "Should have zero TLKs");
142 }
143
144 - (void)testAddMultipleItems {
145 [self createAndSaveFakeKeyHierarchy: self.keychainZoneID]; // Make life easy for this test.
146 [self startCKKSSubsystem];
147
148 [self expectCKModifyItemRecords: 1 currentKeyPointerRecords: 1 zoneID:self.keychainZoneID];
149 [self addGenericPassword: @"data" account: @"account-delete-me"];
150 OCMVerifyAllWithDelay(self.mockDatabase, 20);
151
152 [self expectCKModifyItemRecords: 1 currentKeyPointerRecords: 1 zoneID:self.keychainZoneID];
153 [self addGenericPassword: @"data" account: @"account-delete-me-2"];
154 OCMVerifyAllWithDelay(self.mockDatabase, 20);
155
156 [self expectCKModifyItemRecords: 1 currentKeyPointerRecords: 1 zoneID:self.keychainZoneID];
157 [self addGenericPassword: @"data" account: @"account-delete-me-3"];
158 OCMVerifyAllWithDelay(self.mockDatabase, 20);
159 }
160
161 - (void)testAddItemWithoutUUID {
162 // Test starts with no keys in database, a key hierarchy in our fake CloudKit, and the TLK safely in the local keychain.
163 [self putFakeKeyHierarchyInCloudKit:self.keychainZoneID];
164 [self expectCKKSTLKSelfShareUpload:self.keychainZoneID];
165 [self saveTLKMaterialToKeychain:self.keychainZoneID];
166
167 [self startCKKSSubsystem];
168
169 [self.keychainView waitUntilAllOperationsAreFinished];
170
171 // We expect an upload of the added item, once CKKS finds the UUID-less item and fixes it
172 [self expectCKModifyItemRecords:1 currentKeyPointerRecords:1 zoneID:self.keychainZoneID];
173
174 SecCKKSTestSetDisableAutomaticUUID(true);
175 [self addGenericPassword: @"data" account: @"account-delete-me-no-UUID" expecting:errSecSuccess message: @"Add item (no UUID) to keychain"];
176 SecCKKSTestSetDisableAutomaticUUID(false);
177
178 [self findGenericPassword:@"account-delete-me-no-UUID" expecting:errSecSuccess];
179
180 OCMVerifyAllWithDelay(self.mockDatabase, 20);
181 }
182
183 - (void)testModifyItem {
184 [self createAndSaveFakeKeyHierarchy: self.keychainZoneID]; // Make life easy for this test.
185
186 NSString* account = @"account-delete-me";
187
188 [self startCKKSSubsystem];
189
190 // We expect a single record to be uploaded.
191 [self expectCKModifyItemRecords: 1 currentKeyPointerRecords: 1 zoneID:self.keychainZoneID];
192 [self addGenericPassword: @"data" account: account];
193 OCMVerifyAllWithDelay(self.mockDatabase, 20);
194
195 // And then modified.
196 [self expectCKModifyItemRecords: 1 currentKeyPointerRecords: 1 zoneID:self.keychainZoneID];
197 [self updateGenericPassword: @"otherdata" account:account];
198 OCMVerifyAllWithDelay(self.mockDatabase, 20);
199 }
200
201 - (void)testModifyItemImmediately {
202 [self createAndSaveFakeKeyHierarchy: self.keychainZoneID]; // Make life easy for this test.
203 NSString* account = @"account-delete-me";
204
205 [self startCKKSSubsystem];
206 [self holdCloudKitModifications];
207
208 // We expect a single record to be uploaded, but need to hold the operation from finishing until we can modify the item locally
209 [self expectCKModifyItemRecords: 1 currentKeyPointerRecords: 1 zoneID:self.keychainZoneID
210 checkItem:[self checkPasswordBlock:self.keychainZoneID account:account password:@"data"]];
211 [self addGenericPassword: @"data" account: account];
212 OCMVerifyAllWithDelay(self.mockDatabase, 20);
213
214 // Right now, the write in CloudKit is pending. Make the local modification...
215 [self updateGenericPassword: @"otherdata" account:account];
216
217 // And then schedule the update
218 [self expectCKModifyItemRecords: 1 currentKeyPointerRecords: 1 zoneID:self.keychainZoneID
219 checkItem:[self checkPasswordBlock:self.keychainZoneID account:account password:@"otherdata"]];
220 [self releaseCloudKitModificationHold];
221
222 OCMVerifyAllWithDelay(self.mockDatabase, 20);
223 }
224
225 - (void)testModifyItemPrimaryKey {
226 [self createAndSaveFakeKeyHierarchy: self.keychainZoneID]; // Make life easy for this test.
227
228 NSString* account = @"account-delete-me";
229
230 [self startCKKSSubsystem];
231
232 // We expect a single record to be uploaded.
233 [self expectCKModifyItemRecords: 1 currentKeyPointerRecords: 1 zoneID:self.keychainZoneID];
234 [self addGenericPassword: @"data" account: account];
235 OCMVerifyAllWithDelay(self.mockDatabase, 20);
236
237 // And then modified. Since we're changing the "primary key", we expect to delete the old record and upload a new one.
238 [self expectCKModifyItemRecords:1 deletedRecords:1 currentKeyPointerRecords:1 zoneID:self.keychainZoneID checkItem:nil];
239 [self updateAccountOfGenericPassword: @"new-account-delete-me" account:account];
240 OCMVerifyAllWithDelay(self.mockDatabase, 20);
241 }
242
243 - (void)testModifyItemDuringReencrypt {
244 [self createAndSaveFakeKeyHierarchy: self.keychainZoneID]; // Make life easy for this test.
245 NSString* account = @"account-delete-me";
246
247 [self startCKKSSubsystem];
248 [self holdCloudKitModifications];
249
250 // We expect a single record to be uploaded, but need to hold the operation from finishing until we can modify the item locally
251 [self expectCKModifyItemRecords: 1 currentKeyPointerRecords: 1 zoneID:self.keychainZoneID
252 checkItem:[self checkPasswordBlock:self.keychainZoneID account:account password:@"data"]];
253 [self addGenericPassword: @"data" account: account];
254 OCMVerifyAllWithDelay(self.mockDatabase, 20);
255
256 // Right now, the write in CloudKit is pending. Make the local modification...
257 [self updateGenericPassword: @"otherdata" account:account];
258
259 // And then schedule the update
260 [self expectCKModifyItemRecords: 1 currentKeyPointerRecords: 1 zoneID:self.keychainZoneID
261 checkItem:[self checkPasswordBlock:self.keychainZoneID account:account password:@"otherdata"]];
262
263 // Stop the reencrypt operation from happening
264 self.keychainView.holdReencryptOutgoingItemsOperation = [CKKSGroupOperation named:@"reencrypt-hold" withBlock: ^{
265 ckksnotice_global("ckks", "releasing reencryption hold");
266 }];
267
268 // The cloudkit operation finishes, letting the next OQO proceed (and set up the reencryption operation)
269 [self releaseCloudKitModificationHold];
270
271 // And wait for this to finish...
272 [self.keychainView waitForOperationsOfClass:[CKKSOutgoingQueueOperation class]];
273 // And once more to quiesce.
274 [self.keychainView waitForOperationsOfClass:[CKKSOutgoingQueueOperation class]];
275
276 // Pause outgoing queue operations to ensure the reencryption operation runs first
277 self.keychainView.holdOutgoingQueueOperation = [CKKSGroupOperation named:@"outgoing-hold" withBlock: ^{
278 ckksnotice_global("ckks", "releasing outgoing-queue hold");
279 }];
280
281 // Run the reencrypt items operation to completion.
282 [self.operationQueue addOperation: self.keychainView.holdReencryptOutgoingItemsOperation];
283 [self.keychainView waitForOperationsOfClass:[CKKSReencryptOutgoingItemsOperation class]];
284
285 [self.operationQueue addOperation: self.keychainView.holdOutgoingQueueOperation];
286
287 OCMVerifyAllWithDelay(self.mockDatabase, 20);
288 [self.keychainView waitUntilAllOperationsAreFinished];
289 [self waitForCKModifications];
290 }
291
292 - (void)testModifyItemBeforeReencrypt {
293 [self createAndSaveFakeKeyHierarchy: self.keychainZoneID]; // Make life easy for this test.
294 NSString* account = @"account-delete-me";
295
296 [self startCKKSSubsystem];
297 [self holdCloudKitModifications];
298
299 // We expect a single record to be uploaded, but need to hold the operation from finishing until we can modify the item locally
300 [self expectCKModifyItemRecords: 1 currentKeyPointerRecords: 1 zoneID:self.keychainZoneID
301 checkItem:[self checkPasswordBlock:self.keychainZoneID account:account password:@"data"]];
302 [self addGenericPassword: @"data" account: account];
303 OCMVerifyAllWithDelay(self.mockDatabase, 20);
304
305 // Right now, the write in CloudKit is pending. Make the local modification...
306 [self updateGenericPassword: @"otherdata" account:account];
307
308 // And then schedule the update, but for the final version of the password
309 [self expectCKModifyItemRecords: 1 currentKeyPointerRecords: 1 zoneID:self.keychainZoneID
310 checkItem:[self checkPasswordBlock:self.keychainZoneID account:account password:@"third"]];
311
312 // Stop the reencrypt operation from happening
313 self.keychainView.holdReencryptOutgoingItemsOperation = [CKKSGroupOperation named:@"reencrypt-hold" withBlock: ^{
314 ckksnotice_global("ckks", "releasing reencryption hold");
315 }];
316
317 // The cloudkit operation finishes, letting the next OQO proceed (and set up the reencryption operation)
318 [self releaseCloudKitModificationHold];
319
320 // And wait for this to finish...
321 [self.keychainView waitForOperationsOfClass:[CKKSOutgoingQueueOperation class]];
322 // And once more to quiesce.
323 [self.keychainView waitForOperationsOfClass:[CKKSOutgoingQueueOperation class]];
324
325 [self updateGenericPassword: @"third" account:account];
326
327 // Item should upload.
328 OCMVerifyAllWithDelay(self.mockDatabase, 20);
329
330 // Run the reencrypt items operation to completion.
331 [self.operationQueue addOperation: self.keychainView.holdReencryptOutgoingItemsOperation];
332 [self.keychainView waitForOperationsOfClass:[CKKSReencryptOutgoingItemsOperation class]];
333
334 [self.keychainView waitUntilAllOperationsAreFinished];
335 [self waitForCKModifications];
336 }
337
338 - (void)testModifyItemDuringNetworkFailure {
339 [self createAndSaveFakeKeyHierarchy: self.keychainZoneID]; // Make life easy for this test.
340 NSString* account = @"account-delete-me";
341
342 [self startCKKSSubsystem];
343 [self holdCloudKitModifications];
344
345 // We expect a single record to be uploaded, but need to hold the operation from finishing until we can modify the item locally
346 [self failNextCKAtomicModifyItemRecordsUpdateFailure:self.keychainZoneID];
347
348 [self addGenericPassword: @"data" account: account];
349 OCMVerifyAllWithDelay(self.mockDatabase, 20);
350
351 // Right now, the write in CloudKit is pending. Make the local modification...
352 [self updateGenericPassword: @"otherdata" account:account];
353
354 // And then schedule the update, but for the final version of the password
355 [self expectCKModifyItemRecords: 1 currentKeyPointerRecords: 1 zoneID:self.keychainZoneID
356 checkItem:[self checkPasswordBlock:self.keychainZoneID account:account password:@"otherdata"]];
357
358 // The cloudkit operation finishes, letting the next OQO proceed (and set up uploading the new item)
359 [self releaseCloudKitModificationHold];
360
361 // Item should upload.
362 OCMVerifyAllWithDelay(self.mockDatabase, 20);
363
364 [self.keychainView waitUntilAllOperationsAreFinished];
365 [self waitForCKModifications];
366 }
367
368 - (void)testOutgoingQueueRecoverFromStaleInflightEntry {
369 // CKKS is restarting with an existing in-flight OQE
370 // Note that this test is incomplete, and doesn't re-add the item to the local keychain
371 [self createAndSaveFakeKeyHierarchy: self.keychainZoneID]; // Make life easy for this test.
372 NSString* account = @"fake-account";
373
374 [self.keychainView dispatchSyncWithSQLTransaction:^CKKSDatabaseTransactionResult{
375 NSError* error = nil;
376
377 CKRecordID* ckrid = [[CKRecordID alloc] initWithRecordName:@"50184A35-4480-E8BA-769B-567CF72F1EC0" zoneID:self.keychainZoneID];
378
379 CKKSItem* item = [self newItem:ckrid withNewItemData:[self fakeRecordDictionary:account zoneID:self.keychainZoneID] key:self.keychainZoneKeys.classC];
380 XCTAssertNotNil(item, "Should be able to create a new fake item");
381
382 CKKSOutgoingQueueEntry* oqe = [[CKKSOutgoingQueueEntry alloc] initWithCKKSItem:item action:SecCKKSActionAdd state:SecCKKSStateInFlight waitUntil:nil accessGroup:@"ckks"];
383 XCTAssertNotNil(oqe, "Should be able to create a new fake OQE");
384 [oqe saveToDatabase:&error];
385
386 XCTAssertNil(error, "Shouldn't error saving new OQE to database");
387 return CKKSDatabaseTransactionCommit;
388 }];
389
390 NSError *error = NULL;
391 XCTAssertEqual([CKKSOutgoingQueueEntry countByState:SecCKKSStateInFlight zone:self.keychainZoneID error:&error], 1,
392 "Expected on inflight entry in outgoing queue: %@", error);
393
394 // When CKKS restarts, it should find and re-upload this item
395 [self expectCKModifyItemRecords: 1 currentKeyPointerRecords: 1 zoneID:self.keychainZoneID
396 checkItem:[self checkPasswordBlock:self.keychainZoneID account:account password:@"data"]];
397
398 [self startCKKSSubsystem];
399 [self.keychainView waitForFetchAndIncomingQueueProcessing];
400
401 self.keychainView = [[CKKSViewManager manager] restartZone: self.keychainZoneID.zoneName];
402 [self beginSOSTrustedViewOperation:self.keychainView];
403 [self.keychainView waitForKeyHierarchyReadiness];
404 OCMVerifyAllWithDelay(self.mockDatabase, 20);
405 }
406
407 - (void)testOutgoingQueueRecoverFromNetworkFailure {
408 [self createAndSaveFakeKeyHierarchy: self.keychainZoneID]; // Make life easy for this test.
409 NSString* account = @"account-delete-me";
410
411 [self startCKKSSubsystem];
412 [self holdCloudKitModifications];
413
414 // We expect a single record to be uploaded, but need to hold the operation from finishing until we can modify the item locally
415
416 NSError* greyMode = [[CKPrettyError alloc] initWithDomain:CKErrorDomain code:CKErrorNotAuthenticated userInfo:@{
417 CKErrorRetryAfterKey: @(0.2),
418 }];
419 [self failNextCKAtomicModifyItemRecordsUpdateFailure:self.keychainZoneID blockAfterReject:nil withError:greyMode];
420
421 [self addGenericPassword: @"data" account: account];
422 OCMVerifyAllWithDelay(self.mockDatabase, 20);
423
424 // And then schedule the retried update
425 [self expectCKModifyItemRecords: 1 currentKeyPointerRecords: 1 zoneID:self.keychainZoneID
426 checkItem:[self checkPasswordBlock:self.keychainZoneID account:account password:@"data"]];
427
428 // The cloudkit operation finishes, letting the next OQO proceed (and set up uploading the new item)
429 [self releaseCloudKitModificationHold];
430
431 OCMVerifyAllWithDelay(self.mockDatabase, 20);
432
433 [self.keychainView waitUntilAllOperationsAreFinished];
434 [self waitForCKModifications];
435 }
436
437 - (void)testDeleteItem {
438 [self createAndSaveFakeKeyHierarchy: self.keychainZoneID]; // Make life easy for this test.
439
440 [self startCKKSSubsystem];
441
442 // We expect a single record to be uploaded.
443 [self expectCKModifyItemRecords: 1 currentKeyPointerRecords: 1 zoneID:self.keychainZoneID];
444 [self addGenericPassword: @"data" account: @"account-delete-me"];
445 OCMVerifyAllWithDelay(self.mockDatabase, 20);
446
447 // We expect a single record to be deleted.
448 [self expectCKDeleteItemRecords: 1 zoneID:self.keychainZoneID];
449 [self deleteGenericPassword:@"account-delete-me"];
450 OCMVerifyAllWithDelay(self.mockDatabase, 20);
451 }
452
453 - (void)testDeleteItemAndReaddAtSameUUID {
454 [self createAndSaveFakeKeyHierarchy: self.keychainZoneID]; // Make life easy for this test.
455
456 [self startCKKSSubsystem];
457
458 // We expect a single record to be uploaded.
459 __block CKRecordID* itemRecordID = nil;
460 [self expectCKModifyItemRecords:1 currentKeyPointerRecords:1 zoneID:self.keychainZoneID checkItem:^BOOL(CKRecord * _Nonnull record) {
461 itemRecordID = record.recordID;
462 return YES;
463 }];
464 [self addGenericPassword:@"data" account:@"account-delete-me"];
465 OCMVerifyAllWithDelay(self.mockDatabase, 20);
466
467 // We expect a single record to be deleted.
468 [self expectCKDeleteItemRecords:1 zoneID:self.keychainZoneID];
469 [self deleteGenericPassword:@"account-delete-me"];
470 OCMVerifyAllWithDelay(self.mockDatabase, 20);
471
472 // And the item is readded. It should come back to its previous UUID.
473 XCTAssertNotNil(itemRecordID, "Should have an item record ID");
474 [self expectCKModifyItemRecords:1 currentKeyPointerRecords:1 zoneID:self.keychainZoneID checkItem:^BOOL(CKRecord * _Nonnull record) {
475 XCTAssertEqualObjects(itemRecordID.recordName, record.recordID.recordName, "Uploaded item UUID should match previous upload");
476 return YES;
477 }];
478 [self addGenericPassword:@"data" account:@"account-delete-me"];
479 OCMVerifyAllWithDelay(self.mockDatabase, 20);
480 }
481
482 - (void)testDeleteItemImmediatelyAfterModify {
483 [self createAndSaveFakeKeyHierarchy: self.keychainZoneID]; // Make life easy for this test.
484 NSString* account = @"account-delete-me";
485
486 [self startCKKSSubsystem];
487
488 // We expect a single record to be uploaded.
489 [self expectCKModifyItemRecords: 1 currentKeyPointerRecords: 1 zoneID:self.keychainZoneID];
490 [self addGenericPassword: @"data" account: account];
491 OCMVerifyAllWithDelay(self.mockDatabase, 20);
492
493 // Now, hold the modify
494 [self holdCloudKitModifications];
495
496 // We expect a single record to be uploaded, but need to hold the operation from finishing until we can modify the item locally
497 [self expectCKModifyItemRecords: 1 currentKeyPointerRecords: 1 zoneID:self.keychainZoneID
498 checkItem:[self checkPasswordBlock:self.keychainZoneID account:account password:@"otherdata"]];
499
500 [self updateGenericPassword: @"otherdata" account:account];
501 OCMVerifyAllWithDelay(self.mockDatabase, 20);
502
503 // Right now, the write in CloudKit is pending. Make the local deletion...
504 [self deleteGenericPassword:account];
505
506 // And then schedule the update
507 [self expectCKDeleteItemRecords:1 zoneID:self.keychainZoneID];
508 [self releaseCloudKitModificationHold];
509
510 OCMVerifyAllWithDelay(self.mockDatabase, 20);
511 }
512
513 - (void)testDeleteItemDuringAddUpload {
514 [self createAndSaveFakeKeyHierarchy: self.keychainZoneID];
515 NSString* account = @"account-delete-me";
516
517 [self startCKKSSubsystem];
518 XCTAssertEqual(0, [self.keychainView.keyHierarchyConditions[SecCKKSZoneKeyStateReady] wait:10*NSEC_PER_SEC], @"key state should enter 'ready'");
519
520 // We expect a single record to be uploaded. But, while that's happening, delete it via the API.
521
522 XCTestExpectation *deleteBlock = [self expectationWithDescription:@"delete block called"];
523
524 WEAKIFY(self);
525 self.keychainZone.blockBeforeWriteOperation = ^() {
526 STRONGIFY(self);
527 [self deleteGenericPassword:account];
528 self.keychainZone.blockBeforeWriteOperation = nil;
529 [deleteBlock fulfill];
530 };
531
532 [self expectCKModifyItemRecords:1 currentKeyPointerRecords:1 zoneID:self.keychainZoneID];
533
534 // This should cause a deletion
535 [self expectCKDeleteItemRecords:1 zoneID:self.keychainZoneID];
536 [self addGenericPassword:@"data" account:account];
537 OCMVerifyAllWithDelay(self.mockDatabase, 20);
538
539 [self waitForExpectations: @[deleteBlock] timeout:5];
540 }
541
542 - (void)testDeleteItemDuringModificationUpload {
543 [self createAndSaveFakeKeyHierarchy: self.keychainZoneID];
544 NSString* account = @"account-delete-me";
545
546 [self startCKKSSubsystem];
547 XCTAssertEqual(0, [self.keychainView.keyHierarchyConditions[SecCKKSZoneKeyStateReady] wait:10*NSEC_PER_SEC], @"key state should enter 'ready'");
548
549 // We expect a single record to be uploaded.
550 [self expectCKModifyItemRecords:1 currentKeyPointerRecords:1 zoneID:self.keychainZoneID];
551 [self addGenericPassword: @"data" account: account];
552 OCMVerifyAllWithDelay(self.mockDatabase, 20);
553
554 // We expect a single modification record to be uploaded, and want to delete the item while the upload is ongoing
555 [self expectCKModifyItemRecords:1 currentKeyPointerRecords:1 zoneID:self.keychainZoneID
556 checkItem:[self checkPasswordBlock:self.keychainZoneID account:account password:@"otherdata"]];
557 [self expectCKDeleteItemRecords:1 zoneID:self.keychainZoneID];
558
559 XCTestExpectation *deleteBlock = [self expectationWithDescription:@"delete block called"];
560
561 WEAKIFY(self);
562 self.keychainZone.blockBeforeWriteOperation = ^() {
563 STRONGIFY(self);
564 [self deleteGenericPassword:account];
565 self.keychainZone.blockBeforeWriteOperation = nil;
566 [deleteBlock fulfill];
567 };
568
569 [self updateGenericPassword:@"otherdata" account:account];
570 OCMVerifyAllWithDelay(self.mockDatabase, 20);
571
572 [self waitForExpectations: @[deleteBlock] timeout:5];
573 }
574
575 - (void)testDeleteItemAfterFetchAfterModify {
576 [self createAndSaveFakeKeyHierarchy: self.keychainZoneID]; // Make life easy for this test.
577 NSString* account = @"account-delete-me";
578
579 [self startCKKSSubsystem];
580
581 // We expect a single record to be uploaded.
582 [self expectCKModifyItemRecords: 1 currentKeyPointerRecords: 1 zoneID:self.keychainZoneID];
583 [self addGenericPassword: @"data" account: account];
584 OCMVerifyAllWithDelay(self.mockDatabase, 20);
585
586 // Now, hold the modify
587 //[self holdCloudKitModifications];
588
589 // We expect a single record to be uploaded, but need to hold the operation from finishing until we can modify the item locally
590 [self expectCKModifyItemRecords: 1 currentKeyPointerRecords: 1 zoneID:self.keychainZoneID
591 checkItem:[self checkPasswordBlock:self.keychainZoneID account:account password:@"otherdata"]];
592
593 [self updateGenericPassword: @"otherdata" account:account];
594 OCMVerifyAllWithDelay(self.mockDatabase, 20);
595
596 // Right now, the write in CloudKit is pending. Place a hold on outgoing queue processing
597 // Place a hold on processing the outgoing queue.
598 self.keychainView.holdOutgoingQueueOperation = [CKKSResultOperation named:@"outgoing-queue-hold"
599 withBlock:^{
600 ckksnotice_global("ckks", "Outgoing queue hold released.");
601 }];
602
603 [self deleteGenericPassword:account];
604 [self expectCKDeleteItemRecords:1 zoneID:self.keychainZoneID];
605
606 // Release the CK modification hold
607 //[self releaseCloudKitModificationHold];
608
609 // And cause a fetch
610 [self.injectedManager.zoneChangeFetcher notifyZoneChange:nil];
611 [self.keychainView waitForFetchAndIncomingQueueProcessing];
612 [self.operationQueue addOperation:self.keychainView.holdOutgoingQueueOperation];
613
614 [self.keychainView waitForOperationsOfClass:[CKKSOutgoingQueueOperation class]];
615 OCMVerifyAllWithDelay(self.mockDatabase, 20);
616 }
617
618 - (void)testDeleteItemWithoutTombstones {
619 // The keychain API allows a client to ask for an inconsistent sync state:
620 // They can ask for a local item deletion without propagating the deletion off-device.
621 // This is the only halfway reasonable way to do keychain item deletions on account signout with the current API
622
623 [self createAndSaveFakeKeyHierarchy: self.keychainZoneID]; // Make life easy for this test.
624 NSString* account = @"account-delete-me";
625
626 [self startCKKSSubsystem];
627 XCTAssertEqual(0, [self.keychainView.keyHierarchyConditions[SecCKKSZoneKeyStateReady] wait:10*NSEC_PER_SEC], @"key state should enter 'ready'");
628 [self.keychainView waitForOperationsOfClass:[CKKSIncomingQueueOperation class]];
629
630 // We expect a single record to be uploaded.
631 [self expectCKModifyItemRecords:1 currentKeyPointerRecords:1 zoneID:self.keychainZoneID];
632 [self addGenericPassword: @"data" account: account];
633 OCMVerifyAllWithDelay(self.mockDatabase, 20);
634
635 [self.keychainView waitForOperationsOfClass:[CKKSOutgoingQueueOperation class]];
636
637 [self deleteGenericPasswordWithoutTombstones:account];
638 [self findGenericPassword:account expecting:errSecItemNotFound];
639
640 [self.keychainView waitForOperationsOfClass:[CKKSOutgoingQueueOperation class]];
641
642 // Ensure nothing is in the outgoing queue
643 [self.keychainView dispatchSyncWithReadOnlySQLTransaction:^{
644 NSError* error = nil;
645 NSArray<NSString*>* uuids = [CKKSOutgoingQueueEntry allUUIDs:self.keychainZoneID
646 error:&error];
647 XCTAssertNil(error, "should be no error fetching uuids");
648 XCTAssertEqual(uuids.count, 0u, "There should be zero OQEs");
649 }];
650
651 // And a simple fetch doesn't bring it back
652 [self.injectedManager.zoneChangeFetcher notifyZoneChange:nil];
653 [self.keychainView waitForFetchAndIncomingQueueProcessing];
654 [self findGenericPassword:account expecting:errSecItemNotFound];
655
656 // but a resync does
657 CKKSSynchronizeOperation* resyncOperation = [self.keychainView resyncWithCloud];
658 [resyncOperation waitUntilFinished];
659 XCTAssertNil(resyncOperation.error, "No error during the resync operation");
660
661
662 [self findGenericPassword:account expecting:errSecSuccess];
663
664 OCMVerifyAllWithDelay(self.mockDatabase, 20);
665 }
666
667
668 - (void)testReceiveItem {
669 [self createAndSaveFakeKeyHierarchy: self.keychainZoneID]; // Make life easy for this test.
670 [self startCKKSSubsystem];
671
672 NSDictionary *query = @{(id)kSecClass : (id)kSecClassGenericPassword,
673 (id)kSecAttrAccessGroup : @"com.apple.security.ckks",
674 (id)kSecAttrAccount : @"account-delete-me",
675 (id)kSecAttrSynchronizable : (id)kCFBooleanTrue,
676 (id)kSecMatchLimit : (id)kSecMatchLimitOne,
677 };
678
679 CFTypeRef item = NULL;
680 XCTAssertEqual(errSecItemNotFound, SecItemCopyMatching((__bridge CFDictionaryRef) query, &item), "item should not yet exist");
681
682 XCTAssertEqual(0, [self.keychainView.keyHierarchyConditions[SecCKKSZoneKeyStateReady] wait:20*NSEC_PER_SEC], @"key state should enter 'ready'");
683
684 CKRecord* ckr = [self createFakeRecord: self.keychainZoneID recordName:@"7B598D31-F9C5-481E-98AC-5A507ACB2D85"];
685 [self.keychainZone addToZone: ckr];
686
687 // Trigger a notification (with hilariously fake data)
688 [self.injectedManager.zoneChangeFetcher notifyZoneChange:nil];
689 [self.keychainView waitForFetchAndIncomingQueueProcessing];
690
691 XCTAssertEqual(errSecSuccess, SecItemCopyMatching((__bridge CFDictionaryRef) query, &item), "item should exist now");
692 }
693
694 - (void)testReceiveManyItems {
695 [self createAndSaveFakeKeyHierarchy: self.keychainZoneID]; // Make life easy for this test.
696 [self startCKKSSubsystem];
697
698 [self.keychainZone addToZone: [self createFakeRecord: self.keychainZoneID recordName:@"7B598D31-F9C5-481E-98AC-5A507ACB2D00" withAccount:@"account0"]];
699 [self.keychainZone addToZone: [self createFakeRecord: self.keychainZoneID recordName:@"7B598D31-F9C5-481E-98AC-5A507ACB2D01" withAccount:@"account1"]];
700 [self.keychainZone addToZone: [self createFakeRecord: self.keychainZoneID recordName:@"7B598D31-F9C5-481E-98AC-5A507ACB2D02" withAccount:@"account2"]];
701 [self.keychainZone addToZone: [self createFakeRecord: self.keychainZoneID recordName:@"7B598D31-F9C5-481E-98AC-5A507ACB2D03" withAccount:@"account3"]];
702 [self.keychainZone addToZone: [self createFakeRecord: self.keychainZoneID recordName:@"7B598D31-F9C5-481E-98AC-5A507ACB2D04" withAccount:@"account4"]];
703 [self.keychainZone addToZone: [self createFakeRecord: self.keychainZoneID recordName:@"7B598D31-F9C5-481E-98AC-5A507ACB2D05" withAccount:@"account5"]];
704 [self.keychainZone addToZone: [self createFakeRecord: self.keychainZoneID recordName:@"7B598D31-F9C5-481E-98AC-5A507ACB2D06" withAccount:@"account6"]];
705 [self.keychainZone addToZone: [self createFakeRecord: self.keychainZoneID recordName:@"7B598D31-F9C5-481E-98AC-5A507ACB2D07" withAccount:@"account7"]];
706 [self.keychainZone addToZone: [self createFakeRecord: self.keychainZoneID recordName:@"7B598D31-F9C5-481E-98AC-5A507ACB2D08" withAccount:@"account8"]];
707 [self.keychainZone addToZone: [self createFakeRecord: self.keychainZoneID recordName:@"7B598D31-F9C5-481E-98AC-5A507ACB2D09" withAccount:@"account9"]];
708 [self.keychainZone addToZone: [self createFakeRecord: self.keychainZoneID recordName:@"7B598D31-F9C5-481E-98AC-5A507ACB2D10" withAccount:@"account10"]];
709 [self.keychainZone addToZone: [self createFakeRecord: self.keychainZoneID recordName:@"7B598D31-F9C5-481E-98AC-5A507ACB2D11" withAccount:@"account11"]];
710
711 for(int i = 12; i < 100; i++) {
712 @autoreleasepool {
713 NSString* recordName = [NSString stringWithFormat:@"7B598D31-F9C5-481E-98AC-%012d", i];
714 NSString* account = [NSString stringWithFormat:@"account%d", i];
715
716 [self.keychainZone addToZone: [self createFakeRecord: self.keychainZoneID recordName:recordName withAccount:account]];
717 }
718 }
719
720 [self.injectedManager.zoneChangeFetcher notifyZoneChange:nil];
721 [self.keychainView waitForFetchAndIncomingQueueProcessing];
722
723 [self findGenericPassword: @"account0" expecting:errSecSuccess];
724 [self findGenericPassword: @"account1" expecting:errSecSuccess];
725 [self findGenericPassword: @"account2" expecting:errSecSuccess];
726 [self findGenericPassword: @"account3" expecting:errSecSuccess];
727 [self findGenericPassword: @"account4" expecting:errSecSuccess];
728 [self findGenericPassword: @"account5" expecting:errSecSuccess];
729 [self findGenericPassword: @"account6" expecting:errSecSuccess];
730 [self findGenericPassword: @"account7" expecting:errSecSuccess];
731 [self findGenericPassword: @"account8" expecting:errSecSuccess];
732 [self findGenericPassword: @"account9" expecting:errSecSuccess];
733 [self findGenericPassword: @"account10" expecting:errSecSuccess];
734 [self findGenericPassword: @"account11" expecting:errSecSuccess];
735 }
736
737 - (void)testReceiveCollidingItem {
738 [self createAndSaveFakeKeyHierarchy: self.keychainZoneID]; // Make life easy for this test.
739 [self startCKKSSubsystem];
740
741 NSDictionary *query = @{(id)kSecClass : (id)kSecClassGenericPassword,
742 (id)kSecAttrAccessGroup : @"com.apple.security.ckks",
743 (id)kSecAttrAccount : @"account-delete-me",
744 (id)kSecAttrSynchronizable : (id)kCFBooleanTrue,
745 (id)kSecMatchLimit : (id)kSecMatchLimitOne,
746 };
747
748 CFTypeRef item = NULL;
749 XCTAssertEqual(errSecItemNotFound, SecItemCopyMatching((__bridge CFDictionaryRef) query, &item), "item should not yet exist");
750
751 CKRecord* ckr = [self createFakeRecord: self.keychainZoneID recordName: @"7B598D31-F9C5-481E-98AC-5A507ACB2D85"];
752 CKRecord* ckr2 = [self createFakeRecord: self.keychainZoneID recordName: @"F9C58D31-7B59-481E-98AC-5A507ACB2D85"];
753
754 [self.keychainZone addToZone: ckr];
755 [self.keychainZone addToZone: ckr2];
756
757 // We expect a delete operation with the "higher" UUID.
758 [self expectCKDeleteItemRecords:1 zoneID:self.keychainZoneID];
759
760 // Trigger a notification (with hilariously fake data)
761 [self.injectedManager.zoneChangeFetcher notifyZoneChange:nil];
762
763 OCMVerifyAllWithDelay(self.mockDatabase, 20);
764 XCTAssertEqual(errSecSuccess, SecItemCopyMatching((__bridge CFDictionaryRef) query, &item), "item should exist now");
765
766 [self waitForCKModifications];
767 XCTAssertNil(self.keychainZone.currentDatabase[ckr2.recordID], "Correct record was deleted from CloudKit");
768
769 // And the local item should have ckr's UUID
770 [self checkGenericPasswordStoredUUID:ckr.recordID.recordName account:@"account-delete-me"];
771 }
772
773 - (void)testReceiveCorruptedItem {
774 [self createAndSaveFakeKeyHierarchy: self.keychainZoneID]; // Make life easy for this test.
775 [self startCKKSSubsystem];
776
777 [self findGenericPassword:@"account-delete-me" expecting:errSecItemNotFound];
778
779 CKRecord* ckr = [self createFakeRecord:self.keychainZoneID recordName:@"7B598D31-F9C5-481E-98AC-5A507ACB2D85"];
780
781 // I don't know of any codepaths that cause this, but it apparently has happened.
782 ckr[SecCKRecordWrappedKeyKey] = nil;
783 [self.keychainZone addToZone:ckr];
784
785 [self.injectedManager.zoneChangeFetcher notifyZoneChange:nil];
786 [self.keychainView waitForFetchAndIncomingQueueProcessing];
787
788 // The item still shouldn't exist, because it was corrupted in flight
789 [self findGenericPassword:@"account-delete-me" expecting:errSecItemNotFound];
790
791 [self.keychainView dispatchSyncWithReadOnlySQLTransaction:^{
792 NSError* error = nil;
793 NSArray<CKKSIncomingQueueEntry*>* iqes = [CKKSIncomingQueueEntry all:&error];
794 XCTAssertNil(error, "No error loading IQEs");
795 XCTAssertNotNil(iqes, "Could load IQEs");
796 XCTAssertEqual(iqes.count, 1u, "Incoming queue has one item");
797 XCTAssertEqualObjects(iqes[0].state, SecCKKSStateNew, "Item state should be 'new'");
798 }];
799 }
800
801 -(void)testReceiveItemDelete {
802 [self createAndSaveFakeKeyHierarchy: self.keychainZoneID]; // Make life easy for this test.
803 [self startCKKSSubsystem];
804
805 NSDictionary *query = @{(id)kSecClass : (id)kSecClassGenericPassword,
806 (id)kSecAttrAccessGroup : @"com.apple.security.ckks",
807 (id)kSecAttrAccount : @"account-delete-me",
808 (id)kSecAttrSynchronizable : (id)kCFBooleanTrue,
809 (id)kSecReturnAttributes : @YES,
810 (id)kSecMatchLimit : (id)kSecMatchLimitOne,
811 };
812
813 CFTypeRef cfitem = NULL;
814 XCTAssertEqual(errSecItemNotFound, SecItemCopyMatching((__bridge CFDictionaryRef) query, &cfitem), "item should not yet exist");
815
816 [self.injectedManager.zoneChangeFetcher notifyZoneChange:nil];
817 [self.keychainView waitForFetchAndIncomingQueueProcessing];
818
819 CKRecord* ckr = [self createFakeRecord: self.keychainZoneID recordName: @"7B598D31-F9C5-481E-98AC-5A507ACB2D85"];
820 [self.keychainZone addToZone: ckr];
821
822 [self.injectedManager.zoneChangeFetcher notifyZoneChange:nil];
823 [self.keychainView waitForFetchAndIncomingQueueProcessing];
824
825 XCTAssertEqual(errSecSuccess, SecItemCopyMatching((__bridge CFDictionaryRef) query, &cfitem), "item should exist now");
826
827 NSDictionary* item = (NSDictionary*) CFBridgingRelease(cfitem);
828 cfitem = NULL;
829 NSDate* itemModificationDate = item[(id)kSecAttrModificationDate];
830 XCTAssertNotNil(itemModificationDate, "Should have a modification date");
831
832 // Trigger a delete
833 [self.keychainZone deleteCKRecordIDFromZone: [ckr recordID]];
834 [self.injectedManager.zoneChangeFetcher notifyZoneChange:nil];
835 [self.keychainView waitForFetchAndIncomingQueueProcessing];
836
837 XCTAssertEqual(errSecItemNotFound, SecItemCopyMatching((__bridge CFDictionaryRef) query, &cfitem), "item should no longer exist");
838 CFReleaseNull(cfitem);
839
840 // Now, double-check the tombstone. Its modification date should be derived from the item's mdat.
841 NSDictionary *tombquery = @{(id)kSecClass : (id)kSecClassGenericPassword,
842 (id)kSecAttrAccessGroup : @"com.apple.security.ckks",
843 (id)kSecAttrAccount : @"account-delete-me",
844 (id)kSecAttrSynchronizable : (id)kCFBooleanTrue,
845 (id)kSecAttrTombstone : @YES,
846 (id)kSecReturnAttributes : @YES,
847 (id)kSecMatchLimit : (id)kSecMatchLimitOne,};
848
849 CFTypeRef cfref = NULL;
850 OSStatus status = SecItemCopyMatching((__bridge CFDictionaryRef)tombquery, &cfref);
851 XCTAssertEqual(status, errSecSuccess, "Should have found a tombstone");
852
853 NSDictionary* tombstone = (NSDictionary*)CFBridgingRelease(cfref);
854 XCTAssertNotNil(tombstone, "Should have found a tombstone");
855
856 NSDate* tombstoneModificationDate = tombstone[(id)kSecAttrModificationDate];
857 XCTAssertEqual([tombstoneModificationDate compare:itemModificationDate], NSOrderedDescending, "tombstone should be later than item");
858
859 NSTimeInterval tombestoneDelta = [tombstoneModificationDate timeIntervalSinceDate:itemModificationDate];
860 XCTAssertGreaterThan(tombestoneDelta, 0, "Delta should be positive");
861 XCTAssertLessThan(tombestoneDelta, 5, "tombstone mdat should be no later than 5s after item mdat");
862
863 // And just as a sanity, mdat is already far ago, right?
864 NSTimeInterval itemDelta = [[NSDate date] timeIntervalSinceDate:itemModificationDate];
865 XCTAssertGreaterThan(itemDelta, 10, "item mdat should at least 10s in the past");
866 }
867
868 - (void)testReceiveTombstoneItem {
869 [self createAndSaveFakeKeyHierarchy: self.keychainZoneID]; // Make life easy for this test.
870 [self startCKKSSubsystem];
871
872 NSString* account = @"account-delete-me";
873
874 CKRecord* ckr = [self createFakeTombstoneRecord:self.keychainZoneID recordName:@"7B598D31-F9C5-481E-98AC-5A507ACB2D85" account:account];
875 [self.keychainZone addToZone:ckr];
876
877 // This device should delete the tombstone entry
878 [self expectCKDeleteItemRecords:1 zoneID:self.keychainZoneID];
879
880 [self.injectedManager.zoneChangeFetcher notifyZoneChange:nil];
881 [self.keychainView waitForFetchAndIncomingQueueProcessing];
882
883 // The tombstone shouldn't exist
884 NSDictionary *tombquery = @{
885 (id)kSecClass : (id)kSecClassGenericPassword,
886 (id)kSecAttrAccessGroup : @"com.apple.security.ckks",
887 (id)kSecAttrAccount : account,
888 (id)kSecAttrSynchronizable : (id)kCFBooleanTrue,
889 (id)kSecReturnAttributes : @YES,
890 (id)kSecAttrTombstone : @YES,
891 };
892
893 CFTypeRef cftype = NULL;
894 XCTAssertEqual(errSecItemNotFound, SecItemCopyMatching((__bridge CFDictionaryRef)tombquery, &cftype), "item should not exist now");
895 XCTAssertNil((__bridge id)cftype, "Should have found no tombstones");
896
897 // And the delete should occur
898 OCMVerifyAllWithDelay(self.mockDatabase, 20);
899
900 [self.keychainView dispatchSyncWithReadOnlySQLTransaction:^{
901 NSError* error = nil;
902 NSArray<NSString*>* uuids = [CKKSIncomingQueueEntry allUUIDs:self.keychainZoneID
903 error:&error];
904 XCTAssertNil(error, "should be no error fetching uuids");
905 XCTAssertEqual(uuids.count, 0u, "There should be zero IQEs");
906 }];
907 }
908
909 - (void)testReceiveItemDeleteAndReaddAtDifferentUUIDInSameFetch {
910 [self createAndSaveFakeKeyHierarchy: self.keychainZoneID];
911 [self startCKKSSubsystem];
912
913 NSString* itemAccount = @"account-delete-me";
914 [self findGenericPassword:itemAccount expecting:errSecItemNotFound];
915
916 NSString* uuidOriginalItem = @"7B598D31-F9C5-481E-98AC-5A507ACB2D85";
917 NSString* uuidGreater = @"7B598D31-FFFF-FFFF-98AC-5A507ACB2D85";
918
919 CKRecord* ckr = [self createFakeRecord:self.keychainZoneID recordName:uuidOriginalItem];
920 CKRecord* ckrGreater = [self createFakeRecord:self.keychainZoneID recordName:uuidGreater];
921
922 [self.keychainZone addToZone:ckr];
923
924 [self.injectedManager.zoneChangeFetcher notifyZoneChange:nil];
925 [self.keychainView waitForFetchAndIncomingQueueProcessing];
926
927 [self findGenericPassword:itemAccount expecting:errSecSuccess];
928 [self checkGenericPasswordStoredUUID:uuidOriginalItem account:itemAccount];
929
930 // Now, the item is deleted and re-added with a greater UUID
931 [self.keychainZone deleteCKRecordIDFromZone:[ckr recordID]];
932 [self.keychainZone addToZone:ckrGreater];
933
934 // This node should not upload anything.
935 [[self.mockDatabase reject] addOperation:[OCMArg any]];
936
937 [self.injectedManager.zoneChangeFetcher notifyZoneChange:nil];
938 [self.keychainView waitForFetchAndIncomingQueueProcessing];
939
940 [self.keychainView waitForOperationsOfClass:[CKKSOutgoingQueueOperation class]];
941 [self.keychainView waitForOperationsOfClass:[CKKSIncomingQueueOperation class]];
942
943 // Item should still exist.
944 [self findGenericPassword:itemAccount expecting:errSecSuccess];
945 [self checkGenericPasswordStoredUUID:uuidGreater account:itemAccount];
946 }
947
948 -(void)testReceiveItemPhantomDelete {
949 [self createAndSaveFakeKeyHierarchy: self.keychainZoneID]; // Make life easy for this test.
950 [self startCKKSSubsystem];
951
952 NSDictionary *query = @{(id)kSecClass : (id)kSecClassGenericPassword,
953 (id)kSecAttrAccessGroup : @"com.apple.security.ckks",
954 (id)kSecAttrAccount : @"account-delete-me",
955 (id)kSecAttrSynchronizable : (id)kCFBooleanTrue,
956 (id)kSecMatchLimit : (id)kSecMatchLimitOne,
957 };
958
959 CFTypeRef item = NULL;
960 XCTAssertEqual(errSecItemNotFound, SecItemCopyMatching((__bridge CFDictionaryRef) query, &item), "item should not yet exist");
961
962 [self.injectedManager.zoneChangeFetcher notifyZoneChange:nil];
963 [self.keychainView waitForFetchAndIncomingQueueProcessing];
964
965 CKRecord* ckr = [self createFakeRecord: self.keychainZoneID recordName: @"7B598D31-F9C5-481E-98AC-5A507ACB2D85"];
966 [self.keychainZone addToZone: ckr];
967
968 [self.injectedManager.zoneChangeFetcher notifyZoneChange:nil];
969 [self.keychainView waitForFetchAndIncomingQueueProcessing];
970
971 XCTAssertEqual(errSecSuccess, SecItemCopyMatching((__bridge CFDictionaryRef) query, &item), "item should exist now");
972 CFReleaseNull(item);
973
974 [self.keychainView waitUntilAllOperationsAreFinished];
975
976 // Trigger a delete
977 [self.keychainZone deleteCKRecordIDFromZone: [ckr recordID]];
978
979 // and add another, incorrect IQE
980 [self.keychainView dispatchSyncWithSQLTransaction:^CKKSDatabaseTransactionResult{
981 // Inefficient, but hey, it works
982 CKRecord* record = [self createFakeRecord: self.keychainZoneID recordName:@"7B598D31-F9C5-FFFF-FFFF-5A507ACB2D85"];
983 CKKSItem* fakeItem = [[CKKSItem alloc] initWithCKRecord: record];
984
985 CKKSIncomingQueueEntry* iqe = [[CKKSIncomingQueueEntry alloc] initWithCKKSItem:fakeItem
986 action:SecCKKSActionDelete
987 state:SecCKKSStateNew];
988 XCTAssertNotNil(iqe, "could create fake IQE");
989 NSError* error = nil;
990 XCTAssert([iqe saveToDatabase: &error], "Saved fake IQE to database");
991 XCTAssertNil(error, "No error saving fake IQE to database");
992 return CKKSDatabaseTransactionCommit;
993 }];
994
995 [self.injectedManager.zoneChangeFetcher notifyZoneChange:nil];
996 [self.keychainView waitForFetchAndIncomingQueueProcessing];
997
998 XCTAssertEqual(errSecItemNotFound, SecItemCopyMatching((__bridge CFDictionaryRef) query, &item), "item should no longer exist");
999
1000 // The incoming queue should be empty
1001 [self.keychainView dispatchSyncWithReadOnlySQLTransaction:^{
1002 NSError* error = nil;
1003 NSArray* iqes = [CKKSIncomingQueueEntry all:&error];
1004 XCTAssertNil(error, "No error loading IQEs");
1005 XCTAssertNotNil(iqes, "Could load IQEs");
1006 XCTAssertEqual(iqes.count, 0u, "Incoming queue is empty");
1007 }];
1008 }
1009
1010 -(void)testReceiveConflictOnJustAddedItem {
1011 [self createAndSaveFakeKeyHierarchy: self.keychainZoneID]; // Make life easy for this test.
1012 [self startCKKSSubsystem];
1013
1014 [self.keychainView waitForKeyHierarchyReadiness];
1015 [self.keychainView waitUntilAllOperationsAreFinished];
1016
1017 // Place a hold on processing the outgoing queue.
1018 self.keychainView.holdOutgoingQueueOperation = [CKKSResultOperation named:@"outgoing-queue-hold"
1019 withBlock:^{
1020 ckksnotice_global("ckks", "Outgoing queue hold released.");
1021 }];
1022
1023 self.keychainView.holdIncomingQueueOperation = [CKKSResultOperation named:@"incoming-queue-hold"
1024 withBlock:^{
1025 ckksnotice_global("ckks", "Incoming queue hold released.");
1026 }];
1027
1028 [self addGenericPassword:@"localchange" account:@"account-delete-me"];
1029
1030 // Pull out the new item's UUID.
1031 __block NSString* itemUUID = nil;
1032 [self.keychainView dispatchSyncWithReadOnlySQLTransaction:^{
1033 NSError* error = nil;
1034 NSArray<NSString*>* uuids = [CKKSOutgoingQueueEntry allUUIDs:self.keychainZoneID ?: [[CKRecordZoneID alloc] initWithZoneName:@"keychain"
1035 ownerName:CKCurrentUserDefaultName]
1036 error:&error];
1037 XCTAssertNil(error, "no error fetching uuids");
1038 XCTAssertEqual(uuids.count, 1u, "There's exactly one outgoing queue entry");
1039 itemUUID = uuids[0];
1040
1041 XCTAssertNotNil(itemUUID, "Have a UUID for our new item");
1042 }];
1043
1044 [self.keychainZone addToZone: [self createFakeRecord: self.keychainZoneID recordName: itemUUID]];
1045
1046 [self.injectedManager.zoneChangeFetcher notifyZoneChange:nil];
1047 [[self.keychainView.zoneChangeFetcher requestSuccessfulFetch:CKKSFetchBecauseTesting] waitUntilFinished];
1048
1049 // Allow the outgoing queue operation to proceed
1050 [self.operationQueue addOperation:self.keychainView.holdOutgoingQueueOperation];
1051 [self.keychainView waitForOperationsOfClass:[CKKSOutgoingQueueOperation class]];
1052
1053 // Allow the incoming queue operation to proceed
1054 [self.operationQueue addOperation:self.keychainView.holdIncomingQueueOperation];
1055 [self.keychainView waitForOperationsOfClass:[CKKSIncomingQueueOperation class]];
1056
1057 [self checkGenericPassword:@"data" account:@"account-delete-me"];
1058
1059 [self.keychainView waitUntilAllOperationsAreFinished];
1060 }
1061
1062 - (void)testReceiveCloudKitConflictOnJustAddedItems {
1063 [self createAndSaveFakeKeyHierarchy: self.keychainZoneID]; // Make life easy for this test.
1064 [self startCKKSSubsystem];
1065
1066 [self.keychainView waitForKeyHierarchyReadiness];
1067 [self.keychainView waitUntilAllOperationsAreFinished];
1068
1069 // Place a hold on processing the outgoing queue.
1070 [self.keychainView waitForOperationsOfClass:[CKKSOutgoingQueueOperation class]];
1071 self.keychainView.holdOutgoingQueueOperation = [CKKSResultOperation named:@"outgoing-queue-hold" withBlock:^{
1072 ckksnotice_global("ckks", "Outgoing queue hold released.");
1073 }];
1074
1075 [self addGenericPassword:@"localchange" account:@"account-delete-me"];
1076
1077 // Pull out the new item's UUID.
1078 __block NSString* itemUUID = nil;
1079 [self.keychainView dispatchSyncWithReadOnlySQLTransaction:^{
1080 NSError* error = nil;
1081 NSArray<NSString*>* uuids = [CKKSOutgoingQueueEntry allUUIDs:self.keychainZoneID ?: [[CKRecordZoneID alloc] initWithZoneName:@"keychain"
1082 ownerName:CKCurrentUserDefaultName]
1083 error:&error];
1084 XCTAssertNil(error, "no error fetching uuids");
1085 XCTAssertEqual(uuids.count, 1u, "There's exactly one outgoing queue entry");
1086 itemUUID = uuids[0];
1087
1088 XCTAssertNotNil(itemUUID, "Have a UUID for our new item");
1089 }];
1090
1091 // Add a second item: this item should be uploaded after the failure of the first item
1092 [self addGenericPassword:@"localchange" account:@"account-delete-me-2"];
1093
1094 [self.keychainZone addToZone: [self createFakeRecord: self.keychainZoneID recordName: itemUUID]];
1095
1096 // Also, this write will increment the class C current pointer's etag
1097 CKRecordID* currentClassCID = [[CKRecordID alloc] initWithRecordName: @"classC" zoneID: self.keychainZoneID];
1098 CKRecord* currentClassC = self.keychainZone.currentDatabase[currentClassCID];
1099 XCTAssertNotNil(currentClassC, "Should have the class C current key pointer record");
1100 [self.keychainZone addCKRecordToZone:[currentClassC copy]];
1101 XCTAssertNotEqualObjects(currentClassC.etag, self.keychainZone.currentDatabase[currentClassCID].etag, "Etag should have changed");
1102
1103 [self expectCKAtomicModifyItemRecordsUpdateFailure: self.keychainZoneID];
1104 [self expectCKModifyItemRecords:1 currentKeyPointerRecords:1 zoneID:self.keychainZoneID
1105 checkItem: [self checkClassCBlock:self.keychainZoneID message:@"Object was encrypted under class C key in hierarchy"]];
1106
1107 // Allow the outgoing queue operation to proceed
1108 [self.operationQueue addOperation:self.keychainView.holdOutgoingQueueOperation];
1109
1110 OCMVerifyAllWithDelay(self.mockDatabase, 20);
1111 [self.keychainView waitUntilAllOperationsAreFinished];
1112
1113 [self checkGenericPassword:@"data" account:@"account-delete-me"];
1114 [self checkGenericPassword:@"localchange" account:@"account-delete-me-2"];
1115 }
1116
1117
1118 -(void)testReceiveUnknownField {
1119 [self createAndSaveFakeKeyHierarchy: self.keychainZoneID]; // Make life easy for this test.
1120
1121 [self startCKKSSubsystem];
1122 [self.keychainView waitForKeyHierarchyReadiness];
1123
1124 NSError* error = nil;
1125
1126 // Manually encrypt an item
1127 NSString* recordName = @"7B598D31-F9C5-481E-98AC-5A507ACB2D85";
1128 CKRecordID* recordID = [[CKRecordID alloc] initWithRecordName:recordName zoneID:self.keychainZoneID];
1129 NSDictionary* item = [self fakeRecordDictionary: @"account-delete-me" zoneID:self.keychainZoneID];
1130 CKKSItem* cipheritem = [[CKKSItem alloc] initWithUUID:recordID.recordName
1131 parentKeyUUID:self.keychainZoneKeys.classA.uuid
1132 zoneID:recordID.zoneID];
1133 CKKSKey* itemkey = [CKKSKey randomKeyWrappedByParent: self.keychainZoneKeys.classA error:&error];
1134 XCTAssertNotNil(itemkey, "Got a key");
1135 cipheritem.wrappedkey = itemkey.wrappedkey;
1136 XCTAssertNotNil(cipheritem.wrappedkey, "Got a wrapped key");
1137
1138 NSData* future_data_field = [@"asdf" dataUsingEncoding:NSUTF8StringEncoding];
1139 NSString* future_string_field = @"authstring";
1140 NSString* future_server_field = @"server_can_change_at_any_time";
1141 NSNumber* future_number_field = [NSNumber numberWithInt:30];
1142
1143 // Use version 2, so future fields will be authenticated
1144 cipheritem.encver = CKKSItemEncryptionVersion2;
1145 NSMutableDictionary<NSString*, NSData*>* authenticatedData = [[cipheritem makeAuthenticatedDataDictionaryUpdatingCKKSItem: nil encryptionVersion:CKKSItemEncryptionVersion2] mutableCopy];
1146
1147 authenticatedData[@"future_data_field"] = future_data_field;
1148 authenticatedData[@"future_string_field"] = [future_string_field dataUsingEncoding:NSUTF8StringEncoding];
1149
1150 uint64_t n = OSSwapHostToLittleConstInt64([future_number_field unsignedLongValue]);
1151 authenticatedData[@"future_number_field"] = [NSData dataWithBytes:&n length:sizeof(n)];
1152
1153
1154 cipheritem.encitem = [CKKSItemEncrypter encryptDictionary:item key:itemkey.aessivkey authenticatedData:authenticatedData error:&error];
1155 XCTAssertNil(error, "no error encrypting object");
1156 XCTAssertNotNil(cipheritem.encitem, "Recieved ciphertext");
1157
1158 CKRecord* ckr = [cipheritem CKRecordWithZoneID: recordID.zoneID];
1159 ckr[@"future_data_field"] = future_data_field;
1160 ckr[@"future_string_field"] = future_string_field;
1161 ckr[@"future_number_field"] = future_number_field;
1162 ckr[@"server_new_server_field"] = future_server_field;
1163 [self.keychainZone addToZone:ckr];
1164
1165 [self.injectedManager.zoneChangeFetcher notifyZoneChange:nil];
1166 [self.keychainView waitForFetchAndIncomingQueueProcessing];
1167
1168 NSDictionary* query = @{(id)kSecClass: (id)kSecClassGenericPassword,
1169 (id)kSecReturnAttributes: @YES,
1170 (id)kSecAttrSynchronizable: @YES,
1171 (id)kSecAttrAccount: @"account-delete-me",
1172 (id)kSecMatchLimit: (id)kSecMatchLimitOne,
1173 };
1174 CFTypeRef cfresult = NULL;
1175 XCTAssertEqual(errSecSuccess, SecItemCopyMatching((__bridge CFDictionaryRef) query, &cfresult), "Found synced item");
1176
1177 // Test that if this item is updated, it remains encrypted in v2, and future_field still exists
1178 [self expectCKModifyItemRecords: 1 currentKeyPointerRecords: 1 zoneID:self.keychainZoneID];
1179 [self updateGenericPassword:@"different password" account:@"account-delete-me"];
1180
1181 OCMVerifyAllWithDelay(self.mockDatabase, 20);
1182 [self waitForCKModifications];
1183
1184 CKRecord* newRecord = self.keychainZone.currentDatabase[recordID];
1185 XCTAssertEqualObjects(newRecord[@"future_data_field"], future_data_field, "future_data_field still exists");
1186 XCTAssertEqualObjects(newRecord[@"future_string_field"], future_string_field, "future_string_field still exists");
1187 XCTAssertEqualObjects(newRecord[@"future_number_field"], future_number_field, "future_string_field still exists");
1188 XCTAssertEqualObjects(newRecord[@"server_new_server_field"], future_server_field, "future_server_field stille exists");
1189
1190 CKKSItem* newItem = [[CKKSItem alloc] initWithCKRecord:newRecord];
1191 CKKSAESSIVKey* newItemKey = [self.keychainZoneKeys.classA unwrapAESKey:newItem.wrappedkey error:&error];
1192 XCTAssertNil(error, "No error unwrapping AES key");
1193 XCTAssertNotNil(newItemKey, "Have an unwrapped AES key for this item");
1194
1195 NSDictionary* uploadedData = [CKKSItemEncrypter decryptDictionary:newRecord[SecCKRecordDataKey]
1196 key:newItemKey
1197 authenticatedData:authenticatedData
1198 error:&error];
1199 XCTAssertNil(error, "No error decrypting dictionary");
1200 XCTAssertNotNil(uploadedData, "Authenticated re-uploaded data including future_field");
1201 XCTAssertEqualObjects(uploadedData[@"v_Data"], [@"different password" dataUsingEncoding:NSUTF8StringEncoding], "Passwords match");
1202 }
1203
1204
1205 -(void)testReceiveRecordEncryptedv1 {
1206 [self createAndSaveFakeKeyHierarchy: self.keychainZoneID]; // Make life easy for this test.
1207
1208 [self startCKKSSubsystem];
1209 [self.keychainView waitForKeyHierarchyReadiness];
1210
1211 NSError* error = nil;
1212
1213 // Manually encrypt an item
1214 NSString* recordName = @"7B598D31-F9C5-481E-98AC-5A507ACB2D85";
1215 CKRecordID* recordID = [[CKRecordID alloc] initWithRecordName:recordName zoneID:self.keychainZoneID];
1216 NSDictionary* item = [self fakeRecordDictionary: @"account-delete-me" zoneID:self.keychainZoneID];
1217 CKKSItem* cipheritem = [[CKKSItem alloc] initWithUUID:recordID.recordName
1218 parentKeyUUID:self.keychainZoneKeys.classC.uuid
1219 zoneID:recordID.zoneID];
1220 CKKSKey* itemkey = [CKKSKey randomKeyWrappedByParent: self.keychainZoneKeys.classC error:&error];
1221 XCTAssertNotNil(itemkey, "Got a key");
1222 cipheritem.wrappedkey = itemkey.wrappedkey;
1223 XCTAssertNotNil(cipheritem.wrappedkey, "Got a wrapped key");
1224
1225 cipheritem.encver = CKKSItemEncryptionVersion1;
1226
1227 NSMutableDictionary<NSString*, NSData*>* authenticatedData = [[cipheritem makeAuthenticatedDataDictionaryUpdatingCKKSItem: nil encryptionVersion:cipheritem.encver] mutableCopy];
1228
1229 cipheritem.encitem = [CKKSItemEncrypter encryptDictionary:item key:itemkey.aessivkey authenticatedData:authenticatedData error:&error];
1230 XCTAssertNil(error, "no error encrypting object");
1231 XCTAssertNotNil(cipheritem.encitem, "Recieved ciphertext");
1232
1233 [self.keychainZone addToZone:[cipheritem CKRecordWithZoneID: recordID.zoneID]];
1234
1235 [self.injectedManager.zoneChangeFetcher notifyZoneChange:nil];
1236 [self.keychainView waitForFetchAndIncomingQueueProcessing];
1237
1238 NSDictionary* query = @{(id)kSecClass: (id)kSecClassGenericPassword,
1239 (id)kSecReturnAttributes: @YES,
1240 (id)kSecAttrSynchronizable: @YES,
1241 (id)kSecAttrAccount: @"account-delete-me",
1242 (id)kSecMatchLimit: (id)kSecMatchLimitOne,
1243 };
1244 CFTypeRef cfresult = NULL;
1245 XCTAssertEqual(errSecSuccess, SecItemCopyMatching((__bridge CFDictionaryRef) query, &cfresult), "Found synced item");
1246 CFReleaseNull(cfresult);
1247
1248 // Test that if this item is updated, it is encrypted in v2
1249 [self expectCKModifyItemRecords: 1 currentKeyPointerRecords: 1 zoneID:self.keychainZoneID];
1250 [self updateGenericPassword:@"different password" account:@"account-delete-me"];
1251
1252 OCMVerifyAllWithDelay(self.mockDatabase, 20);
1253 [self waitForCKModifications];
1254
1255 CKRecord* newRecord = self.keychainZone.currentDatabase[recordID];
1256 XCTAssertEqualObjects(newRecord[SecCKRecordEncryptionVersionKey], [NSNumber numberWithInteger:(int) CKKSItemEncryptionVersion2], "Uploaded using encv2");
1257 }
1258
1259 - (void)testLocalUpdateToTombstoneItem {
1260 // Some CKKS clients may accidentally upload entries with tomb=1.
1261 // We should delete these items with extreme predjudice.
1262 [self createAndSaveFakeKeyHierarchy: self.keychainZoneID];
1263
1264 [self startCKKSSubsystem];
1265
1266 // We expect a single record to be uploaded.
1267 [self expectCKModifyItemRecords:1 currentKeyPointerRecords:1 zoneID:self.keychainZoneID];
1268 [self addGenericPassword: @"data" account: @"account-delete-me"];
1269 OCMVerifyAllWithDelay(self.mockDatabase, 20);
1270
1271 // We expect a single record to be deleted.
1272 [self expectCKDeleteItemRecords: 1 zoneID:self.keychainZoneID];
1273 [self deleteGenericPassword:@"account-delete-me"];
1274 OCMVerifyAllWithDelay(self.mockDatabase, 20);
1275
1276 // Now, SOS comes along and updates the tombstone
1277 // CKKS should _not_ try to upload a tombstone
1278 NSDictionary *tombquery = @{
1279 (id)kSecClass : (id)kSecClassGenericPassword,
1280 (id)kSecAttrAccessGroup : @"com.apple.security.ckks",
1281 (id)kSecAttrAccount : @"account-delete-me",
1282 (id)kSecAttrSynchronizable : (id)kCFBooleanTrue,
1283 (id)kSecAttrTombstone : @YES,
1284 };
1285
1286 NSDictionary* update = @{
1287 (id)kSecAttrModificationDate : [NSDate date],
1288 };
1289
1290 __block CFErrorRef cferror = NULL;
1291 kc_with_dbt(true, &cferror, ^bool (SecDbConnectionRef dbt) {
1292 bool ok = kc_transaction_type(dbt, kSecDbExclusiveRemoteSOSTransactionType, &cferror, ^bool {
1293 OSStatus status = SecItemUpdate((__bridge CFDictionaryRef)tombquery, (__bridge CFDictionaryRef)update);
1294 XCTAssertEqual(status, errSecSuccess, "Should have been able to update a tombstone");
1295
1296 return true;
1297 });
1298 return ok;
1299 });
1300
1301 XCTAssertNil((__bridge NSError*)cferror, "Should be no error updating a tombstone");
1302
1303 [self.keychainView waitForOperationsOfClass:[CKKSOutgoingQueueOperation class]];
1304 }
1305
1306 - (void)testIgnoreUpdateToModificationDateItem {
1307 [self createAndSaveFakeKeyHierarchy:self.keychainZoneID];
1308 [self startCKKSSubsystem];
1309
1310 // We expect a single record to be uploaded.
1311 [self expectCKModifyItemRecords:1 currentKeyPointerRecords:1 zoneID:self.keychainZoneID];
1312 [self addGenericPassword:@"data" account: @"account-delete-me"];
1313 OCMVerifyAllWithDelay(self.mockDatabase, 20);
1314
1315 // Nothing more should be uploaded
1316 NSDictionary *query = @{
1317 (id)kSecClass : (id)kSecClassGenericPassword,
1318 (id)kSecAttrAccessGroup : @"com.apple.security.ckks",
1319 (id)kSecAttrAccount : @"account-delete-me",
1320 (id)kSecAttrSynchronizable : (id)kCFBooleanTrue,
1321 };
1322
1323 NSDictionary* update = @{
1324 (id)kSecAttrModificationDate : [NSDate date],
1325 };
1326
1327 __block CFErrorRef cferror = NULL;
1328 kc_with_dbt(true, &cferror, ^bool (SecDbConnectionRef dbt) {
1329 bool ok = kc_transaction_type(dbt, kSecDbExclusiveRemoteSOSTransactionType, &cferror, ^bool {
1330 OSStatus status = SecItemUpdate((__bridge CFDictionaryRef)query, (__bridge CFDictionaryRef)update);
1331 XCTAssertEqual(status, errSecSuccess, "Should have been able to update the item");
1332
1333 return true;
1334 });
1335 return ok;
1336 });
1337
1338 XCTAssertNil((__bridge NSError*)cferror, "Should be no error updating just the mdat");
1339
1340 [self.keychainView waitForOperationsOfClass:[CKKSOutgoingQueueOperation class]];
1341 }
1342
1343 - (void)testUploadPagination {
1344 [self putFakeKeyHierarchyInCloudKit:self.keychainZoneID];
1345 [self putFakeDeviceStatusInCloudKit:self.keychainZoneID];
1346 [self saveTLKMaterialToKeychain:self.keychainZoneID];
1347 [self expectCKKSTLKSelfShareUpload:self.keychainZoneID];
1348
1349 for(size_t count = 0; count < 250; count++) {
1350 [self addGenericPassword: @"data" account: [NSString stringWithFormat:@"account-delete-me-%03lu", count]];
1351 }
1352
1353 [self expectCKModifyItemRecords: SecCKKSOutgoingQueueItemsAtOnce currentKeyPointerRecords: 1 zoneID:self.keychainZoneID];
1354 [self expectCKModifyItemRecords: SecCKKSOutgoingQueueItemsAtOnce currentKeyPointerRecords: 1 zoneID:self.keychainZoneID];
1355 [self expectCKModifyItemRecords: 50 currentKeyPointerRecords: 1 zoneID:self.keychainZoneID];
1356
1357 [self startCKKSSubsystem];
1358
1359 // For the next 5 seconds, try to add and then find an item. Each attempt should be fairly quick: no long multisecond pauses while CKKS Scans
1360 NSTimeInterval elapsed = 0;
1361 uint64_t count = 0;
1362 while(elapsed < 10) {
1363 NSDate* begin = [NSDate now];
1364
1365 NSString* account = [NSString stringWithFormat:@"non-syncable-%d", (int)count];
1366
1367 NSDictionary* query = @{
1368 (id)kSecClass : (id)kSecClassGenericPassword,
1369 (id)kSecAttrAccount : account,
1370 (id)kSecAttrSynchronizable : @NO,
1371 (id)kSecAttrAccessGroup : @"com.apple.security.ckks",
1372 (id)kSecValueData : [@"asdf" dataUsingEncoding:NSUTF8StringEncoding],
1373 };
1374
1375 XCTAssertEqual(SecItemAdd((__bridge CFDictionaryRef)query, NULL), errSecSuccess, @"Should be able to add nonsyncable item");
1376 ckksnotice("ckkstest", self.keychainView, "SecItemAdd of %@ successful", account);
1377
1378 NSDictionary *findQuery = @{
1379 (id)kSecClass : (id)kSecClassGenericPassword,
1380 (id)kSecAttrAccount : account,
1381 (id)kSecAttrAccessGroup : @"com.apple.security.ckks",
1382 (id)kSecMatchLimit : (id)kSecMatchLimitOne,
1383 };
1384 XCTAssertEqual(SecItemCopyMatching((__bridge CFDictionaryRef)findQuery, NULL), errSecSuccess, "Finding item %@", account);
1385 ckksnotice("ckkstest", self.keychainView, "SecItemCopyMatching of %@ successful", account);
1386
1387 NSDate* end = [NSDate now];
1388 NSTimeInterval delta = [end timeIntervalSinceDate:begin];
1389
1390 XCTAssertLessThan(delta, 2, @"Keychain API should respond in two seconds");
1391 ckksnotice("ckkstest", self.keychainView, "SecItemAdd/SecItemCopyMatching pair of %@ took %.4fs", account, delta);
1392
1393 usleep(10000); // sleep for 10ms, to let some other things get done
1394
1395 // And retake the time elasped for the overall count
1396 elapsed += [[NSDate now] timeIntervalSinceDate:begin];
1397 count += 1;
1398 }
1399
1400 OCMVerifyAllWithDelay(self.mockDatabase, 40);
1401 }
1402
1403 - (void)testUploadInitialKeyHierarchy {
1404 // Test starts with nothing in database. CKKS should get into the "please upload my keys" state, then Octagon should perform the upload
1405
1406 // Spin up CKKS subsystem.
1407 [self startCKKSSubsystem];
1408
1409 [self performOctagonTLKUpload:self.ckksViews];
1410 OCMVerifyAllWithDelay(self.mockDatabase, 20);
1411
1412 XCTAssertEqual(0, [self.keychainView.keyHierarchyConditions[SecCKKSZoneKeyStateReady] wait:20*NSEC_PER_SEC], @"key state should enter 'ready'");
1413 }
1414
1415 - (void)testDoNotErrorIfNudgedWhileWaitingForTLKUpload {
1416 // Test starts with nothing in database. CKKS should get into the "please upload my keys" state, then Octagon should perform the upload
1417
1418 // Spin up CKKS subsystem.
1419 [self startCKKSSubsystem];
1420
1421 NSMutableArray<CKKSResultOperation<CKKSKeySetProviderOperationProtocol>*>* keysetOps = [NSMutableArray array];
1422
1423 for(CKKSKeychainView* view in self.ckksViews) {
1424 XCTAssertEqual(0, [view.keyHierarchyConditions[SecCKKSZoneKeyStateWaitForTLKCreation] wait:40*NSEC_PER_SEC], @"key state should enter 'waitfortlkcreation' (view %@)", view);
1425 [keysetOps addObject: [view findKeySet:NO]];
1426 }
1427
1428 // Now that we've kicked them all off, wait for them to resolve (and nudge each one, as if a key was saved)
1429 for(CKKSKeychainView* view in self.ckksViews) {
1430 XCTAssertEqual(0, [view.keyHierarchyConditions[SecCKKSZoneKeyStateWaitForTLKUpload] wait:40*NSEC_PER_SEC], @"key state should enter 'waitfortlkupload'");
1431
1432 CKKSCondition* viewProcess = view.keyHierarchyConditions[SecCKKSZoneKeyStateProcess];
1433 [view keyStateMachineRequestProcess];
1434
1435 // Since we do need to leave SecCKKSZoneKeyStateWaitForTLKUpload if a fetch occurs with new keys, make sure we do the right thing
1436 XCTAssertEqual(0, [viewProcess wait:10*NSEC_PER_MSEC], "CKKS should reprocess the key hierarchy when nudged");
1437 XCTAssertEqual(0, [view.keyHierarchyConditions[SecCKKSZoneKeyStateWaitForTLKUpload] wait:40*NSEC_PER_SEC], @"key state should re-enter 'waitfortlkupload'");
1438 }
1439
1440 // The views should remain in waitfortlkcreation, and not go through process into an error
1441
1442 NSMutableArray<CKRecord*>* keyHierarchyRecords = [NSMutableArray array];
1443
1444 for(CKKSResultOperation<CKKSKeySetProviderOperationProtocol>* keysetOp in keysetOps) {
1445 // Wait until finished is usually a bad idea. We could rip this out into an operation if we'd like.
1446 [keysetOp waitUntilFinished];
1447 XCTAssertNil(keysetOp.error, "Should be no error fetching keyset from CKKS");
1448
1449 NSArray<CKRecord*>* records = [self putKeySetInCloudKit:keysetOp.keyset];
1450 [keyHierarchyRecords addObjectsFromArray:records];
1451 }
1452
1453 // Tell our views about our shiny new records!
1454 for(CKKSKeychainView* view in self.ckksViews) {
1455 [view receiveTLKUploadRecords: keyHierarchyRecords];
1456 }
1457 OCMVerifyAllWithDelay(self.mockDatabase, 20);
1458
1459 XCTAssertEqual(0, [self.keychainView.keyHierarchyConditions[SecCKKSZoneKeyStateReady] wait:20*NSEC_PER_SEC], @"key state should enter 'ready'");
1460 }
1461
1462
1463 - (void)testReceiveChangedKeySetFromWaitingForTLKUpload {
1464 // Test starts with nothing in database. CKKS should get into the "please upload my keys" state
1465
1466 [self startCKKSSubsystem];
1467
1468 // After each zone arrives in WaitForTLKCreation, new keys are uploaded
1469 for(CKKSKeychainView* view in self.ckksViews) {
1470 XCTAssertEqual(0, [view.keyHierarchyConditions[SecCKKSZoneKeyStateWaitForTLKCreation] wait:40*NSEC_PER_SEC], @"key state should enter 'waitfortlkcreation' (view %@)", view);
1471 }
1472
1473 for(CKKSKeychainView* view in self.ckksViews) {
1474 [self putFakeKeyHierarchyInCloudKit:view.zoneID];
1475 [self putFakeDeviceStatusInCloudKit:view.zoneID];
1476 }
1477
1478 // If we ask the zones for their keysets, they should return the local set ready for upload
1479 NSMutableArray<CKKSResultOperation<CKKSKeySetProviderOperationProtocol>*>* keysetOps = [NSMutableArray array];
1480
1481 for(CKKSKeychainView* view in self.ckksViews) {
1482 [keysetOps addObject:[view findKeySet:NO]];
1483 }
1484
1485 for(CKKSResultOperation<CKKSKeySetProviderOperationProtocol>* keysetOp in keysetOps) {
1486 [keysetOp waitUntilFinished];
1487 XCTAssertNil(keysetOp.error, "Should be no error fetching keyset from CKKS");
1488
1489 CKRecordZoneID* zoneID = [[CKRecordZoneID alloc] initWithZoneName:keysetOp.zoneName
1490 ownerName:CKCurrentUserDefaultName];
1491 ZoneKeys* zk = self.keys[zoneID];
1492 XCTAssertNotNil(zk, "Should have new zone keys for zone %@", keysetOp.zoneName);
1493 XCTAssertNotEqualObjects(keysetOp.keyset.currentTLKPointer.currentKeyUUID, zk.tlk.uuid, "Fetched TLK and CK TLK should be different");
1494 }
1495
1496 // Now, find the keysets again, asking for a fetch this time
1497 NSMutableArray<CKKSResultOperation<CKKSKeySetProviderOperationProtocol>*>* fetchedKeysetOps = [NSMutableArray array];
1498
1499 for(CKKSKeychainView* view in self.ckksViews) {
1500 [fetchedKeysetOps addObject:[view findKeySet:YES]];
1501 }
1502
1503 for(CKKSResultOperation<CKKSKeySetProviderOperationProtocol>* keysetOp in fetchedKeysetOps) {
1504 [keysetOp waitUntilFinished];
1505 XCTAssertNil(keysetOp.error, "Should be no error fetching keyset from CKKS");
1506
1507 CKRecordZoneID* zoneID = [[CKRecordZoneID alloc] initWithZoneName:keysetOp.zoneName
1508 ownerName:CKCurrentUserDefaultName];
1509 ZoneKeys* zk = self.keys[zoneID];
1510 XCTAssertNotNil(zk, "Should have new zone keys for zone %@", keysetOp.zoneName);
1511 XCTAssertEqualObjects(keysetOp.keyset.currentTLKPointer.currentKeyUUID, zk.tlk.uuid, "Fetched TLK and CK TLK should now match");
1512 }
1513 }
1514
1515 - (void)testProvideKeysetFromNoTrust {
1516 [self putFakeKeyHierarchyInCloudKit:self.keychainZoneID];
1517
1518 self.mockSOSAdapter.circleStatus = kSOSCCNotInCircle;
1519 [self startCKKSSubsystem];
1520
1521 XCTAssertEqual(0, [self.keychainView.keyHierarchyConditions[SecCKKSZoneKeyStateWaitForTrust] wait:20*NSEC_PER_SEC], @"Key state should become 'waitfortrust'");
1522
1523 CKKSResultOperation<CKKSKeySetProviderOperationProtocol>* keysetOp = [self.keychainView findKeySet:NO];
1524 [keysetOp timeout:20*NSEC_PER_SEC];
1525 [keysetOp waitUntilFinished];
1526
1527 XCTAssertNil(keysetOp.error, "Should be no error fetching a keyset");
1528 }
1529
1530 - (void)testProvideKeysetFromNoTrustWithRefetch {
1531 [self putFakeKeyHierarchyInCloudKit:self.keychainZoneID];
1532
1533 self.mockSOSAdapter.circleStatus = kSOSCCNotInCircle;
1534 [self startCKKSSubsystem];
1535
1536 XCTAssertEqual(0, [self.keychainView.keyHierarchyConditions[SecCKKSZoneKeyStateWaitForTrust] wait:20*NSEC_PER_SEC], @"Key state should become 'waitfortrust'");
1537
1538 self.silentFetchesAllowed = false;
1539 [self expectCKFetch];
1540
1541 CKKSResultOperation<CKKSKeySetProviderOperationProtocol>* keysetOp = [self.keychainView findKeySet:YES];
1542 [keysetOp timeout:20*NSEC_PER_SEC];
1543 [keysetOp waitUntilFinished];
1544
1545 XCTAssertNil(keysetOp.error, "Should be no error fetching a keyset");
1546
1547 OCMVerifyAllWithDelay(self.mockDatabase, 20);
1548 }
1549
1550 - (void)testProvideKeysetAfterReceivingTLKInNoTrust {
1551 [self putFakeKeyHierarchyInCloudKit:self.keychainZoneID];
1552
1553 self.mockSOSAdapter.circleStatus = kSOSCCNotInCircle;
1554 [self startCKKSSubsystem];
1555
1556 XCTAssertEqual(0, [self.keychainView.keyHierarchyConditions[SecCKKSZoneKeyStateWaitForTrust] wait:20*NSEC_PER_SEC], @"Key state should become 'waitfortrust'");
1557
1558 // This isn't necessarily SOS, but perhaps SBD.
1559 [self saveTLKMaterialToKeychainSimulatingSOS:self.keychainZoneID];
1560
1561 // Still ends up in waitfortrust...
1562 XCTAssertEqual(0, [self.keychainView.keyHierarchyConditions[SecCKKSZoneKeyStateWaitForTrust] wait:20*NSEC_PER_SEC], @"Key state should become 'waitfortrust'");
1563
1564 CKKSResultOperation<CKKSKeySetProviderOperationProtocol>* keysetOp = [self.keychainView findKeySet:NO];
1565 [keysetOp timeout:20*NSEC_PER_SEC];
1566 [keysetOp waitUntilFinished];
1567
1568 XCTAssertNil(keysetOp.error, "Should be no error fetching a keyset");
1569 XCTAssertNotNil(keysetOp.keyset, "Should have a keyset");
1570 XCTAssertNotNil(keysetOp.keyset.tlk, "Should have a TLK");
1571 }
1572
1573 - (void)testUploadInitialKeyHierarchyAfterLockedStart {
1574 // 'Lock' the keybag
1575 self.aksLockState = true;
1576 [self.lockStateTracker recheck];
1577
1578 [self startCKKSSubsystem];
1579
1580 XCTAssertEqual(0, [self.keychainView.keyHierarchyConditions[SecCKKSZoneKeyStateWaitForTLKCreation] wait:20*NSEC_PER_SEC], @"Key state should get stuck in waitfortlkcreation");
1581
1582 CKKSResultOperation<CKKSKeySetProviderOperationProtocol>* keysetOp = [self.keychainView findKeySet:NO];
1583
1584 // Wait for the key hierarchy state machine to get stuck waiting for the unlock dependency. No uploads should occur.
1585 XCTAssertEqual(0, [self.keychainView.keyHierarchyConditions[SecCKKSZoneKeyStateWaitForUnlock] wait:20*NSEC_PER_SEC], @"Key state should get stuck in waitforunlock");
1586
1587 // After unlock, the key hierarchy should be created.
1588 self.aksLockState = false;
1589 [self.lockStateTracker recheck];
1590
1591 [keysetOp timeout:10 * NSEC_PER_SEC];
1592 [keysetOp waitUntilFinished];
1593 XCTAssertNil(keysetOp.error, @"Should be no error performing keyset op");
1594
1595 XCTAssertEqual(0, [self.keychainView.keyHierarchyConditions[SecCKKSZoneKeyStateWaitForTLKUpload] wait:20*NSEC_PER_SEC], @"Key state should enter 'waitfortlkupload'");
1596
1597 NSArray<CKRecord*>* keyHierarchyRecords = [self putKeySetInCloudKit:keysetOp.keyset];
1598 [self.keychainView receiveTLKUploadRecords:keyHierarchyRecords];
1599
1600 XCTAssertEqual(0, [self.keychainView.keyHierarchyConditions[SecCKKSZoneKeyStateReady] wait:20*NSEC_PER_SEC], @"Key state should enter 'ready'");
1601
1602 // We expect a single class C record to be uploaded.
1603 [self expectCKModifyItemRecords: 1 currentKeyPointerRecords: 1 zoneID:self.keychainZoneID checkItem: [self checkClassCBlock:self.keychainZoneID message:@"Object was encrypted under class C key in hierarchy"]];
1604
1605 [self addGenericPassword: @"data" account: @"account-delete-me"];
1606 OCMVerifyAllWithDelay(self.mockDatabase, 20);
1607 }
1608
1609 - (void)testExitWaitForTLKUploadIfTLKsCreated {
1610 [self startCKKSSubsystem];
1611
1612 XCTAssertEqual(0, [self.keychainView.keyHierarchyConditions[SecCKKSZoneKeyStateWaitForTLKCreation] wait:20*NSEC_PER_SEC], @"Key state should get stuck in waitfortlkcreation");
1613
1614 CKKSResultOperation<CKKSKeySetProviderOperationProtocol>* keysetOp = [self.keychainView findKeySet:NO];
1615
1616 [keysetOp timeout:10 * NSEC_PER_SEC];
1617 [keysetOp waitUntilFinished];
1618 XCTAssertNil(keysetOp.error, @"Should be no error performing keyset op");
1619
1620 XCTAssertEqual(0, [self.keychainView.keyHierarchyConditions[SecCKKSZoneKeyStateWaitForTLKUpload] wait:20*NSEC_PER_SEC], @"Key state should enter 'waitfortlkupload'");
1621
1622 // But another device beats us to it!
1623 [self putFakeKeyHierarchyInCloudKit:self.keychainZoneID];
1624 [self putFakeDeviceStatusInCloudKit:self.keychainZoneID];
1625
1626 [self.injectedManager.zoneChangeFetcher notifyZoneChange:nil];
1627
1628 XCTAssertEqual(0, [self.keychainView.keyHierarchyConditions[SecCKKSZoneKeyStateWaitForTLK] wait:20*NSEC_PER_SEC], @"Key state should enter 'waitfortlk'");
1629 OCMVerifyAllWithDelay(self.mockDatabase, 20);
1630 }
1631
1632 - (void)testExitWaitForTLKUploadIfTLKsCreatedWhileNoTrust {
1633 self.mockSOSAdapter.circleStatus = kSOSCCNotInCircle;
1634 [self.accountStateTracker notifyCircleStatusChangeAndWaitForSignal];
1635
1636 [self startCKKSSubsystem];
1637
1638 XCTAssertEqual(0, [self.keychainView.keyHierarchyConditions[SecCKKSZoneKeyStateWaitForTLKCreation] wait:20*NSEC_PER_SEC], @"Key state should get stuck in waitfortlkcreation");
1639
1640 CKKSResultOperation<CKKSKeySetProviderOperationProtocol>* keysetOp = [self.keychainView findKeySet:NO];
1641
1642 [keysetOp timeout:10 * NSEC_PER_SEC];
1643 [keysetOp waitUntilFinished];
1644 XCTAssertNil(keysetOp.error, @"Should be no error performing keyset op");
1645
1646 XCTAssertEqual(0, [self.keychainView.keyHierarchyConditions[SecCKKSZoneKeyStateWaitForTLKUpload] wait:20*NSEC_PER_SEC], @"Key state should enter 'waitfortlkupload'");
1647
1648 // But another device beats us to it!
1649 [self putFakeKeyHierarchyInCloudKit:self.keychainZoneID];
1650 [self putFakeDeviceStatusInCloudKit:self.keychainZoneID];
1651
1652 [self.injectedManager.zoneChangeFetcher notifyZoneChange:nil];
1653
1654 XCTAssertEqual(0, [self.keychainView.keyHierarchyConditions[SecCKKSZoneKeyStateWaitForTrust] wait:20*NSEC_PER_SEC], @"Key state should enter 'waitfortrust'");
1655 OCMVerifyAllWithDelay(self.mockDatabase, 20);
1656 }
1657
1658 - (void)testLockImmediatelyAfterUploadingInitialKeyHierarchy {
1659
1660 __weak __typeof(self) weakSelf = self;
1661
1662 [self startCKKSSubsystem];
1663 [self performOctagonTLKUpload:self.ckksViews afterUpload:^{
1664 __strong __typeof(self) strongSelf = weakSelf;
1665 [strongSelf holdCloudKitFetches];
1666 }];
1667
1668 // Should enter 'ready'
1669 XCTAssertEqual(0, [self.keychainView.keyHierarchyConditions[SecCKKSZoneKeyStateReady] wait:20*NSEC_PER_SEC], @"Key state should become 'ready'");
1670 OCMVerifyAllWithDelay(self.mockDatabase, 20);
1671
1672 // Now, lock and allow fetches again
1673 self.aksLockState = true;
1674 [self.lockStateTracker recheck];
1675 [self releaseCloudKitFetchHold];
1676
1677 CKKSResultOperation* op = [self.keychainView.zoneChangeFetcher requestSuccessfulFetch:CKKSFetchBecauseTesting];
1678 [op waitUntilFinished];
1679
1680 OCMVerifyAllWithDelay(self.mockDatabase, 20);
1681
1682 // Wait for CKKS to shake itself out...
1683 [self.keychainView waitForOperationsOfClass:[CKKSProcessReceivedKeysOperation class]];
1684
1685 // Should be in ReadyPendingUnlock
1686 XCTAssertEqual(0, [self.keychainView.keyHierarchyConditions[SecCKKSZoneKeyStateReadyPendingUnlock] wait:20*NSEC_PER_SEC], @"Key state should become 'readypendingunlock'");
1687
1688 // We expect a single class C record to be uploaded.
1689 [self expectCKModifyItemRecords: 1 currentKeyPointerRecords: 1 zoneID:self.keychainZoneID
1690 checkItem: [self checkClassCBlock:self.keychainZoneID message:@"Object was encrypted under class C key in hierarchy"]];
1691
1692 [self addGenericPassword: @"data" account: @"account-delete-me"];
1693 OCMVerifyAllWithDelay(self.mockDatabase, 20);
1694 }
1695
1696 - (void)testReceiveKeyHierarchyAfterLockedStart {
1697 // 'Lock' the keybag
1698 self.aksLockState = true;
1699 [self.lockStateTracker recheck];
1700
1701 [self startCKKSSubsystem];
1702
1703 // Wait for the key hierarchy state machine to get stuck waiting for the unlock dependency. No uploads should occur.
1704 XCTAssertEqual(0, [self.keychainView.keyHierarchyConditions[SecCKKSZoneKeyStateWaitForTLKCreation] wait:20*NSEC_PER_SEC], @"Key state should get stuck in waitfortlkcreation");
1705
1706 // Now, another device comes along and creates the hierarchy; we download it; and it and sends us the TLK
1707 [self putFakeKeyHierarchyInCloudKit:self.keychainZoneID];
1708 [self putFakeDeviceStatusInCloudKit:self.keychainZoneID];
1709 [self.injectedManager.zoneChangeFetcher notifyZoneChange:nil];
1710 [[self.keychainView.zoneChangeFetcher requestSuccessfulFetch:CKKSFetchBecauseTesting] waitUntilFinished];
1711
1712 self.aksLockState = false;
1713 [self.lockStateTracker recheck];
1714 XCTAssertEqual(0, [self.keychainView.keyHierarchyConditions[SecCKKSZoneKeyStateWaitForTLK] wait:20*NSEC_PER_SEC], @"Key state should end up in waitfortlk");
1715
1716 // After unlock, the TLK arrives
1717 [self expectCKKSTLKSelfShareUpload:self.keychainZoneID];
1718 [self saveTLKMaterialToKeychain:self.keychainZoneID];
1719
1720 // We expect a single class C record to be uploaded.
1721 [self expectCKModifyItemRecords: 1 currentKeyPointerRecords: 1 zoneID:self.keychainZoneID checkItem: [self checkClassCBlock:self.keychainZoneID message:@"Object was encrypted under class C key in hierarchy"]];
1722
1723 [self addGenericPassword: @"data" account: @"account-delete-me"];
1724 XCTAssertEqual(0, [self.keychainView.keyHierarchyConditions[SecCKKSZoneKeyStateReady] wait:20*NSEC_PER_SEC], @"Key state should become 'ready'");
1725 OCMVerifyAllWithDelay(self.mockDatabase, 20);
1726 }
1727
1728 - (void)testLoadKeyHierarchyAfterLockedStart {
1729 [self createAndSaveFakeKeyHierarchy: self.keychainZoneID];
1730
1731 // 'Lock' the keybag
1732 self.aksLockState = true;
1733 [self.lockStateTracker recheck];
1734
1735 [self startCKKSSubsystem];
1736
1737 // Wait for the key hierarchy state machine to get stuck waiting for the unlock dependency. No uploads should occur.
1738 XCTAssertEqual(0, [self.keychainView.keyHierarchyConditions[SecCKKSZoneKeyStateReadyPendingUnlock] wait:20*NSEC_PER_SEC], @"Key state should become 'readypendingunlock'");
1739
1740 self.aksLockState = false;
1741 [self.lockStateTracker recheck];
1742
1743 // We expect a single class C record to be uploaded.
1744 [self expectCKModifyItemRecords: 1 currentKeyPointerRecords: 1 zoneID:self.keychainZoneID checkItem: [self checkClassCBlock:self.keychainZoneID message:@"Object was encrypted under class C key in hierarchy"]];
1745
1746 [self addGenericPassword: @"data" account: @"account-delete-me"];
1747 OCMVerifyAllWithDelay(self.mockDatabase, 20);
1748 }
1749
1750 - (void)testUploadAndUseKeyHierarchy {
1751 [self startCKKSSubsystem];
1752 [self performOctagonTLKUpload:self.ckksViews];
1753
1754 NSDictionary *query = @{(id)kSecClass : (id)kSecClassGenericPassword,
1755 (id)kSecAttrAccessGroup : @"com.apple.security.ckks",
1756 (id)kSecAttrAccount : @"account-delete-me",
1757 (id)kSecAttrSynchronizable : (id)kCFBooleanTrue,
1758 (id)kSecMatchLimit : (id)kSecMatchLimitOne,
1759 };
1760 CFTypeRef item = NULL;
1761 XCTAssertEqual(errSecItemNotFound, SecItemCopyMatching((__bridge CFDictionaryRef) query, &item), "item should not exist");
1762
1763 OCMVerifyAllWithDelay(self.mockDatabase, 20);
1764 [self waitForCKModifications];
1765
1766 // We expect a single class C record to be uploaded.
1767 [self expectCKModifyItemRecords: 1 currentKeyPointerRecords: 1 zoneID:self.keychainZoneID checkItem: [self checkClassCBlock:self.keychainZoneID message:@"Object was encrypted under class C key in hierarchy"]];
1768
1769 [self addGenericPassword: @"data" account: @"account-delete-me"];
1770 OCMVerifyAllWithDelay(self.mockDatabase, 20);
1771
1772 // now, expect a single class A record to be uploaded
1773 [self expectCKModifyItemRecords: 1 currentKeyPointerRecords: 1 zoneID:self.keychainZoneID checkItem: [self checkClassABlock:self.keychainZoneID message:@"Object was encrypted under class A key in hierarchy"]];
1774
1775 XCTAssertEqual(errSecSuccess, SecItemAdd((__bridge CFDictionaryRef)@{
1776 (id)kSecClass : (id)kSecClassGenericPassword,
1777 (id)kSecAttrAccessGroup : @"com.apple.security.sos",
1778 (id)kSecAttrAccessible: (id)kSecAttrAccessibleWhenUnlocked,
1779 (id)kSecAttrAccount : @"account-class-A",
1780 (id)kSecAttrSynchronizable : (id)kCFBooleanTrue,
1781 (id)kSecAttrSyncViewHint : self.keychainView.zoneName,
1782 (id)kSecValueData : (id) [@"asdf" dataUsingEncoding:NSUTF8StringEncoding],
1783 }, NULL), @"Adding class A item");
1784 OCMVerifyAllWithDelay(self.mockDatabase, 20);
1785 }
1786
1787 - (void)testUploadInitialKeyHierarchyTriggersBackup {
1788 // We also expect the view manager's notifyNewTLKsInKeychain call to fire (after some delay)
1789 OCMExpect([self.mockCKKSViewManager notifyNewTLKsInKeychain]);
1790
1791 // Spin up CKKS subsystem.
1792 [self startCKKSSubsystem];
1793 [self performOctagonTLKUpload:self.ckksViews];
1794
1795 OCMVerifyAllWithDelay(self.mockDatabase, 20);
1796 OCMVerifyAllWithDelay(self.mockCKKSViewManager, 10);
1797 }
1798
1799 - (void)testResetCloudKitZoneFromNoTLK {
1800 self.suggestTLKUpload = OCMClassMock([CKKSNearFutureScheduler class]);
1801 OCMExpect([self.suggestTLKUpload trigger]);
1802
1803 self.silentZoneDeletesAllowed = true;
1804
1805 // If CKKS sees a zone it's never going to be able to read, it should reset that zone
1806 [self putFakeKeyHierarchyInCloudKit:self.keychainZoneID];
1807 // explicitly do not save a fake device status here
1808 self.keychainZone.flag = true;
1809
1810 [self startCKKSSubsystem];
1811 XCTAssertEqual(0, [self.keychainView.keyHierarchyConditions[SecCKKSZoneKeyStateResettingZone] wait:20*NSEC_PER_SEC], @"Key state should become 'resetzone'");
1812
1813 // But then, it'll fire off the reset and reach 'ready', with a little help from octagon
1814 OCMVerifyAllWithDelay(self.suggestTLKUpload, 10);
1815 [self performOctagonTLKUpload:self.ckksViews];
1816
1817 OCMVerifyAllWithDelay(self.mockDatabase, 20);
1818 XCTAssertEqual(0, [self.keychainView.keyHierarchyConditions[SecCKKSZoneKeyStateReady] wait:20*NSEC_PER_SEC], @"Key state should become 'ready'");
1819
1820 // And the zone should have been cleared and re-made
1821 XCTAssertFalse(self.keychainZone.flag, "Zone flag should have been reset to false");
1822 }
1823
1824 - (void)testResetCloudKitZoneFromNoTLKWithOtherWaitForTLKDevices {
1825 self.suggestTLKUpload = OCMClassMock([CKKSNearFutureScheduler class]);
1826 OCMExpect([self.suggestTLKUpload trigger]);
1827
1828 self.silentZoneDeletesAllowed = true;
1829
1830 // If CKKS sees a zone it's never going to be able to read, it should reset that zone
1831 [self putFakeKeyHierarchyInCloudKit:self.keychainZoneID];
1832 // Save a fake device status here, but modify its key state to be 'waitfortlk': it has no idea what the TLK is either
1833 [self putFakeDeviceStatusInCloudKit:self.keychainZoneID];
1834 [self putFakeOctagonOnlyDeviceStatusInCloudKit:self.keychainZoneID];
1835
1836 for(CKRecord* record in self.keychainZone.currentDatabase.allValues) {
1837 if([record.recordType isEqualToString:SecCKRecordDeviceStateType]) {
1838 record[SecCKRecordKeyState] = CKKSZoneKeyToNumber(SecCKKSZoneKeyStateWaitForTLK);
1839 }
1840 }
1841
1842 self.keychainZone.flag = true;
1843
1844 [self startCKKSSubsystem];
1845 XCTAssertEqual(0, [self.keychainView.keyHierarchyConditions[SecCKKSZoneKeyStateResettingZone] wait:20*NSEC_PER_SEC], @"Key state should become 'resetzone'");
1846
1847 OCMVerifyAllWithDelay(self.suggestTLKUpload, 10);
1848 [self performOctagonTLKUpload:self.ckksViews];
1849
1850 // But then, it'll fire off the reset and reach 'ready'
1851 OCMVerifyAllWithDelay(self.mockDatabase, 20);
1852 XCTAssertEqual(0, [self.keychainView.keyHierarchyConditions[SecCKKSZoneKeyStateReady] wait:20*NSEC_PER_SEC], @"Key state should become 'ready'");
1853
1854 // And the zone should have been cleared and re-made
1855 XCTAssertFalse(self.keychainZone.flag, "Zone flag should have been reset to false");
1856 }
1857
1858 - (void)testResetCloudKitZoneFromNoTLKIgnoringInactiveDevices {
1859 self.suggestTLKUpload = OCMClassMock([CKKSNearFutureScheduler class]);
1860 OCMExpect([self.suggestTLKUpload trigger]);
1861
1862 self.silentZoneDeletesAllowed = true;
1863
1864 // If CKKS sees a zone it's never going to be able to read, it should reset that zone
1865 [self putFakeKeyHierarchyInCloudKit:self.keychainZoneID];
1866 // Save a fake device status here, but modify its creation and modification times to be months ago
1867 [self putFakeDeviceStatusInCloudKit:self.keychainZoneID];
1868 [self putFakeOctagonOnlyDeviceStatusInCloudKit:self.keychainZoneID];
1869
1870 // Put a 'in-circle' TLKShare record, but also modify its creation and modification times
1871 CKKSSOSSelfPeer* untrustedPeer = [[CKKSSOSSelfPeer alloc] initWithSOSPeerID:@"untrusted-peer"
1872 encryptionKey:[[SFECKeyPair alloc] initRandomKeyPairWithSpecifier:[[SFECKeySpecifier alloc] initWithCurve:SFEllipticCurveNistp384]]
1873 signingKey:[[SFECKeyPair alloc] initRandomKeyPairWithSpecifier:[[SFECKeySpecifier alloc] initWithCurve:SFEllipticCurveNistp384]]
1874 viewList:self.managedViewList];
1875 [self putTLKShareInCloudKit:self.keychainZoneKeys.tlk from:untrustedPeer to:untrustedPeer zoneID:self.keychainZoneID];
1876
1877 for(CKRecord* record in self.keychainZone.currentDatabase.allValues) {
1878 if([record.recordType isEqualToString:SecCKRecordDeviceStateType] || [record.recordType isEqualToString:SecCKRecordTLKShareType]) {
1879 record.creationDate = [NSDate distantPast];
1880 record.modificationDate = [NSDate distantPast];
1881 }
1882 }
1883
1884 self.keychainZone.flag = true;
1885
1886 [self startCKKSSubsystem];
1887 XCTAssertEqual(0, [self.keychainView.keyHierarchyConditions[SecCKKSZoneKeyStateResettingZone] wait:20*NSEC_PER_SEC], @"Key state should become 'resetzone'");
1888
1889 OCMVerifyAllWithDelay(self.suggestTLKUpload, 10);
1890 [self performOctagonTLKUpload:self.ckksViews];
1891
1892 // But then, it'll fire off the reset and reach 'ready'
1893 OCMVerifyAllWithDelay(self.mockDatabase, 20);
1894 XCTAssertEqual(0, [self.keychainView.keyHierarchyConditions[SecCKKSZoneKeyStateReady] wait:20*NSEC_PER_SEC], @"Key state should become 'ready'");
1895
1896 // And the zone should have been cleared and re-made
1897 XCTAssertFalse(self.keychainZone.flag, "Zone flag should have been reset to false");
1898 }
1899
1900 - (void)testDoNotResetCloudKitZoneDuringBadCircleState {
1901 self.mockSOSAdapter.circleStatus = kSOSCCNotInCircle;
1902 [self.accountStateTracker notifyCircleStatusChangeAndWaitForSignal];
1903
1904 // This test has stuff in CloudKit, but no TLKs.
1905 // CKKS should NOT reset the CK zone.
1906 [self putFakeKeyHierarchyInCloudKit:self.keychainZoneID];
1907 self.zones[self.keychainZoneID].flag = true;
1908
1909 [self startCKKSSubsystem];
1910
1911 // But since we're out of circle, this test needs to initialize the zone itself
1912 [self.keychainView beginCloudKitOperation];
1913
1914 XCTAssertEqual(0, [self.keychainView.keyHierarchyConditions[SecCKKSZoneKeyStateWaitForTrust] wait:20*NSEC_PER_SEC], "CKKS entered waitfortrust");
1915 OCMVerifyAllWithDelay(self.mockDatabase, 20);
1916
1917 FakeCKZone* keychainZone = self.zones[self.keychainZoneID];
1918 XCTAssertNotNil(keychainZone, "Should still have a keychain zone");
1919 XCTAssertTrue(keychainZone.flag, "keychain zone should not have been recreated");
1920 }
1921
1922 - (void)testDoNotResetCloudKitZoneFromWaitForTLKDueToRecentDeviceState {
1923 [self putFakeKeyHierarchyInCloudKit:self.keychainZoneID];
1924
1925 // CKKS shouldn't reset this zone, due to a recent device status claiming to have TLKs
1926 [self putFakeDeviceStatusInCloudKit:self.keychainZoneID];
1927
1928 // Also, CKKS _should_ be able to return the key hierarchy if asked before it starts
1929 CKKSResultOperation<CKKSKeySetProviderOperationProtocol>* keysetOp = [self.keychainView findKeySet:NO];
1930
1931 NSDateComponents* offset = [[NSDateComponents alloc] init];
1932 [offset setDay:-5];
1933 NSDate* updateTime = [[NSCalendar currentCalendar] dateByAddingComponents:offset toDate:[NSDate date] options:0];
1934 for(CKRecord* record in self.keychainZone.currentDatabase.allValues) {
1935 if([record.recordType isEqualToString:SecCKRecordDeviceStateType] || [record.recordType isEqualToString:SecCKRecordTLKShareType]) {
1936 record.creationDate = updateTime;
1937 record.modificationDate = updateTime;
1938 }
1939 }
1940
1941 self.keychainZone.flag = true;
1942 [self startCKKSSubsystem];
1943
1944 XCTAssertEqual(0, [self.keychainView.keyHierarchyConditions[SecCKKSZoneKeyStateWaitForTLK] wait:20*NSEC_PER_SEC], @"Key state should become 'waitfortlk'");
1945
1946 XCTAssertTrue(self.keychainZone.flag, "Zone flag should not have been reset to false");
1947
1948 // And, ensure that the keyset op ran and has results
1949 CKKSResultOperation* waitOp = [CKKSResultOperation named:@"test op" withBlock:^{}];
1950 [waitOp addDependency:keysetOp];
1951 [waitOp timeout:2*NSEC_PER_SEC];
1952 [self.operationQueue addOperation:waitOp];
1953 [waitOp waitUntilFinished];
1954
1955 XCTAssert(keysetOp.finished, "Keyset op should have finished");
1956 XCTAssertNil(keysetOp.error, "keyset op should not have errored");
1957 XCTAssertNotNil(keysetOp.keyset, "keyset op should have a keyset");
1958 XCTAssertNotNil(keysetOp.keyset.currentTLKPointer, "keyset should have a current TLK pointer");
1959 XCTAssertEqualObjects(keysetOp.keyset.currentTLKPointer.currentKeyUUID, self.keychainZoneKeys.tlk.uuid, "keyset should match what's in zone");
1960 }
1961
1962 - (void)testDoNotCloudKitZoneFromWaitForTLKDueToRecentButUntrustedDeviceState {
1963 [self putFakeKeyHierarchyInCloudKit:self.keychainZoneID];
1964
1965 // CKKS should reset this zone, even though to a recent device status claiming to have TLKs. The device isn't trusted
1966 self.silentZoneDeletesAllowed = true;
1967 [self putFakeDeviceStatusInCloudKit:self.keychainZoneID];
1968 [self.mockSOSAdapter.trustedPeers removeObject:self.remoteSOSOnlyPeer];
1969
1970 self.keychainZone.flag = true;
1971 [self startCKKSSubsystem];
1972
1973 XCTAssertEqual(0, [self.keychainView.keyHierarchyConditions[SecCKKSZoneKeyStateWaitForTLK] wait:20*NSEC_PER_SEC], @"Key state should become 'waitfortlk'");
1974 XCTAssertTrue(self.keychainZone.flag, "Zone flag should not have been reset to false");
1975
1976 // And ensure it doesn't go on to 'reset'
1977 XCTAssertNotEqual(0, [self.keychainView.keyHierarchyConditions[SecCKKSZoneKeyStateResettingZone] wait:100*NSEC_PER_MSEC], @"Key state should not become 'resetzone'");
1978 }
1979
1980 - (void)testResetCloudKitZoneFromWaitForTLKDueToLessRecentAndUntrustedDeviceState {
1981 self.suggestTLKUpload = OCMClassMock([CKKSNearFutureScheduler class]);
1982 OCMExpect([self.suggestTLKUpload trigger]);
1983
1984 [self putFakeKeyHierarchyInCloudKit:self.keychainZoneID];
1985
1986 // CKKS should reset this zone, even though to a recent device status claiming to have TLKs. The device isn't trusted
1987 self.silentZoneDeletesAllowed = true;
1988 [self putFakeDeviceStatusInCloudKit:self.keychainZoneID];
1989 [self.mockSOSAdapter.trustedPeers removeObject:self.remoteSOSOnlyPeer];
1990
1991 NSDateComponents* offset = [[NSDateComponents alloc] init];
1992 [offset setDay:-5];
1993 NSDate* updateTime = [[NSCalendar currentCalendar] dateByAddingComponents:offset toDate:[NSDate date] options:0];
1994 for(CKRecord* record in self.keychainZone.currentDatabase.allValues) {
1995 if([record.recordType isEqualToString:SecCKRecordDeviceStateType] || [record.recordType isEqualToString:SecCKRecordTLKShareType]) {
1996 record.creationDate = updateTime;
1997 record.modificationDate = updateTime;
1998 }
1999 }
2000
2001 self.keychainZone.flag = true;
2002 [self startCKKSSubsystem];
2003 XCTAssertEqual(0, [self.keychainView.keyHierarchyConditions[SecCKKSZoneKeyStateResettingZone] wait:20*NSEC_PER_SEC], @"Key state should become 'resetzone'");
2004
2005 OCMVerifyAllWithDelay(self.suggestTLKUpload, 10);
2006 [self performOctagonTLKUpload:self.ckksViews];
2007
2008 // Then we should reset.
2009 OCMVerifyAllWithDelay(self.mockDatabase, 20);
2010 XCTAssertEqual(0, [self.keychainView.keyHierarchyConditions[SecCKKSZoneKeyStateReady] wait:20*NSEC_PER_SEC], @"Key state should become 'ready'");
2011
2012 // And the zone should have been cleared and re-made
2013 XCTAssertFalse(self.keychainZone.flag, "Zone flag should have been reset to false");
2014 }
2015
2016 - (void)testAcceptExistingKeyHierarchy {
2017 // Test starts with no keys in CKKS database, but one in our fake CloudKit.
2018 // Test also begins with the TLK having arrived in the local keychain (via SOS)
2019 [self putFakeKeyHierarchyInCloudKit:self.keychainZoneID];
2020 [self saveTLKMaterialToKeychain:self.keychainZoneID];
2021 [self expectCKKSTLKSelfShareUpload:self.keychainZoneID];
2022
2023 // Spin up CKKS subsystem.
2024 [self startCKKSSubsystem];
2025
2026 // The CKKS subsystem should only upload its TLK share
2027 XCTAssertEqual(0, [self.keychainView.keyHierarchyConditions[SecCKKSZoneKeyStateReady] wait:20*NSEC_PER_SEC], "Key state should have become ready");
2028
2029 OCMVerifyAllWithDelay(self.mockDatabase, 20);
2030
2031 // Verify that there are three local keys, and three local current key records
2032 __weak __typeof(self) weakSelf = self;
2033 [self.keychainView dispatchSyncWithReadOnlySQLTransaction:^{
2034 __strong __typeof(weakSelf) strongSelf = weakSelf;
2035 XCTAssertNotNil(strongSelf, "self exists");
2036
2037 NSError* error = nil;
2038
2039 NSArray<CKKSKey*>* keys = [CKKSKey localKeys:strongSelf.keychainZoneID error:&error];
2040 XCTAssertNil(error, "no error fetching keys");
2041 XCTAssertEqual(keys.count, 3u, "Three keys in local database");
2042
2043 NSArray<CKKSCurrentKeyPointer*>* currentkeys = [CKKSCurrentKeyPointer all: &error];
2044 XCTAssertNil(error, "no error fetching current keys");
2045 XCTAssertEqual(currentkeys.count, 3u, "Three current key pointers in local database");
2046 }];
2047 }
2048
2049 - (void)testAcceptExistingAndUseKeyHierarchy {
2050 // Test starts with nothing in database, but one in our fake CloudKit.
2051 [self putFakeKeyHierarchyInCloudKit:self.keychainZoneID];
2052 [self putFakeDeviceStatusInCloudKit:self.keychainZoneID];
2053 // But, CKKS shouldn't ever reset the zone
2054 self.keychainZone.flag = true;
2055
2056 [self startCKKSSubsystem];
2057 XCTAssertEqual(0, [self.keychainView.keyHierarchyConditions[SecCKKSZoneKeyStateWaitForTLK] wait:200*NSEC_PER_SEC], "Key state should have become waitfortlk");
2058
2059 // Now, save the TLK to the keychain (to simulate it coming in later via SOS). We'll create a TLK share for ourselves.
2060 [self expectCKKSTLKSelfShareUpload:self.keychainZoneID];
2061 [self saveTLKMaterialToKeychainSimulatingSOS:self.keychainZoneID];
2062
2063 // Wait for the key hierarchy to sort itself out, to make it easier on this test; see testOnboardOldItemsWithExistingKeyHierarchy for the other test.
2064 // The CKKS subsystem should write its TLK share, but nothing else
2065 XCTAssertEqual(0, [self.keychainView.keyHierarchyConditions[SecCKKSZoneKeyStateReady] wait:20*NSEC_PER_SEC], "Key state should have become ready");
2066
2067 // We expect a single record to be uploaded for each key class
2068 [self expectCKModifyItemRecords: 1 currentKeyPointerRecords: 1 zoneID:self.keychainZoneID checkItem: [self checkClassCBlock:self.keychainZoneID message:@"Object was encrypted under class C key in hierarchy"]];
2069 [self addGenericPassword: @"data" account: @"account-delete-me"];
2070 OCMVerifyAllWithDelay(self.mockDatabase, 20);
2071
2072 [self expectCKModifyItemRecords: 1 currentKeyPointerRecords: 1 zoneID:self.keychainZoneID checkItem: [self checkClassABlock:self.keychainZoneID message:@"Object was encrypted under class A key in hierarchy"]];
2073 [self addGenericPassword:@"asdf"
2074 account:@"account-class-A"
2075 viewHint:nil
2076 access:(id)kSecAttrAccessibleWhenUnlocked
2077 expecting:errSecSuccess
2078 message:@"Adding class A item"];
2079 OCMVerifyAllWithDelay(self.mockDatabase, 20);
2080 XCTAssertTrue(self.keychainZone.flag, "Keychain zone shouldn't have been reset");
2081 }
2082
2083 - (void)testAcceptExistingKeyHierarchyDespiteLocked {
2084 // Test starts with no keys in CKKS database, but one in our fake CloudKit.
2085 // Test also begins with the TLK having arrived in the local keychain (via SOS)
2086
2087 [self putFakeKeyHierarchyInCloudKit:self.keychainZoneID];
2088 [self saveTLKMaterialToKeychain:self.keychainZoneID];
2089
2090 self.aksLockState = true;
2091 [self.lockStateTracker recheck];
2092
2093 // Spin up CKKS subsystem.
2094 [self startCKKSSubsystem];
2095
2096 XCTAssertEqual(0, [self.keychainView.keyHierarchyConditions[SecCKKSZoneKeyStateWaitForUnlock] wait:20*NSEC_PER_SEC], "Key state should have become waitforunlock");
2097
2098 // CKKS will give itself a TLK Share
2099 [self expectCKKSTLKSelfShareUpload:self.keychainZoneID];
2100
2101 // Now that all operations are complete, 'unlock' AKS
2102 self.aksLockState = false;
2103 [self.lockStateTracker recheck];
2104
2105 OCMVerifyAllWithDelay(self.mockDatabase, 20);
2106 XCTAssertEqual(0, [self.keychainView.keyHierarchyConditions[SecCKKSZoneKeyStateReady] wait:20*NSEC_PER_SEC], "Key state should have become ready");
2107
2108 // Verify that there are three local keys, and three local current key records
2109 __weak __typeof(self) weakSelf = self;
2110 [self.keychainView dispatchSyncWithReadOnlySQLTransaction:^{
2111 __strong __typeof(weakSelf) strongSelf = weakSelf;
2112 XCTAssertNotNil(strongSelf, "self exists");
2113
2114 NSError* error = nil;
2115
2116 NSArray<CKKSKey*>* keys = [CKKSKey localKeys:strongSelf.keychainZoneID error:&error];
2117 XCTAssertNil(error, "no error fetching keys");
2118 XCTAssertEqual(keys.count, 3u, "Three keys in local database");
2119
2120 NSArray<CKKSCurrentKeyPointer*>* currentkeys = [CKKSCurrentKeyPointer all: &error];
2121 XCTAssertNil(error, "no error fetching current keys");
2122 XCTAssertEqual(currentkeys.count, 3u, "Three current key pointers in local database");
2123 }];
2124 }
2125
2126 - (void)testReceiveClassCWhileALocked {
2127 // Test starts with a key hierarchy already existing.
2128 [self createAndSaveFakeKeyHierarchy:self.keychainZoneID];
2129 [self expectCKKSTLKSelfShareUpload:self.keychainZoneID];
2130 [self startCKKSSubsystem];
2131
2132 XCTAssertEqual(0, [self.keychainView.keyHierarchyConditions[SecCKKSZoneKeyStateReady] wait:20*NSEC_PER_SEC], @"Key state should become 'ready'");
2133
2134 [self.injectedManager.zoneChangeFetcher notifyZoneChange:nil];
2135 [self.keychainView waitForFetchAndIncomingQueueProcessing];
2136
2137 [self findGenericPassword:@"classCItem" expecting:errSecItemNotFound];
2138 [self findGenericPassword:@"classAItem" expecting:errSecItemNotFound];
2139
2140 // 'Lock' the keybag
2141 self.aksLockState = true;
2142 [self.lockStateTracker recheck];
2143
2144 XCTAssertNotNil(self.keychainZoneKeys, "Have zone keys for zone");
2145 XCTAssertNotNil(self.keychainZoneKeys.classA, "Have class A key for zone");
2146 XCTAssertNotNil(self.keychainZoneKeys.classC, "Have class C key for zone");
2147
2148 [self.keychainView.stateMachine handleFlag:CKKSFlagKeyStateProcessRequested];
2149
2150 // And ensure we end up back in 'readypendingunlock': we have the keys, we're just locked now
2151 [self.keychainView waitForOperationsOfClass:[CKKSProcessReceivedKeysOperation class]];
2152 XCTAssertEqual(0, [self.keychainView.keyHierarchyConditions[SecCKKSZoneKeyStateReadyPendingUnlock] wait:20*NSEC_PER_SEC], @"Key state should become 'readypendingunlock'");
2153
2154 [self.keychainZone addToZone: [self createFakeRecord: self.keychainZoneID recordName:@"7B598D31-F9C5-481E-98AC-5A507ACB2D85" withAccount:@"classCItem" key:self.keychainZoneKeys.classC]];
2155 [self.keychainZone addToZone: [self createFakeRecord: self.keychainZoneID recordName:@"7B598D31-FFFF-FFFF-FFFF-5A507ACB2D85" withAccount:@"classAItem" key:self.keychainZoneKeys.classA]];
2156
2157 CKKSResultOperation* op = self.keychainView.resultsOfNextProcessIncomingQueueOperation;
2158 [self.injectedManager.zoneChangeFetcher notifyZoneChange:nil];
2159 [self.keychainView waitForFetchAndIncomingQueueProcessing];
2160 // The processing op should NOT error, even though it didn't manage to process the classA item
2161 XCTAssertNil(op.error, "no error while failing to process a class A item");
2162
2163 CKKSResultOperation* erroringOp = [self.keychainView processIncomingQueue:true];
2164 [erroringOp waitUntilFinished];
2165 XCTAssertNotNil(erroringOp.error, "error exists while processing a class A item");
2166
2167 [self findGenericPassword:@"classCItem" expecting:errSecSuccess];
2168 [self findGenericPassword:@"classAItem" expecting:errSecItemNotFound];
2169
2170 self.aksLockState = false;
2171 [self.lockStateTracker recheck];
2172 [self.keychainView waitUntilAllOperationsAreFinished];
2173
2174 [self findGenericPassword:@"classCItem" expecting:errSecSuccess];
2175 [self findGenericPassword:@"classAItem" expecting:errSecSuccess];
2176 }
2177
2178 - (void)testRestartWhileLocked {
2179 [self startCKKSSubsystem];
2180 [self performOctagonTLKUpload:self.ckksViews];
2181
2182 XCTAssertEqual(0, [self.keychainView.keyHierarchyConditions[SecCKKSZoneKeyStateReady] wait:20*NSEC_PER_SEC], @"Key state should become 'ready'");
2183
2184 // 'Lock' the keybag
2185 self.aksLockState = true;
2186 [self.lockStateTracker recheck];
2187
2188 [self.keychainView halt];
2189 self.keychainView = [[CKKSViewManager manager] restartZone: self.keychainZoneID.zoneName];
2190 [self.keychainView beginCloudKitOperation];
2191 [self beginSOSTrustedViewOperation:self.keychainView];
2192
2193 XCTAssertEqual(0, [self.keychainView.keyHierarchyConditions[SecCKKSZoneKeyStateReadyPendingUnlock] wait:20*NSEC_PER_SEC], @"Key state should become 'readypendingunlock'");
2194
2195 self.aksLockState = false;
2196 [self.lockStateTracker recheck];
2197
2198 XCTAssertEqual(0, [self.keychainView.keyHierarchyConditions[SecCKKSZoneKeyStateReady] wait:20*NSEC_PER_SEC], @"Key state should become 'ready'");
2199 }
2200
2201 - (void)testExternalKeyRoll {
2202 // Test starts with no keys in database, a key hierarchy in our fake CloudKit, and the TLK safely in the local keychain.
2203 [self putFakeKeyHierarchyInCloudKit:self.keychainZoneID];
2204 [self expectCKKSTLKSelfShareUpload:self.keychainZoneID];
2205 [self saveTLKMaterialToKeychain:self.keychainZoneID];
2206
2207 // Spin up CKKS subsystem.
2208 [self startCKKSSubsystem];
2209
2210 // The CKKS subsystem should not try to write anything to the CloudKit database.
2211 OCMVerifyAllWithDelay(self.mockDatabase, 20);
2212
2213 __weak __typeof(self) weakSelf = self;
2214
2215 // We expect a single record to be uploaded.
2216 [self expectCKModifyItemRecords: 1 currentKeyPointerRecords: 1 zoneID:self.keychainZoneID checkItem: [self checkClassCBlock:self.keychainZoneID message:@"Object was encrypted under class C key in hierarchy"]];
2217
2218 [self addGenericPassword: @"data" account: @"account-delete-me"];
2219
2220 OCMVerifyAllWithDelay(self.mockDatabase, 20);
2221 [self waitForCKModifications];
2222
2223 [self rollFakeKeyHierarchyInCloudKit:self.keychainZoneID];
2224 [self expectCKKSTLKSelfShareUpload:self.keychainZoneID];
2225 [self saveTLKMaterialToKeychain:self.keychainZoneID];
2226
2227 [self.injectedManager.zoneChangeFetcher notifyZoneChange:nil];
2228
2229 // Make life easy on this test; testAcceptKeyConflictAndUploadReencryptedItem will check the case when we don't receive the notification
2230 [self.keychainView waitForFetchAndIncomingQueueProcessing];
2231
2232 // Just in extra case of threading issues, force a reexamination of the key hierarchy
2233 [self.keychainView dispatchSyncWithSQLTransaction:^CKKSDatabaseTransactionResult{
2234 [self.keychainView _onqueuePokeKeyStateMachine];
2235 return CKKSDatabaseTransactionCommit;
2236 }];
2237
2238 XCTAssertEqual(0, [self.keychainView.keyHierarchyConditions[SecCKKSZoneKeyStateReady] wait:20*NSEC_PER_SEC], @"Key state should become 'ready'");
2239
2240 // Verify that there are six local keys, and three local current key records
2241 [self.keychainView dispatchSyncWithReadOnlySQLTransaction:^{
2242 __strong __typeof(weakSelf) strongSelf = weakSelf;
2243 XCTAssertNotNil(strongSelf, "self exists");
2244
2245 NSError* error = nil;
2246 NSArray<CKKSKey*>* keys = [CKKSKey localKeys:self.keychainZoneID error:&error];
2247 XCTAssertNil(error, "no error fetching keys");
2248 XCTAssertEqual(keys.count, 6u, "Six keys in local database");
2249
2250 NSArray<CKKSCurrentKeyPointer*>* currentkeys = [CKKSCurrentKeyPointer all: &error];
2251 XCTAssertNil(error, "no error fetching current keys");
2252 XCTAssertEqual(currentkeys.count, 3u, "Three current key pointers in local database");
2253
2254 for(CKKSCurrentKeyPointer* key in currentkeys) {
2255 if([key.keyclass isEqualToString: SecCKKSKeyClassTLK]) {
2256 XCTAssertEqualObjects(key.currentKeyUUID, strongSelf.keychainZoneKeys.tlk.uuid);
2257 } else if([key.keyclass isEqualToString: SecCKKSKeyClassA]) {
2258 XCTAssertEqualObjects(key.currentKeyUUID, strongSelf.keychainZoneKeys.classA.uuid);
2259 } else if([key.keyclass isEqualToString: SecCKKSKeyClassC]) {
2260 XCTAssertEqualObjects(key.currentKeyUUID, strongSelf.keychainZoneKeys.classC.uuid);
2261 } else {
2262 XCTFail("Unknown key class: %@", key.keyclass);
2263 }
2264 }
2265 }];
2266
2267 // We expect a single record to be uploaded.
2268 [self expectCKModifyItemRecords: 1 currentKeyPointerRecords: 1 zoneID:self.keychainZoneID checkItem: [self checkClassCBlock:self.keychainZoneID message:@"Object was encrypted under class C key in hierarchy"]];
2269
2270 // TODO: remove this by writing code for item reencrypt after key arrival
2271 [self.injectedManager.zoneChangeFetcher notifyZoneChange:nil];
2272 [self.keychainView waitForFetchAndIncomingQueueProcessing];
2273
2274 [self addGenericPassword: @"data" account: @"account-delete-me-rolled-key"];
2275
2276 OCMVerifyAllWithDelay(self.mockDatabase, 20);
2277 }
2278
2279 - (void)testAcceptKeyConflictAndUploadReencryptedItem {
2280 // Test starts with no keys in database, a key hierarchy in our fake CloudKit, and the TLK safely in the local keychain.
2281 [self putFakeKeyHierarchyInCloudKit:self.keychainZoneID];
2282 [self expectCKKSTLKSelfShareUpload:self.keychainZoneID];
2283 [self saveTLKMaterialToKeychain:self.keychainZoneID];
2284
2285 [self startCKKSSubsystem];
2286 [self.keychainView waitUntilAllOperationsAreFinished];
2287
2288 XCTAssertEqual(0, [self.keychainView.keyHierarchyConditions[SecCKKSZoneKeyStateReady] wait:20*NSEC_PER_SEC], @"key state should enter 'ready'");
2289
2290 // We expect a single record to be uploaded.
2291 [self expectCKModifyItemRecords: 1 currentKeyPointerRecords: 1 zoneID:self.keychainZoneID checkItem: [self checkClassCBlock:self.keychainZoneID message:@"Object was encrypted under class C key in hierarchy"]];
2292
2293 [self addGenericPassword: @"data" account: @"account-delete-me"];
2294
2295 OCMVerifyAllWithDelay(self.mockDatabase, 20);
2296 [self waitForCKModifications];
2297 XCTAssertEqual(0, [self.keychainView.keyHierarchyConditions[SecCKKSZoneKeyStateReady] wait:20*NSEC_PER_SEC], @"key state should enter 'ready'");
2298
2299 [self rollFakeKeyHierarchyInCloudKit:self.keychainZoneID];
2300
2301 // Do not trigger a notification here. This should cause a conflict updating the current key records
2302
2303 // We expect a single record to be uploaded, but that the write will be rejected
2304 // We then expect that item to be reuploaded with the current key
2305
2306 [self expectCKAtomicModifyItemRecordsUpdateFailure: self.keychainZoneID];
2307 [self addGenericPassword: @"data" account: @"account-delete-me-rolled-key"];
2308 OCMVerifyAllWithDelay(self.mockDatabase, 20);
2309
2310 [self expectCKModifyItemRecords: 1 currentKeyPointerRecords: 1 zoneID:self.keychainZoneID checkItem: [self checkClassCBlock:self.keychainZoneID message:@"Object was encrypted under rolled class C key in hierarchy"]];
2311
2312 // New key arrives via SOS!
2313 [self expectCKKSTLKSelfShareUpload:self.keychainZoneID];
2314 [self saveTLKMaterialToKeychainSimulatingSOS:self.keychainZoneID];
2315
2316 OCMVerifyAllWithDelay(self.mockDatabase, 20);
2317 }
2318
2319 - (void)testAcceptKeyConflictAndUploadReencryptedItems {
2320 // Test starts with no keys in database, a key hierarchy in our fake CloudKit, and the TLK safely in the local keychain.
2321 [self putFakeKeyHierarchyInCloudKit:self.keychainZoneID];
2322 [self expectCKKSTLKSelfShareUpload:self.keychainZoneID];
2323 [self saveTLKMaterialToKeychain:self.keychainZoneID];
2324
2325 [self startCKKSSubsystem];
2326 [self.keychainView waitUntilAllOperationsAreFinished];
2327
2328 // We expect a single record to be uploaded.
2329 [self expectCKModifyItemRecords:1 currentKeyPointerRecords:1 zoneID:self.keychainZoneID
2330 checkItem:[self checkClassCBlock:self.keychainZoneID message:@"Object was encrypted under class C key in hierarchy"]];
2331
2332 [self addGenericPassword: @"data" account: @"account-delete-me"];
2333
2334 OCMVerifyAllWithDelay(self.mockDatabase, 20);
2335 [self waitForCKModifications];
2336
2337 [self rollFakeKeyHierarchyInCloudKit:self.keychainZoneID];
2338
2339 // Do not trigger a notification here. This should cause a conflict updating the current key records
2340
2341 // We expect a single record to be uploaded, but that the write will be rejected
2342 // We then expect that item to be reuploaded with the current key
2343
2344 [self expectCKAtomicModifyItemRecordsUpdateFailure: self.keychainZoneID];
2345 [self addGenericPassword: @"data" account: @"account-delete-me-rolled-key"];
2346 [self addGenericPassword: @"data" account: @"account-delete-me-rolled-key-2"];
2347 OCMVerifyAllWithDelay(self.mockDatabase, 20);
2348
2349 [self expectCKModifyItemRecords:2 currentKeyPointerRecords:1 zoneID:self.keychainZoneID
2350 checkItem:[self checkClassCBlock:self.keychainZoneID message:@"Object was encrypted under rolled class C key in hierarchy"]];
2351
2352 // New key arrives via SOS!
2353 [self expectCKKSTLKSelfShareUpload:self.keychainZoneID];
2354 [self saveTLKMaterialToKeychainSimulatingSOS:self.keychainZoneID];
2355
2356 OCMVerifyAllWithDelay(self.mockDatabase, 20);
2357 }
2358
2359 - (void)testAcceptKeyHierarchyResetAndUploadReencryptedItem {
2360 // Test starts with nothing in CloudKit. CKKS uploads a key hierarchy, then it's silently replaced.
2361 // CKKS should notice the replacement, and reupload the item.
2362
2363 [self startCKKSSubsystem];
2364
2365 [self performOctagonTLKUpload:self.ckksViews];
2366 OCMVerifyAllWithDelay(self.mockDatabase, 20);
2367
2368 XCTAssertEqual(0, [self.keychainView.keyHierarchyConditions[SecCKKSZoneKeyStateReady] wait:20*NSEC_PER_SEC], @"key state should enter 'ready'");
2369
2370 // We expect a single record to be uploaded.
2371 [self expectCKModifyItemRecords:1 currentKeyPointerRecords:1 zoneID:self.keychainZoneID checkItem: [self checkClassCBlock:self.keychainZoneID message:@"Object was encrypted under class C key in hierarchy"]];
2372
2373 [self addGenericPassword: @"data" account: @"account-delete-me"];
2374
2375 OCMVerifyAllWithDelay(self.mockDatabase, 20);
2376 [self waitForCKModifications];
2377
2378 // A new peer arrives and resets the world! It sends us a share, though.
2379 CKKSSOSSelfPeer* remotePeer1 = [[CKKSSOSSelfPeer alloc] initWithSOSPeerID:@"remote-peer1"
2380 encryptionKey:[[SFECKeyPair alloc] initRandomKeyPairWithSpecifier:[[SFECKeySpecifier alloc] initWithCurve:SFEllipticCurveNistp384]]
2381 signingKey:[[SFECKeyPair alloc] initRandomKeyPairWithSpecifier:[[SFECKeySpecifier alloc] initWithCurve:SFEllipticCurveNistp384]]
2382 viewList:self.managedViewList];
2383 [self.mockSOSAdapter.trustedPeers addObject:remotePeer1];
2384
2385 NSString* classCUUID = self.keychainZoneKeys.classC.uuid;
2386
2387 self.zones[self.keychainZoneID] = [[FakeCKZone alloc] initZone:self.keychainZoneID];
2388 self.keys[self.keychainZoneID] = nil;
2389 [self putFakeKeyHierarchyInCloudKit:self.keychainZoneID];
2390 [self putTLKSharesInCloudKit:self.keychainZoneKeys.tlk from:remotePeer1 zoneID:self.keychainZoneID];
2391
2392 XCTAssertNotEqual(classCUUID, self.keychainZoneKeys.classC.uuid, @"Class C UUID should have changed");
2393
2394 // Upon adding an item, we expect a failed OQO, then another OQO with the two items (encrypted correctly)
2395 [self expectCKAtomicModifyItemRecordsUpdateFailure:self.keychainZoneID];
2396
2397 [self expectCKModifyItemRecords:2
2398 currentKeyPointerRecords:1
2399 zoneID:self.keychainZoneID
2400 checkItem:[self checkClassCBlock:self.keychainZoneID message:@"Object was encrypted under class C key in hierarchy"]];
2401
2402 // We also expect a self share upload, once CKKS figures out the right key hierarchy
2403 [self expectCKKSTLKSelfShareUpload:self.keychainZoneID];
2404
2405 [self addGenericPassword: @"data" account: @"account-delete-me-after-reset"];
2406
2407 OCMVerifyAllWithDelay(self.mockDatabase, 20);
2408 }
2409
2410 - (void)testRecoverFromRequestKeyRefetchWithoutRolling {
2411 // Simply requesting a key state refetch shouldn't roll the key hierarchy.
2412
2413 [self createAndSaveFakeKeyHierarchy: self.keychainZoneID]; // Make life easy for this test.
2414
2415 // Spin up CKKS subsystem.
2416 [self startCKKSSubsystem];
2417
2418 // Items should upload.
2419 [self expectCKModifyItemRecords: 1 currentKeyPointerRecords: 1 zoneID:self.keychainZoneID];
2420 [self addGenericPassword: @"data" account: @"account-delete-me"];
2421 OCMVerifyAllWithDelay(self.mockDatabase, 20);
2422
2423 [self waitForCKModifications];
2424
2425 OCMVerifyAllWithDelay(self.mockDatabase, 20);
2426
2427 // CKKS should not roll the keys while progressing back to 'ready', but it will fetch once
2428 self.silentFetchesAllowed = false;
2429 [self expectCKFetch];
2430
2431 [self.keychainView.stateMachine handleFlag:CKKSFlagFetchRequested];
2432
2433 XCTAssertEqual(0, [self.keychainView.keyHierarchyConditions[SecCKKSZoneKeyStateReady] wait:20*NSEC_PER_SEC], "Key state should have returned to ready");
2434 OCMVerifyAllWithDelay(self.mockDatabase, 20);
2435 }
2436
2437 - (void)testRecoverFromIncrementedCurrentKeyPointerEtag {
2438 // CloudKit sometimes reports the current key pointers have changed (etag mismatch), but their content hasn't.
2439 // In this case, CKKS shouldn't roll the TLK.
2440
2441 [self createAndSaveFakeKeyHierarchy: self.keychainZoneID]; // Make life easy for this test.
2442
2443 // Spin up CKKS subsystem.
2444 [self startCKKSSubsystem];
2445 XCTAssertEqual(0, [self.keychainView.keyHierarchyConditions[SecCKKSZoneKeyStateReady] wait:20*NSEC_PER_SEC], @"key state should enter 'ready'");
2446 [self.keychainView waitForFetchAndIncomingQueueProcessing]; // just to be sure it's fetched
2447
2448 // Items should upload.
2449 [self expectCKModifyItemRecords: 1 currentKeyPointerRecords: 1 zoneID:self.keychainZoneID];
2450 [self addGenericPassword: @"data" account: @"account-delete-me"];
2451 OCMVerifyAllWithDelay(self.mockDatabase, 20);
2452
2453 [self waitForCKModifications];
2454
2455 // Bump the etag on the class C current key record, but don't change any data
2456 CKRecordID* currentClassCID = [[CKRecordID alloc] initWithRecordName: @"classC" zoneID: self.keychainZoneID];
2457 CKRecord* currentClassC = self.keychainZone.currentDatabase[currentClassCID];
2458 XCTAssertNotNil(currentClassC, "Should have the class C current key pointer record");
2459
2460 [self.keychainZone addCKRecordToZone:[currentClassC copy]];
2461 XCTAssertNotEqualObjects(currentClassC.etag, self.keychainZone.currentDatabase[currentClassCID].etag, "Etag should have changed");
2462
2463 // Add another item. This write should fail, then CKKS should recover without rolling the key hierarchy or issuing a fetch.
2464 self.silentFetchesAllowed = false;
2465 [self expectCKAtomicModifyItemRecordsUpdateFailure:self.keychainZoneID];
2466 [self expectCKModifyItemRecords: 1 currentKeyPointerRecords: 1 zoneID:self.keychainZoneID];
2467 [self addGenericPassword: @"data" account: @"account-delete-me-2"];
2468 OCMVerifyAllWithDelay(self.mockDatabase, 20);
2469 }
2470
2471 - (void)testRecoverMultipleItemsFromIncrementedCurrentKeyPointerEtag {
2472 // CloudKit sometimes reports the current key pointers have changed (etag mismatch), but their content hasn't.
2473 // In this case, CKKS shouldn't roll the TLK.
2474 [self createAndSaveFakeKeyHierarchy: self.keychainZoneID]; // Make life easy for this test.
2475
2476 // Spin up CKKS subsystem.
2477 [self startCKKSSubsystem];
2478 XCTAssertEqual(0, [self.keychainView.keyHierarchyConditions[SecCKKSZoneKeyStateReady] wait:20*NSEC_PER_SEC], @"key state should enter 'ready'");
2479 [self.keychainView waitForFetchAndIncomingQueueProcessing]; // just to be sure it's fetched
2480
2481 // Items should upload.
2482 [self expectCKModifyItemRecords: 1 currentKeyPointerRecords: 1 zoneID:self.keychainZoneID];
2483 [self addGenericPassword: @"data" account: @"account-delete-me"];
2484 OCMVerifyAllWithDelay(self.mockDatabase, 20);
2485
2486 [self waitForCKModifications];
2487
2488 // Bump the etag on the class C current key record, but don't change any data
2489 CKRecordID* currentClassCID = [[CKRecordID alloc] initWithRecordName: @"classC" zoneID: self.keychainZoneID];
2490 CKRecord* currentClassC = self.keychainZone.currentDatabase[currentClassCID];
2491 XCTAssertNotNil(currentClassC, "Should have the class C current key pointer record");
2492
2493 [self.keychainZone addCKRecordToZone:[currentClassC copy]];
2494 XCTAssertNotEqualObjects(currentClassC.etag, self.keychainZone.currentDatabase[currentClassCID].etag, "Etag should have changed");
2495
2496 // Add another item. This write should fail, then CKKS should recover without rolling the key hierarchy or issuing a fetch.
2497 [self.keychainView waitForOperationsOfClass:[CKKSOutgoingQueueOperation class]];
2498 self.keychainView.holdOutgoingQueueOperation = [CKKSGroupOperation named:@"outgoing-hold" withBlock: ^{
2499 ckksnotice_global("ckks", "releasing outgoing-queue hold");
2500 }];
2501
2502 self.silentFetchesAllowed = false;
2503 [self expectCKAtomicModifyItemRecordsUpdateFailure:self.keychainZoneID];
2504 [self expectCKModifyItemRecords:2 currentKeyPointerRecords: 1 zoneID:self.keychainZoneID];
2505 [self addGenericPassword: @"data" account: @"account-delete-me-2"];
2506 [self addGenericPassword: @"data" account: @"account-delete-me-3"];
2507
2508 [self.operationQueue addOperation: self.keychainView.holdOutgoingQueueOperation];
2509 OCMVerifyAllWithDelay(self.mockDatabase, 20);
2510 }
2511
2512 - (void)testOnboardOldItemsCreatingKeyHierarchy {
2513 // In this test, we'll check if the CKKS subsystem will pick up a keychain item which existed before the key hierarchy, both with and without a UUID attached at item creation
2514
2515 // Test starts with nothing in CloudKit, and CKKS blocked. Add one item without a UUID...
2516
2517 SecCKKSTestSetDisableAutomaticUUID(true);
2518 [self addGenericPassword: @"data" account: @"account-delete-me-no-UUID" expecting:errSecSuccess message: @"Add item (no UUID) to keychain"];
2519
2520 // and an item with a UUID...
2521 SecCKKSTestSetDisableAutomaticUUID(false);
2522 [self addGenericPassword: @"data" account: @"account-delete-me-with-UUID" expecting:errSecSuccess message: @"Add item (w/ UUID) to keychain"];
2523
2524 // We then expect an upload of the added items
2525 [self expectCKModifyItemRecords: 2 currentKeyPointerRecords: 1 zoneID:self.keychainZoneID];
2526
2527 [self startCKKSSubsystem];
2528 [self performOctagonTLKUpload:self.ckksViews];
2529
2530 OCMVerifyAllWithDelay(self.mockDatabase, 20);
2531 }
2532
2533 - (void)testOnboardOldItemsWithExistingKeyHierarchy {
2534 [self createAndSaveFakeKeyHierarchy: self.keychainZoneID]; // Make life easy for this test.
2535
2536 [self expectCKModifyItemRecords: 1 currentKeyPointerRecords: 1 zoneID:self.keychainZoneID];
2537 [self addGenericPassword: @"data" account: @"account-delete-me"];
2538
2539 [self startCKKSSubsystem];
2540 OCMVerifyAllWithDelay(self.mockDatabase, 20);
2541 }
2542
2543 - (void)testOnboardOldItemsWithExistingKeyHierarchyExtantTLK {
2544 // Test starts key hierarchy in our fake CloudKit, the TLK arrived in the local keychain, and CKKS blocked.
2545 [self putFakeKeyHierarchyInCloudKit:self.keychainZoneID];
2546 [self expectCKKSTLKSelfShareUpload:self.keychainZoneID];
2547 [self saveTLKMaterialToKeychain:self.keychainZoneID];
2548
2549 // Add one item without a UUID...
2550 SecCKKSTestSetDisableAutomaticUUID(true);
2551 [self addGenericPassword: @"data" account: @"account-delete-me-no-UUID" expecting:errSecSuccess message: @"Add item (no UUID) to keychain"];
2552
2553 // and an item with a UUID...
2554 SecCKKSTestSetDisableAutomaticUUID(false);
2555 [self addGenericPassword: @"data" account: @"account-delete-me-with-UUID" expecting:errSecSuccess message: @"Add item (w/ UUID) to keychain"];
2556
2557 // Now that we have an item in the keychain, allow CKKS to spin up. It should upload the item, using the key hierarchy already extant
2558 // We expect a single record to be uploaded.
2559 [self expectCKModifyItemRecords: 2 currentKeyPointerRecords: 1 zoneID:self.keychainZoneID checkItem: [self checkClassCBlock:self.keychainZoneID message:@"Object was encrypted under class C key in hierarchy"]];
2560
2561 // Spin up CKKS subsystem.
2562 [self startCKKSSubsystem];
2563
2564 OCMVerifyAllWithDelay(self.mockDatabase, 20);
2565 }
2566
2567 - (void)testOnboardOldItemsWithExistingKeyHierarchyLateTLK {
2568 // Test starts key hierarchy in our fake CloudKit, and CKKS blocked.
2569 [self putFakeKeyHierarchyInCloudKit:self.keychainZoneID];
2570 [self putFakeDeviceStatusInCloudKit:self.keychainZoneID];
2571 self.keychainZone.flag = true;
2572
2573 // Add one item without a UUID...
2574 SecCKKSTestSetDisableAutomaticUUID(true);
2575 [self addGenericPassword: @"data" account: @"account-delete-me-no-UUID" expecting:errSecSuccess message: @"Add item (no UUID) to keychain"];
2576
2577 // and an item with a UUID...
2578 SecCKKSTestSetDisableAutomaticUUID(false);
2579 [self addGenericPassword: @"data" account: @"account-delete-me-with-UUID" expecting:errSecSuccess message: @"Add item (w/ UUID) to keychain"];
2580
2581 // Now that we have an item in the keychain, allow CKKS to spin up. It should upload the item, using the key hierarchy already extant
2582
2583 // Spin up CKKS subsystem.
2584 [self startCKKSSubsystem];
2585 XCTAssertEqual(0, [self.keychainView.keyHierarchyConditions[SecCKKSZoneKeyStateWaitForTLK] wait:20*NSEC_PER_SEC], "Key state should have become waitfortlk");
2586
2587 // Now, save the TLK to the keychain (to simulate it coming in via SOS).
2588 [self expectCKKSTLKSelfShareUpload:self.keychainZoneID];
2589 [self saveTLKMaterialToKeychainSimulatingSOS:self.keychainZoneID];
2590
2591 // We expect a single record to be uploaded.
2592 [self expectCKModifyItemRecords: 2 currentKeyPointerRecords: 1 zoneID:self.keychainZoneID checkItem: [self checkClassCBlock:self.keychainZoneID message:@"Object was encrypted under class C key in hierarchy"]];
2593
2594 OCMVerifyAllWithDelay(self.mockDatabase, 20);
2595 XCTAssertTrue(self.keychainZone.flag, "Keychain zone shouldn't have been reset");
2596 }
2597
2598 - (void)testOnboardOldItemMatchingExistingCKKSItem {
2599 [self createAndSaveFakeKeyHierarchy: self.keychainZoneID];
2600
2601 NSString* itemAccount = @"account-delete-me";
2602 [self addGenericPassword:@"password" account:itemAccount];
2603
2604 CKRecord* ckr = [self createFakeRecord:self.keychainZoneID recordName:@"7B598D31-F9C5-481E-98AC-5A507ACB2D85"];
2605 [self.keychainZone addToZone:ckr];
2606
2607 [self startCKKSSubsystem];
2608
2609 XCTAssertEqual(0, [self.keychainView.keyHierarchyConditions[SecCKKSZoneKeyStateReady] wait:20*NSEC_PER_SEC], "Key state should have become ready");
2610
2611 [self.keychainView waitForOperationsOfClass:[CKKSOutgoingQueueOperation class]];
2612 [self.keychainView waitForOperationsOfClass:[CKKSIncomingQueueOperation class]];
2613
2614 [self findGenericPassword:itemAccount expecting:errSecSuccess];
2615
2616 // And, the local item should now match the UUID downloaded from CKKS
2617 [self checkGenericPasswordStoredUUID:@"7B598D31-F9C5-481E-98AC-5A507ACB2D85" account:itemAccount];
2618 }
2619
2620 - (void)testResync {
2621 // We need to set up a desynced situation to test our resync.
2622 // First, let CKKS start up and send several items to CloudKit (that we'll then desync!)
2623 __block NSError* error = nil;
2624
2625 // Test starts with keys in CloudKit (so we can create items later)
2626 [self putFakeKeyHierarchyInCloudKit:self.keychainZoneID];
2627 [self expectCKKSTLKSelfShareUpload:self.keychainZoneID];
2628 [self saveTLKMaterialToKeychain:self.keychainZoneID];
2629
2630 [self addGenericPassword: @"data" account: @"first"];
2631 [self addGenericPassword: @"data" account: @"second"];
2632 [self addGenericPassword: @"data" account: @"third"];
2633 [self addGenericPassword: @"data" account: @"fourth"];
2634 [self addGenericPassword: @"data" account: @"fifth"];
2635 [self addGenericPassword: @"data" account: @"sixth"];
2636 NSUInteger passwordCount = 6u;
2637
2638 [self checkGenericPassword: @"data" account: @"first"];
2639 [self checkGenericPassword: @"data" account: @"second"];
2640 [self checkGenericPassword: @"data" account: @"third"];
2641 [self checkGenericPassword: @"data" account: @"fourth"];
2642 [self checkGenericPassword: @"data" account: @"fifth"];
2643 [self checkGenericPassword: @"data" account: @"sixth"];
2644
2645 [self expectCKModifyItemRecords: passwordCount currentKeyPointerRecords: 1 zoneID:self.keychainZoneID];
2646
2647 [self startCKKSSubsystem];
2648
2649 // Wait for uploads to happen
2650 OCMVerifyAllWithDelay(self.mockDatabase, 20);
2651 [self waitForCKModifications];
2652 // One TLK share record
2653 XCTAssertEqual(self.keychainZone.currentDatabase.count, SYSTEM_DB_RECORD_COUNT+passwordCount+1, "Have SYSTEM_DB_RECORD_COUNT+passwordCount+1 objects in cloudkit");
2654
2655 // Now, corrupt away!
2656 // Extract all passwordCount items for Corruption
2657 NSArray<CKRecord*>* items = [self.keychainZone.currentDatabase.allValues filteredArrayUsingPredicate: [NSPredicate predicateWithFormat:@"self.recordType like %@", SecCKRecordItemType]];
2658 XCTAssertEqual(items.count, passwordCount, "Have %lu Items in cloudkit", (unsigned long)passwordCount);
2659
2660 // For the first record, delete all traces of it from CKKS. But! it remains in local keychain.
2661 // Expected outcome: CKKS resyncs; item exists again.
2662 CKRecord* delete = items[0];
2663 NSString* deleteAccount = [[self decryptRecord: delete] objectForKey: (__bridge id) kSecAttrAccount];
2664 XCTAssertNotNil(deleteAccount, "received an account for the local delete object");
2665
2666 __weak __typeof(self) weakSelf = self;
2667 [self.keychainView dispatchSyncWithSQLTransaction:^CKKSDatabaseTransactionResult{
2668 __strong __typeof(weakSelf) strongSelf = weakSelf;
2669 XCTAssertNotNil(strongSelf, "self exists");
2670
2671 CKKSMirrorEntry* ckme = [CKKSMirrorEntry tryFromDatabase:delete.recordID.recordName zoneID:strongSelf.keychainZoneID error:&error];
2672 if(ckme) {
2673 [ckme deleteFromDatabase: &error];
2674 }
2675 XCTAssertNil(error, "no error removing CKME");
2676 CKKSOutgoingQueueEntry* oqe = [CKKSOutgoingQueueEntry tryFromDatabase:delete.recordID.recordName zoneID:strongSelf.keychainZoneID error:&error];
2677 if(oqe) {
2678 [oqe deleteFromDatabase: &error];
2679 }
2680 XCTAssertNil(error, "no error removing OQE");
2681 CKKSIncomingQueueEntry* iqe = [CKKSIncomingQueueEntry tryFromDatabase:delete.recordID.recordName zoneID:strongSelf.keychainZoneID error:&error];
2682 if(iqe) {
2683 [iqe deleteFromDatabase: &error];
2684 }
2685 XCTAssertNil(error, "no error removing IQE");
2686 return CKKSDatabaseTransactionCommit;
2687 }];
2688
2689 // For the second record, delete all traces of it from CloudKit.
2690 // Expected outcome: deleted locally
2691 CKRecord* remoteDelete = items[1];
2692 NSString* remoteDeleteAccount = [[self decryptRecord: remoteDelete] objectForKey: (__bridge id) kSecAttrAccount];
2693 XCTAssertNotNil(remoteDeleteAccount, "received an account for the remote delete object");
2694
2695 [self.keychainZone deleteCKRecordIDFromZone: remoteDelete.recordID];
2696 for(NSMutableDictionary<CKRecordID*, CKRecord*>* database in self.keychainZone.pastDatabases.allValues) {
2697 [database removeObjectForKey: remoteDelete.recordID];
2698 }
2699
2700 // The third record gets modified in CloudKit, but not locally.
2701 // Expected outcome: use the CloudKit version
2702 CKRecord* remoteDataChanged = items[2];
2703 NSMutableDictionary* remoteDataDictionary = [[self decryptRecord: remoteDataChanged] mutableCopy];
2704 NSString* remoteDataChangedAccount = [remoteDataDictionary objectForKey: (__bridge id) kSecAttrAccount];
2705 XCTAssertNotNil(remoteDataChangedAccount, "Received an account for the remote-data-changed object");
2706 remoteDataDictionary[(__bridge id) kSecValueData] = [@"CloudKitWins" dataUsingEncoding: NSUTF8StringEncoding];
2707
2708 CKRecord* newData = [self newRecord: remoteDataChanged.recordID withNewItemData: remoteDataDictionary];
2709 [self.keychainZone addToZone: newData];
2710 for(NSMutableDictionary<CKRecordID*, CKRecord*>* database in self.keychainZone.pastDatabases.allValues) {
2711 database[remoteDataChanged.recordID] = newData;
2712 }
2713
2714 // The fourth record stays in-sync. Good work, everyone!
2715 // Expected outcome: stays in-sync
2716 NSString* insyncAccount = [[self decryptRecord: items[3]] objectForKey: (__bridge id) kSecAttrAccount];
2717 XCTAssertNotNil(insyncAccount, "Received an account for the in-sync object");
2718
2719 // The fifth record is updated locally, but CKKS didn't get the notification, and so the local CKMirror and CloudKit don't have it
2720 // Expected outcome: local change should be steamrolled by the cloud version
2721 CKRecord* localDataChanged = items[4];
2722 NSMutableDictionary* localDataDictionary = [[self decryptRecord: localDataChanged] mutableCopy];
2723 NSString* localDataChangedAccount = [localDataDictionary objectForKey: (__bridge id) kSecAttrAccount];
2724 SecCKKSDisable();
2725 [self updateGenericPassword:@"newpassword" account:localDataChangedAccount];
2726 [self checkGenericPassword:@"newpassword" account:localDataChangedAccount];
2727 SecCKKSEnable();
2728
2729 // The sixth record matches what's in CloudKit, but the local UUID has changed (and CKKS didn't notice, for whatever reason)
2730 CKRecord* uuidMismatch = items[5];
2731 NSMutableDictionary* uuidMisMatchDictionary = [[self decryptRecord:uuidMismatch] mutableCopy];
2732 NSString* uuidMismatchAccount = uuidMisMatchDictionary[(__bridge id)kSecAttrAccount];
2733 NSString* newUUID = @"55463F83-3AAE-462D-B95F-2FA6AD088980";
2734 SecCKKSDisable();
2735 [self setGenericPasswordStoredUUID:newUUID account:uuidMismatchAccount];
2736 [self checkGenericPasswordStoredUUID:newUUID account:uuidMismatchAccount];
2737 SecCKKSEnable();
2738
2739 // To make this more challenging, CK returns the refetch in multiple batches. This shouldn't affect the resync...
2740 CKServerChangeToken* ck1 = self.keychainZone.currentChangeToken;
2741 self.silentFetchesAllowed = false;
2742 [self expectCKFetch];
2743 [self expectCKFetchWithFilter:^BOOL(FakeCKFetchRecordZoneChangesOperation * _Nonnull frzco) {
2744 // Assert that the fetch is happening with the change token we paused at before
2745 CKServerChangeToken* changeToken = frzco.configurationsByRecordZoneID[self.keychainZoneID].previousServerChangeToken;
2746 if(changeToken && [changeToken isEqual:ck1]) {
2747 return YES;
2748 } else {
2749 return NO;
2750 }
2751 } runBeforeFinished:^{}];
2752
2753 self.keychainZone.limitFetchTo = ck1;
2754 self.keychainZone.limitFetchError = [[CKPrettyError alloc] initWithDomain:CKErrorDomain code:CKErrorNetworkFailure userInfo:@{CKErrorRetryAfterKey : [NSNumber numberWithInt:4]}];
2755
2756 // The seventh record gets magically added to CloudKit, but CKKS has never heard of it
2757 // (emulates a lost record on the client, but that CloudKit already believes it's sent the record for)
2758 // Expected outcome: added to local keychain
2759 NSString* remoteOnlyAccount = @"remote-only";
2760 CKRecord* ckr = [self createFakeRecord:self.keychainZoneID recordName:@"7B598D31-F9C5-481E-98AC-5A507ACB2D85" withAccount: remoteOnlyAccount];
2761 [self.keychainZone addToZone: ckr];
2762 for(NSMutableDictionary<CKRecordID*, CKRecord*>* database in self.keychainZone.pastDatabases.allValues) {
2763 database[ckr.recordID] = ckr;
2764 }
2765
2766 ckksnotice("ckksresync", self.keychainView, "local delete: %@ %@", delete.recordID.recordName, deleteAccount);
2767 ckksnotice("ckksresync", self.keychainView, "Remote deletion: %@ %@", remoteDelete.recordID.recordName, remoteDeleteAccount);
2768 ckksnotice("ckksresync", self.keychainView, "Remote data changed: %@ %@", remoteDataChanged.recordID.recordName, remoteDataChangedAccount);
2769 ckksnotice("ckksresync", self.keychainView, "in-sync: %@ %@", items[3].recordID.recordName, insyncAccount);
2770 ckksnotice("ckksresync", self.keychainView, "local update: %@ %@", items[4].recordID.recordName, localDataChangedAccount);
2771 ckksnotice("ckksresync", self.keychainView, "uuid mismatch: %@ %@", items[5].recordID.recordName, uuidMismatchAccount);
2772 ckksnotice("ckksresync", self.keychainView, "Remote only: %@ %@", ckr.recordID.recordName, remoteOnlyAccount);
2773
2774 CKKSSynchronizeOperation* resyncOperation = [self.keychainView resyncWithCloud];
2775 [resyncOperation waitUntilFinished];
2776
2777 XCTAssertNil(resyncOperation.error, "No error during the resync operation");
2778
2779 OCMVerifyAllWithDelay(self.mockDatabase, 20);
2780
2781 // Now do some checking. Remember, we don't know which record we corrupted, so use the parsed account variables to check.
2782
2783 [self findGenericPassword: deleteAccount expecting: errSecSuccess];
2784 [self findGenericPassword: remoteDeleteAccount expecting: errSecItemNotFound];
2785 [self findGenericPassword: remoteDataChangedAccount expecting: errSecSuccess];
2786 [self findGenericPassword: insyncAccount expecting: errSecSuccess];
2787 [self findGenericPassword: localDataChangedAccount expecting: errSecSuccess];
2788 [self findGenericPassword: uuidMismatchAccount expecting: errSecSuccess];
2789 [self findGenericPassword: remoteOnlyAccount expecting: errSecSuccess];
2790
2791 [self checkGenericPassword: @"data" account: deleteAccount];
2792 //[self checkGenericPassword: @"data" account: remoteDeleteAccount];
2793 [self checkGenericPassword: @"CloudKitWins" account: remoteDataChangedAccount];
2794 [self checkGenericPassword: @"data" account: insyncAccount];
2795 [self checkGenericPassword:@"data" account:localDataChangedAccount];
2796 [self checkGenericPassword:@"data" account:uuidMismatchAccount];
2797 [self checkGenericPassword: @"data" account: remoteOnlyAccount];
2798
2799 [self checkGenericPasswordStoredUUID:uuidMismatch.recordID.recordName account:uuidMismatchAccount];
2800
2801 [self.keychainView dispatchSyncWithReadOnlySQLTransaction:^{
2802 __strong __typeof(weakSelf) strongSelf = weakSelf;
2803 XCTAssertNotNil(strongSelf, "self exists");
2804
2805 CKKSMirrorEntry* ckme = nil;
2806
2807 ckme = [CKKSMirrorEntry tryFromDatabase:delete.recordID.recordName zoneID:strongSelf.keychainZoneID error:&error];
2808 XCTAssertNil(error);
2809 XCTAssertNotNil(ckme);
2810
2811 ckme = [CKKSMirrorEntry tryFromDatabase:remoteDelete.recordID.recordName zoneID:strongSelf.keychainZoneID error:&error];
2812 XCTAssertNil(error);
2813 XCTAssertNil(ckme); // deleted!
2814
2815 ckme = [CKKSMirrorEntry tryFromDatabase:remoteDataChanged.recordID.recordName zoneID:strongSelf.keychainZoneID error:&error];
2816 XCTAssertNil(error);
2817 XCTAssertNotNil(ckme);
2818
2819 ckme = [CKKSMirrorEntry tryFromDatabase:items[3].recordID.recordName zoneID:strongSelf.keychainZoneID error:&error];
2820 XCTAssertNil(error);
2821 XCTAssertNotNil(ckme);
2822
2823 ckme = [CKKSMirrorEntry tryFromDatabase:items[4].recordID.recordName zoneID:strongSelf.keychainZoneID error:&error];
2824 XCTAssertNil(error);
2825 XCTAssertNotNil(ckme);
2826
2827 ckme = [CKKSMirrorEntry tryFromDatabase:ckr.recordID.recordName zoneID:strongSelf.keychainZoneID error:&error];
2828 XCTAssertNil(error);
2829 XCTAssertNotNil(ckme);
2830 }];
2831 }
2832
2833 - (void)testResyncItemsMissingFromLocalKeychain {
2834 [self createAndSaveFakeKeyHierarchy: self.keychainZoneID]; // Make life easy for this test.
2835
2836 // We want:
2837 // one password correctly synced between local keychain and CloudKit
2838 // one password incorrectly disappeared from local keychain, but in mirror table
2839 // one password sitting in the outgoing queue
2840 // one password sitting in the incoming queue
2841
2842 // Add and sync two passwords
2843 [self addGenericPassword: @"data" account: @"first"];
2844 [self addGenericPassword: @"data" account: @"second"];
2845
2846 [self checkGenericPassword: @"data" account: @"first"];
2847 [self checkGenericPassword: @"data" account: @"second"];
2848
2849 [self expectCKModifyItemRecords:2 currentKeyPointerRecords:1 zoneID:self.keychainZoneID];
2850 [self startCKKSSubsystem];
2851 OCMVerifyAllWithDelay(self.mockDatabase, 20);
2852 [self waitForCKModifications];
2853
2854 [self.injectedManager.zoneChangeFetcher notifyZoneChange:nil];
2855 [self.keychainView waitForFetchAndIncomingQueueProcessing];
2856
2857 // Now, place an item in the outgoing queue
2858
2859 //[self addGenericPassword: @"data" account: @"third"];
2860 //[self checkGenericPassword: @"data" account: @"third"];
2861
2862 // Now, corrupt away!
2863 // Extract all passwordCount items for Corruption
2864 NSArray<CKRecord*>* items = [self.keychainZone.currentDatabase.allValues filteredArrayUsingPredicate: [NSPredicate predicateWithFormat:@"self.recordType like %@", SecCKRecordItemType]];
2865 XCTAssertEqual(items.count, 2u, "Have %lu Items in cloudkit", (unsigned long)2u);
2866
2867 // For the first record, surreptitiously remove from local keychain
2868 CKRecord* remove = items[0];
2869 NSString* removeAccount = [[self decryptRecord:remove] objectForKey:(__bridge id)kSecAttrAccount];
2870 XCTAssertNotNil(removeAccount, "received an account for the local delete object");
2871
2872 NSURL* kcpath = (__bridge_transfer NSURL*)SecCopyURLForFileInKeychainDirectory((__bridge CFStringRef)@"keychain-2-debug.db");
2873 sqlite3* db;
2874 sqlite3_open([[kcpath path] UTF8String], &db);
2875 NSString* query = [NSString stringWithFormat:@"DELETE FROM genp WHERE uuid=\"%@\"", remove.recordID.recordName];
2876 char* sqlerror = NULL;
2877 XCTAssertEqual(SQLITE_OK, sqlite3_exec(db, [query UTF8String], NULL, NULL, &sqlerror), "SQL deletion shouldn't error");
2878 XCTAssertTrue(sqlerror == NULL, "No error string should have been returned: %s", sqlerror);
2879 if(sqlerror) {
2880 sqlite3_free(sqlerror);
2881 sqlerror = NULL;
2882 }
2883 sqlite3_close(db);
2884
2885 // The second record is kept in-sync
2886
2887 // Now, add an in-flight change (for record 3)
2888 [self holdCloudKitModifications];
2889 [self expectCKModifyItemRecords:1 currentKeyPointerRecords:1 zoneID:self.keychainZoneID];
2890 [self addGenericPassword:@"data" account:@"third"];
2891 OCMVerifyAllWithDelay(self.mockDatabase, 20);
2892
2893 // For the fourth, add a new record but prevent incoming queue processing
2894 self.keychainView.holdIncomingQueueOperation = [CKKSResultOperation named:@"hold-incoming" withBlock:^{}];
2895
2896 CKRecord* ckr = [self createFakeRecord: self.keychainZoneID recordName:@"7B598D31-F9C5-481E-98AC-5A507ACB2D85" withAccount:@"fourth"];
2897 [self.keychainZone addToZone:ckr];
2898 [self.injectedManager.zoneChangeFetcher notifyZoneChange:nil];
2899
2900 // Now, where are we....
2901 CKKSScanLocalItemsOperation* scanLocal = [self.keychainView scanLocalItems:@"test-scan"];
2902 // This operation will wait for the CKKSOutgoingQueue operation (currently held writing to cloudkit) to finish before beginning
2903
2904 // Allow everything to proceed
2905 [self releaseCloudKitModificationHold];
2906
2907 [scanLocal waitUntilFinished];
2908 XCTAssertEqual(scanLocal.missingLocalItemsFound, 1u, "Should have found one missing item");
2909
2910 [self.operationQueue addOperation:self.keychainView.holdIncomingQueueOperation];
2911
2912 OCMVerifyAllWithDelay(self.mockDatabase, 20);
2913
2914 [self.keychainView waitForFetchAndIncomingQueueProcessing];
2915 [self.keychainView waitForOperationsOfClass:[CKKSIncomingQueueOperation class]];
2916
2917 // And ensure that all four items are present again
2918 [self findGenericPassword: @"first" expecting: errSecSuccess];
2919 [self findGenericPassword: @"second" expecting: errSecSuccess];
2920 [self findGenericPassword: @"third" expecting: errSecSuccess];
2921 [self findGenericPassword: @"fourth" expecting: errSecSuccess];
2922 }
2923
2924 - (void)testScanItemsChangedInLocalKeychain {
2925 [self createAndSaveFakeKeyHierarchy: self.keychainZoneID]; // Make life easy for this test.
2926
2927 // Add and sync two passwords
2928 NSString* itemAccount = @"first";
2929 [self addGenericPassword:@"data" account:itemAccount];
2930 [self addGenericPassword:@"data" account:@"second"];
2931
2932 [self checkGenericPassword:@"data" account:itemAccount];
2933 [self checkGenericPassword:@"data" account:@"second"];
2934
2935 [self expectCKModifyItemRecords:2 currentKeyPointerRecords:1 zoneID:self.keychainZoneID];
2936 [self startCKKSSubsystem];
2937 OCMVerifyAllWithDelay(self.mockDatabase, 20);
2938 [self waitForCKModifications];
2939
2940 [self.injectedManager.zoneChangeFetcher notifyZoneChange:nil];
2941 [self.keychainView waitForFetchAndIncomingQueueProcessing];
2942
2943 [self.keychainView waitForOperationsOfClass:[CKKSScanLocalItemsOperation class]];
2944 [self.keychainView waitForOperationsOfClass:[CKKSOutgoingQueueOperation class]];
2945
2946 // Now, have CKKS miss an update
2947 SecCKKSDisable();
2948 [self updateGenericPassword:@"newpassword" account:itemAccount];
2949 [self checkGenericPassword:@"newpassword" account:itemAccount];
2950 SecCKKSEnable();
2951
2952 // Now, where are we....
2953 [self expectCKModifyItemRecords:1 currentKeyPointerRecords:1 zoneID:self.keychainZoneID
2954 checkItem:[self checkPasswordBlock:self.keychainZoneID account:itemAccount password:@"newpassword"]];
2955
2956 CKKSScanLocalItemsOperation* scanLocal = [self.keychainView scanLocalItems:@"test-scan"];
2957 [scanLocal waitUntilFinished];
2958
2959 XCTAssertEqual(scanLocal.recordsAdded, 1u, "Should have added a single record");
2960
2961 OCMVerifyAllWithDelay(self.mockDatabase, 20);
2962
2963 // And ensure that all four items are present again
2964 [self findGenericPassword: @"first" expecting: errSecSuccess];
2965 [self findGenericPassword: @"second" expecting: errSecSuccess];
2966 }
2967
2968 - (void)testEnsureScanOccursOnNextStartIfCancelled {
2969 // We want to set up a situation where a CKKSScanLocalItemsOperation is cancelled by daemon quitting.
2970 NSString* itemAccount = @"first";
2971 [self addGenericPassword:@"data" account:itemAccount];
2972 [self addGenericPassword:@"data" account:@"second"];
2973
2974 // We're going to pretend that the scan doesn't happen due to daemon restart
2975 SecCKKSSetTestSkipScan(true);
2976
2977 [self createAndSaveFakeKeyHierarchy:self.keychainZoneID]; // Make life easy for this test.
2978
2979 [self startCKKSSubsystem];
2980 OCMVerifyAllWithDelay(self.mockDatabase, 20);
2981 [self waitForCKModifications];
2982
2983 XCTAssertEqual(0, [self.keychainView.keyHierarchyConditions[SecCKKSZoneKeyStateReady] wait:20*NSEC_PER_SEC], @"Key state should arrive at ready");
2984
2985 [self.keychainView waitForOperationsOfClass:[CKKSScanLocalItemsOperation class]];
2986 [self.keychainView waitForOperationsOfClass:[CKKSOutgoingQueueOperation class]];
2987
2988 // CKKS should perform normally if new items are added
2989 [self expectCKModifyItemRecords:1 currentKeyPointerRecords:1 zoneID:self.keychainZoneID];
2990 [self addGenericPassword:@"found" account:@"after-setup"];
2991 OCMVerifyAllWithDelay(self.mockDatabase, 20);
2992
2993 // Now, simulate a restart
2994 SecCKKSSetTestSkipScan(false);
2995
2996 [self expectCKModifyItemRecords:2 currentKeyPointerRecords:1 zoneID:self.keychainZoneID];
2997
2998 [self.keychainView halt];
2999 self.keychainView = [[CKKSViewManager manager] restartZone:self.keychainZoneID.zoneName];
3000 [self.keychainView beginCloudKitOperation];
3001 [self beginSOSTrustedViewOperation:self.keychainView];
3002
3003 OCMVerifyAllWithDelay(self.mockDatabase, 20);
3004
3005 [self findGenericPassword:@"first" expecting:errSecSuccess];
3006 [self findGenericPassword:@"second" expecting:errSecSuccess];
3007 }
3008
3009 - (void)testResyncLocal {
3010 [self putFakeKeyHierarchyInCloudKit:self.keychainZoneID];
3011 [self expectCKKSTLKSelfShareUpload:self.keychainZoneID];
3012 [self saveTLKMaterialToKeychain:self.keychainZoneID];
3013
3014 [self addGenericPassword: @"data" account: @"first"];
3015 [self addGenericPassword: @"data" account: @"second"];
3016 NSUInteger passwordCount = 2u;
3017
3018 [self expectCKModifyItemRecords: passwordCount currentKeyPointerRecords: 1 zoneID:self.keychainZoneID];
3019 [self startCKKSSubsystem];
3020
3021 // Wait for uploads to happen
3022 OCMVerifyAllWithDelay(self.mockDatabase, 20);
3023 [self waitForCKModifications];
3024
3025 // Local resyncs shouldn't fetch clouds.
3026 self.silentFetchesAllowed = false;
3027 SecCKKSDisable();
3028 [self deleteGenericPassword:@"first"];
3029 [self deleteGenericPassword:@"second"];
3030 SecCKKSEnable();
3031
3032 // And they're gone!
3033 [self findGenericPassword:@"first" expecting:errSecItemNotFound];
3034 [self findGenericPassword:@"second" expecting:errSecItemNotFound];
3035
3036 CKKSLocalSynchronizeOperation* op = [self.keychainView resyncLocal];
3037 [op waitUntilFinished];
3038 XCTAssertNil(op.error, "Shouldn't be an error resyncing locally");
3039
3040 // And they're back!
3041 [self checkGenericPassword: @"data" account: @"first"];
3042 [self checkGenericPassword: @"data" account: @"second"];
3043 }
3044
3045 - (void)testPlistRestoreResyncsLocal {
3046 [self putFakeKeyHierarchyInCloudKit:self.keychainZoneID];
3047 [self expectCKKSTLKSelfShareUpload:self.keychainZoneID];
3048 [self saveTLKMaterialToKeychain:self.keychainZoneID];
3049
3050 [self addGenericPassword: @"data" account: @"first"];
3051 [self addGenericPassword: @"data" account: @"second"];
3052 NSUInteger passwordCount = 2u;
3053
3054 [self checkGenericPassword: @"data" account: @"first"];
3055 [self checkGenericPassword: @"data" account: @"second"];
3056
3057 [self expectCKModifyItemRecords:passwordCount currentKeyPointerRecords:1 zoneID:self.keychainZoneID];
3058 [self startCKKSSubsystem];
3059
3060 // Wait for uploads to happen
3061 OCMVerifyAllWithDelay(self.mockDatabase, 20);
3062 [self waitForCKModifications];
3063
3064 // o no
3065 // This 'restores' a plist keychain backup
3066 // That will kick off a local resync in CKKS, so hold that until we're ready...
3067 self.keychainView.holdLocalSynchronizeOperation = [CKKSResultOperation named:@"hold-local-synchronize" withBlock:^{}];
3068
3069 // Local resyncs shouldn't fetch clouds.
3070 self.silentFetchesAllowed = false;
3071
3072 CFErrorRef cferror = NULL;
3073 kc_with_dbt(true, &cferror, ^bool (SecDbConnectionRef dbt) {
3074 CFErrorRef cfcferror = NULL;
3075
3076 bool ret = SecServerImportKeychainInPlist(dbt, SecSecurityClientGet(), KEYBAG_NONE, KEYBAG_NONE,
3077 (__bridge CFDictionaryRef)@{}, kSecBackupableItemFilter, false, &cfcferror);
3078
3079 XCTAssertNil(CFBridgingRelease(cfcferror), "Shouldn't error importing a 'backup'");
3080 XCTAssert(ret, "Importing a 'backup' should have succeeded");
3081 return true;
3082 });
3083 XCTAssertNil(CFBridgingRelease(cferror), "Shouldn't error mucking about in the db");
3084
3085 // Restore is additive so original items stick around
3086 [self findGenericPassword:@"first" expecting:errSecSuccess];
3087 [self findGenericPassword:@"second" expecting:errSecSuccess];
3088
3089 // Allow the local resync to continue...
3090 [self.operationQueue addOperation:self.keychainView.holdLocalSynchronizeOperation];
3091 [self.keychainView waitForOperationsOfClass:[CKKSLocalSynchronizeOperation class]];
3092
3093 // Items are still here!
3094 [self checkGenericPassword: @"data" account: @"first"];
3095 [self checkGenericPassword: @"data" account: @"second"];
3096 }
3097
3098 - (void)testRestartWithoutRefetch {
3099 // Restarting the CKKS operation should check that it's been 15 minutes since the last fetch before it fetches again. Simulate this.
3100 [self startCKKSSubsystem];
3101 [self performOctagonTLKUpload:self.ckksViews];
3102
3103 [self waitForCKModifications];
3104 OCMVerifyAllWithDelay(self.mockDatabase, 20);
3105
3106 XCTAssertEqual(0, [self.keychainView.keyHierarchyConditions[SecCKKSZoneKeyStateReady] wait:20*NSEC_PER_SEC], @"Key state should arrive at ready");
3107
3108 [self.keychainView waitForOperationsOfClass:[CKKSScanLocalItemsOperation class]];
3109 [self.keychainView waitForOperationsOfClass:[CKKSOutgoingQueueOperation class]];
3110
3111 // Tear down the CKKS object and disallow fetches
3112 [self.keychainView halt];
3113 self.silentFetchesAllowed = false;
3114
3115 self.keychainView = [[CKKSViewManager manager] restartZone: self.keychainZoneID.zoneName];
3116 [self beginSOSTrustedViewOperation:self.keychainView];
3117 XCTAssertEqual(0, [self.keychainView.keyHierarchyConditions[SecCKKSZoneKeyStateReady] wait:20*NSEC_PER_SEC], @"Key state should arrive at ready");
3118 OCMVerifyAllWithDelay(self.mockDatabase, 20);
3119
3120 XCTAssertFalse(self.keychainView.initiatedLocalScan, "Should not have initiated a local items scan due to a restart with a recent fetch");
3121
3122 // Okay, cool, rad, now let's set the date to be very long ago and check that there's positively a fetch
3123 [self.keychainView halt];
3124 self.silentFetchesAllowed = false;
3125
3126 [self.keychainView dispatchSyncWithSQLTransaction:^CKKSDatabaseTransactionResult{
3127 NSError* error = nil;
3128 CKKSZoneStateEntry* ckse = [CKKSZoneStateEntry fromDatabase:self.keychainZoneID.zoneName error:&error];
3129
3130 XCTAssertNil(error, "no error pulling ckse from database");
3131 XCTAssertNotNil(ckse, "received a ckse");
3132
3133 ckse.lastFetchTime = [NSDate distantPast];
3134 [ckse saveToDatabase: &error];
3135 XCTAssertNil(error, "no error saving to database");
3136 return CKKSDatabaseTransactionCommit;
3137 }];
3138
3139 [self expectCKFetch];
3140 self.keychainView = [[CKKSViewManager manager] restartZone: self.keychainZoneID.zoneName];
3141 [self beginSOSTrustedViewOperation:self.keychainView];
3142 [self.keychainView waitForKeyHierarchyReadiness];
3143 OCMVerifyAllWithDelay(self.mockDatabase, 20);
3144
3145 XCTAssertFalse(self.keychainView.initiatedLocalScan, "Should not have initiated a local items scan due to a restart (when we haven't fetched in a while, but did scan recently)");
3146
3147 // Now restart again, but cause a scan to occur
3148 [self.keychainView dispatchSyncWithSQLTransaction:^CKKSDatabaseTransactionResult{
3149 NSError* error = nil;
3150 CKKSZoneStateEntry* ckse = [CKKSZoneStateEntry fromDatabase:self.keychainZoneID.zoneName error:&error];
3151 XCTAssertNil(error, "no error pulling ckse from database");
3152 XCTAssertNotNil(ckse, "received a ckse");
3153
3154 ckse.lastLocalKeychainScanTime = [NSDate distantPast];
3155 [ckse saveToDatabase:&error];
3156 XCTAssertNil(error, "no error saving to database");
3157 return CKKSDatabaseTransactionCommit;
3158 }];
3159
3160 [self.keychainView halt];
3161 self.keychainView = [[CKKSViewManager manager] restartZone: self.keychainZoneID.zoneName];
3162 [self beginSOSTrustedViewOperation:self.keychainView];
3163 XCTAssertEqual(0, [self.keychainView.keyHierarchyConditions[SecCKKSZoneKeyStateReady] wait:20*NSEC_PER_SEC], @"Key state should arrive at ready");
3164 OCMVerifyAllWithDelay(self.mockDatabase, 20);
3165
3166 [self.keychainView waitForOperationsOfClass:[CKKSScanLocalItemsOperation class]];
3167 [self.keychainView waitForOperationsOfClass:[CKKSOutgoingQueueOperation class]];
3168
3169 XCTAssertTrue(self.keychainView.initiatedLocalScan, "Should have initiated a local items scan due to 24-hr notification");
3170 }
3171
3172 - (void)testFetchAndScanOn24HrNotification {
3173 // Every 24 hrs, CKKS should fetch if there hasn't been a fetch in a while.
3174 [self startCKKSSubsystem];
3175 [self performOctagonTLKUpload:self.ckksViews];
3176
3177 [self waitForCKModifications];
3178 OCMVerifyAllWithDelay(self.mockDatabase, 20);
3179
3180 XCTAssertEqual(0, [self.keychainView.keyHierarchyConditions[SecCKKSZoneKeyStateReady] wait:20*NSEC_PER_SEC], @"Key state should arrive at ready");
3181
3182 [self.keychainView waitForOperationsOfClass:[CKKSScanLocalItemsOperation class]];
3183 [self.keychainView waitForOperationsOfClass:[CKKSOutgoingQueueOperation class]];
3184
3185 // We now get the 24-hr notification. Set this bit for later checking
3186 XCTAssertTrue(self.keychainView.initiatedLocalScan, "Should have initiated a local items scan during bringup");
3187 self.keychainView.initiatedLocalScan = NO;
3188
3189 self.silentFetchesAllowed = false;
3190
3191 [self.keychainView xpc24HrNotification];
3192
3193 XCTAssertEqual(0, [self.keychainView.keyHierarchyConditions[SecCKKSZoneKeyStateReady] wait:20*NSEC_PER_SEC], @"Key state should arrive at ready");
3194 OCMVerifyAllWithDelay(self.mockDatabase, 20);
3195
3196 XCTAssertFalse(self.keychainView.initiatedLocalScan, "Should not have initiated a local items scan due to a 24-hr notification with a recent fetch");
3197
3198 // Okay, cool, rad, now let's set the last local-keychain-scan date to be very long ago and retry
3199 // This shouldn't fetch, but it should scan the local keychain
3200 self.silentFetchesAllowed = false;
3201
3202 [self.keychainView dispatchSyncWithSQLTransaction:^CKKSDatabaseTransactionResult{
3203 NSError* error = nil;
3204 CKKSZoneStateEntry* ckse = [CKKSZoneStateEntry fromDatabase:self.keychainZoneID.zoneName error:&error];
3205 XCTAssertNil(error, "no error pulling ckse from database");
3206 XCTAssertNotNil(ckse, "received a ckse");
3207
3208 ckse.lastLocalKeychainScanTime = [NSDate distantPast];
3209 [ckse saveToDatabase:&error];
3210 XCTAssertNil(error, "no error saving to database");
3211 return CKKSDatabaseTransactionCommit;
3212 }];
3213
3214 [self.keychainView xpc24HrNotification];
3215 XCTAssertEqual(0, [self.keychainView.keyHierarchyConditions[SecCKKSZoneKeyStateReady] wait:20*NSEC_PER_SEC], @"Key state should arrive at ready");
3216 OCMVerifyAllWithDelay(self.mockDatabase, 20);
3217
3218 [self.keychainView waitForOperationsOfClass:[CKKSScanLocalItemsOperation class]];
3219 [self.keychainView waitForOperationsOfClass:[CKKSOutgoingQueueOperation class]];
3220
3221 XCTAssertTrue(self.keychainView.initiatedLocalScan, "Should have initiated a local items scan due to 24-hr notification");
3222 self.keychainView.initiatedLocalScan = false;
3223
3224 // And check that the fetch occurs as well
3225 [self.keychainView dispatchSyncWithSQLTransaction:^CKKSDatabaseTransactionResult{
3226 NSError* error = nil;
3227 CKKSZoneStateEntry* ckse = [CKKSZoneStateEntry fromDatabase:self.keychainZoneID.zoneName error:&error];
3228 XCTAssertNil(error, "no error pulling ckse from database");
3229 XCTAssertNotNil(ckse, "received a ckse");
3230
3231 ckse.lastFetchTime = [NSDate distantPast];
3232 [ckse saveToDatabase:&error];
3233 XCTAssertNil(error, "no error saving to database");
3234 return CKKSDatabaseTransactionCommit;
3235 }];
3236
3237 [self expectCKFetch];
3238 [self.keychainView xpc24HrNotification];
3239 XCTAssertEqual(0, [self.keychainView.keyHierarchyConditions[SecCKKSZoneKeyStateReady] wait:20*NSEC_PER_SEC], @"Key state should arrive at ready");
3240 OCMVerifyAllWithDelay(self.mockDatabase, 20);
3241
3242 [self.keychainView waitForOperationsOfClass:[CKKSScanLocalItemsOperation class]];
3243 [self.keychainView waitForOperationsOfClass:[CKKSOutgoingQueueOperation class]];
3244
3245 XCTAssertFalse(self.keychainView.initiatedLocalScan, "Should not have initiated a local items scan due to 24-hr notification (if we've done one recently)");
3246 }
3247
3248 - (void)testRecoverFromZoneCreationFailure {
3249 // Fail the zone creation.
3250 self.zones[self.keychainZoneID] = nil; // delete the autocreated zone
3251 [self failNextZoneCreation:self.keychainZoneID];
3252
3253 // Spin up CKKS subsystem.
3254 [self startCKKSSubsystem];
3255
3256 // CKKS should figure it out, and fix it
3257 [self performOctagonTLKUpload:self.ckksViews];
3258 OCMVerifyAllWithDelay(self.mockDatabase, 20);
3259
3260 [self expectCKModifyItemRecords: 1 currentKeyPointerRecords: 1 zoneID:self.keychainZoneID
3261 checkItem: [self checkClassCBlock:self.keychainZoneID message:@"Object was encrypted under class C key in hierarchy"]];
3262 [self addGenericPassword: @"data" account: @"account-delete-me"];
3263 OCMVerifyAllWithDelay(self.mockDatabase, 20);
3264
3265 XCTAssertNil(self.zones[self.keychainZoneID].creationError, "Creation error was unset (and so CKKS probably dealt with the error");
3266 }
3267
3268 - (void)testRecoverFromZoneSubscriptionFailure {
3269 // Fail the zone subscription.
3270 [self failNextZoneSubscription:self.keychainZoneID];
3271
3272 // Spin up CKKS subsystem.
3273 [self startCKKSSubsystem];
3274
3275 // The CKKS subsystem should figure out the issue, and fix it before Octagon uploads its items
3276 [self performOctagonTLKUpload:self.ckksViews];
3277
3278 [self.keychainView waitForKeyHierarchyReadiness];
3279 OCMVerifyAllWithDelay(self.mockDatabase, 20);
3280
3281 [self expectCKModifyItemRecords: 1 currentKeyPointerRecords: 1 zoneID:self.keychainZoneID
3282 checkItem: [self checkClassCBlock:self.keychainZoneID message:@"Object was encrypted under class C key in hierarchy"]];
3283 [self addGenericPassword: @"data" account: @"account-delete-me"];
3284 OCMVerifyAllWithDelay(self.mockDatabase, 20);
3285
3286 XCTAssertNil(self.zones[self.keychainZoneID].subscriptionError, "Subscription error was unset (and so CKKS probably dealt with the error");
3287 }
3288
3289 - (void)testRecoverFromZoneSubscriptionFailureDueToZoneNotExisting {
3290 // This is different from testRecoverFromZoneSubscriptionFailure, since the zone is gone. CKKS must attempt to re-create the zone.
3291
3292 // Silently fail the zone creation
3293 self.zones[self.keychainZoneID] = nil; // delete the autocreated zone
3294 [self failNextZoneCreationSilently:self.keychainZoneID];
3295
3296 // Spin up CKKS subsystem.
3297 [self startCKKSSubsystem];
3298
3299 // The CKKS subsystem should figure out the issue, and fix it.
3300 [self performOctagonTLKUpload:self.ckksViews];
3301
3302 XCTAssertEqual(0, [self.keychainView.keyHierarchyConditions[SecCKKSZoneKeyStateReady] wait:20*NSEC_PER_SEC], @"Key state should arrive at ready");
3303 OCMVerifyAllWithDelay(self.mockDatabase, 20);
3304
3305 [self expectCKModifyItemRecords: 1 currentKeyPointerRecords: 1 zoneID:self.keychainZoneID
3306 checkItem: [self checkClassCBlock:self.keychainZoneID message:@"Object was encrypted under class C key in hierarchy"]];
3307 [self addGenericPassword: @"data" account: @"account-delete-me"];
3308 OCMVerifyAllWithDelay(self.mockDatabase, 20);
3309
3310 XCTAssertFalse(self.zones[self.keychainZoneID].flag, "Zone flag was reset");
3311 XCTAssertNil(self.zones[self.keychainZoneID].subscriptionError, "Subscription error was unset (and so CKKS probably dealt with the error");
3312 }
3313
3314 - (void)testRecoverFromDeletedTLKWithStashedTLK {
3315 // We need to handle the case where our syncable TLKs are deleted for some reason. The device that has them might resurrect them
3316
3317 [self createAndSaveFakeKeyHierarchy: self.keychainZoneID]; // Make life easy for this test.
3318 NSError* error = nil;
3319
3320 // Stash the TLKs.
3321 [self.keychainZoneKeys.tlk saveKeyMaterialToKeychain:true error:&error];
3322 XCTAssertNil(error, "Should have received no error stashing the new TLK in the keychain");
3323
3324 // And delete the non-stashed version
3325 [self.keychainZoneKeys.tlk deleteKeyMaterialFromKeychain:&error];
3326 XCTAssertNil(error, "Should have received no error deleting the new TLK from the keychain");
3327
3328 // Spin up CKKS subsystem.
3329 [self startCKKSSubsystem];
3330
3331 [self.keychainView waitForKeyHierarchyReadiness];
3332 OCMVerifyAllWithDelay(self.mockDatabase, 20);
3333
3334 [self expectCKModifyItemRecords: 1 currentKeyPointerRecords: 1 zoneID:self.keychainZoneID checkItem: [self checkClassCBlock:self.keychainZoneID message:@"Object was encrypted under class C key in hierarchy"]];
3335 [self addGenericPassword: @"data" account: @"account-delete-me"];
3336 OCMVerifyAllWithDelay(self.mockDatabase, 20);
3337
3338 // CKKS should recreate the syncable TLK.
3339 [self checkNSyncableTLKsInKeychain: 1];
3340 }
3341
3342 - (void)testRecoverFromDeletedTLKWithStashedTLKUponRestart {
3343 // We need to handle the case where our syncable TLKs are deleted for some reason. The device that has them might resurrect them
3344
3345 [self createAndSaveFakeKeyHierarchy: self.keychainZoneID]; // Make life easy for this test.
3346 // Spin up CKKS subsystem.
3347 [self startCKKSSubsystem];
3348 [self.keychainView waitForKeyHierarchyReadiness];
3349
3350 // Tear down the CKKS object
3351 [self.keychainView halt];
3352
3353 NSError* error = nil;
3354
3355 // Stash the TLKs.
3356 [self.keychainZoneKeys.tlk saveKeyMaterialToKeychain:true error:&error];
3357 XCTAssertNil(error, "Should have received no error stashing the new TLK in the keychain");
3358
3359 [self.keychainZoneKeys.tlk deleteKeyMaterialFromKeychain:&error];
3360 XCTAssertNil(error, "Should have received no error deleting the new TLK from the keychain");
3361
3362 self.keychainView = [[CKKSViewManager manager] restartZone: self.keychainZoneID.zoneName];
3363 [self beginSOSTrustedViewOperation:self.keychainView];
3364 [self.keychainView waitForKeyHierarchyReadiness];
3365 OCMVerifyAllWithDelay(self.mockDatabase, 20);
3366
3367 [self expectCKModifyItemRecords: 1 currentKeyPointerRecords: 1 zoneID:self.keychainZoneID checkItem: [self checkClassCBlock:self.keychainZoneID message:@"Object was encrypted under class C key in hierarchy"]];
3368 [self addGenericPassword: @"data" account: @"account-delete-me"];
3369 OCMVerifyAllWithDelay(self.mockDatabase, 20);
3370
3371 // CKKS should recreate the syncable TLK.
3372 [self checkNSyncableTLKsInKeychain: 1];
3373 }
3374
3375 /*
3376 // <rdar://problem/49024967> Octagon: tests for CK exceptions out of cuttlefish
3377 - (void)testRecoverFromTLKWriteFailure {
3378 // We need to handle the case where a device's first TLK write doesn't go through (due to whatever reason).
3379 // Test starts with nothing in CloudKit, and will fail the first TLK write.
3380 NSError* noNetwork = [[CKPrettyError alloc] initWithDomain:CKErrorDomain code:CKErrorNetworkUnavailable userInfo:@{}];
3381 [self failNextCKAtomicModifyItemRecordsUpdateFailure:self.keychainZoneID blockAfterReject:nil withError:noNetwork];
3382
3383 // Spin up CKKS subsystem.
3384 [self startCKKSSubsystem];
3385
3386 // The CKKS subsystem should figure out the issue, and fix it.
3387 [self expectCKModifyKeyRecords: 3 currentKeyPointerRecords: 3 tlkShareRecords: 1 zoneID:self.keychainZoneID];
3388
3389 [self.keychainView waitForKeyHierarchyReadiness];
3390 OCMVerifyAllWithDelay(self.mockDatabase, 20);
3391
3392 [self expectCKModifyItemRecords: 1 currentKeyPointerRecords: 1 zoneID:self.keychainZoneID checkItem: [self checkClassCBlock:self.keychainZoneID message:@"Object was encrypted under class C key in hierarchy"]];
3393 [self addGenericPassword: @"data" account: @"account-delete-me"];
3394 OCMVerifyAllWithDelay(self.mockDatabase, 20);
3395
3396 // A network failure creating new TLKs shouldn't delete the 'failed' syncable one.
3397 [self checkNSyncableTLKsInKeychain: 2];
3398 }
3399 */
3400
3401 // This test needs to be moved and rewritten now that Octagon handles TLK uploads
3402 // <rdar://problem/49024967> Octagon: tests for CK exceptions out of cuttlefish
3403 /*
3404 - (void)testRecoverFromTLKRace {
3405 // We need to handle the case where a device's first TLK write doesn't go through (due to whatever reason).
3406 // Test starts with nothing in CloudKit, and will fail the first TLK write.
3407 [self failNextCKAtomicModifyItemRecordsUpdateFailure:self.keychainZoneID blockAfterReject: ^{
3408 [self putFakeKeyHierarchyInCloudKit:self.keychainZoneID];
3409 }];
3410
3411 // Spin up CKKS subsystem.
3412 [self startCKKSSubsystem];
3413
3414 // The first TLK write should fail, and then our fake TLKs should be there in CloudKit.
3415 // It shouldn't write anything back up to CloudKit.
3416 OCMVerifyAllWithDelay(self.mockDatabase, 20);
3417
3418 // Now the TLKs arrive from the other device...
3419 [self expectCKKSTLKSelfShareUpload:self.keychainZoneID];
3420 [self saveTLKMaterialToKeychainSimulatingSOS:self.keychainZoneID];
3421 [self.keychainView waitForKeyHierarchyReadiness];
3422
3423 [self expectCKModifyItemRecords: 1 currentKeyPointerRecords: 1 zoneID:self.keychainZoneID checkItem: [self checkClassCBlock:self.keychainZoneID message:@"Object was encrypted under class C key in hierarchy"]];
3424 [self addGenericPassword: @"data" account: @"account-delete-me"];
3425 OCMVerifyAllWithDelay(self.mockDatabase, 20);
3426
3427 // A race failure creating new TLKs should delete the old syncable one.
3428 [self checkNSyncableTLKsInKeychain: 1];
3429 }
3430 */
3431
3432 - (void)testRecoverFromNullCurrentKeyPointers {
3433 // The current key pointers in cloudkit shouldn't ever not exist if keys do. But, if they don't, CKKS must recover.
3434
3435 // Test starts with a broken key hierarchy in our fake CloudKit, but the TLK already arrived.
3436 [self putFakeKeyHierarchyInCloudKit:self.keychainZoneID];
3437 [self saveTLKMaterialToKeychain:self.keychainZoneID];
3438
3439 ZoneKeys* zonekeys = self.keys[self.keychainZoneID];
3440 FakeCKZone* ckzone = self.zones[self.keychainZoneID];
3441 ckzone.currentDatabase[zonekeys.currentTLKPointer.storedCKRecord.recordID][SecCKRecordParentKeyRefKey] = nil;
3442 ckzone.currentDatabase[zonekeys.currentClassAPointer.storedCKRecord.recordID][SecCKRecordParentKeyRefKey] = nil;
3443 ckzone.currentDatabase[zonekeys.currentClassCPointer.storedCKRecord.recordID][SecCKRecordParentKeyRefKey] = nil;
3444
3445 // Spin up CKKS subsystem.
3446 [self startCKKSSubsystem];
3447
3448 // The CKKS subsystem should figure out the issue, and fix it.
3449 [self expectCKModifyKeyRecords:0 currentKeyPointerRecords:3 tlkShareRecords:1 zoneID:self.keychainZoneID];
3450
3451 [self.keychainView waitForKeyHierarchyReadiness];
3452
3453 OCMVerifyAllWithDelay(self.mockDatabase, 20);
3454 }
3455
3456 - (void)testRecoverFromNoCurrentKeyPointers {
3457 // The current key pointers in cloudkit shouldn't ever point to nil. But, if they do, CKKS must recover.
3458
3459 // Test starts with a broken key hierarchy in our fake CloudKit, but the TLK already arrived.
3460 [self putFakeKeyHierarchyInCloudKit:self.keychainZoneID];
3461 [self saveTLKMaterialToKeychain:self.keychainZoneID];
3462
3463 ZoneKeys* zonekeys = self.keys[self.keychainZoneID];
3464 XCTAssertNil([self.zones[self.keychainZoneID] deleteCKRecordIDFromZone: zonekeys.currentTLKPointer.storedCKRecord.recordID], "Deleted TLK pointer from zone");
3465 XCTAssertNil([self.zones[self.keychainZoneID] deleteCKRecordIDFromZone: zonekeys.currentClassAPointer.storedCKRecord.recordID], "Deleted class a pointer from zone");
3466 XCTAssertNil([self.zones[self.keychainZoneID] deleteCKRecordIDFromZone: zonekeys.currentClassCPointer.storedCKRecord.recordID], "Deleted class c pointer from zone");
3467
3468 // Spin up CKKS subsystem.
3469 [self startCKKSSubsystem];
3470
3471 // The CKKS subsystem should figure out the issue, and fix it.
3472 [self expectCKModifyKeyRecords:0 currentKeyPointerRecords:3 tlkShareRecords:1 zoneID:self.keychainZoneID];
3473
3474 XCTAssertEqual(0, [self.keychainView.keyHierarchyConditions[SecCKKSZoneKeyStateReady] wait:20*NSEC_PER_SEC], "Key state should have become ready");
3475
3476 OCMVerifyAllWithDelay(self.mockDatabase, 20);
3477 }
3478
3479 /*
3480 // <rdar://problem/49024967> Octagon: tests for CK exceptions out of cuttlefish
3481 - (void)testRecoverFromBadChangeTag {
3482 // We received a report where a machine appeared to have an up-to-date change tag, but was continuously attempting to create a new TLK hierarchy. No idea why.
3483
3484 // Test starts with a broken key hierarchy in our fake CloudKit, but a (incorrectly) up-to-date change tag stored locally.
3485 [self putFakeKeyHierarchyInCloudKit:self.keychainZoneID];
3486 SecCKKSTestSetDisableKeyNotifications(true); // Don't tell CKKS about this key material; we're pretending like this is a securityd restart
3487 [self saveTLKMaterialToKeychain:self.keychainZoneID];
3488 SecCKKSTestSetDisableKeyNotifications(false);
3489
3490 [self.keychainView dispatchSync: ^bool {
3491 NSError* error = nil;
3492 CKKSZoneStateEntry* ckse = [CKKSZoneStateEntry state:self.keychainZoneID.zoneName];
3493 XCTAssertNotNil(ckse, "should have received a ckse");
3494
3495 ckse.ckzonecreated = true;
3496 ckse.ckzonesubscribed = true;
3497 ckse.changeToken = self.keychainZone.currentChangeToken;
3498
3499 [ckse saveToDatabase: &error];
3500 XCTAssertNil(error, "shouldn't have gotten an error saving to database");
3501 return true;
3502 }];
3503
3504 // The CKKS subsystem should try to write TLKs, but fail. It'll then upload a TLK share for the keys already in CloudKit
3505 [self expectCKAtomicModifyItemRecordsUpdateFailure:self.keychainZoneID];
3506 [self expectCKKSTLKSelfShareUpload:self.keychainZoneID];
3507
3508 // Spin up CKKS subsystem.
3509 [self startCKKSSubsystem];
3510 OCMVerifyAllWithDelay(self.mockDatabase, 20);
3511
3512 // CKKS should then happily use the keys in CloudKit
3513 [self createClassCItemAndWaitForUpload:self.keychainZoneID account:@"account-delete-me"];
3514 [self createClassAItemAndWaitForUpload:self.keychainZoneID account:@"account-delete-me-class-a"];
3515 }
3516 */
3517
3518 - (void)testRecoverFromDeletedKeysNewItem {
3519 [self startCKKSSubsystem];
3520 [self performOctagonTLKUpload:self.ckksViews];
3521
3522 XCTAssertEqual(0, [self.keychainView.keyHierarchyConditions[SecCKKSZoneKeyStateReady] wait:20*NSEC_PER_SEC], @"Key state should arrive at ready");
3523
3524 // We expect a single class C record to be uploaded.
3525 [self expectCKModifyItemRecords: 1 currentKeyPointerRecords: 1 zoneID:self.keychainZoneID
3526 checkItem: [self checkClassCBlock:self.keychainZoneID message:@"Object was encrypted under class C key in hierarchy"]];
3527
3528 [self addGenericPassword: @"data" account: @"account-delete-me"];
3529 OCMVerifyAllWithDelay(self.mockDatabase, 20);
3530
3531 [self waitForCKModifications];
3532 [self.keychainView waitUntilAllOperationsAreFinished];
3533
3534 // Now, delete the local keys from the keychain (but leave the synced TLK)
3535 SecCKKSTestSetDisableKeyNotifications(true);
3536 XCTAssertEqual(errSecSuccess, SecItemDelete((__bridge CFDictionaryRef)@{
3537 (id)kSecClass : (id)kSecClassInternetPassword,
3538 (id)kSecUseDataProtectionKeychain : @YES,
3539 (id)kSecAttrAccessGroup : @"com.apple.security.ckks",
3540 (id)kSecAttrSynchronizable : (id)kCFBooleanFalse,
3541 }), @"Deleting local keys");
3542 SecCKKSTestSetDisableKeyNotifications(false);
3543
3544 NSError* error = nil;
3545 [self.keychainZoneKeys.classC loadKeyMaterialFromKeychain:&error];
3546 XCTAssertNotNil(error, "Error loading class C key material from keychain");
3547
3548 // We expect a single class C record to be uploaded.
3549 [self expectCKModifyItemRecords: 1 currentKeyPointerRecords: 1 zoneID:self.keychainZoneID
3550 checkItem: [self checkClassCBlock:self.keychainZoneID message:@"Object was encrypted under class C key in hierarchy"]];
3551
3552 [self addGenericPassword: @"datadata" account: @"account-no-keys"];
3553 OCMVerifyAllWithDelay(self.mockDatabase, 20);
3554
3555 // We expect a single class A record to be uploaded.
3556 [self expectCKModifyItemRecords: 1 currentKeyPointerRecords: 1 zoneID:self.keychainZoneID
3557 checkItem: [self checkClassABlock:self.keychainZoneID message:@"Object was encrypted under class A key in hierarchy"]];
3558 [self addGenericPassword:@"asdf"
3559 account:@"account-class-A"
3560 viewHint:nil
3561 access:(id)kSecAttrAccessibleWhenUnlocked
3562 expecting:errSecSuccess
3563 message:@"Adding class A item"];
3564 OCMVerifyAllWithDelay(self.mockDatabase, 20);
3565 }
3566
3567 - (void)testRecoverFromDeletedKeysReceive {
3568 [self startCKKSSubsystem];
3569 [self performOctagonTLKUpload:self.ckksViews];
3570
3571 XCTAssertEqual(0, [self.keychainView.keyHierarchyConditions[SecCKKSZoneKeyStateReady] wait:20*NSEC_PER_SEC], @"Key state should arrive at ready");
3572
3573 [self waitForCKModifications];
3574 [self.keychainView waitUntilAllOperationsAreFinished];
3575
3576 [self.injectedManager.zoneChangeFetcher notifyZoneChange:nil];
3577 [self.keychainView waitForFetchAndIncomingQueueProcessing];
3578
3579 CKRecord* ckr = [self createFakeRecord: self.keychainZoneID recordName:@"7B598D31-F9C5-481E-98AC-5A507ACB2D85" withAccount:@"account0"];
3580
3581 // Now, delete the local keys from the keychain (but leave the synced TLK)
3582 SecCKKSTestSetDisableKeyNotifications(true);
3583 XCTAssertEqual(errSecSuccess, SecItemDelete((__bridge CFDictionaryRef)@{
3584 (id)kSecClass : (id)kSecClassInternetPassword,
3585 (id)kSecUseDataProtectionKeychain : @YES,
3586 (id)kSecAttrAccessGroup : @"com.apple.security.ckks",
3587 (id)kSecAttrSynchronizable : (id)kCFBooleanFalse,
3588 }), @"Deleting local keys");
3589 SecCKKSTestSetDisableKeyNotifications(false);
3590
3591 [self.keychainZone addToZone: ckr];
3592 [self.injectedManager.zoneChangeFetcher notifyZoneChange:nil];
3593 [self.keychainView waitForFetchAndIncomingQueueProcessing];
3594
3595 [self findGenericPassword: @"account0" expecting:errSecSuccess];
3596 }
3597
3598 - (void)testRecoverDeletedTLK {
3599 // If the TLK disappears halfway through, well, that's no good. But we should recover using TLK sharing
3600
3601 [self startCKKSSubsystem];
3602 [self performOctagonTLKUpload:self.ckksViews];
3603
3604 XCTAssertEqual(0, [self.keychainView.keyHierarchyConditions[SecCKKSZoneKeyStateReady] wait:20*NSEC_PER_SEC], "Key state should have returned to ready");
3605
3606 OCMVerifyAllWithDelay(self.mockDatabase, 20);
3607 [self waitForCKModifications];
3608
3609 CKRecord* ckr = [self createFakeRecord: self.keychainZoneID recordName:@"7B598D31-F9C5-481E-98AC-5A507ACB2D85" withAccount:@"account0"];
3610 [self.keychainView waitUntilAllOperationsAreFinished];
3611
3612 // Now, delete the local keys from the keychain
3613 SecCKKSTestSetDisableKeyNotifications(true);
3614 XCTAssertEqual(errSecSuccess, SecItemDelete((__bridge CFDictionaryRef)@{
3615 (id)kSecClass : (id)kSecClassInternetPassword,
3616 (id)kSecUseDataProtectionKeychain : @YES,
3617 (id)kSecAttrAccessGroup : @"com.apple.security.ckks",
3618 (id)kSecAttrSynchronizable : (id)kSecAttrSynchronizableAny,
3619 }), @"Deleting CKKS keys");
3620 SecCKKSTestSetDisableKeyNotifications(false);
3621
3622 // Trigger a notification (with hilariously fake data)
3623 [self.keychainZone addToZone: ckr];
3624
3625 [self.injectedManager.zoneChangeFetcher notifyZoneChange:nil];
3626 [self.keychainView waitForFetchAndIncomingQueueProcessing];
3627
3628 XCTAssertEqual(0, [self.keychainView.keyHierarchyConditions[SecCKKSZoneKeyStateReady] wait:20*NSEC_PER_SEC], "Key state should return to 'ready'");
3629
3630 [self.injectedManager.zoneChangeFetcher notifyZoneChange:nil];
3631 [self.keychainView waitForFetchAndIncomingQueueProcessing]; // Do this again, to allow for non-atomic key state machinery switching
3632
3633 [self findGenericPassword: @"account0" expecting:errSecSuccess];
3634 }
3635
3636 - (void)testRecoverMissingRolledKey {
3637 [self putFakeKeyHierarchyInCloudKit:self.keychainZoneID];
3638
3639 NSString* accountShouldExist = @"under-rolled-key";
3640 NSString* accountWillExist = @"under-rolled-key-later";
3641 CKRecord* ckr = [self createFakeRecord:self.keychainZoneID recordName:@"7B598D31-F9C5-481E-98AC-5A507ACB2D85" withAccount:accountShouldExist];
3642 [self.keychainZone addToZone: ckr];
3643
3644 CKRecord* ckrAddedLater = [self createFakeRecord:self.keychainZoneID recordName:@"7B598D31-F9C5-481E-98AC-5A507ACB2D85" withAccount:accountWillExist];
3645 CKKSKey* pastClassCKey = self.keychainZoneKeys.classC;
3646
3647 [self rollFakeKeyHierarchyInCloudKit:self.keychainZoneID];
3648 [self saveTLKMaterialToKeychain:self.keychainZoneID];
3649
3650 [self expectCKKSTLKSelfShareUpload:self.keychainZoneID];
3651
3652 [self startCKKSSubsystem];
3653 XCTAssertEqual(0, [self.keychainView.keyHierarchyConditions[SecCKKSZoneKeyStateReady] wait:20*NSEC_PER_SEC], "Key state should have returned to ready");
3654
3655 OCMVerifyAllWithDelay(self.mockDatabase, 20);
3656 [self waitForCKModifications];
3657
3658 [self.keychainView waitForOperationsOfClass:[CKKSIncomingQueueOperation class]];
3659 [self findGenericPassword:accountShouldExist expecting:errSecSuccess];
3660 [self findGenericPassword:accountWillExist expecting:errSecItemNotFound];
3661
3662 // Now, find and delete the class C key that ckrAddedLater is under
3663 NSError* error = nil;
3664 XCTAssertTrue([pastClassCKey deleteKeyMaterialFromKeychain:&error], "Should be able to delete old key material from keychain");
3665 XCTAssertNil(error, "Should be no error deleting old key material from keychain");
3666
3667 [self.keychainZone addToZone:ckrAddedLater];
3668 [self.injectedManager.zoneChangeFetcher notifyZoneChange:nil];
3669 [self.keychainView waitForFetchAndIncomingQueueProcessing];
3670
3671 [self findGenericPassword:accountShouldExist expecting:errSecSuccess];
3672 [self findGenericPassword:accountWillExist expecting:errSecSuccess];
3673
3674 XCTAssertTrue([pastClassCKey loadKeyMaterialFromKeychain:&error], "Class C key should be back in the keychain");
3675 XCTAssertNil(error, "Should be no error loading key from keychain");
3676 }
3677
3678 - (void)testRecoverMissingRolledClassAKeyWhileLocked {
3679 [self putFakeKeyHierarchyInCloudKit:self.keychainZoneID];
3680
3681 NSString* accountShouldExist = @"under-rolled-key";
3682 NSString* accountWillExist = @"under-rolled-key-later";
3683 CKRecord* ckr = [self createFakeRecord:self.keychainZoneID recordName:@"7B598D31-F9C5-481E-98AC-5A507ACB2D85" withAccount:accountShouldExist key:self.keychainZoneKeys.classA];
3684 [self.keychainZone addToZone: ckr];
3685
3686 CKRecord* ckrAddedLater = [self createFakeRecord:self.keychainZoneID recordName:@"7B598D31-F9C5-481E-98AC-5A507ACB2D85" withAccount:accountWillExist key:self.keychainZoneKeys.classA];
3687 CKKSKey* pastClassAKey = self.keychainZoneKeys.classA;
3688
3689 [self rollFakeKeyHierarchyInCloudKit:self.keychainZoneID];
3690 [self saveTLKMaterialToKeychain:self.keychainZoneID];
3691
3692 [self expectCKKSTLKSelfShareUpload:self.keychainZoneID];
3693
3694 [self startCKKSSubsystem];
3695 XCTAssertEqual(0, [self.keychainView.keyHierarchyConditions[SecCKKSZoneKeyStateReady] wait:20*NSEC_PER_SEC], "Key state should have returned to ready");
3696
3697 OCMVerifyAllWithDelay(self.mockDatabase, 20);
3698 [self waitForCKModifications];
3699
3700 [self.keychainView waitForOperationsOfClass:[CKKSIncomingQueueOperation class]];
3701 [self findGenericPassword:accountShouldExist expecting:errSecSuccess];
3702 [self findGenericPassword:accountWillExist expecting:errSecItemNotFound];
3703
3704 // Now, find and delete the class C key that ckrAddedLater is under
3705 NSError* error = nil;
3706 XCTAssertTrue([pastClassAKey deleteKeyMaterialFromKeychain:&error], "Should be able to delete old key material from keychain");
3707 XCTAssertNil(error, "Should be no error deleting old key material from keychain");
3708
3709 // now, lock the keychain
3710 self.aksLockState = true;
3711 [self.lockStateTracker recheck];
3712
3713 [self.keychainZone addToZone:ckrAddedLater];
3714 [self.injectedManager.zoneChangeFetcher notifyZoneChange:nil];
3715 [self.keychainView waitForFetchAndIncomingQueueProcessing];
3716
3717 // Item should still not exist due to the lock state....
3718 [self findGenericPassword:accountShouldExist expecting:errSecSuccess];
3719 [self findGenericPassword:accountWillExist expecting:errSecItemNotFound];
3720
3721 XCTAssertEqual(0, [self.keychainView.keyHierarchyConditions[SecCKKSZoneKeyStateReadyPendingUnlock] wait:20*NSEC_PER_SEC], "Key state should have returned to readypendingunlock");
3722
3723 self.aksLockState = false;
3724 [self.lockStateTracker recheck];
3725
3726 // And now it does
3727 [self.keychainView waitUntilAllOperationsAreFinished];
3728 [self findGenericPassword:accountShouldExist expecting:errSecSuccess];
3729 [self findGenericPassword:accountWillExist expecting:errSecSuccess];
3730
3731 XCTAssertTrue([pastClassAKey loadKeyMaterialFromKeychain:&error], "Class A key should be back in the keychain");
3732 XCTAssertNil(error, "Should be no error loading key from keychain");
3733 }
3734
3735 - (void)testRecoverFromBadCurrentKeyPointer {
3736 // The current key pointers in cloudkit shouldn't ever point to missing entries. But, if they do, CKKS must recover.
3737
3738 // Test starts with a broken key hierarchy in our fake CloudKit, but the TLK already arrived.
3739 [self putFakeKeyHierarchyInCloudKit:self.keychainZoneID];
3740 [self saveTLKMaterialToKeychain:self.keychainZoneID];
3741
3742 ZoneKeys* zonekeys = self.keys[self.keychainZoneID];
3743 FakeCKZone* ckzone = self.zones[self.keychainZoneID];
3744 ckzone.currentDatabase[zonekeys.currentTLKPointer.storedCKRecord.recordID][SecCKRecordParentKeyRefKey] = [[CKReference alloc] initWithRecordID: [[CKRecordID alloc] initWithRecordName: @"not a real tlk" zoneID: self.keychainZoneID] action: CKReferenceActionNone];
3745 ckzone.currentDatabase[zonekeys.currentClassAPointer.storedCKRecord.recordID][SecCKRecordParentKeyRefKey] = [[CKReference alloc] initWithRecordID: [[CKRecordID alloc] initWithRecordName: @"not a real class a key" zoneID: self.keychainZoneID] action: CKReferenceActionNone];
3746 ckzone.currentDatabase[zonekeys.currentClassCPointer.storedCKRecord.recordID][SecCKRecordParentKeyRefKey] = [[CKReference alloc] initWithRecordID: [[CKRecordID alloc] initWithRecordName: @"not a real class c key" zoneID: self.keychainZoneID] action: CKReferenceActionNone];
3747
3748 // Spin up CKKS subsystem.
3749 [self startCKKSSubsystem];
3750
3751 // The CKKS subsystem should figure out the issue, and fix it (while uploading itself a TLK Share)
3752 [self expectCKModifyKeyRecords: 0 currentKeyPointerRecords: 3 tlkShareRecords: 1 zoneID:self.keychainZoneID];
3753
3754 XCTAssertEqual(0, [self.keychainView.keyHierarchyConditions[SecCKKSZoneKeyStateReady] wait:20*NSEC_PER_SEC], "Key state should have become ready");
3755
3756 OCMVerifyAllWithDelay(self.mockDatabase, 20);
3757 }
3758
3759 - (void)testRecoverFromIncorrectCurrentTLKPointer {
3760 // The current key pointers in cloudkit shouldn't ever point to wrong entries. But, if they do, CKKS must recover.
3761
3762 // Test starts with a rolled hierarchy, and CKPs pointing to the wrong items
3763 [self putFakeKeyHierarchyInCloudKit:self.keychainZoneID];
3764 [self saveTLKMaterialToKeychain:self.keychainZoneID];
3765
3766 CKKSCurrentKeyPointer* oldTLKCKP = self.keychainZoneKeys.currentTLKPointer;
3767 CKRecord* oldTLKPointer = [self.keychainZone.currentDatabase[self.keychainZoneKeys.currentTLKPointer.storedCKRecord.recordID] copy];
3768
3769 [self rollFakeKeyHierarchyInCloudKit:self.keychainZoneID];
3770 [self saveTLKMaterialToKeychain:self.keychainZoneID];
3771
3772 ZoneKeys* newZoneKeys = [self.keychainZoneKeys copy];
3773
3774 // And put the oldTLKPointer back
3775 [self.zones[self.keychainZoneID] addToZone:oldTLKPointer];
3776 self.keychainZoneKeys.currentTLKPointer = oldTLKCKP;
3777
3778 // Make sure it stuck:
3779 XCTAssertNotEqualObjects(self.keychainZoneKeys.currentTLKPointer,
3780 newZoneKeys.currentTLKPointer,
3781 "current TLK pointer should now not point to proper TLK");
3782
3783 // Spin up CKKS subsystem.
3784 [self startCKKSSubsystem];
3785
3786 // The CKKS subsystem should figure out the issue, and fix it (while uploading itself a TLK Share)
3787 [self expectCKModifyKeyRecords:0 currentKeyPointerRecords:3 tlkShareRecords:1 zoneID:self.keychainZoneID];
3788
3789 XCTAssertEqual(0, [self.keychainView.keyHierarchyConditions[SecCKKSZoneKeyStateReady] wait:20*NSEC_PER_SEC], "Key state should have become ready");
3790
3791 OCMVerifyAllWithDelay(self.mockDatabase, 20);
3792 [self waitForCKModifications];
3793
3794 XCTAssertEqualObjects(self.keychainZoneKeys.currentTLKPointer,
3795 newZoneKeys.currentTLKPointer,
3796 "current TLK pointer should now point to proper TLK");
3797 XCTAssertEqualObjects(self.keychainZoneKeys.currentClassAPointer,
3798 newZoneKeys.currentClassAPointer,
3799 "current Class A pointer should now point to proper Class A key");
3800 XCTAssertEqualObjects(self.keychainZoneKeys.currentClassCPointer,
3801 newZoneKeys.currentClassCPointer,
3802 "current Class C pointer should now point to proper Class C key");
3803 }
3804
3805 - (void)testRecoverFromDesyncedKeyRecordsViaResync {
3806 // We need to set up a desynced situation to test our resync.
3807 // First, let CKKS start up and send several items to CloudKit (that we'll then desync!)
3808 __block NSError* error = nil;
3809
3810 // Test starts with keys in CloudKit (so we can create items later)
3811 [self putFakeKeyHierarchyInCloudKit:self.keychainZoneID];
3812 [self expectCKKSTLKSelfShareUpload:self.keychainZoneID];
3813 [self saveTLKMaterialToKeychain:self.keychainZoneID];
3814
3815 [self addGenericPassword: @"data" account: @"first"];
3816 [self expectCKModifyItemRecords:1 currentKeyPointerRecords:1 zoneID:self.keychainZoneID];
3817
3818 [self startCKKSSubsystem];
3819
3820 // Wait for uploads to happen
3821 OCMVerifyAllWithDelay(self.mockDatabase, 20);
3822 [self waitForCKModifications];
3823
3824 // Now, delete most of the key records are from on-disk, but the change token is not changed
3825 [self.keychainView dispatchSyncWithSQLTransaction:^CKKSDatabaseTransactionResult{
3826 CKKSCurrentKeySet* keyset = [CKKSCurrentKeySet loadForZone:self.keychainZoneID];
3827
3828 XCTAssertNotNil(keyset.currentTLKPointer, @"should be a TLK pointer");
3829 XCTAssertNotNil(keyset.currentClassAPointer, @"should be a class A pointer");
3830 XCTAssertNotNil(keyset.currentClassCPointer, @"should be a class C pointer");
3831
3832 [keyset.currentTLKPointer deleteFromDatabase:&error];
3833 XCTAssertNil(error, "Should be no error deleting TLK pointer from database");
3834 [keyset.currentClassAPointer deleteFromDatabase:&error];
3835 XCTAssertNil(error, "Should be no error deleting class A pointer from database");
3836
3837 XCTAssertNotNil(keyset.tlk, @"should be a TLK");
3838 XCTAssertNotNil(keyset.classA, @"should be a classA key");
3839 XCTAssertNotNil(keyset.classC, @"should be a classC key");
3840
3841 [keyset.tlk deleteFromDatabase:&error];
3842 XCTAssertNil(error, "Should be no error deleting TLK from database");
3843
3844 [keyset.classA deleteFromDatabase:&error];
3845 XCTAssertNil(error, "Should be no error deleting classA from database");
3846
3847 [keyset.classC deleteFromDatabase:&error];
3848 XCTAssertNil(error, "Should be no error deleting classC from database");
3849
3850 return CKKSDatabaseTransactionCommit;
3851 }];
3852
3853 // A restart should realize there's an issue, and pause for help
3854 // Ideally we'd try a refetch here to see if we're very wrong, but that's hard to avoid making into an infinite loop
3855 [self.keychainView halt];
3856 self.keychainView = [[CKKSViewManager manager] restartZone: self.keychainZoneID.zoneName];
3857 [self.keychainView beginCloudKitOperation];
3858 [self beginSOSTrustedViewOperation:self.keychainView];
3859
3860 XCTAssertEqual(0, [self.keychainView.keyHierarchyConditions[SecCKKSZoneKeyStateWaitForTLKCreation] wait:20*NSEC_PER_SEC], @"key state should enter 'waitfortlkcreation'");
3861
3862 // But, a resync should fix you back up
3863 CKKSSynchronizeOperation* resyncOperation = [self.keychainView resyncWithCloud];
3864 [resyncOperation waitUntilFinished];
3865 XCTAssertNil(resyncOperation.error, "No error during the resync operation");
3866
3867 XCTAssertEqual(0, [self.keychainView.keyHierarchyConditions[SecCKKSZoneKeyStateReady] wait:20*NSEC_PER_SEC], @"key state should enter 'ready'");
3868 }
3869
3870 - (void)testRecoverFromCloudKitFetchFail {
3871 // Test starts with nothing in database, but one in our fake CloudKit.
3872 [self putFakeKeyHierarchyInCloudKit:self.keychainZoneID];
3873 [self putFakeDeviceStatusInCloudKit:self.keychainZoneID];
3874
3875 // The first two CKRecordZoneChanges should fail with a 'network unavailable' error.
3876 [self.keychainZone failNextFetchWith:[[NSError alloc] initWithDomain:CKErrorDomain code:CKErrorNetworkUnavailable userInfo:@{}]];
3877 [self.keychainZone failNextFetchWith:[[NSError alloc] initWithDomain:CKErrorDomain code:CKErrorNetworkUnavailable userInfo:@{}]];
3878
3879 // Spin up CKKS subsystem.
3880 [self startCKKSSubsystem];
3881
3882 // Now, save the TLK to the keychain (to simulate it coming in later via SOS).
3883 [self expectCKKSTLKSelfShareUpload:self.keychainZoneID];
3884 [self saveTLKMaterialToKeychainSimulatingSOS:self.keychainZoneID];
3885
3886 // We expect a single record to be uploaded
3887 [self expectCKModifyItemRecords: 1 currentKeyPointerRecords: 1 zoneID:self.keychainZoneID checkItem: [self checkClassCBlock:self.keychainZoneID message:@"Object was encrypted under class C key in hierarchy"]];
3888 [self addGenericPassword: @"data" account: @"account-delete-me"];
3889 OCMVerifyAllWithDelay(self.mockDatabase, 20);
3890
3891 [self expectCKModifyItemRecords: 1 currentKeyPointerRecords: 1 zoneID:self.keychainZoneID checkItem: [self checkClassABlock:self.keychainZoneID message:@"Object was encrypted under class A key in hierarchy"]];
3892 [self addGenericPassword:@"asdf"
3893 account:@"account-class-A"
3894 viewHint:nil
3895 access:(id)kSecAttrAccessibleWhenUnlocked
3896 expecting:errSecSuccess
3897 message:@"Adding class A item"];
3898 OCMVerifyAllWithDelay(self.mockDatabase, 20);
3899 }
3900
3901 - (void)testRecoverFromCloudKitFetchNetworkFailAfterReady {
3902 // Test starts with nothing in database, but one in our fake CloudKit.
3903 [self createAndSaveFakeKeyHierarchy: self.keychainZoneID]; // Make life easy for this test.
3904
3905 // Spin up CKKS subsystem.
3906 [self startCKKSSubsystem];
3907
3908 XCTAssertEqual(0, [self.keychainView.keyHierarchyConditions[SecCKKSZoneKeyStateReady] wait:20*NSEC_PER_SEC], "CKKS entered ready");
3909
3910 // Network is unavailable
3911 [self.reachabilityTracker setNetworkReachability:false];
3912
3913 CKRecord* ckr = [self createFakeRecord: self.keychainZoneID recordName:@"7B598D31-F9C5-481E-98AC-5A507ACB2D85"];
3914 [self.keychainZone addToZone:ckr];
3915
3916 [self findGenericPassword:@"account-delete-me" expecting:errSecItemNotFound];
3917
3918 // Say network is available
3919 [self.reachabilityTracker setNetworkReachability:true];
3920
3921 [self.injectedManager.zoneChangeFetcher notifyZoneChange:nil];
3922 [self.keychainView waitForFetchAndIncomingQueueProcessing];
3923
3924 [self findGenericPassword:@"account-delete-me" expecting:errSecSuccess];
3925 }
3926
3927 - (void)testRecoverFromCloudKitFetchNetworkFailBeforeReady {
3928 // Test starts with nothing in database, but one in our fake CloudKit.
3929 [self putFakeKeyHierarchyInCloudKit:self.keychainZoneID];
3930
3931 CKRecord* ckr = [self createFakeRecord: self.keychainZoneID recordName:@"7B598D31-F9C5-481E-98AC-5A507ACB2D85"];
3932 [self.keychainZone addToZone:ckr];
3933
3934 // Network is unavailable
3935 [self.reachabilityTracker setNetworkReachability:false];
3936
3937 // Spin up CKKS subsystem.
3938 [self startCKKSSubsystem];
3939
3940 XCTAssertEqual(0, [self.keychainView.keyHierarchyConditions[SecCKKSZoneKeyStateInitializing] wait:20*NSEC_PER_SEC], "CKKS entered initializing");
3941
3942 // Now, save the TLK to the keychain (to simulate it coming in later via SOS).
3943 [self expectCKKSTLKSelfShareUpload:self.keychainZoneID];
3944 [self saveTLKMaterialToKeychainSimulatingSOS:self.keychainZoneID];
3945
3946 [self findGenericPassword:@"account-delete-me" expecting:errSecItemNotFound];
3947
3948 // Say network is available
3949 [self.reachabilityTracker setNetworkReachability:true];
3950
3951 [self.keychainView waitUntilAllOperationsAreFinished];
3952 OCMVerifyAllWithDelay(self.mockDatabase, 20);
3953
3954 [self findGenericPassword:@"account-delete-me" expecting:errSecSuccess];
3955 }
3956
3957 - (void)testWaitAfterCloudKitNetworkFailDuringOutgoingQueueOperation {
3958 [self createAndSaveFakeKeyHierarchy: self.keychainZoneID]; // Make life easy for this test.
3959
3960 [self startCKKSSubsystem];
3961
3962 XCTAssertEqual(0, [self.keychainView.keyHierarchyConditions[SecCKKSZoneKeyStateReady] wait:8*NSEC_PER_SEC], "CKKS entered ready");
3963
3964 [self.keychainView waitForOperationsOfClass:[CKKSOutgoingQueueOperation class]];
3965
3966 // Network is now unavailable
3967 [self.reachabilityTracker setNetworkReachability:false];
3968
3969 NSError* noNetwork = [[CKPrettyError alloc] initWithDomain:CKErrorDomain code:CKErrorNetworkUnavailable userInfo:@{
3970 CKErrorRetryAfterKey: @(0.2),
3971 }];
3972 [self failNextCKAtomicModifyItemRecordsUpdateFailure:self.keychainZoneID blockAfterReject:nil withError:noNetwork];
3973 [self addGenericPassword: @"data" account: @"account-delete-me"];
3974
3975 OCMVerifyAllWithDelay(self.mockDatabase, 20);
3976 sleep(2);
3977
3978 // Once network is available again, the write should happen
3979 [self expectCKModifyItemRecords:1 currentKeyPointerRecords:1 zoneID:self.keychainZoneID
3980 checkItem:[self checkClassCBlock:self.keychainZoneID message:@"Object was encrypted under class C key in hierarchy"]];
3981
3982 [self.reachabilityTracker setNetworkReachability:true];
3983
3984 [self findGenericPassword:@"account-delete-me" expecting:errSecSuccess];
3985
3986 OCMVerifyAllWithDelay(self.mockDatabase, 20);
3987 }
3988
3989 - (void)testRecoverFromCloudKitFetchFailWithDelay {
3990 // Test starts with nothing in database, but one in our fake CloudKit.
3991 [self putFakeKeyHierarchyInCloudKit:self.keychainZoneID];
3992 [self putFakeDeviceStatusInCloudKit:self.keychainZoneID];
3993
3994 // The first CKRecordZoneChanges should fail with a 'delay' error.
3995 self.silentFetchesAllowed = false;
3996 [self.keychainZone failNextFetchWith:[[CKPrettyError alloc] initWithDomain:CKErrorDomain code:CKErrorRequestRateLimited userInfo:@{CKErrorRetryAfterKey : [NSNumber numberWithInt:4]}]];
3997 [self expectCKFetch];
3998
3999 // Spin up CKKS subsystem.
4000 [self startCKKSSubsystem];
4001
4002 // Ensure it doesn't fetch within these three seconds (if it does, an exception will throw).
4003 sleep(3);
4004
4005 // Okay, you can fetch again.
4006 [self expectCKFetch];
4007
4008 // Now, save the TLK to the keychain (to simulate it coming in later via SOS).
4009 [self expectCKKSTLKSelfShareUpload:self.keychainZoneID];
4010 [self saveTLKMaterialToKeychainSimulatingSOS:self.keychainZoneID];
4011
4012 // We expect a single record to be uploaded
4013 [self expectCKModifyItemRecords: 1 currentKeyPointerRecords: 1 zoneID:self.keychainZoneID checkItem: [self checkClassCBlock:self.keychainZoneID message:@"Object was encrypted under class C key in hierarchy"]];
4014 [self addGenericPassword: @"data" account: @"account-delete-me"];
4015 OCMVerifyAllWithDelay(self.mockDatabase, 20);
4016 }
4017
4018 - (void)testHandleZoneDeletedWhileFetching {
4019 // Test starts with no keys in database, a key hierarchy in our fake CloudKit, and the TLK safely in the local keychain.
4020 [self putFakeKeyHierarchyInCloudKit:self.keychainZoneID];
4021 [self expectCKKSTLKSelfShareUpload:self.keychainZoneID];
4022 [self saveTLKMaterialToKeychain:self.keychainZoneID];
4023
4024 // The first CKRecordZoneChanges should fail with a 'zone not found' error (race between zone creation as part of initalization and zone deletion from another device)
4025 [self.keychainZone failNextFetchWith:[[NSError alloc] initWithDomain:CKErrorDomain code:CKErrorZoneNotFound userInfo:@{}]];
4026
4027 [self startCKKSSubsystem];
4028
4029 XCTAssertEqual(0, [self.keychainView.keyHierarchyConditions[SecCKKSZoneKeyStateReady] wait:10*NSEC_PER_SEC], @"Key state should become 'ready'");
4030 }
4031
4032 - (void)testRecoverFromCloudKitOldChangeToken {
4033 // Test starts with nothing in database, but one in our fake CloudKit.
4034 [self putFakeKeyHierarchyInCloudKit:self.keychainZoneID];
4035
4036 // Spin up CKKS subsystem.
4037 [self startCKKSSubsystem];
4038
4039 // Now, save the TLK to the keychain (to simulate it coming in later via SOS).
4040 [self expectCKKSTLKSelfShareUpload:self.keychainZoneID];
4041 [self saveTLKMaterialToKeychainSimulatingSOS:self.keychainZoneID];
4042
4043 // We expect a single record to be uploaded
4044 [self expectCKModifyItemRecords: 1 currentKeyPointerRecords: 1 zoneID:self.keychainZoneID checkItem: [self checkClassCBlock:self.keychainZoneID message:@"Object was encrypted under class C key in hierarchy"]];
4045 [self addGenericPassword: @"data" account: @"account-delete-me"];
4046 OCMVerifyAllWithDelay(self.mockDatabase, 20);
4047
4048 // Delete all old database states, to destroy the change tag validity
4049 [self.keychainZone.pastDatabases removeAllObjects];
4050
4051 // We expect a total local flush and refetch
4052 self.silentFetchesAllowed = false;
4053 [self expectCKFetch]; // one to fail with a CKErrorChangeTokenExpired error
4054 [self expectCKFetch]; // and one to succeed
4055
4056 // Trigger a fake change notification
4057 [self.injectedManager.zoneChangeFetcher notifyZoneChange:nil];
4058
4059 OCMVerifyAllWithDelay(self.mockDatabase, 20);
4060
4061 // And check that a new upload happens just fine.
4062 [self expectCKModifyItemRecords: 1 currentKeyPointerRecords: 1 zoneID:self.keychainZoneID checkItem: [self checkClassABlock:self.keychainZoneID message:@"Object was encrypted under class A key in hierarchy"]];
4063 [self addGenericPassword:@"asdf"
4064 account:@"account-class-A"
4065 viewHint:nil
4066 access:(id)kSecAttrAccessibleWhenUnlocked
4067 expecting:errSecSuccess
4068 message:@"Adding class A item"];
4069 OCMVerifyAllWithDelay(self.mockDatabase, 20);
4070 }
4071
4072 - (void)testRecoverFromCloudKitUnknownDeviceStateRecord {
4073 // Test starts with nothing in database, but one in our fake CloudKit.
4074 [self putFakeKeyHierarchyInCloudKit:self.keychainZoneID];
4075 [self expectCKKSTLKSelfShareUpload:self.keychainZoneID];
4076 [self saveTLKMaterialToKeychainSimulatingSOS:self.keychainZoneID];
4077
4078 // Save a new device state record with some fake etag
4079 [self.keychainView dispatchSyncWithSQLTransaction:^CKKSDatabaseTransactionResult{
4080 CKKSDeviceStateEntry* cdse = [[CKKSDeviceStateEntry alloc] initForDevice:self.ckDeviceID
4081 osVersion:@"fake-record"
4082 lastUnlockTime:[NSDate date]
4083 octagonPeerID:nil
4084 octagonStatus:nil
4085 circlePeerID:self.mockSOSAdapter.selfPeer.peerID
4086 circleStatus:kSOSCCInCircle
4087 keyState:SecCKKSZoneKeyStateWaitForTLK
4088 currentTLKUUID:nil
4089 currentClassAUUID:nil
4090 currentClassCUUID:nil
4091 zoneID:self.keychainZoneID
4092 encodedCKRecord:nil];
4093 XCTAssertNotNil(cdse, "Should have created a fake CDSE");
4094 CKRecord* record = [cdse CKRecordWithZoneID:self.keychainZoneID];
4095 XCTAssertNotNil(record, "Should have created a fake CDSE CKRecord");
4096 record.etag = @"fake etag";
4097 cdse.storedCKRecord = record;
4098
4099 NSError* error = nil;
4100 [cdse saveToDatabase:&error];
4101 XCTAssertNil(error, @"No error saving cdse to database");
4102
4103 return CKKSDatabaseTransactionCommit;
4104 }];
4105
4106 // Spin up CKKS subsystem.
4107 [self startCKKSSubsystem];
4108
4109 // We expect a record failure, since the device state record is broke
4110 [self expectCKAtomicModifyItemRecordsUpdateFailure:self.keychainZoneID];
4111
4112 // And then we expect a clean write
4113 [self expectCKModifyItemRecords:1 currentKeyPointerRecords:1 zoneID:self.keychainZoneID
4114 checkItem:[self checkClassCBlock:self.keychainZoneID message:@"Object was encrypted under class C key in hierarchy"]];
4115
4116 [self addGenericPassword: @"data" account: @"account-delete-me"];
4117 OCMVerifyAllWithDelay(self.mockDatabase, 20);
4118 }
4119
4120 - (void)testRecoverFromCloudKitUnknownItemRecord {
4121 [self createAndSaveFakeKeyHierarchy: self.keychainZoneID]; // Make life easy for this test.
4122
4123 // Spin up CKKS subsystem.
4124 [self startCKKSSubsystem];
4125
4126 [self findGenericPassword:@"account-delete-me" expecting:errSecItemNotFound];
4127
4128 CKRecord* ckr = [self createFakeRecord: self.keychainZoneID recordName:@"7B598D31-F9C5-481E-98AC-5A507ACB2D85"];
4129 [self.keychainZone addToZone:ckr];
4130
4131 [self.injectedManager.zoneChangeFetcher notifyZoneChange:nil];
4132 [self.keychainView waitForFetchAndIncomingQueueProcessing];
4133
4134 [self findGenericPassword:@"account-delete-me" expecting:errSecSuccess];
4135
4136 // Delete the record from CloudKit, but miss the notification
4137 XCTAssertNil([self.keychainZone deleteCKRecordIDFromZone: ckr.recordID], "Deleting the record from fake CloudKit should succeed");
4138
4139 // Expect a failed upload when we modify the item
4140 [self expectCKAtomicModifyItemRecordsUpdateFailure:self.keychainZoneID];
4141 [self updateGenericPassword:@"never seen again" account:@"account-delete-me"];
4142 OCMVerifyAllWithDelay(self.mockDatabase, 20);
4143
4144 [self.keychainView waitUntilAllOperationsAreFinished];
4145
4146 // And the item should be disappeared from the local keychain
4147 [self findGenericPassword:@"account-delete-me" expecting:errSecItemNotFound];
4148 }
4149
4150 - (void)testRecoverFromCloudKitUserDeletedZone {
4151 // Test starts with nothing in database, but one in our fake CloudKit.
4152 [self putFakeKeyHierarchyInCloudKit:self.keychainZoneID];
4153
4154 // Spin up CKKS subsystem.
4155 [self startCKKSSubsystem];
4156
4157 // Now, save the TLK to the keychain (to simulate it coming in later via SOS).
4158 [self expectCKKSTLKSelfShareUpload:self.keychainZoneID];
4159 [self saveTLKMaterialToKeychainSimulatingSOS:self.keychainZoneID];
4160
4161 // We expect a single record to be uploaded
4162 [self expectCKModifyItemRecords: 1 currentKeyPointerRecords: 1 zoneID:self.keychainZoneID checkItem: [self checkClassCBlock:self.keychainZoneID message:@"Object was encrypted under class C key in hierarchy"]];
4163 [self addGenericPassword: @"data" account: @"account-delete-me"];
4164 OCMVerifyAllWithDelay(self.mockDatabase, 20);
4165
4166 // The first CKRecordZoneChanges should fail with a 'CKErrorUserDeletedZone' error. This will cause a local reset, ending up with zone re-creation.
4167 self.zones[self.keychainZoneID] = nil; // delete the zone
4168 self.keys[self.keychainZoneID] = nil;
4169 [self.keychainZone failNextFetchWith:[[NSError alloc] initWithDomain:CKErrorDomain code:CKErrorUserDeletedZone userInfo:@{}]];
4170
4171 // We expect CKKS to recreate the zone, then have octagon reupload the keys, and then the class C item upload
4172 [self expectCKModifyItemRecords: 1 currentKeyPointerRecords: 1 zoneID:self.keychainZoneID checkItem: [self checkClassCBlock:self.keychainZoneID message:@"Object was encrypted under class C key in hierarchy"]];
4173
4174 [self.injectedManager.zoneChangeFetcher notifyZoneChange:nil];
4175
4176 [self performOctagonTLKUpload:self.ckksViews];
4177
4178 OCMVerifyAllWithDelay(self.mockDatabase, 20);
4179
4180 // And check that a new upload occurs.
4181 [self expectCKModifyItemRecords: 1 currentKeyPointerRecords: 1 zoneID:self.keychainZoneID checkItem: [self checkClassABlock:self.keychainZoneID message:@"Object was encrypted under class A key in hierarchy"]];
4182
4183 [self addGenericPassword:@"asdf"
4184 account:@"account-class-A"
4185 viewHint:nil
4186 access:(id)kSecAttrAccessibleWhenUnlocked
4187 expecting:errSecSuccess
4188 message:@"Adding class A item"];
4189 OCMVerifyAllWithDelay(self.mockDatabase, 20);
4190 }
4191
4192 - (void)testRecoverFromCloudKitZoneNotFoundWithoutZoneDeletion {
4193 // Test starts with nothing in database, but one in our fake CloudKit.
4194 [self putFakeKeyHierarchyInCloudKit:self.keychainZoneID];
4195 [self putFakeDeviceStatusInCloudKit:self.keychainZoneID];
4196
4197 // Spin up CKKS subsystem.
4198 [self startCKKSSubsystem];
4199
4200 // Now, save the TLK to the keychain (to simulate it coming in later via SOS).
4201 [self expectCKKSTLKSelfShareUpload:self.keychainZoneID];
4202 [self saveTLKMaterialToKeychainSimulatingSOS:self.keychainZoneID];
4203
4204 // We expect a single record to be uploaded
4205 [self expectCKModifyItemRecords: 1 currentKeyPointerRecords: 1 zoneID:self.keychainZoneID checkItem: [self checkClassCBlock:self.keychainZoneID message:@"Object was encrypted under class C key in hierarchy"]];
4206 [self addGenericPassword: @"data" account: @"account-delete-me"];
4207 OCMVerifyAllWithDelay(self.mockDatabase, 20);
4208
4209 XCTAssertEqual(0, [self.keychainView.keyHierarchyConditions[SecCKKSZoneKeyStateReady] wait:20*NSEC_PER_SEC], "CKKS should enter 'ready'");
4210
4211 [self waitForCKModifications];
4212 [self.keychainView waitForOperationsOfClass:[CKKSScanLocalItemsOperation class]];
4213
4214 // The next CKRecordZoneChanges will fail with a 'zone not found' error.
4215 self.zones[self.keychainZoneID] = nil; // delete the zone
4216 self.keys[self.keychainZoneID] = nil;
4217
4218 // We expect CKKS to reset itself and recover, then have octagon upload the keys, and then the class C item upload
4219 [self expectCKModifyItemRecords: 1 currentKeyPointerRecords: 1 zoneID:self.keychainZoneID checkItem: [self checkClassCBlock:self.keychainZoneID message:@"Object was encrypted under class C key in hierarchy"]];
4220
4221 [self.injectedManager.zoneChangeFetcher notifyZoneChange:nil];
4222
4223 [self performOctagonTLKUpload:self.ckksViews];
4224 OCMVerifyAllWithDelay(self.mockDatabase, 20);
4225 [self waitForCKModifications];
4226
4227 // And check that a new upload occurs.
4228 [self expectCKModifyItemRecords: 1 currentKeyPointerRecords: 1 zoneID:self.keychainZoneID checkItem: [self checkClassABlock:self.keychainZoneID message:@"Object was encrypted under class A key in hierarchy"]];
4229
4230 [self addGenericPassword:@"asdf"
4231 account:@"account-class-A"
4232 viewHint:nil
4233 access:(id)kSecAttrAccessibleWhenUnlocked
4234 expecting:errSecSuccess
4235 message:@"Adding class A item"];
4236 OCMVerifyAllWithDelay(self.mockDatabase, 20);
4237 }
4238
4239 - (void)testRecoverFromCloudKitZoneNotFoundFetchBeforeSigninOccurs {
4240 self.zones[self.keychainZoneID] = nil; // delete the autocreated zone
4241
4242 // Before CKKS sign-in, it receives a fetch rpc
4243 XCTestExpectation *fetchReturns = [self expectationWithDescription:@"fetch returned"];
4244 [self.injectedManager rpcFetchAndProcessChanges:nil reply:^(NSError *result) {
4245 XCTAssertNil(result, "Should be no error fetching and processing changes");
4246 [fetchReturns fulfill];
4247 }];
4248
4249 [self startCKKSSubsystem];
4250
4251 [self performOctagonTLKUpload:self.ckksViews];
4252 XCTAssertEqual(0, [self.keychainView.keyHierarchyConditions[SecCKKSZoneKeyStateReady] wait:20*NSEC_PER_SEC], "CKKS should enter 'ready'");
4253
4254 // We expect a single record to be uploaded
4255 [self expectCKModifyItemRecords:1 currentKeyPointerRecords:1 zoneID:self.keychainZoneID
4256 checkItem:[self checkClassCBlock:self.keychainZoneID message:@"Object was encrypted under class C key in hierarchy"]];
4257 [self addGenericPassword: @"data" account: @"account-delete-me"];
4258 OCMVerifyAllWithDelay(self.mockDatabase, 20);
4259
4260 // The fetch should have come back by now
4261 [self waitForExpectations: @[fetchReturns] timeout:5];
4262 }
4263
4264 - (void)testNoCloudKitAccount {
4265 // Test starts with nothing in database and the user logged out of CloudKit. We expect no CKKS operations.
4266 self.accountStatus = CKAccountStatusNoAccount;
4267 self.mockSOSAdapter.circleStatus = kSOSCCNotInCircle;
4268
4269 self.silentFetchesAllowed = false;
4270 [self startCKKSSubsystem];
4271
4272 OCMVerifyAllWithDelay(self.mockDatabase, 20);
4273
4274 [self addGenericPassword: @"data" account: @"account-delete-me"];
4275
4276 // simulate a NSNotification callback (but still logged out)
4277 self.accountStatus = CKAccountStatusNoAccount;
4278 [self.accountStateTracker notifyCKAccountStatusChangeAndWaitForSignal];
4279
4280 // There should be no further uploads, even when we save keychain items
4281 [self addGenericPassword: @"data" account: @"account-delete-me-2"];
4282 [self addGenericPassword: @"data" account: @"account-delete-me-3"];
4283
4284 OCMVerifyAllWithDelay(self.mockDatabase, 20);
4285
4286 // Test that there are no items in the database (since we never logged in)
4287 [self checkNoCKKSData: self.keychainView];
4288 }
4289
4290 - (void)testSACloudKitAccount {
4291 // Test starts with nothing in database and the user logged into CloudKit and in circle, but the account is not HSA2.
4292 self.mockSOSAdapter.circleStatus = kSOSCCInCircle;
4293
4294 self.accountStatus = CKAccountStatusAvailable;
4295
4296 self.silentFetchesAllowed = false;
4297
4298 // Octagon does not initialize the ckks views when not in an HSA2 account
4299 self.automaticallyBeginCKKSViewCloudKitOperation = false;
4300 [self startCKKSSubsystem];
4301
4302 [self.accountStateTracker notifyCKAccountStatusChangeAndWaitForSignal];
4303
4304 OCMVerifyAllWithDelay(self.mockDatabase, 20);
4305 XCTAssertEqual(0, [self.keychainView.keyHierarchyConditions[SecCKKSZoneKeyStateWaitForCloudKitAccountStatus] wait:20*NSEC_PER_SEC], "CKKS should enter 'waitforcloudkitaccount'");
4306
4307 // There should be no uploads, even when we save keychain items and enter/exit circle
4308 [self addGenericPassword: @"data" account: @"account-delete-me"];
4309
4310 self.mockSOSAdapter.circleStatus = kSOSCCNotInCircle;
4311 [self.accountStateTracker notifyCircleStatusChangeAndWaitForSignal];
4312 [self endSOSTrustedOperationForAllViews];
4313 [self addGenericPassword: @"data" account: @"account-delete-me-2"];
4314
4315 self.mockSOSAdapter.circleStatus = kSOSCCInCircle;
4316 [self.accountStateTracker notifyCircleStatusChangeAndWaitForSignal];
4317 [self beginSOSTrustedViewOperation:self.keychainView];
4318 [self addGenericPassword: @"data" account: @"account-delete-me-3"];
4319
4320 OCMVerifyAllWithDelay(self.mockDatabase, 20);
4321
4322 // Test that there are no items in the database (since we never were in an HSA2 account)
4323 [self checkNoCKKSData: self.keychainView];
4324 }
4325
4326 - (void)testEarlyLogin
4327 {
4328 self.mockSOSAdapter.circleStatus = kSOSCCNotInCircle;
4329
4330 // Octagon should initialize these views
4331 self.automaticallyBeginCKKSViewCloudKitOperation = true;
4332
4333 self.accountStatus = CKAccountStatusAvailable;
4334 //[self.accountStateTracker notifyCKAccountStatusChangeAndWaitForSignal];
4335
4336 [self startCKKSSubsystem];
4337
4338 // CKKS should end up in 'waitfortlkcreation', as there's no trust and no TLKs
4339 XCTAssertEqual(0, [self.keychainView.keyHierarchyConditions[SecCKKSZoneKeyStateWaitForTLKCreation] wait:20*NSEC_PER_SEC], "CKKS entered 'waitfortlkcreation'");
4340
4341 // Now, renotify the account status, and ensure that CKKS doesn't reenter 'initializing'
4342 CKKSCondition* initializing = self.keychainView.keyHierarchyConditions[SecCKKSZoneKeyStateInitializing];
4343
4344 [self.accountStateTracker notifyCKAccountStatusChangeAndWaitForSignal];
4345
4346 XCTAssertNotEqual(0, [initializing wait:500*NSEC_PER_MSEC], "CKKS should not enter initializing when the device HSA status changes");
4347 }
4348
4349 - (void)testNoCircle {
4350 // Test starts with nothing in database and the user logged into CloudKit, but out of Circle.
4351 self.mockSOSAdapter.circleStatus = kSOSCCNotInCircle;
4352
4353 self.accountStatus = CKAccountStatusAvailable;
4354
4355 [self startCKKSSubsystem];
4356
4357 [self.accountStateTracker notifyCKAccountStatusChangeAndWaitForSignal];
4358
4359 OCMVerifyAllWithDelay(self.mockDatabase, 20);
4360
4361 [self addGenericPassword: @"data" account: @"account-delete-me"];
4362
4363 XCTAssertEqual(0, [self.keychainView.keyHierarchyConditions[SecCKKSZoneKeyStateWaitForTLKCreation] wait:20*NSEC_PER_SEC], "CKKS entered 'waitfortlkcreation'");
4364
4365 // simulate a NSNotification callback (but still logged out)
4366 self.accountStatus = CKAccountStatusNoAccount;
4367 [self.accountStateTracker notifyCKAccountStatusChangeAndWaitForSignal];
4368
4369 XCTAssertEqual(0, [self.keychainView.keyHierarchyConditions[SecCKKSZoneKeyStateLoggedOut] wait:20*NSEC_PER_SEC], "CKKS entered 'loggedout'");
4370
4371 // There should be no further uploads, even when we save keychain items
4372 [self addGenericPassword: @"data" account: @"account-delete-me-2"];
4373 [self addGenericPassword: @"data" account: @"account-delete-me-3"];
4374
4375 OCMVerifyAllWithDelay(self.mockDatabase, 20);
4376
4377 // Test that there are no items in the database (since we never logged in)
4378 [self checkNoCKKSData: self.keychainView];
4379 }
4380
4381 - (void)testCircleDepartAndRejoin {
4382 // Test starts with CKKS in ready
4383 [self createAndSaveFakeKeyHierarchy: self.keychainZoneID]; // Make life easy for this test.
4384 [self startCKKSSubsystem];
4385
4386 XCTAssertEqual(0, [self.keychainView.keyHierarchyConditions[SecCKKSZoneKeyStateReady] wait:20*NSEC_PER_SEC], @"Key state should have arrived at ready");
4387 [self.keychainView waitForOperationsOfClass:[CKKSOutgoingQueueOperation class]];
4388
4389 // But then, trust departs
4390 self.mockSOSAdapter.circleStatus = kSOSCCNotInCircle;
4391 [self.accountStateTracker notifyCircleStatusChangeAndWaitForSignal];
4392 [self endSOSTrustedOperationForAllViews];
4393
4394 XCTAssertEqual(0, [self.keychainView.keyHierarchyConditions[SecCKKSZoneKeyStateWaitForTrust] wait:20*NSEC_PER_SEC], "CKKS entered 'waitfortrust'");
4395
4396 // There should be no further uploads, even when we save keychain items
4397 [self addGenericPassword: @"data" account: @"account-delete-me-2"];
4398 [self addGenericPassword: @"data" account: @"account-delete-me-3"];
4399
4400 // Then trust returns. We expect two uploads
4401 [self expectCKModifyItemRecords:2 currentKeyPointerRecords: 1 zoneID:self.keychainZoneID
4402 checkItem: [self checkClassCBlock:self.keychainZoneID message:@"Object was encrypted under class C key in hierarchy"]];
4403 self.mockSOSAdapter.circleStatus = kSOSCCInCircle;
4404 [self.accountStateTracker notifyCircleStatusChangeAndWaitForSignal];
4405 [self beginSOSTrustedViewOperation:self.keychainView];
4406
4407 XCTAssertEqual(0, [self.keychainView.keyHierarchyConditions[SecCKKSZoneKeyStateReady] wait:20*NSEC_PER_SEC], "CKKS entered 'ready'");
4408 OCMVerifyAllWithDelay(self.mockDatabase, 20);
4409 }
4410
4411 - (void)testCloudKitLogin {
4412 // Test starts with nothing in database and the user logged out of CloudKit. We expect no CKKS operations.
4413 self.accountStatus = CKAccountStatusNoAccount;
4414 self.mockSOSAdapter.circleStatus = kSOSCCNotInCircle;
4415
4416 // Before we inform CKKS of its account state....
4417 XCTAssertNotEqual(0, [self.keychainView.accountStateKnown wait:50*NSEC_PER_MSEC], "CKK shouldn't know the account state");
4418
4419 [self startCKKSSubsystem];
4420
4421 XCTAssertEqual(0, [self.keychainView.loggedOut wait:500*NSEC_PER_MSEC], "Should have been told of a 'logout' event on startup");
4422 XCTAssertNotEqual(0, [self.keychainView.loggedIn wait:100*NSEC_PER_MSEC], "'login' event shouldn't have happened");
4423 XCTAssertEqual(0, [self.keychainView.accountStateKnown wait:50*NSEC_PER_MSEC], "CKK should know the account state");
4424
4425 OCMVerifyAllWithDelay(self.mockDatabase, 20);
4426
4427 // simulate a cloudkit login and NSNotification callback
4428 self.accountStatus = CKAccountStatusAvailable;
4429 [self.accountStateTracker notifyCKAccountStatusChangeAndWaitForSignal];
4430
4431 // No writes yet, since we're not in circle
4432 XCTAssertEqual(0, [self.keychainView.keyHierarchyConditions[SecCKKSZoneKeyStateWaitForTLKCreation] wait:20*NSEC_PER_SEC], "CKKS entered 'waitfortlkcreation'");
4433
4434 self.mockSOSAdapter.circleStatus = kSOSCCInCircle;
4435 [self.accountStateTracker notifyCircleStatusChangeAndWaitForSignal];
4436 [self beginSOSTrustedOperationForAllViews];
4437
4438 [self performOctagonTLKUpload:self.ckksViews];
4439
4440 XCTAssertEqual(0, [self.keychainView.loggedIn wait:2000*NSEC_PER_MSEC], "Should have been told of a 'login'");
4441 XCTAssertNotEqual(0, [self.keychainView.loggedOut wait:100*NSEC_PER_MSEC], "'logout' event should be reset");
4442 XCTAssertEqual(0, [self.keychainView.accountStateKnown wait:50*NSEC_PER_MSEC], "CKK should know the account state");
4443
4444 OCMVerifyAllWithDelay(self.mockDatabase, 20);
4445 [self waitForCKModifications];
4446
4447 // We expect a single class C record to be uploaded.
4448 [self expectCKModifyItemRecords: 1 currentKeyPointerRecords: 1 zoneID:self.keychainZoneID checkItem: [self checkClassCBlock:self.keychainZoneID message:@"Object was encrypted under class C key in hierarchy"]];
4449 [self addGenericPassword: @"data" account: @"account-delete-me"];
4450
4451 OCMVerifyAllWithDelay(self.mockDatabase, 20);
4452 [self waitForCKModifications];
4453 }
4454
4455 - (void)testCloudKitLogoutLogin {
4456 XCTAssertNotEqual(0, [self.keychainView.accountStateKnown wait:50*NSEC_PER_MSEC], "CKK shouldn't know the account state");
4457 [self startCKKSSubsystem];
4458 [self performOctagonTLKUpload:self.ckksViews];
4459 XCTAssertEqual(0, [self.keychainView.loggedIn wait:2000*NSEC_PER_MSEC], "Should have been told of a 'login'");
4460 XCTAssertNotEqual(0, [self.keychainView.loggedOut wait:100*NSEC_PER_MSEC], "'logout' event should be reset");
4461 XCTAssertEqual(0, [self.keychainView.accountStateKnown wait:50*NSEC_PER_MSEC], "CKK should know the account state");
4462
4463 OCMVerifyAllWithDelay(self.mockDatabase, 20);
4464 [self waitForCKModifications];
4465
4466 // We expect a single class C record to be uploaded.
4467 [self expectCKModifyItemRecords: 1 currentKeyPointerRecords: 1 zoneID:self.keychainZoneID checkItem: [self checkClassCBlock:self.keychainZoneID message:@"Object was encrypted under class C key in hierarchy"]];
4468 [self addGenericPassword: @"data" account: @"account-delete-me"];
4469
4470 OCMVerifyAllWithDelay(self.mockDatabase, 20);
4471 [self waitForCKModifications];
4472
4473 // simulate a cloudkit logout and NSNotification callback
4474 self.accountStatus = CKAccountStatusNoAccount;
4475 [self.accountStateTracker notifyCKAccountStatusChangeAndWaitForSignal];
4476 self.mockSOSAdapter.circleStatus = kSOSCCNotInCircle;
4477 [self.accountStateTracker notifyCircleStatusChangeAndWaitForSignal];
4478 [self endSOSTrustedOperationForAllViews];
4479
4480 // Test that there are no items in the database after logout
4481 XCTAssertEqual(0, [self.keychainView.loggedOut wait:2000*NSEC_PER_MSEC], "Should have been told of a 'logout'");
4482 XCTAssertNotEqual(0, [self.keychainView.loggedIn wait:100*NSEC_PER_MSEC], "'login' event should be reset");
4483 XCTAssertEqual(0, [self.keychainView.accountStateKnown wait:50*NSEC_PER_MSEC], "CKK should know the account state");
4484 [self checkNoCKKSData: self.keychainView];
4485
4486 // There should be no further uploads, even when we save keychain items
4487 [self addGenericPassword: @"data" account: @"account-delete-me-2"];
4488 [self addGenericPassword: @"data" account: @"account-delete-me-3"];
4489
4490 [self.keychainView waitForOperationsOfClass:[CKKSOutgoingQueueOperation class]];
4491 OCMVerifyAllWithDelay(self.mockDatabase, 20);
4492 XCTAssertEqual(0, [self.keychainView.keyHierarchyConditions[SecCKKSZoneKeyStateLoggedOut] wait:20*NSEC_PER_SEC], "CKKS entered 'logged out'");
4493
4494 // simulate a cloudkit login
4495 // We should expect CKKS to re-find the key hierarchy it already uploaded, and then send up the two records we added during the pause
4496 [self expectCKModifyItemRecords: 2 currentKeyPointerRecords: 1 zoneID:self.keychainZoneID];
4497
4498 self.accountStatus = CKAccountStatusAvailable;
4499 [self.accountStateTracker notifyCKAccountStatusChangeAndWaitForSignal];
4500
4501 self.mockSOSAdapter.circleStatus = kSOSCCInCircle;
4502 [self.accountStateTracker notifyCircleStatusChangeAndWaitForSignal];
4503 [self beginSOSTrustedViewOperation:self.keychainView];
4504
4505 XCTAssertEqual(0, [self.keychainView.loggedIn wait:2000*NSEC_PER_MSEC], "Should have been told of a 'login'");
4506 XCTAssertNotEqual(0, [self.keychainView.loggedOut wait:100*NSEC_PER_MSEC], "'logout' event should be reset");
4507 XCTAssertEqual(0, [self.keychainView.accountStateKnown wait:50*NSEC_PER_MSEC], "CKK should know the account state");
4508
4509 OCMVerifyAllWithDelay(self.mockDatabase, 20);
4510
4511 // Let everything settle...
4512 XCTAssertEqual(0, [self.keychainView.keyHierarchyConditions[SecCKKSZoneKeyStateReady] wait:20*NSEC_PER_SEC], "CKKS entered 'ready'");
4513 [self waitForCKModifications];
4514
4515 // Logout again
4516 self.accountStatus = CKAccountStatusNoAccount;
4517 [self.accountStateTracker notifyCKAccountStatusChangeAndWaitForSignal];
4518
4519 self.mockSOSAdapter.circleStatus = kSOSCCNotInCircle;
4520 [self.accountStateTracker notifyCircleStatusChangeAndWaitForSignal];
4521 [self endSOSTrustedOperationForAllViews];
4522
4523 // Test that there are no items in the database after logout
4524 XCTAssertEqual(0, [self.keychainView.loggedOut wait:2000*NSEC_PER_MSEC], "Should have been told of a 'logout'");
4525 XCTAssertNotEqual(0, [self.keychainView.loggedIn wait:100*NSEC_PER_MSEC], "'login' event should be reset");
4526 XCTAssertEqual(0, [self.keychainView.accountStateKnown wait:50*NSEC_PER_MSEC], "CKK should know the account state");
4527 [self checkNoCKKSData: self.keychainView];
4528
4529 // There should be no further uploads, even when we save keychain items
4530 [self addGenericPassword: @"data" account: @"account-delete-me-5"];
4531 [self addGenericPassword: @"data" account: @"account-delete-me-6"];
4532
4533 [self.keychainView waitForOperationsOfClass:[CKKSOutgoingQueueOperation class]];
4534 OCMVerifyAllWithDelay(self.mockDatabase, 20);
4535
4536 // simulate a cloudkit login
4537 // We should expect CKKS to re-find the key hierarchy it already uploaded, and then send up the two records we added during the pause
4538 [self expectCKModifyItemRecords: 2 currentKeyPointerRecords: 1 zoneID:self.keychainZoneID];
4539
4540 self.accountStatus = CKAccountStatusAvailable;
4541 [self.accountStateTracker notifyCKAccountStatusChangeAndWaitForSignal];
4542
4543 self.mockSOSAdapter.circleStatus = kSOSCCInCircle;
4544 [self.accountStateTracker notifyCircleStatusChangeAndWaitForSignal];
4545 [self beginSOSTrustedViewOperation:self.keychainView];
4546
4547 XCTAssertEqual(0, [self.keychainView.loggedIn wait:2000*NSEC_PER_MSEC], "Should have been told of a 'login'");
4548 XCTAssertNotEqual(0, [self.keychainView.loggedOut wait:100*NSEC_PER_MSEC], "'logout' event should be reset");
4549 XCTAssertEqual(0, [self.keychainView.accountStateKnown wait:50*NSEC_PER_MSEC], "CKK should know the account state");
4550
4551 OCMVerifyAllWithDelay(self.mockDatabase, 20);
4552
4553 // Let everything settle...
4554 [self.keychainView waitUntilAllOperationsAreFinished];
4555 XCTAssertEqual(0, [self.keychainView.keyHierarchyConditions[SecCKKSZoneKeyStateReady] wait:20*NSEC_PER_SEC], "CKKS entered 'ready'");
4556
4557 // Logout again
4558 self.accountStatus = CKAccountStatusNoAccount;
4559 [self.accountStateTracker notifyCKAccountStatusChangeAndWaitForSignal];
4560
4561 self.mockSOSAdapter.circleStatus = kSOSCCNotInCircle;
4562 [self.accountStateTracker notifyCircleStatusChangeAndWaitForSignal];
4563 [self endSOSTrustedOperationForAllViews];
4564
4565 // Test that there are no items in the database after logout
4566 XCTAssertEqual(0, [self.keychainView.loggedOut wait:2000*NSEC_PER_MSEC], "Should have been told of a 'logout'");
4567 XCTAssertNotEqual(0, [self.keychainView.loggedIn wait:100*NSEC_PER_MSEC], "'login' event should be reset");
4568 XCTAssertEqual(0, [self.keychainView.accountStateKnown wait:50*NSEC_PER_MSEC], "CKK should know the account state");
4569 XCTAssertEqual(0, [self.keychainView.keyHierarchyConditions[SecCKKSZoneKeyStateLoggedOut] wait:20*NSEC_PER_SEC], "CKKS entered 'logged out'");
4570 [self checkNoCKKSData: self.keychainView];
4571
4572 // Force zone into error state
4573 OctagonStateTransitionOperation* transitionOp = [OctagonStateTransitionOperation named:@"enter" entering:SecCKKSZoneKeyStateError];
4574 OctagonStateTransitionRequest* request = [[OctagonStateTransitionRequest alloc] init:@"enter-wait-for-trust"
4575 sourceStates:[NSSet setWithArray:[CKKSZoneKeyStateMap() allKeys]]
4576 serialQueue:self.keychainView.queue
4577 timeout:10 * NSEC_PER_SEC
4578 transitionOp:transitionOp];
4579 [self.keychainView.stateMachine handleExternalRequest:request];
4580 XCTAssertEqual(0, [self.keychainView.keyHierarchyConditions[SecCKKSZoneKeyStateError] wait:20*NSEC_PER_SEC], "CKKS entered 'error'");
4581
4582 self.accountStatus = CKAccountStatusAvailable;
4583 [self.accountStateTracker notifyCKAccountStatusChangeAndWaitForSignal];
4584
4585 self.mockSOSAdapter.circleStatus = kSOSCCInCircle;
4586 [self.accountStateTracker notifyCircleStatusChangeAndWaitForSignal];
4587 [self beginSOSTrustedViewOperation:self.keychainView];
4588
4589 XCTestExpectation *operationRun = [self expectationWithDescription:@"operation run"];
4590 NSOperation* op = [NSBlockOperation named:@"test" withBlock:^{
4591 [operationRun fulfill];
4592 }];
4593
4594 [op addDependency:self.keychainView.keyStateReadyDependency];
4595 [self.operationQueue addOperation:op];
4596
4597 XCTAssertEqual(0, [self.keychainView.keyHierarchyConditions[SecCKKSZoneKeyStateReady] wait:20*NSEC_PER_SEC], "CKKS entered 'ready'");
4598
4599 XCTAssertEqual(0, [self.keychainView.loggedIn wait:2000*NSEC_PER_MSEC], "Should have been told of a 'login'");
4600 XCTAssertNotEqual(0, [self.keychainView.loggedOut wait:100*NSEC_PER_MSEC], "'logout' event should be reset");
4601 XCTAssertEqual(0, [self.keychainView.accountStateKnown wait:50*NSEC_PER_MSEC], "CKK should know the account state");
4602
4603 OCMVerifyAllWithDelay(self.mockDatabase, 20);
4604 [self waitForExpectations: @[operationRun] timeout:10];
4605 XCTAssertEqual(0, [self.keychainView.keyHierarchyConditions[SecCKKSZoneKeyStateReady] wait:20*NSEC_PER_SEC], "CKKS entered 'ready'");
4606 }
4607
4608 - (void)testCloudKitLogoutDueToGreyMode {
4609 XCTAssertNotEqual(0, [self.keychainView.accountStateKnown wait:50*NSEC_PER_MSEC], "CKK shouldn't know the account state");
4610 [self startCKKSSubsystem];
4611 [self performOctagonTLKUpload:self.ckksViews];
4612 XCTAssertEqual(0, [self.keychainView.loggedIn wait:20*NSEC_PER_SEC], "Should have been told of a 'login'");
4613 XCTAssertNotEqual(0, [self.keychainView.loggedOut wait:50*NSEC_PER_MSEC], "'logout' event should be reset");
4614 XCTAssertEqual(0, [self.keychainView.accountStateKnown wait:50*NSEC_PER_MSEC], "CKK should know the account state");
4615
4616 XCTAssertEqual(0, [self.keychainView.keyHierarchyConditions[SecCKKSZoneKeyStateReady] wait:20*NSEC_PER_SEC], @"Key state should become 'ready'");
4617
4618 OCMVerifyAllWithDelay(self.mockDatabase, 20);
4619 [self waitForCKModifications];
4620
4621 // simulate a cloudkit grey mode switch and NSNotification callback. CKKS should treat this as a logout
4622 self.iCloudHasValidCredentials = false;
4623 [self.accountStateTracker notifyCKAccountStatusChangeAndWaitForSignal];
4624
4625 // Test that there are no items in the database after logout
4626 XCTAssertEqual(0, [self.keychainView.loggedOut wait:20*NSEC_PER_SEC], "Should have been told of a 'logout'");
4627 XCTAssertNotEqual(0, [self.keychainView.loggedIn wait:50*NSEC_PER_MSEC], "'login' event should be reset");
4628 XCTAssertEqual(0, [self.keychainView.accountStateKnown wait:50*NSEC_PER_MSEC], "CKK should know the account state");
4629 XCTAssertEqual(0, [self.keychainView.keyHierarchyConditions[SecCKKSZoneKeyStateLoggedOut] wait:20*NSEC_PER_SEC], "CKKS entered 'logged out'");
4630 [self checkNoCKKSData:self.keychainView];
4631
4632 // There should be no further uploads, even when we save keychain items
4633 [self addGenericPassword: @"data" account: @"account-delete-me-2"];
4634 [self addGenericPassword: @"data" account: @"account-delete-me-3"];
4635
4636 [self.keychainView waitForOperationsOfClass:[CKKSOutgoingQueueOperation class]];
4637 [self.keychainView waitForOperationsOfClass:[CKKSIncomingQueueOperation class]];
4638 OCMVerifyAllWithDelay(self.mockDatabase, 20);
4639
4640 // Also, fetches shouldn't occur
4641 self.silentFetchesAllowed = false;
4642 NSOperation* op = [self.keychainView.zoneChangeFetcher requestSuccessfulFetch:CKKSFetchBecauseTesting];
4643 CKKSResultOperation* timeoutOp = [CKKSResultOperation named:@"timeout" withBlock:^{}];
4644 [timeoutOp addDependency:op];
4645 [timeoutOp timeout:4*NSEC_PER_SEC];
4646 [self.operationQueue addOperation:timeoutOp];
4647 [timeoutOp waitUntilFinished];
4648
4649 // CloudKit figures its life out. We expect the two passwords from before to be uploaded
4650 [self expectCKModifyItemRecords:2 currentKeyPointerRecords:1 zoneID:self.keychainZoneID];
4651 self.silentFetchesAllowed = true;
4652 self.iCloudHasValidCredentials = true;
4653 [self.accountStateTracker notifyCKAccountStatusChangeAndWaitForSignal];
4654
4655 XCTAssertEqual(0, [self.keychainView.loggedIn wait:20*NSEC_PER_SEC], "Should have been told of a 'login'");
4656 XCTAssertNotEqual(0, [self.keychainView.loggedOut wait:50*NSEC_PER_MSEC], "'logout' event should be reset");
4657 XCTAssertEqual(0, [self.keychainView.accountStateKnown wait:50*NSEC_PER_MSEC], "CKK should know the account state");
4658 OCMVerifyAllWithDelay(self.mockDatabase, 20);
4659
4660 // And fetching still works!
4661 [self.keychainZone addToZone: [self createFakeRecord: self.keychainZoneID recordName:@"7B598D31-F9C5-481E-98AC-5A507ACB2D00" withAccount:@"account0"]];
4662 [self.injectedManager.zoneChangeFetcher notifyZoneChange:nil];
4663 [self.keychainView waitForFetchAndIncomingQueueProcessing];
4664 [self findGenericPassword: @"account0" expecting:errSecSuccess];
4665 XCTAssertEqual(0, [self.keychainView.keyHierarchyConditions[SecCKKSZoneKeyStateReady] wait:20*NSEC_PER_SEC], "CKKS entered 'ready'");
4666 }
4667
4668 - (void)testCloudKitLoginRace {
4669 // Test starts with nothing in database, and 'in circle', but securityd hasn't received notification if we're logged into CloudKit.
4670 // CKKS should call handleLogout, as the CK account is not present.
4671
4672 // note: don't unblock the ck account state object yet...
4673
4674 self.mockSOSAdapter.circleStatus = kSOSCCInCircle;
4675 [self.accountStateTracker notifyCircleStatusChangeAndWaitForSignal];
4676
4677 // Add a keychain item, and make sure it doesn't upload yet.
4678 [self addGenericPassword: @"data" account: @"account-delete-me"];
4679 XCTAssertEqual(0, [self.keychainView.keyHierarchyConditions[SecCKKSZoneKeyStateWaitForCloudKitAccountStatus] wait:20*NSEC_PER_SEC], "CKKS entered 'waitforcloudkitaccount'");
4680 XCTAssertNotEqual(0, [self.keychainView.keyHierarchyConditions[SecCKKSZoneKeyStateLoggedOut] wait:1*NSEC_PER_SEC], "CKKS shouldn't have entered 'waitforcloudkitaccount'");
4681
4682 OCMVerifyAllWithDelay(self.mockDatabase, 20);
4683
4684 // Now that we're here (and logged out), bring the account up
4685
4686 // We expect a single class C record to be uploaded.
4687 [self expectCKModifyItemRecords: 1 currentKeyPointerRecords: 1 zoneID:self.keychainZoneID checkItem: [self checkClassCBlock:self.keychainZoneID message:@"Object was encrypted under class C key in hierarchy"]];
4688
4689 self.accountStatus = CKAccountStatusAvailable;
4690 [self startCKKSSubsystem];
4691 [self performOctagonTLKUpload:self.ckksViews];
4692
4693 // simulate another NSNotification callback
4694 [self.accountStateTracker notifyCKAccountStatusChangeAndWaitForSignal];
4695
4696 OCMVerifyAllWithDelay(self.mockDatabase, 20);
4697 [self waitForCKModifications];
4698
4699 // Make sure new items upload too
4700 [self expectCKModifyItemRecords: 1 currentKeyPointerRecords: 1 zoneID:self.keychainZoneID checkItem: [self checkClassCBlock:self.keychainZoneID message:@"Object was encrypted under class C key in hierarchy"]];
4701 [self addGenericPassword: @"data" account: @"account-delete-me-2"];
4702 OCMVerifyAllWithDelay(self.mockDatabase, 20);
4703
4704 [self.keychainView waitUntilAllOperationsAreFinished];
4705 [self waitForCKModifications];
4706 [self.keychainView halt];
4707 }
4708
4709 - (void)testDontLogOutIfBeforeFirstUnlock {
4710 /*
4711 // test starts as if a previously logged-in device has just rebooted
4712 self.aksLockState = true;
4713 self.accountStatus = CKAccountStatusAvailable;
4714
4715 // This is the original state of the account tracker
4716 self.circleStatus = [[SOSAccountStatus alloc] init:kSOSCCError error:nil];
4717 [self.accountStateTracker notifyCircleStatusChangeAndWaitForSignal];
4718
4719 // And this is what the first circle status fetch will actually return
4720 self.circleStatus = [[SOSAccountStatus alloc] init:kSOSCCError error:[NSError errorWithDomain:(__bridge id)kSOSErrorDomain code:kSOSErrorNotReady description:@"fake error: device is locked, so SOS doesn't know if it's in-circle"]];
4721 [self.accountStateTracker notifyCircleStatusChangeAndWaitForSignal];
4722
4723 XCTAssertEqual(self.accountStateTracker.currentComputedAccountStatus, CKKSAccountStatusUnknown, "Account tracker status should just be 'unknown'");
4724 XCTAssertNotEqual(0, [self.keychainView.accountStateKnown wait:50*NSEC_PER_MSEC], "CKKS should not yet know the CK account state");
4725
4726 [self startCKKSSubsystem];
4727
4728 XCTAssertEqual(0, [self.keychainView.loggedIn wait:8*NSEC_PER_SEC], "'login' event should have happened");
4729 XCTAssertNotEqual(0, [self.keychainView.loggedOut wait:10*NSEC_PER_MSEC], "Should not have been told of a CK 'logout' event on startup");
4730 XCTAssertEqual(0, [self.keychainView.accountStateKnown wait:1*NSEC_PER_SEC], "CKKS should know the account state");
4731
4732 // And assume another CK status change
4733 [self.accountStateTracker notifyCKAccountStatusChangeAndWaitForSignal];
4734 XCTAssertEqual(self.accountStateTracker.currentComputedAccountStatus, CKKSAccountStatusUnknown, "Account tracker status should just be 'no account'");
4735 XCTAssertEqual(0, [self.keychainView.accountStateKnown wait:50*NSEC_PER_MSEC], "CKKS should know the CK account state");
4736
4737 [self expectCKModifyKeyRecords: 3 currentKeyPointerRecords: 3 tlkShareRecords: 1 zoneID:self.keychainZoneID];
4738
4739 self.aksLockState = false;
4740
4741 self.mockSOSAdapter.circleStatus = kSOSCCInCircle;
4742 [self.accountStateTracker notifyCircleStatusChangeAndWaitForSignal];
4743 [self beginSOSTrustedViewOperation:self.keychainView];
4744
4745 XCTAssertEqual(0, [self.keychainView.loggedIn wait:2000*NSEC_PER_MSEC], "Should have been told of a 'login'");
4746 XCTAssertNotEqual(0, [self.keychainView.loggedOut wait:100*NSEC_PER_MSEC], "'logout' event should be reset");
4747 XCTAssertEqual(0, [self.keychainView.accountStateKnown wait:50*NSEC_PER_MSEC], "CKK should know the account state");
4748
4749 OCMVerifyAllWithDelay(self.mockDatabase, 20);
4750 [self waitForCKModifications];
4751
4752 // We expect a single class C record to be uploaded.
4753 [self expectCKModifyItemRecords: 1 currentKeyPointerRecords: 1 zoneID:self.keychainZoneID checkItem: [self checkClassCBlock:self.keychainZoneID message:@"Object was encrypted under class C key in hierarchy"]];
4754 [self addGenericPassword: @"data" account: @"account-delete-me"];
4755
4756 OCMVerifyAllWithDelay(self.mockDatabase, 20);
4757 [self waitForCKModifications];*/
4758 }
4759
4760 - (void)testSyncableItemsAddedWhileLoggedOut {
4761 // Test that once CKKS is up and 'logged out', nothing happens when syncable items are added
4762 self.accountStatus = CKAccountStatusNoAccount;
4763 [self startCKKSSubsystem];
4764
4765 XCTAssertEqual([self.keychainView.loggedOut wait:500*NSEC_PER_MSEC], 0, "CKKS should be told that it's logged out");
4766
4767 // CKKS shouldn't decide to poke its state machine, but it should still send the notification
4768 XCTestExpectation* viewChangeNotification = [self expectChangeForView:self.keychainZoneID.zoneName];
4769
4770 [self addGenericPassword: @"data" account: @"account-delete-me-2"];
4771
4772 [self waitForExpectations:@[viewChangeNotification] timeout:8];
4773 }
4774
4775 - (void)testUploadSyncableItemsAddedWhileUntrusted {
4776 self.mockSOSAdapter.circleStatus = kSOSCCNotInCircle;
4777 [self.accountStateTracker notifyCircleStatusChangeAndWaitForSignal];
4778
4779 [self putFakeKeyHierarchyInCloudKit:self.keychainZoneID];
4780
4781 [self startCKKSSubsystem];
4782
4783 XCTAssertEqual([self.keychainView.loggedIn wait:500*NSEC_PER_MSEC], 0, "CKKS should be told that it's logged in");
4784
4785 XCTAssertEqual(0, [self.keychainView.keyHierarchyConditions[SecCKKSZoneKeyStateWaitForTrust] wait:20*NSEC_PER_SEC], "CKKS entered waitfortrust");
4786 OCMVerifyAllWithDelay(self.mockDatabase, 20);
4787
4788 [self addGenericPassword: @"data" account: @"account-delete-me-2"];
4789
4790 NSError* error = nil;
4791 NSDictionary* currentOQEs = [CKKSOutgoingQueueEntry countsByStateInZone:self.keychainZoneID error:&error];
4792 XCTAssertNil(error, "Should be no error counting OQEs");
4793 XCTAssertEqual(0, currentOQEs.count, "Should be no OQEs");
4794
4795 // Now, insert a restart to simulate securityd restarting (and throwing away all pending operations), then a real sign in
4796 self.keychainView = [[CKKSViewManager manager] restartZone: self.keychainZoneID.zoneName];
4797 [self endSOSTrustedViewOperation:self.keychainView];
4798 XCTAssertEqual(0, [self.keychainView.keyHierarchyConditions[SecCKKSZoneKeyStateWaitForTrust] wait:20*NSEC_PER_SEC], "CKKS entered waitfortrust");
4799
4800 // Okay! Upon sign in, this item should be uploaded
4801 [self expectCKModifyItemRecords:1 currentKeyPointerRecords:1 zoneID:self.keychainZoneID
4802 checkItem: [self checkClassCBlock:self.keychainZoneID message:@"Object was encrypted under class C key in hierarchy"]];
4803
4804 [self putSelfTLKSharesInCloudKit:self.keychainZoneID];
4805 self.mockSOSAdapter.circleStatus = kSOSCCInCircle;
4806 [self.accountStateTracker notifyCircleStatusChangeAndWaitForSignal];
4807 [self beginSOSTrustedViewOperation:self.keychainView];
4808
4809 XCTAssertEqual(0, [self.keychainView.keyHierarchyConditions[SecCKKSZoneKeyStateReady] wait:20*NSEC_PER_SEC], "CKKS entered ready");
4810 OCMVerifyAllWithDelay(self.mockDatabase, 20);
4811 }
4812
4813 - (void)testSyncableItemAddedOnDaemonRestartBeforePolicyLoaded {
4814 [self createAndSaveFakeKeyHierarchy:self.keychainZoneID];
4815 [self startCKKSSubsystem];
4816
4817 XCTAssertEqual(0, [self.keychainView.keyHierarchyConditions[SecCKKSZoneKeyStateReady] wait:20*NSEC_PER_SEC], "CKKS entered ready");
4818
4819 [self.keychainView waitForOperationsOfClass:[CKKSScanLocalItemsOperation class]];
4820 [self.keychainView waitForOperationsOfClass:[CKKSOutgoingQueueOperation class]];
4821
4822 // Daemon restarts
4823 self.automaticallyBeginCKKSViewCloudKitOperation = false;
4824 [self.injectedManager resetSyncingPolicy];
4825 [self.injectedManager haltZone:self.keychainZoneID.zoneName];
4826
4827 // This item addition shouldn't be uploaded yet, or in any queues
4828 [self addGenericPassword:@"data" account:@"account-delete-me-2"];
4829
4830 NSError* error = nil;
4831 NSDictionary* currentOQEs = [CKKSOutgoingQueueEntry countsByStateInZone:self.keychainZoneID error:&error];
4832 XCTAssertNil(error, "Should be no error counting OQEs");
4833 XCTAssertEqual(0, currentOQEs.count, "Should be no OQEs");
4834
4835 [self.injectedManager setCurrentSyncingPolicy:self.viewSortingPolicyForManagedViewList];
4836 self.keychainView = [self.injectedManager findView:self.keychainZoneID.zoneName];
4837 // end of daemon restart
4838
4839 [self expectCKModifyItemRecords:1 currentKeyPointerRecords:1 zoneID:self.keychainZoneID
4840 checkItem:[self checkClassCBlock:self.keychainZoneID message:@"Object was encrypted under class C key in hierarchy"]];
4841
4842 [self.injectedManager beginCloudKitOperationOfAllViews];
4843 [self beginSOSTrustedViewOperation:self.keychainView];
4844
4845 XCTAssertEqual(0, [self.keychainView.keyHierarchyConditions[SecCKKSZoneKeyStateReady] wait:20*NSEC_PER_SEC], "CKKS entered ready");
4846 OCMVerifyAllWithDelay(self.mockDatabase, 20);
4847 }
4848
4849 // Note that this test assumes that the keychainView object was created at daemon restart.
4850 // I don't really know how to write a test for that...
4851 - (void)testSyncableItemAddedOnDaemonRestartBeforeCloudKitAccountKnown {
4852 [self createAndSaveFakeKeyHierarchy:self.keychainZoneID];
4853 [self startCKKSSubsystem];
4854
4855 XCTAssertEqual(0, [self.keychainView.keyHierarchyConditions[SecCKKSZoneKeyStateReady] wait:20*NSEC_PER_SEC], "CKKS entered ready");
4856
4857 // Daemon restarts
4858 self.automaticallyBeginCKKSViewCloudKitOperation = false;
4859 self.keychainView = [[CKKSViewManager manager] restartZone: self.keychainZoneID.zoneName];
4860 [self beginSOSTrustedViewOperation:self.keychainView];
4861
4862 [self addGenericPassword: @"data" account: @"account-delete-me-2"];
4863 XCTAssertNotEqual(0, [self.keychainView.accountStateKnown wait:100*NSEC_PER_MSEC], "CKKS should still have no idea what the account state is");
4864 XCTAssertEqual(self.keychainView.accountStatus, CKKSAccountStatusUnknown, "Account status should be unknown");
4865 XCTAssertEqual(0, [self.keychainView.keyHierarchyConditions[SecCKKSZoneKeyStateWaitForCloudKitAccountStatus] wait:20*NSEC_PER_SEC], "CKKS entered 'waitforcloudkitaccount'");
4866
4867 [self.keychainView beginCloudKitOperation];
4868
4869 [self expectCKModifyItemRecords:1 currentKeyPointerRecords:1 zoneID:self.keychainZoneID
4870 checkItem: [self checkClassCBlock:self.keychainZoneID message:@"Object was encrypted under class C key in hierarchy"]];
4871 XCTAssertEqual(0, [self.keychainView.keyHierarchyConditions[SecCKKSZoneKeyStateReady] wait:20*NSEC_PER_SEC], "CKKS entered ready");
4872 OCMVerifyAllWithDelay(self.mockDatabase, 20);
4873 }
4874
4875 - (void)testSyncableItemModifiedOnDaemonRestartBeforeCloudKitAccountKnown {
4876 [self createAndSaveFakeKeyHierarchy:self.keychainZoneID];
4877 [self startCKKSSubsystem];
4878
4879 XCTAssertEqual(0, [self.keychainView.keyHierarchyConditions[SecCKKSZoneKeyStateReady] wait:20*NSEC_PER_SEC], "CKKS entered ready");
4880
4881 [self expectCKModifyItemRecords:1 currentKeyPointerRecords:1 zoneID:self.keychainZoneID
4882 checkItem: [self checkClassCBlock:self.keychainZoneID message:@"Object was encrypted under class C key in hierarchy"]];
4883 [self addGenericPassword: @"data" account: @"account-delete-me-2"];
4884 OCMVerifyAllWithDelay(self.mockDatabase, 20);
4885
4886 // Daemon restarts
4887 self.automaticallyBeginCKKSViewCloudKitOperation = false;
4888 self.keychainView = [[CKKSViewManager manager] restartZone: self.keychainZoneID.zoneName];
4889 [self beginSOSTrustedViewOperation:self.keychainView];
4890
4891 [self updateGenericPassword:@"newdata" account: @"account-delete-me-2"];
4892 XCTAssertNotEqual(0, [self.keychainView.accountStateKnown wait:100*NSEC_PER_MSEC], "CKKS should still have no idea what the account state is");
4893 XCTAssertEqual(self.keychainView.accountStatus, CKKSAccountStatusUnknown, "Account status should be unknown");
4894 XCTAssertEqual(0, [self.keychainView.keyHierarchyConditions[SecCKKSZoneKeyStateWaitForCloudKitAccountStatus] wait:20*NSEC_PER_SEC], "CKKS entered 'waitforcloudkitaccount'");
4895
4896 [self.keychainView beginCloudKitOperation];
4897
4898 [self expectCKModifyItemRecords:1 currentKeyPointerRecords:1 zoneID:self.keychainZoneID
4899 checkItem: [self checkClassCBlock:self.keychainZoneID message:@"Object was encrypted under class C key in hierarchy"]];
4900 XCTAssertEqual(0, [self.keychainView.keyHierarchyConditions[SecCKKSZoneKeyStateReady] wait:20*NSEC_PER_SEC], "CKKS entered ready");
4901 OCMVerifyAllWithDelay(self.mockDatabase, 20);
4902 }
4903
4904 - (void)testSyncableItemDeletedOnDaemonRestartBeforeCloudKitAccountKnown {
4905 [self createAndSaveFakeKeyHierarchy:self.keychainZoneID];
4906 [self startCKKSSubsystem];
4907
4908 XCTAssertEqual(0, [self.keychainView.keyHierarchyConditions[SecCKKSZoneKeyStateReady] wait:20*NSEC_PER_SEC], "CKKS entered ready");
4909
4910 [self expectCKModifyItemRecords:1 currentKeyPointerRecords:1 zoneID:self.keychainZoneID
4911 checkItem: [self checkClassCBlock:self.keychainZoneID message:@"Object was encrypted under class C key in hierarchy"]];
4912 [self addGenericPassword: @"data" account: @"account-delete-me-2"];
4913 OCMVerifyAllWithDelay(self.mockDatabase, 20);
4914
4915 // Daemon restarts
4916 self.automaticallyBeginCKKSViewCloudKitOperation = false;
4917 self.keychainView = [[CKKSViewManager manager] restartZone: self.keychainZoneID.zoneName];
4918 [self beginSOSTrustedViewOperation:self.keychainView];
4919
4920 [self deleteGenericPassword:@"account-delete-me-2"];
4921 XCTAssertNotEqual(0, [self.keychainView.accountStateKnown wait:100*NSEC_PER_MSEC], "CKKS should still have no idea what the account state is");
4922 XCTAssertEqual(self.keychainView.accountStatus, CKKSAccountStatusUnknown, "Account status should be unknown");
4923 XCTAssertEqual(0, [self.keychainView.keyHierarchyConditions[SecCKKSZoneKeyStateWaitForCloudKitAccountStatus] wait:20*NSEC_PER_SEC], "CKKS entered 'waitforcloudkitaccount'");
4924
4925 [self.keychainView beginCloudKitOperation];
4926
4927 [self expectCKDeleteItemRecords:1 zoneID:self.keychainZoneID];
4928 XCTAssertEqual(0, [self.keychainView.keyHierarchyConditions[SecCKKSZoneKeyStateReady] wait:20*NSEC_PER_SEC], "CKKS entered ready");
4929 OCMVerifyAllWithDelay(self.mockDatabase, 20);
4930 }
4931
4932 - (void)testNotStuckAfterReset {
4933 [self createAndSaveFakeKeyHierarchy: self.keychainZoneID]; // Make life easy for this test.
4934
4935 XCTestExpectation *operationRun = [self expectationWithDescription:@"operation run"];
4936 NSOperation* op = [NSBlockOperation named:@"test" withBlock:^{
4937 [operationRun fulfill];
4938 }];
4939
4940 [op addDependency:self.keychainView.keyStateReadyDependency];
4941 [self.operationQueue addOperation:op];
4942
4943 // And handle a spurious logout
4944 [self.keychainView handleCKLogout];
4945
4946 [self startCKKSSubsystem];
4947
4948 [self waitForExpectations: @[operationRun] timeout:20];
4949 XCTAssertEqual(0, [self.keychainView.keyHierarchyConditions[SecCKKSZoneKeyStateReady] wait:20*NSEC_PER_SEC], "CKKS entered 'ready'");
4950 }
4951
4952 - (void)testCKKSControlBringup {
4953 NSXPCInterface *interface = CKKSSetupControlProtocol([NSXPCInterface interfaceWithProtocol:@protocol(CKKSControlProtocol)]);
4954 XCTAssertNotNil(interface, "Received a configured CKKS interface");
4955 }
4956
4957 - (void)testMetricsUpload {
4958
4959 XCTestExpectation *upload = [self expectationWithDescription:@"CAMetrics"];
4960 XCTestExpectation *collection = [self expectationWithDescription:@"CAMetrics"];
4961
4962 id saMock = OCMClassMock([SecCoreAnalytics class]);
4963 OCMStub([saMock sendEvent:[OCMArg any] event:[OCMArg any]]).andDo(^(NSInvocation* invocation) {
4964 [upload fulfill];
4965 });
4966
4967 NSString *sampleSampler = @"stuff";
4968
4969 [[CKKSAnalytics logger] AddMultiSamplerForName:sampleSampler withTimeInterval:SFAnalyticsSamplerIntervalOncePerReport block:^NSDictionary<NSString *,NSNumber *> *{
4970 [collection fulfill];
4971 return @{ @"hej" : @1 };
4972 }];
4973
4974
4975 [self createAndSaveFakeKeyHierarchy: self.keychainZoneID]; // Make life easy for this test.
4976 [self startCKKSSubsystem];
4977
4978 XCTAssertEqual(0, [self.keychainView.keyHierarchyConditions[SecCKKSZoneKeyStateReady] wait:20*NSEC_PER_SEC], @"Key state should have arrived at ready");
4979
4980 [self expectCKModifyRecords:@{SecCKRecordDeviceStateType: [NSNumber numberWithInt:1]}
4981 deletedRecordTypeCounts:nil
4982 zoneID:self.keychainZoneID
4983 checkModifiedRecord:nil
4984 runAfterModification:nil];
4985
4986 [self.injectedManager xpc24HrNotification];
4987
4988 [self waitForExpectations: @[upload, collection] timeout:10];
4989 [[CKKSAnalytics logger] removeMultiSamplerForName:sampleSampler];
4990 }
4991
4992 - (void)testSaveManyTLKShares {
4993 // Spin up CKKS subsystem.
4994 [self startCKKSSubsystem];
4995
4996 [self performOctagonTLKUpload:self.ckksViews];
4997 OCMVerifyAllWithDelay(self.mockDatabase, 20);
4998
4999 XCTAssertEqual(0, [self.keychainView.keyHierarchyConditions[SecCKKSZoneKeyStateReady] wait:20*NSEC_PER_SEC], @"key state should enter 'ready'");
5000
5001 NSMutableArray<CKKSSOSSelfPeer*>* peers = [NSMutableArray array];
5002
5003 for(int i = 0; i < 20; i++) {
5004 CKKSSOSSelfPeer* untrustedPeer = [[CKKSSOSSelfPeer alloc] initWithSOSPeerID:[NSString stringWithFormat:@"untrusted-peer-%d", i]
5005 encryptionKey:[[SFECKeyPair alloc] initRandomKeyPairWithSpecifier:[[SFECKeySpecifier alloc] initWithCurve:SFEllipticCurveNistp384]]
5006 signingKey:[[SFECKeyPair alloc] initRandomKeyPairWithSpecifier:[[SFECKeySpecifier alloc] initWithCurve:SFEllipticCurveNistp384]]
5007 viewList:self.managedViewList];
5008
5009 [peers addObject:untrustedPeer];
5010 }
5011
5012 NSMutableArray<CKRecord*>* tlkShareRecords = [NSMutableArray array];
5013
5014 for(CKKSSOSSelfPeer* peer1 in peers) {
5015 for(CKKSSOSSelfPeer* peer2 in peers) {
5016 NSError* error = nil;
5017 CKKSTLKShareRecord* share = [CKKSTLKShareRecord share:self.keychainZoneKeys.tlk
5018 as:peer1
5019 to:peer2
5020 epoch:-1
5021 poisoned:0
5022 error:&error];
5023 XCTAssertNil(error, "Should have been no error sharing a CKKSKey");
5024 XCTAssertNotNil(share, "Should be able to create a share");
5025
5026 CKRecord* shareRecord = [share CKRecordWithZoneID:self.keychainZoneID];
5027 [tlkShareRecords addObject:shareRecord];
5028 }
5029 }
5030
5031 [self measureBlock:^{
5032 [self.keychainView dispatchSyncWithSQLTransaction:^CKKSDatabaseTransactionResult{
5033 for(CKRecord* record in tlkShareRecords) {
5034 [self.keychainView _onqueueCKRecordChanged:record resync:false];
5035 }
5036 return CKKSDatabaseTransactionCommit;
5037 }];
5038 }];
5039 }
5040
5041 - (void)testReceiveNotificationDuringLaunch {
5042 [self putFakeKeyHierarchyInCloudKit:self.keychainZoneID];
5043
5044 [self holdCloudKitModifyRecordZones];
5045
5046 // Spin up CKKS subsystem.
5047 [self startCKKSSubsystem];
5048
5049 CKKSCondition* fetcherCondition = self.keychainView.zoneChangeFetcher.fetchScheduler.liveRequestReceived;
5050
5051 [self saveTLKMaterialToKeychainSimulatingSOS:self.keychainZoneID];
5052
5053 [self.injectedManager.zoneChangeFetcher notifyZoneChange:nil];
5054
5055 XCTAssertNotEqual(0, [fetcherCondition wait:(3 * NSEC_PER_SEC)], "not supposed to get a fetch data");
5056
5057 [self expectCKKSTLKSelfShareUpload:self.keychainZoneID];
5058 self.silentFetchesAllowed = false;
5059 [self expectCKFetch];
5060 [self releaseCloudKitModifyRecordZonesHold];
5061
5062 XCTAssertEqual(0, [self.keychainView.keyHierarchyConditions[SecCKKSZoneKeyStateReady] wait:20*NSEC_PER_SEC], "CKKS entered ready");
5063 OCMVerifyAllWithDelay(self.mockDatabase, 20);
5064 }
5065
5066 @end
5067
5068 #endif // OCTAGON