2 * Copyright (c) 2017 Apple Inc. All Rights Reserved.
4 * @APPLE_LICENSE_HEADER_START@
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
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.
21 * @APPLE_LICENSE_HEADER_END@
26 #import <CloudKit/CloudKit.h>
27 #import <XCTest/XCTest.h>
28 #import <OCMock/OCMock.h>
30 #import "keychain/ckks/tests/CloudKitMockXCTest.h"
31 #import "keychain/ckks/tests/CloudKitKeychainSyncingMockXCTest.h"
32 #import "keychain/ckks/CKKS.h"
33 #import "keychain/ckks/CKKSKey.h"
34 #import "keychain/ckks/CKKSPeer.h"
35 #import "keychain/ckks/CKKSTLKShare.h"
36 #import "keychain/ckks/CKKSViewManager.h"
38 #import "keychain/ckks/tests/MockCloudKit.h"
39 #import "keychain/ckks/tests/CKKSTests.h"
41 @interface CloudKitKeychainSyncingTLKSharingTests : CloudKitKeychainSyncingTestsBase
42 @property CKKSSOSSelfPeer* remotePeer1;
43 @property CKKSSOSPeer* remotePeer2;
46 @property CKKSSOSSelfPeer* untrustedPeer;
49 @implementation CloudKitKeychainSyncingTLKSharingTests
54 self.remotePeer1 = [[CKKSSOSSelfPeer alloc] initWithSOSPeerID:@"remote-peer1"
55 encryptionKey:[[SFECKeyPair alloc] initRandomKeyPairWithSpecifier:[[SFECKeySpecifier alloc] initWithCurve:SFEllipticCurveNistp384]]
56 signingKey:[[SFECKeyPair alloc] initRandomKeyPairWithSpecifier:[[SFECKeySpecifier alloc] initWithCurve:SFEllipticCurveNistp384]]];
58 self.remotePeer2 = [[CKKSSOSPeer alloc] initWithSOSPeerID:@"remote-peer2"
59 encryptionPublicKey:[[SFECKeyPair alloc] initRandomKeyPairWithSpecifier:[[SFECKeySpecifier alloc] initWithCurve:SFEllipticCurveNistp384]].publicKey
60 signingPublicKey:[[SFECKeyPair alloc] initRandomKeyPairWithSpecifier:[[SFECKeySpecifier alloc] initWithCurve:SFEllipticCurveNistp384]].publicKey];
62 // Local SOS trusts these peers
63 [self.currentPeers addObject:self.remotePeer1];
64 [self.currentPeers addObject:self.remotePeer2];
66 self.untrustedPeer = [[CKKSSOSSelfPeer alloc] initWithSOSPeerID:@"untrusted-peer"
67 encryptionKey:[[SFECKeyPair alloc] initRandomKeyPairWithSpecifier:[[SFECKeySpecifier alloc] initWithCurve:SFEllipticCurveNistp384]]
68 signingKey:[[SFECKeyPair alloc] initRandomKeyPairWithSpecifier:[[SFECKeySpecifier alloc] initWithCurve:SFEllipticCurveNistp384]]];
72 self.remotePeer1 = nil;
73 self.remotePeer2 = nil;
74 self.untrustedPeer = nil;
79 - (void)testAcceptExistingTLKSharedKeyHierarchy {
80 // Test starts with no keys in CKKS database, but one in our fake CloudKit.
81 [self putFakeKeyHierarchyInCloudKit:self.keychainZoneID];
83 // Test also starts with the TLK shared to all trusted peers from peer1
84 [self putTLKSharesInCloudKit:self.keychainZoneKeys.tlk from:self.remotePeer1 zoneID:self.keychainZoneID];
86 // The CKKS subsystem should accept the keys, and share the TLK back to itself
87 [self expectCKModifyKeyRecords:0 currentKeyPointerRecords:0 tlkShareRecords:1 zoneID:self.keychainZoneID];
88 [self startCKKSSubsystem];
89 XCTAssertEqual(0, [self.keychainView.keyHierarchyConditions[SecCKKSZoneKeyStateReady] wait:10*NSEC_PER_SEC], "Key state should become ready");
91 OCMVerifyAllWithDelay(self.mockDatabase, 8);
93 // Verify that there are three local keys, and three local current key records
94 __weak __typeof(self) weakSelf = self;
95 [self.keychainView dispatchSync: ^bool{
96 __strong __typeof(weakSelf) strongSelf = weakSelf;
97 XCTAssertNotNil(strongSelf, "self exists");
101 NSArray<CKKSKey*>* keys = [CKKSKey localKeys:strongSelf.keychainZoneID error:&error];
102 XCTAssertNil(error, "no error fetching keys");
103 XCTAssertEqual(keys.count, 3u, "Three keys in local database");
105 NSArray<CKKSCurrentKeyPointer*>* currentkeys = [CKKSCurrentKeyPointer all:&error];
106 XCTAssertNil(error, "no error fetching current keys");
107 XCTAssertEqual(currentkeys.count, 3u, "Three current key pointers in local database");
113 - (void)testAcceptExistingTLKSharedKeyHierarchyAndUse {
114 // Test starts with nothing in database, but one in our fake CloudKit.
115 [self putFakeKeyHierarchyInCloudKit:self.keychainZoneID];
117 // Test also starts with the TLK shared to all trusted peers from peer1
118 [self putTLKSharesInCloudKit:self.keychainZoneKeys.tlk from:self.remotePeer1 zoneID:self.keychainZoneID];
120 // The CKKS subsystem should accept the keys, and share the TLK back to itself
121 [self expectCKModifyKeyRecords:0 currentKeyPointerRecords:0 tlkShareRecords:1 zoneID:self.keychainZoneID];
122 [self startCKKSSubsystem];
123 XCTAssertEqual(0, [self.keychainView.keyHierarchyConditions[SecCKKSZoneKeyStateReady] wait:20*NSEC_PER_SEC], "Key state should become ready");
125 // We expect a single record to be uploaded for each key class
126 [self expectCKModifyItemRecords: 1 currentKeyPointerRecords: 1 zoneID:self.keychainZoneID
127 checkItem: [self checkClassCBlock:self.keychainZoneID message:@"Object was encrypted under class C key in hierarchy"]];
128 [self addGenericPassword: @"data" account: @"account-delete-me"];
129 OCMVerifyAllWithDelay(self.mockDatabase, 8);
131 [self expectCKModifyItemRecords: 1 currentKeyPointerRecords: 1 zoneID:self.keychainZoneID
132 checkItem: [self checkClassABlock:self.keychainZoneID message:@"Object was encrypted under class A key in hierarchy"]];
133 [self addGenericPassword:@"asdf"
134 account:@"account-class-A"
136 access:(id)kSecAttrAccessibleWhenUnlocked
137 expecting:errSecSuccess
138 message:@"Adding class A item"];
139 OCMVerifyAllWithDelay(self.mockDatabase, 8);
142 - (void)testNewTLKSharesHaveChangeTags {
143 // Since there's currently no flow for CKKS to ever update a TLK share when things are working properly, do some hackery
145 // Test starts with no keys in CKKS database, but one in our fake CloudKit.
146 [self putFakeKeyHierarchyInCloudKit:self.keychainZoneID];
148 // Test also starts with the TLK shared to all trusted peers from peer1
149 [self putTLKSharesInCloudKit:self.keychainZoneKeys.tlk from:self.remotePeer1 zoneID:self.keychainZoneID];
150 [self saveTLKSharesInLocalDatabase:self.keychainZoneID];
152 // The CKKS subsystem should accept the keys, and share the TLK back to itself
153 [self expectCKModifyKeyRecords:0 currentKeyPointerRecords:0 tlkShareRecords:1 zoneID:self.keychainZoneID];
154 [self startCKKSSubsystem];
155 XCTAssertEqual(0, [self.keychainView.keyHierarchyConditions[SecCKKSZoneKeyStateReady] wait:10*NSEC_PER_SEC], "Key state should become ready");
157 OCMVerifyAllWithDelay(self.mockDatabase, 8);
158 [self waitForCKModifications];
160 // Verify that making a new share will have the old share's change tag
161 __weak __typeof(self) weakSelf = self;
162 [self.keychainView dispatchSyncWithAccountKeys: ^bool{
163 __strong __typeof(weakSelf) strongSelf = weakSelf;
164 XCTAssertNotNil(strongSelf, "self exists");
166 NSError* error = nil;
167 CKKSTLKShare* share = [CKKSTLKShare share:strongSelf.keychainZoneKeys.tlk
168 as:strongSelf.currentSelfPeer
169 to:strongSelf.currentSelfPeer
173 XCTAssertNil(error, "Shouldn't be an error creating a share");
174 XCTAssertNotNil(share, "Should be able to create share");
176 CKRecord* newRecord = [share CKRecordWithZoneID:strongSelf.keychainZoneID];
177 XCTAssertNotNil(newRecord, "Should be able to create a CKRecord");
179 CKRecord* cloudKitRecord = strongSelf.keychainZone.currentDatabase[newRecord.recordID];
180 XCTAssertNotNil(cloudKitRecord, "Should have found existing CKRecord in cloudkit");
181 XCTAssertNotNil(cloudKitRecord.recordChangeTag, "Existing record should have a change tag");
183 XCTAssertEqualObjects(cloudKitRecord.recordChangeTag, newRecord.recordChangeTag, "Change tags on existing and new records should match");
189 - (void)testReceiveTLKShareRecordsAndDeletes {
190 // Test starts with no keys in CKKS database, but one in our fake CloudKit.
191 [self putFakeKeyHierarchyInCloudKit:self.keychainZoneID];
193 // Test also starts with the TLK shared to all trusted peers from peer1
194 [self putTLKSharesInCloudKit:self.keychainZoneKeys.tlk from:self.remotePeer1 zoneID:self.keychainZoneID];
196 // The CKKS subsystem should accept the keys, and share the TLK back to itself
197 [self expectCKModifyKeyRecords:0 currentKeyPointerRecords:0 tlkShareRecords:1 zoneID:self.keychainZoneID];
198 [self startCKKSSubsystem];
199 XCTAssertEqual(0, [self.keychainView.keyHierarchyConditions[SecCKKSZoneKeyStateReady] wait:20*NSEC_PER_SEC], "Key state should become ready");
201 // The CKKS subsystem should not try to write anything to the CloudKit database while it's accepting the keys
202 XCTAssertEqual(0, [self.keychainView.keyHierarchyConditions[SecCKKSZoneKeyStateReady] wait:10*NSEC_PER_SEC], "Key state should become ready");
203 OCMVerifyAllWithDelay(self.mockDatabase, 8);
206 // Make another share, but from an untrusted peer to some other peer. local shouldn't necessarily care.
207 NSError* error = nil;
208 CKKSTLKShare* share = [CKKSTLKShare share:self.keychainZoneKeys.tlk
209 as:self.untrustedPeer
214 XCTAssertNil(error, "Should have been no error sharing a CKKSKey");
215 XCTAssertNotNil(share, "Should be able to create a share");
217 CKRecord* shareCKRecord = [share CKRecordWithZoneID: self.keychainZoneID];
218 XCTAssertNotNil(shareCKRecord, "Should have been able to create a CKRecord");
219 [self.keychainZone addToZone:shareCKRecord];
220 [self.keychainView notifyZoneChange:nil];
221 [self.keychainView waitForFetchAndIncomingQueueProcessing];
223 [self.keychainView dispatchSync:^bool {
224 NSError* blockerror = nil;
225 CKKSTLKShare* localshare = [CKKSTLKShare tryFromDatabaseFromCKRecordID:shareCKRecord.recordID error:&blockerror];
226 XCTAssertNil(blockerror, "Shouldn't error finding TLKShare record in database");
227 XCTAssertNotNil(localshare, "Should be able to find a TLKShare record in database");
231 // Delete the record in CloudKit...
232 [self.keychainZone deleteCKRecordIDFromZone:shareCKRecord.recordID];
233 [self.keychainView notifyZoneChange:nil];
234 [self.keychainView waitForFetchAndIncomingQueueProcessing];
236 // Should be gone now.
237 [self.keychainView dispatchSync:^bool {
238 NSError* blockerror = nil;
239 CKKSTLKShare* localshare = [CKKSTLKShare tryFromDatabaseFromCKRecordID:shareCKRecord.recordID error:&blockerror];
241 XCTAssertNil(blockerror, "Shouldn't error trying to find non-existent TLKShare record in database");
242 XCTAssertNil(localshare, "Shouldn't be able to find a TLKShare record in database");
248 - (void)testReceiveSharedTLKWhileInWaitForTLK {
249 // Test starts with nothing in database, but one in our fake CloudKit.
250 [self putFakeKeyHierarchyInCloudKit:self.keychainZoneID];
252 // Spin up CKKS subsystem.
253 [self startCKKSSubsystem];
255 // The CKKS subsystem should not try to write anything to the CloudKit database, but it should enter waitfortlk
256 XCTAssertEqual(0, [self.keychainView.keyHierarchyConditions[SecCKKSZoneKeyStateWaitForTLK] wait:20*NSEC_PER_SEC], "Key state should become waitfortlk");
258 // peer1 arrives to save the day
259 // The CKKS subsystem should accept the keys, and share the TLK back to itself
260 [self expectCKModifyKeyRecords:0 currentKeyPointerRecords:0 tlkShareRecords:1 zoneID:self.keychainZoneID];
262 [self putTLKSharesInCloudKit:self.keychainZoneKeys.tlk from:self.remotePeer1 zoneID:self.keychainZoneID];
263 [self.keychainView notifyZoneChange:nil];
264 [self.keychainView waitForFetchAndIncomingQueueProcessing];
266 XCTAssertEqual(0, [self.keychainView.keyHierarchyConditions[SecCKKSZoneKeyStateReady] wait:20*NSEC_PER_SEC], "Key state should become ready");
268 // We expect a single record to be uploaded for each key class
269 [self expectCKModifyItemRecords: 1 currentKeyPointerRecords: 1 zoneID:self.keychainZoneID
270 checkItem: [self checkClassCBlock:self.keychainZoneID message:@"Object was encrypted under class C key in hierarchy"]];
271 [self addGenericPassword: @"data" account: @"account-delete-me"];
272 OCMVerifyAllWithDelay(self.mockDatabase, 8);
274 [self expectCKModifyItemRecords: 1 currentKeyPointerRecords: 1 zoneID:self.keychainZoneID
275 checkItem: [self checkClassABlock:self.keychainZoneID message:@"Object was encrypted under class A key in hierarchy"]];
276 [self addGenericPassword:@"asdf"
277 account:@"account-class-A"
279 access:(id)kSecAttrAccessibleWhenUnlocked
280 expecting:errSecSuccess
281 message:@"Adding class A item"];
282 OCMVerifyAllWithDelay(self.mockDatabase, 8);
285 - (void)testReceiveTLKShareWhileLocked {
286 // Test starts with no keys in CKKS database, but one in our fake CloudKit.
287 [self putFakeKeyHierarchyInCloudKit:self.keychainZoneID];
289 // Test also starts with the TLK shared to all trusted peers from peer1
290 [self putTLKSharesInCloudKit:self.keychainZoneKeys.tlk from:self.remotePeer1 zoneID:self.keychainZoneID];
292 self.aksLockState = true;
293 [self.lockStateTracker recheck];
295 // Spin up CKKS subsystem.
296 [self startCKKSSubsystem];
298 // The CKKS subsystem should not try to write anything to the CloudKit database, but it should enter waitforunlock
299 XCTAssertEqual(0, [self.keychainView.keyHierarchyConditions[SecCKKSZoneKeyStateWaitForUnlock] wait:10*NSEC_PER_SEC], "Key state should become waitforunlock");
301 // Now unlock things. We expect a TLKShare upload.
302 [self expectCKModifyKeyRecords:0 currentKeyPointerRecords:0 tlkShareRecords:1 zoneID:self.keychainZoneID];
304 self.aksLockState = false;
305 [self.lockStateTracker recheck];
307 XCTAssertEqual(0, [self.keychainView.keyHierarchyConditions[SecCKKSZoneKeyStateReady] wait:10*NSEC_PER_SEC], "Key state should become ready");
308 OCMVerifyAllWithDelay(self.mockDatabase, 8);
311 - (void)testUploadTLKSharesForExistingHierarchy {
312 // Test starts with key material locally and in CloudKit.
313 [self putFakeKeyHierarchyInCloudKit:self.keychainZoneID];
314 [self saveTLKMaterialToKeychain:self.keychainZoneID];
316 [self expectCKModifyKeyRecords: 0 currentKeyPointerRecords:0 tlkShareRecords:3 zoneID:self.keychainZoneID];
317 [self startCKKSSubsystem];
319 OCMVerifyAllWithDelay(self.mockDatabase, 8);
322 - (void)testUploadTLKSharesForExistingHierarchyOnRestart {
323 // Bring up CKKS. It'll upload a few TLK Shares, but we'll delete them to get into state
324 [self putFakeKeyHierarchyInCloudKit:self.keychainZoneID];
325 [self saveTLKMaterialToKeychain:self.keychainZoneID];
327 [self expectCKModifyKeyRecords:0 currentKeyPointerRecords:0 tlkShareRecords:3 zoneID:self.keychainZoneID];
328 [self startCKKSSubsystem];
330 XCTAssertEqual(0, [self.keychainView.keyHierarchyConditions[SecCKKSZoneKeyStateReady] wait:10*NSEC_PER_SEC], "Key state should become ready");
331 OCMVerifyAllWithDelay(self.mockDatabase, 8);
332 [self waitForCKModifications];
334 // Now, delete all the TLK Shares, so CKKS will upload them again
335 [self.keychainView dispatchSync:^bool {
336 NSError* error = nil;
337 [CKKSTLKShare deleteAll:self.keychainZoneID error:&error];
338 XCTAssertNil(error, "Shouldn't be an error deleting all TLKShares");
340 NSArray<CKRecord*>* records = [self.zones[self.keychainZoneID].currentDatabase allValues];
341 for(CKRecord* record in records) {
342 if([record.recordType isEqualToString:SecCKRecordTLKShareType]) {
343 [self.zones[self.keychainZoneID] deleteFromHistory:record.recordID];
350 // Restart. We expect an upload of 3 TLK shares.
351 [self expectCKModifyKeyRecords: 0 currentKeyPointerRecords:0 tlkShareRecords:3 zoneID:self.keychainZoneID];
352 self.keychainView = [self.injectedManager restartZone: self.keychainZoneID.zoneName];
354 OCMVerifyAllWithDelay(self.mockDatabase, 8);
355 XCTAssertEqual(0, [self.keychainView.keyHierarchyConditions[SecCKKSZoneKeyStateReady] wait:10*NSEC_PER_SEC], "Key state should become ready");
358 - (void)testHandleExternalSharedTLKRoll {
359 // Test starts with key material locally and in CloudKit.
360 [self putFakeKeyHierarchyInCloudKit:self.keychainZoneID];
361 [self saveTLKMaterialToKeychain:self.keychainZoneID];
363 [self expectCKModifyKeyRecords: 0 currentKeyPointerRecords:0 tlkShareRecords:3 zoneID:self.keychainZoneID];
364 [self startCKKSSubsystem];
366 OCMVerifyAllWithDelay(self.mockDatabase, 8);
368 // Now the external peer rolls the TLK and updates the shares
369 [self rollFakeKeyHierarchyInCloudKit:self.keychainZoneID];
370 [self putTLKSharesInCloudKit:self.keychainZoneKeys.tlk from:self.remotePeer1 zoneID:self.keychainZoneID];
372 // CKKS will share the TLK back to itself
373 [self expectCKModifyKeyRecords: 0 currentKeyPointerRecords:0 tlkShareRecords:1 zoneID:self.keychainZoneID];
375 // Trigger a notification
376 [self.keychainView notifyZoneChange:nil];
377 [self.keychainView waitForFetchAndIncomingQueueProcessing];
378 XCTAssertEqual(0, [self.keychainView.keyHierarchyConditions[SecCKKSZoneKeyStateReady] wait:10*NSEC_PER_SEC], "Key state should become ready");
379 OCMVerifyAllWithDelay(self.mockDatabase, 8);
380 [self waitForCKModifications];
382 // We expect a single record to be uploaded.
383 [self expectCKModifyItemRecords: 1 currentKeyPointerRecords: 1 zoneID:self.keychainZoneID
384 checkItem: [self checkClassCBlock:self.keychainZoneID message:@"Object was encrypted under class C key in hierarchy"]];
385 [self addGenericPassword: @"data" account: @"account-delete-me-rolled-key"];
387 OCMVerifyAllWithDelay(self.mockDatabase, 8);
390 - (void)testUploadTLKSharesForExternalTLKRollWithoutShares {
391 // Test starts with key material locally and in CloudKit.
392 [self putFakeKeyHierarchyInCloudKit:self.keychainZoneID];
393 [self saveTLKMaterialToKeychain:self.keychainZoneID];
395 [self expectCKModifyKeyRecords: 0 currentKeyPointerRecords:0 tlkShareRecords:3 zoneID:self.keychainZoneID];
396 [self startCKKSSubsystem];
398 OCMVerifyAllWithDelay(self.mockDatabase, 8);
400 // Now, an old (Tigris) peer rolls the TLK, but doesn't share it
401 // CKKS should get excited and throw 3 new share records up
402 [self expectCKModifyKeyRecords: 0 currentKeyPointerRecords:0 tlkShareRecords:3 zoneID:self.keychainZoneID];
404 // Wait for that modification to finish before changing CK data
405 [self waitForCKModifications];
407 [self rollFakeKeyHierarchyInCloudKit:self.keychainZoneID];
408 [self saveTLKMaterialToKeychain:self.keychainZoneID];
410 // Trigger a notification
411 [self.keychainView notifyZoneChange:nil];
413 OCMVerifyAllWithDelay(self.mockDatabase, 8);
414 XCTAssertEqual(0, [self.keychainView.keyHierarchyConditions[SecCKKSZoneKeyStateReady] wait:10*NSEC_PER_SEC], "Key state should become ready");
416 // We expect a single record to be uploaded.
417 [self expectCKModifyItemRecords: 1 currentKeyPointerRecords: 1 zoneID:self.keychainZoneID
418 checkItem: [self checkClassCBlock:self.keychainZoneID message:@"Object was encrypted under class C key in hierarchy"]];
419 [self addGenericPassword: @"data" account: @"account-delete-me-rolled-key"];
421 OCMVerifyAllWithDelay(self.mockDatabase, 8);
424 - (void)testRecoverFromTLKShareUploadFailure {
425 // Test starts with key material locally and in CloudKit.
426 [self putFakeKeyHierarchyInCloudKit:self.keychainZoneID];
427 [self saveTLKMaterialToKeychain:self.keychainZoneID];
429 __weak __typeof(self) weakSelf = self;
430 [self failNextCKAtomicModifyItemRecordsUpdateFailure:self.keychainZoneID blockAfterReject:^{
431 __strong __typeof(self) strongSelf = weakSelf;
432 [strongSelf expectCKModifyKeyRecords: 0 currentKeyPointerRecords:0 tlkShareRecords:3 zoneID:self.keychainZoneID];
434 [self startCKKSSubsystem];
436 OCMVerifyAllWithDelay(self.mockDatabase, 8);
437 XCTAssertEqual(0, [self.keychainView.keyHierarchyConditions[SecCKKSZoneKeyStateReady] wait:10*NSEC_PER_SEC], "Key state should become ready");
440 - (void)testFillInMissingPeerShares {
441 // Test starts with nothing in database, but one in our fake CloudKit.
442 [self putFakeKeyHierarchyInCloudKit:self.keychainZoneID];
444 // Test also starts with the TLK shared to just the local peer from peer1
445 // We expect the local peer to send it to peer2
446 [self putTLKSharesInCloudKit:self.keychainZoneKeys.tlk from:self.remotePeer1 zoneID:self.keychainZoneID];
448 // The CKKS subsystem should accept the keys, and share the TLK back to itself
449 [self expectCKModifyKeyRecords:0 currentKeyPointerRecords:0 tlkShareRecords:1 zoneID:self.keychainZoneID];
450 [self startCKKSSubsystem];
451 XCTAssertEqual(0, [self.keychainView.keyHierarchyConditions[SecCKKSZoneKeyStateReady] wait:20*NSEC_PER_SEC], "Key state should become ready");
453 // We expect a single record to be uploaded for each key class
454 [self expectCKModifyItemRecords: 1 currentKeyPointerRecords: 1 zoneID:self.keychainZoneID
455 checkItem: [self checkClassCBlock:self.keychainZoneID message:@"Object was encrypted under class C key in hierarchy"]];
456 [self addGenericPassword: @"data" account: @"account-delete-me"];
457 OCMVerifyAllWithDelay(self.mockDatabase, 8);
459 [self expectCKModifyItemRecords: 1 currentKeyPointerRecords: 1 zoneID:self.keychainZoneID
460 checkItem: [self checkClassABlock:self.keychainZoneID message:@"Object was encrypted under class A key in hierarchy"]];
461 [self addGenericPassword:@"asdf"
462 account:@"account-class-A"
464 access:(id)kSecAttrAccessibleWhenUnlocked
465 expecting:errSecSuccess
466 message:@"Adding class A item"];
467 OCMVerifyAllWithDelay(self.mockDatabase, 8);
470 - (void)testDontAcceptTLKFromUntrustedPeer {
471 // Test starts with nothing in database, but key hierarchy in our fake CloudKit.
472 [self putFakeKeyHierarchyInCloudKit:self.keychainZoneID];
474 // Test also starts with the key hierarchy shared from a non-trusted peer
475 [self putTLKSharesInCloudKit:self.keychainZoneKeys.tlk from:self.untrustedPeer zoneID:self.keychainZoneID];
477 // The CKKS subsystem should go into waitfortlk, since it doesn't trust this peer
478 [self startCKKSSubsystem];
479 XCTAssertEqual(0, [self.keychainView.keyHierarchyConditions[SecCKKSZoneKeyStateWaitForTLK] wait:20*NSEC_PER_SEC], "Key state should become ready");
482 - (void)testAcceptSharedTLKOnTrustSetAdditionOfSharer {
483 // Test starts with nothing in database, but key hierarchy in our fake CloudKit.
484 [self putFakeKeyHierarchyInCloudKit:self.keychainZoneID];
486 // Test also starts with the key hierarchy shared from a non-trusted peer
487 // note that it would share it itself too
488 [self putTLKSharesInCloudKit:self.keychainZoneKeys.tlk from:self.untrustedPeer zoneID:self.keychainZoneID];
489 [self putTLKShareInCloudKit:self.keychainZoneKeys.tlk from:self.untrustedPeer to:self.untrustedPeer zoneID:self.keychainZoneID];
491 // The CKKS subsystem should go into waitfortlk, since it doesn't trust this peer
492 [self startCKKSSubsystem];
493 XCTAssertEqual(0, [self.keychainView.keyHierarchyConditions[SecCKKSZoneKeyStateWaitForTLK] wait:20*NSEC_PER_SEC], "Key state should become waitfortlk");
495 // Wait to be sure we really get into that state
496 [self.keychainView waitForOperationsOfClass:[CKKSProcessReceivedKeysOperation class]];
498 // Now, trust the previously-untrusted peer
499 [self.currentPeers addObject: self.untrustedPeer];
500 [self.injectedManager sendTrustedPeerSetChangedUpdate];
502 // The CKKS subsystem should now accept the key, and share the TLK back to itself
503 [self expectCKModifyKeyRecords:0 currentKeyPointerRecords:0 tlkShareRecords:1 zoneID:self.keychainZoneID];
505 XCTAssertEqual(0, [self.keychainView.keyHierarchyConditions[SecCKKSZoneKeyStateReady] wait:8*NSEC_PER_SEC], "Key state should become ready");
507 // And use it as well
508 [self expectCKModifyItemRecords: 1 currentKeyPointerRecords: 1 zoneID:self.keychainZoneID
509 checkItem: [self checkClassCBlock:self.keychainZoneID message:@"Object was encrypted under class C key in hierarchy"]];
510 [self addGenericPassword: @"data" account: @"account-delete-me"];
511 OCMVerifyAllWithDelay(self.mockDatabase, 8);
514 - (void)testSendNewTLKSharesOnTrustSetAddition {
515 // step 1: add a new peer; we should share the TLK with them
516 // start with no trusted peers
517 [self.currentPeers removeAllObjects];
519 [self expectCKModifyKeyRecords:3 currentKeyPointerRecords:3 tlkShareRecords:1 zoneID:self.keychainZoneID];
520 [self startCKKSSubsystem];
522 [self expectCKModifyItemRecords: 1 currentKeyPointerRecords: 1 zoneID:self.keychainZoneID
523 checkItem: [self checkClassCBlock:self.keychainZoneID message:@"Object was encrypted under class C key in hierarchy"]];
524 [self addGenericPassword: @"data" account: @"account-delete-me"];
525 OCMVerifyAllWithDelay(self.mockDatabase, 8);
527 // Cool! New peer arrives!
528 [self expectCKModifyKeyRecords: 0 currentKeyPointerRecords:0 tlkShareRecords:1 zoneID:self.keychainZoneID];
529 [self.currentPeers addObject:self.remotePeer1];
530 [self.injectedManager sendTrustedPeerSetChangedUpdate];
532 OCMVerifyAllWithDelay(self.mockDatabase, 8);
533 [self waitForCKModifications];
535 // step 2: add a new peer who already has a share; no share should be created
536 [self putTLKShareInCloudKit:self.keychainZoneKeys.tlk from:self.remotePeer1 to:self.remotePeer2 zoneID:self.keychainZoneID];
537 [self.keychainView notifyZoneChange:nil];
538 [self.keychainView waitForFetchAndIncomingQueueProcessing];
540 // CKKS should not upload a tlk share for this peer
541 [self.currentPeers addObject:self.remotePeer2];
542 [self.injectedManager sendTrustedPeerSetChangedUpdate];
544 [self.keychainView waitUntilAllOperationsAreFinished];
547 - (void)testFillInMissingPeerSharesAfterUnlock {
548 // step 1: add a new peer; we should share the TLK with them
549 // start with no trusted peers
550 [self.currentPeers removeAllObjects];
552 [self expectCKModifyKeyRecords:3 currentKeyPointerRecords:3 tlkShareRecords:1 zoneID:self.keychainZoneID];
553 [self startCKKSSubsystem];
555 XCTAssertEqual(0, [self.keychainView.keyHierarchyConditions[SecCKKSZoneKeyStateReady] wait:8*NSEC_PER_SEC], @"Key state should become 'ready'");
557 [self expectCKModifyItemRecords: 1 currentKeyPointerRecords: 1 zoneID:self.keychainZoneID
558 checkItem: [self checkClassCBlock:self.keychainZoneID message:@"Object was encrypted under class C key in hierarchy"]];
559 [self addGenericPassword: @"data" account: @"account-delete-me"];
560 OCMVerifyAllWithDelay(self.mockDatabase, 8);
563 self.aksLockState = true;
564 [self.lockStateTracker recheck];
566 // New peer arrives! This can't actually happen (since we have to be unlocked to accept a new peer), but this will exercise CKKS
567 [self.currentPeers addObject:self.remotePeer1];
568 [self.injectedManager sendTrustedPeerSetChangedUpdate];
570 // CKKS should notice that it has things to do...
571 XCTAssertEqual(0, [self.keychainView.keyHierarchyConditions[SecCKKSZoneKeyStateReadyPendingUnlock] wait:8*NSEC_PER_SEC], @"Key state should become 'readypendingunlock'");
574 [self expectCKModifyKeyRecords: 0 currentKeyPointerRecords:0 tlkShareRecords:1 zoneID:self.keychainZoneID];
575 self.aksLockState = false;
576 [self.lockStateTracker recheck];
578 OCMVerifyAllWithDelay(self.mockDatabase, 8);
580 // and return to ready
581 XCTAssertEqual(0, [self.keychainView.keyHierarchyConditions[SecCKKSZoneKeyStateReady] wait:8*NSEC_PER_SEC], @"Key state should become 'ready'");
584 - (void)testAddItemDuringNewTLKSharesOnTrustSetAddition {
585 // step 1: add a new peer; we should share the TLK with them
586 // start with no trusted peers
587 [self.currentPeers removeAllObjects];
589 [self expectCKModifyKeyRecords:3 currentKeyPointerRecords:3 tlkShareRecords:1 zoneID:self.keychainZoneID];
590 [self startCKKSSubsystem];
592 OCMVerifyAllWithDelay(self.mockDatabase, 8);
593 [self waitForCKModifications];
595 // Hold the TLK share modification
596 [self holdCloudKitModifications];
598 [self expectCKModifyKeyRecords: 0 currentKeyPointerRecords:0 tlkShareRecords:1 zoneID:self.keychainZoneID];
599 [self.currentPeers addObject:self.remotePeer1];
600 [self.injectedManager sendTrustedPeerSetChangedUpdate];
602 OCMVerifyAllWithDelay(self.mockDatabase, 8);
604 // While CloudKit is hanging the write, add an item
605 [self addGenericPassword: @"data" account: @"account-delete-me"];
607 // After that returns, release the write. CKKS should upload the new item
608 [self expectCKModifyItemRecords:1 currentKeyPointerRecords:1 zoneID:self.keychainZoneID
609 checkItem:[self checkClassCBlock:self.keychainZoneID message:@"Object was encrypted under class C key in hierarchy"]];
610 [self releaseCloudKitModificationHold];
612 OCMVerifyAllWithDelay(self.mockDatabase, 8);
615 - (void)testSendNewTLKSharesOnTrustSetRemoval {
616 // Not implemented. Trust set removal demands a key roll, but let's not get ahead of ourselves...