]> git.saurik.com Git - apple/security.git/blob - keychain/ckks/tests/CKKSTests.m
Security-58286.41.2.tar.gz
[apple/security.git] / keychain / ckks / tests / CKKSTests.m
1 /*
2 * Copyright (c) 2016 Apple Inc. All Rights Reserved.
3 *
4 * @APPLE_LICENSE_HEADER_START@
5 *
6 * This file contains Original Code and/or Modifications of Original Code
7 * as defined in and that are subject to the Apple Public Source License
8 * Version 2.0 (the 'License'). You may not use this file except in
9 * compliance with the License. Please obtain a copy of the License at
10 * http://www.opensource.apple.com/apsl/ and read it before using this
11 * file.
12 *
13 * The Original Code and all software distributed under the License are
14 * distributed on an 'AS IS' basis, WITHOUT WARRANTY OF ANY KIND, EITHER
15 * EXPRESS OR IMPLIED, AND APPLE HEREBY DISCLAIMS ALL SUCH WARRANTIES,
16 * INCLUDING WITHOUT LIMITATION, ANY WARRANTIES OF MERCHANTABILITY,
17 * FITNESS FOR A PARTICULAR PURPOSE, QUIET ENJOYMENT OR NON-INFRINGEMENT.
18 * Please see the License for the specific language governing rights and
19 * limitations under the License.
20 *
21 * @APPLE_LICENSE_HEADER_END@
22 */
23
24 #if OCTAGON
25
26 #import "CKKSTests.h"
27 #import <CloudKit/CloudKit.h>
28 #import <Foundation/NSXPCConnection_Private.h>
29 #import <XCTest/XCTest.h>
30 #import <OCMock/OCMock.h>
31
32 #include <Security/SecItemPriv.h>
33 #include <securityd/SecItemDb.h>
34 #include <securityd/SecItemServer.h>
35
36 #import "keychain/ckks/tests/CloudKitMockXCTest.h"
37 #import "keychain/ckks/tests/CloudKitKeychainSyncingMockXCTest.h"
38 #import "keychain/ckks/CKKS.h"
39 #import "keychain/ckks/CKKSControlProtocol.h"
40 #import "keychain/ckks/CKKSCurrentKeyPointer.h"
41 #import "keychain/ckks/CKKSItemEncrypter.h"
42 #import "keychain/ckks/CKKSKey.h"
43 #import "keychain/ckks/CKKSOutgoingQueueEntry.h"
44 #import "keychain/ckks/CKKSIncomingQueueEntry.h"
45 #import "keychain/ckks/CKKSSynchronizeOperation.h"
46 #import "keychain/ckks/CKKSViewManager.h"
47 #import "keychain/ckks/CKKSZoneStateEntry.h"
48 #import "keychain/ckks/CKKSManifest.h"
49 #import "keychain/ckks/CKKSAnalyticsLogger.h"
50 #import "keychain/ckks/CKKSHealKeyHierarchyOperation.h"
51 #import "keychain/ckks/CKKSZoneChangeFetcher.h"
52
53 #import "keychain/ckks/tests/MockCloudKit.h"
54
55 #import "keychain/ckks/tests/CKKSTests.h"
56
57 @implementation CloudKitKeychainSyncingTestsBase
58
59 - (ZoneKeys*)keychainZoneKeys {
60 return self.keys[self.keychainZoneID];
61 }
62
63 // Override our base class
64 -(NSSet*)managedViewList {
65 return [NSSet setWithObject:@"keychain"];
66 }
67
68 + (void)setUp {
69 SecCKKSEnable();
70 SecCKKSResetSyncing();
71 [super setUp];
72 }
73
74 - (void)setUp {
75 [super setUp];
76
77 self.keychainZoneID = [[CKRecordZoneID alloc] initWithZoneName:@"keychain" ownerName:CKCurrentUserDefaultName];
78 self.keychainZone = [[FakeCKZone alloc] initZone: self.keychainZoneID];
79
80 [self.ckksZones addObject:self.keychainZoneID];
81
82 // Wait for the ViewManager to be brought up
83 XCTAssertEqual(0, [self.injectedManager.completedSecCKKSInitialize wait:4*NSEC_PER_SEC], "No timeout waiting for SecCKKSInitialize");
84
85 self.keychainView = [[CKKSViewManager manager] findView:@"keychain"];
86 XCTAssertNotNil(self.keychainView, "CKKSViewManager created the keychain view");
87
88 // Check that your environment is set up correctly
89 XCTAssertFalse([CKKSManifest shouldSyncManifests], "Manifests syncing is disabled");
90 XCTAssertFalse([CKKSManifest shouldEnforceManifests], "Manifests enforcement is disabled");
91 }
92
93 + (void)tearDown {
94 [super tearDown];
95 SecCKKSResetSyncing();
96 }
97
98 - (void)tearDown {
99 // Fetch status, to make sure we can
100 NSDictionary* status = [self.keychainView status];
101 (void)status;
102
103 [self.keychainView halt];
104 [self.keychainView waitUntilAllOperationsAreFinished];
105
106 self.keychainView = nil;
107 self.keychainZoneID = nil;
108
109 [super tearDown];
110 }
111
112 - (FakeCKZone*)keychainZone {
113 return self.zones[self.keychainZoneID];
114 }
115
116 - (void)setKeychainZone: (FakeCKZone*) zone {
117 self.zones[self.keychainZoneID] = zone;
118 }
119
120 @end
121
122 @implementation CloudKitKeychainSyncingTests
123
124 #pragma mark - Tests
125
126 - (void)testBringupToKeyStateReady {
127 [self createAndSaveFakeKeyHierarchy: self.keychainZoneID]; // Make life easy for this test.
128 [self startCKKSSubsystem];
129
130 XCTAssertEqual(0, [self.keychainView.keyHierarchyConditions[SecCKKSZoneKeyStateReady] wait:4*NSEC_PER_SEC], @"Key state should have arrived at ready");
131 }
132
133 - (void)testAddItem {
134 [self createAndSaveFakeKeyHierarchy: self.keychainZoneID]; // Make life easy for this test.
135
136 // We expect a single record to be uploaded.
137 [self expectCKModifyItemRecords: 1 currentKeyPointerRecords: 1 zoneID:self.keychainZoneID];
138
139 [self startCKKSSubsystem];
140 XCTAssertEqual(0, [self.keychainView.keyHierarchyConditions[SecCKKSZoneKeyStateReady] wait:4*NSEC_PER_SEC], @"Key state should have arrived at ready");
141
142 [self addGenericPassword: @"data" account: @"account-delete-me"];
143
144 OCMVerifyAllWithDelay(self.mockDatabase, 8);
145 }
146
147 - (void)testActiveTLKS {
148 [self createAndSaveFakeKeyHierarchy: self.keychainZoneID]; // Make life easy for this test.
149
150 // We expect a single record to be uploaded.
151 [self expectCKModifyItemRecords: 1 currentKeyPointerRecords: 1 zoneID:self.keychainZoneID];
152
153 [self startCKKSSubsystem];
154
155 [self addGenericPassword: @"data" account: @"account-delete-me"];
156
157 OCMVerifyAllWithDelay(self.mockDatabase, 8);
158
159 NSDictionary<NSString *,NSString *>* tlks = [[CKKSViewManager manager] activeTLKs];
160
161 XCTAssertEqual([tlks count], (NSUInteger)1, "One TLK");
162 XCTAssertNotNil(tlks[@"keychain"], "keychain have a UUID");
163 }
164
165
166 - (void)testAddMultipleItems {
167 [self createAndSaveFakeKeyHierarchy: self.keychainZoneID]; // Make life easy for this test.
168 [self startCKKSSubsystem];
169
170 [self expectCKModifyItemRecords: 1 currentKeyPointerRecords: 1 zoneID:self.keychainZoneID];
171 [self addGenericPassword: @"data" account: @"account-delete-me"];
172 OCMVerifyAllWithDelay(self.mockDatabase, 8);
173
174 [self expectCKModifyItemRecords: 1 currentKeyPointerRecords: 1 zoneID:self.keychainZoneID];
175 [self addGenericPassword: @"data" account: @"account-delete-me-2"];
176 OCMVerifyAllWithDelay(self.mockDatabase, 8);
177
178 [self expectCKModifyItemRecords: 1 currentKeyPointerRecords: 1 zoneID:self.keychainZoneID];
179 [self addGenericPassword: @"data" account: @"account-delete-me-3"];
180 OCMVerifyAllWithDelay(self.mockDatabase, 8);
181 }
182
183 - (void)testAddItemWithoutUUID {
184 // Test starts with no keys in database, a key hierarchy in our fake CloudKit, and the TLK safely in the local keychain.
185 [self putFakeKeyHierarchyInCloudKit:self.keychainZoneID];
186 [self expectCKKSTLKSelfShareUpload:self.keychainZoneID];
187 [self saveTLKMaterialToKeychain:self.keychainZoneID];
188
189 [self startCKKSSubsystem];
190
191 [self.keychainView waitUntilAllOperationsAreFinished];
192
193 SecCKKSTestSetDisableAutomaticUUID(true);
194 [self addGenericPassword: @"data" account: @"account-delete-me-no-UUID" expecting:errSecSuccess message: @"Add item (no UUID) to keychain"];
195
196 SecCKKSTestSetDisableAutomaticUUID(false);
197
198 // We then expect an upload of the added item
199 [self expectCKModifyItemRecords: 1 currentKeyPointerRecords: 1 zoneID:self.keychainZoneID];
200
201 OCMVerifyAllWithDelay(self.mockDatabase, 8);
202 }
203
204 - (void)testModifyItem {
205 [self createAndSaveFakeKeyHierarchy: self.keychainZoneID]; // Make life easy for this test.
206
207 NSString* account = @"account-delete-me";
208
209 [self startCKKSSubsystem];
210
211 // We expect a single record to be uploaded.
212 [self expectCKModifyItemRecords: 1 currentKeyPointerRecords: 1 zoneID:self.keychainZoneID];
213 [self addGenericPassword: @"data" account: account];
214 OCMVerifyAllWithDelay(self.mockDatabase, 8);
215
216 // And then modified.
217 [self expectCKModifyItemRecords: 1 currentKeyPointerRecords: 1 zoneID:self.keychainZoneID];
218 [self updateGenericPassword: @"otherdata" account:account];
219 OCMVerifyAllWithDelay(self.mockDatabase, 8);
220 }
221
222 - (void)testModifyItemImmediately {
223 [self createAndSaveFakeKeyHierarchy: self.keychainZoneID]; // Make life easy for this test.
224 NSString* account = @"account-delete-me";
225
226 [self startCKKSSubsystem];
227 [self holdCloudKitModifications];
228
229 // We expect a single record to be uploaded, but need to hold the operation from finishing until we can modify the item locally
230 [self expectCKModifyItemRecords: 1 currentKeyPointerRecords: 1 zoneID:self.keychainZoneID
231 checkItem:[self checkPasswordBlock:self.keychainZoneID account:account password:@"data"]];
232 [self addGenericPassword: @"data" account: account];
233 OCMVerifyAllWithDelay(self.mockDatabase, 8);
234
235 // Right now, the write in CloudKit is pending. Make the local modification...
236 [self updateGenericPassword: @"otherdata" account:account];
237
238 // And then schedule the update
239 [self expectCKModifyItemRecords: 1 currentKeyPointerRecords: 1 zoneID:self.keychainZoneID
240 checkItem:[self checkPasswordBlock:self.keychainZoneID account:account password:@"otherdata"]];
241 [self releaseCloudKitModificationHold];
242
243 OCMVerifyAllWithDelay(self.mockDatabase, 8);
244 }
245
246 - (void)testModifyItemPrimaryKey {
247 [self createAndSaveFakeKeyHierarchy: self.keychainZoneID]; // Make life easy for this test.
248
249 NSString* account = @"account-delete-me";
250
251 [self startCKKSSubsystem];
252
253 // We expect a single record to be uploaded.
254 [self expectCKModifyItemRecords: 1 currentKeyPointerRecords: 1 zoneID:self.keychainZoneID];
255 [self addGenericPassword: @"data" account: account];
256 OCMVerifyAllWithDelay(self.mockDatabase, 8);
257
258 // And then modified. Since we're changing the "primary key", we expect to delete the old record and upload a new one.
259 [self expectCKModifyItemRecords:1 deletedRecords:1 currentKeyPointerRecords:1 zoneID:self.keychainZoneID checkItem:nil];
260 [self updateAccountOfGenericPassword: @"new-account-delete-me" account:account];
261 OCMVerifyAllWithDelay(self.mockDatabase, 8);
262 }
263
264 - (void)testModifyItemDuringReencrypt {
265 [self createAndSaveFakeKeyHierarchy: self.keychainZoneID]; // Make life easy for this test.
266 NSString* account = @"account-delete-me";
267
268 [self startCKKSSubsystem];
269 [self holdCloudKitModifications];
270
271 // We expect a single record to be uploaded, but need to hold the operation from finishing until we can modify the item locally
272 [self expectCKModifyItemRecords: 1 currentKeyPointerRecords: 1 zoneID:self.keychainZoneID
273 checkItem:[self checkPasswordBlock:self.keychainZoneID account:account password:@"data"]];
274 [self addGenericPassword: @"data" account: account];
275 OCMVerifyAllWithDelay(self.mockDatabase, 8);
276
277 // Right now, the write in CloudKit is pending. Make the local modification...
278 [self updateGenericPassword: @"otherdata" account:account];
279
280 // And then schedule the update
281 [self expectCKModifyItemRecords: 1 currentKeyPointerRecords: 1 zoneID:self.keychainZoneID
282 checkItem:[self checkPasswordBlock:self.keychainZoneID account:account password:@"otherdata"]];
283
284 // Stop the reencrypt operation from happening
285 self.keychainView.holdReencryptOutgoingItemsOperation = [CKKSGroupOperation named:@"reencrypt-hold" withBlock: ^{
286 secnotice("ckks", "releasing reencryption hold");
287 }];
288
289 // The cloudkit operation finishes, letting the next OQO proceed (and set up the reencryption operation)
290 [self releaseCloudKitModificationHold];
291
292 // And wait for this to finish...
293 [self.keychainView waitForOperationsOfClass:[CKKSOutgoingQueueOperation class]];
294 // And once more to quiesce.
295 [self.keychainView waitForOperationsOfClass:[CKKSOutgoingQueueOperation class]];
296
297 // Pause outgoing queue operations to ensure the reencryption operation runs first
298 self.keychainView.holdOutgoingQueueOperation = [CKKSGroupOperation named:@"outgoing-hold" withBlock: ^{
299 secnotice("ckks", "releasing outgoing-queue hold");
300 }];
301
302 // Run the reencrypt items operation to completion.
303 [self.operationQueue addOperation: self.keychainView.holdReencryptOutgoingItemsOperation];
304 [self.keychainView waitForOperationsOfClass:[CKKSReencryptOutgoingItemsOperation class]];
305
306 [self.operationQueue addOperation: self.keychainView.holdOutgoingQueueOperation];
307
308 OCMVerifyAllWithDelay(self.mockDatabase, 8);
309 [self.keychainView waitUntilAllOperationsAreFinished];
310 [self waitForCKModifications];
311 }
312
313 - (void)testModifyItemBeforeReencrypt {
314 [self createAndSaveFakeKeyHierarchy: self.keychainZoneID]; // Make life easy for this test.
315 NSString* account = @"account-delete-me";
316
317 [self startCKKSSubsystem];
318 [self holdCloudKitModifications];
319
320 // We expect a single record to be uploaded, but need to hold the operation from finishing until we can modify the item locally
321 [self expectCKModifyItemRecords: 1 currentKeyPointerRecords: 1 zoneID:self.keychainZoneID
322 checkItem:[self checkPasswordBlock:self.keychainZoneID account:account password:@"data"]];
323 [self addGenericPassword: @"data" account: account];
324 OCMVerifyAllWithDelay(self.mockDatabase, 8);
325
326 // Right now, the write in CloudKit is pending. Make the local modification...
327 [self updateGenericPassword: @"otherdata" account:account];
328
329 // And then schedule the update, but for the final version of the password
330 [self expectCKModifyItemRecords: 1 currentKeyPointerRecords: 1 zoneID:self.keychainZoneID
331 checkItem:[self checkPasswordBlock:self.keychainZoneID account:account password:@"third"]];
332
333 // Stop the reencrypt operation from happening
334 self.keychainView.holdReencryptOutgoingItemsOperation = [CKKSGroupOperation named:@"reencrypt-hold" withBlock: ^{
335 secnotice("ckks", "releasing reencryption hold");
336 }];
337
338 // The cloudkit operation finishes, letting the next OQO proceed (and set up the reencryption operation)
339 [self releaseCloudKitModificationHold];
340
341 // And wait for this to finish...
342 [self.keychainView waitForOperationsOfClass:[CKKSOutgoingQueueOperation class]];
343 // And once more to quiesce.
344 [self.keychainView waitForOperationsOfClass:[CKKSOutgoingQueueOperation class]];
345
346 [self updateGenericPassword: @"third" account:account];
347
348 // Item should upload.
349 OCMVerifyAllWithDelay(self.mockDatabase, 8);
350
351 // Run the reencrypt items operation to completion.
352 [self.operationQueue addOperation: self.keychainView.holdReencryptOutgoingItemsOperation];
353 [self.keychainView waitForOperationsOfClass:[CKKSReencryptOutgoingItemsOperation class]];
354
355 [self.keychainView waitUntilAllOperationsAreFinished];
356 [self waitForCKModifications];
357 }
358
359 - (void)testModifyItemDuringNetworkFailure {
360 [self createAndSaveFakeKeyHierarchy: self.keychainZoneID]; // Make life easy for this test.
361 NSString* account = @"account-delete-me";
362
363 [self startCKKSSubsystem];
364 [self holdCloudKitModifications];
365
366 // We expect a single record to be uploaded, but need to hold the operation from finishing until we can modify the item locally
367 [self failNextCKAtomicModifyItemRecordsUpdateFailure:self.keychainZoneID];
368
369 [self addGenericPassword: @"data" account: account];
370 OCMVerifyAllWithDelay(self.mockDatabase, 8);
371
372 // Right now, the write in CloudKit is pending. Make the local modification...
373 [self updateGenericPassword: @"otherdata" account:account];
374
375 // And then schedule the update, but for the final version of the password
376 [self expectCKModifyItemRecords: 1 currentKeyPointerRecords: 1 zoneID:self.keychainZoneID
377 checkItem:[self checkPasswordBlock:self.keychainZoneID account:account password:@"otherdata"]];
378
379 // The cloudkit operation finishes, letting the next OQO proceed (and set up uploading the new item)
380 [self releaseCloudKitModificationHold];
381
382 // Item should upload.
383 OCMVerifyAllWithDelay(self.mockDatabase, 8);
384
385 [self.keychainView waitUntilAllOperationsAreFinished];
386 [self waitForCKModifications];
387 }
388
389 - (void)testOutgoingQueueRecoverFromStaleInflightEntry {
390 // CKKS is restarting with an existing in-flight OQE
391 // Note that this test is incomplete, and doesn't re-add the item to the local keychain
392 [self createAndSaveFakeKeyHierarchy: self.keychainZoneID]; // Make life easy for this test.
393 NSString* account = @"fake-account";
394
395 [self.keychainView dispatchSync:^bool {
396 NSError* error = nil;
397
398 CKRecordID* ckrid = [[CKRecordID alloc] initWithRecordName:@"DD7C2F9B-B22D-3B90-C299-E3B48174BFA3" zoneID:self.keychainZoneID];
399
400 CKKSItem* item = [self newItem:ckrid withNewItemData:[self fakeRecordDictionary:account zoneID:self.keychainZoneID] key:self.keychainZoneKeys.classC];
401 XCTAssertNotNil(item, "Should be able to create a new fake item");
402
403 CKKSOutgoingQueueEntry* oqe = [[CKKSOutgoingQueueEntry alloc] initWithCKKSItem:item action:SecCKKSActionAdd state:SecCKKSStateInFlight waitUntil:nil accessGroup:@"ckks"];
404 XCTAssertNotNil(oqe, "Should be able to create a new fake OQE");
405 [oqe saveToDatabase:&error];
406
407 XCTAssertNil(error, "Shouldn't error saving new OQE to database");
408 return true;
409 }];
410
411 // When CKKS restarts, it should find and re-upload this item
412 [self expectCKModifyItemRecords: 1 currentKeyPointerRecords: 1 zoneID:self.keychainZoneID
413 checkItem:[self checkPasswordBlock:self.keychainZoneID account:account password:@"data"]];
414
415 [self startCKKSSubsystem];
416 [self.keychainView waitForFetchAndIncomingQueueProcessing];
417
418 self.keychainView = [[CKKSViewManager manager] restartZone: self.keychainZoneID.zoneName];
419 [self.keychainView waitForKeyHierarchyReadiness];
420 OCMVerifyAllWithDelay(self.mockDatabase, 8);
421 }
422
423 - (void)testOutgoingQueueRecoverFromNetworkFailure {
424 [self createAndSaveFakeKeyHierarchy: self.keychainZoneID]; // Make life easy for this test.
425 NSString* account = @"account-delete-me";
426
427 [self startCKKSSubsystem];
428 [self holdCloudKitModifications];
429
430 // We expect a single record to be uploaded, but need to hold the operation from finishing until we can modify the item locally
431
432 NSError* greyMode = [[CKPrettyError alloc] initWithDomain:CKErrorDomain code:CKErrorNotAuthenticated userInfo:@{}];
433 [self failNextCKAtomicModifyItemRecordsUpdateFailure:self.keychainZoneID blockAfterReject:nil withError:greyMode];
434
435 [self addGenericPassword: @"data" account: account];
436 OCMVerifyAllWithDelay(self.mockDatabase, 8);
437
438 // And then schedule the retried update
439 [self expectCKModifyItemRecords: 1 currentKeyPointerRecords: 1 zoneID:self.keychainZoneID
440 checkItem:[self checkPasswordBlock:self.keychainZoneID account:account password:@"data"]];
441
442 // The cloudkit operation finishes, letting the next OQO proceed (and set up uploading the new item)
443 [self releaseCloudKitModificationHold];
444
445 OCMVerifyAllWithDelay(self.mockDatabase, 8);
446
447 [self.keychainView waitUntilAllOperationsAreFinished];
448 [self waitForCKModifications];
449 }
450
451 - (void)testDeleteItem {
452 [self createAndSaveFakeKeyHierarchy: self.keychainZoneID]; // Make life easy for this test.
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-delete-me"];
459 OCMVerifyAllWithDelay(self.mockDatabase, 8);
460
461 // We expect a single record to be deleted.
462 [self expectCKDeleteItemRecords: 1 zoneID:self.keychainZoneID];
463 [self deleteGenericPassword:@"account-delete-me"];
464 OCMVerifyAllWithDelay(self.mockDatabase, 8);
465 }
466
467 - (void)testDeleteItemImmediatelyAfterModify {
468 [self createAndSaveFakeKeyHierarchy: self.keychainZoneID]; // Make life easy for this test.
469 NSString* account = @"account-delete-me";
470
471 [self startCKKSSubsystem];
472
473 // We expect a single record to be uploaded.
474 [self expectCKModifyItemRecords: 1 currentKeyPointerRecords: 1 zoneID:self.keychainZoneID];
475 [self addGenericPassword: @"data" account: account];
476 OCMVerifyAllWithDelay(self.mockDatabase, 8);
477
478 // Now, hold the modify
479 [self holdCloudKitModifications];
480
481 // We expect a single record to be uploaded, but need to hold the operation from finishing until we can modify the item locally
482 [self expectCKModifyItemRecords: 1 currentKeyPointerRecords: 1 zoneID:self.keychainZoneID
483 checkItem:[self checkPasswordBlock:self.keychainZoneID account:account password:@"otherdata"]];
484
485 [self updateGenericPassword: @"otherdata" account:account];
486 OCMVerifyAllWithDelay(self.mockDatabase, 8);
487
488 // Right now, the write in CloudKit is pending. Make the local deletion...
489 [self deleteGenericPassword:account];
490
491 // And then schedule the update
492 [self expectCKDeleteItemRecords:1 zoneID:self.keychainZoneID];
493 [self releaseCloudKitModificationHold];
494
495 OCMVerifyAllWithDelay(self.mockDatabase, 8);
496 }
497
498 - (void)testDeleteItemAfterFetchAfterModify {
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, 8);
508
509 // Now, hold the modify
510 //[self holdCloudKitModifications];
511
512 // We expect a single record to be uploaded, but need to hold the operation from finishing until we can modify the item locally
513 [self expectCKModifyItemRecords: 1 currentKeyPointerRecords: 1 zoneID:self.keychainZoneID
514 checkItem:[self checkPasswordBlock:self.keychainZoneID account:account password:@"otherdata"]];
515
516 [self updateGenericPassword: @"otherdata" account:account];
517 OCMVerifyAllWithDelay(self.mockDatabase, 8);
518
519 // Right now, the write in CloudKit is pending. Place a hold on outgoing queue processing
520 // Place a hold on processing the outgoing queue.
521 CKKSResultOperation* blockOutgoing = [CKKSResultOperation operationWithBlock:^{
522 secnotice("ckks", "Outgoing queue hold released.");
523 }];
524 blockOutgoing.name = @"outgoing-queue-hold";
525 CKKSResultOperation* outgoingQueueOperation = [self.keychainView processOutgoingQueueAfter:blockOutgoing ckoperationGroup:nil];
526
527 [self deleteGenericPassword:account];
528
529 [self expectCKDeleteItemRecords:1 zoneID:self.keychainZoneID];
530
531 // Release the CK modification hold
532 //[self releaseCloudKitModificationHold];
533
534 // And cause a fetch
535 [self.keychainView waitForFetchAndIncomingQueueProcessing];
536 [self.operationQueue addOperation:blockOutgoing];
537 [outgoingQueueOperation waitUntilFinished];
538
539 OCMVerifyAllWithDelay(self.mockDatabase, 8);
540 }
541
542
543 - (void)testReceiveItem {
544 [self createAndSaveFakeKeyHierarchy: self.keychainZoneID]; // Make life easy for this test.
545 [self startCKKSSubsystem];
546
547 NSDictionary *query = @{(id)kSecClass : (id)kSecClassGenericPassword,
548 (id)kSecAttrAccessGroup : @"com.apple.security.ckks",
549 (id)kSecAttrAccount : @"account-delete-me",
550 (id)kSecAttrSynchronizable : (id)kCFBooleanTrue,
551 (id)kSecMatchLimit : (id)kSecMatchLimitOne,
552 };
553
554 CFTypeRef item = NULL;
555 XCTAssertEqual(errSecItemNotFound, SecItemCopyMatching((__bridge CFDictionaryRef) query, &item), "item should not yet exist");
556
557 CKRecord* ckr = [self createFakeRecord: self.keychainZoneID recordName:@"7B598D31-F9C5-481E-98AC-5A507ACB2D85"];
558 [self.keychainZone addToZone: ckr];
559
560 // Trigger a notification (with hilariously fake data)
561 [self.keychainView notifyZoneChange:nil];
562
563 [self.keychainView waitForFetchAndIncomingQueueProcessing];
564 XCTAssertEqual(errSecSuccess, SecItemCopyMatching((__bridge CFDictionaryRef) query, &item), "item should exist now");
565 }
566
567 - (void)testReceiveManyItems {
568 [self createAndSaveFakeKeyHierarchy: self.keychainZoneID]; // Make life easy for this test.
569 [self startCKKSSubsystem];
570
571 [self.keychainZone addToZone: [self createFakeRecord: self.keychainZoneID recordName:@"7B598D31-F9C5-481E-98AC-5A507ACB2D00" withAccount:@"account0"]];
572 [self.keychainZone addToZone: [self createFakeRecord: self.keychainZoneID recordName:@"7B598D31-F9C5-481E-98AC-5A507ACB2D01" withAccount:@"account1"]];
573 [self.keychainZone addToZone: [self createFakeRecord: self.keychainZoneID recordName:@"7B598D31-F9C5-481E-98AC-5A507ACB2D02" withAccount:@"account2"]];
574 [self.keychainZone addToZone: [self createFakeRecord: self.keychainZoneID recordName:@"7B598D31-F9C5-481E-98AC-5A507ACB2D03" withAccount:@"account3"]];
575 [self.keychainZone addToZone: [self createFakeRecord: self.keychainZoneID recordName:@"7B598D31-F9C5-481E-98AC-5A507ACB2D04" withAccount:@"account4"]];
576 [self.keychainZone addToZone: [self createFakeRecord: self.keychainZoneID recordName:@"7B598D31-F9C5-481E-98AC-5A507ACB2D05" withAccount:@"account5"]];
577 [self.keychainZone addToZone: [self createFakeRecord: self.keychainZoneID recordName:@"7B598D31-F9C5-481E-98AC-5A507ACB2D06" withAccount:@"account6"]];
578 [self.keychainZone addToZone: [self createFakeRecord: self.keychainZoneID recordName:@"7B598D31-F9C5-481E-98AC-5A507ACB2D07" withAccount:@"account7"]];
579 [self.keychainZone addToZone: [self createFakeRecord: self.keychainZoneID recordName:@"7B598D31-F9C5-481E-98AC-5A507ACB2D08" withAccount:@"account8"]];
580 [self.keychainZone addToZone: [self createFakeRecord: self.keychainZoneID recordName:@"7B598D31-F9C5-481E-98AC-5A507ACB2D09" withAccount:@"account9"]];
581 [self.keychainZone addToZone: [self createFakeRecord: self.keychainZoneID recordName:@"7B598D31-F9C5-481E-98AC-5A507ACB2D10" withAccount:@"account10"]];
582 [self.keychainZone addToZone: [self createFakeRecord: self.keychainZoneID recordName:@"7B598D31-F9C5-481E-98AC-5A507ACB2D11" withAccount:@"account11"]];
583
584 // Trigger a notification (with hilariously fake data)
585 [self.keychainView notifyZoneChange:nil];
586
587 [self.keychainView waitForFetchAndIncomingQueueProcessing];
588
589 [self findGenericPassword: @"account0" expecting:errSecSuccess];
590 [self findGenericPassword: @"account1" expecting:errSecSuccess];
591 [self findGenericPassword: @"account2" expecting:errSecSuccess];
592 [self findGenericPassword: @"account3" expecting:errSecSuccess];
593 [self findGenericPassword: @"account4" expecting:errSecSuccess];
594 [self findGenericPassword: @"account5" expecting:errSecSuccess];
595 [self findGenericPassword: @"account6" expecting:errSecSuccess];
596 [self findGenericPassword: @"account7" expecting:errSecSuccess];
597 [self findGenericPassword: @"account8" expecting:errSecSuccess];
598 [self findGenericPassword: @"account9" expecting:errSecSuccess];
599 [self findGenericPassword: @"account10" expecting:errSecSuccess];
600 [self findGenericPassword: @"account11" expecting:errSecSuccess];
601 }
602
603 - (void)testReceiveCollidingItem {
604 [self createAndSaveFakeKeyHierarchy: self.keychainZoneID]; // Make life easy for this test.
605 [self startCKKSSubsystem];
606
607 NSDictionary *query = @{(id)kSecClass : (id)kSecClassGenericPassword,
608 (id)kSecAttrAccessGroup : @"com.apple.security.ckks",
609 (id)kSecAttrAccount : @"account-delete-me",
610 (id)kSecAttrSynchronizable : (id)kCFBooleanTrue,
611 (id)kSecMatchLimit : (id)kSecMatchLimitOne,
612 };
613
614 CFTypeRef item = NULL;
615 XCTAssertEqual(errSecItemNotFound, SecItemCopyMatching((__bridge CFDictionaryRef) query, &item), "item should not yet exist");
616
617 CKRecord* ckr = [self createFakeRecord: self.keychainZoneID recordName: @"7B598D31-F9C5-481E-98AC-5A507ACB2D85"];
618 CKRecord* ckr2 = [self createFakeRecord: self.keychainZoneID recordName: @"F9C58D31-7B59-481E-98AC-5A507ACB2D85"];
619
620 [self.keychainZone addToZone: ckr];
621 [self.keychainZone addToZone: ckr2];
622
623 // We expect a delete operation with the "higher" UUID.
624 [self expectCKDeleteItemRecords:1 zoneID:self.keychainZoneID];
625
626 // Trigger a notification (with hilariously fake data)
627 [self.keychainView notifyZoneChange:nil];
628
629 OCMVerifyAllWithDelay(self.mockDatabase, 6);
630 XCTAssertEqual(errSecSuccess, SecItemCopyMatching((__bridge CFDictionaryRef) query, &item), "item should exist now");
631
632 [self waitForCKModifications];
633 XCTAssertNil(self.keychainZone.currentDatabase[ckr2.recordID], "Correct record was deleted from CloudKit");
634 }
635
636 -(void)testReceiveItemDelete {
637 [self createAndSaveFakeKeyHierarchy: self.keychainZoneID]; // Make life easy for this test.
638 [self startCKKSSubsystem];
639
640 NSDictionary *query = @{(id)kSecClass : (id)kSecClassGenericPassword,
641 (id)kSecAttrAccessGroup : @"com.apple.security.ckks",
642 (id)kSecAttrAccount : @"account-delete-me",
643 (id)kSecAttrSynchronizable : (id)kCFBooleanTrue,
644 (id)kSecMatchLimit : (id)kSecMatchLimitOne,
645 };
646
647 CFTypeRef item = NULL;
648 XCTAssertEqual(errSecItemNotFound, SecItemCopyMatching((__bridge CFDictionaryRef) query, &item), "item should not yet exist");
649
650 [self.keychainView waitForFetchAndIncomingQueueProcessing];
651
652 CKRecord* ckr = [self createFakeRecord: self.keychainZoneID recordName: @"7B598D31-F9C5-481E-98AC-5A507ACB2D85"];
653 [self.keychainZone addToZone: ckr];
654
655 // Trigger a notification (with hilariously fake data)
656 [self.keychainView notifyZoneChange:nil];
657 [self.keychainView waitForFetchAndIncomingQueueProcessing];
658
659 XCTAssertEqual(errSecSuccess, SecItemCopyMatching((__bridge CFDictionaryRef) query, &item), "item should exist now");
660 CFReleaseNull(item);
661
662 // Trigger a delete
663 [self.keychainZone deleteCKRecordIDFromZone: [ckr recordID]];
664 [self.keychainView notifyZoneChange:nil];
665 [self.keychainView waitForFetchAndIncomingQueueProcessing];
666
667 XCTAssertEqual(errSecItemNotFound, SecItemCopyMatching((__bridge CFDictionaryRef) query, &item), "item should no longer exist");
668 }
669
670 -(void)testReceiveItemPhantomDelete {
671 [self createAndSaveFakeKeyHierarchy: self.keychainZoneID]; // Make life easy for this test.
672 [self startCKKSSubsystem];
673
674 NSDictionary *query = @{(id)kSecClass : (id)kSecClassGenericPassword,
675 (id)kSecAttrAccessGroup : @"com.apple.security.ckks",
676 (id)kSecAttrAccount : @"account-delete-me",
677 (id)kSecAttrSynchronizable : (id)kCFBooleanTrue,
678 (id)kSecMatchLimit : (id)kSecMatchLimitOne,
679 };
680
681 CFTypeRef item = NULL;
682 XCTAssertEqual(errSecItemNotFound, SecItemCopyMatching((__bridge CFDictionaryRef) query, &item), "item should not yet exist");
683
684 [self.keychainView waitForFetchAndIncomingQueueProcessing];
685
686 CKRecord* ckr = [self createFakeRecord: self.keychainZoneID recordName: @"7B598D31-F9C5-481E-98AC-5A507ACB2D85"];
687 [self.keychainZone addToZone: ckr];
688
689 // Trigger a notification (with hilariously fake data)
690 [self.keychainView notifyZoneChange:nil];
691 [self.keychainView waitForFetchAndIncomingQueueProcessing];
692
693 XCTAssertEqual(errSecSuccess, SecItemCopyMatching((__bridge CFDictionaryRef) query, &item), "item should exist now");
694 CFReleaseNull(item);
695
696 [self.keychainView waitUntilAllOperationsAreFinished];
697
698 // Trigger a delete
699 [self.keychainZone deleteCKRecordIDFromZone: [ckr recordID]];
700
701 // and add another, incorrect IQE
702 [self.keychainView dispatchSync: ^bool {
703 // Inefficient, but hey, it works
704 CKRecord* record = [self createFakeRecord: self.keychainZoneID recordName:@"7B598D31-F9C5-FFFF-FFFF-5A507ACB2D85"];
705 CKKSItem* fakeItem = [[CKKSItem alloc] initWithCKRecord: record];
706
707 CKKSIncomingQueueEntry* iqe = [[CKKSIncomingQueueEntry alloc] initWithCKKSItem:fakeItem
708 action:SecCKKSActionDelete
709 state:SecCKKSStateNew];
710 XCTAssertNotNil(iqe, "could create fake IQE");
711 NSError* error = nil;
712 XCTAssert([iqe saveToDatabase: &error], "Saved fake IQE to database");
713 XCTAssertNil(error, "No error saving fake IQE to database");
714 return true;
715 }];
716
717 [self.keychainView notifyZoneChange:nil];
718 [self.keychainView waitForFetchAndIncomingQueueProcessing];
719
720 XCTAssertEqual(errSecItemNotFound, SecItemCopyMatching((__bridge CFDictionaryRef) query, &item), "item should no longer exist");
721
722 // The incoming queue should be empty
723 [self.keychainView dispatchSync: ^bool {
724 NSError* error = nil;
725 NSArray* iqes = [CKKSIncomingQueueEntry all:&error];
726 XCTAssertNil(error, "No error loading IQEs");
727 XCTAssertNotNil(iqes, "Could load IQEs");
728 XCTAssertEqual(iqes.count, 0u, "Incoming queue is empty");
729 }];
730 }
731
732 -(void)testReceiveConflictOnJustAddedItem {
733 [self createAndSaveFakeKeyHierarchy: self.keychainZoneID]; // Make life easy for this test.
734 [self startCKKSSubsystem];
735
736 [self.keychainView waitForKeyHierarchyReadiness];
737 [self.keychainView waitUntilAllOperationsAreFinished];
738
739 // Place a hold on processing the outgoing queue.
740 CKKSResultOperation* blockOutgoing = [CKKSResultOperation operationWithBlock:^{
741 secnotice("ckks", "Outgoing queue hold released.");
742 }];
743 blockOutgoing.name = @"outgoing-queue-hold";
744 CKKSResultOperation* outgoingQueueOperation = [self.keychainView processOutgoingQueueAfter:blockOutgoing ckoperationGroup:nil];
745
746 CKKSResultOperation* blockIncoming = [CKKSResultOperation operationWithBlock:^{
747 secnotice("ckks", "Incoming queue hold released.");
748 }];
749 blockIncoming.name = @"incoming-queue-hold";
750 CKKSResultOperation* incomingQueueOperation = [self.keychainView processIncomingQueue:false after: blockIncoming];
751
752 [self addGenericPassword:@"localchange" account:@"account-delete-me"];
753
754 // Pull out the new item's UUID.
755 __block NSString* itemUUID = nil;
756 [self.keychainView dispatchSync:^bool {
757 NSError* error = nil;
758 NSArray<NSString*>* uuids = [CKKSOutgoingQueueEntry allUUIDs:self.keychainZoneID ?: [[CKRecordZoneID alloc] initWithZoneName:@"keychain"
759 ownerName:CKCurrentUserDefaultName]
760 error:&error];
761 XCTAssertNil(error, "no error fetching uuids");
762 XCTAssertEqual(uuids.count, 1u, "There's exactly one outgoing queue entry");
763 itemUUID = uuids[0];
764
765 XCTAssertNotNil(itemUUID, "Have a UUID for our new item");
766 return false;
767 }];
768
769 [self.keychainZone addToZone: [self createFakeRecord: self.keychainZoneID recordName: itemUUID]];
770
771 [self.keychainView notifyZoneChange:nil];
772 [[self.keychainView.zoneChangeFetcher requestSuccessfulFetch:CKKSFetchBecauseTesting] waitUntilFinished];
773
774 // Allow the outgoing queue operation to proceed
775 [self.operationQueue addOperation:blockOutgoing];
776 [outgoingQueueOperation waitUntilFinished];
777
778 // Allow the incoming queue operation to proceed
779 [self.operationQueue addOperation:blockIncoming];
780 [incomingQueueOperation waitUntilFinished];
781
782 [self checkGenericPassword:@"data" account:@"account-delete-me"];
783
784 [self.keychainView waitUntilAllOperationsAreFinished];
785 }
786
787 -(void)testReceiveUnknownField {
788 [self createAndSaveFakeKeyHierarchy: self.keychainZoneID]; // Make life easy for this test.
789
790 [self startCKKSSubsystem];
791 [self.keychainView waitForKeyHierarchyReadiness];
792
793 NSError* error = nil;
794
795 // Manually encrypt an item
796 NSString* recordName = @"7B598D31-F9C5-481E-98AC-5A507ACB2D85";
797 CKRecordID* recordID = [[CKRecordID alloc] initWithRecordName:recordName zoneID:self.keychainZoneID];
798 NSDictionary* item = [self fakeRecordDictionary: @"account-delete-me" zoneID:self.keychainZoneID];
799 CKKSItem* cipheritem = [[CKKSItem alloc] initWithUUID:recordID.recordName
800 parentKeyUUID:self.keychainZoneKeys.classA.uuid
801 zoneID:recordID.zoneID];
802 CKKSKey* itemkey = [CKKSKey randomKeyWrappedByParent: self.keychainZoneKeys.classA error:&error];
803 XCTAssertNotNil(itemkey, "Got a key");
804 cipheritem.wrappedkey = itemkey.wrappedkey;
805 XCTAssertNotNil(cipheritem.wrappedkey, "Got a wrapped key");
806
807 NSData* future_data_field = [@"asdf" dataUsingEncoding:NSUTF8StringEncoding];
808 NSString* future_string_field = @"authstring";
809 NSString* future_server_field = @"server_can_change_at_any_time";
810 NSNumber* future_number_field = [NSNumber numberWithInt:30];
811
812 // Use version 2, so future fields will be authenticated
813 cipheritem.encver = CKKSItemEncryptionVersion2;
814 NSMutableDictionary<NSString*, NSData*>* authenticatedData = [[cipheritem makeAuthenticatedDataDictionaryUpdatingCKKSItem: nil encryptionVersion:CKKSItemEncryptionVersion2] mutableCopy];
815
816 authenticatedData[@"future_data_field"] = future_data_field;
817 authenticatedData[@"future_string_field"] = [future_string_field dataUsingEncoding:NSUTF8StringEncoding];
818
819 uint64_t n = OSSwapHostToLittleConstInt64([future_number_field unsignedLongValue]);
820 authenticatedData[@"future_number_field"] = [NSData dataWithBytes:&n length:sizeof(n)];
821
822
823 cipheritem.encitem = [CKKSItemEncrypter encryptDictionary:item key:itemkey.aessivkey authenticatedData:authenticatedData error:&error];
824 XCTAssertNil(error, "no error encrypting object");
825 XCTAssertNotNil(cipheritem.encitem, "Recieved ciphertext");
826
827 CKRecord* ckr = [cipheritem CKRecordWithZoneID: recordID.zoneID];
828 ckr[@"future_data_field"] = future_data_field;
829 ckr[@"future_string_field"] = future_string_field;
830 ckr[@"future_number_field"] = future_number_field;
831 ckr[@"server_new_server_field"] = future_server_field;
832 [self.keychainZone addToZone:ckr];
833
834 [self.keychainView waitForFetchAndIncomingQueueProcessing];
835
836 NSDictionary* query = @{(id)kSecClass: (id)kSecClassGenericPassword,
837 (id)kSecReturnAttributes: @YES,
838 (id)kSecAttrSynchronizable: @YES,
839 (id)kSecAttrAccount: @"account-delete-me",
840 (id)kSecMatchLimit: (id)kSecMatchLimitOne,
841 };
842 CFTypeRef cfresult = NULL;
843 XCTAssertEqual(errSecSuccess, SecItemCopyMatching((__bridge CFDictionaryRef) query, &cfresult), "Found synced item");
844
845 // Test that if this item is updated, it remains encrypted in v2, and future_field still exists
846 [self expectCKModifyItemRecords: 1 currentKeyPointerRecords: 1 zoneID:self.keychainZoneID];
847 [self updateGenericPassword:@"different password" account:@"account-delete-me"];
848
849 OCMVerifyAllWithDelay(self.mockDatabase, 8);
850 [self waitForCKModifications];
851
852 CKRecord* newRecord = self.keychainZone.currentDatabase[recordID];
853 XCTAssertEqualObjects(newRecord[@"future_data_field"], future_data_field, "future_data_field still exists");
854 XCTAssertEqualObjects(newRecord[@"future_string_field"], future_string_field, "future_string_field still exists");
855 XCTAssertEqualObjects(newRecord[@"future_number_field"], future_number_field, "future_string_field still exists");
856 XCTAssertEqualObjects(newRecord[@"server_new_server_field"], future_server_field, "future_server_field stille exists");
857
858 CKKSItem* newItem = [[CKKSItem alloc] initWithCKRecord:newRecord];
859 CKKSAESSIVKey* newItemKey = [self.keychainZoneKeys.classA unwrapAESKey:newItem.wrappedkey error:&error];
860 XCTAssertNil(error, "No error unwrapping AES key");
861 XCTAssertNotNil(newItemKey, "Have an unwrapped AES key for this item");
862
863 NSDictionary* uploadedData = [CKKSItemEncrypter decryptDictionary:newRecord[SecCKRecordDataKey]
864 key:newItemKey
865 authenticatedData:authenticatedData
866 error:&error];
867 XCTAssertNil(error, "No error decrypting dictionary");
868 XCTAssertNotNil(uploadedData, "Authenticated re-uploaded data including future_field");
869 XCTAssertEqualObjects(uploadedData[@"v_Data"], [@"different password" dataUsingEncoding:NSUTF8StringEncoding], "Passwords match");
870 }
871
872
873 -(void)testReceiveRecordEncryptedv1 {
874 [self createAndSaveFakeKeyHierarchy: self.keychainZoneID]; // Make life easy for this test.
875
876 [self startCKKSSubsystem];
877 [self.keychainView waitForKeyHierarchyReadiness];
878
879 NSError* error = nil;
880
881 // Manually encrypt an item
882 NSString* recordName = @"7B598D31-F9C5-481E-98AC-5A507ACB2D85";
883 CKRecordID* recordID = [[CKRecordID alloc] initWithRecordName:recordName zoneID:self.keychainZoneID];
884 NSDictionary* item = [self fakeRecordDictionary: @"account-delete-me" zoneID:self.keychainZoneID];
885 CKKSItem* cipheritem = [[CKKSItem alloc] initWithUUID:recordID.recordName
886 parentKeyUUID:self.keychainZoneKeys.classC.uuid
887 zoneID:recordID.zoneID];
888 CKKSKey* itemkey = [CKKSKey randomKeyWrappedByParent: self.keychainZoneKeys.classC error:&error];
889 XCTAssertNotNil(itemkey, "Got a key");
890 cipheritem.wrappedkey = itemkey.wrappedkey;
891 XCTAssertNotNil(cipheritem.wrappedkey, "Got a wrapped key");
892
893 cipheritem.encver = CKKSItemEncryptionVersion1;
894
895 NSMutableDictionary<NSString*, NSData*>* authenticatedData = [[cipheritem makeAuthenticatedDataDictionaryUpdatingCKKSItem: nil encryptionVersion:cipheritem.encver] mutableCopy];
896
897 cipheritem.encitem = [CKKSItemEncrypter encryptDictionary:item key:itemkey.aessivkey authenticatedData:authenticatedData error:&error];
898 XCTAssertNil(error, "no error encrypting object");
899 XCTAssertNotNil(cipheritem.encitem, "Recieved ciphertext");
900
901 [self.keychainZone addToZone:[cipheritem CKRecordWithZoneID: recordID.zoneID]];
902
903 [self.keychainView waitForFetchAndIncomingQueueProcessing];
904
905 NSDictionary* query = @{(id)kSecClass: (id)kSecClassGenericPassword,
906 (id)kSecReturnAttributes: @YES,
907 (id)kSecAttrSynchronizable: @YES,
908 (id)kSecAttrAccount: @"account-delete-me",
909 (id)kSecMatchLimit: (id)kSecMatchLimitOne,
910 };
911 CFTypeRef cfresult = NULL;
912 XCTAssertEqual(errSecSuccess, SecItemCopyMatching((__bridge CFDictionaryRef) query, &cfresult), "Found synced item");
913 CFReleaseNull(cfresult);
914
915 // Test that if this item is updated, it is encrypted in v2
916 [self expectCKModifyItemRecords: 1 currentKeyPointerRecords: 1 zoneID:self.keychainZoneID];
917 [self updateGenericPassword:@"different password" account:@"account-delete-me"];
918
919 OCMVerifyAllWithDelay(self.mockDatabase, 4);
920 [self waitForCKModifications];
921
922 CKRecord* newRecord = self.keychainZone.currentDatabase[recordID];
923 XCTAssertEqualObjects(newRecord[SecCKRecordEncryptionVersionKey], [NSNumber numberWithInteger:(int) CKKSItemEncryptionVersion2], "Uploaded using encv2");
924 }
925
926 - (void)testUploadPagination {
927 [self createAndSaveFakeKeyHierarchy: self.keychainZoneID]; // Make life easy for this test.
928
929 for(size_t count = 0; count < 250; count++) {
930 [self addGenericPassword: @"data" account: [NSString stringWithFormat:@"account-delete-me-%03lu", count]];
931 }
932
933 [self startCKKSSubsystem];
934
935 [self expectCKModifyItemRecords: SecCKKSOutgoingQueueItemsAtOnce currentKeyPointerRecords: 1 zoneID:self.keychainZoneID];
936 [self expectCKModifyItemRecords: SecCKKSOutgoingQueueItemsAtOnce currentKeyPointerRecords: 1 zoneID:self.keychainZoneID];
937 [self expectCKModifyItemRecords: 50 currentKeyPointerRecords: 1 zoneID:self.keychainZoneID];
938
939 OCMVerifyAllWithDelay(self.mockDatabase, 160);
940 }
941
942 - (void)testUploadInitialKeyHierarchy {
943 // Test starts with nothing in database. We expect some sort of TLK/key hierarchy upload.
944 [self expectCKModifyKeyRecords: 3 currentKeyPointerRecords: 3 tlkShareRecords: 1 zoneID:self.keychainZoneID];
945
946 // Spin up CKKS subsystem.
947 [self startCKKSSubsystem];
948
949 OCMVerifyAllWithDelay(self.mockDatabase, 8);
950 }
951
952 - (void)testUploadInitialKeyHierarchyAfterLockedStart {
953 // 'Lock' the keybag
954 self.aksLockState = true;
955 [self.lockStateTracker recheck];
956
957 [self startCKKSSubsystem];
958
959 // Wait for the key hierarchy state machine to get stuck waiting for the unlock dependency. No uploads should occur.
960 XCTAssertEqual(0, [self.keychainView.keyHierarchyConditions[SecCKKSZoneKeyStateWaitForUnlock] wait:8*NSEC_PER_SEC], @"Key state should get stuck in waitforunlock");
961
962 // After unlock, the key hierarchy should be created.
963 [self expectCKModifyKeyRecords: 3 currentKeyPointerRecords: 3 tlkShareRecords: 1 zoneID:self.keychainZoneID];
964
965 self.aksLockState = false;
966 [self.lockStateTracker recheck];
967
968 OCMVerifyAllWithDelay(self.mockDatabase, 8);
969
970 // We expect a single class C record to be uploaded.
971 [self expectCKModifyItemRecords: 1 currentKeyPointerRecords: 1 zoneID:self.keychainZoneID checkItem: [self checkClassCBlock:self.keychainZoneID message:@"Object was encrypted under class C key in hierarchy"]];
972
973 [self addGenericPassword: @"data" account: @"account-delete-me"];
974 OCMVerifyAllWithDelay(self.mockDatabase, 8);
975 }
976
977 - (void)testLockImmediatelyAfterUploadingInitialKeyHierarchy {
978
979 // Upon upload, block fetches
980 __weak __typeof(self) weakSelf = self;
981 [self expectCKModifyRecords: @{
982 SecCKRecordIntermediateKeyType: [NSNumber numberWithUnsignedInteger: 3],
983 SecCKRecordCurrentKeyType: [NSNumber numberWithUnsignedInteger: 3],
984 SecCKRecordTLKShareType: [NSNumber numberWithUnsignedInteger: 1],
985 }
986 deletedRecordTypeCounts:nil
987 zoneID:self.keychainZoneID
988 checkModifiedRecord:nil
989 runAfterModification:^{
990 __strong __typeof(self) strongSelf = weakSelf;
991 [strongSelf holdCloudKitFetches];
992 }];
993
994 [self startCKKSSubsystem];
995
996 // Should enter 'ready'
997 XCTAssertEqual(0, [self.keychainView.keyHierarchyConditions[SecCKKSZoneKeyStateReady] wait:180*NSEC_PER_SEC], @"Key state should become 'ready'");
998 OCMVerifyAllWithDelay(self.mockDatabase, 8);
999
1000 // Now, lock and allow fetches again
1001 self.aksLockState = true;
1002 [self.lockStateTracker recheck];
1003 [self releaseCloudKitFetchHold];
1004
1005 CKKSResultOperation* op = [self.keychainView.zoneChangeFetcher requestSuccessfulFetch:CKKSFetchBecauseTesting];
1006 [op waitUntilFinished];
1007
1008 OCMVerifyAllWithDelay(self.mockDatabase, 8);
1009
1010 // Wait for CKKS to shake itself out...
1011 [self.keychainView waitForOperationsOfClass:[CKKSProcessReceivedKeysOperation class]];
1012
1013 // Should be in ReadyPendingUnlock
1014 XCTAssertEqual(0, [self.keychainView.keyHierarchyConditions[SecCKKSZoneKeyStateReadyPendingUnlock] wait:8*NSEC_PER_SEC], @"Key state should become 'readypendingunlock'");
1015
1016 // We expect a single class C record to be uploaded.
1017 [self expectCKModifyItemRecords: 1 currentKeyPointerRecords: 1 zoneID:self.keychainZoneID
1018 checkItem: [self checkClassCBlock:self.keychainZoneID message:@"Object was encrypted under class C key in hierarchy"]];
1019
1020 [self addGenericPassword: @"data" account: @"account-delete-me"];
1021 OCMVerifyAllWithDelay(self.mockDatabase, 8);
1022 }
1023
1024 - (void)testReceiveKeyHierarchyAfterLockedStart {
1025 // 'Lock' the keybag
1026 self.aksLockState = true;
1027 [self.lockStateTracker recheck];
1028
1029 [self startCKKSSubsystem];
1030
1031 // Wait for the key hierarchy state machine to get stuck waiting for the unlock dependency. No uploads should occur.
1032 XCTAssertEqual(0, [self.keychainView.keyHierarchyConditions[SecCKKSZoneKeyStateFetchComplete] wait:8*NSEC_PER_SEC], @"Key state should get stuck in fetchcomplete");
1033
1034 // Now, another device comes along and creates the hierarchy; we download it; and it and sends us the TLK
1035 [self putFakeKeyHierarchyInCloudKit:self.keychainZoneID];
1036 [self.keychainView notifyZoneChange:nil];
1037 [[self.keychainView.zoneChangeFetcher requestSuccessfulFetch:CKKSFetchBecauseTesting] waitUntilFinished];
1038
1039 self.aksLockState = false;
1040 [self.lockStateTracker recheck];
1041
1042 // After unlock, the TLK arrives
1043 [self expectCKKSTLKSelfShareUpload:self.keychainZoneID];
1044 [self saveTLKMaterialToKeychain:self.keychainZoneID];
1045
1046 // We expect a single class C record to be uploaded.
1047 [self expectCKModifyItemRecords: 1 currentKeyPointerRecords: 1 zoneID:self.keychainZoneID checkItem: [self checkClassCBlock:self.keychainZoneID message:@"Object was encrypted under class C key in hierarchy"]];
1048
1049 [self addGenericPassword: @"data" account: @"account-delete-me"];
1050 OCMVerifyAllWithDelay(self.mockDatabase, 8);
1051 }
1052
1053 - (void)testLoadKeyHierarchyAfterLockedStart {
1054 [self createAndSaveFakeKeyHierarchy: self.keychainZoneID];
1055
1056 // 'Lock' the keybag
1057 self.aksLockState = true;
1058 [self.lockStateTracker recheck];
1059
1060 [self startCKKSSubsystem];
1061
1062 // Wait for the key hierarchy state machine to get stuck waiting for the unlock dependency. No uploads should occur.
1063 XCTAssertEqual(0, [self.keychainView.keyHierarchyConditions[SecCKKSZoneKeyStateReadyPendingUnlock] wait:8*NSEC_PER_SEC], @"Key state should become 'readypendingunlock'");
1064
1065 self.aksLockState = false;
1066 [self.lockStateTracker recheck];
1067
1068 // We expect a single class C record to be uploaded.
1069 [self expectCKModifyItemRecords: 1 currentKeyPointerRecords: 1 zoneID:self.keychainZoneID checkItem: [self checkClassCBlock:self.keychainZoneID message:@"Object was encrypted under class C key in hierarchy"]];
1070
1071 [self addGenericPassword: @"data" account: @"account-delete-me"];
1072 OCMVerifyAllWithDelay(self.mockDatabase, 8);
1073 }
1074
1075 - (void)testUploadAndUseKeyHierarchy {
1076 // Test starts with nothing in database. We expect some sort of TLK/key hierarchy upload.
1077 [self expectCKModifyKeyRecords: 3 currentKeyPointerRecords: 3 tlkShareRecords: 1 zoneID:self.keychainZoneID];
1078
1079 [self startCKKSSubsystem];
1080
1081 NSDictionary *query = @{(id)kSecClass : (id)kSecClassGenericPassword,
1082 (id)kSecAttrAccessGroup : @"com.apple.security.ckks",
1083 (id)kSecAttrAccount : @"account-delete-me",
1084 (id)kSecAttrSynchronizable : (id)kCFBooleanTrue,
1085 (id)kSecMatchLimit : (id)kSecMatchLimitOne,
1086 };
1087 CFTypeRef item = NULL;
1088 XCTAssertEqual(errSecItemNotFound, SecItemCopyMatching((__bridge CFDictionaryRef) query, &item), "item should not exist");
1089
1090 OCMVerifyAllWithDelay(self.mockDatabase, 8);
1091 [self waitForCKModifications];
1092
1093 // We expect a single class C record to be uploaded.
1094 [self expectCKModifyItemRecords: 1 currentKeyPointerRecords: 1 zoneID:self.keychainZoneID checkItem: [self checkClassCBlock:self.keychainZoneID message:@"Object was encrypted under class C key in hierarchy"]];
1095
1096 [self addGenericPassword: @"data" account: @"account-delete-me"];
1097 OCMVerifyAllWithDelay(self.mockDatabase, 8);
1098
1099 // now, expect a single class A record to be uploaded
1100 [self expectCKModifyItemRecords: 1 currentKeyPointerRecords: 1 zoneID:self.keychainZoneID checkItem: [self checkClassABlock:self.keychainZoneID message:@"Object was encrypted under class A key in hierarchy"]];
1101
1102 XCTAssertEqual(errSecSuccess, SecItemAdd((__bridge CFDictionaryRef)@{
1103 (id)kSecClass : (id)kSecClassGenericPassword,
1104 (id)kSecAttrAccessGroup : @"com.apple.security.sos",
1105 (id)kSecAttrAccessible: (id)kSecAttrAccessibleWhenUnlocked,
1106 (id)kSecAttrAccount : @"account-class-A",
1107 (id)kSecAttrSynchronizable : (id)kCFBooleanTrue,
1108 (id)kSecValueData : (id) [@"asdf" dataUsingEncoding:NSUTF8StringEncoding],
1109 }, NULL), @"Adding class A item");
1110 OCMVerifyAllWithDelay(self.mockDatabase, 8);
1111 }
1112
1113 - (void)testUploadInitialKeyHierarchyTriggersBackup {
1114 // Test starts with nothing in database. We expect some sort of TLK/key hierarchy upload.
1115 [self expectCKModifyKeyRecords: 3 currentKeyPointerRecords: 3 tlkShareRecords: 1 zoneID:self.keychainZoneID];
1116
1117 // We also expect the view manager's notifyNewTLKsInKeychain call to fire (after some delay)
1118 OCMExpect([self.mockCKKSViewManager notifyNewTLKsInKeychain]);
1119
1120 // Spin up CKKS subsystem.
1121 [self startCKKSSubsystem];
1122
1123 OCMVerifyAllWithDelay(self.mockDatabase, 8);
1124 OCMVerifyAllWithDelay(self.mockCKKSViewManager, 10);
1125 }
1126
1127 - (void)testAcceptExistingKeyHierarchy {
1128 // Test starts with no keys in CKKS database, but one in our fake CloudKit.
1129 // Test also begins with the TLK having arrived in the local keychain (via SOS)
1130 [self putFakeKeyHierarchyInCloudKit:self.keychainZoneID];
1131 [self saveTLKMaterialToKeychain:self.keychainZoneID];
1132 [self expectCKKSTLKSelfShareUpload:self.keychainZoneID];
1133
1134 // Spin up CKKS subsystem.
1135 [self startCKKSSubsystem];
1136
1137 // The CKKS subsystem should only upload its TLK share
1138 XCTAssertEqual(0, [self.keychainView.keyHierarchyConditions[SecCKKSZoneKeyStateReady] wait:5*NSEC_PER_SEC], "Key state should have become ready");
1139
1140 OCMVerifyAllWithDelay(self.mockDatabase, 8);
1141
1142 // Verify that there are three local keys, and three local current key records
1143 __weak __typeof(self) weakSelf = self;
1144 [self.keychainView dispatchSync: ^bool{
1145 __strong __typeof(weakSelf) strongSelf = weakSelf;
1146 XCTAssertNotNil(strongSelf, "self exists");
1147
1148 NSError* error = nil;
1149
1150 NSArray<CKKSKey*>* keys = [CKKSKey localKeys:strongSelf.keychainZoneID error:&error];
1151 XCTAssertNil(error, "no error fetching keys");
1152 XCTAssertEqual(keys.count, 3u, "Three keys in local database");
1153
1154 NSArray<CKKSCurrentKeyPointer*>* currentkeys = [CKKSCurrentKeyPointer all: &error];
1155 XCTAssertNil(error, "no error fetching current keys");
1156 XCTAssertEqual(currentkeys.count, 3u, "Three current key pointers in local database");
1157
1158 return false;
1159 }];
1160 }
1161
1162 - (void)testAcceptExistingAndUseKeyHierarchy {
1163 // Test starts with nothing in database, but one in our fake CloudKit.
1164 [self putFakeKeyHierarchyInCloudKit:self.keychainZoneID];
1165
1166 [self startCKKSSubsystem];
1167 XCTAssertEqual(0, [self.keychainView.keyHierarchyConditions[SecCKKSZoneKeyStateWaitForTLK] wait:5*NSEC_PER_SEC], "Key state should have become waitfortlk");
1168
1169 // Now, save the TLK to the keychain (to simulate it coming in later via SOS). We'll create a TLK share for ourselves.
1170 [self expectCKKSTLKSelfShareUpload:self.keychainZoneID];
1171 [self saveTLKMaterialToKeychainSimulatingSOS:self.keychainZoneID];
1172
1173 // Wait for the key hierarchy to sort itself out, to make it easier on this test; see testOnboardOldItemsWithExistingKeyHierarchy for the other test.
1174 // The CKKS subsystem should write its TLK share, but nothing else
1175 XCTAssertEqual(0, [self.keychainView.keyHierarchyConditions[SecCKKSZoneKeyStateReady] wait:5*NSEC_PER_SEC], "Key state should have become ready");
1176
1177 // We expect a single record to be uploaded for each key class
1178 [self expectCKModifyItemRecords: 1 currentKeyPointerRecords: 1 zoneID:self.keychainZoneID checkItem: [self checkClassCBlock:self.keychainZoneID message:@"Object was encrypted under class C key in hierarchy"]];
1179 [self addGenericPassword: @"data" account: @"account-delete-me"];
1180 OCMVerifyAllWithDelay(self.mockDatabase, 8);
1181
1182 [self expectCKModifyItemRecords: 1 currentKeyPointerRecords: 1 zoneID:self.keychainZoneID checkItem: [self checkClassABlock:self.keychainZoneID message:@"Object was encrypted under class A key in hierarchy"]];
1183 [self addGenericPassword:@"asdf"
1184 account:@"account-class-A"
1185 viewHint:nil
1186 access:(id)kSecAttrAccessibleWhenUnlocked
1187 expecting:errSecSuccess
1188 message:@"Adding class A item"];
1189 OCMVerifyAllWithDelay(self.mockDatabase, 8);
1190 }
1191
1192 - (void)testAcceptExistingKeyHierarchyDespiteLocked {
1193 // Test starts with no keys in CKKS database, but one in our fake CloudKit.
1194 // Test also begins with the TLK having arrived in the local keychain (via SOS)
1195 // However, the CKKSKeychainView's "_onqueueWithAccountKeysCheckTLK" method should return a keychain error the first time through, indicating that the keybag is locked
1196 [self putFakeKeyHierarchyInCloudKit:self.keychainZoneID];
1197 [self saveTLKMaterialToKeychain:self.keychainZoneID];
1198
1199 self.aksLockState = true;
1200 [self.lockStateTracker recheck];
1201
1202 id partialKVMock = OCMPartialMock(self.keychainView);
1203 OCMExpect([partialKVMock _onqueueWithAccountKeysCheckTLK: [OCMArg any] error: [OCMArg setTo:[[NSError alloc] initWithDomain:@"securityd" code:errSecInteractionNotAllowed userInfo:nil]]]).andReturn(false);
1204
1205 // Spin up CKKS subsystem.
1206 [self startCKKSSubsystem];
1207
1208 OCMVerifyAllWithDelay(partialKVMock, 4);
1209
1210 // CKKS will give itself a TLK Share
1211 [self expectCKKSTLKSelfShareUpload:self.keychainZoneID];
1212
1213 // Now that all operations are complete, 'unlock' AKS
1214 self.aksLockState = false;
1215 [self.lockStateTracker recheck];
1216
1217 XCTAssertEqual(0, [self.keychainView.keyHierarchyConditions[SecCKKSZoneKeyStateReady] wait:5*NSEC_PER_SEC], "Key state should have become ready");
1218 OCMVerifyAllWithDelay(self.mockDatabase, 4);
1219
1220 // Verify that there are three local keys, and three local current key records
1221 __weak __typeof(self) weakSelf = self;
1222 [self.keychainView dispatchSync: ^bool{
1223 __strong __typeof(weakSelf) strongSelf = weakSelf;
1224 XCTAssertNotNil(strongSelf, "self exists");
1225
1226 NSError* error = nil;
1227
1228 NSArray<CKKSKey*>* keys = [CKKSKey localKeys:strongSelf.keychainZoneID error:&error];
1229 XCTAssertNil(error, "no error fetching keys");
1230 XCTAssertEqual(keys.count, 3u, "Three keys in local database");
1231
1232 NSArray<CKKSCurrentKeyPointer*>* currentkeys = [CKKSCurrentKeyPointer all: &error];
1233 XCTAssertNil(error, "no error fetching current keys");
1234 XCTAssertEqual(currentkeys.count, 3u, "Three current key pointers in local database");
1235
1236 return false;
1237 }];
1238
1239 [partialKVMock stopMocking];
1240 }
1241
1242 - (void)testReceiveClassCWhileALocked {
1243 // Test starts with a key hierarchy already existing.
1244 [self createAndSaveFakeKeyHierarchy:self.keychainZoneID];
1245 [self expectCKKSTLKSelfShareUpload:self.keychainZoneID];
1246 [self startCKKSSubsystem];
1247
1248 XCTAssertEqual(0, [self.keychainView.keyHierarchyConditions[SecCKKSZoneKeyStateReady] wait:8*NSEC_PER_SEC], @"Key state should become 'ready'");
1249 [self.keychainView waitForFetchAndIncomingQueueProcessing];
1250
1251 [self findGenericPassword:@"classCItem" expecting:errSecItemNotFound];
1252 [self findGenericPassword:@"classAItem" expecting:errSecItemNotFound];
1253
1254 // 'Lock' the keybag
1255 self.aksLockState = true;
1256 [self.lockStateTracker recheck];
1257
1258 XCTAssertNotNil(self.keychainZoneKeys, "Have zone keys for zone");
1259 XCTAssertNotNil(self.keychainZoneKeys.classA, "Have class A key for zone");
1260 XCTAssertNotNil(self.keychainZoneKeys.classC, "Have class C key for zone");
1261
1262 [self.keychainView dispatchSync: ^bool {
1263 [self.keychainView _onqueueKeyStateMachineRequestProcess];
1264 return true;
1265 }];
1266 // And ensure we end up back in 'readypendingunlock': we have the keys, we're just locked now
1267 [self.keychainView waitForOperationsOfClass:[CKKSProcessReceivedKeysOperation class]];
1268 XCTAssertEqual(0, [self.keychainView.keyHierarchyConditions[SecCKKSZoneKeyStateReadyPendingUnlock] wait:8*NSEC_PER_SEC], @"Key state should become 'readypendingunlock'");
1269
1270 [self.keychainZone addToZone: [self createFakeRecord: self.keychainZoneID recordName:@"7B598D31-F9C5-481E-98AC-5A507ACB2D85" withAccount:@"classCItem" key:self.keychainZoneKeys.classC]];
1271 [self.keychainZone addToZone: [self createFakeRecord: self.keychainZoneID recordName:@"7B598D31-FFFF-FFFF-FFFF-5A507ACB2D85" withAccount:@"classAItem" key:self.keychainZoneKeys.classA]];
1272
1273 CKKSResultOperation* op = [self.keychainView waitForFetchAndIncomingQueueProcessing];
1274 // The processing op should NOT error, even though it didn't manage to process the classA item
1275 XCTAssertNil(op.error, "no error while failing to process a class A item");
1276
1277 CKKSResultOperation* erroringOp = [self.keychainView processIncomingQueue:true];
1278 [erroringOp waitUntilFinished];
1279 XCTAssertNotNil(erroringOp.error, "error exists while processing a class A item");
1280
1281 [self findGenericPassword:@"classCItem" expecting:errSecSuccess];
1282 [self findGenericPassword:@"classAItem" expecting:errSecItemNotFound];
1283
1284 self.aksLockState = false;
1285 [self.lockStateTracker recheck];
1286 [self.keychainView waitUntilAllOperationsAreFinished];
1287
1288 [self findGenericPassword:@"classCItem" expecting:errSecSuccess];
1289 [self findGenericPassword:@"classAItem" expecting:errSecSuccess];
1290 }
1291
1292 - (void)testRestartWhileLocked {
1293 [self expectCKModifyKeyRecords: 3 currentKeyPointerRecords: 3 tlkShareRecords: 1 zoneID:self.keychainZoneID];
1294 [self startCKKSSubsystem];
1295
1296 XCTAssertEqual(0, [self.keychainView.keyHierarchyConditions[SecCKKSZoneKeyStateReady] wait:8*NSEC_PER_SEC], @"Key state should become 'ready'");
1297
1298 // 'Lock' the keybag
1299 self.aksLockState = true;
1300 [self.lockStateTracker recheck];
1301
1302 [self.keychainView halt];
1303 self.keychainView = [[CKKSViewManager manager] restartZone: self.keychainZoneID.zoneName];
1304
1305 XCTAssertEqual(0, [self.keychainView.keyHierarchyConditions[SecCKKSZoneKeyStateReadyPendingUnlock] wait:8*NSEC_PER_SEC], @"Key state should become 'readypendingunlock'");
1306
1307 self.aksLockState = false;
1308 [self.lockStateTracker recheck];
1309
1310 XCTAssertEqual(0, [self.keychainView.keyHierarchyConditions[SecCKKSZoneKeyStateReady] wait:8*NSEC_PER_SEC], @"Key state should become 'ready'");
1311 }
1312
1313 - (void)testExternalKeyRoll {
1314 // Test starts with no keys in database, a key hierarchy in our fake CloudKit, and the TLK safely in the local keychain.
1315 [self putFakeKeyHierarchyInCloudKit:self.keychainZoneID];
1316 [self expectCKKSTLKSelfShareUpload:self.keychainZoneID];
1317 [self saveTLKMaterialToKeychain:self.keychainZoneID];
1318
1319 // Spin up CKKS subsystem.
1320 [self startCKKSSubsystem];
1321
1322 // The CKKS subsystem should not try to write anything to the CloudKit database.
1323 OCMVerifyAllWithDelay(self.mockDatabase, 8);
1324
1325 __weak __typeof(self) weakSelf = self;
1326
1327 // We expect a single record to be uploaded.
1328 [self expectCKModifyItemRecords: 1 currentKeyPointerRecords: 1 zoneID:self.keychainZoneID checkItem: [self checkClassCBlock:self.keychainZoneID message:@"Object was encrypted under class C key in hierarchy"]];
1329
1330 [self addGenericPassword: @"data" account: @"account-delete-me"];
1331
1332 OCMVerifyAllWithDelay(self.mockDatabase, 8);
1333 [self waitForCKModifications];
1334
1335 [self rollFakeKeyHierarchyInCloudKit:self.keychainZoneID];
1336 [self expectCKKSTLKSelfShareUpload:self.keychainZoneID];
1337 [self saveTLKMaterialToKeychain:self.keychainZoneID];
1338
1339 // Trigger a notification
1340 [self.keychainView notifyZoneChange:nil];
1341
1342 // Make life easy on this test; testAcceptKeyConflictAndUploadReencryptedItem will check the case when we don't receive the notification
1343 [self.keychainView waitForFetchAndIncomingQueueProcessing];
1344
1345 // Verify that there are six local keys, and three local current key records
1346 [self.keychainView dispatchSync: ^bool{
1347 __strong __typeof(weakSelf) strongSelf = weakSelf;
1348 XCTAssertNotNil(strongSelf, "self exists");
1349
1350 NSError* error = nil;
1351 NSArray<CKKSKey*>* keys = [CKKSKey localKeys:self.keychainZoneID error:&error];
1352 XCTAssertNil(error, "no error fetching keys");
1353 XCTAssertEqual(keys.count, 6u, "Six keys in local database");
1354
1355 NSArray<CKKSCurrentKeyPointer*>* currentkeys = [CKKSCurrentKeyPointer all: &error];
1356 XCTAssertNil(error, "no error fetching current keys");
1357 XCTAssertEqual(currentkeys.count, 3u, "Three current key pointers in local database");
1358
1359 for(CKKSCurrentKeyPointer* key in currentkeys) {
1360 if([key.keyclass isEqualToString: SecCKKSKeyClassTLK]) {
1361 XCTAssertEqualObjects(key.currentKeyUUID, strongSelf.keychainZoneKeys.tlk.uuid);
1362 } else if([key.keyclass isEqualToString: SecCKKSKeyClassA]) {
1363 XCTAssertEqualObjects(key.currentKeyUUID, strongSelf.keychainZoneKeys.classA.uuid);
1364 } else if([key.keyclass isEqualToString: SecCKKSKeyClassC]) {
1365 XCTAssertEqualObjects(key.currentKeyUUID, strongSelf.keychainZoneKeys.classC.uuid);
1366 } else {
1367 XCTFail("Unknown key class: %@", key.keyclass);
1368 }
1369 }
1370
1371 return false;
1372 }];
1373
1374 // We expect a single record to be uploaded.
1375 [self expectCKModifyItemRecords: 1 currentKeyPointerRecords: 1 zoneID:self.keychainZoneID checkItem: [self checkClassCBlock:self.keychainZoneID message:@"Object was encrypted under class C key in hierarchy"]];
1376
1377 // TODO: remove this by writing code for item reencrypt after key arrival
1378 [self.keychainView waitForFetchAndIncomingQueueProcessing];
1379
1380 [self addGenericPassword: @"data" account: @"account-delete-me-rolled-key"];
1381
1382 OCMVerifyAllWithDelay(self.mockDatabase, 8);
1383 }
1384
1385 - (void)testAcceptKeyConflictAndUploadReencryptedItem {
1386 // Test starts with no keys in database, a key hierarchy in our fake CloudKit, and the TLK safely in the local keychain.
1387 [self putFakeKeyHierarchyInCloudKit:self.keychainZoneID];
1388 [self expectCKKSTLKSelfShareUpload:self.keychainZoneID];
1389 [self saveTLKMaterialToKeychain:self.keychainZoneID];
1390
1391 [self startCKKSSubsystem];
1392 [self.keychainView waitUntilAllOperationsAreFinished];
1393
1394 // We expect a single record to be uploaded.
1395 [self expectCKModifyItemRecords: 1 currentKeyPointerRecords: 1 zoneID:self.keychainZoneID checkItem: [self checkClassCBlock:self.keychainZoneID message:@"Object was encrypted under class C key in hierarchy"]];
1396
1397 [self addGenericPassword: @"data" account: @"account-delete-me"];
1398
1399 OCMVerifyAllWithDelay(self.mockDatabase, 8);
1400 [self waitForCKModifications];
1401
1402 [self rollFakeKeyHierarchyInCloudKit:self.keychainZoneID];
1403
1404 // Do not trigger a notification here. This should cause a conflict updating the current key records
1405
1406 // We expect a single record to be uploaded, but that the write will be rejected
1407 // We then expect that item to be reuploaded with the current key
1408
1409 [self expectCKAtomicModifyItemRecordsUpdateFailure: self.keychainZoneID];
1410 [self addGenericPassword: @"data" account: @"account-delete-me-rolled-key"];
1411 OCMVerifyAllWithDelay(self.mockDatabase, 8);
1412
1413 [self expectCKModifyItemRecords: 1 currentKeyPointerRecords: 1 zoneID:self.keychainZoneID checkItem: [self checkClassCBlock:self.keychainZoneID message:@"Object was encrypted under rolled class C key in hierarchy"]];
1414
1415 // New key arrives via SOS!
1416 [self expectCKKSTLKSelfShareUpload:self.keychainZoneID];
1417 [self saveTLKMaterialToKeychainSimulatingSOS:self.keychainZoneID];
1418
1419 OCMVerifyAllWithDelay(self.mockDatabase, 8);
1420 }
1421
1422 - (void)testRecoverFromRequestKeyRefetchWithoutRolling {
1423 // Simply requesting a key state refetch shouldn't roll the key hierarchy.
1424
1425 [self createAndSaveFakeKeyHierarchy: self.keychainZoneID]; // Make life easy for this test.
1426
1427 // Spin up CKKS subsystem.
1428 [self startCKKSSubsystem];
1429
1430 // Items should upload.
1431 [self expectCKModifyItemRecords: 1 currentKeyPointerRecords: 1 zoneID:self.keychainZoneID];
1432 [self addGenericPassword: @"data" account: @"account-delete-me"];
1433 OCMVerifyAllWithDelay(self.mockDatabase, 8);
1434
1435 [self waitForCKModifications];
1436
1437
1438 // CKKS should not roll the keys while progressing back to 'ready', but it will fetch once
1439 self.silentFetchesAllowed = false;
1440 [self expectCKFetch];
1441
1442 [self.keychainView dispatchSync: ^bool {
1443 [self.keychainView _onqueueKeyStateMachineRequestFetch];
1444 return true;
1445 }];
1446
1447 XCTAssertEqual(0, [self.keychainView.keyHierarchyConditions[SecCKKSZoneKeyStateReady] wait:5*NSEC_PER_SEC], "Key state should have returned to ready");
1448 OCMVerifyAllWithDelay(self.mockDatabase, 8);
1449 }
1450
1451 - (void)testRecoverFromIncrementedCurrentKeyPointerEtag {
1452 // CloudKit sometimes reports the current key pointers have changed (etag mismatch), but their content hasn't.
1453 // In this case, CKKS shouldn't roll the TLK.
1454
1455 [self createAndSaveFakeKeyHierarchy: self.keychainZoneID]; // Make life easy for this test.
1456
1457 // Spin up CKKS subsystem.
1458 [self startCKKSSubsystem];
1459 [self.keychainView waitForFetchAndIncomingQueueProcessing]; // just to be sure it's fetched
1460
1461 // Items should upload.
1462 [self expectCKModifyItemRecords: 1 currentKeyPointerRecords: 1 zoneID:self.keychainZoneID];
1463 [self addGenericPassword: @"data" account: @"account-delete-me"];
1464 OCMVerifyAllWithDelay(self.mockDatabase, 8);
1465
1466 [self waitForCKModifications];
1467
1468 // Bump the etag on the class C current key record, but don't change any data
1469 CKRecordID* currentClassCID = [[CKRecordID alloc] initWithRecordName: @"classC" zoneID: self.keychainZoneID];
1470 CKRecord* currentClassC = self.keychainZone.currentDatabase[currentClassCID];
1471 XCTAssertNotNil(currentClassC, "Should have the class C current key pointer record");
1472
1473 [self.keychainZone addCKRecordToZone:[currentClassC copy]];
1474 XCTAssertNotEqualObjects(currentClassC.etag, self.keychainZone.currentDatabase[currentClassCID].etag, "Etag should have changed");
1475
1476 // Add another item. This write should fail, then CKKS should recover without rolling the key hierarchy or issuing a fetch.
1477 self.silentFetchesAllowed = false;
1478 [self expectCKAtomicModifyItemRecordsUpdateFailure:self.keychainZoneID];
1479 [self expectCKModifyItemRecords: 1 currentKeyPointerRecords: 1 zoneID:self.keychainZoneID];
1480 [self addGenericPassword: @"data" account: @"account-delete-me-2"];
1481 OCMVerifyAllWithDelay(self.mockDatabase, 8);
1482 }
1483
1484 - (void)testOnboardOldItemsCreatingKeyHierarchy {
1485 // 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
1486
1487 // Test starts with nothing in CloudKit, and CKKS blocked. Add one item without a UUID...
1488
1489 SecCKKSTestSetDisableAutomaticUUID(true);
1490 [self addGenericPassword: @"data" account: @"account-delete-me-no-UUID" expecting:errSecSuccess message: @"Add item (no UUID) to keychain"];
1491
1492 // and an item with a UUID...
1493 SecCKKSTestSetDisableAutomaticUUID(false);
1494 [self addGenericPassword: @"data" account: @"account-delete-me-with-UUID" expecting:errSecSuccess message: @"Add item (w/ UUID) to keychain"];
1495
1496 // We expect an upload of the key hierarchy
1497 [self expectCKModifyKeyRecords: 3 currentKeyPointerRecords: 3 tlkShareRecords: 1 zoneID:self.keychainZoneID];
1498
1499 // We then expect an upload of the added items
1500 [self expectCKModifyItemRecords: 2 currentKeyPointerRecords: 1 zoneID:self.keychainZoneID];
1501
1502 [self startCKKSSubsystem];
1503
1504 OCMVerifyAllWithDelay(self.mockDatabase, 8);
1505 }
1506
1507 - (void)testOnboardOldItemsWithExistingKeyHierarchy {
1508 [self createAndSaveFakeKeyHierarchy: self.keychainZoneID]; // Make life easy for this test.
1509
1510 [self expectCKModifyItemRecords: 1 currentKeyPointerRecords: 1 zoneID:self.keychainZoneID];
1511 [self addGenericPassword: @"data" account: @"account-delete-me"];
1512
1513 [self startCKKSSubsystem];
1514 OCMVerifyAllWithDelay(self.mockDatabase, 8);
1515 }
1516
1517 - (void)testOnboardOldItemsWithExistingKeyHierarchyExtantTLK {
1518 // Test starts key hierarchy in our fake CloudKit, the TLK arrived in the local keychain, and CKKS blocked.
1519 [self putFakeKeyHierarchyInCloudKit:self.keychainZoneID];
1520 [self expectCKKSTLKSelfShareUpload:self.keychainZoneID];
1521 [self saveTLKMaterialToKeychain:self.keychainZoneID];
1522
1523 // Add one item without a UUID...
1524 SecCKKSTestSetDisableAutomaticUUID(true);
1525 [self addGenericPassword: @"data" account: @"account-delete-me-no-UUID" expecting:errSecSuccess message: @"Add item (no UUID) to keychain"];
1526
1527 // and an item with a UUID...
1528 SecCKKSTestSetDisableAutomaticUUID(false);
1529 [self addGenericPassword: @"data" account: @"account-delete-me-with-UUID" expecting:errSecSuccess message: @"Add item (w/ UUID) to keychain"];
1530
1531 // 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
1532 // We expect a single record to be uploaded.
1533 [self expectCKModifyItemRecords: 2 currentKeyPointerRecords: 1 zoneID:self.keychainZoneID checkItem: [self checkClassCBlock:self.keychainZoneID message:@"Object was encrypted under class C key in hierarchy"]];
1534
1535 // Spin up CKKS subsystem.
1536 [self startCKKSSubsystem];
1537
1538 OCMVerifyAllWithDelay(self.mockDatabase, 8);
1539 }
1540
1541 - (void)testOnboardOldItemsWithExistingKeyHierarchyLateTLK {
1542 // Test starts key hierarchy in our fake CloudKit, and CKKS blocked.
1543 [self putFakeKeyHierarchyInCloudKit:self.keychainZoneID];
1544
1545 // Add one item without a UUID...
1546 SecCKKSTestSetDisableAutomaticUUID(true);
1547 [self addGenericPassword: @"data" account: @"account-delete-me-no-UUID" expecting:errSecSuccess message: @"Add item (no UUID) to keychain"];
1548
1549 // and an item with a UUID...
1550 SecCKKSTestSetDisableAutomaticUUID(false);
1551 [self addGenericPassword: @"data" account: @"account-delete-me-with-UUID" expecting:errSecSuccess message: @"Add item (w/ UUID) to keychain"];
1552
1553 // 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
1554
1555 // Spin up CKKS subsystem.
1556 [self startCKKSSubsystem];
1557 XCTAssertEqual(0, [self.keychainView.keyHierarchyConditions[SecCKKSZoneKeyStateWaitForTLK] wait:5*NSEC_PER_SEC], "Key state should have become waitfortlk");
1558
1559 // Now, save the TLK to the keychain (to simulate it coming in via SOS).
1560 [self expectCKKSTLKSelfShareUpload:self.keychainZoneID];
1561 [self saveTLKMaterialToKeychainSimulatingSOS:self.keychainZoneID];
1562
1563 // We expect a single record to be uploaded.
1564 [self expectCKModifyItemRecords: 2 currentKeyPointerRecords: 1 zoneID:self.keychainZoneID checkItem: [self checkClassCBlock:self.keychainZoneID message:@"Object was encrypted under class C key in hierarchy"]];
1565
1566 OCMVerifyAllWithDelay(self.mockDatabase, 8);
1567 }
1568
1569 - (void)testResync {
1570 // We need to set up a desynced situation to test our resync.
1571 // First, let CKKS start up and send several items to CloudKit (that we'll then desync!)
1572 __block NSError* error = nil;
1573
1574 // Test starts with keys in CloudKit (so we can create items later)
1575 [self putFakeKeyHierarchyInCloudKit:self.keychainZoneID];
1576 [self expectCKKSTLKSelfShareUpload:self.keychainZoneID];
1577 [self saveTLKMaterialToKeychain:self.keychainZoneID];
1578
1579 [self addGenericPassword: @"data" account: @"first"];
1580 [self addGenericPassword: @"data" account: @"second"];
1581 [self addGenericPassword: @"data" account: @"third"];
1582 [self addGenericPassword: @"data" account: @"fourth"];
1583 NSUInteger passwordCount = 4u;
1584
1585 [self checkGenericPassword: @"data" account: @"first"];
1586 [self checkGenericPassword: @"data" account: @"second"];
1587 [self checkGenericPassword: @"data" account: @"third"];
1588 [self checkGenericPassword: @"data" account: @"fourth"];
1589
1590 [self expectCKModifyItemRecords: passwordCount currentKeyPointerRecords: 1 zoneID:self.keychainZoneID];
1591
1592 [self startCKKSSubsystem];
1593
1594 // Wait for uploads to happen
1595 OCMVerifyAllWithDelay(self.mockDatabase, 8);
1596 [self waitForCKModifications];
1597 // One TLK share record
1598 XCTAssertEqual(self.keychainZone.currentDatabase.count, SYSTEM_DB_RECORD_COUNT+passwordCount+1, "Have 6+passwordCount objects in cloudkit");
1599
1600 // Now, corrupt away!
1601 // Extract all passwordCount items for Corruption
1602 NSArray<CKRecord*>* items = [self.keychainZone.currentDatabase.allValues filteredArrayUsingPredicate: [NSPredicate predicateWithFormat:@"self.recordType like %@", SecCKRecordItemType]];
1603 XCTAssertEqual(items.count, passwordCount, "Have %lu Items in cloudkit", (unsigned long)passwordCount);
1604
1605 // For the first record, delete all traces of it from CKKS. But! it remains in local keychain.
1606 // Expected outcome: CKKS resyncs; item exists again.
1607 CKRecord* delete = items[0];
1608 NSString* deleteAccount = [[self decryptRecord: delete] objectForKey: (__bridge id) kSecAttrAccount];
1609 XCTAssertNotNil(deleteAccount, "received an account for the local delete object");
1610
1611 __weak __typeof(self) weakSelf = self;
1612 [self.keychainView dispatchSync:^bool{
1613 __strong __typeof(weakSelf) strongSelf = weakSelf;
1614 XCTAssertNotNil(strongSelf, "self exists");
1615
1616 CKKSMirrorEntry* ckme = [CKKSMirrorEntry tryFromDatabase:delete.recordID.recordName zoneID:strongSelf.keychainZoneID error:&error];
1617 if(ckme) {
1618 [ckme deleteFromDatabase: &error];
1619 }
1620 XCTAssertNil(error, "no error removing CKME");
1621 CKKSOutgoingQueueEntry* oqe = [CKKSOutgoingQueueEntry tryFromDatabase:delete.recordID.recordName zoneID:strongSelf.keychainZoneID error:&error];
1622 if(oqe) {
1623 [oqe deleteFromDatabase: &error];
1624 }
1625 XCTAssertNil(error, "no error removing OQE");
1626 CKKSIncomingQueueEntry* iqe = [CKKSIncomingQueueEntry tryFromDatabase:delete.recordID.recordName zoneID:strongSelf.keychainZoneID error:&error];
1627 if(iqe) {
1628 [iqe deleteFromDatabase: &error];
1629 }
1630 XCTAssertNil(error, "no error removing IQE");
1631 return true;
1632 }];
1633
1634 // For the second record, delete all traces of it from CloudKit.
1635 // Expected outcome: deleted locally
1636 CKRecord* remoteDelete = items[1];
1637 NSString* remoteDeleteAccount = [[self decryptRecord: remoteDelete] objectForKey: (__bridge id) kSecAttrAccount];
1638 XCTAssertNotNil(remoteDeleteAccount, "received an account for the remote delete object");
1639
1640 [self.keychainZone deleteCKRecordIDFromZone: remoteDelete.recordID];
1641 for(NSMutableDictionary<CKRecordID*, CKRecord*>* database in self.keychainZone.pastDatabases.allValues) {
1642 [database removeObjectForKey: remoteDelete.recordID];
1643 }
1644
1645 // The third record gets modified in CloudKit, but not locally.
1646 // Expected outcome: use the CloudKit version
1647 CKRecord* remoteDataChanged = items[2];
1648 NSMutableDictionary* remoteDataDictionary = [[self decryptRecord: remoteDataChanged] mutableCopy];
1649 NSString* remoteDataChangedAccount = [remoteDataDictionary objectForKey: (__bridge id) kSecAttrAccount];
1650 XCTAssertNotNil(remoteDataChangedAccount, "Received an account for the remote-data-changed object");
1651 remoteDataDictionary[(__bridge id) kSecValueData] = [@"CloudKitWins" dataUsingEncoding: NSUTF8StringEncoding];
1652
1653 CKRecord* newData = [self newRecord: remoteDataChanged.recordID withNewItemData: remoteDataDictionary];
1654 [self.keychainZone addToZone: newData];
1655 for(NSMutableDictionary<CKRecordID*, CKRecord*>* database in self.keychainZone.pastDatabases.allValues) {
1656 database[remoteDataChanged.recordID] = newData;
1657 }
1658
1659 // The fourth record stays in-sync. Good work, everyone!
1660 // Expected outcome: stays in-sync
1661 NSString* insyncAccount = [[self decryptRecord: items[3]] objectForKey: (__bridge id) kSecAttrAccount];
1662 XCTAssertNotNil(insyncAccount, "Received an account for the in-sync object");
1663
1664 // The fifth record gets magically added to CloudKit, but CKKS has never heard of it
1665 // (emulates a lost record on the client, but that CloudKit already believes it's sent the record for)
1666 // Expected outcome: added to local keychain
1667 NSString* remoteOnlyAccount = @"remote-only";
1668 CKRecord* ckr = [self createFakeRecord:self.keychainZoneID recordName:@"7B598D31-F9C5-481E-98AC-5A507ACB2D85" withAccount: remoteOnlyAccount];
1669 [self.keychainZone addToZone: ckr];
1670 for(NSMutableDictionary<CKRecordID*, CKRecord*>* database in self.keychainZone.pastDatabases.allValues) {
1671 database[ckr.recordID] = ckr;
1672 }
1673
1674 ckksnotice("ckksresync", self.keychainView, "local delete: %@ %@", delete.recordID.recordName, deleteAccount);
1675 ckksnotice("ckksresync", self.keychainView, "Remote deletion: %@ %@", remoteDelete.recordID.recordName, remoteDeleteAccount);
1676 ckksnotice("ckksresync", self.keychainView, "Remote data changed: %@ %@", remoteDataChanged.recordID.recordName, remoteDataChangedAccount);
1677 ckksnotice("ckksresync", self.keychainView, "in-sync: %@ %@", items[3].recordID.recordName, insyncAccount);
1678 ckksnotice("ckksresync", self.keychainView, "Remote only: %@ %@", ckr.recordID.recordName, remoteOnlyAccount);
1679
1680 CKKSSynchronizeOperation* resyncOperation = [self.keychainView resyncWithCloud];
1681 [resyncOperation waitUntilFinished];
1682
1683 XCTAssertNil(resyncOperation.error, "No error during the resync operation");
1684
1685 // Now do some checking. Remember, we don't know which record we corrupted, so use the parsed account variables to check.
1686
1687 [self findGenericPassword: deleteAccount expecting: errSecSuccess];
1688 [self findGenericPassword: remoteDeleteAccount expecting: errSecItemNotFound];
1689 [self findGenericPassword: remoteDataChangedAccount expecting: errSecSuccess];
1690 [self findGenericPassword: insyncAccount expecting: errSecSuccess];
1691 [self findGenericPassword: remoteOnlyAccount expecting: errSecSuccess];
1692
1693 [self checkGenericPassword: @"data" account: deleteAccount];
1694 //[self checkGenericPassword: @"data" account: remoteDeleteAccount];
1695 [self checkGenericPassword: @"CloudKitWins" account: remoteDataChangedAccount];
1696 [self checkGenericPassword: @"data" account: insyncAccount];
1697 [self checkGenericPassword: @"data" account: remoteOnlyAccount];
1698
1699 [self.keychainView dispatchSync:^bool{
1700 __strong __typeof(weakSelf) strongSelf = weakSelf;
1701 XCTAssertNotNil(strongSelf, "self exists");
1702
1703 CKKSMirrorEntry* ckme = nil;
1704
1705 ckme = [CKKSMirrorEntry tryFromDatabase:delete.recordID.recordName zoneID:strongSelf.keychainZoneID error:&error];
1706 XCTAssertNil(error);
1707 XCTAssertNotNil(ckme);
1708
1709 ckme = [CKKSMirrorEntry tryFromDatabase:remoteDelete.recordID.recordName zoneID:strongSelf.keychainZoneID error:&error];
1710 XCTAssertNil(error);
1711 XCTAssertNil(ckme); // deleted!
1712
1713 ckme = [CKKSMirrorEntry tryFromDatabase:remoteDataChanged.recordID.recordName zoneID:strongSelf.keychainZoneID error:&error];
1714 XCTAssertNil(error);
1715 XCTAssertNotNil(ckme);
1716
1717 ckme = [CKKSMirrorEntry tryFromDatabase:items[3].recordID.recordName zoneID:strongSelf.keychainZoneID error:&error];
1718 XCTAssertNil(error);
1719 XCTAssertNotNil(ckme);
1720
1721 ckme = [CKKSMirrorEntry tryFromDatabase:ckr.recordID.recordName zoneID:strongSelf.keychainZoneID error:&error];
1722 XCTAssertNil(error);
1723 XCTAssertNotNil(ckme);
1724 return true;
1725 }];
1726 }
1727 - (void)testResyncLocal {
1728 [self putFakeKeyHierarchyInCloudKit:self.keychainZoneID];
1729 [self expectCKKSTLKSelfShareUpload:self.keychainZoneID];
1730 [self saveTLKMaterialToKeychain:self.keychainZoneID];
1731
1732 [self addGenericPassword: @"data" account: @"first"];
1733 [self addGenericPassword: @"data" account: @"second"];
1734 NSUInteger passwordCount = 2u;
1735
1736 [self expectCKModifyItemRecords: passwordCount currentKeyPointerRecords: 1 zoneID:self.keychainZoneID];
1737 [self startCKKSSubsystem];
1738
1739 // Wait for uploads to happen
1740 OCMVerifyAllWithDelay(self.mockDatabase, 8);
1741 [self waitForCKModifications];
1742
1743 // Local resyncs shouldn't fetch clouds.
1744 self.silentFetchesAllowed = false;
1745 SecCKKSDisable();
1746 [self deleteGenericPassword:@"first"];
1747 [self deleteGenericPassword:@"second"];
1748 SecCKKSEnable();
1749
1750 // And they're gone!
1751 [self findGenericPassword:@"first" expecting:errSecItemNotFound];
1752 [self findGenericPassword:@"second" expecting:errSecItemNotFound];
1753
1754 CKKSLocalSynchronizeOperation* op = [self.keychainView resyncLocal];
1755 [op waitUntilFinished];
1756 XCTAssertNil(op.error, "Shouldn't be an error resyncing locally");
1757
1758 // And they're back!
1759 [self checkGenericPassword: @"data" account: @"first"];
1760 [self checkGenericPassword: @"data" account: @"second"];
1761 }
1762
1763 - (void)testPlistRestoreResyncsLocal {
1764 [self putFakeKeyHierarchyInCloudKit:self.keychainZoneID];
1765 [self expectCKKSTLKSelfShareUpload:self.keychainZoneID];
1766 [self saveTLKMaterialToKeychain:self.keychainZoneID];
1767
1768 [self addGenericPassword: @"data" account: @"first"];
1769 [self addGenericPassword: @"data" account: @"second"];
1770 NSUInteger passwordCount = 2u;
1771
1772 [self checkGenericPassword: @"data" account: @"first"];
1773 [self checkGenericPassword: @"data" account: @"second"];
1774
1775 [self expectCKModifyItemRecords:passwordCount currentKeyPointerRecords:1 zoneID:self.keychainZoneID];
1776 [self startCKKSSubsystem];
1777
1778 // Wait for uploads to happen
1779 OCMVerifyAllWithDelay(self.mockDatabase, 8);
1780 [self waitForCKModifications];
1781
1782 // o no
1783 // This 'restores' a plist keychain backup
1784 // That will kick off a local resync in CKKS, so hold that until we're ready...
1785 self.keychainView.holdLocalSynchronizeOperation = [CKKSResultOperation named:@"hold-local-synchronize" withBlock:^{}];
1786
1787 // Local resyncs shouldn't fetch clouds.
1788 self.silentFetchesAllowed = false;
1789
1790 CFErrorRef cferror = NULL;
1791 kc_with_dbt(true, &cferror, ^bool (SecDbConnectionRef dbt) {
1792 CFErrorRef cfcferror = NULL;
1793
1794 bool ret = SecServerImportKeychainInPlist(dbt, SecSecurityClientGet(), KEYBAG_NONE, KEYBAG_NONE,
1795 (__bridge CFDictionaryRef)@{}, kSecBackupableItemFilter, &cfcferror);
1796
1797 XCTAssertNil(CFBridgingRelease(cfcferror), "Shouldn't error importing a 'backup'");
1798 XCTAssert(ret, "Importing a 'backup' should have succeeded");
1799 return true;
1800 });
1801 XCTAssertNil(CFBridgingRelease(cferror), "Shouldn't error mucking about in the db");
1802
1803 // And they're gone!
1804 [self findGenericPassword:@"first" expecting:errSecItemNotFound];
1805 [self findGenericPassword:@"second" expecting:errSecItemNotFound];
1806
1807 // Allow the local resync to continue...
1808 [self.operationQueue addOperation:self.keychainView.holdLocalSynchronizeOperation];
1809 [self.keychainView waitForOperationsOfClass:[CKKSLocalSynchronizeOperation class]];
1810
1811 // And they're back!
1812 [self checkGenericPassword: @"data" account: @"first"];
1813 [self checkGenericPassword: @"data" account: @"second"];
1814 }
1815
1816 - (void)testMultipleZoneAdd {
1817 // Test starts with nothing in database. We expect some sort of TLK/key hierarchy upload.
1818 [self expectCKModifyKeyRecords: 3 currentKeyPointerRecords: 3 tlkShareRecords: 1 zoneID:self.keychainZoneID];
1819
1820 // Bring up a new zone: we expect a key hierarchy upload.
1821 [self.injectedManager findOrCreateView:(id)kSecAttrViewHintAppleTV];
1822 CKRecordZoneID* appleTVZoneID = [[CKRecordZoneID alloc] initWithZoneName:(__bridge NSString*) kSecAttrViewHintAppleTV ownerName:CKCurrentUserDefaultName];
1823 [self expectCKModifyKeyRecords: 3 currentKeyPointerRecords: 3 tlkShareRecords: 1 zoneID:appleTVZoneID];
1824
1825 // We also expect the view manager's notifyNewTLKsInKeychain call to fire once (after some delay)
1826 OCMExpect([self.mockCKKSViewManager notifyNewTLKsInKeychain]);
1827
1828 // Let the horses loose
1829 [self startCKKSSubsystem];
1830
1831 // We expect a single record to be uploaded to the 'keychain' view
1832 [self expectCKModifyItemRecords: 1 currentKeyPointerRecords: 1 zoneID:self.keychainZoneID];
1833 [self addGenericPassword: @"data" account: @"account-delete-me"];
1834 OCMVerifyAllWithDelay(self.mockDatabase, 8);
1835
1836 // We expect a single record to be uploaded to the 'atv' view
1837 [self expectCKModifyItemRecords: 1 currentKeyPointerRecords: 1 zoneID:appleTVZoneID];
1838 [self addGenericPassword: @"atv"
1839 account: @"tvaccount"
1840 viewHint:(__bridge NSString*) kSecAttrViewHintAppleTV
1841 access:(id)kSecAttrAccessibleAfterFirstUnlock
1842 expecting:errSecSuccess message:@"AppleTV view-hinted object"];
1843
1844 OCMVerifyAllWithDelay(self.mockDatabase, 8);
1845
1846 OCMVerifyAllWithDelay(self.mockCKKSViewManager, 10);
1847 }
1848
1849 - (void)testMultipleZoneDelete {
1850 // Test starts with nothing in database. We expect some sort of TLK/key hierarchy upload.
1851 [self expectCKModifyKeyRecords: 3 currentKeyPointerRecords: 3 tlkShareRecords: 1 zoneID:self.keychainZoneID];
1852
1853 [self startCKKSSubsystem];
1854
1855 // We expect a single record to be uploaded.
1856 [self expectCKModifyItemRecords: 1 currentKeyPointerRecords: 1 zoneID:self.keychainZoneID];
1857 [self addGenericPassword: @"data" account: @"account-delete-me"];
1858 OCMVerifyAllWithDelay(self.mockDatabase, 8);
1859
1860 // Bring up a new zone: we expect a key hierarchy and an item.
1861 [self.injectedManager findOrCreateView:(id)kSecAttrViewHintAppleTV];
1862 CKRecordZoneID* appleTVZoneID = [[CKRecordZoneID alloc] initWithZoneName:(__bridge NSString*) kSecAttrViewHintAppleTV ownerName:CKCurrentUserDefaultName];
1863 [self expectCKModifyKeyRecords: 3 currentKeyPointerRecords: 3 tlkShareRecords: 1 zoneID:appleTVZoneID];
1864 [self expectCKModifyItemRecords: 1 currentKeyPointerRecords: 1 zoneID:appleTVZoneID];
1865
1866 [self addGenericPassword: @"atv"
1867 account: @"tvaccount"
1868 viewHint:(__bridge NSString*) kSecAttrViewHintAppleTV
1869 access:(id)kSecAttrAccessibleAfterFirstUnlock
1870 expecting:errSecSuccess
1871 message:@"AppleTV view-hinted object"];
1872 OCMVerifyAllWithDelay(self.mockDatabase, 8);
1873
1874 // We expect a single record to be deleted from the ATV zone
1875 [self expectCKDeleteItemRecords: 1 zoneID:appleTVZoneID];
1876 [self deleteGenericPassword:@"tvaccount"];
1877 OCMVerifyAllWithDelay(self.mockDatabase, 8);
1878
1879 // Now we expect a single record to be deleted from the test zone
1880 [self expectCKDeleteItemRecords: 1 zoneID:self.keychainZoneID];
1881 [self deleteGenericPassword:@"account-delete-me"];
1882 OCMVerifyAllWithDelay(self.mockDatabase, 8);
1883 }
1884
1885 - (void)testRestartWithoutRefetch {
1886 // Restarting the CKKS operation should check that it's been 15 minutes since the last fetch before it fetches again. Simulate this.
1887
1888 // Test starts with nothing in database. We expect some sort of TLK/key hierarchy upload.
1889 [self expectCKModifyKeyRecords: 3 currentKeyPointerRecords: 3 tlkShareRecords: 1 zoneID:self.keychainZoneID];
1890 [self startCKKSSubsystem];
1891
1892 [self.keychainView waitForKeyHierarchyReadiness];
1893 [self waitForCKModifications];
1894 OCMVerifyAllWithDelay(self.mockDatabase, 8);
1895
1896 // Tear down the CKKS object and disallow fetches
1897 [self.keychainView halt];
1898 self.silentFetchesAllowed = false;
1899
1900 self.keychainView = [[CKKSViewManager manager] restartZone: self.keychainZoneID.zoneName];
1901 [self.keychainView waitForKeyHierarchyReadiness];
1902 OCMVerifyAllWithDelay(self.mockDatabase, 8);
1903
1904 // Okay, cool, rad, now let's set the date to be very long ago and check that there's positively a fetch
1905 [self.keychainView halt];
1906 self.silentFetchesAllowed = false;
1907
1908 [self.keychainView dispatchSync: ^bool {
1909 NSError* error = nil;
1910 CKKSZoneStateEntry* ckse = [CKKSZoneStateEntry fromDatabase:self.keychainZoneID.zoneName error:&error];
1911
1912 XCTAssertNil(error, "no error pulling ckse from database");
1913 XCTAssertNotNil(ckse, "received a ckse");
1914
1915 ckse.lastFetchTime = [NSDate distantPast];
1916 [ckse saveToDatabase: &error];
1917 XCTAssertNil(error, "no error saving to database");
1918 return true;
1919 }];
1920
1921 [self expectCKFetch];
1922 self.keychainView = [[CKKSViewManager manager] restartZone: self.keychainZoneID.zoneName];
1923 [self.keychainView waitForKeyHierarchyReadiness];
1924 OCMVerifyAllWithDelay(self.mockDatabase, 8);
1925 }
1926
1927 - (void)testRecoverFromZoneCreationFailure {
1928 // Fail the zone creation.
1929 self.zones[self.keychainZoneID] = nil; // delete the autocreated zone
1930 [self failNextZoneCreation:self.keychainZoneID];
1931
1932 // Spin up CKKS subsystem.
1933 [self startCKKSSubsystem];
1934
1935 // The CKKS subsystem should figure out the issue, and fix it.
1936 [self expectCKModifyKeyRecords: 3 currentKeyPointerRecords: 3 tlkShareRecords: 1 zoneID:self.keychainZoneID];
1937
1938 [self.keychainView waitForKeyHierarchyReadiness];
1939 OCMVerifyAllWithDelay(self.mockDatabase, 8);
1940
1941 [self expectCKModifyItemRecords: 1 currentKeyPointerRecords: 1 zoneID:self.keychainZoneID
1942 checkItem: [self checkClassCBlock:self.keychainZoneID message:@"Object was encrypted under class C key in hierarchy"]];
1943 [self addGenericPassword: @"data" account: @"account-delete-me"];
1944 OCMVerifyAllWithDelay(self.mockDatabase, 8);
1945
1946 XCTAssertNil(self.zones[self.keychainZoneID].creationError, "Creation error was unset (and so CKKS probably dealt with the error");
1947 }
1948
1949 - (void)testRecoverFromZoneSubscriptionFailure {
1950 // Fail the zone subscription.
1951 [self failNextZoneSubscription:self.keychainZoneID];
1952
1953 // Spin up CKKS subsystem.
1954 [self startCKKSSubsystem];
1955
1956 // The CKKS subsystem should figure out the issue, and fix it.
1957 [self expectCKModifyKeyRecords: 3 currentKeyPointerRecords: 3 tlkShareRecords: 1 zoneID:self.keychainZoneID];
1958
1959 [self.keychainView waitForKeyHierarchyReadiness];
1960 OCMVerifyAllWithDelay(self.mockDatabase, 8);
1961
1962 [self expectCKModifyItemRecords: 1 currentKeyPointerRecords: 1 zoneID:self.keychainZoneID
1963 checkItem: [self checkClassCBlock:self.keychainZoneID message:@"Object was encrypted under class C key in hierarchy"]];
1964 [self addGenericPassword: @"data" account: @"account-delete-me"];
1965 OCMVerifyAllWithDelay(self.mockDatabase, 8);
1966
1967 XCTAssertNil(self.zones[self.keychainZoneID].subscriptionError, "Subscription error was unset (and so CKKS probably dealt with the error");
1968 }
1969
1970 - (void)testRecoverFromZoneSubscriptionFailureDueToZoneNotExisting {
1971 // This is different from testRecoverFromZoneSubscriptionFailure, since the zone is gone. CKKS must attempt to re-create the zone.
1972
1973 // Silently fail the zone creation
1974 self.zones[self.keychainZoneID] = nil; // delete the autocreated zone
1975 [self failNextZoneCreationSilently:self.keychainZoneID];
1976
1977 // Spin up CKKS subsystem.
1978 [self startCKKSSubsystem];
1979
1980 // The CKKS subsystem should figure out the issue, and fix it.
1981 [self expectCKModifyKeyRecords: 3 currentKeyPointerRecords: 3 tlkShareRecords: 1 zoneID:self.keychainZoneID];
1982
1983 [self.keychainView waitForKeyHierarchyReadiness];
1984 OCMVerifyAllWithDelay(self.mockDatabase, 8);
1985
1986 [self expectCKModifyItemRecords: 1 currentKeyPointerRecords: 1 zoneID:self.keychainZoneID
1987 checkItem: [self checkClassCBlock:self.keychainZoneID message:@"Object was encrypted under class C key in hierarchy"]];
1988 [self addGenericPassword: @"data" account: @"account-delete-me"];
1989 OCMVerifyAllWithDelay(self.mockDatabase, 8);
1990
1991 XCTAssertFalse(self.zones[self.keychainZoneID].flag, "Zone flag was reset");
1992 XCTAssertNil(self.zones[self.keychainZoneID].subscriptionError, "Subscription error was unset (and so CKKS probably dealt with the error");
1993 }
1994
1995 - (void)testRecoverFromDeletedTLKWithStashedTLK {
1996 // We need to handle the case where our syncable TLKs are deleted for some reason. The device that has them might resurrect them
1997
1998 [self createAndSaveFakeKeyHierarchy: self.keychainZoneID]; // Make life easy for this test.
1999 NSError* error = nil;
2000
2001 // Stash the TLKs.
2002 [self.keychainZoneKeys.tlk saveKeyMaterialToKeychain:true error:&error];
2003 XCTAssertNil(error, "Should have received no error stashing the new TLK in the keychain");
2004
2005 // And delete the non-stashed version
2006 [self.keychainZoneKeys.tlk deleteKeyMaterialFromKeychain:&error];
2007 XCTAssertNil(error, "Should have received no error deleting the new TLK from the keychain");
2008
2009 // Spin up CKKS subsystem.
2010 [self startCKKSSubsystem];
2011
2012 [self.keychainView waitForKeyHierarchyReadiness];
2013 OCMVerifyAllWithDelay(self.mockDatabase, 8);
2014
2015 [self expectCKModifyItemRecords: 1 currentKeyPointerRecords: 1 zoneID:self.keychainZoneID checkItem: [self checkClassCBlock:self.keychainZoneID message:@"Object was encrypted under class C key in hierarchy"]];
2016 [self addGenericPassword: @"data" account: @"account-delete-me"];
2017 OCMVerifyAllWithDelay(self.mockDatabase, 8);
2018
2019 // CKKS should recreate the syncable TLK.
2020 [self checkNSyncableTLKsInKeychain: 1];
2021 }
2022
2023 - (void)testRecoverFromDeletedTLKWithStashedTLKUponRestart {
2024 // We need to handle the case where our syncable TLKs are deleted for some reason. The device that has them might resurrect them
2025
2026 [self createAndSaveFakeKeyHierarchy: self.keychainZoneID]; // Make life easy for this test.
2027 // Spin up CKKS subsystem.
2028 [self startCKKSSubsystem];
2029 [self.keychainView waitForKeyHierarchyReadiness];
2030
2031 // Tear down the CKKS object
2032 [self.keychainView halt];
2033
2034 NSError* error = nil;
2035
2036 // Stash the TLKs.
2037 [self.keychainZoneKeys.tlk saveKeyMaterialToKeychain:true error:&error];
2038 XCTAssertNil(error, "Should have received no error stashing the new TLK in the keychain");
2039
2040 [self.keychainZoneKeys.tlk deleteKeyMaterialFromKeychain:&error];
2041 XCTAssertNil(error, "Should have received no error deleting the new TLK from the keychain");
2042
2043 self.keychainView = [[CKKSViewManager manager] restartZone: self.keychainZoneID.zoneName];
2044 [self.keychainView waitForKeyHierarchyReadiness];
2045 OCMVerifyAllWithDelay(self.mockDatabase, 8);
2046
2047 [self expectCKModifyItemRecords: 1 currentKeyPointerRecords: 1 zoneID:self.keychainZoneID checkItem: [self checkClassCBlock:self.keychainZoneID message:@"Object was encrypted under class C key in hierarchy"]];
2048 [self addGenericPassword: @"data" account: @"account-delete-me"];
2049 OCMVerifyAllWithDelay(self.mockDatabase, 8);
2050
2051 // CKKS should recreate the syncable TLK.
2052 [self checkNSyncableTLKsInKeychain: 1];
2053 }
2054
2055 - (void)testRecoverFromTLKWriteFailure {
2056 // We need to handle the case where a device's first TLK write doesn't go through (due to whatever reason).
2057 // Test starts with nothing in CloudKit, and will fail the first TLK write.
2058 NSError* noNetwork = [[CKPrettyError alloc] initWithDomain:CKErrorDomain code:CKErrorNetworkUnavailable userInfo:@{}];
2059 [self failNextCKAtomicModifyItemRecordsUpdateFailure:self.keychainZoneID blockAfterReject:nil withError:noNetwork];
2060
2061 // Spin up CKKS subsystem.
2062 [self startCKKSSubsystem];
2063
2064 // The CKKS subsystem should figure out the issue, and fix it.
2065 [self expectCKModifyKeyRecords: 3 currentKeyPointerRecords: 3 tlkShareRecords: 1 zoneID:self.keychainZoneID];
2066
2067 [self.keychainView waitForKeyHierarchyReadiness];
2068 OCMVerifyAllWithDelay(self.mockDatabase, 8);
2069
2070 [self expectCKModifyItemRecords: 1 currentKeyPointerRecords: 1 zoneID:self.keychainZoneID checkItem: [self checkClassCBlock:self.keychainZoneID message:@"Object was encrypted under class C key in hierarchy"]];
2071 [self addGenericPassword: @"data" account: @"account-delete-me"];
2072 OCMVerifyAllWithDelay(self.mockDatabase, 8);
2073
2074 // A network failure creating new TLKs shouldn't delete the 'failed' syncable one.
2075 [self checkNSyncableTLKsInKeychain: 2];
2076 }
2077
2078 - (void)testRecoverFromTLKRace {
2079 // We need to handle the case where a device's first TLK write doesn't go through (due to whatever reason).
2080 // Test starts with nothing in CloudKit, and will fail the first TLK write.
2081 [self failNextCKAtomicModifyItemRecordsUpdateFailure:self.keychainZoneID blockAfterReject: ^{
2082 [self putFakeKeyHierarchyInCloudKit:self.keychainZoneID];
2083 }];
2084
2085 // Spin up CKKS subsystem.
2086 [self startCKKSSubsystem];
2087
2088 // The first TLK write should fail, and then our fake TLKs should be there in CloudKit.
2089 // It shouldn't write anything back up to CloudKit.
2090 OCMVerifyAllWithDelay(self.mockDatabase, 8);
2091
2092 // Now the TLKs arrive from the other device...
2093 [self expectCKKSTLKSelfShareUpload:self.keychainZoneID];
2094 [self saveTLKMaterialToKeychainSimulatingSOS:self.keychainZoneID];
2095 [self.keychainView waitForKeyHierarchyReadiness];
2096
2097 [self expectCKModifyItemRecords: 1 currentKeyPointerRecords: 1 zoneID:self.keychainZoneID checkItem: [self checkClassCBlock:self.keychainZoneID message:@"Object was encrypted under class C key in hierarchy"]];
2098 [self addGenericPassword: @"data" account: @"account-delete-me"];
2099 OCMVerifyAllWithDelay(self.mockDatabase, 8);
2100
2101 // A race failure creating new TLKs should delete the old syncable one.
2102 [self checkNSyncableTLKsInKeychain: 1];
2103 }
2104
2105 - (void)testRecoverFromNullCurrentKeyPointers {
2106 // The current key pointers in cloudkit shouldn't ever not exist if keys do. But, if they don't, CKKS must recover.
2107
2108 // Test starts with a broken key hierarchy in our fake CloudKit, but the TLK already arrived.
2109 [self putFakeKeyHierarchyInCloudKit:self.keychainZoneID];
2110 [self saveTLKMaterialToKeychain:self.keychainZoneID];
2111
2112 ZoneKeys* zonekeys = self.keys[self.keychainZoneID];
2113 FakeCKZone* ckzone = self.zones[self.keychainZoneID];
2114 ckzone.currentDatabase[zonekeys.currentTLKPointer.storedCKRecord.recordID][SecCKRecordParentKeyRefKey] = nil;
2115 ckzone.currentDatabase[zonekeys.currentClassAPointer.storedCKRecord.recordID][SecCKRecordParentKeyRefKey] = nil;
2116 ckzone.currentDatabase[zonekeys.currentClassCPointer.storedCKRecord.recordID][SecCKRecordParentKeyRefKey] = nil;
2117
2118 // Spin up CKKS subsystem.
2119 [self startCKKSSubsystem];
2120
2121 // The CKKS subsystem should figure out the issue, and fix it.
2122 [self expectCKModifyKeyRecords:0 currentKeyPointerRecords:3 tlkShareRecords:1 zoneID:self.keychainZoneID];
2123
2124 [self.keychainView waitForKeyHierarchyReadiness];
2125
2126 OCMVerifyAllWithDelay(self.mockDatabase, 8);
2127 }
2128
2129 - (void)testRecoverFromNoCurrentKeyPointers {
2130 // The current key pointers in cloudkit shouldn't ever point to nil. But, if they do, CKKS must recover.
2131
2132 // Test starts with a broken key hierarchy in our fake CloudKit, but the TLK already arrived.
2133 [self putFakeKeyHierarchyInCloudKit:self.keychainZoneID];
2134 [self saveTLKMaterialToKeychain:self.keychainZoneID];
2135
2136 ZoneKeys* zonekeys = self.keys[self.keychainZoneID];
2137 XCTAssertNil([self.zones[self.keychainZoneID] deleteCKRecordIDFromZone: zonekeys.currentTLKPointer.storedCKRecord.recordID], "Deleted TLK pointer from zone");
2138 XCTAssertNil([self.zones[self.keychainZoneID] deleteCKRecordIDFromZone: zonekeys.currentClassAPointer.storedCKRecord.recordID], "Deleted class a pointer from zone");
2139 XCTAssertNil([self.zones[self.keychainZoneID] deleteCKRecordIDFromZone: zonekeys.currentClassCPointer.storedCKRecord.recordID], "Deleted class c pointer from zone");
2140
2141 // Spin up CKKS subsystem.
2142 [self startCKKSSubsystem];
2143
2144 // The CKKS subsystem should figure out the issue, and fix it.
2145 [self expectCKModifyKeyRecords:0 currentKeyPointerRecords:3 tlkShareRecords:1 zoneID:self.keychainZoneID];
2146
2147 XCTAssertEqual(0, [self.keychainView.keyHierarchyConditions[SecCKKSZoneKeyStateReady] wait:8*NSEC_PER_SEC], "Key state should have become ready");
2148
2149 OCMVerifyAllWithDelay(self.mockDatabase, 8);
2150 }
2151
2152 - (void)testRecoverFromBadChangeTag {
2153 // 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.
2154
2155 // Test starts with a broken key hierarchy in our fake CloudKit, but a (incorrectly) up-to-date change tag stored locally.
2156 [self putFakeKeyHierarchyInCloudKit:self.keychainZoneID];
2157 SecCKKSTestSetDisableKeyNotifications(true); // Don't tell CKKS about this key material; we're pretending like this is a securityd restart
2158 [self saveTLKMaterialToKeychain:self.keychainZoneID];
2159 SecCKKSTestSetDisableKeyNotifications(false);
2160
2161 [self.keychainView dispatchSync: ^bool {
2162 NSError* error = nil;
2163 CKKSZoneStateEntry* ckse = [CKKSZoneStateEntry state:self.keychainZoneID.zoneName];
2164 XCTAssertNotNil(ckse, "should have received a ckse");
2165
2166 ckse.ckzonecreated = true;
2167 ckse.ckzonesubscribed = true;
2168 ckse.changeToken = self.keychainZone.currentChangeToken;
2169
2170 [ckse saveToDatabase: &error];
2171 XCTAssertNil(error, "shouldn't have gotten an error saving to database");
2172 return true;
2173 }];
2174
2175 // The CKKS subsystem should try to write TLKs, but fail. It'll then upload a TLK share for the keys already in CloudKit
2176 [self expectCKAtomicModifyItemRecordsUpdateFailure:self.keychainZoneID];
2177 [self expectCKKSTLKSelfShareUpload:self.keychainZoneID];
2178
2179 // Spin up CKKS subsystem.
2180 [self startCKKSSubsystem];
2181 OCMVerifyAllWithDelay(self.mockDatabase, 16);
2182
2183 // CKKS should then happily use the keys in CloudKit
2184 [self createClassCItemAndWaitForUpload:self.keychainZoneID account:@"account-delete-me"];
2185 [self createClassAItemAndWaitForUpload:self.keychainZoneID account:@"account-delete-me-class-a"];
2186 }
2187
2188 - (void)testRecoverFromDeletedKeysNewItem {
2189 // Test starts with nothing in database. We expect some sort of TLK/key hierarchy upload.
2190 [self expectCKModifyKeyRecords: 3 currentKeyPointerRecords: 3 tlkShareRecords: 1 zoneID:self.keychainZoneID];
2191
2192 [self startCKKSSubsystem];
2193 [self.keychainView waitForKeyHierarchyReadiness];
2194
2195 // We expect a single class C record to be uploaded.
2196 [self expectCKModifyItemRecords: 1 currentKeyPointerRecords: 1 zoneID:self.keychainZoneID
2197 checkItem: [self checkClassCBlock:self.keychainZoneID message:@"Object was encrypted under class C key in hierarchy"]];
2198
2199 [self addGenericPassword: @"data" account: @"account-delete-me"];
2200 OCMVerifyAllWithDelay(self.mockDatabase, 8);
2201
2202 [self waitForCKModifications];
2203 [self.keychainView waitUntilAllOperationsAreFinished];
2204
2205 // Now, delete the local keys from the keychain (but leave the synced TLK)
2206 SecCKKSTestSetDisableKeyNotifications(true);
2207 XCTAssertEqual(errSecSuccess, SecItemDelete((__bridge CFDictionaryRef)@{
2208 (id)kSecClass : (id)kSecClassInternetPassword,
2209 (id)kSecAttrNoLegacy : @YES,
2210 (id)kSecAttrAccessGroup : @"com.apple.security.ckks",
2211 (id)kSecAttrSynchronizable : (id)kCFBooleanFalse,
2212 }), @"Deleting local keys");
2213 SecCKKSTestSetDisableKeyNotifications(false);
2214
2215 NSError* error = nil;
2216 [self.keychainZoneKeys.classC loadKeyMaterialFromKeychain:&error];
2217 XCTAssertNotNil(error, "Error loading class C key material from keychain");
2218
2219 // We expect a single class C record to be uploaded.
2220 [self expectCKModifyItemRecords: 1 currentKeyPointerRecords: 1 zoneID:self.keychainZoneID
2221 checkItem: [self checkClassCBlock:self.keychainZoneID message:@"Object was encrypted under class C key in hierarchy"]];
2222
2223 [self addGenericPassword: @"datadata" account: @"account-no-keys"];
2224 OCMVerifyAllWithDelay(self.mockDatabase, 8);
2225
2226 // We expect a single class A record to be uploaded.
2227 [self expectCKModifyItemRecords: 1 currentKeyPointerRecords: 1 zoneID:self.keychainZoneID
2228 checkItem: [self checkClassABlock:self.keychainZoneID message:@"Object was encrypted under class A key in hierarchy"]];
2229 [self addGenericPassword:@"asdf"
2230 account:@"account-class-A"
2231 viewHint:nil
2232 access:(id)kSecAttrAccessibleWhenUnlocked
2233 expecting:errSecSuccess
2234 message:@"Adding class A item"];
2235 OCMVerifyAllWithDelay(self.mockDatabase, 8);
2236 }
2237
2238 - (void)testRecoverFromDeletedKeysReceive {
2239 // Test starts with nothing in database. We expect some sort of TLK/key hierarchy upload.
2240 [self expectCKModifyKeyRecords: 3 currentKeyPointerRecords: 3 tlkShareRecords: 1 zoneID:self.keychainZoneID];
2241
2242 [self startCKKSSubsystem];
2243 [self.keychainView waitForKeyHierarchyReadiness];
2244
2245 [self waitForCKModifications];
2246 [self.keychainView waitUntilAllOperationsAreFinished];
2247
2248 [self.keychainView waitForFetchAndIncomingQueueProcessing];
2249
2250 CKRecord* ckr = [self createFakeRecord: self.keychainZoneID recordName:@"7B598D31-F9C5-481E-98AC-5A507ACB2D85" withAccount:@"account0"];
2251
2252 // Now, delete the local keys from the keychain (but leave the synced TLK)
2253 SecCKKSTestSetDisableKeyNotifications(true);
2254 XCTAssertEqual(errSecSuccess, SecItemDelete((__bridge CFDictionaryRef)@{
2255 (id)kSecClass : (id)kSecClassInternetPassword,
2256 (id)kSecAttrNoLegacy : @YES,
2257 (id)kSecAttrAccessGroup : @"com.apple.security.ckks",
2258 (id)kSecAttrSynchronizable : (id)kCFBooleanFalse,
2259 }), @"Deleting local keys");
2260 SecCKKSTestSetDisableKeyNotifications(false);
2261
2262 // Trigger a notification (with hilariously fake data)
2263 [self.keychainZone addToZone: ckr];
2264 [self.keychainView notifyZoneChange:nil];
2265 [self.keychainView waitForFetchAndIncomingQueueProcessing];
2266 [self.keychainView waitForFetchAndIncomingQueueProcessing];
2267
2268 [self findGenericPassword: @"account0" expecting:errSecSuccess];
2269 }
2270
2271 - (void)disabledtestRecoverDeletedTLKAndPause {
2272 // If the TLK disappears halfway through, well, that's no good. But we should make it into waitfortlk.
2273
2274 // Test starts with nothing in database. We expect some sort of TLK/key hierarchy upload.
2275 [self expectCKModifyKeyRecords: 3 currentKeyPointerRecords: 3 tlkShareRecords: 1 zoneID:self.keychainZoneID];
2276
2277 [self startCKKSSubsystem];
2278 [self.keychainView waitForKeyHierarchyReadiness];
2279
2280 [self.keychainView waitForFetchAndIncomingQueueProcessing];
2281
2282 CKRecord* ckr = [self createFakeRecord: self.keychainZoneID recordName:@"7B598D31-F9C5-481E-98AC-5A507ACB2D85" withAccount:@"account0"];
2283 [self.keychainView waitUntilAllOperationsAreFinished];
2284
2285 // Now, delete the local keys from the keychain
2286 SecCKKSTestSetDisableKeyNotifications(true);
2287 XCTAssertEqual(errSecSuccess, SecItemDelete((__bridge CFDictionaryRef)@{
2288 (id)kSecClass : (id)kSecClassInternetPassword,
2289 (id)kSecAttrNoLegacy : @YES,
2290 (id)kSecAttrAccessGroup : @"com.apple.security.ckks",
2291 (id)kSecAttrSynchronizable : (id)kCFBooleanFalse,
2292 }), @"Deleting local keys");
2293 XCTAssertEqual(errSecSuccess, SecItemDelete((__bridge CFDictionaryRef)@{
2294 (id)kSecClass : (id)kSecClassInternetPassword,
2295 (id)kSecAttrNoLegacy : @YES,
2296 (id)kSecAttrAccessGroup : @"com.apple.security.ckks",
2297 (id)kSecAttrSynchronizable : (id)kCFBooleanTrue,
2298 }), @"Deleting TLK");
2299 SecCKKSTestSetDisableKeyNotifications(false);
2300
2301 // Trigger a notification (with hilariously fake data)
2302 [self.keychainZone addToZone: ckr];
2303 [self.keychainView notifyZoneChange:nil];
2304
2305 [self.keychainView waitForFetchAndIncomingQueueProcessing];
2306 [self.keychainView waitForOperationsOfClass:[CKKSHealKeyHierarchyOperation class]];
2307
2308 XCTAssertEqualObjects(self.keychainView.keyHierarchyState, SecCKKSZoneKeyStateWaitForTLK, "CKKS re-entered waitfortlk");
2309 }
2310
2311 - (void)testRecoverFromBadCurrentKeyPointer {
2312 // The current key pointers in cloudkit shouldn't ever point to missing entries. But, if they do, CKKS must recover.
2313
2314 // Test starts with a broken key hierarchy in our fake CloudKit, but the TLK already arrived.
2315 [self putFakeKeyHierarchyInCloudKit:self.keychainZoneID];
2316 [self saveTLKMaterialToKeychain:self.keychainZoneID];
2317
2318 ZoneKeys* zonekeys = self.keys[self.keychainZoneID];
2319 FakeCKZone* ckzone = self.zones[self.keychainZoneID];
2320 ckzone.currentDatabase[zonekeys.currentTLKPointer.storedCKRecord.recordID][SecCKRecordParentKeyRefKey] = [[CKReference alloc] initWithRecordID: [[CKRecordID alloc] initWithRecordName: @"not a real tlk" zoneID: self.keychainZoneID] action: CKReferenceActionNone];
2321 ckzone.currentDatabase[zonekeys.currentClassAPointer.storedCKRecord.recordID][SecCKRecordParentKeyRefKey] = [[CKReference alloc] initWithRecordID: [[CKRecordID alloc] initWithRecordName: @"not a real class a key" zoneID: self.keychainZoneID] action: CKReferenceActionNone];
2322 ckzone.currentDatabase[zonekeys.currentClassCPointer.storedCKRecord.recordID][SecCKRecordParentKeyRefKey] = [[CKReference alloc] initWithRecordID: [[CKRecordID alloc] initWithRecordName: @"not a real class c key" zoneID: self.keychainZoneID] action: CKReferenceActionNone];
2323
2324 // Spin up CKKS subsystem.
2325 [self startCKKSSubsystem];
2326
2327 // The CKKS subsystem should figure out the issue, and fix it (while uploading itself a TLK Share)
2328 [self expectCKModifyKeyRecords: 0 currentKeyPointerRecords: 3 tlkShareRecords: 1 zoneID:self.keychainZoneID];
2329
2330 XCTAssertEqual(0, [self.keychainView.keyHierarchyConditions[SecCKKSZoneKeyStateReady] wait:8*NSEC_PER_SEC], "Key state should have become ready");
2331
2332 OCMVerifyAllWithDelay(self.mockDatabase, 8);
2333 }
2334
2335
2336 - (void)testRecoverFromCloudKitFetchFail {
2337 // Test starts with nothing in database, but one in our fake CloudKit.
2338 [self putFakeKeyHierarchyInCloudKit:self.keychainZoneID];
2339
2340 // The first two CKRecordZoneChanges should fail with a 'network unavailable' error.
2341 [self.keychainZone failNextFetchWith:[[NSError alloc] initWithDomain:CKErrorDomain code:CKErrorNetworkUnavailable userInfo:@{}]];
2342 [self.keychainZone failNextFetchWith:[[NSError alloc] initWithDomain:CKErrorDomain code:CKErrorNetworkUnavailable userInfo:@{}]];
2343
2344 // Spin up CKKS subsystem.
2345 [self startCKKSSubsystem];
2346
2347 // Now, save the TLK to the keychain (to simulate it coming in later via SOS).
2348 [self expectCKKSTLKSelfShareUpload:self.keychainZoneID];
2349 [self saveTLKMaterialToKeychainSimulatingSOS:self.keychainZoneID];
2350
2351 // We expect a single record to be uploaded
2352 [self expectCKModifyItemRecords: 1 currentKeyPointerRecords: 1 zoneID:self.keychainZoneID checkItem: [self checkClassCBlock:self.keychainZoneID message:@"Object was encrypted under class C key in hierarchy"]];
2353 [self addGenericPassword: @"data" account: @"account-delete-me"];
2354 OCMVerifyAllWithDelay(self.mockDatabase, 12);
2355
2356 [self expectCKModifyItemRecords: 1 currentKeyPointerRecords: 1 zoneID:self.keychainZoneID checkItem: [self checkClassABlock:self.keychainZoneID message:@"Object was encrypted under class A key in hierarchy"]];
2357 [self addGenericPassword:@"asdf"
2358 account:@"account-class-A"
2359 viewHint:nil
2360 access:(id)kSecAttrAccessibleWhenUnlocked
2361 expecting:errSecSuccess
2362 message:@"Adding class A item"];
2363 OCMVerifyAllWithDelay(self.mockDatabase, 8);
2364 }
2365
2366 - (void)testRecoverFromCloudKitFetchFailWithDelay {
2367 // Test starts with nothing in database, but one in our fake CloudKit.
2368 [self putFakeKeyHierarchyInCloudKit:self.keychainZoneID];
2369
2370 // The first CKRecordZoneChanges should fail with a 'delay' error.
2371 self.silentFetchesAllowed = false;
2372 [self.keychainZone failNextFetchWith:[[CKPrettyError alloc] initWithDomain:CKErrorDomain code:CKErrorRequestRateLimited userInfo:@{CKErrorRetryAfterKey : [NSNumber numberWithInt:4]}]];
2373 [self expectCKFetch];
2374
2375 // Spin up CKKS subsystem.
2376 [self startCKKSSubsystem];
2377
2378 // Ensure it doesn't fetch within these three seconds (if it does, an exception will throw).
2379 sleep(3);
2380
2381 // Okay, you can fetch again.
2382 [self expectCKFetch];
2383
2384 // Now, save the TLK to the keychain (to simulate it coming in later via SOS).
2385 [self expectCKKSTLKSelfShareUpload:self.keychainZoneID];
2386 [self saveTLKMaterialToKeychainSimulatingSOS:self.keychainZoneID];
2387
2388 // We expect a single record to be uploaded
2389 [self expectCKModifyItemRecords: 1 currentKeyPointerRecords: 1 zoneID:self.keychainZoneID checkItem: [self checkClassCBlock:self.keychainZoneID message:@"Object was encrypted under class C key in hierarchy"]];
2390 [self addGenericPassword: @"data" account: @"account-delete-me"];
2391 OCMVerifyAllWithDelay(self.mockDatabase, 8);
2392 }
2393
2394 - (void)testRecoverFromCloudKitOldChangeToken {
2395 // Test starts with nothing in database, but one in our fake CloudKit.
2396 [self putFakeKeyHierarchyInCloudKit:self.keychainZoneID];
2397
2398 // Spin up CKKS subsystem.
2399 [self startCKKSSubsystem];
2400
2401 // Now, save the TLK to the keychain (to simulate it coming in later via SOS).
2402 [self expectCKKSTLKSelfShareUpload:self.keychainZoneID];
2403 [self saveTLKMaterialToKeychainSimulatingSOS:self.keychainZoneID];
2404
2405 // We expect a single record to be uploaded
2406 [self expectCKModifyItemRecords: 1 currentKeyPointerRecords: 1 zoneID:self.keychainZoneID checkItem: [self checkClassCBlock:self.keychainZoneID message:@"Object was encrypted under class C key in hierarchy"]];
2407 [self addGenericPassword: @"data" account: @"account-delete-me"];
2408 OCMVerifyAllWithDelay(self.mockDatabase, 8);
2409
2410 // Delete all old database states, to destroy the change tag validity
2411 [self.keychainZone.pastDatabases removeAllObjects];
2412
2413 // We expect a total local flush and refetch
2414 self.silentFetchesAllowed = false;
2415 [self expectCKFetch]; // one to fail with a CKErrorChangeTokenExpired error
2416 [self expectCKFetch]; // and one to succeed
2417
2418 // Trigger a fake change notification
2419 [self.keychainView notifyZoneChange:nil];
2420
2421 OCMVerifyAllWithDelay(self.mockDatabase, 8);
2422
2423 // And check that a new upload happens just fine.
2424 [self expectCKModifyItemRecords: 1 currentKeyPointerRecords: 1 zoneID:self.keychainZoneID checkItem: [self checkClassABlock:self.keychainZoneID message:@"Object was encrypted under class A key in hierarchy"]];
2425 [self addGenericPassword:@"asdf"
2426 account:@"account-class-A"
2427 viewHint:nil
2428 access:(id)kSecAttrAccessibleWhenUnlocked
2429 expecting:errSecSuccess
2430 message:@"Adding class A item"];
2431 OCMVerifyAllWithDelay(self.mockDatabase, 8);
2432 }
2433
2434 - (void)testRecoverFromCloudKitUnknownDeviceStateRecord {
2435 // Test starts with nothing in database, but one in our fake CloudKit.
2436 [self putFakeKeyHierarchyInCloudKit:self.keychainZoneID];
2437 [self expectCKKSTLKSelfShareUpload:self.keychainZoneID];
2438 [self saveTLKMaterialToKeychainSimulatingSOS:self.keychainZoneID];
2439
2440 // Save a new device state record with some fake etag
2441 [self.keychainView dispatchSync: ^bool {
2442 CKKSDeviceStateEntry* cdse = [[CKKSDeviceStateEntry alloc] initForDevice:self.ckDeviceID
2443 circlePeerID:self.circlePeerID
2444 circleStatus:kSOSCCInCircle
2445 keyState:SecCKKSZoneKeyStateWaitForTLK
2446 currentTLKUUID:nil
2447 currentClassAUUID:nil
2448 currentClassCUUID:nil
2449 zoneID:self.keychainZoneID
2450 encodedCKRecord:nil];
2451 XCTAssertNotNil(cdse, "Should have created a fake CDSE");
2452 CKRecord* record = [cdse CKRecordWithZoneID:self.keychainZoneID];
2453 XCTAssertNotNil(record, "Should have created a fake CDSE CKRecord");
2454 record.etag = @"fake etag";
2455 cdse.storedCKRecord = record;
2456
2457 NSError* error = nil;
2458 [cdse saveToDatabase:&error];
2459 XCTAssertNil(error, @"No error saving cdse to database");
2460
2461 return true;
2462 }];
2463
2464 // Spin up CKKS subsystem.
2465 [self startCKKSSubsystem];
2466
2467 // We expect a record failure, since the device state record is broke
2468 [self expectCKAtomicModifyItemRecordsUpdateFailure:self.keychainZoneID];
2469
2470 // And then we expect a clean write
2471 [self expectCKModifyItemRecords:1 currentKeyPointerRecords:1 zoneID:self.keychainZoneID
2472 checkItem:[self checkClassCBlock:self.keychainZoneID message:@"Object was encrypted under class C key in hierarchy"]];
2473
2474 [self addGenericPassword: @"data" account: @"account-delete-me"];
2475 OCMVerifyAllWithDelay(self.mockDatabase, 8);
2476 }
2477
2478 - (void)testRecoverFromCloudKitUnknownItemRecord {
2479 [self createAndSaveFakeKeyHierarchy: self.keychainZoneID]; // Make life easy for this test.
2480
2481 // Spin up CKKS subsystem.
2482 [self startCKKSSubsystem];
2483
2484 [self findGenericPassword:@"account-delete-me" expecting:errSecItemNotFound];
2485
2486 CKRecord* ckr = [self createFakeRecord: self.keychainZoneID recordName:@"7B598D31-F9C5-481E-98AC-5A507ACB2D85"];
2487 [self.keychainZone addToZone:ckr];
2488
2489 [self.keychainView notifyZoneChange:nil];
2490 [self.keychainView waitForFetchAndIncomingQueueProcessing];
2491
2492 [self findGenericPassword:@"account-delete-me" expecting:errSecSuccess];
2493
2494 // Delete the record from CloudKit, but miss the notification
2495 XCTAssertNil([self.keychainZone deleteCKRecordIDFromZone: ckr.recordID], "Deleting the record from fake CloudKit should succeed");
2496
2497 // Expect a failed upload when we modify the item
2498 [self expectCKAtomicModifyItemRecordsUpdateFailure:self.keychainZoneID];
2499 [self updateGenericPassword:@"never seen again" account:@"account-delete-me"];
2500 OCMVerifyAllWithDelay(self.mockDatabase, 8);
2501
2502 [self.keychainView waitUntilAllOperationsAreFinished];
2503
2504 // And the item should be disappeared from the local keychain
2505 [self findGenericPassword:@"account-delete-me" expecting:errSecItemNotFound];
2506 }
2507
2508 - (void)testRecoverFromCloudKitUserDeletedZone {
2509 // Test starts with nothing in database, but one in our fake CloudKit.
2510 [self putFakeKeyHierarchyInCloudKit:self.keychainZoneID];
2511
2512 // Spin up CKKS subsystem.
2513 [self startCKKSSubsystem];
2514
2515 // Now, save the TLK to the keychain (to simulate it coming in later via SOS).
2516 [self expectCKKSTLKSelfShareUpload:self.keychainZoneID];
2517 [self saveTLKMaterialToKeychainSimulatingSOS:self.keychainZoneID];
2518
2519 // We expect a single record to be uploaded
2520 [self expectCKModifyItemRecords: 1 currentKeyPointerRecords: 1 zoneID:self.keychainZoneID checkItem: [self checkClassCBlock:self.keychainZoneID message:@"Object was encrypted under class C key in hierarchy"]];
2521 [self addGenericPassword: @"data" account: @"account-delete-me"];
2522 OCMVerifyAllWithDelay(self.mockDatabase, 8);
2523
2524 // The first CKRecordZoneChanges should fail with a 'CKErrorUserDeletedZone' error.
2525 [self.keychainZone failNextFetchWith:[[NSError alloc] initWithDomain:CKErrorDomain code:CKErrorUserDeletedZone userInfo:@{}]];
2526
2527 // We expect a key hierarchy upload, and then the class C item upload
2528 [self expectCKModifyKeyRecords: 3 currentKeyPointerRecords: 3 tlkShareRecords: 1 zoneID:self.keychainZoneID];
2529 [self expectCKModifyItemRecords: 1 currentKeyPointerRecords: 1 zoneID:self.keychainZoneID checkItem: [self checkClassCBlock:self.keychainZoneID message:@"Object was encrypted under class C key in hierarchy"]];
2530
2531 [self.keychainView notifyZoneChange:nil];
2532
2533 OCMVerifyAllWithDelay(self.mockDatabase, 8);
2534
2535 // And check that a new upload occurs.
2536 [self expectCKModifyItemRecords: 1 currentKeyPointerRecords: 1 zoneID:self.keychainZoneID checkItem: [self checkClassABlock:self.keychainZoneID message:@"Object was encrypted under class A key in hierarchy"]];
2537
2538 [self addGenericPassword:@"asdf"
2539 account:@"account-class-A"
2540 viewHint:nil
2541 access:(id)kSecAttrAccessibleWhenUnlocked
2542 expecting:errSecSuccess
2543 message:@"Adding class A item"];
2544 OCMVerifyAllWithDelay(self.mockDatabase, 8);
2545 }
2546
2547 - (void)testRecoverFromCloudKitZoneNotFoundZoneDeletionSuccess {
2548 // Test starts with nothing in database, but one in our fake CloudKit.
2549 [self putFakeKeyHierarchyInCloudKit:self.keychainZoneID];
2550
2551 // Spin up CKKS subsystem.
2552 [self startCKKSSubsystem];
2553
2554 // Now, save the TLK to the keychain (to simulate it coming in later via SOS).
2555 [self expectCKKSTLKSelfShareUpload:self.keychainZoneID];
2556 [self saveTLKMaterialToKeychainSimulatingSOS:self.keychainZoneID];
2557
2558 // We expect a single record to be uploaded
2559 [self expectCKModifyItemRecords: 1 currentKeyPointerRecords: 1 zoneID:self.keychainZoneID checkItem: [self checkClassCBlock:self.keychainZoneID message:@"Object was encrypted under class C key in hierarchy"]];
2560 [self addGenericPassword: @"data" account: @"account-delete-me"];
2561 OCMVerifyAllWithDelay(self.mockDatabase, 8);
2562
2563 [self waitForCKModifications];
2564 self.zones[self.keychainZoneID] = nil; // delete the autocreated zone
2565
2566 // The next CKRecordZoneChanges should fail with a 'zone not found' error.
2567 // BUT: when it goes to delete the zone as part of the reset, that should succeed. So, we won't delete the zone here.
2568 NSError* zoneNotFoundError = [[CKPrettyError alloc] initWithDomain:CKErrorDomain
2569 code:CKErrorZoneNotFound
2570 userInfo:@{}];
2571 NSError* error = [[CKPrettyError alloc] initWithDomain:CKErrorDomain
2572 code:CKErrorPartialFailure
2573 userInfo:@{CKPartialErrorsByItemIDKey: @{self.keychainZoneID:zoneNotFoundError}}];
2574 [self.keychainZone failNextFetchWith:error];
2575
2576 // We expect CKKS to reset itself and recover, then a key hierarchy upload, and then the class C item upload
2577 [self expectCKModifyKeyRecords: 3 currentKeyPointerRecords: 3 tlkShareRecords: 1 zoneID:self.keychainZoneID];
2578 [self expectCKModifyItemRecords: 1 currentKeyPointerRecords: 1 zoneID:self.keychainZoneID checkItem: [self checkClassCBlock:self.keychainZoneID message:@"Object was encrypted under class C key in hierarchy"]];
2579
2580 [self.keychainView notifyZoneChange:nil];
2581
2582 OCMVerifyAllWithDelay(self.mockDatabase, 8);
2583
2584 // And check that a new upload occurs.
2585 [self expectCKModifyItemRecords: 1 currentKeyPointerRecords: 1 zoneID:self.keychainZoneID checkItem: [self checkClassABlock:self.keychainZoneID message:@"Object was encrypted under class A key in hierarchy"]];
2586
2587 [self addGenericPassword:@"asdf"
2588 account:@"account-class-A"
2589 viewHint:nil
2590 access:(id)kSecAttrAccessibleWhenUnlocked
2591 expecting:errSecSuccess
2592 message:@"Adding class A item"];
2593 OCMVerifyAllWithDelay(self.mockDatabase, 8);
2594 }
2595
2596 - (void)testRecoverFromCloudKitZoneNotFoundZoneDeletionFail {
2597 // Test starts with nothing in database, but one in our fake CloudKit.
2598 [self putFakeKeyHierarchyInCloudKit:self.keychainZoneID];
2599
2600 // Spin up CKKS subsystem.
2601 [self startCKKSSubsystem];
2602
2603 // Now, save the TLK to the keychain (to simulate it coming in later via SOS).
2604 [self expectCKKSTLKSelfShareUpload:self.keychainZoneID];
2605 [self saveTLKMaterialToKeychainSimulatingSOS:self.keychainZoneID];
2606
2607 // We expect a single record to be uploaded
2608 [self expectCKModifyItemRecords: 1 currentKeyPointerRecords: 1 zoneID:self.keychainZoneID checkItem: [self checkClassCBlock:self.keychainZoneID message:@"Object was encrypted under class C key in hierarchy"]];
2609 [self addGenericPassword: @"data" account: @"account-delete-me"];
2610 OCMVerifyAllWithDelay(self.mockDatabase, 8);
2611
2612 [self waitForCKModifications];
2613 self.zones[self.keychainZoneID] = nil; // delete the autocreated zone
2614
2615 // We expect CKKS to reset itself and recover, then a key hierarchy upload, and then the class C item upload
2616 [self expectCKModifyKeyRecords: 3 currentKeyPointerRecords: 3 tlkShareRecords: 1 zoneID:self.keychainZoneID];
2617 [self expectCKModifyItemRecords: 1 currentKeyPointerRecords: 1 zoneID:self.keychainZoneID checkItem: [self checkClassCBlock:self.keychainZoneID message:@"Object was encrypted under class C key in hierarchy"]];
2618
2619 [self.keychainView notifyZoneChange:nil];
2620 OCMVerifyAllWithDelay(self.mockDatabase, 8);
2621
2622 // And check that a new upload occurs.
2623 [self expectCKModifyItemRecords: 1 currentKeyPointerRecords: 1 zoneID:self.keychainZoneID checkItem: [self checkClassABlock:self.keychainZoneID message:@"Object was encrypted under class A key in hierarchy"]];
2624
2625 [self addGenericPassword:@"asdf"
2626 account:@"account-class-A"
2627 viewHint:nil
2628 access:(id)kSecAttrAccessibleWhenUnlocked
2629 expecting:errSecSuccess
2630 message:@"Adding class A item"];
2631 OCMVerifyAllWithDelay(self.mockDatabase, 8);
2632 }
2633
2634 - (void)testNoCloudKitAccount {
2635 // Test starts with nothing in database and the user logged out of CloudKit. We expect no CKKS operations.
2636 self.accountStatus = CKAccountStatusNoAccount;
2637 self.circleStatus = kSOSCCNotInCircle;
2638 [self.accountStateTracker notifyCircleStatusChangeAndWaitForSignal];
2639
2640 self.silentFetchesAllowed = false;
2641 [self startCKKSSubsystem];
2642
2643 OCMVerifyAllWithDelay(self.mockDatabase, 8);
2644
2645 [self addGenericPassword: @"data" account: @"account-delete-me"];
2646 [self.keychainView waitUntilAllOperationsAreFinished];
2647
2648 // simulate a NSNotification callback (but still logged out)
2649 self.accountStatus = CKAccountStatusNoAccount;
2650 [self.accountStateTracker notifyCKAccountStatusChangeAndWaitForSignal];
2651
2652 // There should be no further uploads, even when we save keychain items
2653 [self addGenericPassword: @"data" account: @"account-delete-me-2"];
2654 [self addGenericPassword: @"data" account: @"account-delete-me-3"];
2655
2656 [self.keychainView waitUntilAllOperationsAreFinished];
2657 OCMVerifyAllWithDelay(self.mockDatabase, 8);
2658
2659 // Test that there are no items in the database (since we never logged in)
2660 [self checkNoCKKSData: self.keychainView];
2661 }
2662
2663 - (void)testSACloudKitAccount {
2664 // Test starts with nothing in database and the user logged into CloudKit and in circle, but the account is not HSA2.
2665 self.circleStatus = kSOSCCInCircle;
2666 [self.accountStateTracker notifyCircleStatusChangeAndWaitForSignal];
2667
2668 self.accountStatus = CKAccountStatusAvailable;
2669 self.supportsDeviceToDeviceEncryption = NO;
2670
2671 self.silentFetchesAllowed = false;
2672 [self startCKKSSubsystem];
2673
2674 OCMVerifyAllWithDelay(self.mockDatabase, 8);
2675
2676 // There should be no uploads, even when we save keychain items and enter/exit circle
2677 [self addGenericPassword: @"data" account: @"account-delete-me"];
2678 [self.keychainView waitUntilAllOperationsAreFinished];
2679
2680 self.circleStatus = kSOSCCNotInCircle;
2681 [self.accountStateTracker notifyCircleStatusChangeAndWaitForSignal];
2682 [self addGenericPassword: @"data" account: @"account-delete-me-2"];
2683
2684 self.circleStatus = kSOSCCInCircle;
2685 [self.accountStateTracker notifyCircleStatusChangeAndWaitForSignal];
2686 [self addGenericPassword: @"data" account: @"account-delete-me-3"];
2687
2688 [self.keychainView waitUntilAllOperationsAreFinished];
2689 OCMVerifyAllWithDelay(self.mockDatabase, 8);
2690
2691 // Test that there are no items in the database (since we never were in an HSA2 account)
2692 [self checkNoCKKSData: self.keychainView];
2693 }
2694
2695 - (void)testNoCircle {
2696 // Test starts with nothing in database and the user logged into CloudKit, but out of Circle.
2697 self.circleStatus = kSOSCCNotInCircle;
2698 [self.accountStateTracker notifyCircleStatusChangeAndWaitForSignal];
2699
2700 self.accountStatus = CKAccountStatusAvailable;
2701
2702 self.silentFetchesAllowed = false;
2703 [self startCKKSSubsystem];
2704
2705 OCMVerifyAllWithDelay(self.mockDatabase, 8);
2706
2707 [self addGenericPassword: @"data" account: @"account-delete-me"];
2708 [self.keychainView waitUntilAllOperationsAreFinished];
2709
2710 // simulate a NSNotification callback (but still logged out)
2711 self.accountStatus = CKAccountStatusNoAccount;
2712 [self.accountStateTracker notifyCKAccountStatusChangeAndWaitForSignal];
2713
2714 // There should be no further uploads, even when we save keychain items
2715 [self addGenericPassword: @"data" account: @"account-delete-me-2"];
2716 [self addGenericPassword: @"data" account: @"account-delete-me-3"];
2717
2718 [self.keychainView waitUntilAllOperationsAreFinished];
2719 OCMVerifyAllWithDelay(self.mockDatabase, 8);
2720
2721 // Test that there are no items in the database (since we never logged in)
2722 [self checkNoCKKSData: self.keychainView];
2723 }
2724
2725 - (void)testCloudKitLogin {
2726 // Test starts with nothing in database and the user logged out of CloudKit. We expect no CKKS operations.
2727 self.accountStatus = CKAccountStatusNoAccount;
2728 self.circleStatus = kSOSCCNotInCircle;
2729 [self.accountStateTracker notifyCircleStatusChangeAndWaitForSignal];
2730 [self startCKKSSubsystem];
2731
2732 XCTAssertEqual(0, [self.keychainView.loggedOut wait:500*NSEC_PER_MSEC], "Should have been told of a 'logout' event on startup");
2733 XCTAssertNotEqual(0, [self.keychainView.loggedIn wait:100*NSEC_PER_MSEC], "'login' event shouldn't have happened");
2734
2735 [self.keychainView waitUntilAllOperationsAreFinished];
2736 OCMVerifyAllWithDelay(self.mockDatabase, 8);
2737
2738 // simulate a cloudkit login and NSNotification callback
2739 self.accountStatus = CKAccountStatusAvailable;
2740 [self.accountStateTracker notifyCKAccountStatusChangeAndWaitForSignal];
2741
2742 // No writes yet, since we're not in circle
2743 [self.keychainView waitUntilAllOperationsAreFinished];
2744 OCMVerifyAllWithDelay(self.mockDatabase, 8);
2745
2746 // We expect some sort of TLK/key hierarchy upload once we are notified of entering the circle.
2747 [self expectCKModifyKeyRecords: 3 currentKeyPointerRecords: 3 tlkShareRecords: 1 zoneID:self.keychainZoneID];
2748
2749 self.circleStatus = kSOSCCInCircle;
2750 [self.accountStateTracker notifyCircleStatusChangeAndWaitForSignal];
2751
2752 XCTAssertEqual(0, [self.keychainView.loggedIn wait:2000*NSEC_PER_MSEC], "Should have been told of a 'login'");
2753 XCTAssertNotEqual(0, [self.keychainView.loggedOut wait:100*NSEC_PER_MSEC], "'logout' event should be reset");
2754
2755 OCMVerifyAllWithDelay(self.mockDatabase, 8);
2756 [self waitForCKModifications];
2757
2758 // We expect a single class C record to be uploaded.
2759 [self expectCKModifyItemRecords: 1 currentKeyPointerRecords: 1 zoneID:self.keychainZoneID checkItem: [self checkClassCBlock:self.keychainZoneID message:@"Object was encrypted under class C key in hierarchy"]];
2760 [self addGenericPassword: @"data" account: @"account-delete-me"];
2761
2762 OCMVerifyAllWithDelay(self.mockDatabase, 8);
2763 [self waitForCKModifications];
2764 }
2765
2766 - (void)testCloudKitLogoutLogin {
2767 // Test starts with nothing in database. We expect some sort of TLK/key hierarchy upload.
2768 [self expectCKModifyKeyRecords: 3 currentKeyPointerRecords: 3 tlkShareRecords: 1 zoneID:self.keychainZoneID];
2769
2770 [self startCKKSSubsystem];
2771 XCTAssertEqual(0, [self.keychainView.loggedIn wait:2000*NSEC_PER_MSEC], "Should have been told of a 'login'");
2772 XCTAssertNotEqual(0, [self.keychainView.loggedOut wait:100*NSEC_PER_MSEC], "'logout' event should be reset");
2773
2774 OCMVerifyAllWithDelay(self.mockDatabase, 20);
2775 [self waitForCKModifications];
2776
2777 // We expect a single class C record to be uploaded.
2778 [self expectCKModifyItemRecords: 1 currentKeyPointerRecords: 1 zoneID:self.keychainZoneID checkItem: [self checkClassCBlock:self.keychainZoneID message:@"Object was encrypted under class C key in hierarchy"]];
2779 [self addGenericPassword: @"data" account: @"account-delete-me"];
2780
2781 OCMVerifyAllWithDelay(self.mockDatabase, 20);
2782 [self waitForCKModifications];
2783
2784 // simulate a cloudkit logout and NSNotification callback
2785 self.accountStatus = CKAccountStatusNoAccount;
2786 [self.accountStateTracker notifyCKAccountStatusChangeAndWaitForSignal];
2787 self.circleStatus = kSOSCCNotInCircle;
2788 [self.accountStateTracker notifyCircleStatusChangeAndWaitForSignal];
2789
2790 // Test that there are no items in the database after logout
2791 XCTAssertEqual(0, [self.keychainView.loggedOut wait:2000*NSEC_PER_MSEC], "Should have been told of a 'logout'");
2792 XCTAssertNotEqual(0, [self.keychainView.loggedIn wait:100*NSEC_PER_MSEC], "'login' event should be reset");
2793 [self checkNoCKKSData: self.keychainView];
2794
2795 // There should be no further uploads, even when we save keychain items
2796 [self addGenericPassword: @"data" account: @"account-delete-me-2"];
2797 [self addGenericPassword: @"data" account: @"account-delete-me-3"];
2798
2799 [self.keychainView waitUntilAllOperationsAreFinished];
2800 OCMVerifyAllWithDelay(self.mockDatabase, 20);
2801
2802 // simulate a cloudkit login
2803 // 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
2804 [self expectCKModifyItemRecords: 2 currentKeyPointerRecords: 1 zoneID:self.keychainZoneID];
2805
2806 self.accountStatus = CKAccountStatusAvailable;
2807 [self.accountStateTracker notifyCKAccountStatusChangeAndWaitForSignal];
2808 self.circleStatus = kSOSCCInCircle;
2809 [self.accountStateTracker notifyCircleStatusChangeAndWaitForSignal];
2810
2811 XCTAssertEqual(0, [self.keychainView.loggedIn wait:2000*NSEC_PER_MSEC], "Should have been told of a 'login'");
2812 XCTAssertNotEqual(0, [self.keychainView.loggedOut wait:100*NSEC_PER_MSEC], "'logout' event should be reset");
2813
2814 OCMVerifyAllWithDelay(self.mockDatabase, 20);
2815
2816 // Let everything settle...
2817 [self.keychainView waitUntilAllOperationsAreFinished];
2818
2819 // Logout again
2820 self.accountStatus = CKAccountStatusNoAccount;
2821 [self.accountStateTracker notifyCKAccountStatusChangeAndWaitForSignal];
2822 self.circleStatus = kSOSCCNotInCircle;
2823 [self.accountStateTracker notifyCircleStatusChangeAndWaitForSignal];
2824
2825 // Test that there are no items in the database after logout
2826 XCTAssertEqual(0, [self.keychainView.loggedOut wait:2000*NSEC_PER_MSEC], "Should have been told of a 'logout'");
2827 XCTAssertNotEqual(0, [self.keychainView.loggedIn wait:100*NSEC_PER_MSEC], "'login' event should be reset");
2828 [self checkNoCKKSData: self.keychainView];
2829
2830 // There should be no further uploads, even when we save keychain items
2831 [self addGenericPassword: @"data" account: @"account-delete-me-5"];
2832 [self addGenericPassword: @"data" account: @"account-delete-me-6"];
2833
2834 [self.keychainView waitUntilAllOperationsAreFinished];
2835 OCMVerifyAllWithDelay(self.mockDatabase, 20);
2836
2837 // simulate a cloudkit login
2838 // 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
2839 [self expectCKModifyItemRecords: 2 currentKeyPointerRecords: 1 zoneID:self.keychainZoneID];
2840
2841 self.accountStatus = CKAccountStatusAvailable;
2842 [self.accountStateTracker notifyCKAccountStatusChangeAndWaitForSignal];
2843 self.circleStatus = kSOSCCInCircle;
2844 [self.accountStateTracker notifyCircleStatusChangeAndWaitForSignal];
2845
2846 XCTAssertEqual(0, [self.keychainView.loggedIn wait:2000*NSEC_PER_MSEC], "Should have been told of a 'login'");
2847 XCTAssertNotEqual(0, [self.keychainView.loggedOut wait:100*NSEC_PER_MSEC], "'logout' event should be reset");
2848
2849 OCMVerifyAllWithDelay(self.mockDatabase, 20);
2850 }
2851
2852 - (void)testCloudKitLoginRace {
2853 // Test starts with nothing in database, and 'in circle', but securityd hasn't received notification if we're logged into CloudKit.
2854 // CKKS should not call handleLogout.
2855
2856 id partialKVMock = OCMPartialMock(self.keychainView);
2857 OCMReject([partialKVMock handleCKLogout]);
2858 // note: don't unblock the ck account state object yet...
2859
2860 self.circleStatus = kSOSCCInCircle;
2861 [self.accountStateTracker notifyCircleStatusChangeAndWaitForSignal];
2862
2863 // Add a keychain item, but make sure it doesn't upload yet.
2864 [self addGenericPassword: @"data" account: @"account-delete-me"];
2865
2866 [self.keychainView waitUntilAllOperationsAreFinished];
2867 OCMVerifyAllWithDelay(self.mockDatabase, 8);
2868
2869 // Now that we're here (and handleCKLogout hasn't been called), bring the account up
2870
2871 // We expect some sort of TLK/key hierarchy upload once we are notified of entering the circle.
2872 [self expectCKModifyKeyRecords: 3 currentKeyPointerRecords: 3 tlkShareRecords: 1 zoneID:self.keychainZoneID];
2873
2874 // We expect a single class C record to be uploaded.
2875 [self expectCKModifyItemRecords: 1 currentKeyPointerRecords: 1 zoneID:self.keychainZoneID checkItem: [self checkClassCBlock:self.keychainZoneID message:@"Object was encrypted under class C key in hierarchy"]];
2876
2877 self.accountStatus = CKAccountStatusAvailable;
2878 [self startCKAccountStatusMock];
2879
2880 // simulate another NSNotification callback
2881 [self.accountStateTracker notifyCKAccountStatusChangeAndWaitForSignal];
2882
2883 OCMVerifyAllWithDelay(self.mockDatabase, 8);
2884 [self waitForCKModifications];
2885
2886 // Make sure new items upload too
2887 [self expectCKModifyItemRecords: 1 currentKeyPointerRecords: 1 zoneID:self.keychainZoneID checkItem: [self checkClassCBlock:self.keychainZoneID message:@"Object was encrypted under class C key in hierarchy"]];
2888 [self addGenericPassword: @"data" account: @"account-delete-me-2"];
2889 OCMVerifyAllWithDelay(self.mockDatabase, 8);
2890
2891 [self.keychainView waitUntilAllOperationsAreFinished];
2892 [self waitForCKModifications];
2893 [self.keychainView halt];
2894
2895 [partialKVMock stopMocking];
2896 }
2897
2898 - (void)testDeviceStateUploadGood {
2899 [self createAndSaveFakeKeyHierarchy: self.keychainZoneID]; // Make life easy for this test.
2900
2901 [self startCKKSSubsystem];
2902 [self.keychainView waitForKeyHierarchyReadiness];
2903
2904 __weak __typeof(self) weakSelf = self;
2905 [self expectCKModifyRecords: @{SecCKRecordDeviceStateType: [NSNumber numberWithInt:1]}
2906 deletedRecordTypeCounts:nil
2907 zoneID:self.keychainZoneID
2908 checkModifiedRecord: ^BOOL (CKRecord* record){
2909 if([record.recordType isEqualToString: SecCKRecordDeviceStateType]) {
2910 // Check that all the things matches
2911 __strong __typeof(weakSelf) strongSelf = weakSelf;
2912 XCTAssertNotNil(strongSelf, "self exists");
2913
2914 ZoneKeys* zoneKeys = strongSelf.keys[strongSelf.keychainZoneID];
2915 XCTAssertNotNil(zoneKeys, "Have zone keys for %@", strongSelf.keychainZoneID);
2916
2917 XCTAssertEqualObjects(record[SecCKRecordCirclePeerID], strongSelf.circlePeerID, "peer ID matches what we gave it");
2918 XCTAssertEqualObjects(record[SecCKRecordCircleStatus], [NSNumber numberWithInt:kSOSCCInCircle], "device is in circle");
2919 XCTAssertEqualObjects(record[SecCKRecordKeyState], CKKSZoneKeyToNumber(SecCKKSZoneKeyStateReady), "Device is in ready");
2920
2921 XCTAssertEqualObjects([record[SecCKRecordCurrentTLK] recordID].recordName, zoneKeys.tlk.uuid, "Correct TLK uuid");
2922 XCTAssertEqualObjects([record[SecCKRecordCurrentClassA] recordID].recordName, zoneKeys.classA.uuid, "Correct class A uuid");
2923 XCTAssertEqualObjects([record[SecCKRecordCurrentClassC] recordID].recordName, zoneKeys.classC.uuid, "Correct class C uuid");
2924 return YES;
2925 } else {
2926 return NO;
2927 }
2928 }
2929 runAfterModification:nil];
2930
2931 [self.keychainView updateDeviceState:false waitForKeyHierarchyInitialization:2*NSEC_PER_SEC ckoperationGroup:nil];
2932
2933 OCMVerifyAllWithDelay(self.mockDatabase, 8);
2934 }
2935
2936 - (void)testDeviceStateUploadRateLimited {
2937 [self createAndSaveFakeKeyHierarchy: self.keychainZoneID]; // Make life easy for this test.
2938
2939 [self startCKKSSubsystem];
2940 [self.keychainView waitForKeyHierarchyReadiness];
2941
2942 __weak __typeof(self) weakSelf = self;
2943 [self expectCKModifyRecords: @{SecCKRecordDeviceStateType: [NSNumber numberWithInt:1]}
2944 deletedRecordTypeCounts:nil
2945 zoneID:self.keychainZoneID
2946 checkModifiedRecord: ^BOOL (CKRecord* record){
2947 if([record.recordType isEqualToString: SecCKRecordDeviceStateType]) {
2948 // Check that all the things matches
2949 __strong __typeof(weakSelf) strongSelf = weakSelf;
2950 XCTAssertNotNil(strongSelf, "self exists");
2951
2952 ZoneKeys* zoneKeys = strongSelf.keys[strongSelf.keychainZoneID];
2953 XCTAssertNotNil(zoneKeys, "Have zone keys for %@", strongSelf.keychainZoneID);
2954
2955 XCTAssertEqualObjects(record[SecCKRecordCirclePeerID], strongSelf.circlePeerID, "peer ID matches what we gave it");
2956 XCTAssertEqualObjects(record[SecCKRecordCircleStatus], [NSNumber numberWithInt:kSOSCCInCircle], "device is in circle");
2957 XCTAssertEqualObjects(record[SecCKRecordKeyState], CKKSZoneKeyToNumber(SecCKKSZoneKeyStateReady), "Device is in ready");
2958
2959 XCTAssertEqualObjects([record[SecCKRecordCurrentTLK] recordID].recordName, zoneKeys.tlk.uuid, "Correct TLK uuid");
2960 XCTAssertEqualObjects([record[SecCKRecordCurrentClassA] recordID].recordName, zoneKeys.classA.uuid, "Correct class A uuid");
2961 XCTAssertEqualObjects([record[SecCKRecordCurrentClassC] recordID].recordName, zoneKeys.classC.uuid, "Correct class C uuid");
2962 return YES;
2963 } else {
2964 return NO;
2965 }
2966 }
2967 runAfterModification:nil];
2968
2969 CKKSUpdateDeviceStateOperation* op = [self.keychainView updateDeviceState:true waitForKeyHierarchyInitialization:2*NSEC_PER_SEC ckoperationGroup:nil];
2970 OCMVerifyAllWithDelay(self.mockDatabase, 8);
2971 [op waitUntilFinished];
2972
2973 // Check that an immediate rate-limited retry doesn't upload anything
2974 op = [self.keychainView updateDeviceState:true waitForKeyHierarchyInitialization:2*NSEC_PER_SEC ckoperationGroup:nil];
2975 [op waitUntilFinished];
2976
2977 // But not rate-limiting works just fine!
2978 [self expectCKModifyRecords:@{SecCKRecordDeviceStateType: [NSNumber numberWithInt:1]}
2979 deletedRecordTypeCounts:nil
2980 zoneID:self.keychainZoneID
2981 checkModifiedRecord:nil
2982 runAfterModification:nil];
2983 op = [self.keychainView updateDeviceState:false waitForKeyHierarchyInitialization:2*NSEC_PER_SEC ckoperationGroup:nil];
2984 OCMVerifyAllWithDelay(self.mockDatabase, 8);
2985 [op waitUntilFinished];
2986
2987 // And now, if the update is old enough, that'll work too
2988 [self.keychainView dispatchSync:^bool {
2989 NSError* error = nil;
2990 CKKSDeviceStateEntry* cdse = [CKKSDeviceStateEntry fromDatabase:self.accountStateTracker.ckdeviceID zoneID:self.keychainZoneID error:&error];
2991 XCTAssertNil(error, "No error fetching device state entry");
2992 XCTAssertNotNil(cdse, "Fetched device state entry");
2993
2994 CKRecord* record = cdse.storedCKRecord;
2995
2996 NSDate* m = record.modificationDate;
2997 XCTAssertNotNil(m, "Have modification date");
2998
2999 // Four days ago!
3000 NSDateComponents* offset = [[NSDateComponents alloc] init];
3001 [offset setHour:-4 * 24];
3002 NSDate* m2 = [[NSCalendar currentCalendar] dateByAddingComponents:offset toDate:m options:0];
3003
3004 XCTAssertNotNil(m2, "Made modification date");
3005
3006 record.modificationDate = m2;
3007 [cdse setStoredCKRecord:record];
3008
3009 [cdse saveToDatabase:&error];
3010 XCTAssertNil(error, "No error saving device state entry");
3011
3012 return true;
3013 }];
3014
3015 // And now the rate-limiting doesn't get in the way
3016 [self expectCKModifyRecords:@{SecCKRecordDeviceStateType: [NSNumber numberWithInt:1]}
3017 deletedRecordTypeCounts:nil
3018 zoneID:self.keychainZoneID
3019 checkModifiedRecord:nil
3020 runAfterModification:nil];
3021 op = [self.keychainView updateDeviceState:true waitForKeyHierarchyInitialization:2*NSEC_PER_SEC ckoperationGroup:nil];
3022 OCMVerifyAllWithDelay(self.mockDatabase, 12);
3023 [op waitUntilFinished];
3024 }
3025
3026 - (void)testDeviceStateUploadRateLimitedAfterNormalUpload {
3027 [self createAndSaveFakeKeyHierarchy: self.keychainZoneID]; // Make life easy for this test.
3028
3029 [self startCKKSSubsystem];
3030 [self.keychainView waitForKeyHierarchyReadiness];
3031
3032 [self expectCKModifyItemRecords: 1 currentKeyPointerRecords: 1 zoneID:self.keychainZoneID];
3033 [self addGenericPassword:@"password" account:@"account-delete-me"];
3034 OCMVerifyAllWithDelay(self.mockDatabase, 8);
3035
3036 // Check that an immediate rate-limited retry doesn't upload anything
3037 CKKSUpdateDeviceStateOperation* op = [self.keychainView updateDeviceState:true waitForKeyHierarchyInitialization:2*NSEC_PER_SEC ckoperationGroup:nil];
3038 [op waitUntilFinished];
3039 }
3040
3041 - (void)testDeviceStateUploadWaitsForKeyHierarchy {
3042 [self createAndSaveFakeKeyHierarchy: self.keychainZoneID]; // Make life easy for this test.
3043
3044 // Ask to wait for quite a while if we don't become ready
3045 [self.keychainView updateDeviceState:false waitForKeyHierarchyInitialization:20*NSEC_PER_SEC ckoperationGroup:nil];
3046
3047 __weak __typeof(self) weakSelf = self;
3048 // Expect a ready upload
3049 [self expectCKModifyRecords: @{SecCKRecordDeviceStateType: [NSNumber numberWithInt:1]}
3050 deletedRecordTypeCounts:nil
3051 zoneID:self.keychainZoneID
3052 checkModifiedRecord: ^BOOL (CKRecord* record){
3053 if([record.recordType isEqualToString: SecCKRecordDeviceStateType]) {
3054 __strong __typeof(weakSelf) strongSelf = weakSelf;
3055 XCTAssertNotNil(strongSelf, "self exists");
3056
3057 ZoneKeys* zoneKeys = strongSelf.keys[strongSelf.keychainZoneID];
3058 XCTAssertNotNil(zoneKeys, "Have zone keys for %@", strongSelf.keychainZoneID);
3059
3060 XCTAssertEqualObjects(record[SecCKRecordCirclePeerID], strongSelf.circlePeerID, "peer ID matches what we gave it");
3061 XCTAssertEqualObjects(record[SecCKRecordCircleStatus], [NSNumber numberWithInt:kSOSCCInCircle], "device is in circle");
3062 XCTAssertEqualObjects(record[SecCKRecordKeyState], CKKSZoneKeyToNumber(SecCKKSZoneKeyStateReady), "Device is in ready");
3063
3064 XCTAssertEqualObjects([record[SecCKRecordCurrentTLK] recordID].recordName, zoneKeys.tlk.uuid, "Correct TLK uuid");
3065 XCTAssertEqualObjects([record[SecCKRecordCurrentClassA] recordID].recordName, zoneKeys.classA.uuid, "Correct class A uuid");
3066 XCTAssertEqualObjects([record[SecCKRecordCurrentClassC] recordID].recordName, zoneKeys.classC.uuid, "Correct class C uuid");
3067 return YES;
3068 } else {
3069 return NO;
3070 }
3071 }
3072 runAfterModification:nil];
3073
3074 // And allow the key state to progress
3075 [self startCKKSSubsystem];
3076 OCMVerifyAllWithDelay(self.mockDatabase, 8);
3077 }
3078
3079 - (void)testDeviceStateReceive {
3080 [self createAndSaveFakeKeyHierarchy: self.keychainZoneID]; // Make life easy for this test.
3081
3082 ZoneKeys* zoneKeys = self.keys[self.keychainZoneID];
3083 XCTAssertNotNil(zoneKeys, "Have zone keys for %@", self.keychainZoneID);
3084
3085 [self startCKKSSubsystem];
3086 [self.keychainView waitForKeyHierarchyReadiness];
3087
3088 CKKSDeviceStateEntry* cdse = [[CKKSDeviceStateEntry alloc] initForDevice:@"otherdevice"
3089 circlePeerID:@"asdfasdf"
3090 circleStatus:kSOSCCInCircle
3091 keyState:SecCKKSZoneKeyStateReady
3092 currentTLKUUID:zoneKeys.tlk.uuid
3093 currentClassAUUID:zoneKeys.classA.uuid
3094 currentClassCUUID:zoneKeys.classC.uuid
3095 zoneID:self.keychainZoneID
3096 encodedCKRecord:nil];
3097 CKRecord* record = [cdse CKRecordWithZoneID:self.keychainZoneID];
3098 [self.keychainZone addToZone:record];
3099
3100 // Trigger a notification (with hilariously fake data)
3101 [self.keychainView notifyZoneChange:nil];
3102 [self.keychainView waitForFetchAndIncomingQueueProcessing];
3103
3104 [self.keychainView dispatchSync: ^bool {
3105 NSError* error = nil;
3106 NSArray<CKKSDeviceStateEntry*>* cdses = [CKKSDeviceStateEntry allInZone:self.keychainZoneID error:&error];
3107 XCTAssertNil(error, "No error fetching CDSEs");
3108 XCTAssertNotNil(cdses, "An array of CDSEs was returned");
3109 XCTAssert(cdses.count >= 1u, "At least one CDSE came back");
3110
3111 CKKSDeviceStateEntry* item = nil;
3112 for(CKKSDeviceStateEntry* dbcdse in cdses) {
3113 if([dbcdse.device isEqualToString:@"otherdevice"]) {
3114 item = dbcdse;
3115 }
3116 }
3117 XCTAssertNotNil(item, "Found a cdse for otherdevice");
3118
3119 XCTAssertEqualObjects(cdse, item, "Saved item matches pre-cloudkit item");
3120
3121 XCTAssertEqualObjects(item.circlePeerID, @"asdfasdf", "correct peer id");
3122 XCTAssertEqualObjects(item.keyState, SecCKKSZoneKeyStateReady, "correct key state");
3123 XCTAssertEqualObjects(item.currentTLKUUID, zoneKeys.tlk.uuid, "correct tlk uuid");
3124 XCTAssertEqualObjects(item.currentClassAUUID, zoneKeys.classA.uuid, "correct classA uuid");
3125 XCTAssertEqualObjects(item.currentClassCUUID, zoneKeys.classC.uuid, "correct classC uuid");
3126
3127 return false;
3128 }];
3129
3130 OCMVerifyAllWithDelay(self.mockDatabase, 8);
3131 }
3132
3133 - (void)testDeviceStateUploadBadKeyState {
3134 // This test has stuff in CloudKit, but no TLKs. It should become very sad.
3135 [self putFakeKeyHierarchyInCloudKit: self.keychainZoneID];
3136
3137 [self startCKKSSubsystem];
3138 XCTAssertEqual(0, [self.keychainView.keyHierarchyConditions[SecCKKSZoneKeyStateWaitForTLK] wait:8*NSEC_PER_SEC], "CKKS entered waitfortlk");
3139 XCTAssertEqualObjects(self.keychainView.keyHierarchyState, SecCKKSZoneKeyStateWaitForTLK, "CKKS entered waitfortlk");
3140
3141 __weak __typeof(self) weakSelf = self;
3142 [self expectCKModifyRecords: @{SecCKRecordDeviceStateType: [NSNumber numberWithInt:1]}
3143 deletedRecordTypeCounts:nil
3144 zoneID:self.keychainZoneID
3145 checkModifiedRecord: ^BOOL (CKRecord* record){
3146 if([record.recordType isEqualToString: SecCKRecordDeviceStateType]) {
3147 // Check that all the things matches
3148 __strong __typeof(weakSelf) strongSelf = weakSelf;
3149 XCTAssertNotNil(strongSelf, "self exists");
3150
3151 XCTAssertEqualObjects(record[SecCKRecordCirclePeerID], strongSelf.circlePeerID, "peer ID matches what we gave it");
3152 XCTAssertEqualObjects(record[SecCKRecordCircleStatus], [NSNumber numberWithInt:kSOSCCInCircle], "device is in circle");
3153 XCTAssertEqualObjects(record[SecCKRecordKeyState], CKKSZoneKeyToNumber(SecCKKSZoneKeyStateWaitForTLK), "Device is in waitfortlk");
3154
3155 XCTAssertNil(record[SecCKRecordCurrentTLK] , "No TLK");
3156 XCTAssertNil(record[SecCKRecordCurrentClassA], "No class A key");
3157 XCTAssertNil(record[SecCKRecordCurrentClassC], "No class C key");
3158 return YES;
3159 } else {
3160 return NO;
3161 }
3162 }
3163 runAfterModification:nil];
3164
3165 [self.keychainView updateDeviceState:false waitForKeyHierarchyInitialization:500*NSEC_PER_MSEC ckoperationGroup:nil];
3166
3167 OCMVerifyAllWithDelay(self.mockDatabase, 8);
3168 }
3169
3170 - (void)testDeviceStateUploadBadKeyStateAfterRestart {
3171 // This test has stuff in CloudKit, but no TLKs. It should become very sad.
3172 [self putFakeKeyHierarchyInCloudKit: self.keychainZoneID];
3173
3174 [self startCKKSSubsystem];
3175 XCTAssertEqual(0, [self.keychainView.keyHierarchyConditions[SecCKKSZoneKeyStateWaitForTLK] wait:8*NSEC_PER_SEC], "CKKS entered waitfortlk");
3176 XCTAssertEqualObjects(self.keychainView.keyHierarchyState, SecCKKSZoneKeyStateWaitForTLK, "CKKS entered waitfortlk");
3177
3178 // And restart CKKS...
3179 self.keychainView = [[CKKSViewManager manager] restartZone: self.keychainZoneID.zoneName];
3180 XCTAssertEqual(0, [self.keychainView.keyHierarchyConditions[SecCKKSZoneKeyStateWaitForTLK] wait:8*NSEC_PER_SEC], "CKKS entered waitfortlk");
3181 XCTAssertEqualObjects(self.keychainView.keyHierarchyState, SecCKKSZoneKeyStateWaitForTLK, "CKKS entered waitfortlk");
3182
3183 __weak __typeof(self) weakSelf = self;
3184 [self expectCKModifyRecords: @{SecCKRecordDeviceStateType: [NSNumber numberWithInt:1]}
3185 deletedRecordTypeCounts:nil
3186 zoneID:self.keychainZoneID
3187 checkModifiedRecord: ^BOOL (CKRecord* record){
3188 if([record.recordType isEqualToString: SecCKRecordDeviceStateType]) {
3189 // Check that all the things matches
3190 __strong __typeof(weakSelf) strongSelf = weakSelf;
3191 XCTAssertNotNil(strongSelf, "self exists");
3192
3193 XCTAssertEqualObjects(record[SecCKRecordCirclePeerID], strongSelf.circlePeerID, "peer ID matches what we gave it");
3194 XCTAssertEqualObjects(record[SecCKRecordCircleStatus], [NSNumber numberWithInt:kSOSCCInCircle], "device is in circle");
3195 XCTAssertEqualObjects(record[SecCKRecordKeyState], CKKSZoneKeyToNumber(SecCKKSZoneKeyStateWaitForTLK), "Device is in waitfortlk");
3196
3197 XCTAssertNil(record[SecCKRecordCurrentTLK] , "No TLK");
3198 XCTAssertNil(record[SecCKRecordCurrentClassA], "No class A key");
3199 XCTAssertNil(record[SecCKRecordCurrentClassC], "No class C key");
3200 return YES;
3201 } else {
3202 return NO;
3203 }
3204 }
3205 runAfterModification:nil];
3206
3207 [self.keychainView updateDeviceState:false waitForKeyHierarchyInitialization:500*NSEC_PER_MSEC ckoperationGroup:nil];
3208
3209 OCMVerifyAllWithDelay(self.mockDatabase, 8);
3210 }
3211
3212
3213 - (void)testDeviceStateUploadBadCircleState {
3214 self.circleStatus = kSOSCCNotInCircle;
3215 [self.accountStateTracker notifyCircleStatusChangeAndWaitForSignal];
3216
3217 // This test has stuff in CloudKit, but no TLKs.
3218 [self putFakeKeyHierarchyInCloudKit: self.keychainZoneID];
3219
3220 [self startCKKSSubsystem];
3221
3222 XCTAssertEqual(0, [self.keychainView.keyHierarchyConditions[SecCKKSZoneKeyStateInitializing] wait:8*NSEC_PER_SEC], "CKKS entered initializing");
3223 XCTAssertEqualObjects(self.keychainView.keyHierarchyState, SecCKKSZoneKeyStateInitializing, "CKKS entered intializing");
3224
3225 __weak __typeof(self) weakSelf = self;
3226 [self expectCKModifyRecords: @{SecCKRecordDeviceStateType: [NSNumber numberWithInt:1]}
3227 deletedRecordTypeCounts:nil
3228 zoneID:self.keychainZoneID
3229 checkModifiedRecord: ^BOOL (CKRecord* record){
3230 if([record.recordType isEqualToString: SecCKRecordDeviceStateType]) {
3231 // Check that all the things matches
3232 __strong __typeof(weakSelf) strongSelf = weakSelf;
3233 XCTAssertNotNil(strongSelf, "self exists");
3234
3235 XCTAssertNil(record[SecCKRecordCirclePeerID], "no peer ID if device is not in circle");
3236 XCTAssertEqualObjects(record[SecCKRecordCircleStatus], [NSNumber numberWithInt:kSOSCCNotInCircle], "device is not in circle");
3237 XCTAssertEqualObjects(record[SecCKRecordKeyState], CKKSZoneKeyToNumber(SecCKKSZoneKeyStateInitializing), "Device is in keystate:initializing");
3238
3239 XCTAssertNil(record[SecCKRecordCurrentTLK] , "No TLK");
3240 XCTAssertNil(record[SecCKRecordCurrentClassA], "No class A key");
3241 XCTAssertNil(record[SecCKRecordCurrentClassC], "No class C key");
3242 return YES;
3243 } else {
3244 return NO;
3245 }
3246 }
3247 runAfterModification:nil];
3248
3249 CKKSUpdateDeviceStateOperation* op = [self.keychainView updateDeviceState:false waitForKeyHierarchyInitialization:500*NSEC_PER_MSEC ckoperationGroup:nil];
3250 OCMVerifyAllWithDelay(self.mockDatabase, 8);
3251
3252 [op waitUntilFinished];
3253 XCTAssertNil(op.error, "No error uploading 'out of circle' device state");
3254 }
3255
3256 - (void)testNotStuckAfterReset {
3257 [self createAndSaveFakeKeyHierarchy: self.keychainZoneID]; // Make life easy for this test.
3258
3259 XCTestExpectation *operationRun = [self expectationWithDescription:@"operation run"];
3260 NSOperation* op = [NSBlockOperation named:@"test" withBlock:^{
3261 [operationRun fulfill];
3262 }];
3263
3264 [op addDependency:self.keychainView.keyStateReadyDependency];
3265 [self.operationQueue addOperation:op];
3266
3267 // And handle a spurious logout
3268 [self.keychainView handleCKLogout];
3269
3270 [self startCKKSSubsystem];
3271
3272 [self waitForExpectations: @[operationRun] timeout:80];
3273 }
3274
3275 - (void)testCKKSControlBringup {
3276 xpc_endpoint_t endpoint = SecServerCreateCKKSEndpoint();
3277 XCTAssertNotNil(endpoint, "Received endpoint");
3278
3279 NSXPCInterface *interface = CKKSSetupControlProtocol([NSXPCInterface interfaceWithProtocol:@protocol(CKKSControlProtocol)]);
3280 XCTAssertNotNil(interface, "Received a configured CKKS interface");
3281
3282 NSXPCListenerEndpoint *listenerEndpoint = [[NSXPCListenerEndpoint alloc] init];
3283 [listenerEndpoint _setEndpoint:endpoint];
3284
3285 NSXPCConnection* connection = [[NSXPCConnection alloc] initWithListenerEndpoint:listenerEndpoint];
3286 XCTAssertNotNil(connection , "Received an active connection");
3287
3288 connection.remoteObjectInterface = interface;
3289 }
3290
3291 @end
3292
3293 #endif // OCTAGON