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