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