2 * Copyright (c) 2016 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 #include <Security/SecItemPriv.h>
31 #include "OSX/sec/Security/SecItemShim.h"
33 #include <Security/SecEntitlements.h>
34 #include <ipc/server_security_helpers.h>
35 #import <Foundation/NSXPCConnection_Private.h>
37 #import "keychain/categories/NSError+UsefulConstructors.h"
39 #import "keychain/ckks/tests/CloudKitMockXCTest.h"
40 #import "keychain/ckks/tests/CloudKitKeychainSyncingMockXCTest.h"
41 #import "keychain/ckks/CKKS.h"
42 #import "keychain/ckks/CKKSItem.h"
43 #import "keychain/ckks/CKKSItemEncrypter.h"
44 #import "keychain/ckks/CKKSKey.h"
45 #import "keychain/ckks/CKKSViewManager.h"
46 #import "keychain/ckks/CKKSZoneStateEntry.h"
48 #import "keychain/ckks/CKKSControl.h"
49 #import "keychain/ckks/CloudKitCategories.h"
51 #import "keychain/ckks/tests/MockCloudKit.h"
52 #import "keychain/ckks/tests/CKKSTests.h"
53 #import "keychain/ckks/tests/CKKSTests+API.h"
55 @implementation CloudKitKeychainSyncingTestsBase (APITests)
57 -(NSMutableDictionary*)pcsAddItemQuery:(NSString*)account
59 serviceIdentifier:(NSNumber*)serviceIdentifier
60 publicKey:(NSData*)publicKey
61 publicIdentity:(NSData*)publicIdentity
64 (id)kSecClass : (id)kSecClassGenericPassword,
65 (id)kSecReturnPersistentRef: @YES,
66 (id)kSecReturnAttributes: @YES,
67 (id)kSecAttrAccessGroup : @"com.apple.security.ckks",
68 (id)kSecAttrAccessible: (id)kSecAttrAccessibleAfterFirstUnlock,
69 (id)kSecAttrAccount : account,
70 (id)kSecAttrSynchronizable : (id)kCFBooleanTrue,
71 (id)kSecValueData : data,
72 (id)kSecAttrDeriveSyncIDFromItemAttributes : (id)kCFBooleanTrue,
73 (id)kSecAttrPCSPlaintextServiceIdentifier : serviceIdentifier,
74 (id)kSecAttrPCSPlaintextPublicKey : publicKey,
75 (id)kSecAttrPCSPlaintextPublicIdentity : publicIdentity,
79 -(NSDictionary*)pcsAddItem:(NSString*)account
81 serviceIdentifier:(NSNumber*)serviceIdentifier
82 publicKey:(NSData*)publicKey
83 publicIdentity:(NSData*)publicIdentity
84 expectingSync:(bool)expectingSync
86 NSMutableDictionary* query = [self pcsAddItemQuery:account
88 serviceIdentifier:(NSNumber*)serviceIdentifier
89 publicKey:(NSData*)publicKey
90 publicIdentity:(NSData*)publicIdentity];
91 CFTypeRef result = NULL;
92 XCTestExpectation* syncExpectation = [self expectationWithDescription: @"callback occurs"];
94 XCTAssertEqual(errSecSuccess, _SecItemAddAndNotifyOnSync((__bridge CFDictionaryRef) query, &result, ^(bool didSync, CFErrorRef error) {
96 XCTAssertTrue(didSync, "Item synced");
97 XCTAssertNil((__bridge NSError*)error, "No error syncing item");
99 XCTAssertFalse(didSync, "Item did not sync");
100 XCTAssertNotNil((__bridge NSError*)error, "Error syncing item");
103 [syncExpectation fulfill];
104 }), @"_SecItemAddAndNotifyOnSync succeeded");
106 // Verify that the item was written to CloudKit
107 OCMVerifyAllWithDelay(self.mockDatabase, 20);
109 // In real code, you'd need to wait for the _SecItemAddAndNotifyOnSync callback to succeed before proceeding
110 [self waitForExpectations:@[syncExpectation] timeout:20];
112 return (NSDictionary*) CFBridgingRelease(result);
115 - (BOOL (^) (CKRecord*)) checkPCSFieldsBlock: (CKRecordZoneID*) zoneID
116 PCSServiceIdentifier:(NSNumber*)servIdentifier
117 PCSPublicKey:(NSData*)publicKey
118 PCSPublicIdentity:(NSData*)publicIdentity
120 __weak __typeof(self) weakSelf = self;
121 return ^BOOL(CKRecord* record) {
122 __strong __typeof(weakSelf) strongSelf = weakSelf;
123 XCTAssertNotNil(strongSelf, "self exists");
125 XCTAssert([record[SecCKRecordPCSServiceIdentifier] isEqual: servIdentifier], "PCS Service identifier matches input");
126 XCTAssert([record[SecCKRecordPCSPublicKey] isEqual: publicKey], "PCS Public Key matches input");
127 XCTAssert([record[SecCKRecordPCSPublicIdentity] isEqual: publicIdentity], "PCS Public Identity matches input");
129 if([record[SecCKRecordPCSServiceIdentifier] isEqual: servIdentifier] &&
130 [record[SecCKRecordPCSPublicKey] isEqual: publicKey] &&
131 [record[SecCKRecordPCSPublicIdentity] isEqual: publicIdentity]) {
140 @interface CloudKitKeychainSyncingAPITests : CloudKitKeychainSyncingTestsBase
143 @implementation CloudKitKeychainSyncingAPITests
144 - (void)testSecuritydClientBringup {
146 CFErrorRef cferror = nil;
147 xpc_endpoint_t endpoint = SecCreateSecuritydXPCServerEndpoint(&cferror);
148 XCTAssertNil((__bridge id)cferror, "No error creating securityd endpoint");
149 XCTAssertNotNil(endpoint, "Received securityd endpoint");
152 NSXPCInterface *interface = [NSXPCInterface interfaceWithProtocol:@protocol(SecuritydXPCProtocol)];
153 [SecuritydXPCClient configureSecuritydXPCProtocol: interface];
154 XCTAssertNotNil(interface, "Received a configured CKKS interface");
157 NSXPCListenerEndpoint *listenerEndpoint = [[NSXPCListenerEndpoint alloc] init];
158 [listenerEndpoint _setEndpoint:endpoint];
160 NSXPCConnection* connection = [[NSXPCConnection alloc] initWithListenerEndpoint:listenerEndpoint];
161 XCTAssertNotNil(connection , "Received an active connection");
163 connection.remoteObjectInterface = interface;
167 - (void)testAddAndNotifyOnSync {
168 [self createAndSaveFakeKeyHierarchy: self.keychainZoneID]; // Make life easy for this test.
170 [self expectCKModifyItemRecords: 1 currentKeyPointerRecords: 1 zoneID:self.keychainZoneID];
171 [self startCKKSSubsystem];
173 // Let things shake themselves out.
174 [self.keychainView waitForKeyHierarchyReadiness];
175 [self waitForCKModifications];
177 NSMutableDictionary* query = [@{
178 (id)kSecClass : (id)kSecClassGenericPassword,
179 (id)kSecAttrAccessGroup : @"com.apple.security.ckks",
180 (id)kSecAttrAccessible: (id)kSecAttrAccessibleAfterFirstUnlock,
181 (id)kSecAttrAccount : @"testaccount",
182 (id)kSecAttrSynchronizable : (id)kCFBooleanTrue,
183 (id)kSecValueData : (id) [@"asdf" dataUsingEncoding:NSUTF8StringEncoding],
186 XCTestExpectation* blockExpectation = [self expectationWithDescription: @"callback occurs"];
188 XCTAssertEqual(errSecSuccess, _SecItemAddAndNotifyOnSync((__bridge CFDictionaryRef) query, NULL, ^(bool didSync, CFErrorRef error) {
189 XCTAssertTrue(didSync, "Item synced properly");
190 XCTAssertNil((__bridge NSError*)error, "No error syncing item");
192 [blockExpectation fulfill];
193 }), @"_SecItemAddAndNotifyOnSync succeeded");
195 [self waitForExpectationsWithTimeout:5.0 handler:nil];
198 - (void)testAddAndNotifyOnSyncSkipsQueue {
199 // Use the PCS plaintext fields to determine which object is which
200 SecResetLocalSecuritydXPCFakeEntitlements();
201 SecAddLocalSecuritydXPCFakeEntitlement(kSecEntitlementPrivateCKKSPlaintextFields, kCFBooleanTrue);
202 SecAddLocalSecuritydXPCFakeEntitlement(kSecEntitlementPrivateCKKSWriteCurrentItemPointers, kCFBooleanTrue);
203 SecAddLocalSecuritydXPCFakeEntitlement(kSecEntitlementPrivateCKKSReadCurrentItemPointers, kCFBooleanTrue);
205 [self createAndSaveFakeKeyHierarchy: self.keychainZoneID]; // Make life easy for this test.
207 [self startCKKSSubsystem];
208 [self.keychainView waitForKeyHierarchyReadiness];
209 [self waitForCKModifications];
210 OCMVerifyAllWithDelay(self.mockDatabase, 40);
212 self.keychainView.holdOutgoingQueueOperation = [CKKSGroupOperation named:@"outgoing-hold" withBlock: ^{
213 secnotice("ckks", "releasing outgoing-queue hold");
216 for(size_t count = 0; count < 150; count++) {
217 [self addGenericPassword:@"data" account:[NSString stringWithFormat:@"account-delete-me-%03lu", count]];
220 NSMutableDictionary* query = [@{
221 (id)kSecClass : (id)kSecClassGenericPassword,
222 (id)kSecAttrAccessGroup : @"com.apple.security.ckks",
223 (id)kSecAttrAccessible: (id)kSecAttrAccessibleAfterFirstUnlock,
224 (id)kSecAttrAccount : @"testaccount",
225 (id)kSecAttrSynchronizable : (id)kCFBooleanTrue,
226 (id)kSecAttrPCSPlaintextPublicKey : [@"asdf" dataUsingEncoding:NSUTF8StringEncoding],
227 (id)kSecValueData : (id) [@"asdf" dataUsingEncoding:NSUTF8StringEncoding],
230 XCTestExpectation* blockExpectation = [self expectationWithDescription: @"callback occurs"];
232 XCTAssertEqual(errSecSuccess, _SecItemAddAndNotifyOnSync((__bridge CFDictionaryRef) query, NULL, ^(bool didSync, CFErrorRef error) {
233 XCTAssertTrue(didSync, "Item synced properly");
234 XCTAssertNil((__bridge NSError*)error, "No error syncing item");
236 [blockExpectation fulfill];
237 }), @"_SecItemAddAndNotifyOnSync succeeded");
239 // Release the hounds
240 [self.operationQueue addOperation:self.keychainView.holdOutgoingQueueOperation];
242 XCTestExpectation* firstQueueOperation = [self expectationWithDescription: @"found the item in the first queue iteration"];
243 [self expectCKModifyItemRecords:SecCKKSOutgoingQueueItemsAtOnce
244 currentKeyPointerRecords:1
245 zoneID:self.keychainZoneID
246 checkItem:^BOOL(CKRecord * _Nonnull record) {
247 if(record[SecCKRecordPCSPublicKey]) {
248 [firstQueueOperation fulfill];
252 [self expectCKModifyItemRecords:51 currentKeyPointerRecords:1 zoneID:self.keychainZoneID];
254 [self waitForExpectationsWithTimeout:5.0 handler:nil];
257 - (void)testAddAndNotifyOnSyncFailure {
258 [self createAndSaveFakeKeyHierarchy: self.keychainZoneID]; // Make life easy for this test.
260 [self startCKKSSubsystem];
261 [self.keychainView waitForFetchAndIncomingQueueProcessing];
263 // Due to item UUID selection, this item will be added with UUID 50184A35-4480-E8BA-769B-567CF72F1EC0.
264 // Add it to CloudKit first!
265 CKRecord* ckr = [self createFakeRecord: self.keychainZoneID recordName:@"50184A35-4480-E8BA-769B-567CF72F1EC0"];
266 [self.keychainZone addToZone: ckr];
270 [self expectCKAtomicModifyItemRecordsUpdateFailure: self.keychainZoneID];
272 NSMutableDictionary* query = [@{
273 (id)kSecClass : (id)kSecClassGenericPassword,
274 (id)kSecAttrAccessGroup : @"com.apple.security.ckks",
275 (id)kSecAttrAccessible: (id)kSecAttrAccessibleAfterFirstUnlock,
276 (id)kSecAttrAccount : @"testaccount",
277 (id)kSecAttrSynchronizable : (id)kCFBooleanTrue,
278 (id)kSecValueData : (id) [@"asdf" dataUsingEncoding:NSUTF8StringEncoding],
279 (id)kSecAttrSyncViewHint : self.keychainView.zoneName, // @ fake view hint for fake view
282 XCTestExpectation* blockExpectation = [self expectationWithDescription: @"callback occurs"];
284 XCTAssertEqual(errSecSuccess, _SecItemAddAndNotifyOnSync((__bridge CFDictionaryRef) query, NULL, ^(bool didSync, CFErrorRef error) {
285 XCTAssertFalse(didSync, "Item did not sync (as expected)");
286 XCTAssertNotNil((__bridge NSError*)error, "error exists when item fails to sync");
288 [blockExpectation fulfill];
289 }), @"_SecItemAddAndNotifyOnSync succeeded");
291 [self waitForExpectationsWithTimeout:5.0 handler:nil];
292 [self waitForCKModifications];
295 - (void)testAddAndNotifyOnSyncLoggedOut {
296 // Test starts with nothing in database and the user logged out of CloudKit. We expect no CKKS operations.
297 self.accountStatus = CKAccountStatusNoAccount;
298 self.silentFetchesAllowed = false;
299 [self startCKKSSubsystem];
301 XCTAssertEqual(0, [self.keychainView.loggedOut wait:20*NSEC_PER_SEC], "CKKS should positively log out");
303 NSMutableDictionary* query = [@{
304 (id)kSecClass : (id)kSecClassGenericPassword,
305 (id)kSecAttrAccessGroup : @"com.apple.security.ckks",
306 (id)kSecAttrAccessible: (id)kSecAttrAccessibleAfterFirstUnlock,
307 (id)kSecAttrAccount : @"testaccount",
308 (id)kSecAttrSynchronizable : (id)kCFBooleanTrue,
309 (id)kSecValueData : (id) [@"asdf" dataUsingEncoding:NSUTF8StringEncoding],
312 XCTestExpectation* blockExpectation = [self expectationWithDescription: @"callback occurs"];
314 XCTAssertEqual(errSecSuccess, _SecItemAddAndNotifyOnSync((__bridge CFDictionaryRef) query, NULL, ^(bool didSync, CFErrorRef error) {
315 XCTAssertFalse(didSync, "Item did not sync (with no iCloud account)");
316 XCTAssertNotNil((__bridge NSError*)error, "Error exists syncing item while logged out");
318 [blockExpectation fulfill];
319 }), @"_SecItemAddAndNotifyOnSync succeeded");
321 [self waitForExpectationsWithTimeout:5.0 handler:nil];
324 - (void)testAddAndNotifyOnSyncAccountStatusUnclear {
325 // Test starts with nothing in database, but CKKS hasn't been told we've logged out yet.
326 // We expect no CKKS operations.
327 self.accountStatus = CKAccountStatusNoAccount;
328 self.silentFetchesAllowed = false;
330 NSMutableDictionary* query = [@{
331 (id)kSecClass : (id)kSecClassGenericPassword,
332 (id)kSecAttrAccessGroup : @"com.apple.security.ckks",
333 (id)kSecAttrAccessible: (id)kSecAttrAccessibleAfterFirstUnlock,
334 (id)kSecAttrAccount : @"testaccount",
335 (id)kSecAttrSynchronizable : (id)kCFBooleanTrue,
336 (id)kSecValueData : (id) [@"asdf" dataUsingEncoding:NSUTF8StringEncoding],
339 XCTestExpectation* blockExpectation = [self expectationWithDescription: @"callback occurs"];
341 XCTAssertEqual(errSecSuccess, _SecItemAddAndNotifyOnSync((__bridge CFDictionaryRef) query, NULL, ^(bool didSync, CFErrorRef error) {
342 XCTAssertFalse(didSync, "Item did not sync (with no iCloud account)");
343 XCTAssertNotNil((__bridge NSError*)error, "Error exists syncing item while logged out");
345 [blockExpectation fulfill];
346 }), @"_SecItemAddAndNotifyOnSync succeeded");
348 // And now, allow CKKS to discover we're logged out
349 [self startCKKSSubsystem];
350 XCTAssertEqual(0, [self.keychainView.loggedOut wait:20*NSEC_PER_SEC], "CKKS should positively log out");
352 [self waitForExpectationsWithTimeout:5.0 handler:nil];
355 - (void)testAddAndNotifyOnSyncBeforeKeyHierarchyReady {
356 // Test starts with a key hierarchy in cloudkit and the TLK having arrived
357 [self putFakeKeyHierarchyInCloudKit:self.keychainZoneID];
358 [self saveTLKMaterialToKeychain:self.keychainZoneID];
359 [self expectCKKSTLKSelfShareUpload:self.keychainZoneID];
361 // But block CloudKit fetches (so the key hierarchy won't be ready when we add this new item)
362 [self holdCloudKitFetches];
364 [self startCKKSSubsystem];
365 XCTAssertEqual(0, [self.keychainView.loggedIn wait:20*NSEC_PER_SEC], "CKKS should log in");
366 [self.keychainView.zoneSetupOperation waitUntilFinished];
368 NSMutableDictionary* query = [@{
369 (id)kSecClass : (id)kSecClassGenericPassword,
370 (id)kSecAttrAccessGroup : @"com.apple.security.ckks",
371 (id)kSecAttrAccessible: (id)kSecAttrAccessibleAfterFirstUnlock,
372 (id)kSecAttrAccount : @"testaccount",
373 (id)kSecAttrSynchronizable : (id)kCFBooleanTrue,
374 (id)kSecAttrSyncViewHint : self.keychainView.zoneName,
375 (id)kSecValueData : (id) [@"asdf" dataUsingEncoding:NSUTF8StringEncoding],
378 XCTestExpectation* blockExpectation = [self expectationWithDescription: @"callback occurs"];
380 XCTAssertEqual(errSecSuccess, _SecItemAddAndNotifyOnSync((__bridge CFDictionaryRef) query, NULL, ^(bool didSync, CFErrorRef error) {
381 XCTAssertTrue(didSync, "Item synced");
382 XCTAssertNil((__bridge NSError*)error, "Shouldn't have received an error syncing item");
384 [blockExpectation fulfill];
385 }), @"_SecItemAddAndNotifyOnSync succeeded");
387 // We should be in the 'fetch' state, but no further
388 XCTAssertEqual(0, [self.keychainView.keyHierarchyConditions[SecCKKSZoneKeyStateFetch] wait:20*NSEC_PER_SEC], @"Should have reached key state 'fetch', but no further");
390 // When we release the fetch, the callback should still fire and the item should upload
391 [self expectCKModifyItemRecords: 1 currentKeyPointerRecords: 1 zoneID:self.keychainZoneID];
392 [self releaseCloudKitFetchHold];
393 XCTAssertEqual(0, [self.keychainView.keyHierarchyConditions[SecCKKSZoneKeyStateReady] wait:20*NSEC_PER_SEC], @"Should have reached key state 'ready'");
395 // Verify that the item was written to CloudKit
396 OCMVerifyAllWithDelay(self.mockDatabase, 20);
398 [self waitForExpectationsWithTimeout:5.0 handler:nil];
401 - (void)testPCSUnencryptedFieldsAdd {
402 [self createAndSaveFakeKeyHierarchy: self.keychainZoneID]; // Make life easy for this test.
404 [self startCKKSSubsystem];
405 [self.keychainView waitForKeyHierarchyReadiness];
407 NSNumber* servIdentifier = @3;
408 NSData* publicKey = [@"asdfasdf" dataUsingEncoding:NSUTF8StringEncoding];
409 NSData* publicIdentity = [@"somedata" dataUsingEncoding:NSUTF8StringEncoding];
411 [self expectCKModifyItemRecords: 1 currentKeyPointerRecords: 1 zoneID:self.keychainZoneID
412 checkItem: [self checkPCSFieldsBlock:self.keychainZoneID
413 PCSServiceIdentifier:(NSNumber *)servIdentifier
414 PCSPublicKey:publicKey
415 PCSPublicIdentity:publicIdentity]];
417 NSMutableDictionary* query = [@{
418 (id)kSecClass : (id)kSecClassGenericPassword,
419 (id)kSecAttrAccessGroup : @"com.apple.security.ckks",
420 (id)kSecAttrAccessible: (id)kSecAttrAccessibleAfterFirstUnlock,
421 (id)kSecAttrAccount : @"testaccount",
422 (id)kSecAttrSynchronizable : (id)kCFBooleanTrue,
423 (id)kSecValueData : (id) [@"asdf" dataUsingEncoding:NSUTF8StringEncoding],
424 (id)kSecAttrDeriveSyncIDFromItemAttributes : (id)kCFBooleanTrue,
425 (id)kSecAttrPCSPlaintextServiceIdentifier : servIdentifier,
426 (id)kSecAttrPCSPlaintextPublicKey : publicKey,
427 (id)kSecAttrPCSPlaintextPublicIdentity : publicIdentity,
428 (id)kSecAttrSyncViewHint : self.keychainView.zoneName, // allows a CKKSScanOperation to find this item
431 XCTAssertEqual(errSecSuccess, SecItemAdd((__bridge CFDictionaryRef) query, NULL), @"SecItemAdd succeeded");
433 // Verify that the item is written to CloudKit
434 OCMVerifyAllWithDelay(self.mockDatabase, 20);
436 CFTypeRef item = NULL;
437 query[(id)kSecValueData] = nil;
438 query[(id)kSecReturnAttributes] = @YES;
439 XCTAssertEqual(errSecSuccess, SecItemCopyMatching((__bridge CFDictionaryRef) query, &item), "item should still exist");
441 NSDictionary* itemAttributes = (NSDictionary*) CFBridgingRelease(item);
442 XCTAssertEqualObjects(itemAttributes[(id)kSecAttrPCSPlaintextServiceIdentifier], servIdentifier, "Service Identifier exists");
443 XCTAssertEqualObjects(itemAttributes[(id)kSecAttrPCSPlaintextPublicKey], publicKey, "public key exists");
444 XCTAssertEqualObjects(itemAttributes[(id)kSecAttrPCSPlaintextPublicIdentity], publicIdentity, "public identity exists");
446 // Find the item record in CloudKit. Since we're using kSecAttrDeriveSyncIDFromItemAttributes,
447 // the record ID is likely 50184A35-4480-E8BA-769B-567CF72F1EC0
448 [self waitForCKModifications];
449 CKRecordID* recordID = [[CKRecordID alloc] initWithRecordName: @"50184A35-4480-E8BA-769B-567CF72F1EC0" zoneID:self.keychainZoneID];
450 CKRecord* record = self.keychainZone.currentDatabase[recordID];
451 XCTAssertNotNil(record, "Found record in CloudKit at expected UUID");
453 XCTAssertEqualObjects(record[SecCKRecordPCSServiceIdentifier], servIdentifier, "Service identifier sent to cloudkit");
454 XCTAssertEqualObjects(record[SecCKRecordPCSPublicKey], publicKey, "public key sent to cloudkit");
455 XCTAssertEqualObjects(record[SecCKRecordPCSPublicIdentity], publicIdentity, "public identity sent to cloudkit");
458 - (void)testPCSUnencryptedFieldsModify {
459 [self createAndSaveFakeKeyHierarchy: self.keychainZoneID]; // Make life easy for this test.
461 [self startCKKSSubsystem];
462 [self.keychainView waitForKeyHierarchyReadiness];
464 NSNumber* servIdentifier = @3;
465 NSData* publicKey = [@"asdfasdf" dataUsingEncoding:NSUTF8StringEncoding];
466 NSData* publicIdentity = [@"somedata" dataUsingEncoding:NSUTF8StringEncoding];
468 [self expectCKModifyItemRecords: 1 currentKeyPointerRecords: 1 zoneID:self.keychainZoneID
469 checkItem: [self checkPCSFieldsBlock:self.keychainZoneID
470 PCSServiceIdentifier:(NSNumber *)servIdentifier
471 PCSPublicKey:publicKey
472 PCSPublicIdentity:publicIdentity]];
474 NSMutableDictionary* query = [@{
475 (id)kSecClass : (id)kSecClassGenericPassword,
476 (id)kSecAttrAccessGroup : @"com.apple.security.ckks",
477 (id)kSecAttrAccessible: (id)kSecAttrAccessibleAfterFirstUnlock,
478 (id)kSecAttrAccount : @"testaccount",
479 (id)kSecAttrSynchronizable : (id)kCFBooleanTrue,
480 (id)kSecValueData : (id) [@"asdf" dataUsingEncoding:NSUTF8StringEncoding],
481 (id)kSecAttrDeriveSyncIDFromItemAttributes : (id)kCFBooleanTrue,
482 (id)kSecAttrPCSPlaintextServiceIdentifier : servIdentifier,
483 (id)kSecAttrPCSPlaintextPublicKey : publicKey,
484 (id)kSecAttrPCSPlaintextPublicIdentity : publicIdentity,
485 (id)kSecAttrSyncViewHint : self.keychainView.zoneName, // allows a CKKSScanOperation to find this item
488 XCTAssertEqual(errSecSuccess, SecItemAdd((__bridge CFDictionaryRef) query, NULL), @"SecItemAdd succeeded");
490 OCMVerifyAllWithDelay(self.mockDatabase, 20);
491 [self waitForCKModifications];
493 query[(id)kSecValueData] = nil;
494 query[(id)kSecAttrPCSPlaintextServiceIdentifier] = nil;
495 query[(id)kSecAttrPCSPlaintextPublicKey] = nil;
496 query[(id)kSecAttrPCSPlaintextPublicIdentity] = nil;
499 publicKey = [@"new public key" dataUsingEncoding:NSUTF8StringEncoding];
501 NSNumber* newServiceIdentifier = @10;
502 NSData* newPublicKey = [@"new public key" dataUsingEncoding:NSUTF8StringEncoding];
503 NSData* newPublicIdentity = [@"new public identity" dataUsingEncoding:NSUTF8StringEncoding];
505 NSDictionary* update = @{
506 (id)kSecAttrPCSPlaintextServiceIdentifier : newServiceIdentifier,
507 (id)kSecAttrPCSPlaintextPublicKey : newPublicKey,
508 (id)kSecAttrPCSPlaintextPublicIdentity : newPublicIdentity,
511 [self expectCKModifyItemRecords: 1 currentKeyPointerRecords: 1 zoneID:self.keychainZoneID
512 checkItem: [self checkPCSFieldsBlock:self.keychainZoneID
513 PCSServiceIdentifier:(NSNumber *)newServiceIdentifier
514 PCSPublicKey:newPublicKey
515 PCSPublicIdentity:newPublicIdentity]];
517 XCTAssertEqual(errSecSuccess, SecItemUpdate((__bridge CFDictionaryRef) query, (__bridge CFDictionaryRef) update), @"SecItemUpdate succeeded");
518 OCMVerifyAllWithDelay(self.mockDatabase, 20);
520 CFTypeRef item = NULL;
521 query[(id)kSecValueData] = nil;
522 query[(id)kSecReturnAttributes] = @YES;
523 XCTAssertEqual(errSecSuccess, SecItemCopyMatching((__bridge CFDictionaryRef) query, &item), "item should still exist");
525 NSDictionary* itemAttributes = (NSDictionary*) CFBridgingRelease(item);
526 XCTAssertEqualObjects(itemAttributes[(id)kSecAttrPCSPlaintextServiceIdentifier], newServiceIdentifier, "Service Identifier exists");
527 XCTAssertEqualObjects(itemAttributes[(id)kSecAttrPCSPlaintextPublicKey], newPublicKey, "public key exists");
528 XCTAssertEqualObjects(itemAttributes[(id)kSecAttrPCSPlaintextPublicIdentity], newPublicIdentity, "public identity exists");
530 // Find the item record in CloudKit. Since we're using kSecAttrDeriveSyncIDFromItemAttributes,
531 // the record ID is likely 50184A35-4480-E8BA-769B-567CF72F1EC0
532 [self waitForCKModifications];
533 CKRecordID* recordID = [[CKRecordID alloc] initWithRecordName: @"50184A35-4480-E8BA-769B-567CF72F1EC0" zoneID:self.keychainZoneID];
534 CKRecord* record = self.keychainZone.currentDatabase[recordID];
535 XCTAssertNotNil(record, "Found record in CloudKit at expected UUID");
537 XCTAssertEqualObjects(record[SecCKRecordPCSServiceIdentifier], newServiceIdentifier, "Service identifier sent to cloudkit");
538 XCTAssertEqualObjects(record[SecCKRecordPCSPublicKey], newPublicKey, "public key sent to cloudkit");
539 XCTAssertEqualObjects(record[SecCKRecordPCSPublicIdentity], newPublicIdentity, "public identity sent to cloudkit");
542 // As of [<rdar://problem/32558310> CKKS: Re-authenticate PCSPublicFields], these fields are NOT server-modifiable. This test proves it.
543 - (void)testPCSUnencryptedFieldsServerModifyFail {
544 [self createAndSaveFakeKeyHierarchy: self.keychainZoneID]; // Make life easy for this test.
546 [self startCKKSSubsystem];
547 [self.keychainView waitForKeyHierarchyReadiness];
549 NSNumber* servIdentifier = @3;
550 NSData* publicKey = [@"asdfasdf" dataUsingEncoding:NSUTF8StringEncoding];
551 NSData* publicIdentity = [@"somedata" dataUsingEncoding:NSUTF8StringEncoding];
553 [self expectCKModifyItemRecords: 1 currentKeyPointerRecords: 1 zoneID:self.keychainZoneID
554 checkItem: [self checkPCSFieldsBlock:self.keychainZoneID
555 PCSServiceIdentifier:(NSNumber *)servIdentifier
556 PCSPublicKey:publicKey
557 PCSPublicIdentity:publicIdentity]];
559 NSMutableDictionary* query = [@{
560 (id)kSecClass : (id)kSecClassGenericPassword,
561 (id)kSecAttrAccessGroup : @"com.apple.security.ckks",
562 (id)kSecAttrAccessible: (id)kSecAttrAccessibleAfterFirstUnlock,
563 (id)kSecAttrAccount : @"testaccount",
564 (id)kSecAttrSynchronizable : (id)kCFBooleanTrue,
565 (id)kSecValueData : (id) [@"asdf" dataUsingEncoding:NSUTF8StringEncoding],
566 (id)kSecAttrDeriveSyncIDFromItemAttributes : (id)kCFBooleanTrue,
567 (id)kSecAttrPCSPlaintextServiceIdentifier : servIdentifier,
568 (id)kSecAttrPCSPlaintextPublicKey : publicKey,
569 (id)kSecAttrPCSPlaintextPublicIdentity : publicIdentity,
570 (id)kSecAttrSyncViewHint : self.keychainView.zoneName, // fake, for CKKSScanOperation
573 XCTAssertEqual(errSecSuccess, SecItemAdd((__bridge CFDictionaryRef) query, NULL), @"SecItemAdd succeeded");
575 OCMVerifyAllWithDelay(self.mockDatabase, 20);
576 [self waitForCKModifications];
578 // Find the item record in CloudKit. Since we're using kSecAttrDeriveSyncIDFromItemAttributes,
579 // the record ID is likely 50184A35-4480-E8BA-769B-567CF72F1EC0
580 CKRecordID* recordID = [[CKRecordID alloc] initWithRecordName: @"50184A35-4480-E8BA-769B-567CF72F1EC0" zoneID:self.keychainZoneID];
581 CKRecord* record = self.keychainZone.currentDatabase[recordID];
582 XCTAssertNotNil(record, "Found record in CloudKit at expected UUID");
584 // Items are encrypted using encv2
585 XCTAssertEqualObjects(record[SecCKRecordEncryptionVersionKey], [NSNumber numberWithInteger:(int) CKKSItemEncryptionVersion2], "Uploaded using encv2");
588 // Test has already failed; find the record just to be nice.
589 for(CKRecord* maybe in self.keychainZone.currentDatabase.allValues) {
590 if(maybe[SecCKRecordPCSServiceIdentifier] != nil) {
596 NSNumber* newServiceIdentifier = @10;
597 NSData* newPublicKey = [@"new public key" dataUsingEncoding:NSUTF8StringEncoding];
598 NSData* newPublicIdentity = [@"new public identity" dataUsingEncoding:NSUTF8StringEncoding];
600 // Change the public key and public identity
601 record = [record copyWithZone: nil];
602 record[SecCKRecordPCSServiceIdentifier] = newServiceIdentifier;
603 record[SecCKRecordPCSPublicKey] = newPublicKey;
604 record[SecCKRecordPCSPublicIdentity] = newPublicIdentity;
605 [self.keychainZone addToZone: record];
607 // Trigger a notification
608 [self.keychainView notifyZoneChange:nil];
609 [self.keychainView waitForFetchAndIncomingQueueProcessing];
611 CFTypeRef item = NULL;
612 query[(id)kSecValueData] = nil;
613 query[(id)kSecAttrPCSPlaintextServiceIdentifier] = nil;
614 query[(id)kSecAttrPCSPlaintextPublicKey] = nil;
615 query[(id)kSecAttrPCSPlaintextPublicIdentity] = nil;
616 query[(id)kSecReturnAttributes] = @YES;
617 XCTAssertEqual(errSecSuccess, SecItemCopyMatching((__bridge CFDictionaryRef) query, &item), "item should still exist");
619 NSDictionary* itemAttributes = (NSDictionary*) CFBridgingRelease(item);
620 XCTAssertEqualObjects(itemAttributes[(id)kSecAttrPCSPlaintextServiceIdentifier], servIdentifier, "service identifier is not updated");
621 XCTAssertEqualObjects(itemAttributes[(id)kSecAttrPCSPlaintextPublicKey], publicKey, "public key not updated");
622 XCTAssertEqualObjects(itemAttributes[(id)kSecAttrPCSPlaintextPublicIdentity], publicIdentity, "public identity not updated");
625 -(void)testPCSUnencryptedFieldsRecieveUnauthenticatedFields {
626 [self createAndSaveFakeKeyHierarchy: self.keychainZoneID]; // Make life easy for this test.
628 [self startCKKSSubsystem];
629 [self.keychainView waitForKeyHierarchyReadiness];
631 NSNumber* servIdentifier = @3;
632 NSData* publicKey = [@"asdfasdf" dataUsingEncoding:NSUTF8StringEncoding];
633 NSData* publicIdentity = [@"somedata" dataUsingEncoding:NSUTF8StringEncoding];
635 NSError* error = nil;
637 // Manually encrypt an item
638 NSString* recordName = @"7B598D31-F9C5-481E-98AC-5A507ACB2D85";
639 CKRecordID* recordID = [[CKRecordID alloc] initWithRecordName:recordName zoneID:self.keychainZoneID];
640 NSDictionary* item = [self fakeRecordDictionary: @"account-delete-me" zoneID:self.keychainZoneID];
641 CKKSItem* cipheritem = [[CKKSItem alloc] initWithUUID:recordID.recordName
642 parentKeyUUID:self.keychainZoneKeys.classC.uuid
643 zoneID:recordID.zoneID];
644 CKKSKey* itemkey = [CKKSKey randomKeyWrappedByParent: self.keychainZoneKeys.classC error:&error];
645 XCTAssertNotNil(itemkey, "Got a key");
646 cipheritem.wrappedkey = itemkey.wrappedkey;
647 XCTAssertNotNil(cipheritem.wrappedkey, "Got a wrapped key");
649 cipheritem.encver = CKKSItemEncryptionVersion1;
651 // This item has the PCS public fields, but they are not authenticated
652 cipheritem.plaintextPCSServiceIdentifier = servIdentifier;
653 cipheritem.plaintextPCSPublicKey = publicKey;
654 cipheritem.plaintextPCSPublicIdentity = publicIdentity;
656 NSDictionary<NSString*, NSData*>* authenticatedData = [cipheritem makeAuthenticatedDataDictionaryUpdatingCKKSItem: nil encryptionVersion:CKKSItemEncryptionVersion1];
657 cipheritem.encitem = [CKKSItemEncrypter encryptDictionary:item key:itemkey.aessivkey authenticatedData:authenticatedData error:&error];
658 XCTAssertNil(error, "no error encrypting object");
659 XCTAssertNotNil(cipheritem.encitem, "Recieved ciphertext");
661 [self.keychainZone addToZone:[cipheritem CKRecordWithZoneID: recordID.zoneID]];
663 [self.keychainView waitForFetchAndIncomingQueueProcessing];
665 NSDictionary* query = @{(id)kSecClass: (id)kSecClassGenericPassword,
666 (id)kSecReturnAttributes: @YES,
667 (id)kSecAttrSynchronizable: @YES,
668 (id)kSecAttrAccount: @"account-delete-me",
669 (id)kSecMatchLimit: (id)kSecMatchLimitOne,
671 CFTypeRef cfresult = NULL;
672 XCTAssertEqual(errSecSuccess, SecItemCopyMatching((__bridge CFDictionaryRef) query, &cfresult), "Found synced item");
674 NSDictionary* result = CFBridgingRelease(cfresult);
675 XCTAssertEqualObjects(result[(id)kSecAttrPCSPlaintextServiceIdentifier], servIdentifier, "Received PCS service identifier");
676 XCTAssertEqualObjects(result[(id)kSecAttrPCSPlaintextPublicKey], publicKey, "Received PCS public key");
677 XCTAssertEqualObjects(result[(id)kSecAttrPCSPlaintextPublicIdentity], publicIdentity, "Received PCS public identity");
680 -(void)testPCSUnencryptedFieldsRecieveAuthenticatedFields {
681 [self createAndSaveFakeKeyHierarchy: self.keychainZoneID]; // Make life easy for this test.
683 [self startCKKSSubsystem];
684 [self.keychainView waitForKeyHierarchyReadiness];
685 [self.keychainView waitForOperationsOfClass:[CKKSIncomingQueueOperation class]];
687 NSNumber* servIdentifier = @3;
688 NSData* publicKey = [@"asdfasdf" dataUsingEncoding:NSUTF8StringEncoding];
689 NSData* publicIdentity = [@"somedata" dataUsingEncoding:NSUTF8StringEncoding];
691 NSError* error = nil;
693 // Manually encrypt an item
694 NSString* recordName = @"7B598D31-F9C5-481E-98AC-5A507ACB2D85";
695 CKRecordID* recordID = [[CKRecordID alloc] initWithRecordName:recordName zoneID:self.keychainZoneID];
696 NSDictionary* item = [self fakeRecordDictionary: @"account-delete-me" zoneID:self.keychainZoneID];
697 CKKSItem* cipheritem = [[CKKSItem alloc] initWithUUID:recordID.recordName
698 parentKeyUUID:self.keychainZoneKeys.classC.uuid
699 zoneID:recordID.zoneID];
700 CKKSKey* itemkey = [CKKSKey randomKeyWrappedByParent: self.keychainZoneKeys.classC error:&error];
701 XCTAssertNotNil(itemkey, "Got a key");
702 cipheritem.wrappedkey = itemkey.wrappedkey;
703 XCTAssertNotNil(cipheritem.wrappedkey, "Got a wrapped key");
705 cipheritem.encver = CKKSItemEncryptionVersion2;
707 // This item has the PCS public fields, and they are authenticated (since we're using v2)
708 cipheritem.plaintextPCSServiceIdentifier = servIdentifier;
709 cipheritem.plaintextPCSPublicKey = publicKey;
710 cipheritem.plaintextPCSPublicIdentity = publicIdentity;
712 // Use version 2, so PCS plaintext fields will be authenticated
713 NSMutableDictionary<NSString*, NSData*>* authenticatedData = [[cipheritem makeAuthenticatedDataDictionaryUpdatingCKKSItem: nil encryptionVersion:CKKSItemEncryptionVersion2] mutableCopy];
715 cipheritem.encitem = [CKKSItemEncrypter encryptDictionary:item key:itemkey.aessivkey authenticatedData:authenticatedData error:&error];
716 XCTAssertNil(error, "no error encrypting object");
717 XCTAssertNotNil(cipheritem.encitem, "Recieved ciphertext");
719 [self.keychainZone addToZone:[cipheritem CKRecordWithZoneID: recordID.zoneID]];
721 [self.keychainView waitForFetchAndIncomingQueueProcessing];
723 NSDictionary* query = @{(id)kSecClass: (id)kSecClassGenericPassword,
724 (id)kSecReturnAttributes: @YES,
725 (id)kSecAttrSynchronizable: @YES,
726 (id)kSecAttrAccount: @"account-delete-me",
727 (id)kSecMatchLimit: (id)kSecMatchLimitOne,
729 CFTypeRef cfresult = NULL;
730 XCTAssertEqual(errSecSuccess, SecItemCopyMatching((__bridge CFDictionaryRef) query, &cfresult), "Found synced item");
732 NSDictionary* result = CFBridgingRelease(cfresult);
733 XCTAssertEqualObjects(result[(id)kSecAttrPCSPlaintextServiceIdentifier], servIdentifier, "Received PCS service identifier");
734 XCTAssertEqualObjects(result[(id)kSecAttrPCSPlaintextPublicKey], publicKey, "Received PCS public key");
735 XCTAssertEqualObjects(result[(id)kSecAttrPCSPlaintextPublicIdentity], publicIdentity, "Received PCS public identity");
737 // Test that if this item is updated, it remains encrypted in v2
738 [self expectCKModifyItemRecords: 1 currentKeyPointerRecords: 1 zoneID:self.keychainZoneID
739 checkItem: [self checkPCSFieldsBlock:self.keychainZoneID
740 PCSServiceIdentifier:(NSNumber *)servIdentifier
741 PCSPublicKey:publicKey
742 PCSPublicIdentity:publicIdentity]];
743 [self updateGenericPassword:@"different password" account:@"account-delete-me"];
745 OCMVerifyAllWithDelay(self.mockDatabase, 20);
746 [self waitForCKModifications];
748 CKRecord* newRecord = self.keychainZone.currentDatabase[recordID];
749 XCTAssertEqualObjects(newRecord[SecCKRecordPCSServiceIdentifier], servIdentifier, "Didn't change service identifier");
750 XCTAssertEqualObjects(newRecord[SecCKRecordPCSPublicKey], publicKey, "Didn't change public key");
751 XCTAssertEqualObjects(newRecord[SecCKRecordPCSPublicIdentity], publicIdentity, "Didn't change public identity");
752 XCTAssertEqualObjects(newRecord[SecCKRecordEncryptionVersionKey], [NSNumber numberWithInteger:(int) CKKSItemEncryptionVersion2], "Uploaded using encv2");
755 -(void)testResetLocal {
756 // Test starts with nothing in database, but one in our fake CloudKit.
757 [self putFakeKeyHierarchyInCloudKit:self.keychainZoneID];
758 [self expectCKKSTLKSelfShareUpload:self.keychainZoneID];
759 [self saveTLKMaterialToKeychainSimulatingSOS:self.keychainZoneID];
761 // Spin up CKKS subsystem.
762 [self startCKKSSubsystem];
764 // We expect a single record to be uploaded
765 [self expectCKModifyItemRecords: 1 currentKeyPointerRecords: 1 zoneID:self.keychainZoneID checkItem: [self checkClassCBlock:self.keychainZoneID message:@"Object was encrypted under class C key in hierarchy"]];
766 [self addGenericPassword: @"data" account: @"account-delete-me"];
767 OCMVerifyAllWithDelay(self.mockDatabase, 20);
769 // After the local reset, we expect: a fetch, then nothing
770 self.silentFetchesAllowed = false;
771 [self expectCKFetch];
773 XCTestExpectation* resetExpectation = [self expectationWithDescription: @"local reset callback occurs"];
774 [self.injectedManager rpcResetLocal:nil reply:^(NSError* result) {
775 XCTAssertNil(result, "no error resetting local");
776 [resetExpectation fulfill];
778 [self waitForExpectations:@[resetExpectation] timeout:20];
780 OCMVerifyAllWithDelay(self.mockDatabase, 20);
782 [self expectCKModifyItemRecords: 1 currentKeyPointerRecords: 1 zoneID:self.keychainZoneID checkItem: [self checkClassABlock:self.keychainZoneID message:@"Object was encrypted under class A key in hierarchy"]];
783 [self addGenericPassword:@"asdf"
784 account:@"account-class-A"
786 access:(id)kSecAttrAccessibleWhenUnlocked
787 expecting:errSecSuccess
788 message:@"Adding class A item"];
789 OCMVerifyAllWithDelay(self.mockDatabase, 20);
792 -(void)testResetLocalWhileUntrusted {
793 // We're "logged in to" cloudkit but not in circle.
794 self.mockSOSAdapter.circleStatus = kSOSCCNotInCircle;
795 [self.accountStateTracker notifyCircleStatusChangeAndWaitForSignal];
796 self.silentFetchesAllowed = false;
798 // Test starts with local TLK and key hierarchy in our fake cloudkit
799 [self putFakeKeyHierarchyInCloudKit:self.keychainZoneID];
800 [self saveTLKMaterialToKeychainSimulatingSOS:self.keychainZoneID];
802 // Spin up CKKS subsystem.
803 [self startCKKSSubsystem];
805 XCTAssertEqual(0, [self.keychainView.loggedIn wait:500*NSEC_PER_MSEC], "Should have been told of a 'login' event on startup");
807 NSData* changeTokenData = [[[NSUUID UUID] UUIDString] dataUsingEncoding:NSUTF8StringEncoding];
808 CKServerChangeToken* changeToken = [[CKServerChangeToken alloc] initWithData:changeTokenData];
809 [self.keychainView dispatchSync: ^bool{
810 CKKSZoneStateEntry* ckse = [CKKSZoneStateEntry state:self.keychainView.zoneName];
811 ckse.changeToken = changeToken;
813 NSError* error = nil;
814 [ckse saveToDatabase:&error];
815 XCTAssertNil(error, "No error saving new zone state to database");
819 // after the reset, CKKS should refetch what's available
820 [self expectCKFetch];
822 XCTestExpectation* resetExpectation = [self expectationWithDescription: @"local reset callback occurs"];
823 [self.injectedManager rpcResetLocal:nil reply:^(NSError* result) {
824 XCTAssertNil(result, "no error resetting local");
825 secnotice("ckks", "Received a rpcResetLocal callback");
827 [self.keychainView dispatchSync: ^bool{
828 CKKSZoneStateEntry* ckse = [CKKSZoneStateEntry state:self.keychainView.zoneName];
829 XCTAssertNotEqualObjects(changeToken, ckse.changeToken, "Change token is reset");
833 [resetExpectation fulfill];
836 [self waitForExpectations:@[resetExpectation] timeout:20];
838 XCTAssertEqual(0, [self.keychainView.keyHierarchyConditions[SecCKKSZoneKeyStateWaitForTrust] wait:20*NSEC_PER_SEC], @"Key state should arrive at 'waitfortrust''");
840 // Now regain trust, and see what happens! It should use the existing fetch, pick up the old key hierarchy, and use it
841 [self expectCKKSTLKSelfShareUpload:self.keychainZoneID];
843 self.mockSOSAdapter.circleStatus = kSOSCCInCircle;
844 [self.accountStateTracker notifyCircleStatusChangeAndWaitForSignal];
845 [self beginSOSTrustedViewOperation:self.keychainView];
847 [self expectCKModifyItemRecords: 1 currentKeyPointerRecords: 1 zoneID:self.keychainZoneID checkItem:[self checkClassABlock:self.keychainZoneID message:@"Object was encrypted under class A key in hierarchy"]];
848 [self addGenericPassword:@"asdf"
849 account:@"account-class-A"
851 access:(id)kSecAttrAccessibleWhenUnlocked
852 expecting:errSecSuccess
853 message:@"Adding class A item"];
854 OCMVerifyAllWithDelay(self.mockDatabase, 20);
857 -(void)testResetLocalMultipleTimes {
858 // Test starts with nothing in database, but one in our fake CloudKit.
859 [self putFakeKeyHierarchyInCloudKit:self.keychainZoneID];
860 [self expectCKKSTLKSelfShareUpload:self.keychainZoneID];
861 [self saveTLKMaterialToKeychainSimulatingSOS:self.keychainZoneID];
863 // Spin up CKKS subsystem.
864 [self startCKKSSubsystem];
866 // We expect a single record to be uploaded
867 XCTAssertEqual(0, [self.keychainView.keyHierarchyConditions[SecCKKSZoneKeyStateReady] wait:20*NSEC_PER_SEC], "CKKS entered 'ready'");
868 [self expectCKModifyItemRecords:1 currentKeyPointerRecords:1 zoneID:self.keychainZoneID
869 checkItem:[self checkClassCBlock:self.keychainZoneID message:@"Object was encrypted under class C key in hierarchy"]];
870 [self addGenericPassword: @"data" account: @"account-delete-me"];
871 OCMVerifyAllWithDelay(self.mockDatabase, 20);
872 [self waitForCKModifications];
874 // We're going to request a bunch of CloudKit resets, but hold them from finishing
875 [self holdCloudKitFetches];
877 XCTestExpectation* resetExpectation0 = [self expectationWithDescription: @"reset callback(0) occurs"];
878 XCTestExpectation* resetExpectation1 = [self expectationWithDescription: @"reset callback(1) occurs"];
879 XCTestExpectation* resetExpectation2 = [self expectationWithDescription: @"reset callback(2) occurs"];
880 [self.injectedManager rpcResetLocal:nil reply:^(NSError* result) {
881 XCTAssertNil(result, "should receive no error resetting local");
882 secnotice("ckksreset", "Received a rpcResetLocal(0) callback");
883 [resetExpectation0 fulfill];
885 [self.injectedManager rpcResetLocal:nil reply:^(NSError* result) {
886 XCTAssertNil(result, "should receive no error resetting local");
887 secnotice("ckksreset", "Received a rpcResetLocal(1) callback");
888 [resetExpectation1 fulfill];
890 [self.injectedManager rpcResetLocal:nil reply:^(NSError* result) {
891 XCTAssertNil(result, "should receive no error resetting local");
892 secnotice("ckksreset", "Received a rpcResetLocal(2) callback");
893 [resetExpectation2 fulfill];
896 // After the reset(s), we expect no uploads. Let the resets flow!
897 [self releaseCloudKitFetchHold];
898 [self waitForExpectations:@[resetExpectation0, resetExpectation1, resetExpectation2] timeout:20];
899 XCTAssertEqual(0, [self.keychainView.keyHierarchyConditions[SecCKKSZoneKeyStateReady] wait:20*NSEC_PER_SEC], "CKKS entered 'ready'");
901 OCMVerifyAllWithDelay(self.mockDatabase, 20);
903 [self expectCKModifyItemRecords:1 currentKeyPointerRecords:1 zoneID:self.keychainZoneID
904 checkItem:[self checkClassABlock:self.keychainZoneID message:@"Object was encrypted under class A key in hierarchy"]];
905 [self addGenericPassword:@"asdf"
906 account:@"account-class-A"
908 access:(id)kSecAttrAccessibleWhenUnlocked
909 expecting:errSecSuccess
910 message:@"Adding class A item"];
911 OCMVerifyAllWithDelay(self.mockDatabase, 20);
914 -(void)testResetCloudKitZone {
915 self.suggestTLKUpload = OCMClassMock([CKKSNearFutureScheduler class]);
916 OCMExpect([self.suggestTLKUpload trigger]);
918 self.silentZoneDeletesAllowed = true;
920 // Test starts with nothing in database, but one in our fake CloudKit.
921 [self putFakeKeyHierarchyInCloudKit:self.keychainZoneID];
922 [self expectCKKSTLKSelfShareUpload:self.keychainZoneID];
923 [self saveTLKMaterialToKeychainSimulatingSOS:self.keychainZoneID];
925 // Spin up CKKS subsystem.
926 [self startCKKSSubsystem];
928 // We expect a single record to be uploaded
929 [self expectCKModifyItemRecords: 1 currentKeyPointerRecords: 1 zoneID:self.keychainZoneID checkItem: [self checkClassCBlock:self.keychainZoneID message:@"Object was encrypted under class C key in hierarchy"]];
930 [self addGenericPassword: @"data" account: @"account-delete-me"];
931 OCMVerifyAllWithDelay(self.mockDatabase, 20);
932 [self waitForCKModifications];
934 // During the reset, Octagon will upload the key hierarchy, and then CKKS will upload the class C item
935 [self expectCKModifyItemRecords: 1 currentKeyPointerRecords: 1 zoneID:self.keychainZoneID checkItem: [self checkClassCBlock:self.keychainZoneID message:@"Object was encrypted under class C key in hierarchy"]];
937 XCTestExpectation* resetExpectation = [self expectationWithDescription: @"reset callback occurs"];
938 [self.injectedManager rpcResetCloudKit:nil reason:@"reset-test" reply:^(NSError* result) {
939 XCTAssertNil(result, "no error resetting cloudkit");
940 secnotice("ckks", "Received a resetCloudKit callback");
941 [resetExpectation fulfill];
944 // Sneak in and perform Octagon's duties
945 OCMVerifyAllWithDelay(self.suggestTLKUpload, 10);
946 [self performOctagonTLKUpload:self.ckksViews];
948 [self waitForExpectations:@[resetExpectation] timeout:20];
950 OCMVerifyAllWithDelay(self.mockDatabase, 20);
952 [self expectCKModifyItemRecords: 1 currentKeyPointerRecords: 1 zoneID:self.keychainZoneID checkItem: [self checkClassABlock:self.keychainZoneID message:@"Object was encrypted under class A key in hierarchy"]];
953 [self addGenericPassword:@"asdf"
954 account:@"account-class-A"
956 access:(id)kSecAttrAccessibleWhenUnlocked
957 expecting:errSecSuccess
958 message:@"Adding class A item"];
959 OCMVerifyAllWithDelay(self.mockDatabase, 20);
962 - (void)testResetCloudKitZoneCloudKitRejects {
963 self.suggestTLKUpload = OCMClassMock([CKKSNearFutureScheduler class]);
964 OCMExpect([self.suggestTLKUpload trigger]);
966 self.nextModifyRecordZonesError = [[CKPrettyError alloc] initWithDomain:CKErrorDomain
969 CKErrorRetryAfterKey: @(0.2),
970 NSUnderlyingErrorKey: [[CKPrettyError alloc] initWithDomain:CKErrorDomain
974 self.silentZoneDeletesAllowed = true;
976 // Test starts with nothing in database, but one in our fake CloudKit.
977 [self putFakeKeyHierarchyInCloudKit:self.keychainZoneID];
978 [self expectCKKSTLKSelfShareUpload:self.keychainZoneID];
979 [self saveTLKMaterialToKeychainSimulatingSOS:self.keychainZoneID];
981 // Spin up CKKS subsystem.
982 [self startCKKSSubsystem];
984 // We expect a single record to be uploaded
985 [self expectCKModifyItemRecords: 1 currentKeyPointerRecords: 1 zoneID:self.keychainZoneID checkItem: [self checkClassCBlock:self.keychainZoneID message:@"Object was encrypted under class C key in hierarchy"]];
986 [self addGenericPassword: @"data" account: @"account-delete-me"];
987 OCMVerifyAllWithDelay(self.mockDatabase, 20);
988 [self waitForCKModifications];
990 // During the reset, Octagon will upload the key hierarchy, and then CKKS will upload the class C item
991 [self expectCKModifyItemRecords: 1 currentKeyPointerRecords: 1 zoneID:self.keychainZoneID checkItem: [self checkClassCBlock:self.keychainZoneID message:@"Object was encrypted under class C key in hierarchy"]];
993 XCTestExpectation* resetExpectation = [self expectationWithDescription: @"reset callback occurs"];
994 [self.injectedManager rpcResetCloudKit:nil reason:@"reset-test" reply:^(NSError* result) {
995 XCTAssertNil(result, "no error resetting cloudkit");
996 secnotice("ckks", "Received a resetCloudKit callback");
997 [resetExpectation fulfill];
1000 // Sneak in and perform Octagon's duties
1001 OCMVerifyAllWithDelay(self.suggestTLKUpload, 10);
1002 [self performOctagonTLKUpload:self.ckksViews];
1004 [self waitForExpectations:@[resetExpectation] timeout:20];
1006 OCMVerifyAllWithDelay(self.mockDatabase, 20);
1008 [self expectCKModifyItemRecords: 1 currentKeyPointerRecords: 1 zoneID:self.keychainZoneID checkItem: [self checkClassABlock:self.keychainZoneID message:@"Object was encrypted under class A key in hierarchy"]];
1009 [self addGenericPassword:@"asdf"
1010 account:@"account-class-A"
1012 access:(id)kSecAttrAccessibleWhenUnlocked
1013 expecting:errSecSuccess
1014 message:@"Adding class A item"];
1015 OCMVerifyAllWithDelay(self.mockDatabase, 20);
1017 XCTAssertNil(self.nextModifyRecordZonesError, "Record zone modification error should have been cleared");
1020 - (void)testResetCloudKitZoneDuringWaitForTLK {
1021 self.suggestTLKUpload = OCMClassMock([CKKSNearFutureScheduler class]);
1022 OCMExpect([self.suggestTLKUpload trigger]);
1024 self.silentZoneDeletesAllowed = true;
1026 // Test starts with nothing in database, but one in our fake CloudKit.
1028 [self putFakeKeyHierarchyInCloudKit:self.keychainZoneID];
1029 [self putFakeDeviceStatusInCloudKit:self.keychainZoneID];
1031 // Spin up CKKS subsystem.
1032 [self startCKKSSubsystem];
1034 // No records should be uploaded
1035 [self addGenericPassword: @"data" account: @"account-delete-me"];
1037 XCTAssertEqual(0, [self.keychainView.keyHierarchyConditions[SecCKKSZoneKeyStateWaitForTLK] wait:20*NSEC_PER_SEC], "CKKS should have entered waitfortlk");
1039 // Restart CKKS to really get in the spirit of waitfortlk (and get a pending processOutgoingQueue operation going)
1040 self.keychainView = [[CKKSViewManager manager] restartZone: self.keychainZoneID.zoneName];
1041 self.ckksViews = [NSMutableSet setWithObject:self.keychainView];
1043 [self beginSOSTrustedViewOperation:self.keychainView];
1044 XCTAssertEqual(0, [self.keychainView.keyHierarchyConditions[SecCKKSZoneKeyStateWaitForTLK] wait:20*NSEC_PER_SEC], "CKKS entered waitfortlk");
1046 CKKSOutgoingQueueOperation* outgoingOp = [self.keychainView processOutgoingQueue:nil];
1047 XCTAssertTrue([outgoingOp isPending], "outgoing queue processing should be on hold");
1049 // Now, reset everything. The outgoingOp should get cancelled.
1050 // We expect a key hierarchy upload, and then the class C item upload
1051 [self expectCKModifyItemRecords: 1 currentKeyPointerRecords: 1 zoneID:self.keychainZoneID
1052 checkItem: [self checkClassCBlock:self.keychainZoneID message:@"Object was encrypted under class C key in hierarchy"]];
1054 XCTestExpectation* resetExpectation = [self expectationWithDescription: @"reset callback occurs"];
1055 [self.injectedManager rpcResetCloudKit:nil reason:@"reset-test" reply:^(NSError* result) {
1056 XCTAssertNil(result, "no error resetting cloudkit");
1057 [resetExpectation fulfill];
1060 // Sneak in and perform Octagon's duties
1061 OCMVerifyAllWithDelay(self.suggestTLKUpload, 10);
1062 [self performOctagonTLKUpload:self.ckksViews];
1064 [self waitForExpectations:@[resetExpectation] timeout:20];
1066 XCTAssertTrue([outgoingOp isCancelled], "old stuck ProcessOutgoingQueue should be cancelled");
1067 OCMVerifyAllWithDelay(self.mockDatabase, 20);
1069 // And adding another item works too
1070 [self expectCKModifyItemRecords: 1 currentKeyPointerRecords: 1 zoneID:self.keychainZoneID checkItem: [self checkClassABlock:self.keychainZoneID message:@"Object was encrypted under class A key in hierarchy"]];
1071 [self addGenericPassword:@"asdf"
1072 account:@"account-class-A"
1074 access:(id)kSecAttrAccessibleWhenUnlocked
1075 expecting:errSecSuccess
1076 message:@"Adding class A item"];
1077 OCMVerifyAllWithDelay(self.mockDatabase, 20);
1081 * This test doesn't work, since the resetLocal fails. CKKS gets back into waitfortlk
1082 * but that isn't considered a successful resetLocal.
1084 - (void)testResetLocalDuringWaitForTLK {
1085 // Test starts with nothing in database, but one in our fake CloudKit.
1087 [self putFakeKeyHierarchyInCloudKit:self.keychainZoneID];
1089 // Spin up CKKS subsystem.
1090 [self startCKKSSubsystem];
1092 // No records should be uploaded
1093 [self addGenericPassword: @"data" account: @"account-delete-me"];
1095 XCTAssertEqual(0, [self.keychainView.keyHierarchyConditions[SecCKKSZoneKeyStateWaitForTLK] wait:20*NSEC_PER_SEC], "CKKS should have entered waitfortlk");
1097 // Restart CKKS to really get in the spirit of waitfortlk (and get a pending processOutgoingQueue operation going)
1098 self.keychainView = [[CKKSViewManager manager] restartZone: self.keychainZoneID.zoneName];
1099 [self beginSOSTrustedViewOperation:self.keychainView];
1100 XCTAssertEqual(0, [self.keychainView.keyHierarchyConditions[SecCKKSZoneKeyStateWaitForTLK] wait:20*NSEC_PER_SEC], "CKKS entered waitfortlk");
1102 CKKSOutgoingQueueOperation* outgoingOp = [self.keychainView processOutgoingQueue:nil];
1103 XCTAssertTrue([outgoingOp isPending], "outgoing queue processing should be on hold");
1105 // Now, reset everything. The outgoingOp should get cancelled.
1106 // We expect a key hierarchy upload, and then the class C item upload
1107 [self expectCKModifyKeyRecords: 3 currentKeyPointerRecords: 3 tlkShareRecords:3 zoneID:self.keychainZoneID];
1108 [self expectCKModifyItemRecords: 1 currentKeyPointerRecords: 1 zoneID:self.keychainZoneID
1109 checkItem: [self checkClassCBlock:self.keychainZoneID message:@"Object was encrypted under class C key in hierarchy"]];
1111 XCTestExpectation* resetExpectation = [self expectationWithDescription: @"reset callback occurs"];
1112 [self.injectedManager rpcResetLocal:nil reply:^(NSError* result) {
1113 XCTAssertNil(result, "no error resetting local");
1114 [resetExpectation fulfill];
1116 [self waitForExpectations:@[resetExpectation] timeout:20];
1118 XCTAssertTrue([outgoingOp isCancelled], "old stuck ProcessOutgoingQueue should be cancelled");
1119 OCMVerifyAllWithDelay(self.mockDatabase, 20);
1121 // And adding another item works too
1122 [self expectCKModifyItemRecords: 1 currentKeyPointerRecords: 1 zoneID:self.keychainZoneID checkItem: [self checkClassABlock:self.keychainZoneID message:@"Object was encrypted under class A key in hierarchy"]];
1123 [self addGenericPassword:@"asdf"
1124 account:@"account-class-A"
1126 access:(id)kSecAttrAccessibleWhenUnlocked
1127 expecting:errSecSuccess
1128 message:@"Adding class A item"];
1129 OCMVerifyAllWithDelay(self.mockDatabase, 20);
1132 -(void)testResetCloudKitZoneWhileUntrusted {
1133 self.silentZoneDeletesAllowed = true;
1135 // We're "logged in to" cloudkit but not in circle.
1136 self.mockSOSAdapter.circleStatus = kSOSCCNotInCircle;
1137 [self.accountStateTracker notifyCircleStatusChangeAndWaitForSignal];
1139 // Test starts with nothing in database, but one in our fake CloudKit.
1140 [self putFakeKeyHierarchyInCloudKit:self.keychainZoneID];
1142 // Spin up CKKS subsystem.
1143 [self startCKKSSubsystem];
1145 // Since CKKS is untrusted, it'll fetch the zone but then get stuck
1146 XCTAssertEqual(0, [self.keychainView.keyHierarchyConditions[SecCKKSZoneKeyStateWaitForTrust] wait:20*NSEC_PER_SEC], "CKKS entered 'waitfortrust'");
1148 CKRecord* ckr = [self createFakeRecord: self.keychainZoneID recordName:@"7B598D31-F9C5-481E-98AC-5A507ACB2D85"];
1149 [self.keychainZone addToZone: ckr];
1151 XCTAssertNotNil(self.keychainZone.currentDatabase, "Zone exists");
1152 XCTAssertNotNil(self.keychainZone.currentDatabase[ckr.recordID], "An item exists in the fake zone");
1154 XCTestExpectation* resetExpectation = [self expectationWithDescription: @"reset callback occurs"];
1155 [self.injectedManager rpcResetCloudKit:nil reason:@"reset-test" reply:^(NSError* result) {
1156 XCTAssertNil(result, "no error resetting cloudkit");
1157 secnotice("ckks", "Received a resetCloudKit callback");
1158 [resetExpectation fulfill];
1161 [self waitForExpectations:@[resetExpectation] timeout:20];
1163 XCTAssertNil(self.keychainZone.currentDatabase, "No zone anymore!");
1164 OCMVerifyAllWithDelay(self.mockDatabase, 20);
1166 // Now log in, and see what happens! It should create the zone again and upload a whole new key hierarchy
1167 self.silentFetchesAllowed = true;
1168 self.mockSOSAdapter.circleStatus = kSOSCCInCircle;
1169 [self.accountStateTracker notifyCircleStatusChangeAndWaitForSignal];
1170 [self beginSOSTrustedViewOperation:self.keychainView];
1172 [self performOctagonTLKUpload:self.ckksViews];
1174 OCMVerifyAllWithDelay(self.mockDatabase, 20);
1176 [self expectCKModifyItemRecords: 1 currentKeyPointerRecords: 1 zoneID:self.keychainZoneID checkItem:[self checkClassABlock:self.keychainZoneID message:@"Object was encrypted under class A key in hierarchy"]];
1177 [self addGenericPassword:@"asdf"
1178 account:@"account-class-A"
1180 access:(id)kSecAttrAccessibleWhenUnlocked
1181 expecting:errSecSuccess
1182 message:@"Adding class A item"];
1183 OCMVerifyAllWithDelay(self.mockDatabase, 20);
1186 - (void)testResetCloudKitZoneMultipleTimes {
1187 self.suggestTLKUpload = OCMClassMock([CKKSNearFutureScheduler class]);
1188 OCMExpect([self.suggestTLKUpload trigger]);
1190 self.silentZoneDeletesAllowed = true;
1192 // Test starts with nothing in database, but one in our fake CloudKit.
1193 [self putFakeKeyHierarchyInCloudKit:self.keychainZoneID];
1194 [self expectCKKSTLKSelfShareUpload:self.keychainZoneID];
1195 [self saveTLKMaterialToKeychainSimulatingSOS:self.keychainZoneID];
1197 // Spin up CKKS subsystem.
1198 [self startCKKSSubsystem];
1200 // We expect a single record to be uploaded
1201 XCTAssertEqual(0, [self.keychainView.keyHierarchyConditions[SecCKKSZoneKeyStateReady] wait:20*NSEC_PER_SEC], "CKKS entered 'ready'");
1202 [self expectCKModifyItemRecords:1 currentKeyPointerRecords:1 zoneID:self.keychainZoneID
1203 checkItem:[self checkClassCBlock:self.keychainZoneID message:@"Object was encrypted under class C key in hierarchy"]];
1204 [self addGenericPassword: @"data" account: @"account-delete-me"];
1205 OCMVerifyAllWithDelay(self.mockDatabase, 20);
1206 [self waitForCKModifications];
1208 // We're going to request a bunch of CloudKit resets, but hold them from finishing
1209 [self holdCloudKitFetches];
1211 XCTestExpectation* resetExpectation0 = [self expectationWithDescription: @"reset callback(0) occurs"];
1212 XCTestExpectation* resetExpectation1 = [self expectationWithDescription: @"reset callback(1) occurs"];
1213 XCTestExpectation* resetExpectation2 = [self expectationWithDescription: @"reset callback(2) occurs"];
1214 [self.injectedManager rpcResetCloudKit:nil reason:@"reset-test" reply:^(NSError* result) {
1215 XCTAssertNil(result, "should receive no error resetting cloudkit");
1216 secnotice("ckksreset", "Received a resetCloudKit(0) callback");
1217 [resetExpectation0 fulfill];
1219 [self.injectedManager rpcResetCloudKit:nil reason:@"reset-test" reply:^(NSError* result) {
1220 XCTAssertNil(result, "should receive no error resetting cloudkit");
1221 secnotice("ckksreset", "Received a resetCloudKit(1) callback");
1222 [resetExpectation1 fulfill];
1224 [self.injectedManager rpcResetCloudKit:nil reason:@"reset-test" reply:^(NSError* result) {
1225 XCTAssertNil(result, "should receive no error resetting cloudkit");
1226 secnotice("ckksreset", "Received a resetCloudKit(2) callback");
1227 [resetExpectation2 fulfill];
1230 // After the reset(s), we expect a key hierarchy upload, and then the class C item upload
1231 [self expectCKModifyItemRecords:1 currentKeyPointerRecords:1 zoneID:self.keychainZoneID
1232 checkItem:[self checkClassCBlock:self.keychainZoneID message:@"Object was encrypted under class C key in hierarchy"]];
1234 // And let the resets flow
1235 [self releaseCloudKitFetchHold];
1237 OCMVerifyAllWithDelay(self.suggestTLKUpload, 10);
1238 [self performOctagonTLKUpload:self.ckksViews];
1240 [self waitForExpectations:@[resetExpectation0, resetExpectation1, resetExpectation2] timeout:20];
1241 XCTAssertEqual(0, [self.keychainView.keyHierarchyConditions[SecCKKSZoneKeyStateReady] wait:20*NSEC_PER_SEC], "CKKS entered 'ready'");
1243 OCMVerifyAllWithDelay(self.mockDatabase, 20);
1245 [self expectCKModifyItemRecords:1 currentKeyPointerRecords:1 zoneID:self.keychainZoneID
1246 checkItem:[self checkClassABlock:self.keychainZoneID message:@"Object was encrypted under class A key in hierarchy"]];
1247 [self addGenericPassword:@"asdf"
1248 account:@"account-class-A"
1250 access:(id)kSecAttrAccessibleWhenUnlocked
1251 expecting:errSecSuccess
1252 message:@"Adding class A item"];
1253 OCMVerifyAllWithDelay(self.mockDatabase, 20);
1256 - (void)testRPCFetchAndProcessWhileCloudKitNotResponding {
1257 [self createAndSaveFakeKeyHierarchy: self.keychainZoneID]; // Make life easy for this test.
1258 [self startCKKSSubsystem];
1260 XCTAssertEqual(0, [self.keychainView.keyHierarchyConditions[SecCKKSZoneKeyStateReady] wait:20*NSEC_PER_SEC], "CKKS entered 'ready'");
1261 [self holdCloudKitFetches];
1263 XCTestExpectation* callbackOccurs = [self expectationWithDescription:@"callback-occurs"];
1264 [self.ckksControl rpcFetchAndProcessChanges:nil reply:^(NSError * _Nullable error) {
1265 // done! we should have an underlying error of "fetch isn't working"
1266 XCTAssertNotNil(error, "Should have received an error attempting to fetch and process");
1267 NSError* underlying = error.userInfo[NSUnderlyingErrorKey];
1268 XCTAssertNotNil(underlying, "Should have received an underlying error");
1269 XCTAssertEqualObjects(underlying.domain, CKKSResultDescriptionErrorDomain, "Underlying error should be CKKSResultDescriptionErrorDomain");
1270 XCTAssertEqual(underlying.code, CKKSResultDescriptionPendingSuccessfulFetch, "Underlying error should be 'pending fetch'");
1271 [callbackOccurs fulfill];
1274 [self waitForExpectations:@[callbackOccurs] timeout:20];
1275 [self releaseCloudKitFetchHold];
1276 OCMVerifyAllWithDelay(self.mockDatabase, 20);
1279 - (void)testRPCFetchAndProcessWhileCloudKitErroring {
1280 [self createAndSaveFakeKeyHierarchy: self.keychainZoneID]; // Make life easy for this test.
1281 [self startCKKSSubsystem];
1283 XCTAssertEqual(0, [self.keychainView.keyHierarchyConditions[SecCKKSZoneKeyStateReady] wait:20*NSEC_PER_SEC], "CKKS entered 'ready'");
1285 [self.keychainZone failNextFetchWith:[[CKPrettyError alloc] initWithDomain:CKErrorDomain
1286 code:CKErrorRequestRateLimited
1287 userInfo:@{CKErrorRetryAfterKey : [NSNumber numberWithInt:30]}]];
1289 XCTestExpectation* callbackOccurs = [self expectationWithDescription:@"callback-occurs"];
1290 [self.ckksControl rpcFetchAndProcessChanges:nil reply:^(NSError * _Nullable error) {
1291 // done! we should have an underlying error of "fetch isn't working"
1292 XCTAssertNotNil(error, "Should have received an error attempting to fetch and process");
1293 NSError* underlying = error.userInfo[NSUnderlyingErrorKey];
1294 XCTAssertNotNil(underlying, "Should have received an underlying error");
1295 XCTAssertEqualObjects(underlying.domain, CKKSResultDescriptionErrorDomain, "Underlying error should be CKKSResultDescriptionErrorDomain");
1296 XCTAssertEqual(underlying.code, CKKSResultDescriptionPendingSuccessfulFetch, "Underlying error should be 'pending fetch'");
1298 NSError* underunderlying = underlying.userInfo[NSUnderlyingErrorKey];
1299 XCTAssertNotNil(underunderlying, "Should have received another layer of underlying error");
1300 XCTAssertEqualObjects(underunderlying.domain, CKErrorDomain, "Underlying error should be CKErrorDomain");
1301 XCTAssertEqual(underunderlying.code, CKErrorRequestRateLimited, "Underlying error should be 'rate limited'");
1303 [callbackOccurs fulfill];
1306 [self waitForExpectations:@[callbackOccurs] timeout:20];
1307 OCMVerifyAllWithDelay(self.mockDatabase, 20);
1310 - (void)testRPCFetchAndProcessWhileInWaitForTLK {
1311 [self putFakeKeyHierarchyInCloudKit:self.keychainZoneID];
1312 [self putFakeDeviceStatusInCloudKit:self.keychainZoneID];
1313 [self startCKKSSubsystem];
1315 XCTAssertEqual(0, [self.keychainView.keyHierarchyConditions[SecCKKSZoneKeyStateWaitForTLK] wait:20*NSEC_PER_SEC], "CKKS entered waitfortlk");
1317 XCTestExpectation* callbackOccurs = [self expectationWithDescription:@"callback-occurs"];
1318 [self.ckksControl rpcFetchAndProcessChanges:nil reply:^(NSError * _Nullable error) {
1319 // done! we should have an underlying error of "fetch isn't working"
1320 XCTAssertNotNil(error, "Should have received an error attempting to fetch and process");
1321 NSError* underlying = error.userInfo[NSUnderlyingErrorKey];
1322 XCTAssertNotNil(underlying, "Should have received an underlying error");
1323 XCTAssertEqualObjects(underlying.domain, CKKSResultDescriptionErrorDomain, "Underlying error should be CKKSResultDescriptionErrorDomain");
1324 XCTAssertEqual(underlying.code, CKKSResultDescriptionPendingKeyReady, "Underlying error should be 'pending key ready'");
1325 [callbackOccurs fulfill];
1328 [self waitForExpectations:@[callbackOccurs] timeout:20];
1329 OCMVerifyAllWithDelay(self.mockDatabase, 20);
1332 - (void)testRPCTLKMissingWhenMissing {
1333 // Bring CKKS up in waitfortlk
1334 [self putFakeKeyHierarchyInCloudKit:self.keychainZoneID];
1335 [self putFakeDeviceStatusInCloudKit:self.keychainZoneID];
1336 [self startCKKSSubsystem];
1338 XCTAssertEqual(0, [self.keychainView.keyHierarchyConditions[SecCKKSZoneKeyStateWaitForTLK] wait:20*NSEC_PER_SEC], "CKKS entered waitfortlk");
1340 XCTestExpectation* callbackOccurs = [self expectationWithDescription:@"callback-occurs"];
1342 [self.ckksControl rpcTLKMissing:@"keychain" reply:^(bool missing) {
1343 XCTAssertTrue(missing, "TLKs should be missing");
1344 [callbackOccurs fulfill];
1347 [self waitForExpectations:@[callbackOccurs] timeout:20];
1349 OCMVerifyAllWithDelay(self.mockDatabase, 20);
1352 - (void)testRPCTLKMissingWhenFound {
1353 // Bring CKKS up in 'ready'
1354 [self putFakeKeyHierarchyInCloudKit:self.keychainZoneID];
1355 [self saveTLKMaterialToKeychain:self.keychainZoneID];
1356 [self expectCKKSTLKSelfShareUpload:self.keychainZoneID];
1357 [self startCKKSSubsystem];
1359 XCTAssertEqual(0, [self.keychainView.keyHierarchyConditions[SecCKKSZoneKeyStateReady] wait:20*NSEC_PER_SEC], "CKKS entered 'ready''");
1361 XCTestExpectation* callbackOccurs = [self expectationWithDescription:@"callback-occurs"];
1363 [self.ckksControl rpcTLKMissing:@"keychain" reply:^(bool missing) {
1364 XCTAssertFalse(missing, "TLKs should not be missing");
1365 [callbackOccurs fulfill];
1368 [self waitForExpectations:@[callbackOccurs] timeout:20];
1370 OCMVerifyAllWithDelay(self.mockDatabase, 20);
1373 - (void)testRPCKnownBadStateWhenTLKsMissing {
1374 // Bring CKKS up in waitfortlk
1375 [self putFakeKeyHierarchyInCloudKit:self.keychainZoneID];
1376 [self putFakeDeviceStatusInCloudKit:self.keychainZoneID];
1377 [self startCKKSSubsystem];
1379 XCTAssertEqual(0, [self.keychainView.keyHierarchyConditions[SecCKKSZoneKeyStateWaitForTLK] wait:20*NSEC_PER_SEC], "CKKS entered waitfortlk");
1381 XCTestExpectation* callbackOccurs = [self expectationWithDescription:@"callback-occurs"];
1383 [self.ckksControl rpcKnownBadState:@"keychain" reply:^(CKKSKnownBadState result) {
1384 XCTAssertEqual(result, CKKSKnownStateTLKsMissing, "TLKs should be missing");
1385 [callbackOccurs fulfill];
1388 [self waitForExpectations:@[callbackOccurs] timeout:20];
1390 OCMVerifyAllWithDelay(self.mockDatabase, 20);
1393 - (void)testRPCKnownBadStateWhenInWaitForUnlock {
1394 [self putFakeKeyHierarchyInCloudKit:self.keychainZoneID];
1395 [self saveTLKMaterialToKeychain:self.keychainZoneID];
1396 [self putFakeDeviceStatusInCloudKit:self.keychainZoneID];
1398 // Bring CKKS up in 'waitforunlock'
1399 self.aksLockState = true;
1400 [self.lockStateTracker recheck];
1401 [self startCKKSSubsystem];
1403 // Wait for the key hierarchy state machine to get stuck waiting for the unlock dependency. No uploads should occur.
1404 XCTAssertEqual(0, [self.keychainView.keyHierarchyConditions[SecCKKSZoneKeyStateWaitForUnlock] wait:20*NSEC_PER_SEC], @"Key state should get stuck in waitforunlock");
1406 XCTestExpectation* callbackOccurs = [self expectationWithDescription:@"callback-occurs"];
1408 [self.ckksControl rpcKnownBadState:@"keychain" reply:^(CKKSKnownBadState result) {
1409 XCTAssertEqual(result, CKKSKnownStateWaitForUnlock, "known state should be wait for unlock");
1410 [callbackOccurs fulfill];
1413 [self waitForExpectations:@[callbackOccurs] timeout:20];
1415 OCMVerifyAllWithDelay(self.mockDatabase, 20);
1418 - (void)testRPCKnownBadStateWhenInWaitForUpload {
1419 // Bring CKKS up in 'waitfortupload'
1420 self.aksLockState = true;
1421 [self.lockStateTracker recheck];
1422 [self startCKKSSubsystem];
1424 // Wait for the key hierarchy state machine to get stuck waiting for Octagon. No uploads should occur.
1425 XCTAssertEqual(0, [self.keychainView.keyHierarchyConditions[SecCKKSZoneKeyStateWaitForTLKCreation] wait:20*NSEC_PER_SEC], @"Key state should get stuck in waitfortlkcreation");
1427 XCTestExpectation* callbackOccurs = [self expectationWithDescription:@"callback-occurs"];
1429 [self.ckksControl rpcKnownBadState:@"keychain" reply:^(CKKSKnownBadState result) {
1430 XCTAssertEqual(result, CKKSKnownStateWaitForOctagon, "known state should be wait for Octagon");
1431 [callbackOccurs fulfill];
1434 [self waitForExpectations:@[callbackOccurs] timeout:20];
1436 OCMVerifyAllWithDelay(self.mockDatabase, 20);
1439 - (void)testRPCKnownBadStateWhenInGoodState {
1440 // Bring CKKS up in 'ready'
1441 [self putFakeKeyHierarchyInCloudKit:self.keychainZoneID];
1442 [self saveTLKMaterialToKeychain:self.keychainZoneID];
1443 [self expectCKKSTLKSelfShareUpload:self.keychainZoneID];
1444 [self startCKKSSubsystem];
1446 XCTAssertEqual(0, [self.keychainView.keyHierarchyConditions[SecCKKSZoneKeyStateReady] wait:20*NSEC_PER_SEC], "CKKS entered 'ready''");
1448 XCTestExpectation* callbackOccurs = [self expectationWithDescription:@"callback-occurs"];
1450 [self.ckksControl rpcKnownBadState:@"keychain" reply:^(CKKSKnownBadState result) {
1451 XCTAssertEqual(result, CKKSKnownStatePossiblyGood, "known state should not be possibly-good");
1452 [callbackOccurs fulfill];
1455 [self waitForExpectations:@[callbackOccurs] timeout:20];
1457 OCMVerifyAllWithDelay(self.mockDatabase, 20);
1460 - (void)testRpcStatus {
1461 [self createAndSaveFakeKeyHierarchy: self.keychainZoneID]; // Make life easy for this test.
1463 [self startCKKSSubsystem];
1465 // Let things shake themselves out.
1466 OCMVerifyAllWithDelay(self.mockDatabase, 20);
1467 XCTAssertEqual(0, [self.keychainView.keyHierarchyConditions[SecCKKSZoneKeyStateReady] wait:20*NSEC_PER_SEC], "Key state should return to 'ready'");
1468 [self waitForCKModifications];
1470 XCTestExpectation* callbackOccurs = [self expectationWithDescription:@"callback-occurs"];
1471 [self.ckksControl rpcStatus:@"keychain" reply:^(NSArray<NSDictionary*>* result, NSError* error) {
1472 XCTAssertNil(error, "should be no error fetching status for keychain");
1474 // Ugly "global" hack
1475 XCTAssertEqual(result.count, 2u, "Should have received two result dictionaries back");
1476 NSDictionary* keychainStatus = result[1];
1478 XCTAssertNotNil(keychainStatus, "Should have received at least one zone status back");
1479 XCTAssertEqualObjects(keychainStatus[@"view"], @"keychain", "Should have received status for the keychain view");
1480 XCTAssertEqualObjects(keychainStatus[@"keystate"], SecCKKSZoneKeyStateReady, "Should be in 'ready' status");
1481 XCTAssertNotNil(keychainStatus[@"ckmirror"], "Status should have any ckmirror");
1482 [callbackOccurs fulfill];
1485 [self waitForExpectations:@[callbackOccurs] timeout:20];
1488 - (void)testRpcFastStatus {
1489 [self createAndSaveFakeKeyHierarchy: self.keychainZoneID]; // Make life easy for this test.
1491 [self startCKKSSubsystem];
1493 // Let things shake themselves out.
1494 OCMVerifyAllWithDelay(self.mockDatabase, 20);
1495 XCTAssertEqual(0, [self.keychainView.keyHierarchyConditions[SecCKKSZoneKeyStateReady] wait:20*NSEC_PER_SEC], "Key state should return to 'ready'");
1496 [self waitForCKModifications];
1498 XCTestExpectation* callbackOccurs = [self expectationWithDescription:@"callback-occurs"];
1499 [self.ckksControl rpcFastStatus:@"keychain" reply:^(NSArray<NSDictionary*>* result, NSError* error) {
1500 XCTAssertNil(error, "should be no error fetching status for keychain");
1502 // Ugly "global" hack
1503 XCTAssertEqual(result.count, 1u, "Should have received one result dictionaries back");
1504 NSDictionary* keychainStatus = result[0];
1506 XCTAssertNotNil(keychainStatus, "Should have received at least one zone status back");
1507 XCTAssertEqualObjects(keychainStatus[@"view"], @"keychain", "Should have received status for the keychain view");
1508 XCTAssertEqualObjects(keychainStatus[@"keystate"], SecCKKSZoneKeyStateReady, "Should be in 'ready' status");
1509 XCTAssertNil(keychainStatus[@"ckmirror"], "fastStatus should not have any ckmirror");
1510 [callbackOccurs fulfill];
1513 [self waitForExpectations:@[callbackOccurs] timeout:20];
1517 - (void)testRpcStatusWaitsForAccountDetermination {
1518 [self createAndSaveFakeKeyHierarchy: self.keychainZoneID]; // Make life easy for this test.
1520 // Set up the account state callbacks to happen in one second
1521 dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (1 * NSEC_PER_SEC)), dispatch_get_global_queue(QOS_CLASS_DEFAULT, 0), ^{
1522 // Let CKKS come up (simulating daemon starting due to RPC)
1523 [self startCKKSSubsystem];
1526 // Before CKKS figures out we're in an account, fire off the status RPC.
1527 XCTestExpectation* callbackOccurs = [self expectationWithDescription:@"callback-occurs"];
1528 [self.ckksControl rpcStatus:@"keychain" reply:^(NSArray<NSDictionary*>* result, NSError* error) {
1529 XCTAssertNil(error, "should be no error fetching status for keychain");
1531 // Ugly "global" hack
1532 XCTAssertEqual(result.count, 2u, "Should have received two result dictionaries back");
1533 NSDictionary* keychainStatus = result[1];
1535 XCTAssertNotNil(keychainStatus, "Should have received at least one zone status back");
1536 XCTAssertEqualObjects(keychainStatus[@"view"], @"keychain", "Should have received status for the keychain view");
1537 XCTAssertEqualObjects(keychainStatus[@"keystate"], SecCKKSZoneKeyStateReady, "Should be in 'ready' status");
1538 [callbackOccurs fulfill];
1541 [self waitForExpectations:@[callbackOccurs] timeout:20];
1544 - (void)testRpcStatusIsFastDuringError {
1545 [self createAndSaveFakeKeyHierarchy: self.keychainZoneID]; // Make life easy for this test.
1547 self.keychainFetchError = [NSError errorWithDomain:NSOSStatusErrorDomain code:errSecInternalError description:@"injected keychain failure"];
1549 // Let CKKS come up; it should enter 'error'
1550 [self startCKKSSubsystem];
1551 XCTAssertEqual(0, [self.keychainView.keyHierarchyConditions[SecCKKSZoneKeyStateError] wait:20*NSEC_PER_SEC], "CKKS entered 'error'");
1553 // Fire off the status RPC; it should return immediately
1554 XCTestExpectation* callbackOccurs = [self expectationWithDescription:@"callback-occurs"];
1555 [self.ckksControl rpcStatus:@"keychain" reply:^(NSArray<NSDictionary*>* result, NSError* error) {
1556 XCTAssertNil(error, "should be no error fetching status for keychain");
1558 // Ugly "global" hack
1559 XCTAssertEqual(result.count, 2u, "Should have received two result dictionaries back");
1560 NSDictionary* keychainStatus = result[1];
1562 XCTAssertNotNil(keychainStatus, "Should have received at least one zone status back");
1563 XCTAssertEqualObjects(keychainStatus[@"view"], @"keychain", "Should have received status for the keychain view");
1564 XCTAssertEqualObjects(keychainStatus[@"keystate"], SecCKKSZoneKeyStateError, "Should be in 'ready' status");
1565 [callbackOccurs fulfill];
1568 [self waitForExpectations:@[callbackOccurs] timeout:20];