]> git.saurik.com Git - apple/security.git/blob - keychain/ckks/tests/CKKSTests+API.m
Security-59306.101.1.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.holdOutgoingQueueOperation = [CKKSGroupOperation named:@"outgoing-hold" withBlock: ^{
217 secnotice("ckks", "releasing outgoing-queue hold");
218 }];
219
220 for(size_t count = 0; count < 150; count++) {
221 [self addGenericPassword:@"data" account:[NSString stringWithFormat:@"account-delete-me-%03lu", count]];
222 }
223
224 NSMutableDictionary* query = [@{
225 (id)kSecClass : (id)kSecClassGenericPassword,
226 (id)kSecAttrAccessGroup : @"com.apple.security.ckks",
227 (id)kSecAttrAccessible: (id)kSecAttrAccessibleAfterFirstUnlock,
228 (id)kSecAttrAccount : @"testaccount",
229 (id)kSecAttrSynchronizable : (id)kCFBooleanTrue,
230 (id)kSecAttrSyncViewHint : self.keychainView.zoneName,
231 (id)kSecAttrPCSPlaintextPublicKey : [@"asdf" dataUsingEncoding:NSUTF8StringEncoding],
232 (id)kSecValueData : (id) [@"asdf" dataUsingEncoding:NSUTF8StringEncoding],
233 } mutableCopy];
234
235 XCTestExpectation* blockExpectation = [self expectationWithDescription: @"callback occurs"];
236
237 XCTAssertEqual(errSecSuccess, _SecItemAddAndNotifyOnSync((__bridge CFDictionaryRef) query, NULL, ^(bool didSync, CFErrorRef error) {
238 XCTAssertTrue(didSync, "Item synced properly");
239 XCTAssertNil((__bridge NSError*)error, "No error syncing item");
240
241 [blockExpectation fulfill];
242 }), @"_SecItemAddAndNotifyOnSync succeeded");
243
244 // Release the hounds
245 [self.operationQueue addOperation:self.keychainView.holdOutgoingQueueOperation];
246
247 XCTestExpectation* firstQueueOperation = [self expectationWithDescription: @"found the item in the first queue iteration"];
248 [self expectCKModifyItemRecords:SecCKKSOutgoingQueueItemsAtOnce
249 currentKeyPointerRecords:1
250 zoneID:self.keychainZoneID
251 checkItem:^BOOL(CKRecord * _Nonnull record) {
252 if(record[SecCKRecordPCSPublicKey]) {
253 [firstQueueOperation fulfill];
254 }
255 return YES;
256 }];
257 [self expectCKModifyItemRecords:51 currentKeyPointerRecords:1 zoneID:self.keychainZoneID];
258
259 [self waitForExpectationsWithTimeout:5.0 handler:nil];
260 }
261
262 - (void)testAddAndNotifyOnSyncFailure {
263 [self createAndSaveFakeKeyHierarchy: self.keychainZoneID]; // Make life easy for this test.
264
265 [self startCKKSSubsystem];
266 [self.keychainView waitForFetchAndIncomingQueueProcessing];
267
268 // Due to item UUID selection, this item will be added with UUID 50184A35-4480-E8BA-769B-567CF72F1EC0.
269 // Add it to CloudKit first!
270 CKRecord* ckr = [self createFakeRecord: self.keychainZoneID recordName:@"50184A35-4480-E8BA-769B-567CF72F1EC0"];
271 [self.keychainZone addToZone: ckr];
272
273
274 // Go for it!
275 [self expectCKAtomicModifyItemRecordsUpdateFailure: self.keychainZoneID];
276
277 NSMutableDictionary* query = [@{
278 (id)kSecClass : (id)kSecClassGenericPassword,
279 (id)kSecAttrAccessGroup : @"com.apple.security.ckks",
280 (id)kSecAttrAccessible: (id)kSecAttrAccessibleAfterFirstUnlock,
281 (id)kSecAttrAccount : @"testaccount",
282 (id)kSecAttrSynchronizable : (id)kCFBooleanTrue,
283 (id)kSecAttrSyncViewHint : self.keychainView.zoneName,
284 (id)kSecValueData : (id) [@"asdf" dataUsingEncoding:NSUTF8StringEncoding],
285 (id)kSecAttrSyncViewHint : self.keychainView.zoneName, // @ fake view hint for fake view
286 } mutableCopy];
287
288 XCTestExpectation* blockExpectation = [self expectationWithDescription: @"callback occurs"];
289
290 XCTAssertEqual(errSecSuccess, _SecItemAddAndNotifyOnSync((__bridge CFDictionaryRef) query, NULL, ^(bool didSync, CFErrorRef error) {
291 XCTAssertFalse(didSync, "Item did not sync (as expected)");
292 XCTAssertNotNil((__bridge NSError*)error, "error exists when item fails to sync");
293
294 [blockExpectation fulfill];
295 }), @"_SecItemAddAndNotifyOnSync succeeded");
296
297 [self waitForExpectationsWithTimeout:5.0 handler:nil];
298 [self waitForCKModifications];
299 }
300
301 - (void)testAddAndNotifyOnSyncLoggedOut {
302 // Test starts with nothing in database and the user logged out of CloudKit. We expect no CKKS operations.
303 self.accountStatus = CKAccountStatusNoAccount;
304 self.silentFetchesAllowed = false;
305 [self startCKKSSubsystem];
306
307 XCTAssertEqual(0, [self.keychainView.loggedOut wait:20*NSEC_PER_SEC], "CKKS should positively log out");
308
309 NSMutableDictionary* query = [@{
310 (id)kSecClass : (id)kSecClassGenericPassword,
311 (id)kSecAttrAccessGroup : @"com.apple.security.ckks",
312 (id)kSecAttrAccessible: (id)kSecAttrAccessibleAfterFirstUnlock,
313 (id)kSecAttrAccount : @"testaccount",
314 (id)kSecAttrSynchronizable : (id)kCFBooleanTrue,
315 (id)kSecAttrSyncViewHint : self.keychainView.zoneName,
316 (id)kSecValueData : (id) [@"asdf" dataUsingEncoding:NSUTF8StringEncoding],
317 } mutableCopy];
318
319 XCTestExpectation* blockExpectation = [self expectationWithDescription: @"callback occurs"];
320
321 XCTAssertEqual(errSecSuccess, _SecItemAddAndNotifyOnSync((__bridge CFDictionaryRef) query, NULL, ^(bool didSync, CFErrorRef error) {
322 XCTAssertFalse(didSync, "Item did not sync (with no iCloud account)");
323 XCTAssertNotNil((__bridge NSError*)error, "Error exists syncing item while logged out");
324
325 [blockExpectation fulfill];
326 }), @"_SecItemAddAndNotifyOnSync succeeded");
327
328 [self waitForExpectationsWithTimeout:5.0 handler:nil];
329 }
330
331 - (void)testAddAndNotifyOnSyncAccountStatusUnclear {
332 // Test starts with nothing in database, but CKKS hasn't been told we've logged out yet.
333 // We expect no CKKS operations.
334 self.accountStatus = CKAccountStatusNoAccount;
335 self.silentFetchesAllowed = false;
336
337 NSMutableDictionary* query = [@{
338 (id)kSecClass : (id)kSecClassGenericPassword,
339 (id)kSecAttrAccessGroup : @"com.apple.security.ckks",
340 (id)kSecAttrAccessible: (id)kSecAttrAccessibleAfterFirstUnlock,
341 (id)kSecAttrAccount : @"testaccount",
342 (id)kSecAttrSynchronizable : (id)kCFBooleanTrue,
343 (id)kSecAttrSyncViewHint : self.keychainView.zoneName,
344 (id)kSecValueData : (id) [@"asdf" dataUsingEncoding:NSUTF8StringEncoding],
345 } mutableCopy];
346
347 XCTestExpectation* blockExpectation = [self expectationWithDescription: @"callback occurs"];
348
349 XCTAssertEqual(errSecSuccess, _SecItemAddAndNotifyOnSync((__bridge CFDictionaryRef) query, NULL, ^(bool didSync, CFErrorRef error) {
350 XCTAssertFalse(didSync, "Item did not sync (with no iCloud account)");
351 XCTAssertNotNil((__bridge NSError*)error, "Error exists syncing item while logged out");
352
353 [blockExpectation fulfill];
354 }), @"_SecItemAddAndNotifyOnSync succeeded");
355
356 // And now, allow CKKS to discover we're logged out
357 [self startCKKSSubsystem];
358 XCTAssertEqual(0, [self.keychainView.loggedOut wait:20*NSEC_PER_SEC], "CKKS should positively log out");
359
360 [self waitForExpectationsWithTimeout:5.0 handler:nil];
361 }
362
363 - (void)testAddAndNotifyOnSyncBeforeKeyHierarchyReady {
364 // Test starts with a key hierarchy in cloudkit and the TLK having arrived
365 [self putFakeKeyHierarchyInCloudKit:self.keychainZoneID];
366 [self saveTLKMaterialToKeychain:self.keychainZoneID];
367 [self expectCKKSTLKSelfShareUpload:self.keychainZoneID];
368
369 // But block CloudKit fetches (so the key hierarchy won't be ready when we add this new item)
370 [self holdCloudKitFetches];
371
372 [self startCKKSSubsystem];
373 XCTAssertEqual(0, [self.keychainView.loggedIn wait:20*NSEC_PER_SEC], "CKKS should log in");
374 [self.keychainView.zoneSetupOperation waitUntilFinished];
375
376 NSMutableDictionary* query = [@{
377 (id)kSecClass : (id)kSecClassGenericPassword,
378 (id)kSecAttrAccessGroup : @"com.apple.security.ckks",
379 (id)kSecAttrAccessible: (id)kSecAttrAccessibleAfterFirstUnlock,
380 (id)kSecAttrAccount : @"testaccount",
381 (id)kSecAttrSynchronizable : (id)kCFBooleanTrue,
382 (id)kSecAttrSyncViewHint : self.keychainView.zoneName,
383 (id)kSecValueData : (id) [@"asdf" dataUsingEncoding:NSUTF8StringEncoding],
384 } mutableCopy];
385
386 XCTestExpectation* blockExpectation = [self expectationWithDescription: @"callback occurs"];
387
388 XCTAssertEqual(errSecSuccess, _SecItemAddAndNotifyOnSync((__bridge CFDictionaryRef) query, NULL, ^(bool didSync, CFErrorRef error) {
389 XCTAssertTrue(didSync, "Item synced");
390 XCTAssertNil((__bridge NSError*)error, "Shouldn't have received an error syncing item");
391
392 [blockExpectation fulfill];
393 }), @"_SecItemAddAndNotifyOnSync succeeded");
394
395 // We should be in the 'fetch' state, but no further
396 XCTAssertEqual(0, [self.keychainView.keyHierarchyConditions[SecCKKSZoneKeyStateFetch] wait:20*NSEC_PER_SEC], @"Should have reached key state 'fetch', but no further");
397
398 // When we release the fetch, the callback should still fire and the item should upload
399 [self expectCKModifyItemRecords: 1 currentKeyPointerRecords: 1 zoneID:self.keychainZoneID];
400 [self releaseCloudKitFetchHold];
401 XCTAssertEqual(0, [self.keychainView.keyHierarchyConditions[SecCKKSZoneKeyStateReady] wait:20*NSEC_PER_SEC], @"Should have reached key state 'ready'");
402
403 // Verify that the item was written to CloudKit
404 OCMVerifyAllWithDelay(self.mockDatabase, 20);
405
406 [self waitForExpectationsWithTimeout:5.0 handler:nil];
407 }
408
409 - (void)testAddAndNotifyOnSyncBeforePolicyLoaded {
410 [self putFakeKeyHierarchyInCloudKit:self.keychainZoneID];
411 [self saveTLKMaterialToKeychain:self.keychainZoneID];
412 [self expectCKKSTLKSelfShareUpload:self.keychainZoneID];
413
414 [self startCKKSSubsystem];
415
416 // Allow CKKS to spin up fully
417 XCTAssertEqual(0, [self.keychainView.keyHierarchyConditions[SecCKKSZoneKeyStateReady] wait:20*NSEC_PER_SEC], "CKKS entered ready");
418
419 [self.keychainView waitForOperationsOfClass:[CKKSScanLocalItemsOperation class]];
420 [self.keychainView waitForOperationsOfClass:[CKKSOutgoingQueueOperation class]];
421
422 // Simulate a daemon restart
423 self.automaticallyBeginCKKSViewCloudKitOperation = false;
424 [self.injectedManager resetSyncingPolicy];
425 [self.injectedManager haltZone:self.keychainZoneID.zoneName];
426
427 // Issue the query (to simulate the query starting securityd)
428 NSMutableDictionary* query = [@{
429 (id)kSecClass : (id)kSecClassGenericPassword,
430 (id)kSecAttrAccessGroup : @"com.apple.security.ckks",
431 (id)kSecAttrAccessible: (id)kSecAttrAccessibleAfterFirstUnlock,
432 (id)kSecAttrAccount : @"testaccount",
433 (id)kSecAttrSynchronizable : (id)kCFBooleanTrue,
434 (id)kSecAttrSyncViewHint : self.keychainView.zoneName,
435 (id)kSecValueData : (id) [@"asdf" dataUsingEncoding:NSUTF8StringEncoding],
436 } mutableCopy];
437
438 XCTestExpectation* blockExpectation = [self expectationWithDescription: @"callback occurs"];
439
440 XCTAssertEqual(errSecSuccess, _SecItemAddAndNotifyOnSync((__bridge CFDictionaryRef) query, NULL, ^(bool didSync, CFErrorRef error) {
441 XCTAssertTrue(didSync, "Item synced");
442 XCTAssertNil((__bridge NSError*)error, "Shouldn't have received an error syncing item");
443
444 [blockExpectation fulfill];
445 }), @"_SecItemAddAndNotifyOnSync succeeded");
446
447 // When the policy is loaded, the item should upload and the callback should fire
448 [self expectCKModifyItemRecords:1 currentKeyPointerRecords:1 zoneID:self.keychainZoneID];
449
450 [self.injectedManager setSyncingViews:self.managedViewList sortingPolicy:self.viewSortingPolicyForManagedViewList];
451 self.keychainView = [self.injectedManager findView:self.keychainZoneID.zoneName];
452
453 [self.injectedManager beginCloudKitOperationOfAllViews];
454 [self beginSOSTrustedViewOperation:self.keychainView];
455
456 XCTAssertEqual(0, [self.keychainView.keyHierarchyConditions[SecCKKSZoneKeyStateReady] wait:20*NSEC_PER_SEC], @"Should have reached key state 'ready'");
457 OCMVerifyAllWithDelay(self.mockDatabase, 20);
458
459 [self waitForExpectations:@[blockExpectation] timeout:5];
460 }
461
462 - (void)testPCSUnencryptedFieldsAdd {
463 [self createAndSaveFakeKeyHierarchy: self.keychainZoneID]; // Make life easy for this test.
464
465 [self startCKKSSubsystem];
466 [self.keychainView waitForKeyHierarchyReadiness];
467
468 NSNumber* servIdentifier = @3;
469 NSData* publicKey = [@"asdfasdf" dataUsingEncoding:NSUTF8StringEncoding];
470 NSData* publicIdentity = [@"somedata" dataUsingEncoding:NSUTF8StringEncoding];
471
472 [self expectCKModifyItemRecords: 1 currentKeyPointerRecords: 1 zoneID:self.keychainZoneID
473 checkItem: [self checkPCSFieldsBlock:self.keychainZoneID
474 PCSServiceIdentifier:(NSNumber *)servIdentifier
475 PCSPublicKey:publicKey
476 PCSPublicIdentity:publicIdentity]];
477
478 NSMutableDictionary* query = [@{
479 (id)kSecClass : (id)kSecClassGenericPassword,
480 (id)kSecAttrAccessGroup : @"com.apple.security.ckks",
481 (id)kSecAttrAccessible: (id)kSecAttrAccessibleAfterFirstUnlock,
482 (id)kSecAttrAccount : @"testaccount",
483 (id)kSecAttrSynchronizable : (id)kCFBooleanTrue,
484 (id)kSecValueData : (id) [@"asdf" dataUsingEncoding:NSUTF8StringEncoding],
485 (id)kSecAttrDeriveSyncIDFromItemAttributes : (id)kCFBooleanTrue,
486 (id)kSecAttrPCSPlaintextServiceIdentifier : servIdentifier,
487 (id)kSecAttrPCSPlaintextPublicKey : publicKey,
488 (id)kSecAttrPCSPlaintextPublicIdentity : publicIdentity,
489 (id)kSecAttrSyncViewHint : self.keychainView.zoneName, // allows a CKKSScanOperation to find this item
490 } mutableCopy];
491
492 XCTAssertEqual(errSecSuccess, SecItemAdd((__bridge CFDictionaryRef) query, NULL), @"SecItemAdd succeeded");
493
494 // Verify that the item is written to CloudKit
495 OCMVerifyAllWithDelay(self.mockDatabase, 20);
496
497 CFTypeRef item = NULL;
498 query[(id)kSecValueData] = nil;
499 query[(id)kSecReturnAttributes] = @YES;
500 XCTAssertEqual(errSecSuccess, SecItemCopyMatching((__bridge CFDictionaryRef) query, &item), "item should still exist");
501
502 NSDictionary* itemAttributes = (NSDictionary*) CFBridgingRelease(item);
503 XCTAssertEqualObjects(itemAttributes[(id)kSecAttrPCSPlaintextServiceIdentifier], servIdentifier, "Service Identifier exists");
504 XCTAssertEqualObjects(itemAttributes[(id)kSecAttrPCSPlaintextPublicKey], publicKey, "public key exists");
505 XCTAssertEqualObjects(itemAttributes[(id)kSecAttrPCSPlaintextPublicIdentity], publicIdentity, "public identity exists");
506
507 // Find the item record in CloudKit. Since we're using kSecAttrDeriveSyncIDFromItemAttributes,
508 // the record ID is likely 50184A35-4480-E8BA-769B-567CF72F1EC0
509 [self waitForCKModifications];
510 CKRecordID* recordID = [[CKRecordID alloc] initWithRecordName: @"50184A35-4480-E8BA-769B-567CF72F1EC0" zoneID:self.keychainZoneID];
511 CKRecord* record = self.keychainZone.currentDatabase[recordID];
512 XCTAssertNotNil(record, "Found record in CloudKit at expected UUID");
513
514 XCTAssertEqualObjects(record[SecCKRecordPCSServiceIdentifier], servIdentifier, "Service identifier sent to cloudkit");
515 XCTAssertEqualObjects(record[SecCKRecordPCSPublicKey], publicKey, "public key sent to cloudkit");
516 XCTAssertEqualObjects(record[SecCKRecordPCSPublicIdentity], publicIdentity, "public identity sent to cloudkit");
517 }
518
519 - (void)testPCSUnencryptedFieldsModify {
520 [self createAndSaveFakeKeyHierarchy: self.keychainZoneID]; // Make life easy for this test.
521
522 [self startCKKSSubsystem];
523 [self.keychainView waitForKeyHierarchyReadiness];
524
525 NSNumber* servIdentifier = @3;
526 NSData* publicKey = [@"asdfasdf" dataUsingEncoding:NSUTF8StringEncoding];
527 NSData* publicIdentity = [@"somedata" dataUsingEncoding:NSUTF8StringEncoding];
528
529 [self expectCKModifyItemRecords: 1 currentKeyPointerRecords: 1 zoneID:self.keychainZoneID
530 checkItem: [self checkPCSFieldsBlock:self.keychainZoneID
531 PCSServiceIdentifier:(NSNumber *)servIdentifier
532 PCSPublicKey:publicKey
533 PCSPublicIdentity:publicIdentity]];
534
535 NSMutableDictionary* query = [@{
536 (id)kSecClass : (id)kSecClassGenericPassword,
537 (id)kSecAttrAccessGroup : @"com.apple.security.ckks",
538 (id)kSecAttrAccessible: (id)kSecAttrAccessibleAfterFirstUnlock,
539 (id)kSecAttrAccount : @"testaccount",
540 (id)kSecAttrSynchronizable : (id)kCFBooleanTrue,
541 (id)kSecValueData : (id) [@"asdf" dataUsingEncoding:NSUTF8StringEncoding],
542 (id)kSecAttrDeriveSyncIDFromItemAttributes : (id)kCFBooleanTrue,
543 (id)kSecAttrPCSPlaintextServiceIdentifier : servIdentifier,
544 (id)kSecAttrPCSPlaintextPublicKey : publicKey,
545 (id)kSecAttrPCSPlaintextPublicIdentity : publicIdentity,
546 (id)kSecAttrSyncViewHint : self.keychainView.zoneName, // allows a CKKSScanOperation to find this item
547 } mutableCopy];
548
549 XCTAssertEqual(errSecSuccess, SecItemAdd((__bridge CFDictionaryRef) query, NULL), @"SecItemAdd succeeded");
550
551 OCMVerifyAllWithDelay(self.mockDatabase, 20);
552 [self waitForCKModifications];
553
554 query[(id)kSecValueData] = nil;
555 query[(id)kSecAttrPCSPlaintextServiceIdentifier] = nil;
556 query[(id)kSecAttrPCSPlaintextPublicKey] = nil;
557 query[(id)kSecAttrPCSPlaintextPublicIdentity] = nil;
558
559 servIdentifier = @1;
560 publicKey = [@"new public key" dataUsingEncoding:NSUTF8StringEncoding];
561
562 NSNumber* newServiceIdentifier = @10;
563 NSData* newPublicKey = [@"new public key" dataUsingEncoding:NSUTF8StringEncoding];
564 NSData* newPublicIdentity = [@"new public identity" dataUsingEncoding:NSUTF8StringEncoding];
565
566 NSDictionary* update = @{
567 (id)kSecAttrPCSPlaintextServiceIdentifier : newServiceIdentifier,
568 (id)kSecAttrPCSPlaintextPublicKey : newPublicKey,
569 (id)kSecAttrPCSPlaintextPublicIdentity : newPublicIdentity,
570 };
571
572 [self expectCKModifyItemRecords: 1 currentKeyPointerRecords: 1 zoneID:self.keychainZoneID
573 checkItem: [self checkPCSFieldsBlock:self.keychainZoneID
574 PCSServiceIdentifier:(NSNumber *)newServiceIdentifier
575 PCSPublicKey:newPublicKey
576 PCSPublicIdentity:newPublicIdentity]];
577
578 XCTAssertEqual(errSecSuccess, SecItemUpdate((__bridge CFDictionaryRef) query, (__bridge CFDictionaryRef) update), @"SecItemUpdate succeeded");
579 OCMVerifyAllWithDelay(self.mockDatabase, 20);
580
581 CFTypeRef item = NULL;
582 query[(id)kSecValueData] = nil;
583 query[(id)kSecReturnAttributes] = @YES;
584 XCTAssertEqual(errSecSuccess, SecItemCopyMatching((__bridge CFDictionaryRef) query, &item), "item should still exist");
585
586 NSDictionary* itemAttributes = (NSDictionary*) CFBridgingRelease(item);
587 XCTAssertEqualObjects(itemAttributes[(id)kSecAttrPCSPlaintextServiceIdentifier], newServiceIdentifier, "Service Identifier exists");
588 XCTAssertEqualObjects(itemAttributes[(id)kSecAttrPCSPlaintextPublicKey], newPublicKey, "public key exists");
589 XCTAssertEqualObjects(itemAttributes[(id)kSecAttrPCSPlaintextPublicIdentity], newPublicIdentity, "public identity exists");
590
591 // Find the item record in CloudKit. Since we're using kSecAttrDeriveSyncIDFromItemAttributes,
592 // the record ID is likely 50184A35-4480-E8BA-769B-567CF72F1EC0
593 [self waitForCKModifications];
594 CKRecordID* recordID = [[CKRecordID alloc] initWithRecordName: @"50184A35-4480-E8BA-769B-567CF72F1EC0" zoneID:self.keychainZoneID];
595 CKRecord* record = self.keychainZone.currentDatabase[recordID];
596 XCTAssertNotNil(record, "Found record in CloudKit at expected UUID");
597
598 XCTAssertEqualObjects(record[SecCKRecordPCSServiceIdentifier], newServiceIdentifier, "Service identifier sent to cloudkit");
599 XCTAssertEqualObjects(record[SecCKRecordPCSPublicKey], newPublicKey, "public key sent to cloudkit");
600 XCTAssertEqualObjects(record[SecCKRecordPCSPublicIdentity], newPublicIdentity, "public identity sent to cloudkit");
601 }
602
603 // As of [<rdar://problem/32558310> CKKS: Re-authenticate PCSPublicFields], these fields are NOT server-modifiable. This test proves it.
604 - (void)testPCSUnencryptedFieldsServerModifyFail {
605 [self createAndSaveFakeKeyHierarchy: self.keychainZoneID]; // Make life easy for this test.
606
607 [self startCKKSSubsystem];
608 [self.keychainView waitForKeyHierarchyReadiness];
609
610 NSNumber* servIdentifier = @3;
611 NSData* publicKey = [@"asdfasdf" dataUsingEncoding:NSUTF8StringEncoding];
612 NSData* publicIdentity = [@"somedata" dataUsingEncoding:NSUTF8StringEncoding];
613
614 [self expectCKModifyItemRecords: 1 currentKeyPointerRecords: 1 zoneID:self.keychainZoneID
615 checkItem: [self checkPCSFieldsBlock:self.keychainZoneID
616 PCSServiceIdentifier:(NSNumber *)servIdentifier
617 PCSPublicKey:publicKey
618 PCSPublicIdentity:publicIdentity]];
619
620 NSMutableDictionary* query = [@{
621 (id)kSecClass : (id)kSecClassGenericPassword,
622 (id)kSecAttrAccessGroup : @"com.apple.security.ckks",
623 (id)kSecAttrAccessible: (id)kSecAttrAccessibleAfterFirstUnlock,
624 (id)kSecAttrAccount : @"testaccount",
625 (id)kSecAttrSynchronizable : (id)kCFBooleanTrue,
626 (id)kSecValueData : (id) [@"asdf" dataUsingEncoding:NSUTF8StringEncoding],
627 (id)kSecAttrDeriveSyncIDFromItemAttributes : (id)kCFBooleanTrue,
628 (id)kSecAttrPCSPlaintextServiceIdentifier : servIdentifier,
629 (id)kSecAttrPCSPlaintextPublicKey : publicKey,
630 (id)kSecAttrPCSPlaintextPublicIdentity : publicIdentity,
631 (id)kSecAttrSyncViewHint : self.keychainView.zoneName, // fake, for CKKSScanOperation
632 } mutableCopy];
633
634 XCTAssertEqual(errSecSuccess, SecItemAdd((__bridge CFDictionaryRef) query, NULL), @"SecItemAdd succeeded");
635
636 OCMVerifyAllWithDelay(self.mockDatabase, 20);
637 [self waitForCKModifications];
638
639 // Find the item record in CloudKit. Since we're using kSecAttrDeriveSyncIDFromItemAttributes,
640 // the record ID is likely 50184A35-4480-E8BA-769B-567CF72F1EC0
641 CKRecordID* recordID = [[CKRecordID alloc] initWithRecordName: @"50184A35-4480-E8BA-769B-567CF72F1EC0" zoneID:self.keychainZoneID];
642 CKRecord* record = self.keychainZone.currentDatabase[recordID];
643 XCTAssertNotNil(record, "Found record in CloudKit at expected UUID");
644
645 // Items are encrypted using encv2
646 XCTAssertEqualObjects(record[SecCKRecordEncryptionVersionKey], [NSNumber numberWithInteger:(int) CKKSItemEncryptionVersion2], "Uploaded using encv2");
647
648 if(!record) {
649 // Test has already failed; find the record just to be nice.
650 for(CKRecord* maybe in self.keychainZone.currentDatabase.allValues) {
651 if(maybe[SecCKRecordPCSServiceIdentifier] != nil) {
652 record = maybe;
653 }
654 }
655 }
656
657 NSNumber* newServiceIdentifier = @10;
658 NSData* newPublicKey = [@"new public key" dataUsingEncoding:NSUTF8StringEncoding];
659 NSData* newPublicIdentity = [@"new public identity" dataUsingEncoding:NSUTF8StringEncoding];
660
661 // Change the public key and public identity
662 record = [record copyWithZone: nil];
663 record[SecCKRecordPCSServiceIdentifier] = newServiceIdentifier;
664 record[SecCKRecordPCSPublicKey] = newPublicKey;
665 record[SecCKRecordPCSPublicIdentity] = newPublicIdentity;
666 [self.keychainZone addToZone: record];
667
668 // Trigger a notification
669 [self.keychainView notifyZoneChange:nil];
670 [self.keychainView waitForFetchAndIncomingQueueProcessing];
671
672 CFTypeRef item = NULL;
673 query[(id)kSecValueData] = nil;
674 query[(id)kSecAttrPCSPlaintextServiceIdentifier] = nil;
675 query[(id)kSecAttrPCSPlaintextPublicKey] = nil;
676 query[(id)kSecAttrPCSPlaintextPublicIdentity] = nil;
677 query[(id)kSecReturnAttributes] = @YES;
678 XCTAssertEqual(errSecSuccess, SecItemCopyMatching((__bridge CFDictionaryRef) query, &item), "item should still exist");
679
680 NSDictionary* itemAttributes = (NSDictionary*) CFBridgingRelease(item);
681 XCTAssertEqualObjects(itemAttributes[(id)kSecAttrPCSPlaintextServiceIdentifier], servIdentifier, "service identifier is not updated");
682 XCTAssertEqualObjects(itemAttributes[(id)kSecAttrPCSPlaintextPublicKey], publicKey, "public key not updated");
683 XCTAssertEqualObjects(itemAttributes[(id)kSecAttrPCSPlaintextPublicIdentity], publicIdentity, "public identity not updated");
684 }
685
686 -(void)testPCSUnencryptedFieldsRecieveUnauthenticatedFields {
687 [self createAndSaveFakeKeyHierarchy: self.keychainZoneID]; // Make life easy for this test.
688
689 [self startCKKSSubsystem];
690 [self.keychainView waitForKeyHierarchyReadiness];
691
692 NSNumber* servIdentifier = @3;
693 NSData* publicKey = [@"asdfasdf" dataUsingEncoding:NSUTF8StringEncoding];
694 NSData* publicIdentity = [@"somedata" dataUsingEncoding:NSUTF8StringEncoding];
695
696 NSError* error = nil;
697
698 // Manually encrypt an item
699 NSString* recordName = @"7B598D31-F9C5-481E-98AC-5A507ACB2D85";
700 CKRecordID* recordID = [[CKRecordID alloc] initWithRecordName:recordName zoneID:self.keychainZoneID];
701 NSDictionary* item = [self fakeRecordDictionary: @"account-delete-me" zoneID:self.keychainZoneID];
702 CKKSItem* cipheritem = [[CKKSItem alloc] initWithUUID:recordID.recordName
703 parentKeyUUID:self.keychainZoneKeys.classC.uuid
704 zoneID:recordID.zoneID];
705 CKKSKey* itemkey = [CKKSKey randomKeyWrappedByParent: self.keychainZoneKeys.classC error:&error];
706 XCTAssertNotNil(itemkey, "Got a key");
707 cipheritem.wrappedkey = itemkey.wrappedkey;
708 XCTAssertNotNil(cipheritem.wrappedkey, "Got a wrapped key");
709
710 cipheritem.encver = CKKSItemEncryptionVersion1;
711
712 // This item has the PCS public fields, but they are not authenticated
713 cipheritem.plaintextPCSServiceIdentifier = servIdentifier;
714 cipheritem.plaintextPCSPublicKey = publicKey;
715 cipheritem.plaintextPCSPublicIdentity = publicIdentity;
716
717 NSDictionary<NSString*, NSData*>* authenticatedData = [cipheritem makeAuthenticatedDataDictionaryUpdatingCKKSItem: nil encryptionVersion:CKKSItemEncryptionVersion1];
718 cipheritem.encitem = [CKKSItemEncrypter encryptDictionary:item key:itemkey.aessivkey authenticatedData:authenticatedData error:&error];
719 XCTAssertNil(error, "no error encrypting object");
720 XCTAssertNotNil(cipheritem.encitem, "Recieved ciphertext");
721
722 [self.keychainZone addToZone:[cipheritem CKRecordWithZoneID: recordID.zoneID]];
723
724 [self.keychainView waitForFetchAndIncomingQueueProcessing];
725
726 NSDictionary* query = @{(id)kSecClass: (id)kSecClassGenericPassword,
727 (id)kSecReturnAttributes: @YES,
728 (id)kSecAttrSynchronizable: @YES,
729 (id)kSecAttrAccount: @"account-delete-me",
730 (id)kSecMatchLimit: (id)kSecMatchLimitOne,
731 };
732 CFTypeRef cfresult = NULL;
733 XCTAssertEqual(errSecSuccess, SecItemCopyMatching((__bridge CFDictionaryRef) query, &cfresult), "Found synced item");
734
735 NSDictionary* result = CFBridgingRelease(cfresult);
736 XCTAssertEqualObjects(result[(id)kSecAttrPCSPlaintextServiceIdentifier], servIdentifier, "Received PCS service identifier");
737 XCTAssertEqualObjects(result[(id)kSecAttrPCSPlaintextPublicKey], publicKey, "Received PCS public key");
738 XCTAssertEqualObjects(result[(id)kSecAttrPCSPlaintextPublicIdentity], publicIdentity, "Received PCS public identity");
739 }
740
741 -(void)testPCSUnencryptedFieldsRecieveAuthenticatedFields {
742 [self createAndSaveFakeKeyHierarchy: self.keychainZoneID]; // Make life easy for this test.
743
744 [self startCKKSSubsystem];
745 [self.keychainView waitForKeyHierarchyReadiness];
746 [self.keychainView waitForOperationsOfClass:[CKKSIncomingQueueOperation class]];
747
748 NSNumber* servIdentifier = @3;
749 NSData* publicKey = [@"asdfasdf" dataUsingEncoding:NSUTF8StringEncoding];
750 NSData* publicIdentity = [@"somedata" dataUsingEncoding:NSUTF8StringEncoding];
751
752 NSError* error = nil;
753
754 // Manually encrypt an item
755 NSString* recordName = @"7B598D31-F9C5-481E-98AC-5A507ACB2D85";
756 CKRecordID* recordID = [[CKRecordID alloc] initWithRecordName:recordName zoneID:self.keychainZoneID];
757 NSDictionary* item = [self fakeRecordDictionary: @"account-delete-me" zoneID:self.keychainZoneID];
758 CKKSItem* cipheritem = [[CKKSItem alloc] initWithUUID:recordID.recordName
759 parentKeyUUID:self.keychainZoneKeys.classC.uuid
760 zoneID:recordID.zoneID];
761 CKKSKey* itemkey = [CKKSKey randomKeyWrappedByParent: self.keychainZoneKeys.classC error:&error];
762 XCTAssertNotNil(itemkey, "Got a key");
763 cipheritem.wrappedkey = itemkey.wrappedkey;
764 XCTAssertNotNil(cipheritem.wrappedkey, "Got a wrapped key");
765
766 cipheritem.encver = CKKSItemEncryptionVersion2;
767
768 // This item has the PCS public fields, and they are authenticated (since we're using v2)
769 cipheritem.plaintextPCSServiceIdentifier = servIdentifier;
770 cipheritem.plaintextPCSPublicKey = publicKey;
771 cipheritem.plaintextPCSPublicIdentity = publicIdentity;
772
773 // Use version 2, so PCS plaintext fields will be authenticated
774 NSMutableDictionary<NSString*, NSData*>* authenticatedData = [[cipheritem makeAuthenticatedDataDictionaryUpdatingCKKSItem: nil encryptionVersion:CKKSItemEncryptionVersion2] mutableCopy];
775
776 cipheritem.encitem = [CKKSItemEncrypter encryptDictionary:item key:itemkey.aessivkey authenticatedData:authenticatedData error:&error];
777 XCTAssertNil(error, "no error encrypting object");
778 XCTAssertNotNil(cipheritem.encitem, "Recieved ciphertext");
779
780 [self.keychainZone addToZone:[cipheritem CKRecordWithZoneID: recordID.zoneID]];
781
782 [self.keychainView waitForFetchAndIncomingQueueProcessing];
783
784 NSDictionary* query = @{(id)kSecClass: (id)kSecClassGenericPassword,
785 (id)kSecReturnAttributes: @YES,
786 (id)kSecAttrSynchronizable: @YES,
787 (id)kSecAttrAccount: @"account-delete-me",
788 (id)kSecMatchLimit: (id)kSecMatchLimitOne,
789 };
790 CFTypeRef cfresult = NULL;
791 XCTAssertEqual(errSecSuccess, SecItemCopyMatching((__bridge CFDictionaryRef) query, &cfresult), "Found synced item");
792
793 NSDictionary* result = CFBridgingRelease(cfresult);
794 XCTAssertEqualObjects(result[(id)kSecAttrPCSPlaintextServiceIdentifier], servIdentifier, "Received PCS service identifier");
795 XCTAssertEqualObjects(result[(id)kSecAttrPCSPlaintextPublicKey], publicKey, "Received PCS public key");
796 XCTAssertEqualObjects(result[(id)kSecAttrPCSPlaintextPublicIdentity], publicIdentity, "Received PCS public identity");
797
798 // Test that if this item is updated, it remains encrypted in v2
799 [self expectCKModifyItemRecords: 1 currentKeyPointerRecords: 1 zoneID:self.keychainZoneID
800 checkItem: [self checkPCSFieldsBlock:self.keychainZoneID
801 PCSServiceIdentifier:(NSNumber *)servIdentifier
802 PCSPublicKey:publicKey
803 PCSPublicIdentity:publicIdentity]];
804 [self updateGenericPassword:@"different password" account:@"account-delete-me"];
805
806 OCMVerifyAllWithDelay(self.mockDatabase, 20);
807 [self waitForCKModifications];
808
809 CKRecord* newRecord = self.keychainZone.currentDatabase[recordID];
810 XCTAssertEqualObjects(newRecord[SecCKRecordPCSServiceIdentifier], servIdentifier, "Didn't change service identifier");
811 XCTAssertEqualObjects(newRecord[SecCKRecordPCSPublicKey], publicKey, "Didn't change public key");
812 XCTAssertEqualObjects(newRecord[SecCKRecordPCSPublicIdentity], publicIdentity, "Didn't change public identity");
813 XCTAssertEqualObjects(newRecord[SecCKRecordEncryptionVersionKey], [NSNumber numberWithInteger:(int) CKKSItemEncryptionVersion2], "Uploaded using encv2");
814 }
815
816 -(void)testResetLocal {
817 // Test starts with nothing in database, but one in our fake CloudKit.
818 [self putFakeKeyHierarchyInCloudKit:self.keychainZoneID];
819 [self expectCKKSTLKSelfShareUpload:self.keychainZoneID];
820 [self saveTLKMaterialToKeychainSimulatingSOS:self.keychainZoneID];
821
822 // Spin up CKKS subsystem.
823 [self startCKKSSubsystem];
824
825 // We expect a single record to be uploaded
826 [self expectCKModifyItemRecords: 1 currentKeyPointerRecords: 1 zoneID:self.keychainZoneID checkItem: [self checkClassCBlock:self.keychainZoneID message:@"Object was encrypted under class C key in hierarchy"]];
827 [self addGenericPassword: @"data" account: @"account-delete-me"];
828 OCMVerifyAllWithDelay(self.mockDatabase, 20);
829
830 // After the local reset, we expect: a fetch, then nothing
831 self.silentFetchesAllowed = false;
832 [self expectCKFetch];
833
834 XCTestExpectation* resetExpectation = [self expectationWithDescription: @"local reset callback occurs"];
835 [self.injectedManager rpcResetLocal:nil reply:^(NSError* result) {
836 XCTAssertNil(result, "no error resetting local");
837 [resetExpectation fulfill];
838 }];
839 [self waitForExpectations:@[resetExpectation] timeout:20];
840
841 OCMVerifyAllWithDelay(self.mockDatabase, 20);
842
843 [self expectCKModifyItemRecords: 1 currentKeyPointerRecords: 1 zoneID:self.keychainZoneID checkItem: [self checkClassABlock:self.keychainZoneID message:@"Object was encrypted under class A key in hierarchy"]];
844 [self addGenericPassword:@"asdf"
845 account:@"account-class-A"
846 viewHint:nil
847 access:(id)kSecAttrAccessibleWhenUnlocked
848 expecting:errSecSuccess
849 message:@"Adding class A item"];
850 OCMVerifyAllWithDelay(self.mockDatabase, 20);
851 }
852
853 -(void)testResetLocalWhileUntrusted {
854 // We're "logged in to" cloudkit but not in circle.
855 self.mockSOSAdapter.circleStatus = kSOSCCNotInCircle;
856 [self.accountStateTracker notifyCircleStatusChangeAndWaitForSignal];
857 self.silentFetchesAllowed = false;
858
859 // Test starts with local TLK and key hierarchy in our fake cloudkit
860 [self putFakeKeyHierarchyInCloudKit:self.keychainZoneID];
861 [self saveTLKMaterialToKeychainSimulatingSOS:self.keychainZoneID];
862
863 // Spin up CKKS subsystem.
864 [self startCKKSSubsystem];
865
866 XCTAssertEqual(0, [self.keychainView.loggedIn wait:500*NSEC_PER_MSEC], "Should have been told of a 'login' event on startup");
867
868 NSData* changeTokenData = [[[NSUUID UUID] UUIDString] dataUsingEncoding:NSUTF8StringEncoding];
869 CKServerChangeToken* changeToken = [[CKServerChangeToken alloc] initWithData:changeTokenData];
870 [self.keychainView dispatchSync: ^bool{
871 CKKSZoneStateEntry* ckse = [CKKSZoneStateEntry state:self.keychainView.zoneName];
872 ckse.changeToken = changeToken;
873
874 NSError* error = nil;
875 [ckse saveToDatabase:&error];
876 XCTAssertNil(error, "No error saving new zone state to database");
877 return true;
878 }];
879
880 // after the reset, CKKS should refetch what's available
881 [self expectCKFetch];
882
883 XCTestExpectation* resetExpectation = [self expectationWithDescription: @"local reset callback occurs"];
884 [self.injectedManager rpcResetLocal:nil reply:^(NSError* result) {
885 XCTAssertNil(result, "no error resetting local");
886 secnotice("ckks", "Received a rpcResetLocal callback");
887
888 [self.keychainView dispatchSync: ^bool{
889 CKKSZoneStateEntry* ckse = [CKKSZoneStateEntry state:self.keychainView.zoneName];
890 XCTAssertNotEqualObjects(changeToken, ckse.changeToken, "Change token is reset");
891 return true;
892 }];
893
894 [resetExpectation fulfill];
895 }];
896
897 [self waitForExpectations:@[resetExpectation] timeout:20];
898
899 XCTAssertEqual(0, [self.keychainView.keyHierarchyConditions[SecCKKSZoneKeyStateWaitForTrust] wait:20*NSEC_PER_SEC], @"Key state should arrive at 'waitfortrust''");
900
901 // Now regain trust, and see what happens! It should use the existing fetch, pick up the old key hierarchy, and use it
902 [self expectCKKSTLKSelfShareUpload:self.keychainZoneID];
903
904 self.mockSOSAdapter.circleStatus = kSOSCCInCircle;
905 [self.accountStateTracker notifyCircleStatusChangeAndWaitForSignal];
906 [self beginSOSTrustedViewOperation:self.keychainView];
907
908 [self expectCKModifyItemRecords: 1 currentKeyPointerRecords: 1 zoneID:self.keychainZoneID checkItem:[self checkClassABlock:self.keychainZoneID message:@"Object was encrypted under class A key in hierarchy"]];
909 [self addGenericPassword:@"asdf"
910 account:@"account-class-A"
911 viewHint:nil
912 access:(id)kSecAttrAccessibleWhenUnlocked
913 expecting:errSecSuccess
914 message:@"Adding class A item"];
915 OCMVerifyAllWithDelay(self.mockDatabase, 20);
916 }
917
918 -(void)testResetLocalMultipleTimes {
919 // Test starts with nothing in database, but one in our fake CloudKit.
920 [self putFakeKeyHierarchyInCloudKit:self.keychainZoneID];
921 [self expectCKKSTLKSelfShareUpload:self.keychainZoneID];
922 [self saveTLKMaterialToKeychainSimulatingSOS:self.keychainZoneID];
923
924 // Spin up CKKS subsystem.
925 [self startCKKSSubsystem];
926
927 // We expect a single record to be uploaded
928 XCTAssertEqual(0, [self.keychainView.keyHierarchyConditions[SecCKKSZoneKeyStateReady] wait:20*NSEC_PER_SEC], "CKKS entered 'ready'");
929 [self expectCKModifyItemRecords:1 currentKeyPointerRecords:1 zoneID:self.keychainZoneID
930 checkItem:[self checkClassCBlock:self.keychainZoneID message:@"Object was encrypted under class C key in hierarchy"]];
931 [self addGenericPassword: @"data" account: @"account-delete-me"];
932 OCMVerifyAllWithDelay(self.mockDatabase, 20);
933 [self waitForCKModifications];
934
935 // We're going to request a bunch of CloudKit resets, but hold them from finishing
936 [self holdCloudKitFetches];
937
938 XCTestExpectation* resetExpectation0 = [self expectationWithDescription: @"reset callback(0) occurs"];
939 XCTestExpectation* resetExpectation1 = [self expectationWithDescription: @"reset callback(1) occurs"];
940 XCTestExpectation* resetExpectation2 = [self expectationWithDescription: @"reset callback(2) occurs"];
941 [self.injectedManager rpcResetLocal:nil reply:^(NSError* result) {
942 XCTAssertNil(result, "should receive no error resetting local");
943 secnotice("ckksreset", "Received a rpcResetLocal(0) callback");
944 [resetExpectation0 fulfill];
945 }];
946 [self.injectedManager rpcResetLocal:nil reply:^(NSError* result) {
947 XCTAssertNil(result, "should receive no error resetting local");
948 secnotice("ckksreset", "Received a rpcResetLocal(1) callback");
949 [resetExpectation1 fulfill];
950 }];
951 [self.injectedManager rpcResetLocal:nil reply:^(NSError* result) {
952 XCTAssertNil(result, "should receive no error resetting local");
953 secnotice("ckksreset", "Received a rpcResetLocal(2) callback");
954 [resetExpectation2 fulfill];
955 }];
956
957 // After the reset(s), we expect no uploads. Let the resets flow!
958 [self releaseCloudKitFetchHold];
959 [self waitForExpectations:@[resetExpectation0, resetExpectation1, resetExpectation2] timeout:20];
960 XCTAssertEqual(0, [self.keychainView.keyHierarchyConditions[SecCKKSZoneKeyStateReady] wait:20*NSEC_PER_SEC], "CKKS entered 'ready'");
961
962 OCMVerifyAllWithDelay(self.mockDatabase, 20);
963
964 [self expectCKModifyItemRecords:1 currentKeyPointerRecords:1 zoneID:self.keychainZoneID
965 checkItem:[self checkClassABlock:self.keychainZoneID message:@"Object was encrypted under class A key in hierarchy"]];
966 [self addGenericPassword:@"asdf"
967 account:@"account-class-A"
968 viewHint:nil
969 access:(id)kSecAttrAccessibleWhenUnlocked
970 expecting:errSecSuccess
971 message:@"Adding class A item"];
972 OCMVerifyAllWithDelay(self.mockDatabase, 20);
973 }
974
975 -(void)testResetCloudKitZone {
976 self.suggestTLKUpload = OCMClassMock([CKKSNearFutureScheduler class]);
977 OCMExpect([self.suggestTLKUpload trigger]);
978
979 self.silentZoneDeletesAllowed = true;
980
981 // Test starts with nothing in database, but one in our fake CloudKit.
982 [self putFakeKeyHierarchyInCloudKit:self.keychainZoneID];
983 [self expectCKKSTLKSelfShareUpload:self.keychainZoneID];
984 [self saveTLKMaterialToKeychainSimulatingSOS:self.keychainZoneID];
985
986 // Spin up CKKS subsystem.
987 [self startCKKSSubsystem];
988
989 // We expect a single record to be uploaded
990 [self expectCKModifyItemRecords: 1 currentKeyPointerRecords: 1 zoneID:self.keychainZoneID checkItem: [self checkClassCBlock:self.keychainZoneID message:@"Object was encrypted under class C key in hierarchy"]];
991 [self addGenericPassword: @"data" account: @"account-delete-me"];
992 OCMVerifyAllWithDelay(self.mockDatabase, 20);
993 [self waitForCKModifications];
994
995 // During the reset, Octagon will upload the key hierarchy, and then CKKS will upload the class C item
996 [self expectCKModifyItemRecords: 1 currentKeyPointerRecords: 1 zoneID:self.keychainZoneID checkItem: [self checkClassCBlock:self.keychainZoneID message:@"Object was encrypted under class C key in hierarchy"]];
997
998 XCTestExpectation* resetExpectation = [self expectationWithDescription: @"reset callback occurs"];
999 [self.injectedManager rpcResetCloudKit:nil reason:@"reset-test" reply:^(NSError* result) {
1000 XCTAssertNil(result, "no error resetting cloudkit");
1001 secnotice("ckks", "Received a resetCloudKit callback");
1002 [resetExpectation fulfill];
1003 }];
1004
1005 // Sneak in and perform Octagon's duties
1006 OCMVerifyAllWithDelay(self.suggestTLKUpload, 10);
1007 [self performOctagonTLKUpload:self.ckksViews];
1008
1009 [self waitForExpectations:@[resetExpectation] timeout:20];
1010
1011 OCMVerifyAllWithDelay(self.mockDatabase, 20);
1012
1013 [self expectCKModifyItemRecords: 1 currentKeyPointerRecords: 1 zoneID:self.keychainZoneID checkItem: [self checkClassABlock:self.keychainZoneID message:@"Object was encrypted under class A key in hierarchy"]];
1014 [self addGenericPassword:@"asdf"
1015 account:@"account-class-A"
1016 viewHint:nil
1017 access:(id)kSecAttrAccessibleWhenUnlocked
1018 expecting:errSecSuccess
1019 message:@"Adding class A item"];
1020 OCMVerifyAllWithDelay(self.mockDatabase, 20);
1021 }
1022
1023 - (void)testResetCloudKitZoneCloudKitRejects {
1024 self.suggestTLKUpload = OCMClassMock([CKKSNearFutureScheduler class]);
1025 OCMExpect([self.suggestTLKUpload trigger]);
1026
1027 self.nextModifyRecordZonesError = [[CKPrettyError alloc] initWithDomain:CKErrorDomain
1028 code:CKErrorZoneBusy
1029 userInfo:@{
1030 CKErrorRetryAfterKey: @(0.2),
1031 NSUnderlyingErrorKey: [[CKPrettyError alloc] initWithDomain:CKErrorDomain
1032 code:2029
1033 userInfo:nil],
1034 }];
1035 self.silentZoneDeletesAllowed = true;
1036
1037 // Test starts with nothing in database, but one in our fake CloudKit.
1038 [self putFakeKeyHierarchyInCloudKit:self.keychainZoneID];
1039 [self expectCKKSTLKSelfShareUpload:self.keychainZoneID];
1040 [self saveTLKMaterialToKeychainSimulatingSOS:self.keychainZoneID];
1041
1042 // Spin up CKKS subsystem.
1043 [self startCKKSSubsystem];
1044
1045 // We expect a single record to be uploaded
1046 [self expectCKModifyItemRecords: 1 currentKeyPointerRecords: 1 zoneID:self.keychainZoneID checkItem: [self checkClassCBlock:self.keychainZoneID message:@"Object was encrypted under class C key in hierarchy"]];
1047 [self addGenericPassword: @"data" account: @"account-delete-me"];
1048 OCMVerifyAllWithDelay(self.mockDatabase, 20);
1049 [self waitForCKModifications];
1050
1051 // During the reset, Octagon will upload the key hierarchy, and then CKKS will upload the class C item
1052 [self expectCKModifyItemRecords: 1 currentKeyPointerRecords: 1 zoneID:self.keychainZoneID checkItem: [self checkClassCBlock:self.keychainZoneID message:@"Object was encrypted under class C key in hierarchy"]];
1053
1054 XCTestExpectation* resetExpectation = [self expectationWithDescription: @"reset callback occurs"];
1055 [self.injectedManager rpcResetCloudKit:nil reason:@"reset-test" reply:^(NSError* result) {
1056 XCTAssertNil(result, "no error resetting cloudkit");
1057 secnotice("ckks", "Received a resetCloudKit callback");
1058 [resetExpectation fulfill];
1059 }];
1060
1061 // Sneak in and perform Octagon's duties
1062 OCMVerifyAllWithDelay(self.suggestTLKUpload, 10);
1063 [self performOctagonTLKUpload:self.ckksViews];
1064
1065 [self waitForExpectations:@[resetExpectation] timeout:20];
1066
1067 OCMVerifyAllWithDelay(self.mockDatabase, 20);
1068
1069 [self expectCKModifyItemRecords: 1 currentKeyPointerRecords: 1 zoneID:self.keychainZoneID checkItem: [self checkClassABlock:self.keychainZoneID message:@"Object was encrypted under class A key in hierarchy"]];
1070 [self addGenericPassword:@"asdf"
1071 account:@"account-class-A"
1072 viewHint:nil
1073 access:(id)kSecAttrAccessibleWhenUnlocked
1074 expecting:errSecSuccess
1075 message:@"Adding class A item"];
1076 OCMVerifyAllWithDelay(self.mockDatabase, 20);
1077
1078 XCTAssertNil(self.nextModifyRecordZonesError, "Record zone modification error should have been cleared");
1079 }
1080
1081 - (void)testResetCloudKitZoneDuringWaitForTLK {
1082 self.suggestTLKUpload = OCMClassMock([CKKSNearFutureScheduler class]);
1083 OCMExpect([self.suggestTLKUpload trigger]);
1084
1085 self.silentZoneDeletesAllowed = true;
1086
1087 // Test starts with nothing in database, but one in our fake CloudKit.
1088 // No TLK, though!
1089 [self putFakeKeyHierarchyInCloudKit:self.keychainZoneID];
1090 [self putFakeDeviceStatusInCloudKit:self.keychainZoneID];
1091
1092 // Spin up CKKS subsystem.
1093 [self startCKKSSubsystem];
1094
1095 // No records should be uploaded
1096 [self addGenericPassword: @"data" account: @"account-delete-me"];
1097
1098 XCTAssertEqual(0, [self.keychainView.keyHierarchyConditions[SecCKKSZoneKeyStateWaitForTLK] wait:20*NSEC_PER_SEC], "CKKS should have entered waitfortlk");
1099
1100 // Restart CKKS to really get in the spirit of waitfortlk (and get a pending processOutgoingQueue operation going)
1101 self.keychainView = [[CKKSViewManager manager] restartZone: self.keychainZoneID.zoneName];
1102 self.ckksViews = [NSMutableSet setWithObject:self.keychainView];
1103
1104 [self beginSOSTrustedViewOperation:self.keychainView];
1105 XCTAssertEqual(0, [self.keychainView.keyHierarchyConditions[SecCKKSZoneKeyStateWaitForTLK] wait:20*NSEC_PER_SEC], "CKKS entered waitfortlk");
1106
1107 CKKSOutgoingQueueOperation* outgoingOp = [self.keychainView processOutgoingQueue:nil];
1108 XCTAssertTrue([outgoingOp isPending], "outgoing queue processing should be on hold");
1109
1110 // Now, reset everything. The outgoingOp should get cancelled.
1111 // We expect a key hierarchy upload, and then the class C item upload
1112 [self expectCKModifyItemRecords: 1 currentKeyPointerRecords: 1 zoneID:self.keychainZoneID
1113 checkItem: [self checkClassCBlock:self.keychainZoneID message:@"Object was encrypted under class C key in hierarchy"]];
1114
1115 XCTestExpectation* resetExpectation = [self expectationWithDescription: @"reset callback occurs"];
1116 [self.injectedManager rpcResetCloudKit:nil reason:@"reset-test" reply:^(NSError* result) {
1117 XCTAssertNil(result, "no error resetting cloudkit");
1118 [resetExpectation fulfill];
1119 }];
1120
1121 // Sneak in and perform Octagon's duties
1122 OCMVerifyAllWithDelay(self.suggestTLKUpload, 10);
1123 [self performOctagonTLKUpload:self.ckksViews];
1124
1125 [self waitForExpectations:@[resetExpectation] timeout:20];
1126
1127 XCTAssertTrue([outgoingOp isCancelled], "old stuck ProcessOutgoingQueue should be cancelled");
1128 OCMVerifyAllWithDelay(self.mockDatabase, 20);
1129
1130 // And adding another item works too
1131 [self expectCKModifyItemRecords: 1 currentKeyPointerRecords: 1 zoneID:self.keychainZoneID checkItem: [self checkClassABlock:self.keychainZoneID message:@"Object was encrypted under class A key in hierarchy"]];
1132 [self addGenericPassword:@"asdf"
1133 account:@"account-class-A"
1134 viewHint:nil
1135 access:(id)kSecAttrAccessibleWhenUnlocked
1136 expecting:errSecSuccess
1137 message:@"Adding class A item"];
1138 OCMVerifyAllWithDelay(self.mockDatabase, 20);
1139 }
1140
1141 /*
1142 * This test doesn't work, since the resetLocal fails. CKKS gets back into waitfortlk
1143 * but that isn't considered a successful resetLocal.
1144 *
1145 - (void)testResetLocalDuringWaitForTLK {
1146 // Test starts with nothing in database, but one in our fake CloudKit.
1147 // No TLK, though!
1148 [self putFakeKeyHierarchyInCloudKit:self.keychainZoneID];
1149
1150 // Spin up CKKS subsystem.
1151 [self startCKKSSubsystem];
1152
1153 // No records should be uploaded
1154 [self addGenericPassword: @"data" account: @"account-delete-me"];
1155
1156 XCTAssertEqual(0, [self.keychainView.keyHierarchyConditions[SecCKKSZoneKeyStateWaitForTLK] wait:20*NSEC_PER_SEC], "CKKS should have entered waitfortlk");
1157
1158 // Restart CKKS to really get in the spirit of waitfortlk (and get a pending processOutgoingQueue operation going)
1159 self.keychainView = [[CKKSViewManager manager] restartZone: self.keychainZoneID.zoneName];
1160 [self beginSOSTrustedViewOperation:self.keychainView];
1161 XCTAssertEqual(0, [self.keychainView.keyHierarchyConditions[SecCKKSZoneKeyStateWaitForTLK] wait:20*NSEC_PER_SEC], "CKKS entered waitfortlk");
1162
1163 CKKSOutgoingQueueOperation* outgoingOp = [self.keychainView processOutgoingQueue:nil];
1164 XCTAssertTrue([outgoingOp isPending], "outgoing queue processing should be on hold");
1165
1166 // Now, reset everything. The outgoingOp should get cancelled.
1167 // We expect a key hierarchy upload, and then the class C item upload
1168 [self expectCKModifyKeyRecords: 3 currentKeyPointerRecords: 3 tlkShareRecords:3 zoneID:self.keychainZoneID];
1169 [self expectCKModifyItemRecords: 1 currentKeyPointerRecords: 1 zoneID:self.keychainZoneID
1170 checkItem: [self checkClassCBlock:self.keychainZoneID message:@"Object was encrypted under class C key in hierarchy"]];
1171
1172 XCTestExpectation* resetExpectation = [self expectationWithDescription: @"reset callback occurs"];
1173 [self.injectedManager rpcResetLocal:nil reply:^(NSError* result) {
1174 XCTAssertNil(result, "no error resetting local");
1175 [resetExpectation fulfill];
1176 }];
1177 [self waitForExpectations:@[resetExpectation] timeout:20];
1178
1179 XCTAssertTrue([outgoingOp isCancelled], "old stuck ProcessOutgoingQueue should be cancelled");
1180 OCMVerifyAllWithDelay(self.mockDatabase, 20);
1181
1182 // And adding another item works too
1183 [self expectCKModifyItemRecords: 1 currentKeyPointerRecords: 1 zoneID:self.keychainZoneID checkItem: [self checkClassABlock:self.keychainZoneID message:@"Object was encrypted under class A key in hierarchy"]];
1184 [self addGenericPassword:@"asdf"
1185 account:@"account-class-A"
1186 viewHint:nil
1187 access:(id)kSecAttrAccessibleWhenUnlocked
1188 expecting:errSecSuccess
1189 message:@"Adding class A item"];
1190 OCMVerifyAllWithDelay(self.mockDatabase, 20);
1191 }*/
1192
1193 -(void)testResetCloudKitZoneWhileUntrusted {
1194 self.silentZoneDeletesAllowed = true;
1195
1196 // We're "logged in to" cloudkit but not in circle.
1197 self.mockSOSAdapter.circleStatus = kSOSCCNotInCircle;
1198 [self.accountStateTracker notifyCircleStatusChangeAndWaitForSignal];
1199
1200 // Test starts with nothing in database, but one in our fake CloudKit.
1201 [self putFakeKeyHierarchyInCloudKit:self.keychainZoneID];
1202
1203 // Spin up CKKS subsystem.
1204 [self startCKKSSubsystem];
1205
1206 // Since CKKS is untrusted, it'll fetch the zone but then get stuck
1207 XCTAssertEqual(0, [self.keychainView.keyHierarchyConditions[SecCKKSZoneKeyStateWaitForTrust] wait:20*NSEC_PER_SEC], "CKKS entered 'waitfortrust'");
1208
1209 CKRecord* ckr = [self createFakeRecord: self.keychainZoneID recordName:@"7B598D31-F9C5-481E-98AC-5A507ACB2D85"];
1210 [self.keychainZone addToZone: ckr];
1211
1212 XCTAssertNotNil(self.keychainZone.currentDatabase, "Zone exists");
1213 XCTAssertNotNil(self.keychainZone.currentDatabase[ckr.recordID], "An item exists in the fake zone");
1214
1215 XCTestExpectation* resetExpectation = [self expectationWithDescription: @"reset callback occurs"];
1216 [self.injectedManager rpcResetCloudKit:nil reason:@"reset-test" reply:^(NSError* result) {
1217 XCTAssertNil(result, "no error resetting cloudkit");
1218 secnotice("ckks", "Received a resetCloudKit callback");
1219 [resetExpectation fulfill];
1220 }];
1221
1222 [self waitForExpectations:@[resetExpectation] timeout:20];
1223
1224 XCTAssertNil(self.keychainZone.currentDatabase, "No zone anymore!");
1225 OCMVerifyAllWithDelay(self.mockDatabase, 20);
1226
1227 // Now log in, and see what happens! It should create the zone again and upload a whole new key hierarchy
1228 self.silentFetchesAllowed = true;
1229 self.mockSOSAdapter.circleStatus = kSOSCCInCircle;
1230 [self.accountStateTracker notifyCircleStatusChangeAndWaitForSignal];
1231 [self beginSOSTrustedViewOperation:self.keychainView];
1232
1233 [self performOctagonTLKUpload:self.ckksViews];
1234
1235 OCMVerifyAllWithDelay(self.mockDatabase, 20);
1236
1237 [self expectCKModifyItemRecords: 1 currentKeyPointerRecords: 1 zoneID:self.keychainZoneID checkItem:[self checkClassABlock:self.keychainZoneID message:@"Object was encrypted under class A key in hierarchy"]];
1238 [self addGenericPassword:@"asdf"
1239 account:@"account-class-A"
1240 viewHint:nil
1241 access:(id)kSecAttrAccessibleWhenUnlocked
1242 expecting:errSecSuccess
1243 message:@"Adding class A item"];
1244 OCMVerifyAllWithDelay(self.mockDatabase, 20);
1245 }
1246
1247 - (void)testResetCloudKitZoneMultipleTimes {
1248 self.suggestTLKUpload = OCMClassMock([CKKSNearFutureScheduler class]);
1249 OCMExpect([self.suggestTLKUpload trigger]);
1250
1251 self.silentZoneDeletesAllowed = true;
1252
1253 // Test starts with nothing in database, but one in our fake CloudKit.
1254 [self putFakeKeyHierarchyInCloudKit:self.keychainZoneID];
1255 [self expectCKKSTLKSelfShareUpload:self.keychainZoneID];
1256 [self saveTLKMaterialToKeychainSimulatingSOS:self.keychainZoneID];
1257
1258 // Spin up CKKS subsystem.
1259 [self startCKKSSubsystem];
1260
1261 // We expect a single record to be uploaded
1262 XCTAssertEqual(0, [self.keychainView.keyHierarchyConditions[SecCKKSZoneKeyStateReady] wait:20*NSEC_PER_SEC], "CKKS entered 'ready'");
1263 [self expectCKModifyItemRecords:1 currentKeyPointerRecords:1 zoneID:self.keychainZoneID
1264 checkItem:[self checkClassCBlock:self.keychainZoneID message:@"Object was encrypted under class C key in hierarchy"]];
1265 [self addGenericPassword: @"data" account: @"account-delete-me"];
1266 OCMVerifyAllWithDelay(self.mockDatabase, 20);
1267 [self waitForCKModifications];
1268
1269 // We're going to request a bunch of CloudKit resets, but hold them from finishing
1270 [self holdCloudKitFetches];
1271
1272 XCTestExpectation* resetExpectation0 = [self expectationWithDescription: @"reset callback(0) occurs"];
1273 XCTestExpectation* resetExpectation1 = [self expectationWithDescription: @"reset callback(1) occurs"];
1274 XCTestExpectation* resetExpectation2 = [self expectationWithDescription: @"reset callback(2) occurs"];
1275 [self.injectedManager rpcResetCloudKit:nil reason:@"reset-test" reply:^(NSError* result) {
1276 XCTAssertNil(result, "should receive no error resetting cloudkit");
1277 secnotice("ckksreset", "Received a resetCloudKit(0) callback");
1278 [resetExpectation0 fulfill];
1279 }];
1280 [self.injectedManager rpcResetCloudKit:nil reason:@"reset-test" reply:^(NSError* result) {
1281 XCTAssertNil(result, "should receive no error resetting cloudkit");
1282 secnotice("ckksreset", "Received a resetCloudKit(1) callback");
1283 [resetExpectation1 fulfill];
1284 }];
1285 [self.injectedManager rpcResetCloudKit:nil reason:@"reset-test" reply:^(NSError* result) {
1286 XCTAssertNil(result, "should receive no error resetting cloudkit");
1287 secnotice("ckksreset", "Received a resetCloudKit(2) callback");
1288 [resetExpectation2 fulfill];
1289 }];
1290
1291 // After the reset(s), we expect a key hierarchy upload, and then the class C item upload
1292 [self expectCKModifyItemRecords:1 currentKeyPointerRecords:1 zoneID:self.keychainZoneID
1293 checkItem:[self checkClassCBlock:self.keychainZoneID message:@"Object was encrypted under class C key in hierarchy"]];
1294
1295 // And let the resets flow
1296 [self releaseCloudKitFetchHold];
1297
1298 OCMVerifyAllWithDelay(self.suggestTLKUpload, 10);
1299 [self performOctagonTLKUpload:self.ckksViews];
1300
1301 [self waitForExpectations:@[resetExpectation0, resetExpectation1, resetExpectation2] timeout:20];
1302 XCTAssertEqual(0, [self.keychainView.keyHierarchyConditions[SecCKKSZoneKeyStateReady] wait:20*NSEC_PER_SEC], "CKKS entered 'ready'");
1303
1304 OCMVerifyAllWithDelay(self.mockDatabase, 20);
1305
1306 [self expectCKModifyItemRecords:1 currentKeyPointerRecords:1 zoneID:self.keychainZoneID
1307 checkItem:[self checkClassABlock:self.keychainZoneID message:@"Object was encrypted under class A key in hierarchy"]];
1308 [self addGenericPassword:@"asdf"
1309 account:@"account-class-A"
1310 viewHint:nil
1311 access:(id)kSecAttrAccessibleWhenUnlocked
1312 expecting:errSecSuccess
1313 message:@"Adding class A item"];
1314 OCMVerifyAllWithDelay(self.mockDatabase, 20);
1315 }
1316
1317 - (void)testRPCFetchAndProcessWhileCloudKitNotResponding {
1318 [self createAndSaveFakeKeyHierarchy: self.keychainZoneID]; // Make life easy for this test.
1319 [self startCKKSSubsystem];
1320
1321 XCTAssertEqual(0, [self.keychainView.keyHierarchyConditions[SecCKKSZoneKeyStateReady] wait:20*NSEC_PER_SEC], "CKKS entered 'ready'");
1322 [self holdCloudKitFetches];
1323
1324 XCTestExpectation* callbackOccurs = [self expectationWithDescription:@"callback-occurs"];
1325 [self.ckksControl rpcFetchAndProcessChanges:nil reply:^(NSError * _Nullable error) {
1326 // done! we should have an underlying error of "fetch isn't working"
1327 XCTAssertNotNil(error, "Should have received an error attempting to fetch and process");
1328 NSError* underlying = error.userInfo[NSUnderlyingErrorKey];
1329 XCTAssertNotNil(underlying, "Should have received an underlying error");
1330 XCTAssertEqualObjects(underlying.domain, CKKSResultDescriptionErrorDomain, "Underlying error should be CKKSResultDescriptionErrorDomain");
1331 XCTAssertEqual(underlying.code, CKKSResultDescriptionPendingSuccessfulFetch, "Underlying error should be 'pending fetch'");
1332 [callbackOccurs fulfill];
1333 }];
1334
1335 [self waitForExpectations:@[callbackOccurs] timeout:20];
1336 [self releaseCloudKitFetchHold];
1337 OCMVerifyAllWithDelay(self.mockDatabase, 20);
1338 }
1339
1340 - (void)testRPCFetchAndProcessWhileCloudKitErroring {
1341 [self createAndSaveFakeKeyHierarchy: self.keychainZoneID]; // Make life easy for this test.
1342 [self startCKKSSubsystem];
1343
1344 XCTAssertEqual(0, [self.keychainView.keyHierarchyConditions[SecCKKSZoneKeyStateReady] wait:20*NSEC_PER_SEC], "CKKS entered 'ready'");
1345
1346 [self.keychainZone failNextFetchWith:[[CKPrettyError alloc] initWithDomain:CKErrorDomain
1347 code:CKErrorRequestRateLimited
1348 userInfo:@{CKErrorRetryAfterKey : [NSNumber numberWithInt:30]}]];
1349
1350 XCTestExpectation* callbackOccurs = [self expectationWithDescription:@"callback-occurs"];
1351 [self.ckksControl rpcFetchAndProcessChanges:nil reply:^(NSError * _Nullable error) {
1352 // done! we should have an underlying error of "fetch isn't working"
1353 XCTAssertNotNil(error, "Should have received an error attempting to fetch and process");
1354 NSError* underlying = error.userInfo[NSUnderlyingErrorKey];
1355 XCTAssertNotNil(underlying, "Should have received an underlying error");
1356 XCTAssertEqualObjects(underlying.domain, CKKSResultDescriptionErrorDomain, "Underlying error should be CKKSResultDescriptionErrorDomain");
1357 XCTAssertEqual(underlying.code, CKKSResultDescriptionPendingSuccessfulFetch, "Underlying error should be 'pending fetch'");
1358
1359 NSError* underunderlying = underlying.userInfo[NSUnderlyingErrorKey];
1360 XCTAssertNotNil(underunderlying, "Should have received another layer of underlying error");
1361 XCTAssertEqualObjects(underunderlying.domain, CKErrorDomain, "Underlying error should be CKErrorDomain");
1362 XCTAssertEqual(underunderlying.code, CKErrorRequestRateLimited, "Underlying error should be 'rate limited'");
1363
1364 [callbackOccurs fulfill];
1365 }];
1366
1367 [self waitForExpectations:@[callbackOccurs] timeout:20];
1368 OCMVerifyAllWithDelay(self.mockDatabase, 20);
1369 }
1370
1371 - (void)testRPCFetchAndProcessWhileInWaitForTLK {
1372 [self putFakeKeyHierarchyInCloudKit:self.keychainZoneID];
1373 [self putFakeDeviceStatusInCloudKit:self.keychainZoneID];
1374 [self startCKKSSubsystem];
1375
1376 XCTAssertEqual(0, [self.keychainView.keyHierarchyConditions[SecCKKSZoneKeyStateWaitForTLK] wait:20*NSEC_PER_SEC], "CKKS entered waitfortlk");
1377
1378 XCTestExpectation* callbackOccurs = [self expectationWithDescription:@"callback-occurs"];
1379 [self.ckksControl rpcFetchAndProcessChanges:nil reply:^(NSError * _Nullable error) {
1380 // done! we should have an underlying error of "fetch isn't working"
1381 XCTAssertNotNil(error, "Should have received an error attempting to fetch and process");
1382 NSError* underlying = error.userInfo[NSUnderlyingErrorKey];
1383 XCTAssertNotNil(underlying, "Should have received an underlying error");
1384 XCTAssertEqualObjects(underlying.domain, CKKSResultDescriptionErrorDomain, "Underlying error should be CKKSResultDescriptionErrorDomain");
1385 XCTAssertEqual(underlying.code, CKKSResultDescriptionPendingKeyReady, "Underlying error should be 'pending key ready'");
1386 [callbackOccurs fulfill];
1387 }];
1388
1389 [self waitForExpectations:@[callbackOccurs] timeout:20];
1390 OCMVerifyAllWithDelay(self.mockDatabase, 20);
1391 }
1392
1393 - (void)testRPCTLKMissingWhenMissing {
1394 // Bring CKKS up in waitfortlk
1395 [self putFakeKeyHierarchyInCloudKit:self.keychainZoneID];
1396 [self putFakeDeviceStatusInCloudKit:self.keychainZoneID];
1397 [self startCKKSSubsystem];
1398
1399 XCTAssertEqual(0, [self.keychainView.keyHierarchyConditions[SecCKKSZoneKeyStateWaitForTLK] wait:20*NSEC_PER_SEC], "CKKS entered waitfortlk");
1400
1401 XCTestExpectation* callbackOccurs = [self expectationWithDescription:@"callback-occurs"];
1402
1403 [self.ckksControl rpcTLKMissing:@"keychain" reply:^(bool missing) {
1404 XCTAssertTrue(missing, "TLKs should be missing");
1405 [callbackOccurs fulfill];
1406 }];
1407
1408 [self waitForExpectations:@[callbackOccurs] timeout:20];
1409
1410 OCMVerifyAllWithDelay(self.mockDatabase, 20);
1411 }
1412
1413 - (void)testRPCTLKMissingWhenFound {
1414 // Bring CKKS up in 'ready'
1415 [self putFakeKeyHierarchyInCloudKit:self.keychainZoneID];
1416 [self saveTLKMaterialToKeychain:self.keychainZoneID];
1417 [self expectCKKSTLKSelfShareUpload:self.keychainZoneID];
1418 [self startCKKSSubsystem];
1419
1420 XCTAssertEqual(0, [self.keychainView.keyHierarchyConditions[SecCKKSZoneKeyStateReady] wait:20*NSEC_PER_SEC], "CKKS entered 'ready''");
1421
1422 XCTestExpectation* callbackOccurs = [self expectationWithDescription:@"callback-occurs"];
1423
1424 [self.ckksControl rpcTLKMissing:@"keychain" reply:^(bool missing) {
1425 XCTAssertFalse(missing, "TLKs should not be missing");
1426 [callbackOccurs fulfill];
1427 }];
1428
1429 [self waitForExpectations:@[callbackOccurs] timeout:20];
1430
1431 OCMVerifyAllWithDelay(self.mockDatabase, 20);
1432 }
1433
1434 - (void)testRPCKnownBadStateWhenTLKsMissing {
1435 // Bring CKKS up in waitfortlk
1436 [self putFakeKeyHierarchyInCloudKit:self.keychainZoneID];
1437 [self putFakeDeviceStatusInCloudKit:self.keychainZoneID];
1438 [self startCKKSSubsystem];
1439
1440 XCTAssertEqual(0, [self.keychainView.keyHierarchyConditions[SecCKKSZoneKeyStateWaitForTLK] wait:20*NSEC_PER_SEC], "CKKS entered waitfortlk");
1441
1442 XCTestExpectation* callbackOccurs = [self expectationWithDescription:@"callback-occurs"];
1443
1444 [self.ckksControl rpcKnownBadState:@"keychain" reply:^(CKKSKnownBadState result) {
1445 XCTAssertEqual(result, CKKSKnownStateTLKsMissing, "TLKs should be missing");
1446 [callbackOccurs fulfill];
1447 }];
1448
1449 [self waitForExpectations:@[callbackOccurs] timeout:20];
1450
1451 OCMVerifyAllWithDelay(self.mockDatabase, 20);
1452 }
1453
1454 - (void)testRPCKnownBadStateWhenInWaitForUnlock {
1455 [self putFakeKeyHierarchyInCloudKit:self.keychainZoneID];
1456 [self saveTLKMaterialToKeychain:self.keychainZoneID];
1457 [self putFakeDeviceStatusInCloudKit:self.keychainZoneID];
1458
1459 // Bring CKKS up in 'waitforunlock'
1460 self.aksLockState = true;
1461 [self.lockStateTracker recheck];
1462 [self startCKKSSubsystem];
1463
1464 // Wait for the key hierarchy state machine to get stuck waiting for the unlock dependency. No uploads should occur.
1465 XCTAssertEqual(0, [self.keychainView.keyHierarchyConditions[SecCKKSZoneKeyStateWaitForUnlock] wait:20*NSEC_PER_SEC], @"Key state should get stuck in waitforunlock");
1466
1467 XCTestExpectation* callbackOccurs = [self expectationWithDescription:@"callback-occurs"];
1468
1469 [self.ckksControl rpcKnownBadState:@"keychain" reply:^(CKKSKnownBadState result) {
1470 XCTAssertEqual(result, CKKSKnownStateWaitForUnlock, "known state should be wait for unlock");
1471 [callbackOccurs fulfill];
1472 }];
1473
1474 [self waitForExpectations:@[callbackOccurs] timeout:20];
1475
1476 OCMVerifyAllWithDelay(self.mockDatabase, 20);
1477 }
1478
1479 - (void)testRPCKnownBadStateWhenInWaitForUpload {
1480 // Bring CKKS up in 'waitfortupload'
1481 self.aksLockState = true;
1482 [self.lockStateTracker recheck];
1483 [self startCKKSSubsystem];
1484
1485 // Wait for the key hierarchy state machine to get stuck waiting for Octagon. No uploads should occur.
1486 XCTAssertEqual(0, [self.keychainView.keyHierarchyConditions[SecCKKSZoneKeyStateWaitForTLKCreation] wait:20*NSEC_PER_SEC], @"Key state should get stuck in waitfortlkcreation");
1487
1488 XCTestExpectation* callbackOccurs = [self expectationWithDescription:@"callback-occurs"];
1489
1490 [self.ckksControl rpcKnownBadState:@"keychain" reply:^(CKKSKnownBadState result) {
1491 XCTAssertEqual(result, CKKSKnownStateWaitForOctagon, "known state should be wait for Octagon");
1492 [callbackOccurs fulfill];
1493 }];
1494
1495 [self waitForExpectations:@[callbackOccurs] timeout:20];
1496
1497 OCMVerifyAllWithDelay(self.mockDatabase, 20);
1498 }
1499
1500 - (void)testRPCKnownBadStateWhenInGoodState {
1501 // Bring CKKS up in 'ready'
1502 [self putFakeKeyHierarchyInCloudKit:self.keychainZoneID];
1503 [self saveTLKMaterialToKeychain:self.keychainZoneID];
1504 [self expectCKKSTLKSelfShareUpload:self.keychainZoneID];
1505 [self startCKKSSubsystem];
1506
1507 XCTAssertEqual(0, [self.keychainView.keyHierarchyConditions[SecCKKSZoneKeyStateReady] wait:20*NSEC_PER_SEC], "CKKS entered 'ready''");
1508
1509 XCTestExpectation* callbackOccurs = [self expectationWithDescription:@"callback-occurs"];
1510
1511 [self.ckksControl rpcKnownBadState:@"keychain" reply:^(CKKSKnownBadState result) {
1512 XCTAssertEqual(result, CKKSKnownStatePossiblyGood, "known state should not be possibly-good");
1513 [callbackOccurs fulfill];
1514 }];
1515
1516 [self waitForExpectations:@[callbackOccurs] timeout:20];
1517
1518 OCMVerifyAllWithDelay(self.mockDatabase, 20);
1519 }
1520
1521 - (void)testRpcStatus {
1522 [self createAndSaveFakeKeyHierarchy: self.keychainZoneID]; // Make life easy for this test.
1523
1524 [self startCKKSSubsystem];
1525
1526 // Let things shake themselves out.
1527 OCMVerifyAllWithDelay(self.mockDatabase, 20);
1528 XCTAssertEqual(0, [self.keychainView.keyHierarchyConditions[SecCKKSZoneKeyStateReady] wait:20*NSEC_PER_SEC], "Key state should return to 'ready'");
1529 [self waitForCKModifications];
1530
1531 XCTestExpectation* callbackOccurs = [self expectationWithDescription:@"callback-occurs"];
1532 [self.ckksControl rpcStatus:@"keychain" reply:^(NSArray<NSDictionary*>* result, NSError* error) {
1533 XCTAssertNil(error, "should be no error fetching status for keychain");
1534
1535 // Ugly "global" hack
1536 XCTAssertEqual(result.count, 2u, "Should have received two result dictionaries back");
1537 NSDictionary* keychainStatus = result[1];
1538
1539 XCTAssertNotNil(keychainStatus, "Should have received at least one zone status back");
1540 XCTAssertEqualObjects(keychainStatus[@"view"], @"keychain", "Should have received status for the keychain view");
1541 XCTAssertEqualObjects(keychainStatus[@"keystate"], SecCKKSZoneKeyStateReady, "Should be in 'ready' status");
1542 XCTAssertNotNil(keychainStatus[@"ckmirror"], "Status should have any ckmirror");
1543 [callbackOccurs fulfill];
1544 }];
1545
1546 [self waitForExpectations:@[callbackOccurs] timeout:20];
1547 }
1548
1549 - (void)testRpcFastStatus {
1550 [self createAndSaveFakeKeyHierarchy: self.keychainZoneID]; // Make life easy for this test.
1551
1552 [self startCKKSSubsystem];
1553
1554 // Let things shake themselves out.
1555 OCMVerifyAllWithDelay(self.mockDatabase, 20);
1556 XCTAssertEqual(0, [self.keychainView.keyHierarchyConditions[SecCKKSZoneKeyStateReady] wait:20*NSEC_PER_SEC], "Key state should return to 'ready'");
1557 [self waitForCKModifications];
1558
1559 XCTestExpectation* callbackOccurs = [self expectationWithDescription:@"callback-occurs"];
1560 [self.ckksControl rpcFastStatus:@"keychain" reply:^(NSArray<NSDictionary*>* result, NSError* error) {
1561 XCTAssertNil(error, "should be no error fetching status for keychain");
1562
1563 // Ugly "global" hack
1564 XCTAssertEqual(result.count, 1u, "Should have received one result dictionaries back");
1565 NSDictionary* keychainStatus = result[0];
1566
1567 XCTAssertNotNil(keychainStatus, "Should have received at least one zone status back");
1568 XCTAssertEqualObjects(keychainStatus[@"view"], @"keychain", "Should have received status for the keychain view");
1569 XCTAssertEqualObjects(keychainStatus[@"keystate"], SecCKKSZoneKeyStateReady, "Should be in 'ready' status");
1570 XCTAssertNil(keychainStatus[@"ckmirror"], "fastStatus should not have any ckmirror");
1571 [callbackOccurs fulfill];
1572 }];
1573
1574 [self waitForExpectations:@[callbackOccurs] timeout:20];
1575 }
1576
1577
1578 - (void)testRpcStatusWaitsForAccountDetermination {
1579 [self createAndSaveFakeKeyHierarchy: self.keychainZoneID]; // Make life easy for this test.
1580
1581 // Set up the account state callbacks to happen in one second
1582 dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (1 * NSEC_PER_SEC)), dispatch_get_global_queue(QOS_CLASS_DEFAULT, 0), ^{
1583 // Let CKKS come up (simulating daemon starting due to RPC)
1584 [self startCKKSSubsystem];
1585 });
1586
1587 // Before CKKS figures out we're in an account, fire off the status RPC.
1588 XCTestExpectation* callbackOccurs = [self expectationWithDescription:@"callback-occurs"];
1589 [self.ckksControl rpcStatus:@"keychain" reply:^(NSArray<NSDictionary*>* result, NSError* error) {
1590 XCTAssertNil(error, "should be no error fetching status for keychain");
1591
1592 // Ugly "global" hack
1593 XCTAssertEqual(result.count, 2u, "Should have received two result dictionaries back");
1594 NSDictionary* keychainStatus = result[1];
1595
1596 XCTAssertNotNil(keychainStatus, "Should have received at least one zone status back");
1597 XCTAssertEqualObjects(keychainStatus[@"view"], @"keychain", "Should have received status for the keychain view");
1598 XCTAssertEqualObjects(keychainStatus[@"keystate"], SecCKKSZoneKeyStateReady, "Should be in 'ready' status");
1599 [callbackOccurs fulfill];
1600 }];
1601
1602 [self waitForExpectations:@[callbackOccurs] timeout:20];
1603 }
1604
1605 - (void)testRpcStatusIsFastDuringError {
1606 [self createAndSaveFakeKeyHierarchy: self.keychainZoneID]; // Make life easy for this test.
1607
1608 self.keychainFetchError = [NSError errorWithDomain:NSOSStatusErrorDomain code:errSecInternalError description:@"injected keychain failure"];
1609
1610 // Let CKKS come up; it should enter 'error'
1611 [self startCKKSSubsystem];
1612 XCTAssertEqual(0, [self.keychainView.keyHierarchyConditions[SecCKKSZoneKeyStateError] wait:20*NSEC_PER_SEC], "CKKS entered 'error'");
1613
1614 // Fire off the status RPC; it should return immediately
1615 XCTestExpectation* callbackOccurs = [self expectationWithDescription:@"callback-occurs"];
1616 [self.ckksControl rpcStatus:@"keychain" reply:^(NSArray<NSDictionary*>* result, NSError* error) {
1617 XCTAssertNil(error, "should be no error fetching status for keychain");
1618
1619 // Ugly "global" hack
1620 XCTAssertEqual(result.count, 2u, "Should have received two result dictionaries back");
1621 NSDictionary* keychainStatus = result[1];
1622
1623 XCTAssertNotNil(keychainStatus, "Should have received at least one zone status back");
1624 XCTAssertEqualObjects(keychainStatus[@"view"], @"keychain", "Should have received status for the keychain view");
1625 XCTAssertEqualObjects(keychainStatus[@"keystate"], SecCKKSZoneKeyStateError, "Should be in 'ready' status");
1626 [callbackOccurs fulfill];
1627 }];
1628
1629 [self waitForExpectations:@[callbackOccurs] timeout:20];
1630 }
1631
1632 @end
1633
1634 #endif // OCTAGON