]> git.saurik.com Git - apple/security.git/blob - keychain/ckks/tests/CKKSTests+API.m
Security-59754.80.3.tar.gz
[apple/security.git] / keychain / ckks / tests / CKKSTests+API.m
1 /*
2 * Copyright (c) 2016 Apple Inc. All Rights Reserved.
3 *
4 * @APPLE_LICENSE_HEADER_START@
5 *
6 * This file contains Original Code and/or Modifications of Original Code
7 * as defined in and that are subject to the Apple Public Source License
8 * Version 2.0 (the 'License'). You may not use this file except in
9 * compliance with the License. Please obtain a copy of the License at
10 * http://www.opensource.apple.com/apsl/ and read it before using this
11 * file.
12 *
13 * The Original Code and all software distributed under the License are
14 * distributed on an 'AS IS' basis, WITHOUT WARRANTY OF ANY KIND, EITHER
15 * EXPRESS OR IMPLIED, AND APPLE HEREBY DISCLAIMS ALL SUCH WARRANTIES,
16 * INCLUDING WITHOUT LIMITATION, ANY WARRANTIES OF MERCHANTABILITY,
17 * FITNESS FOR A PARTICULAR PURPOSE, QUIET ENJOYMENT OR NON-INFRINGEMENT.
18 * Please see the License for the specific language governing rights and
19 * limitations under the License.
20 *
21 * @APPLE_LICENSE_HEADER_END@
22 */
23
24 #if OCTAGON
25
26 #import <CloudKit/CloudKit.h>
27 #import <XCTest/XCTest.h>
28 #import <OCMock/OCMock.h>
29
30 #include <Security/SecItemPriv.h>
31 #include "OSX/sec/Security/SecItemShim.h"
32
33 #include <Security/SecEntitlements.h>
34 #include <ipc/server_security_helpers.h>
35 #import <Foundation/NSXPCConnection_Private.h>
36
37 #import "keychain/categories/NSError+UsefulConstructors.h"
38
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"
47
48 #import "keychain/ckks/CKKSControl.h"
49 #import "keychain/ckks/CloudKitCategories.h"
50
51 #import "keychain/ckks/tests/MockCloudKit.h"
52 #import "keychain/ckks/tests/CKKSTests.h"
53 #import "keychain/ckks/tests/CKKSTests+API.h"
54
55 @implementation CloudKitKeychainSyncingTestsBase (APITests)
56
57 -(NSMutableDictionary*)pcsAddItemQuery:(NSString*)account
58 data:(NSData*)data
59 serviceIdentifier:(NSNumber*)serviceIdentifier
60 publicKey:(NSData*)publicKey
61 publicIdentity:(NSData*)publicIdentity
62 {
63 return [@{
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)kSecAttrSyncViewHint : self.keychainView.zoneName,
73 (id)kSecAttrDeriveSyncIDFromItemAttributes : (id)kCFBooleanTrue,
74 (id)kSecAttrPCSPlaintextServiceIdentifier : serviceIdentifier,
75 (id)kSecAttrPCSPlaintextPublicKey : publicKey,
76 (id)kSecAttrPCSPlaintextPublicIdentity : publicIdentity,
77 } mutableCopy];
78 }
79
80 -(NSDictionary*)pcsAddItem:(NSString*)account
81 data:(NSData*)data
82 serviceIdentifier:(NSNumber*)serviceIdentifier
83 publicKey:(NSData*)publicKey
84 publicIdentity:(NSData*)publicIdentity
85 expectingSync:(bool)expectingSync
86 {
87 NSMutableDictionary* query = [self pcsAddItemQuery:account
88 data:data
89 serviceIdentifier:(NSNumber*)serviceIdentifier
90 publicKey:(NSData*)publicKey
91 publicIdentity:(NSData*)publicIdentity];
92 CFTypeRef result = NULL;
93 XCTestExpectation* syncExpectation = [self expectationWithDescription: @"callback occurs"];
94
95 XCTAssertEqual(errSecSuccess, _SecItemAddAndNotifyOnSync((__bridge CFDictionaryRef) query, &result, ^(bool didSync, CFErrorRef error) {
96 if(expectingSync) {
97 XCTAssertTrue(didSync, "Item synced");
98 XCTAssertNil((__bridge NSError*)error, "No error syncing item");
99 } else {
100 XCTAssertFalse(didSync, "Item did not sync");
101 XCTAssertNotNil((__bridge NSError*)error, "Error syncing item");
102 }
103
104 [syncExpectation fulfill];
105 }), @"_SecItemAddAndNotifyOnSync succeeded");
106
107 // Verify that the item was written to CloudKit
108 OCMVerifyAllWithDelay(self.mockDatabase, 20);
109
110 // In real code, you'd need to wait for the _SecItemAddAndNotifyOnSync callback to succeed before proceeding
111 [self waitForExpectations:@[syncExpectation] timeout:20];
112
113 return (NSDictionary*) CFBridgingRelease(result);
114 }
115
116 - (BOOL (^) (CKRecord*)) checkPCSFieldsBlock: (CKRecordZoneID*) zoneID
117 PCSServiceIdentifier:(NSNumber*)servIdentifier
118 PCSPublicKey:(NSData*)publicKey
119 PCSPublicIdentity:(NSData*)publicIdentity
120 {
121 __weak __typeof(self) weakSelf = self;
122 return ^BOOL(CKRecord* record) {
123 __strong __typeof(weakSelf) strongSelf = weakSelf;
124 XCTAssertNotNil(strongSelf, "self exists");
125
126 XCTAssert([record[SecCKRecordPCSServiceIdentifier] isEqual: servIdentifier], "PCS Service identifier matches input");
127 XCTAssert([record[SecCKRecordPCSPublicKey] isEqual: publicKey], "PCS Public Key matches input");
128 XCTAssert([record[SecCKRecordPCSPublicIdentity] isEqual: publicIdentity], "PCS Public Identity matches input");
129
130 if([record[SecCKRecordPCSServiceIdentifier] isEqual: servIdentifier] &&
131 [record[SecCKRecordPCSPublicKey] isEqual: publicKey] &&
132 [record[SecCKRecordPCSPublicIdentity] isEqual: publicIdentity]) {
133 return YES;
134 } else {
135 return NO;
136 }
137 };
138 }
139 @end
140
141 @interface CloudKitKeychainSyncingAPITests : CloudKitKeychainSyncingTestsBase
142 @end
143
144 @implementation CloudKitKeychainSyncingAPITests
145 - (void)testSecuritydClientBringup {
146 #if 0
147 CFErrorRef cferror = nil;
148 xpc_endpoint_t endpoint = SecCreateSecuritydXPCServerEndpoint(&cferror);
149 XCTAssertNil((__bridge id)cferror, "No error creating securityd endpoint");
150 XCTAssertNotNil(endpoint, "Received securityd endpoint");
151 #endif
152
153 NSXPCInterface *interface = [NSXPCInterface interfaceWithProtocol:@protocol(SecuritydXPCProtocol)];
154 [SecuritydXPCClient configureSecuritydXPCProtocol: interface];
155 XCTAssertNotNil(interface, "Received a configured CKKS interface");
156
157 #if 0
158 NSXPCListenerEndpoint *listenerEndpoint = [[NSXPCListenerEndpoint alloc] init];
159 [listenerEndpoint _setEndpoint:endpoint];
160
161 NSXPCConnection* connection = [[NSXPCConnection alloc] initWithListenerEndpoint:listenerEndpoint];
162 XCTAssertNotNil(connection , "Received an active connection");
163
164 connection.remoteObjectInterface = interface;
165 #endif
166 }
167
168 - (void)testAddAndNotifyOnSync {
169 [self createAndSaveFakeKeyHierarchy: self.keychainZoneID]; // Make life easy for this test.
170
171 [self expectCKModifyItemRecords: 1 currentKeyPointerRecords: 1 zoneID:self.keychainZoneID];
172 [self startCKKSSubsystem];
173
174 // Let things shake themselves out.
175 [self.keychainView waitForKeyHierarchyReadiness];
176 [self waitForCKModifications];
177
178 NSMutableDictionary* query = [@{
179 (id)kSecClass : (id)kSecClassGenericPassword,
180 (id)kSecAttrAccessGroup : @"com.apple.security.ckks",
181 (id)kSecAttrAccessible: (id)kSecAttrAccessibleAfterFirstUnlock,
182 (id)kSecAttrAccount : @"testaccount",
183 (id)kSecAttrSynchronizable : (id)kCFBooleanTrue,
184 (id)kSecAttrSyncViewHint : self.keychainView.zoneName,
185 (id)kSecValueData : (id) [@"asdf" dataUsingEncoding:NSUTF8StringEncoding],
186 } mutableCopy];
187
188 XCTestExpectation* blockExpectation = [self expectationWithDescription: @"callback occurs"];
189
190 XCTAssertEqual(errSecSuccess, _SecItemAddAndNotifyOnSync((__bridge CFDictionaryRef) query, NULL, ^(bool didSync, CFErrorRef error) {
191 XCTAssertTrue(didSync, "Item synced properly");
192 XCTAssertNil((__bridge NSError*)error, "No error syncing item");
193
194 [blockExpectation fulfill];
195 }), @"_SecItemAddAndNotifyOnSync succeeded");
196
197 OCMVerifyAllWithDelay(self.mockDatabase, 10);
198
199 [self waitForExpectationsWithTimeout:5.0 handler:nil];
200 }
201
202 - (void)testAddAndNotifyOnSyncSkipsQueue {
203 // Use the PCS plaintext fields to determine which object is which
204 SecResetLocalSecuritydXPCFakeEntitlements();
205 SecAddLocalSecuritydXPCFakeEntitlement(kSecEntitlementPrivateCKKSPlaintextFields, kCFBooleanTrue);
206 SecAddLocalSecuritydXPCFakeEntitlement(kSecEntitlementPrivateCKKSWriteCurrentItemPointers, kCFBooleanTrue);
207 SecAddLocalSecuritydXPCFakeEntitlement(kSecEntitlementPrivateCKKSReadCurrentItemPointers, kCFBooleanTrue);
208
209 [self createAndSaveFakeKeyHierarchy: self.keychainZoneID]; // Make life easy for this test.
210
211 [self startCKKSSubsystem];
212 [self.keychainView waitForKeyHierarchyReadiness];
213 [self waitForCKModifications];
214 OCMVerifyAllWithDelay(self.mockDatabase, 40);
215
216 [self.keychainView waitForOperationsOfClass:[CKKSOutgoingQueueOperation class]];
217 self.keychainView.holdOutgoingQueueOperation = [CKKSGroupOperation named:@"outgoing-hold" withBlock: ^{
218 ckksnotice_global("ckks", "releasing outgoing-queue hold");
219 }];
220
221 for(size_t count = 0; count < 150; count++) {
222 [self addGenericPassword:@"data" account:[NSString stringWithFormat:@"account-delete-me-%03lu", count]];
223 }
224
225 NSMutableDictionary* query = [@{
226 (id)kSecClass : (id)kSecClassGenericPassword,
227 (id)kSecAttrAccessGroup : @"com.apple.security.ckks",
228 (id)kSecAttrAccessible: (id)kSecAttrAccessibleAfterFirstUnlock,
229 (id)kSecAttrAccount : @"testaccount",
230 (id)kSecAttrSynchronizable : (id)kCFBooleanTrue,
231 (id)kSecAttrSyncViewHint : self.keychainView.zoneName,
232 (id)kSecAttrPCSPlaintextPublicKey : [@"asdf" dataUsingEncoding:NSUTF8StringEncoding],
233 (id)kSecValueData : (id) [@"asdf" dataUsingEncoding:NSUTF8StringEncoding],
234 } mutableCopy];
235
236 XCTestExpectation* blockExpectation = [self expectationWithDescription: @"callback occurs"];
237
238 XCTAssertEqual(errSecSuccess, _SecItemAddAndNotifyOnSync((__bridge CFDictionaryRef) query, NULL, ^(bool didSync, CFErrorRef error) {
239 XCTAssertTrue(didSync, "Item synced properly");
240 XCTAssertNil((__bridge NSError*)error, "No error syncing item");
241
242 [blockExpectation fulfill];
243 }), @"_SecItemAddAndNotifyOnSync succeeded");
244
245
246 XCTestExpectation* firstQueueOperation = [self expectationWithDescription: @"found the item in the first queue iteration"];
247 [self expectCKModifyItemRecords:SecCKKSOutgoingQueueItemsAtOnce
248 currentKeyPointerRecords:1
249 zoneID:self.keychainZoneID
250 checkItem:^BOOL(CKRecord * _Nonnull record) {
251 if(record[SecCKRecordPCSPublicKey]) {
252 [firstQueueOperation fulfill];
253 }
254 return YES;
255 }];
256 [self expectCKModifyItemRecords:51 currentKeyPointerRecords:1 zoneID:self.keychainZoneID];
257
258 // Release the hounds
259 [self.operationQueue addOperation:self.keychainView.holdOutgoingQueueOperation];
260
261 [self waitForExpectations:@[blockExpectation, firstQueueOperation] timeout:20];
262 OCMVerifyAllWithDelay(self.mockDatabase, 10);
263 }
264
265 - (void)testAddAndNotifyOnSyncFailure {
266 [self createAndSaveFakeKeyHierarchy: self.keychainZoneID]; // Make life easy for this test.
267
268 [self startCKKSSubsystem];
269 XCTAssertEqual(0, [self.keychainView.keyHierarchyConditions[SecCKKSZoneKeyStateReady] wait:20*NSEC_PER_SEC], @"key state should enter 'ready'");
270
271 [self.keychainView waitForFetchAndIncomingQueueProcessing];
272
273 // Due to item UUID selection, this item will be added with UUID 50184A35-4480-E8BA-769B-567CF72F1EC0.
274 // Add it to CloudKit first!
275 CKRecord* ckr = [self createFakeRecord: self.keychainZoneID recordName:@"50184A35-4480-E8BA-769B-567CF72F1EC0"];
276 [self.keychainZone addToZone: ckr];
277
278
279 // Go for it!
280 [self expectCKAtomicModifyItemRecordsUpdateFailure: self.keychainZoneID];
281
282 NSMutableDictionary* query = [@{
283 (id)kSecClass : (id)kSecClassGenericPassword,
284 (id)kSecAttrAccessGroup : @"com.apple.security.ckks",
285 (id)kSecAttrAccessible: (id)kSecAttrAccessibleAfterFirstUnlock,
286 (id)kSecAttrAccount : @"testaccount",
287 (id)kSecAttrSynchronizable : (id)kCFBooleanTrue,
288 (id)kSecAttrSyncViewHint : self.keychainView.zoneName,
289 (id)kSecValueData : (id) [@"asdf" dataUsingEncoding:NSUTF8StringEncoding],
290 (id)kSecAttrSyncViewHint : self.keychainView.zoneName, // @ fake view hint for fake view
291 } mutableCopy];
292
293 XCTestExpectation* blockExpectation = [self expectationWithDescription: @"callback occurs"];
294
295 XCTAssertEqual(errSecSuccess, _SecItemAddAndNotifyOnSync((__bridge CFDictionaryRef) query, NULL, ^(bool didSync, CFErrorRef error) {
296 XCTAssertFalse(didSync, "Item did not sync (as expected)");
297 XCTAssertNotNil((__bridge NSError*)error, "error exists when item fails to sync");
298
299 [blockExpectation fulfill];
300 }), @"_SecItemAddAndNotifyOnSync succeeded");
301
302 [self waitForExpectationsWithTimeout:5.0 handler:nil];
303 [self waitForCKModifications];
304 }
305
306 - (void)testAddAndNotifyOnSyncLoggedOut {
307 // Test starts with nothing in database and the user logged out of CloudKit. We expect no CKKS operations.
308 self.accountStatus = CKAccountStatusNoAccount;
309 self.silentFetchesAllowed = false;
310 [self startCKKSSubsystem];
311
312 XCTAssertEqual(0, [self.keychainView.loggedOut wait:20*NSEC_PER_SEC], "CKKS should positively log out");
313
314 NSMutableDictionary* query = [@{
315 (id)kSecClass : (id)kSecClassGenericPassword,
316 (id)kSecAttrAccessGroup : @"com.apple.security.ckks",
317 (id)kSecAttrAccessible: (id)kSecAttrAccessibleAfterFirstUnlock,
318 (id)kSecAttrAccount : @"testaccount",
319 (id)kSecAttrSynchronizable : (id)kCFBooleanTrue,
320 (id)kSecAttrSyncViewHint : self.keychainView.zoneName,
321 (id)kSecValueData : (id) [@"asdf" dataUsingEncoding:NSUTF8StringEncoding],
322 } mutableCopy];
323
324 XCTestExpectation* blockExpectation = [self expectationWithDescription: @"callback occurs"];
325
326 XCTAssertEqual(errSecSuccess, _SecItemAddAndNotifyOnSync((__bridge CFDictionaryRef) query, NULL, ^(bool didSync, CFErrorRef error) {
327 XCTAssertFalse(didSync, "Item did not sync (with no iCloud account)");
328 XCTAssertNotNil((__bridge NSError*)error, "Error exists syncing item while logged out");
329
330 [blockExpectation fulfill];
331 }), @"_SecItemAddAndNotifyOnSync succeeded");
332
333 [self waitForExpectationsWithTimeout:5.0 handler:nil];
334 }
335
336 - (void)testAddAndNotifyOnSyncAccountStatusUnclear {
337 // Test starts with nothing in database, but CKKS hasn't been told we've logged out yet.
338 // We expect no CKKS operations.
339 self.accountStatus = CKAccountStatusNoAccount;
340 self.silentFetchesAllowed = false;
341
342 NSMutableDictionary* query = [@{
343 (id)kSecClass : (id)kSecClassGenericPassword,
344 (id)kSecAttrAccessGroup : @"com.apple.security.ckks",
345 (id)kSecAttrAccessible: (id)kSecAttrAccessibleAfterFirstUnlock,
346 (id)kSecAttrAccount : @"testaccount",
347 (id)kSecAttrSynchronizable : (id)kCFBooleanTrue,
348 (id)kSecAttrSyncViewHint : self.keychainView.zoneName,
349 (id)kSecValueData : (id) [@"asdf" dataUsingEncoding:NSUTF8StringEncoding],
350 } mutableCopy];
351
352 XCTestExpectation* blockExpectation = [self expectationWithDescription: @"callback occurs"];
353
354 XCTAssertEqual(errSecSuccess, _SecItemAddAndNotifyOnSync((__bridge CFDictionaryRef) query, NULL, ^(bool didSync, CFErrorRef error) {
355 XCTAssertFalse(didSync, "Item did not sync (with no iCloud account)");
356 XCTAssertNotNil((__bridge NSError*)error, "Error exists syncing item while logged out");
357
358 [blockExpectation fulfill];
359 }), @"_SecItemAddAndNotifyOnSync succeeded");
360
361 // And now, allow CKKS to discover we're logged out
362 [self startCKKSSubsystem];
363 XCTAssertEqual(0, [self.keychainView.loggedOut wait:20*NSEC_PER_SEC], "CKKS should positively log out");
364
365 [self waitForExpectationsWithTimeout:5.0 handler:nil];
366 }
367
368 - (void)testAddAndNotifyOnSyncBeforeKeyHierarchyReady {
369 // Test starts with a key hierarchy in cloudkit and the TLK having arrived
370 [self putFakeKeyHierarchyInCloudKit:self.keychainZoneID];
371 [self saveTLKMaterialToKeychain:self.keychainZoneID];
372 [self expectCKKSTLKSelfShareUpload:self.keychainZoneID];
373
374 // But block CloudKit fetches (so the key hierarchy won't be ready when we add this new item)
375 [self holdCloudKitFetches];
376
377 [self startCKKSSubsystem];
378 XCTAssertEqual(0, [self.keychainView.loggedIn wait:20*NSEC_PER_SEC], "CKKS should log in");
379 XCTAssertEqual(0, [self.keychainView.keyHierarchyConditions[SecCKKSZoneKeyStateFetch] wait:20*NSEC_PER_SEC], @"Should have reached key state 'fetch', but no further");
380
381 NSMutableDictionary* query = [@{
382 (id)kSecClass : (id)kSecClassGenericPassword,
383 (id)kSecAttrAccessGroup : @"com.apple.security.ckks",
384 (id)kSecAttrAccessible: (id)kSecAttrAccessibleAfterFirstUnlock,
385 (id)kSecAttrAccount : @"testaccount",
386 (id)kSecAttrSynchronizable : (id)kCFBooleanTrue,
387 (id)kSecAttrSyncViewHint : self.keychainView.zoneName,
388 (id)kSecValueData : (id) [@"asdf" dataUsingEncoding:NSUTF8StringEncoding],
389 } mutableCopy];
390
391 XCTestExpectation* blockExpectation = [self expectationWithDescription: @"callback occurs"];
392
393 XCTAssertEqual(errSecSuccess, _SecItemAddAndNotifyOnSync((__bridge CFDictionaryRef) query, NULL, ^(bool didSync, CFErrorRef error) {
394 XCTAssertTrue(didSync, "Item synced");
395 XCTAssertNil((__bridge NSError*)error, "Shouldn't have received an error syncing item");
396
397 [blockExpectation fulfill];
398 }), @"_SecItemAddAndNotifyOnSync succeeded");
399
400 // We should be in the 'fetch' state, but no further
401 XCTAssertEqual(0, [self.keychainView.keyHierarchyConditions[SecCKKSZoneKeyStateFetch] wait:20*NSEC_PER_SEC], @"Should have reached key state 'fetch', but no further");
402
403 // When we release the fetch, the callback should still fire and the item should upload
404 [self expectCKModifyItemRecords: 1 currentKeyPointerRecords: 1 zoneID:self.keychainZoneID];
405 [self releaseCloudKitFetchHold];
406 XCTAssertEqual(0, [self.keychainView.keyHierarchyConditions[SecCKKSZoneKeyStateReady] wait:20*NSEC_PER_SEC], @"Should have reached key state 'ready'");
407
408 // Verify that the item was written to CloudKit
409 OCMVerifyAllWithDelay(self.mockDatabase, 20);
410
411 [self waitForExpectationsWithTimeout:5.0 handler:nil];
412 }
413
414 - (void)testAddAndNotifyOnSyncBeforePolicyLoaded {
415 [self putFakeKeyHierarchyInCloudKit:self.keychainZoneID];
416 [self saveTLKMaterialToKeychain:self.keychainZoneID];
417 [self expectCKKSTLKSelfShareUpload:self.keychainZoneID];
418
419 [self startCKKSSubsystem];
420
421 // Allow CKKS to spin up fully
422 XCTAssertEqual(0, [self.keychainView.keyHierarchyConditions[SecCKKSZoneKeyStateReady] wait:20*NSEC_PER_SEC], "CKKS entered ready");
423
424 [self.keychainView waitForOperationsOfClass:[CKKSScanLocalItemsOperation class]];
425 [self.keychainView waitForOperationsOfClass:[CKKSOutgoingQueueOperation class]];
426
427 // Simulate a daemon restart
428 self.automaticallyBeginCKKSViewCloudKitOperation = false;
429 [self.injectedManager resetSyncingPolicy];
430 [self.injectedManager haltZone:self.keychainZoneID.zoneName];
431
432 // Issue the query (to simulate the query starting securityd)
433 NSMutableDictionary* query = [@{
434 (id)kSecClass : (id)kSecClassGenericPassword,
435 (id)kSecAttrAccessGroup : @"com.apple.security.ckks",
436 (id)kSecAttrAccessible: (id)kSecAttrAccessibleAfterFirstUnlock,
437 (id)kSecAttrAccount : @"testaccount",
438 (id)kSecAttrSynchronizable : (id)kCFBooleanTrue,
439 (id)kSecAttrSyncViewHint : self.keychainView.zoneName,
440 (id)kSecValueData : (id) [@"asdf" dataUsingEncoding:NSUTF8StringEncoding],
441 } mutableCopy];
442
443 XCTestExpectation* blockExpectation = [self expectationWithDescription: @"callback occurs"];
444
445 XCTAssertEqual(errSecSuccess, _SecItemAddAndNotifyOnSync((__bridge CFDictionaryRef) query, NULL, ^(bool didSync, CFErrorRef error) {
446 XCTAssertTrue(didSync, "Item synced");
447 XCTAssertNil((__bridge NSError*)error, "Shouldn't have received an error syncing item");
448
449 [blockExpectation fulfill];
450 }), @"_SecItemAddAndNotifyOnSync succeeded");
451
452 // When the policy is loaded, the item should upload and the callback should fire
453 [self expectCKModifyItemRecords:1 currentKeyPointerRecords:1 zoneID:self.keychainZoneID];
454
455 [self.injectedManager setCurrentSyncingPolicy:self.viewSortingPolicyForManagedViewList];
456 self.keychainView = [self.injectedManager findView:self.keychainZoneID.zoneName];
457
458 [self.injectedManager beginCloudKitOperationOfAllViews];
459 [self beginSOSTrustedViewOperation:self.keychainView];
460
461 XCTAssertEqual(0, [self.keychainView.keyHierarchyConditions[SecCKKSZoneKeyStateReady] wait:20*NSEC_PER_SEC], @"Should have reached key state 'ready'");
462 OCMVerifyAllWithDelay(self.mockDatabase, 20);
463
464 [self waitForExpectations:@[blockExpectation] timeout:5];
465 }
466
467 - (void)testAddAndNotifyOnSyncReaddAtSameUUIDAfterDeleteItem {
468 [self createAndSaveFakeKeyHierarchy:self.keychainZoneID]; // Make life easy for this test.
469
470 __block CKRecordID* itemRecordID = nil;
471 [self expectCKModifyItemRecords:1 currentKeyPointerRecords:1 zoneID:self.keychainZoneID checkItem:^BOOL(CKRecord * _Nonnull record) {
472 itemRecordID = record.recordID;
473 return YES;
474 }];
475
476 [self startCKKSSubsystem];
477
478 // Let things shake themselves out.
479 [self.keychainView waitForKeyHierarchyReadiness];
480 [self waitForCKModifications];
481
482 NSMutableDictionary* query = [@{
483 (id)kSecClass : (id)kSecClassGenericPassword,
484 (id)kSecAttrAccessGroup : @"com.apple.security.ckks",
485 (id)kSecAttrAccessible: (id)kSecAttrAccessibleAfterFirstUnlock,
486 (id)kSecAttrAccount : @"testaccount",
487 (id)kSecAttrSynchronizable : (id)kCFBooleanTrue,
488 (id)kSecAttrSyncViewHint : self.keychainView.zoneName,
489 (id)kSecValueData : (id) [@"asdf" dataUsingEncoding:NSUTF8StringEncoding],
490 } mutableCopy];
491
492 XCTestExpectation* blockExpectation = [self expectationWithDescription: @"callback occurs"];
493
494 XCTAssertEqual(errSecSuccess, _SecItemAddAndNotifyOnSync((__bridge CFDictionaryRef) query, NULL, ^(bool didSync, CFErrorRef error) {
495 XCTAssertTrue(didSync, "Item synced properly");
496 XCTAssertNil((__bridge NSError*)error, "No error syncing item");
497
498 [blockExpectation fulfill];
499 }), @"_SecItemAddAndNotifyOnSync succeeded");
500
501 OCMVerifyAllWithDelay(self.mockDatabase, 10);
502
503 [self waitForExpectations:@[blockExpectation] timeout:5];
504
505 // And the item is deleted
506 [self expectCKDeleteItemRecords:1 zoneID:self.keychainZoneID];
507 [self deleteGenericPassword:@"testaccount"];
508 OCMVerifyAllWithDelay(self.mockDatabase, 20);
509
510 // And the item is readded. It should come back to its previous UUID.
511 XCTAssertNotNil(itemRecordID, "Should have an item record ID");
512 [self expectCKModifyItemRecords:1 currentKeyPointerRecords:1 zoneID:self.keychainZoneID checkItem:^BOOL(CKRecord * _Nonnull record) {
513 XCTAssertEqualObjects(itemRecordID.recordName, record.recordID.recordName, "Uploaded item UUID should match previous upload");
514 return YES;
515 }];
516 XCTestExpectation* blockExpectation2 = [self expectationWithDescription: @"callback occurs"];
517 XCTAssertEqual(errSecSuccess, _SecItemAddAndNotifyOnSync((__bridge CFDictionaryRef) query, NULL, ^(bool didSync, CFErrorRef error) {
518 XCTAssertTrue(didSync, "Item synced properly");
519 XCTAssertNil((__bridge NSError*)error, "No error syncing item");
520
521 [blockExpectation2 fulfill];
522 }), @"_SecItemAddAndNotifyOnSync succeeded");
523
524 OCMVerifyAllWithDelay(self.mockDatabase, 10);
525 [self waitForExpectations:@[blockExpectation2] timeout:5];
526 }
527
528 - (void)testPCSUnencryptedFieldsAdd {
529 [self createAndSaveFakeKeyHierarchy: self.keychainZoneID]; // Make life easy for this test.
530
531 [self startCKKSSubsystem];
532 [self.keychainView waitForKeyHierarchyReadiness];
533
534 NSNumber* servIdentifier = @3;
535 NSData* publicKey = [@"asdfasdf" dataUsingEncoding:NSUTF8StringEncoding];
536 NSData* publicIdentity = [@"somedata" dataUsingEncoding:NSUTF8StringEncoding];
537
538 [self expectCKModifyItemRecords: 1 currentKeyPointerRecords: 1 zoneID:self.keychainZoneID
539 checkItem: [self checkPCSFieldsBlock:self.keychainZoneID
540 PCSServiceIdentifier:(NSNumber *)servIdentifier
541 PCSPublicKey:publicKey
542 PCSPublicIdentity:publicIdentity]];
543
544 NSMutableDictionary* query = [@{
545 (id)kSecClass : (id)kSecClassGenericPassword,
546 (id)kSecAttrAccessGroup : @"com.apple.security.ckks",
547 (id)kSecAttrAccessible: (id)kSecAttrAccessibleAfterFirstUnlock,
548 (id)kSecAttrAccount : @"testaccount",
549 (id)kSecAttrSynchronizable : (id)kCFBooleanTrue,
550 (id)kSecValueData : (id) [@"asdf" dataUsingEncoding:NSUTF8StringEncoding],
551 (id)kSecAttrDeriveSyncIDFromItemAttributes : (id)kCFBooleanTrue,
552 (id)kSecAttrPCSPlaintextServiceIdentifier : servIdentifier,
553 (id)kSecAttrPCSPlaintextPublicKey : publicKey,
554 (id)kSecAttrPCSPlaintextPublicIdentity : publicIdentity,
555 (id)kSecAttrSyncViewHint : self.keychainView.zoneName, // allows a CKKSScanOperation to find this item
556 } mutableCopy];
557
558 XCTAssertEqual(errSecSuccess, SecItemAdd((__bridge CFDictionaryRef) query, NULL), @"SecItemAdd succeeded");
559
560 // Verify that the item is written to CloudKit
561 OCMVerifyAllWithDelay(self.mockDatabase, 20);
562
563 CFTypeRef item = NULL;
564 query[(id)kSecValueData] = nil;
565 query[(id)kSecReturnAttributes] = @YES;
566 XCTAssertEqual(errSecSuccess, SecItemCopyMatching((__bridge CFDictionaryRef) query, &item), "item should still exist");
567
568 NSDictionary* itemAttributes = (NSDictionary*) CFBridgingRelease(item);
569 XCTAssertEqualObjects(itemAttributes[(id)kSecAttrPCSPlaintextServiceIdentifier], servIdentifier, "Service Identifier exists");
570 XCTAssertEqualObjects(itemAttributes[(id)kSecAttrPCSPlaintextPublicKey], publicKey, "public key exists");
571 XCTAssertEqualObjects(itemAttributes[(id)kSecAttrPCSPlaintextPublicIdentity], publicIdentity, "public identity exists");
572
573 // Find the item record in CloudKit. Since we're using kSecAttrDeriveSyncIDFromItemAttributes,
574 // the record ID is likely 50184A35-4480-E8BA-769B-567CF72F1EC0
575 [self waitForCKModifications];
576 CKRecordID* recordID = [[CKRecordID alloc] initWithRecordName: @"50184A35-4480-E8BA-769B-567CF72F1EC0" zoneID:self.keychainZoneID];
577 CKRecord* record = self.keychainZone.currentDatabase[recordID];
578 XCTAssertNotNil(record, "Found record in CloudKit at expected UUID");
579
580 XCTAssertEqualObjects(record[SecCKRecordPCSServiceIdentifier], servIdentifier, "Service identifier sent to cloudkit");
581 XCTAssertEqualObjects(record[SecCKRecordPCSPublicKey], publicKey, "public key sent to cloudkit");
582 XCTAssertEqualObjects(record[SecCKRecordPCSPublicIdentity], publicIdentity, "public identity sent to cloudkit");
583 }
584
585 - (void)testPCSUnencryptedFieldsModify {
586 [self createAndSaveFakeKeyHierarchy: self.keychainZoneID]; // Make life easy for this test.
587
588 [self startCKKSSubsystem];
589 [self.keychainView waitForKeyHierarchyReadiness];
590
591 NSNumber* servIdentifier = @3;
592 NSData* publicKey = [@"asdfasdf" dataUsingEncoding:NSUTF8StringEncoding];
593 NSData* publicIdentity = [@"somedata" dataUsingEncoding:NSUTF8StringEncoding];
594
595 [self expectCKModifyItemRecords: 1 currentKeyPointerRecords: 1 zoneID:self.keychainZoneID
596 checkItem: [self checkPCSFieldsBlock:self.keychainZoneID
597 PCSServiceIdentifier:(NSNumber *)servIdentifier
598 PCSPublicKey:publicKey
599 PCSPublicIdentity:publicIdentity]];
600
601 NSMutableDictionary* query = [@{
602 (id)kSecClass : (id)kSecClassGenericPassword,
603 (id)kSecAttrAccessGroup : @"com.apple.security.ckks",
604 (id)kSecAttrAccessible: (id)kSecAttrAccessibleAfterFirstUnlock,
605 (id)kSecAttrAccount : @"testaccount",
606 (id)kSecAttrSynchronizable : (id)kCFBooleanTrue,
607 (id)kSecValueData : (id) [@"asdf" dataUsingEncoding:NSUTF8StringEncoding],
608 (id)kSecAttrDeriveSyncIDFromItemAttributes : (id)kCFBooleanTrue,
609 (id)kSecAttrPCSPlaintextServiceIdentifier : servIdentifier,
610 (id)kSecAttrPCSPlaintextPublicKey : publicKey,
611 (id)kSecAttrPCSPlaintextPublicIdentity : publicIdentity,
612 (id)kSecAttrSyncViewHint : self.keychainView.zoneName, // allows a CKKSScanOperation to find this item
613 } mutableCopy];
614
615 XCTAssertEqual(errSecSuccess, SecItemAdd((__bridge CFDictionaryRef) query, NULL), @"SecItemAdd succeeded");
616
617 OCMVerifyAllWithDelay(self.mockDatabase, 20);
618 [self waitForCKModifications];
619
620 query[(id)kSecValueData] = nil;
621 query[(id)kSecAttrPCSPlaintextServiceIdentifier] = nil;
622 query[(id)kSecAttrPCSPlaintextPublicKey] = nil;
623 query[(id)kSecAttrPCSPlaintextPublicIdentity] = nil;
624
625 servIdentifier = @1;
626 publicKey = [@"new public key" dataUsingEncoding:NSUTF8StringEncoding];
627
628 NSNumber* newServiceIdentifier = @10;
629 NSData* newPublicKey = [@"new public key" dataUsingEncoding:NSUTF8StringEncoding];
630 NSData* newPublicIdentity = [@"new public identity" dataUsingEncoding:NSUTF8StringEncoding];
631
632 NSDictionary* update = @{
633 (id)kSecAttrPCSPlaintextServiceIdentifier : newServiceIdentifier,
634 (id)kSecAttrPCSPlaintextPublicKey : newPublicKey,
635 (id)kSecAttrPCSPlaintextPublicIdentity : newPublicIdentity,
636 };
637
638 [self expectCKModifyItemRecords: 1 currentKeyPointerRecords: 1 zoneID:self.keychainZoneID
639 checkItem: [self checkPCSFieldsBlock:self.keychainZoneID
640 PCSServiceIdentifier:(NSNumber *)newServiceIdentifier
641 PCSPublicKey:newPublicKey
642 PCSPublicIdentity:newPublicIdentity]];
643
644 XCTAssertEqual(errSecSuccess, SecItemUpdate((__bridge CFDictionaryRef) query, (__bridge CFDictionaryRef) update), @"SecItemUpdate succeeded");
645 OCMVerifyAllWithDelay(self.mockDatabase, 20);
646
647 CFTypeRef item = NULL;
648 query[(id)kSecValueData] = nil;
649 query[(id)kSecReturnAttributes] = @YES;
650 XCTAssertEqual(errSecSuccess, SecItemCopyMatching((__bridge CFDictionaryRef) query, &item), "item should still exist");
651
652 NSDictionary* itemAttributes = (NSDictionary*) CFBridgingRelease(item);
653 XCTAssertEqualObjects(itemAttributes[(id)kSecAttrPCSPlaintextServiceIdentifier], newServiceIdentifier, "Service Identifier exists");
654 XCTAssertEqualObjects(itemAttributes[(id)kSecAttrPCSPlaintextPublicKey], newPublicKey, "public key exists");
655 XCTAssertEqualObjects(itemAttributes[(id)kSecAttrPCSPlaintextPublicIdentity], newPublicIdentity, "public identity exists");
656
657 // Find the item record in CloudKit. Since we're using kSecAttrDeriveSyncIDFromItemAttributes,
658 // the record ID is likely 50184A35-4480-E8BA-769B-567CF72F1EC0
659 [self waitForCKModifications];
660 CKRecordID* recordID = [[CKRecordID alloc] initWithRecordName: @"50184A35-4480-E8BA-769B-567CF72F1EC0" zoneID:self.keychainZoneID];
661 CKRecord* record = self.keychainZone.currentDatabase[recordID];
662 XCTAssertNotNil(record, "Found record in CloudKit at expected UUID");
663
664 XCTAssertEqualObjects(record[SecCKRecordPCSServiceIdentifier], newServiceIdentifier, "Service identifier sent to cloudkit");
665 XCTAssertEqualObjects(record[SecCKRecordPCSPublicKey], newPublicKey, "public key sent to cloudkit");
666 XCTAssertEqualObjects(record[SecCKRecordPCSPublicIdentity], newPublicIdentity, "public identity sent to cloudkit");
667 }
668
669 // As of [<rdar://problem/32558310> CKKS: Re-authenticate PCSPublicFields], these fields are NOT server-modifiable. This test proves it.
670 - (void)testPCSUnencryptedFieldsServerModifyFail {
671 [self createAndSaveFakeKeyHierarchy: self.keychainZoneID]; // Make life easy for this test.
672
673 [self startCKKSSubsystem];
674 [self.keychainView waitForKeyHierarchyReadiness];
675
676 NSNumber* servIdentifier = @3;
677 NSData* publicKey = [@"asdfasdf" dataUsingEncoding:NSUTF8StringEncoding];
678 NSData* publicIdentity = [@"somedata" dataUsingEncoding:NSUTF8StringEncoding];
679
680 [self expectCKModifyItemRecords: 1 currentKeyPointerRecords: 1 zoneID:self.keychainZoneID
681 checkItem: [self checkPCSFieldsBlock:self.keychainZoneID
682 PCSServiceIdentifier:(NSNumber *)servIdentifier
683 PCSPublicKey:publicKey
684 PCSPublicIdentity:publicIdentity]];
685
686 NSMutableDictionary* query = [@{
687 (id)kSecClass : (id)kSecClassGenericPassword,
688 (id)kSecAttrAccessGroup : @"com.apple.security.ckks",
689 (id)kSecAttrAccessible: (id)kSecAttrAccessibleAfterFirstUnlock,
690 (id)kSecAttrAccount : @"testaccount",
691 (id)kSecAttrSynchronizable : (id)kCFBooleanTrue,
692 (id)kSecValueData : (id) [@"asdf" dataUsingEncoding:NSUTF8StringEncoding],
693 (id)kSecAttrDeriveSyncIDFromItemAttributes : (id)kCFBooleanTrue,
694 (id)kSecAttrPCSPlaintextServiceIdentifier : servIdentifier,
695 (id)kSecAttrPCSPlaintextPublicKey : publicKey,
696 (id)kSecAttrPCSPlaintextPublicIdentity : publicIdentity,
697 (id)kSecAttrSyncViewHint : self.keychainView.zoneName, // fake, for CKKSScanOperation
698 } mutableCopy];
699
700 XCTAssertEqual(errSecSuccess, SecItemAdd((__bridge CFDictionaryRef) query, NULL), @"SecItemAdd succeeded");
701
702 OCMVerifyAllWithDelay(self.mockDatabase, 20);
703 [self waitForCKModifications];
704
705 // Find the item record in CloudKit. Since we're using kSecAttrDeriveSyncIDFromItemAttributes,
706 // the record ID is likely 50184A35-4480-E8BA-769B-567CF72F1EC0
707 CKRecordID* recordID = [[CKRecordID alloc] initWithRecordName: @"50184A35-4480-E8BA-769B-567CF72F1EC0" zoneID:self.keychainZoneID];
708 CKRecord* record = self.keychainZone.currentDatabase[recordID];
709 XCTAssertNotNil(record, "Found record in CloudKit at expected UUID");
710
711 // Items are encrypted using encv2
712 XCTAssertEqualObjects(record[SecCKRecordEncryptionVersionKey], [NSNumber numberWithInteger:(int) CKKSItemEncryptionVersion2], "Uploaded using encv2");
713
714 if(!record) {
715 // Test has already failed; find the record just to be nice.
716 for(CKRecord* maybe in self.keychainZone.currentDatabase.allValues) {
717 if(maybe[SecCKRecordPCSServiceIdentifier] != nil) {
718 record = maybe;
719 }
720 }
721 }
722
723 NSNumber* newServiceIdentifier = @10;
724 NSData* newPublicKey = [@"new public key" dataUsingEncoding:NSUTF8StringEncoding];
725 NSData* newPublicIdentity = [@"new public identity" dataUsingEncoding:NSUTF8StringEncoding];
726
727 // Change the public key and public identity
728 record = [record copyWithZone: nil];
729 record[SecCKRecordPCSServiceIdentifier] = newServiceIdentifier;
730 record[SecCKRecordPCSPublicKey] = newPublicKey;
731 record[SecCKRecordPCSPublicIdentity] = newPublicIdentity;
732 [self.keychainZone addToZone: record];
733
734 // Trigger a notification
735 [self.injectedManager.zoneChangeFetcher notifyZoneChange:nil];
736 [self.keychainView waitForFetchAndIncomingQueueProcessing];
737
738 CFTypeRef item = NULL;
739 query[(id)kSecValueData] = nil;
740 query[(id)kSecAttrPCSPlaintextServiceIdentifier] = nil;
741 query[(id)kSecAttrPCSPlaintextPublicKey] = nil;
742 query[(id)kSecAttrPCSPlaintextPublicIdentity] = nil;
743 query[(id)kSecReturnAttributes] = @YES;
744 XCTAssertEqual(errSecSuccess, SecItemCopyMatching((__bridge CFDictionaryRef) query, &item), "item should still exist");
745
746 NSDictionary* itemAttributes = (NSDictionary*) CFBridgingRelease(item);
747 XCTAssertEqualObjects(itemAttributes[(id)kSecAttrPCSPlaintextServiceIdentifier], servIdentifier, "service identifier is not updated");
748 XCTAssertEqualObjects(itemAttributes[(id)kSecAttrPCSPlaintextPublicKey], publicKey, "public key not updated");
749 XCTAssertEqualObjects(itemAttributes[(id)kSecAttrPCSPlaintextPublicIdentity], publicIdentity, "public identity not updated");
750 }
751
752 -(void)testPCSUnencryptedFieldsRecieveUnauthenticatedFields {
753 [self createAndSaveFakeKeyHierarchy: self.keychainZoneID]; // Make life easy for this test.
754
755 [self startCKKSSubsystem];
756 [self.keychainView waitForKeyHierarchyReadiness];
757
758 NSNumber* servIdentifier = @3;
759 NSData* publicKey = [@"asdfasdf" dataUsingEncoding:NSUTF8StringEncoding];
760 NSData* publicIdentity = [@"somedata" dataUsingEncoding:NSUTF8StringEncoding];
761
762 NSError* error = nil;
763
764 // Manually encrypt an item
765 NSString* recordName = @"7B598D31-F9C5-481E-98AC-5A507ACB2D85";
766 CKRecordID* recordID = [[CKRecordID alloc] initWithRecordName:recordName zoneID:self.keychainZoneID];
767 NSDictionary* item = [self fakeRecordDictionary: @"account-delete-me" zoneID:self.keychainZoneID];
768 CKKSItem* cipheritem = [[CKKSItem alloc] initWithUUID:recordID.recordName
769 parentKeyUUID:self.keychainZoneKeys.classC.uuid
770 zoneID:recordID.zoneID];
771 CKKSKey* itemkey = [CKKSKey randomKeyWrappedByParent: self.keychainZoneKeys.classC error:&error];
772 XCTAssertNotNil(itemkey, "Got a key");
773 cipheritem.wrappedkey = itemkey.wrappedkey;
774 XCTAssertNotNil(cipheritem.wrappedkey, "Got a wrapped key");
775
776 cipheritem.encver = CKKSItemEncryptionVersion1;
777
778 // This item has the PCS public fields, but they are not authenticated
779 cipheritem.plaintextPCSServiceIdentifier = servIdentifier;
780 cipheritem.plaintextPCSPublicKey = publicKey;
781 cipheritem.plaintextPCSPublicIdentity = publicIdentity;
782
783 NSDictionary<NSString*, NSData*>* authenticatedData = [cipheritem makeAuthenticatedDataDictionaryUpdatingCKKSItem: nil encryptionVersion:CKKSItemEncryptionVersion1];
784 cipheritem.encitem = [CKKSItemEncrypter encryptDictionary:item key:itemkey.aessivkey authenticatedData:authenticatedData error:&error];
785 XCTAssertNil(error, "no error encrypting object");
786 XCTAssertNotNil(cipheritem.encitem, "Recieved ciphertext");
787
788 [self.keychainZone addToZone:[cipheritem CKRecordWithZoneID: recordID.zoneID]];
789
790 [self.injectedManager.zoneChangeFetcher notifyZoneChange:nil];
791 [self.keychainView waitForFetchAndIncomingQueueProcessing];
792
793 NSDictionary* query = @{(id)kSecClass: (id)kSecClassGenericPassword,
794 (id)kSecReturnAttributes: @YES,
795 (id)kSecAttrSynchronizable: @YES,
796 (id)kSecAttrAccount: @"account-delete-me",
797 (id)kSecMatchLimit: (id)kSecMatchLimitOne,
798 };
799 CFTypeRef cfresult = NULL;
800 XCTAssertEqual(errSecSuccess, SecItemCopyMatching((__bridge CFDictionaryRef) query, &cfresult), "Found synced item");
801
802 NSDictionary* result = CFBridgingRelease(cfresult);
803 XCTAssertEqualObjects(result[(id)kSecAttrPCSPlaintextServiceIdentifier], servIdentifier, "Received PCS service identifier");
804 XCTAssertEqualObjects(result[(id)kSecAttrPCSPlaintextPublicKey], publicKey, "Received PCS public key");
805 XCTAssertEqualObjects(result[(id)kSecAttrPCSPlaintextPublicIdentity], publicIdentity, "Received PCS public identity");
806 }
807
808 -(void)testPCSUnencryptedFieldsRecieveAuthenticatedFields {
809 [self createAndSaveFakeKeyHierarchy: self.keychainZoneID]; // Make life easy for this test.
810
811 [self startCKKSSubsystem];
812 [self.keychainView waitForKeyHierarchyReadiness];
813 [self.keychainView waitForOperationsOfClass:[CKKSIncomingQueueOperation class]];
814
815 NSNumber* servIdentifier = @3;
816 NSData* publicKey = [@"asdfasdf" dataUsingEncoding:NSUTF8StringEncoding];
817 NSData* publicIdentity = [@"somedata" dataUsingEncoding:NSUTF8StringEncoding];
818
819 NSError* error = nil;
820
821 // Manually encrypt an item
822 NSString* recordName = @"7B598D31-F9C5-481E-98AC-5A507ACB2D85";
823 CKRecordID* recordID = [[CKRecordID alloc] initWithRecordName:recordName zoneID:self.keychainZoneID];
824 NSDictionary* item = [self fakeRecordDictionary: @"account-delete-me" zoneID:self.keychainZoneID];
825 CKKSItem* cipheritem = [[CKKSItem alloc] initWithUUID:recordID.recordName
826 parentKeyUUID:self.keychainZoneKeys.classC.uuid
827 zoneID:recordID.zoneID];
828 CKKSKey* itemkey = [CKKSKey randomKeyWrappedByParent: self.keychainZoneKeys.classC error:&error];
829 XCTAssertNotNil(itemkey, "Got a key");
830 cipheritem.wrappedkey = itemkey.wrappedkey;
831 XCTAssertNotNil(cipheritem.wrappedkey, "Got a wrapped key");
832
833 cipheritem.encver = CKKSItemEncryptionVersion2;
834
835 // This item has the PCS public fields, and they are authenticated (since we're using v2)
836 cipheritem.plaintextPCSServiceIdentifier = servIdentifier;
837 cipheritem.plaintextPCSPublicKey = publicKey;
838 cipheritem.plaintextPCSPublicIdentity = publicIdentity;
839
840 // Use version 2, so PCS plaintext fields will be authenticated
841 NSMutableDictionary<NSString*, NSData*>* authenticatedData = [[cipheritem makeAuthenticatedDataDictionaryUpdatingCKKSItem: nil encryptionVersion:CKKSItemEncryptionVersion2] mutableCopy];
842
843 cipheritem.encitem = [CKKSItemEncrypter encryptDictionary:item key:itemkey.aessivkey authenticatedData:authenticatedData error:&error];
844 XCTAssertNil(error, "no error encrypting object");
845 XCTAssertNotNil(cipheritem.encitem, "Recieved ciphertext");
846
847 [self.keychainZone addToZone:[cipheritem CKRecordWithZoneID: recordID.zoneID]];
848
849 [self.injectedManager.zoneChangeFetcher notifyZoneChange:nil];
850 [self.keychainView waitForFetchAndIncomingQueueProcessing];
851
852 NSDictionary* query = @{(id)kSecClass: (id)kSecClassGenericPassword,
853 (id)kSecReturnAttributes: @YES,
854 (id)kSecAttrSynchronizable: @YES,
855 (id)kSecAttrAccount: @"account-delete-me",
856 (id)kSecMatchLimit: (id)kSecMatchLimitOne,
857 };
858 CFTypeRef cfresult = NULL;
859 XCTAssertEqual(errSecSuccess, SecItemCopyMatching((__bridge CFDictionaryRef) query, &cfresult), "Found synced item");
860
861 NSDictionary* result = CFBridgingRelease(cfresult);
862 XCTAssertEqualObjects(result[(id)kSecAttrPCSPlaintextServiceIdentifier], servIdentifier, "Received PCS service identifier");
863 XCTAssertEqualObjects(result[(id)kSecAttrPCSPlaintextPublicKey], publicKey, "Received PCS public key");
864 XCTAssertEqualObjects(result[(id)kSecAttrPCSPlaintextPublicIdentity], publicIdentity, "Received PCS public identity");
865
866 // Test that if this item is updated, it remains encrypted in v2
867 [self expectCKModifyItemRecords: 1 currentKeyPointerRecords: 1 zoneID:self.keychainZoneID
868 checkItem: [self checkPCSFieldsBlock:self.keychainZoneID
869 PCSServiceIdentifier:(NSNumber *)servIdentifier
870 PCSPublicKey:publicKey
871 PCSPublicIdentity:publicIdentity]];
872 [self updateGenericPassword:@"different password" account:@"account-delete-me"];
873
874 OCMVerifyAllWithDelay(self.mockDatabase, 20);
875 [self waitForCKModifications];
876
877 CKRecord* newRecord = self.keychainZone.currentDatabase[recordID];
878 XCTAssertEqualObjects(newRecord[SecCKRecordPCSServiceIdentifier], servIdentifier, "Didn't change service identifier");
879 XCTAssertEqualObjects(newRecord[SecCKRecordPCSPublicKey], publicKey, "Didn't change public key");
880 XCTAssertEqualObjects(newRecord[SecCKRecordPCSPublicIdentity], publicIdentity, "Didn't change public identity");
881 XCTAssertEqualObjects(newRecord[SecCKRecordEncryptionVersionKey], [NSNumber numberWithInteger:(int) CKKSItemEncryptionVersion2], "Uploaded using encv2");
882 }
883
884 -(void)testResetLocal {
885 // Test starts with nothing in database, but one in our fake CloudKit.
886 [self putFakeKeyHierarchyInCloudKit:self.keychainZoneID];
887 [self expectCKKSTLKSelfShareUpload:self.keychainZoneID];
888 [self saveTLKMaterialToKeychainSimulatingSOS:self.keychainZoneID];
889
890 // Spin up CKKS subsystem.
891 [self startCKKSSubsystem];
892
893 // We expect a single record to be uploaded
894 [self expectCKModifyItemRecords: 1 currentKeyPointerRecords: 1 zoneID:self.keychainZoneID checkItem: [self checkClassCBlock:self.keychainZoneID message:@"Object was encrypted under class C key in hierarchy"]];
895 [self addGenericPassword: @"data" account: @"account-delete-me"];
896 OCMVerifyAllWithDelay(self.mockDatabase, 20);
897
898 // After the local reset, we expect: a fetch, then nothing
899 self.silentFetchesAllowed = false;
900 [self expectCKFetch];
901
902 XCTestExpectation* resetExpectation = [self expectationWithDescription: @"local reset callback occurs"];
903 [self.injectedManager rpcResetLocal:nil reply:^(NSError* result) {
904 XCTAssertNil(result, "no error resetting local");
905 [resetExpectation fulfill];
906 }];
907 [self waitForExpectations:@[resetExpectation] timeout:20];
908
909 OCMVerifyAllWithDelay(self.mockDatabase, 20);
910
911 [self expectCKModifyItemRecords: 1 currentKeyPointerRecords: 1 zoneID:self.keychainZoneID checkItem: [self checkClassABlock:self.keychainZoneID message:@"Object was encrypted under class A key in hierarchy"]];
912 [self addGenericPassword:@"asdf"
913 account:@"account-class-A"
914 viewHint:nil
915 access:(id)kSecAttrAccessibleWhenUnlocked
916 expecting:errSecSuccess
917 message:@"Adding class A item"];
918 OCMVerifyAllWithDelay(self.mockDatabase, 20);
919 }
920
921 -(void)testResetLocalWhileUntrusted {
922 // We're "logged in to" cloudkit but not in circle.
923 self.mockSOSAdapter.circleStatus = kSOSCCNotInCircle;
924 [self.accountStateTracker notifyCircleStatusChangeAndWaitForSignal];
925 self.silentFetchesAllowed = false;
926
927 // Test starts with local TLK and key hierarchy in our fake cloudkit
928 [self putFakeKeyHierarchyInCloudKit:self.keychainZoneID];
929 [self saveTLKMaterialToKeychainSimulatingSOS:self.keychainZoneID];
930
931 // Spin up CKKS subsystem. It should fetch once.
932 [self expectCKFetch];
933 [self startCKKSSubsystem];
934
935 XCTAssertEqual(0, [self.keychainView.loggedIn wait:500*NSEC_PER_MSEC], "Should have been told of a 'login' event on startup");
936
937 XCTAssertEqual(0, [self.keychainView.keyHierarchyConditions[SecCKKSZoneKeyStateWaitForTrust] wait:20*NSEC_PER_SEC], @"Key state should arrive at 'waitfortrust''");
938 OCMVerifyAllWithDelay(self.mockDatabase, 20);
939
940 NSData* changeTokenData = [[[NSUUID UUID] UUIDString] dataUsingEncoding:NSUTF8StringEncoding];
941 CKServerChangeToken* changeToken = [[CKServerChangeToken alloc] initWithData:changeTokenData];
942 [self.keychainView dispatchSyncWithSQLTransaction:^CKKSDatabaseTransactionResult{
943 CKKSZoneStateEntry* ckse = [CKKSZoneStateEntry state:self.keychainView.zoneName];
944 ckse.changeToken = changeToken;
945
946 NSError* error = nil;
947 [ckse saveToDatabase:&error];
948 XCTAssertNil(error, "No error saving new zone state to database");
949 return CKKSDatabaseTransactionCommit;
950 }];
951
952 // after the reset, CKKS should refetch what's available
953 [self expectCKFetch];
954
955 XCTestExpectation* resetExpectation = [self expectationWithDescription: @"local reset callback occurs"];
956 [self.injectedManager rpcResetLocal:nil reply:^(NSError* result) {
957 XCTAssertNil(result, "no error resetting local");
958 ckksnotice_global("ckks", "Received a rpcResetLocal callback");
959
960 [self.keychainView dispatchSyncWithReadOnlySQLTransaction:^{
961 CKKSZoneStateEntry* ckse = [CKKSZoneStateEntry state:self.keychainView.zoneName];
962 XCTAssertNotEqualObjects(changeToken, ckse.changeToken, "Change token is reset");
963 }];
964
965 [resetExpectation fulfill];
966 }];
967
968 [self waitForExpectations:@[resetExpectation] timeout:20];
969
970 XCTAssertEqual(0, [self.keychainView.keyHierarchyConditions[SecCKKSZoneKeyStateWaitForTrust] wait:20*NSEC_PER_SEC], @"Key state should arrive at 'waitfortrust''");
971
972 // Now regain trust, and see what happens! It should use the existing fetch, pick up the old key hierarchy, and use it
973 [self expectCKKSTLKSelfShareUpload:self.keychainZoneID];
974
975 self.mockSOSAdapter.circleStatus = kSOSCCInCircle;
976 [self.accountStateTracker notifyCircleStatusChangeAndWaitForSignal];
977 [self beginSOSTrustedViewOperation:self.keychainView];
978
979 OCMVerifyAllWithDelay(self.mockDatabase, 20);
980 XCTAssertEqual(0, [self.keychainView.keyHierarchyConditions[SecCKKSZoneKeyStateReady] wait:20*NSEC_PER_SEC], @"Key state should arrive at 'ready''");
981
982 [self expectCKModifyItemRecords: 1 currentKeyPointerRecords: 1 zoneID:self.keychainZoneID checkItem:[self checkClassABlock:self.keychainZoneID message:@"Object was encrypted under class A key in hierarchy"]];
983 [self addGenericPassword:@"asdf"
984 account:@"account-class-A"
985 viewHint:nil
986 access:(id)kSecAttrAccessibleWhenUnlocked
987 expecting:errSecSuccess
988 message:@"Adding class A item"];
989 OCMVerifyAllWithDelay(self.mockDatabase, 20);
990 }
991
992 - (void)testResetLocalWhileLoggedOut {
993 self.mockSOSAdapter.circleStatus = kSOSCCNotInCircle;
994 self.accountStatus = CKAccountStatusNoAccount;
995 [self.accountStateTracker notifyCircleStatusChangeAndWaitForSignal];
996 self.silentFetchesAllowed = false;
997
998 // Test starts with local TLK and key hierarchy in our fake cloudkit
999 [self putFakeKeyHierarchyInCloudKit:self.keychainZoneID];
1000 [self saveTLKMaterialToKeychainSimulatingSOS:self.keychainZoneID];
1001
1002 [self startCKKSSubsystem];
1003
1004 XCTAssertEqual(0, [self.keychainView.loggedOut wait:20*NSEC_PER_SEC], "CKKS should positively log out");
1005 XCTAssertEqual(0, [self.keychainView.keyHierarchyConditions[SecCKKSZoneKeyStateLoggedOut] wait:20*NSEC_PER_SEC], @"Key state should arrive at 'loggedout'");
1006
1007 // Can we reset local data while logged out?
1008 XCTestExpectation* resetExpectation = [self expectationWithDescription: @"local reset callback occurs"];
1009 [self.injectedManager rpcResetLocal:nil reply:^(NSError* result) {
1010 XCTAssertNil(result, "no error resetting local");
1011 ckksnotice_global("ckks", "Received a rpcResetLocal callback");
1012
1013 [resetExpectation fulfill];
1014 }];
1015
1016 [self waitForExpectations:@[resetExpectation] timeout:20];
1017
1018 XCTAssertEqual(0, [self.keychainView.keyHierarchyConditions[SecCKKSZoneKeyStateLoggedOut] wait:20*NSEC_PER_SEC], @"Key state should arrive at 'loggedout'");
1019 }
1020
1021 -(void)testResetLocalMultipleTimes {
1022 // Test starts with nothing in database, but one in our fake CloudKit.
1023 [self putFakeKeyHierarchyInCloudKit:self.keychainZoneID];
1024 [self expectCKKSTLKSelfShareUpload:self.keychainZoneID];
1025 [self saveTLKMaterialToKeychainSimulatingSOS:self.keychainZoneID];
1026
1027 // Spin up CKKS subsystem.
1028 [self startCKKSSubsystem];
1029
1030 // We expect a single record to be uploaded
1031 XCTAssertEqual(0, [self.keychainView.keyHierarchyConditions[SecCKKSZoneKeyStateReady] wait:20*NSEC_PER_SEC], "CKKS entered 'ready'");
1032 [self expectCKModifyItemRecords:1 currentKeyPointerRecords:1 zoneID:self.keychainZoneID
1033 checkItem:[self checkClassCBlock:self.keychainZoneID message:@"Object was encrypted under class C key in hierarchy"]];
1034 [self addGenericPassword: @"data" account: @"account-delete-me"];
1035 OCMVerifyAllWithDelay(self.mockDatabase, 20);
1036 [self waitForCKModifications];
1037
1038 // We're going to request a bunch of CloudKit resets, but hold them from finishing
1039 [self holdCloudKitFetches];
1040
1041 XCTestExpectation* resetExpectation0 = [self expectationWithDescription: @"reset callback(0) occurs"];
1042 XCTestExpectation* resetExpectation1 = [self expectationWithDescription: @"reset callback(1) occurs"];
1043 XCTestExpectation* resetExpectation2 = [self expectationWithDescription: @"reset callback(2) occurs"];
1044 [self.injectedManager rpcResetLocal:nil reply:^(NSError* result) {
1045 XCTAssertNil(result, "should receive no error resetting local");
1046 secnotice("ckksreset", "Received a rpcResetLocal(0) callback");
1047 [resetExpectation0 fulfill];
1048 }];
1049 [self.injectedManager rpcResetLocal:nil reply:^(NSError* result) {
1050 XCTAssertNil(result, "should receive no error resetting local");
1051 secnotice("ckksreset", "Received a rpcResetLocal(1) callback");
1052 [resetExpectation1 fulfill];
1053 }];
1054 [self.injectedManager rpcResetLocal:nil reply:^(NSError* result) {
1055 XCTAssertNil(result, "should receive no error resetting local");
1056 secnotice("ckksreset", "Received a rpcResetLocal(2) callback");
1057 [resetExpectation2 fulfill];
1058 }];
1059
1060 // After the reset(s), we expect no uploads. Let the resets flow!
1061 [self releaseCloudKitFetchHold];
1062 [self waitForExpectations:@[resetExpectation0, resetExpectation1, resetExpectation2] timeout:20];
1063 XCTAssertEqual(0, [self.keychainView.keyHierarchyConditions[SecCKKSZoneKeyStateReady] wait:20*NSEC_PER_SEC], "CKKS entered 'ready'");
1064
1065 OCMVerifyAllWithDelay(self.mockDatabase, 20);
1066
1067 [self expectCKModifyItemRecords:1 currentKeyPointerRecords:1 zoneID:self.keychainZoneID
1068 checkItem:[self checkClassABlock:self.keychainZoneID message:@"Object was encrypted under class A key in hierarchy"]];
1069 [self addGenericPassword:@"asdf"
1070 account:@"account-class-A"
1071 viewHint:nil
1072 access:(id)kSecAttrAccessibleWhenUnlocked
1073 expecting:errSecSuccess
1074 message:@"Adding class A item"];
1075 OCMVerifyAllWithDelay(self.mockDatabase, 20);
1076 }
1077
1078 -(void)testResetCloudKitZone {
1079 self.suggestTLKUpload = OCMClassMock([CKKSNearFutureScheduler class]);
1080 OCMExpect([self.suggestTLKUpload trigger]);
1081
1082 self.silentZoneDeletesAllowed = true;
1083
1084 // Test starts with nothing in database, but one in our fake CloudKit.
1085 [self putFakeKeyHierarchyInCloudKit:self.keychainZoneID];
1086 [self expectCKKSTLKSelfShareUpload:self.keychainZoneID];
1087 [self saveTLKMaterialToKeychainSimulatingSOS:self.keychainZoneID];
1088
1089 // Spin up CKKS subsystem.
1090 [self startCKKSSubsystem];
1091
1092 // We expect a single record to be uploaded
1093 [self expectCKModifyItemRecords: 1 currentKeyPointerRecords: 1 zoneID:self.keychainZoneID checkItem: [self checkClassCBlock:self.keychainZoneID message:@"Object was encrypted under class C key in hierarchy"]];
1094 [self addGenericPassword: @"data" account: @"account-delete-me"];
1095 OCMVerifyAllWithDelay(self.mockDatabase, 20);
1096 [self waitForCKModifications];
1097 XCTAssertEqual(0, [self.keychainView.keyHierarchyConditions[SecCKKSZoneKeyStateReady] wait:20*NSEC_PER_SEC], "CKKS entered 'ready'");
1098
1099 // During the reset, Octagon will upload the key hierarchy, and then CKKS will upload the class C item
1100 [self expectCKModifyItemRecords: 1 currentKeyPointerRecords: 1 zoneID:self.keychainZoneID checkItem: [self checkClassCBlock:self.keychainZoneID message:@"Object was encrypted under class C key in hierarchy"]];
1101
1102 XCTestExpectation* resetExpectation = [self expectationWithDescription: @"reset callback occurs"];
1103 [self.injectedManager rpcResetCloudKit:nil reason:@"reset-test" reply:^(NSError* result) {
1104 XCTAssertNil(result, "no error resetting cloudkit");
1105 ckksnotice_global("ckks", "Received a resetCloudKit callback");
1106 [resetExpectation fulfill];
1107 }];
1108
1109 // Sneak in and perform Octagon's duties
1110 OCMVerifyAllWithDelay(self.suggestTLKUpload, 10);
1111 [self performOctagonTLKUpload:self.ckksViews];
1112
1113 [self waitForExpectations:@[resetExpectation] timeout:20];
1114
1115 OCMVerifyAllWithDelay(self.mockDatabase, 20);
1116
1117 [self expectCKModifyItemRecords: 1 currentKeyPointerRecords: 1 zoneID:self.keychainZoneID checkItem: [self checkClassABlock:self.keychainZoneID message:@"Object was encrypted under class A key in hierarchy"]];
1118 [self addGenericPassword:@"asdf"
1119 account:@"account-class-A"
1120 viewHint:nil
1121 access:(id)kSecAttrAccessibleWhenUnlocked
1122 expecting:errSecSuccess
1123 message:@"Adding class A item"];
1124 OCMVerifyAllWithDelay(self.mockDatabase, 20);
1125
1126 XCTAssertEqual(0, [self.keychainView.keyHierarchyConditions[SecCKKSZoneKeyStateReady] wait:20*NSEC_PER_SEC], "CKKS entered 'ready'");
1127 }
1128
1129 - (void)testResetCloudKitZoneCloudKitRejects {
1130 self.suggestTLKUpload = OCMClassMock([CKKSNearFutureScheduler class]);
1131 OCMExpect([self.suggestTLKUpload trigger]);
1132
1133 self.nextModifyRecordZonesError = [[CKPrettyError alloc] initWithDomain:CKErrorDomain
1134 code:CKErrorZoneBusy
1135 userInfo:@{
1136 CKErrorRetryAfterKey: @(0.2),
1137 NSUnderlyingErrorKey: [[CKPrettyError alloc] initWithDomain:CKErrorDomain
1138 code:2029
1139 userInfo:nil],
1140 }];
1141 self.silentZoneDeletesAllowed = true;
1142
1143 // Test starts with nothing in database, but one in our fake CloudKit.
1144 [self putFakeKeyHierarchyInCloudKit:self.keychainZoneID];
1145 [self expectCKKSTLKSelfShareUpload:self.keychainZoneID];
1146 [self saveTLKMaterialToKeychainSimulatingSOS:self.keychainZoneID];
1147
1148 // Spin up CKKS subsystem.
1149 [self startCKKSSubsystem];
1150
1151 // We expect a single record to be uploaded
1152 [self expectCKModifyItemRecords: 1 currentKeyPointerRecords: 1 zoneID:self.keychainZoneID checkItem: [self checkClassCBlock:self.keychainZoneID message:@"Object was encrypted under class C key in hierarchy"]];
1153 [self addGenericPassword: @"data" account: @"account-delete-me"];
1154 OCMVerifyAllWithDelay(self.mockDatabase, 20);
1155 [self waitForCKModifications];
1156
1157 // During the reset, Octagon will upload the key hierarchy, and then CKKS will upload the class C item
1158 [self expectCKModifyItemRecords: 1 currentKeyPointerRecords: 1 zoneID:self.keychainZoneID checkItem: [self checkClassCBlock:self.keychainZoneID message:@"Object was encrypted under class C key in hierarchy"]];
1159
1160 XCTestExpectation* resetExpectation = [self expectationWithDescription: @"reset callback occurs"];
1161 [self.injectedManager rpcResetCloudKit:nil reason:@"reset-test" reply:^(NSError* result) {
1162 XCTAssertNil(result, "no error resetting cloudkit");
1163 ckksnotice_global("ckks", "Received a resetCloudKit callback");
1164 [resetExpectation fulfill];
1165 }];
1166
1167 // Sneak in and perform Octagon's duties
1168 OCMVerifyAllWithDelay(self.suggestTLKUpload, 10);
1169 [self performOctagonTLKUpload:self.ckksViews];
1170
1171 [self waitForExpectations:@[resetExpectation] timeout:20];
1172
1173 OCMVerifyAllWithDelay(self.mockDatabase, 20);
1174
1175 [self expectCKModifyItemRecords: 1 currentKeyPointerRecords: 1 zoneID:self.keychainZoneID checkItem: [self checkClassABlock:self.keychainZoneID message:@"Object was encrypted under class A key in hierarchy"]];
1176 [self addGenericPassword:@"asdf"
1177 account:@"account-class-A"
1178 viewHint:nil
1179 access:(id)kSecAttrAccessibleWhenUnlocked
1180 expecting:errSecSuccess
1181 message:@"Adding class A item"];
1182 OCMVerifyAllWithDelay(self.mockDatabase, 20);
1183
1184 XCTAssertNil(self.nextModifyRecordZonesError, "Record zone modification error should have been cleared");
1185 }
1186
1187 - (void)testResetCloudKitZoneDuringWaitForTLK {
1188 self.suggestTLKUpload = OCMClassMock([CKKSNearFutureScheduler class]);
1189 OCMExpect([self.suggestTLKUpload trigger]);
1190
1191 self.silentZoneDeletesAllowed = true;
1192
1193 // Test starts with nothing in database, but one in our fake CloudKit.
1194 // No TLK, though!
1195 [self putFakeKeyHierarchyInCloudKit:self.keychainZoneID];
1196 [self putFakeDeviceStatusInCloudKit:self.keychainZoneID];
1197
1198 // Spin up CKKS subsystem.
1199 [self startCKKSSubsystem];
1200
1201 // No records should be uploaded
1202 [self addGenericPassword: @"data" account: @"account-delete-me"];
1203
1204 XCTAssertEqual(0, [self.keychainView.keyHierarchyConditions[SecCKKSZoneKeyStateWaitForTLK] wait:20*NSEC_PER_SEC], "CKKS should have entered waitfortlk");
1205
1206 // Restart CKKS to really get in the spirit of waitfortlk (and get a pending processOutgoingQueue operation going)
1207 self.keychainView = [[CKKSViewManager manager] restartZone: self.keychainZoneID.zoneName];
1208 self.ckksViews = [NSMutableSet setWithObject:self.keychainView];
1209
1210 [self beginSOSTrustedViewOperation:self.keychainView];
1211 XCTAssertEqual(0, [self.keychainView.keyHierarchyConditions[SecCKKSZoneKeyStateWaitForTLK] wait:20*NSEC_PER_SEC], "CKKS entered waitfortlk");
1212
1213 CKKSOutgoingQueueOperation* outgoingOp = [self.keychainView processOutgoingQueue:nil];
1214 XCTAssertTrue([outgoingOp isPending], "outgoing queue processing should be on hold");
1215
1216 // Now, reset everything. The outgoingOp should get cancelled.
1217 // We expect a key hierarchy upload, and then the class C item upload
1218 [self expectCKModifyItemRecords: 1 currentKeyPointerRecords: 1 zoneID:self.keychainZoneID
1219 checkItem: [self checkClassCBlock:self.keychainZoneID message:@"Object was encrypted under class C key in hierarchy"]];
1220
1221 XCTestExpectation* resetExpectation = [self expectationWithDescription: @"reset callback occurs"];
1222 [self.injectedManager rpcResetCloudKit:nil reason:@"reset-test" reply:^(NSError* result) {
1223 XCTAssertNil(result, "no error resetting cloudkit");
1224 [resetExpectation fulfill];
1225 }];
1226
1227 // Sneak in and perform Octagon's duties
1228 OCMVerifyAllWithDelay(self.suggestTLKUpload, 10);
1229 [self performOctagonTLKUpload:self.ckksViews];
1230
1231 [self waitForExpectations:@[resetExpectation] timeout:20];
1232
1233 OCMVerifyAllWithDelay(self.mockDatabase, 20);
1234
1235 // And adding another item works too
1236 [self expectCKModifyItemRecords: 1 currentKeyPointerRecords: 1 zoneID:self.keychainZoneID checkItem: [self checkClassABlock:self.keychainZoneID message:@"Object was encrypted under class A key in hierarchy"]];
1237 [self addGenericPassword:@"asdf"
1238 account:@"account-class-A"
1239 viewHint:nil
1240 access:(id)kSecAttrAccessibleWhenUnlocked
1241 expecting:errSecSuccess
1242 message:@"Adding class A item"];
1243 OCMVerifyAllWithDelay(self.mockDatabase, 20);
1244
1245 XCTAssertTrue([outgoingOp isFinished], "old ProcessOutgoingQueue should be finished");
1246 }
1247
1248 /*
1249 * This test doesn't work, since the resetLocal fails. CKKS gets back into waitfortlk
1250 * but that isn't considered a successful resetLocal.
1251 *
1252 - (void)testResetLocalDuringWaitForTLK {
1253 // Test starts with nothing in database, but one in our fake CloudKit.
1254 // No TLK, though!
1255 [self putFakeKeyHierarchyInCloudKit:self.keychainZoneID];
1256
1257 // Spin up CKKS subsystem.
1258 [self startCKKSSubsystem];
1259
1260 // No records should be uploaded
1261 [self addGenericPassword: @"data" account: @"account-delete-me"];
1262
1263 XCTAssertEqual(0, [self.keychainView.keyHierarchyConditions[SecCKKSZoneKeyStateWaitForTLK] wait:20*NSEC_PER_SEC], "CKKS should have entered waitfortlk");
1264
1265 // Restart CKKS to really get in the spirit of waitfortlk (and get a pending processOutgoingQueue operation going)
1266 self.keychainView = [[CKKSViewManager manager] restartZone: self.keychainZoneID.zoneName];
1267 [self beginSOSTrustedViewOperation:self.keychainView];
1268 XCTAssertEqual(0, [self.keychainView.keyHierarchyConditions[SecCKKSZoneKeyStateWaitForTLK] wait:20*NSEC_PER_SEC], "CKKS entered waitfortlk");
1269
1270 CKKSOutgoingQueueOperation* outgoingOp = [self.keychainView processOutgoingQueue:nil];
1271 XCTAssertTrue([outgoingOp isPending], "outgoing queue processing should be on hold");
1272
1273 // Now, reset everything. The outgoingOp should get cancelled.
1274 // We expect a key hierarchy upload, and then the class C item upload
1275 [self expectCKModifyKeyRecords: 3 currentKeyPointerRecords: 3 tlkShareRecords:3 zoneID:self.keychainZoneID];
1276 [self expectCKModifyItemRecords: 1 currentKeyPointerRecords: 1 zoneID:self.keychainZoneID
1277 checkItem: [self checkClassCBlock:self.keychainZoneID message:@"Object was encrypted under class C key in hierarchy"]];
1278
1279 XCTestExpectation* resetExpectation = [self expectationWithDescription: @"reset callback occurs"];
1280 [self.injectedManager rpcResetLocal:nil reply:^(NSError* result) {
1281 XCTAssertNil(result, "no error resetting local");
1282 [resetExpectation fulfill];
1283 }];
1284 [self waitForExpectations:@[resetExpectation] timeout:20];
1285
1286 XCTAssertTrue([outgoingOp isCancelled], "old stuck ProcessOutgoingQueue should be cancelled");
1287 OCMVerifyAllWithDelay(self.mockDatabase, 20);
1288
1289 // And adding another item works too
1290 [self expectCKModifyItemRecords: 1 currentKeyPointerRecords: 1 zoneID:self.keychainZoneID checkItem: [self checkClassABlock:self.keychainZoneID message:@"Object was encrypted under class A key in hierarchy"]];
1291 [self addGenericPassword:@"asdf"
1292 account:@"account-class-A"
1293 viewHint:nil
1294 access:(id)kSecAttrAccessibleWhenUnlocked
1295 expecting:errSecSuccess
1296 message:@"Adding class A item"];
1297 OCMVerifyAllWithDelay(self.mockDatabase, 20);
1298 }*/
1299
1300 -(void)testResetCloudKitZoneWhileUntrusted {
1301 self.silentZoneDeletesAllowed = true;
1302
1303 // We're "logged in to" cloudkit but not in circle.
1304 self.mockSOSAdapter.circleStatus = kSOSCCNotInCircle;
1305 [self.accountStateTracker notifyCircleStatusChangeAndWaitForSignal];
1306
1307 // Test starts with nothing in database, but one in our fake CloudKit.
1308 [self putFakeKeyHierarchyInCloudKit:self.keychainZoneID];
1309
1310 // Spin up CKKS subsystem.
1311 [self startCKKSSubsystem];
1312
1313 // Since CKKS is untrusted, it'll fetch the zone but then get stuck
1314 XCTAssertEqual(0, [self.keychainView.keyHierarchyConditions[SecCKKSZoneKeyStateWaitForTrust] wait:20*NSEC_PER_SEC], "CKKS entered 'waitfortrust'");
1315
1316 CKRecord* ckr = [self createFakeRecord: self.keychainZoneID recordName:@"7B598D31-F9C5-481E-98AC-5A507ACB2D85"];
1317 [self.keychainZone addToZone: ckr];
1318
1319 XCTAssertNotNil(self.keychainZone.currentDatabase, "Zone exists");
1320 XCTAssertNotNil(self.keychainZone.currentDatabase[ckr.recordID], "An item exists in the fake zone");
1321
1322 // Make the zone, so we know if it was deleted
1323 self.keychainZone.flag = true;
1324
1325 XCTestExpectation* resetExpectation = [self expectationWithDescription: @"reset callback occurs"];
1326 [self.injectedManager rpcResetCloudKit:nil reason:@"reset-test" reply:^(NSError* result) {
1327 XCTAssertNil(result, "no error resetting cloudkit");
1328 ckksnotice_global("ckks", "Received a resetCloudKit callback");
1329 [resetExpectation fulfill];
1330 }];
1331
1332 [self waitForExpectations:@[resetExpectation] timeout:20];
1333
1334 XCTAssertEqual(0, [self.keychainView.keyHierarchyConditions[SecCKKSZoneKeyStateWaitForTLKCreation] wait:20*NSEC_PER_SEC], "CKKS entered 'waitfortlkcreation'");
1335
1336 XCTAssertFalse(self.keychainZone.flag, "Zone was deleted at some point");
1337 OCMVerifyAllWithDelay(self.mockDatabase, 20);
1338
1339 // Now log in, and see what happens! It should create the zone again and upload a whole new key hierarchy
1340 self.silentFetchesAllowed = true;
1341 self.mockSOSAdapter.circleStatus = kSOSCCInCircle;
1342 [self.accountStateTracker notifyCircleStatusChangeAndWaitForSignal];
1343 [self beginSOSTrustedViewOperation:self.keychainView];
1344
1345 [self performOctagonTLKUpload:self.ckksViews];
1346
1347 OCMVerifyAllWithDelay(self.mockDatabase, 20);
1348
1349 [self expectCKModifyItemRecords: 1 currentKeyPointerRecords: 1 zoneID:self.keychainZoneID checkItem:[self checkClassABlock:self.keychainZoneID message:@"Object was encrypted under class A key in hierarchy"]];
1350 [self addGenericPassword:@"asdf"
1351 account:@"account-class-A"
1352 viewHint:nil
1353 access:(id)kSecAttrAccessibleWhenUnlocked
1354 expecting:errSecSuccess
1355 message:@"Adding class A item"];
1356 OCMVerifyAllWithDelay(self.mockDatabase, 20);
1357 }
1358
1359 - (void)testResetCloudKitZoneMultipleTimes {
1360 self.suggestTLKUpload = OCMClassMock([CKKSNearFutureScheduler class]);
1361 OCMExpect([self.suggestTLKUpload trigger]);
1362
1363 self.silentZoneDeletesAllowed = true;
1364
1365 // Test starts with nothing in database, but one in our fake CloudKit.
1366 [self putFakeKeyHierarchyInCloudKit:self.keychainZoneID];
1367 [self expectCKKSTLKSelfShareUpload:self.keychainZoneID];
1368 [self saveTLKMaterialToKeychainSimulatingSOS:self.keychainZoneID];
1369
1370 // Spin up CKKS subsystem.
1371 [self startCKKSSubsystem];
1372
1373 // We expect a single record to be uploaded
1374 XCTAssertEqual(0, [self.keychainView.keyHierarchyConditions[SecCKKSZoneKeyStateReady] wait:20*NSEC_PER_SEC], "CKKS entered 'ready'");
1375 [self expectCKModifyItemRecords:1 currentKeyPointerRecords:1 zoneID:self.keychainZoneID
1376 checkItem:[self checkClassCBlock:self.keychainZoneID message:@"Object was encrypted under class C key in hierarchy"]];
1377 [self addGenericPassword: @"data" account: @"account-delete-me"];
1378 OCMVerifyAllWithDelay(self.mockDatabase, 20);
1379 [self waitForCKModifications];
1380
1381 // We're going to request a bunch of CloudKit resets, but hold them from finishing
1382 [self holdCloudKitFetches];
1383
1384 XCTestExpectation* resetExpectation0 = [self expectationWithDescription: @"reset callback(0) occurs"];
1385 XCTestExpectation* resetExpectation1 = [self expectationWithDescription: @"reset callback(1) occurs"];
1386 XCTestExpectation* resetExpectation2 = [self expectationWithDescription: @"reset callback(2) occurs"];
1387 [self.injectedManager rpcResetCloudKit:nil reason:@"reset-test" reply:^(NSError* result) {
1388 XCTAssertNil(result, "should receive no error resetting cloudkit");
1389 secnotice("ckksreset", "Received a resetCloudKit(0) callback");
1390 [resetExpectation0 fulfill];
1391 }];
1392 [self.injectedManager rpcResetCloudKit:nil reason:@"reset-test" reply:^(NSError* result) {
1393 XCTAssertNil(result, "should receive no error resetting cloudkit");
1394 secnotice("ckksreset", "Received a resetCloudKit(1) callback");
1395 [resetExpectation1 fulfill];
1396 }];
1397 [self.injectedManager rpcResetCloudKit:nil reason:@"reset-test" reply:^(NSError* result) {
1398 XCTAssertNil(result, "should receive no error resetting cloudkit");
1399 secnotice("ckksreset", "Received a resetCloudKit(2) callback");
1400 [resetExpectation2 fulfill];
1401 }];
1402
1403 // After the reset(s), we expect a key hierarchy upload, and then the class C item upload
1404 [self expectCKModifyItemRecords:1 currentKeyPointerRecords:1 zoneID:self.keychainZoneID
1405 checkItem:[self checkClassCBlock:self.keychainZoneID message:@"Object was encrypted under class C key in hierarchy"]];
1406
1407 // And let the resets flow
1408 [self releaseCloudKitFetchHold];
1409
1410 OCMVerifyAllWithDelay(self.suggestTLKUpload, 10);
1411 [self performOctagonTLKUpload:self.ckksViews];
1412
1413 [self waitForExpectations:@[resetExpectation0, resetExpectation1, resetExpectation2] timeout:20];
1414 XCTAssertEqual(0, [self.keychainView.keyHierarchyConditions[SecCKKSZoneKeyStateReady] wait:20*NSEC_PER_SEC], "CKKS entered 'ready'");
1415
1416 OCMVerifyAllWithDelay(self.mockDatabase, 20);
1417
1418 [self expectCKModifyItemRecords:1 currentKeyPointerRecords:1 zoneID:self.keychainZoneID
1419 checkItem:[self checkClassABlock:self.keychainZoneID message:@"Object was encrypted under class A key in hierarchy"]];
1420 [self addGenericPassword:@"asdf"
1421 account:@"account-class-A"
1422 viewHint:nil
1423 access:(id)kSecAttrAccessibleWhenUnlocked
1424 expecting:errSecSuccess
1425 message:@"Adding class A item"];
1426 OCMVerifyAllWithDelay(self.mockDatabase, 20);
1427 }
1428
1429 - (void)testRPCFetchAndProcessWhileCloudKitNotResponding {
1430 [self createAndSaveFakeKeyHierarchy: self.keychainZoneID]; // Make life easy for this test.
1431 [self startCKKSSubsystem];
1432
1433 XCTAssertEqual(0, [self.keychainView.keyHierarchyConditions[SecCKKSZoneKeyStateReady] wait:20*NSEC_PER_SEC], "CKKS entered 'ready'");
1434 [self holdCloudKitFetches];
1435
1436 XCTestExpectation* callbackOccurs = [self expectationWithDescription:@"callback-occurs"];
1437 [self.ckksControl rpcFetchAndProcessChanges:nil reply:^(NSError * _Nullable error) {
1438 // done! we should have an underlying error of "fetch isn't working"
1439 XCTAssertNotNil(error, "Should have received an error attempting to fetch and process");
1440 NSError* underlying = error.userInfo[NSUnderlyingErrorKey];
1441 XCTAssertNotNil(underlying, "Should have received an underlying error");
1442 XCTAssertEqualObjects(underlying.domain, CKKSResultDescriptionErrorDomain, "Underlying error should be CKKSResultDescriptionErrorDomain");
1443 XCTAssertEqual(underlying.code, CKKSResultDescriptionPendingSuccessfulFetch, "Underlying error should be 'pending fetch'");
1444 [callbackOccurs fulfill];
1445 }];
1446
1447 [self waitForExpectations:@[callbackOccurs] timeout:20];
1448 [self releaseCloudKitFetchHold];
1449 OCMVerifyAllWithDelay(self.mockDatabase, 20);
1450 }
1451
1452 - (void)testRPCFetchAndProcessWhileCloudKitErroring {
1453 [self createAndSaveFakeKeyHierarchy: self.keychainZoneID]; // Make life easy for this test.
1454 [self startCKKSSubsystem];
1455
1456 XCTAssertEqual(0, [self.keychainView.keyHierarchyConditions[SecCKKSZoneKeyStateReady] wait:20*NSEC_PER_SEC], "CKKS entered 'ready'");
1457
1458 [self.keychainZone failNextFetchWith:[[CKPrettyError alloc] initWithDomain:CKErrorDomain
1459 code:CKErrorRequestRateLimited
1460 userInfo:@{CKErrorRetryAfterKey : [NSNumber numberWithInt:30]}]];
1461
1462 XCTestExpectation* callbackOccurs = [self expectationWithDescription:@"callback-occurs"];
1463 [self.ckksControl rpcFetchAndProcessChanges:nil reply:^(NSError * _Nullable error) {
1464 // done! we should have an underlying error of "fetch isn't working"
1465 XCTAssertNotNil(error, "Should have received an error attempting to fetch and process");
1466 NSError* underlying = error.userInfo[NSUnderlyingErrorKey];
1467 XCTAssertNotNil(underlying, "Should have received an underlying error");
1468 XCTAssertEqualObjects(underlying.domain, CKKSResultDescriptionErrorDomain, "Underlying error should be CKKSResultDescriptionErrorDomain");
1469 XCTAssertEqual(underlying.code, CKKSResultDescriptionPendingSuccessfulFetch, "Underlying error should be 'pending fetch'");
1470
1471 NSError* underunderlying = underlying.userInfo[NSUnderlyingErrorKey];
1472 XCTAssertNotNil(underunderlying, "Should have received another layer of underlying error");
1473 XCTAssertEqualObjects(underunderlying.domain, CKErrorDomain, "Underlying error should be CKErrorDomain");
1474 XCTAssertEqual(underunderlying.code, CKErrorRequestRateLimited, "Underlying error should be 'rate limited'");
1475
1476 [callbackOccurs fulfill];
1477 }];
1478
1479 [self waitForExpectations:@[callbackOccurs] timeout:20];
1480 OCMVerifyAllWithDelay(self.mockDatabase, 20);
1481 }
1482
1483 - (void)testRPCFetchAndProcessWhileInWaitForTLK {
1484 [self putFakeKeyHierarchyInCloudKit:self.keychainZoneID];
1485 [self putFakeDeviceStatusInCloudKit:self.keychainZoneID];
1486 [self startCKKSSubsystem];
1487
1488 XCTAssertEqual(0, [self.keychainView.keyHierarchyConditions[SecCKKSZoneKeyStateWaitForTLK] wait:20*NSEC_PER_SEC], "CKKS entered waitfortlk");
1489
1490 XCTestExpectation* callbackOccurs = [self expectationWithDescription:@"callback-occurs"];
1491 [self.ckksControl rpcFetchAndProcessChanges:nil reply:^(NSError * _Nullable error) {
1492 // done! we should have an underlying error of "fetch isn't working"
1493 XCTAssertNotNil(error, "Should have received an error attempting to fetch and process");
1494 NSError* underlying = error.userInfo[NSUnderlyingErrorKey];
1495 XCTAssertNotNil(underlying, "Should have received an underlying error");
1496 XCTAssertEqualObjects(underlying.domain, CKKSResultDescriptionErrorDomain, "Underlying error should be CKKSResultDescriptionErrorDomain");
1497 XCTAssertEqual(underlying.code, CKKSResultDescriptionPendingKeyReady, "Underlying error should be 'pending key ready'");
1498 [callbackOccurs fulfill];
1499 }];
1500
1501 [self waitForExpectations:@[callbackOccurs] timeout:20];
1502 OCMVerifyAllWithDelay(self.mockDatabase, 20);
1503 }
1504
1505 - (void)testRPCTLKMissingWhenMissing {
1506 // Bring CKKS up in waitfortlk
1507 [self putFakeKeyHierarchyInCloudKit:self.keychainZoneID];
1508 [self putFakeDeviceStatusInCloudKit:self.keychainZoneID];
1509 [self startCKKSSubsystem];
1510
1511 XCTAssertEqual(0, [self.keychainView.keyHierarchyConditions[SecCKKSZoneKeyStateWaitForTLK] wait:20*NSEC_PER_SEC], "CKKS entered waitfortlk");
1512
1513 XCTestExpectation* callbackOccurs = [self expectationWithDescription:@"callback-occurs"];
1514
1515 [self.ckksControl rpcTLKMissing:@"keychain" reply:^(bool missing) {
1516 XCTAssertTrue(missing, "TLKs should be missing");
1517 [callbackOccurs fulfill];
1518 }];
1519
1520 [self waitForExpectations:@[callbackOccurs] timeout:20];
1521
1522 OCMVerifyAllWithDelay(self.mockDatabase, 20);
1523 }
1524
1525 - (void)testRPCTLKMissingWhenFound {
1526 // Bring CKKS up in 'ready'
1527 [self putFakeKeyHierarchyInCloudKit:self.keychainZoneID];
1528 [self saveTLKMaterialToKeychain:self.keychainZoneID];
1529 [self expectCKKSTLKSelfShareUpload:self.keychainZoneID];
1530 [self startCKKSSubsystem];
1531
1532 XCTAssertEqual(0, [self.keychainView.keyHierarchyConditions[SecCKKSZoneKeyStateReady] wait:20*NSEC_PER_SEC], "CKKS entered 'ready''");
1533
1534 XCTestExpectation* callbackOccurs = [self expectationWithDescription:@"callback-occurs"];
1535
1536 [self.ckksControl rpcTLKMissing:@"keychain" reply:^(bool missing) {
1537 XCTAssertFalse(missing, "TLKs should not be missing");
1538 [callbackOccurs fulfill];
1539 }];
1540
1541 [self waitForExpectations:@[callbackOccurs] timeout:20];
1542
1543 OCMVerifyAllWithDelay(self.mockDatabase, 20);
1544 }
1545
1546 - (void)testRPCKnownBadStateWhenNoCloudKit {
1547 self.accountStatus = CKAccountStatusNoAccount;
1548
1549 [self startCKKSSubsystem];
1550
1551 XCTAssertEqual(0, [self.keychainView.keyHierarchyConditions[SecCKKSZoneKeyStateLoggedOut] wait:20*NSEC_PER_SEC], "CKKS entered loggedout");
1552
1553 XCTestExpectation* callbackOccurs = [self expectationWithDescription:@"callback-occurs"];
1554
1555 [self.ckksControl rpcKnownBadState:@"keychain" reply:^(CKKSKnownBadState result) {
1556 XCTAssertEqual(result, CKKSKnownStateNoCloudKitAccount, "State should be 'no cloudkit account'");
1557 [callbackOccurs fulfill];
1558 }];
1559
1560 [self waitForExpectations:@[callbackOccurs] timeout:20];
1561
1562 OCMVerifyAllWithDelay(self.mockDatabase, 20);
1563 }
1564
1565 - (void)testRPCKnownBadStateWhenNoTrust {
1566 [self putFakeKeyHierarchyInCloudKit:self.keychainZoneID];
1567 [self putFakeDeviceStatusInCloudKit:self.keychainZoneID];
1568
1569 self.mockSOSAdapter.circleStatus = kSOSCCNotInCircle;
1570 [self.accountStateTracker notifyCircleStatusChangeAndWaitForSignal];
1571
1572 [self startCKKSSubsystem];
1573
1574 XCTAssertEqual(0, [self.keychainView.keyHierarchyConditions[SecCKKSZoneKeyStateWaitForTrust] wait:20*NSEC_PER_SEC], "CKKS entered waitfortrust");
1575
1576 XCTestExpectation* callbackOccurs = [self expectationWithDescription:@"callback-occurs"];
1577
1578 [self.ckksControl rpcKnownBadState:@"keychain" reply:^(CKKSKnownBadState result) {
1579 XCTAssertEqual(result, CKKSKnownStateWaitForOctagon, "State should be 'waitforoctagon'");
1580 [callbackOccurs fulfill];
1581 }];
1582
1583 [self waitForExpectations:@[callbackOccurs] timeout:20];
1584
1585 OCMVerifyAllWithDelay(self.mockDatabase, 20);
1586 }
1587
1588 - (void)testRPCKnownBadStateWhenTLKsMissing {
1589 // Bring CKKS up in waitfortlk
1590 [self putFakeKeyHierarchyInCloudKit:self.keychainZoneID];
1591 [self putFakeDeviceStatusInCloudKit:self.keychainZoneID];
1592 [self startCKKSSubsystem];
1593
1594 XCTAssertEqual(0, [self.keychainView.keyHierarchyConditions[SecCKKSZoneKeyStateWaitForTLK] wait:20*NSEC_PER_SEC], "CKKS entered waitfortlk");
1595
1596 XCTestExpectation* callbackOccurs = [self expectationWithDescription:@"callback-occurs"];
1597
1598 [self.ckksControl rpcKnownBadState:@"keychain" reply:^(CKKSKnownBadState result) {
1599 XCTAssertEqual(result, CKKSKnownStateTLKsMissing, "TLKs should be missing");
1600 [callbackOccurs fulfill];
1601 }];
1602
1603 [self waitForExpectations:@[callbackOccurs] timeout:20];
1604
1605 OCMVerifyAllWithDelay(self.mockDatabase, 20);
1606 }
1607
1608 - (void)testRPCKnownBadStateWhenInWaitForUnlock {
1609 [self putFakeKeyHierarchyInCloudKit:self.keychainZoneID];
1610 [self saveTLKMaterialToKeychain:self.keychainZoneID];
1611 [self putFakeDeviceStatusInCloudKit:self.keychainZoneID];
1612
1613 // Bring CKKS up in 'waitforunlock'
1614 self.aksLockState = true;
1615 [self.lockStateTracker recheck];
1616 [self startCKKSSubsystem];
1617
1618 // Wait for the key hierarchy state machine to get stuck waiting for the unlock dependency. No uploads should occur.
1619 XCTAssertEqual(0, [self.keychainView.keyHierarchyConditions[SecCKKSZoneKeyStateWaitForUnlock] wait:20*NSEC_PER_SEC], @"Key state should get stuck in waitforunlock");
1620
1621 XCTestExpectation* callbackOccurs = [self expectationWithDescription:@"callback-occurs"];
1622
1623 [self.ckksControl rpcKnownBadState:@"keychain" reply:^(CKKSKnownBadState result) {
1624 XCTAssertEqual(result, CKKSKnownStateWaitForUnlock, "known state should be wait for unlock");
1625 [callbackOccurs fulfill];
1626 }];
1627
1628 [self waitForExpectations:@[callbackOccurs] timeout:20];
1629
1630 OCMVerifyAllWithDelay(self.mockDatabase, 20);
1631 }
1632
1633 - (void)testRPCKnownBadStateWhenInWaitForUpload {
1634 // Bring CKKS up in 'waitfortupload'
1635 self.aksLockState = true;
1636 [self.lockStateTracker recheck];
1637 [self startCKKSSubsystem];
1638
1639 // Wait for the key hierarchy state machine to get stuck waiting for Octagon. No uploads should occur.
1640 XCTAssertEqual(0, [self.keychainView.keyHierarchyConditions[SecCKKSZoneKeyStateWaitForTLKCreation] wait:20*NSEC_PER_SEC], @"Key state should get stuck in waitfortlkcreation");
1641
1642 XCTestExpectation* callbackOccurs = [self expectationWithDescription:@"callback-occurs"];
1643
1644 [self.ckksControl rpcKnownBadState:@"keychain" reply:^(CKKSKnownBadState result) {
1645 XCTAssertEqual(result, CKKSKnownStateWaitForOctagon, "known state should be wait for Octagon");
1646 [callbackOccurs fulfill];
1647 }];
1648
1649 [self waitForExpectations:@[callbackOccurs] timeout:20];
1650
1651 OCMVerifyAllWithDelay(self.mockDatabase, 20);
1652 }
1653
1654 - (void)testRPCKnownBadStateWhenInGoodState {
1655 // Bring CKKS up in 'ready'
1656 [self putFakeKeyHierarchyInCloudKit:self.keychainZoneID];
1657 [self saveTLKMaterialToKeychain:self.keychainZoneID];
1658 [self expectCKKSTLKSelfShareUpload:self.keychainZoneID];
1659 [self startCKKSSubsystem];
1660
1661 XCTAssertEqual(0, [self.keychainView.keyHierarchyConditions[SecCKKSZoneKeyStateReady] wait:20*NSEC_PER_SEC], "CKKS entered 'ready''");
1662
1663 XCTestExpectation* callbackOccurs = [self expectationWithDescription:@"callback-occurs"];
1664
1665 [self.ckksControl rpcKnownBadState:@"keychain" reply:^(CKKSKnownBadState result) {
1666 XCTAssertEqual(result, CKKSKnownStatePossiblyGood, "known state should not be possibly-good");
1667 [callbackOccurs fulfill];
1668 }];
1669
1670 [self waitForExpectations:@[callbackOccurs] timeout:20];
1671
1672 OCMVerifyAllWithDelay(self.mockDatabase, 20);
1673 }
1674
1675 - (void)testRpcStatus {
1676 [self createAndSaveFakeKeyHierarchy: self.keychainZoneID]; // Make life easy for this test.
1677
1678 [self startCKKSSubsystem];
1679
1680 // Let things shake themselves out.
1681 OCMVerifyAllWithDelay(self.mockDatabase, 20);
1682 XCTAssertEqual(0, [self.keychainView.keyHierarchyConditions[SecCKKSZoneKeyStateReady] wait:20*NSEC_PER_SEC], "Key state should return to 'ready'");
1683 [self waitForCKModifications];
1684
1685 XCTestExpectation* callbackOccurs = [self expectationWithDescription:@"callback-occurs"];
1686 [self.ckksControl rpcStatus:@"keychain" reply:^(NSArray<NSDictionary*>* result, NSError* error) {
1687 XCTAssertNil(error, "should be no error fetching status for keychain");
1688
1689 // Ugly "global" hack
1690 XCTAssertEqual(result.count, 2u, "Should have received two result dictionaries back");
1691 NSDictionary* keychainStatus = result[1];
1692
1693 XCTAssertNotNil(keychainStatus, "Should have received at least one zone status back");
1694 XCTAssertEqualObjects(keychainStatus[@"view"], @"keychain", "Should have received status for the keychain view");
1695 XCTAssertEqualObjects(keychainStatus[@"keystate"], SecCKKSZoneKeyStateReady, "Should be in 'ready' status");
1696 XCTAssertNotNil(keychainStatus[@"ckmirror"], "Status should have any ckmirror");
1697 [callbackOccurs fulfill];
1698 }];
1699
1700 [self waitForExpectations:@[callbackOccurs] timeout:20];
1701 }
1702
1703 - (void)testRpcFastStatus {
1704 [self createAndSaveFakeKeyHierarchy: self.keychainZoneID]; // Make life easy for this test.
1705
1706 [self startCKKSSubsystem];
1707
1708 // Let things shake themselves out.
1709 OCMVerifyAllWithDelay(self.mockDatabase, 20);
1710 XCTAssertEqual(0, [self.keychainView.keyHierarchyConditions[SecCKKSZoneKeyStateReady] wait:20*NSEC_PER_SEC], "Key state should return to 'ready'");
1711 [self waitForCKModifications];
1712
1713 XCTestExpectation* callbackOccurs = [self expectationWithDescription:@"callback-occurs"];
1714 [self.ckksControl rpcFastStatus:@"keychain" reply:^(NSArray<NSDictionary*>* result, NSError* error) {
1715 XCTAssertNil(error, "should be no error fetching status for keychain");
1716
1717 // Ugly "global" hack
1718 XCTAssertEqual(result.count, 1u, "Should have received one result dictionaries back");
1719 NSDictionary* keychainStatus = result[0];
1720
1721 XCTAssertNotNil(keychainStatus, "Should have received at least one zone status back");
1722 XCTAssertEqualObjects(keychainStatus[@"view"], @"keychain", "Should have received status for the keychain view");
1723 XCTAssertEqualObjects(keychainStatus[@"keystate"], SecCKKSZoneKeyStateReady, "Should be in 'ready' status");
1724 XCTAssertNil(keychainStatus[@"ckmirror"], "fastStatus should not have any ckmirror");
1725 [callbackOccurs fulfill];
1726 }];
1727
1728 [self waitForExpectations:@[callbackOccurs] timeout:20];
1729 }
1730
1731
1732 - (void)testRpcStatusWaitsForAccountDetermination {
1733 [self createAndSaveFakeKeyHierarchy: self.keychainZoneID]; // Make life easy for this test.
1734
1735 // Set up the account state callbacks to happen in one second
1736 dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (1 * NSEC_PER_SEC)), dispatch_get_global_queue(QOS_CLASS_DEFAULT, 0), ^{
1737 // Let CKKS come up (simulating daemon starting due to RPC)
1738 [self startCKKSSubsystem];
1739 });
1740
1741 // Before CKKS figures out we're in an account, fire off the status RPC.
1742 XCTestExpectation* callbackOccurs = [self expectationWithDescription:@"callback-occurs"];
1743 [self.ckksControl rpcStatus:@"keychain" reply:^(NSArray<NSDictionary*>* result, NSError* error) {
1744 XCTAssertNil(error, "should be no error fetching status for keychain");
1745
1746 // Ugly "global" hack
1747 XCTAssertEqual(result.count, 2u, "Should have received two result dictionaries back");
1748 NSDictionary* keychainStatus = result[1];
1749
1750 XCTAssertNotNil(keychainStatus, "Should have received at least one zone status back");
1751 XCTAssertEqualObjects(keychainStatus[@"view"], @"keychain", "Should have received status for the keychain view");
1752 XCTAssertEqualObjects(keychainStatus[@"keystate"], SecCKKSZoneKeyStateReady, "Should be in 'ready' status");
1753 [callbackOccurs fulfill];
1754 }];
1755
1756 [self waitForExpectations:@[callbackOccurs] timeout:20];
1757 }
1758
1759 - (void)testRpcStatusIsFastDuringError {
1760 [self createAndSaveFakeKeyHierarchy: self.keychainZoneID]; // Make life easy for this test.
1761
1762 // Let CKKS come up, then force it into error
1763 [self startCKKSSubsystem];
1764 XCTAssertEqual(0, [self.keychainView.keyHierarchyConditions[SecCKKSZoneKeyStateReady] wait:20*NSEC_PER_SEC], "CKKS entered 'ready'");
1765
1766 OctagonStateTransitionOperation* op = [OctagonStateTransitionOperation named:@"enter" entering:SecCKKSZoneKeyStateError];
1767 OctagonStateTransitionRequest* request = [[OctagonStateTransitionRequest alloc] init:@"enter-wait-for-trust"
1768 sourceStates:[NSSet setWithArray:[CKKSZoneKeyStateMap() allKeys]]
1769 serialQueue:self.keychainView.queue
1770 timeout:10 * NSEC_PER_SEC
1771 transitionOp:op];
1772 [self.keychainView.stateMachine handleExternalRequest:request];
1773
1774 XCTAssertEqual(0, [self.keychainView.keyHierarchyConditions[SecCKKSZoneKeyStateError] wait:20*NSEC_PER_SEC], "CKKS entered 'error'");
1775
1776 // Fire off the status RPC; it should return immediately
1777 XCTestExpectation* callbackOccurs = [self expectationWithDescription:@"callback-occurs"];
1778 [self.ckksControl rpcStatus:@"keychain" reply:^(NSArray<NSDictionary*>* result, NSError* error) {
1779 XCTAssertNil(error, "should be no error fetching status for keychain");
1780
1781 // Ugly "global" hack
1782 XCTAssertEqual(result.count, 2u, "Should have received two result dictionaries back");
1783 NSDictionary* keychainStatus = result[1];
1784
1785 XCTAssertNotNil(keychainStatus, "Should have received at least one zone status back");
1786 XCTAssertEqualObjects(keychainStatus[@"view"], @"keychain", "Should have received status for the keychain view");
1787 XCTAssertEqualObjects(keychainStatus[@"keystate"], SecCKKSZoneKeyStateError, "Should be in 'ready' status");
1788 [callbackOccurs fulfill];
1789 }];
1790
1791 [self waitForExpectations:@[callbackOccurs] timeout:20];
1792 }
1793
1794 - (void)testResetLocalAPIWakesDaemon {
1795 [self createAndSaveFakeKeyHierarchy:self.keychainZoneID];
1796 [self startCKKSSubsystem];
1797
1798 // We expect a single record to be uploaded
1799 [self expectCKModifyItemRecords:1 currentKeyPointerRecords:1 zoneID:self.keychainZoneID checkItem:[self checkClassCBlock:self.keychainZoneID message:@"Object was encrypted under class C key in hierarchy"]];
1800 [self addGenericPassword:@"data" account:@"account-delete-me"];
1801 OCMVerifyAllWithDelay(self.mockDatabase, 20);
1802 XCTAssertEqual(0, [self.keychainView.keyHierarchyConditions[SecCKKSZoneKeyStateReady] wait:20*NSEC_PER_SEC], "CKKS entered 'ready'");
1803
1804 // Now, simulate a restart: all views suddenly go missing from the ViewManager
1805 [self.injectedManager clearAllViews];
1806 [self.injectedManager resetSyncingPolicy];
1807 [self.injectedManager beginCloudKitOperationOfAllViews];
1808
1809 // And a reset-local API call wakes the daemon
1810 // The policy arrives after 500ms
1811 dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (0.5 * NSEC_PER_SEC)), dispatch_get_global_queue(QOS_CLASS_DEFAULT, 0), ^{
1812 [self.injectedManager setCurrentSyncingPolicy:self.viewSortingPolicyForManagedViewList];
1813 self.ckksViews = [NSMutableSet setWithArray:[self.injectedManager.views allValues]];
1814 self.keychainView = [self.injectedManager findView:@"keychain"];
1815 [self beginSOSTrustedOperationForAllViews];
1816 });
1817
1818 XCTestExpectation* resetExpectation = [self expectationWithDescription: @"local reset callback occurs"];
1819 [self.injectedManager rpcResetLocal:self.keychainZoneID.zoneName reply:^(NSError* result) {
1820 XCTAssertNil(result, "no error resetting local");
1821 [resetExpectation fulfill];
1822 }];
1823
1824 [self waitForExpectations:@[resetExpectation] timeout:20];
1825
1826 XCTAssertEqual(0, [self.keychainView.keyHierarchyConditions[SecCKKSZoneKeyStateReady] wait:20*NSEC_PER_SEC], "CKKS entered 'ready'");
1827 OCMVerifyAllWithDelay(self.mockDatabase, 20);
1828 }
1829
1830 - (void)testPushAPIWakesDaemon {
1831 [self createAndSaveFakeKeyHierarchy:self.keychainZoneID];
1832 [self startCKKSSubsystem];
1833
1834 // We expect a single record to be uploaded
1835 [self expectCKModifyItemRecords:1 currentKeyPointerRecords:1 zoneID:self.keychainZoneID checkItem:[self checkClassCBlock:self.keychainZoneID message:@"Object was encrypted under class C key in hierarchy"]];
1836 [self addGenericPassword:@"data" account:@"account-delete-me"];
1837 OCMVerifyAllWithDelay(self.mockDatabase, 20);
1838 XCTAssertEqual(0, [self.keychainView.keyHierarchyConditions[SecCKKSZoneKeyStateReady] wait:20*NSEC_PER_SEC], "CKKS entered 'ready'");
1839
1840 // Now, simulate a restart: all views suddenly go missing from the ViewManager
1841 [self.injectedManager clearAllViews];
1842 [self.injectedManager resetSyncingPolicy];
1843 [self.injectedManager beginCloudKitOperationOfAllViews];
1844
1845 // And a fetch API call wakes the daemon
1846 // The policy arrives after 500msx
1847 dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (0.5 * NSEC_PER_SEC)), dispatch_get_global_queue(QOS_CLASS_DEFAULT, 0), ^{
1848 [self.injectedManager setCurrentSyncingPolicy:self.viewSortingPolicyForManagedViewList];
1849 self.ckksViews = [NSMutableSet setWithArray:[self.injectedManager.views allValues]];
1850 [self beginSOSTrustedOperationForAllViews];
1851 });
1852
1853 XCTestExpectation* callbackOccurs = [self expectationWithDescription:@"callback-occurs"];
1854 [self.ckksControl rpcPushOutgoingChanges:nil reply:^(NSError * _Nullable error) {
1855 XCTAssertNil(error, "Should have received no error");
1856 [callbackOccurs fulfill];
1857 }];
1858
1859 [self waitForExpectations:@[callbackOccurs] timeout:60];
1860 OCMVerifyAllWithDelay(self.mockDatabase, 20);
1861 }
1862
1863 - (void)testFetchAPIWakesDaemon {
1864 [self createAndSaveFakeKeyHierarchy:self.keychainZoneID];
1865 [self startCKKSSubsystem];
1866
1867 // We expect a single record to be uploaded
1868 [self expectCKModifyItemRecords:1 currentKeyPointerRecords:1 zoneID:self.keychainZoneID checkItem:[self checkClassCBlock:self.keychainZoneID message:@"Object was encrypted under class C key in hierarchy"]];
1869 [self addGenericPassword:@"data" account:@"account-delete-me"];
1870 OCMVerifyAllWithDelay(self.mockDatabase, 20);
1871 XCTAssertEqual(0, [self.keychainView.keyHierarchyConditions[SecCKKSZoneKeyStateReady] wait:20*NSEC_PER_SEC], "CKKS entered 'ready'");
1872
1873 // Now, simulate a restart: all views suddenly go missing from the ViewManager
1874 [self.injectedManager clearAllViews];
1875 [self.injectedManager resetSyncingPolicy];
1876 [self.injectedManager beginCloudKitOperationOfAllViews];
1877
1878 // And a fetch API call wakes the daemon
1879 // The policy arrives after 500ms
1880 dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (0.5 * NSEC_PER_SEC)), dispatch_get_global_queue(QOS_CLASS_DEFAULT, 0), ^{
1881 [self.injectedManager setCurrentSyncingPolicy:self.viewSortingPolicyForManagedViewList];
1882 self.ckksViews = [NSMutableSet setWithArray:[self.injectedManager.views allValues]];
1883 [self beginSOSTrustedOperationForAllViews];
1884 });
1885
1886 XCTestExpectation* callbackOccurs = [self expectationWithDescription:@"callback-occurs"];
1887 [self.ckksControl rpcFetchAndProcessChanges:nil reply:^(NSError * _Nullable error) {
1888 XCTAssertNil(error, "Should have received no error");
1889 [callbackOccurs fulfill];
1890 }];
1891
1892 [self waitForExpectations:@[callbackOccurs] timeout:20];
1893 OCMVerifyAllWithDelay(self.mockDatabase, 20);
1894 }
1895
1896 @end
1897
1898 #endif // OCTAGON