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