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