]> git.saurik.com Git - apple/security.git/blob - keychain/ckks/tests/CKKSTests+API.m
Security-58286.60.28.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 <Security/SecEntitlements.h>
32 #include <ipc/server_security_helpers.h>
33 #import <Foundation/NSXPCConnection_Private.h>
34
35 #import "keychain/ckks/tests/CloudKitMockXCTest.h"
36 #import "keychain/ckks/tests/CloudKitKeychainSyncingMockXCTest.h"
37 #import "keychain/ckks/CKKS.h"
38 #import "keychain/ckks/CKKSItem.h"
39 #import "keychain/ckks/CKKSItemEncrypter.h"
40 #import "keychain/ckks/CKKSKey.h"
41 #import "keychain/ckks/CKKSViewManager.h"
42 #import "keychain/ckks/CKKSZoneStateEntry.h"
43
44 #import "keychain/ckks/CKKSControl.h"
45 #import "keychain/ckks/CloudKitCategories.h"
46
47 #import "keychain/ckks/tests/MockCloudKit.h"
48 #import "keychain/ckks/tests/CKKSTests.h"
49 #import "keychain/ckks/tests/CKKSTests+API.h"
50
51 @implementation CloudKitKeychainSyncingTestsBase (APITests)
52
53 -(NSMutableDictionary*)pcsAddItemQuery:(NSString*)account
54 data:(NSData*)data
55 serviceIdentifier:(NSNumber*)serviceIdentifier
56 publicKey:(NSData*)publicKey
57 publicIdentity:(NSData*)publicIdentity
58 {
59 return [@{
60 (id)kSecClass : (id)kSecClassGenericPassword,
61 (id)kSecReturnPersistentRef: @YES,
62 (id)kSecReturnAttributes: @YES,
63 (id)kSecAttrAccessGroup : @"com.apple.security.ckks",
64 (id)kSecAttrAccessible: (id)kSecAttrAccessibleAfterFirstUnlock,
65 (id)kSecAttrAccount : account,
66 (id)kSecAttrSynchronizable : (id)kCFBooleanTrue,
67 (id)kSecValueData : data,
68 (id)kSecAttrDeriveSyncIDFromItemAttributes : (id)kCFBooleanTrue,
69 (id)kSecAttrPCSPlaintextServiceIdentifier : serviceIdentifier,
70 (id)kSecAttrPCSPlaintextPublicKey : publicKey,
71 (id)kSecAttrPCSPlaintextPublicIdentity : publicIdentity,
72 } mutableCopy];
73 }
74
75 -(NSDictionary*)pcsAddItem:(NSString*)account
76 data:(NSData*)data
77 serviceIdentifier:(NSNumber*)serviceIdentifier
78 publicKey:(NSData*)publicKey
79 publicIdentity:(NSData*)publicIdentity
80 expectingSync:(bool)expectingSync
81 {
82 NSMutableDictionary* query = [self pcsAddItemQuery:account
83 data:data
84 serviceIdentifier:(NSNumber*)serviceIdentifier
85 publicKey:(NSData*)publicKey
86 publicIdentity:(NSData*)publicIdentity];
87 CFTypeRef result = NULL;
88 XCTestExpectation* syncExpectation = [self expectationWithDescription: @"callback occurs"];
89
90 XCTAssertEqual(errSecSuccess, _SecItemAddAndNotifyOnSync((__bridge CFDictionaryRef) query, &result, ^(bool didSync, CFErrorRef error) {
91 if(expectingSync) {
92 XCTAssertTrue(didSync, "Item synced");
93 XCTAssertNil((__bridge NSError*)error, "No error syncing item");
94 } else {
95 XCTAssertFalse(didSync, "Item did not sync");
96 XCTAssertNotNil((__bridge NSError*)error, "Error syncing item");
97 }
98
99 [syncExpectation fulfill];
100 }), @"_SecItemAddAndNotifyOnSync succeeded");
101
102 // Verify that the item was written to CloudKit
103 OCMVerifyAllWithDelay(self.mockDatabase, 8);
104
105 // In real code, you'd need to wait for the _SecItemAddAndNotifyOnSync callback to succeed before proceeding
106 [self waitForExpectations:@[syncExpectation] timeout:8.0];
107
108 return (NSDictionary*) CFBridgingRelease(result);
109 }
110
111 - (BOOL (^) (CKRecord*)) checkPCSFieldsBlock: (CKRecordZoneID*) zoneID
112 PCSServiceIdentifier:(NSNumber*)servIdentifier
113 PCSPublicKey:(NSData*)publicKey
114 PCSPublicIdentity:(NSData*)publicIdentity
115 {
116 __weak __typeof(self) weakSelf = self;
117 return ^BOOL(CKRecord* record) {
118 __strong __typeof(weakSelf) strongSelf = weakSelf;
119 XCTAssertNotNil(strongSelf, "self exists");
120
121 XCTAssert([record[SecCKRecordPCSServiceIdentifier] isEqual: servIdentifier], "PCS Service identifier matches input");
122 XCTAssert([record[SecCKRecordPCSPublicKey] isEqual: publicKey], "PCS Public Key matches input");
123 XCTAssert([record[SecCKRecordPCSPublicIdentity] isEqual: publicIdentity], "PCS Public Identity matches input");
124
125 if([record[SecCKRecordPCSServiceIdentifier] isEqual: servIdentifier] &&
126 [record[SecCKRecordPCSPublicKey] isEqual: publicKey] &&
127 [record[SecCKRecordPCSPublicIdentity] isEqual: publicIdentity]) {
128 return YES;
129 } else {
130 return NO;
131 }
132 };
133 }
134 @end
135
136 @interface CloudKitKeychainSyncingAPITests : CloudKitKeychainSyncingTestsBase
137 @end
138
139 @implementation CloudKitKeychainSyncingAPITests
140 - (void)testSecuritydClientBringup {
141 #if 0
142 CFErrorRef cferror = nil;
143 xpc_endpoint_t endpoint = SecCreateSecuritydXPCServerEndpoint(&cferror);
144 XCTAssertNil((__bridge id)cferror, "No error creating securityd endpoint");
145 XCTAssertNotNil(endpoint, "Received securityd endpoint");
146 #endif
147
148 NSXPCInterface *interface = [NSXPCInterface interfaceWithProtocol:@protocol(SecuritydXPCProtocol)];
149 [SecuritydXPCClient configureSecuritydXPCProtocol: interface];
150 XCTAssertNotNil(interface, "Received a configured CKKS interface");
151
152 #if 0
153 NSXPCListenerEndpoint *listenerEndpoint = [[NSXPCListenerEndpoint alloc] init];
154 [listenerEndpoint _setEndpoint:endpoint];
155
156 NSXPCConnection* connection = [[NSXPCConnection alloc] initWithListenerEndpoint:listenerEndpoint];
157 XCTAssertNotNil(connection , "Received an active connection");
158
159 connection.remoteObjectInterface = interface;
160 #endif
161 }
162
163 - (void)testAddAndNotifyOnSync {
164 [self createAndSaveFakeKeyHierarchy: self.keychainZoneID]; // Make life easy for this test.
165
166 [self expectCKModifyItemRecords: 1 currentKeyPointerRecords: 1 zoneID:self.keychainZoneID];
167 [self startCKKSSubsystem];
168
169 // Let things shake themselves out.
170 [self.keychainView waitForKeyHierarchyReadiness];
171 [self waitForCKModifications];
172
173 NSMutableDictionary* query = [@{
174 (id)kSecClass : (id)kSecClassGenericPassword,
175 (id)kSecAttrAccessGroup : @"com.apple.security.ckks",
176 (id)kSecAttrAccessible: (id)kSecAttrAccessibleAfterFirstUnlock,
177 (id)kSecAttrAccount : @"testaccount",
178 (id)kSecAttrSynchronizable : (id)kCFBooleanTrue,
179 (id)kSecValueData : (id) [@"asdf" dataUsingEncoding:NSUTF8StringEncoding],
180 } mutableCopy];
181
182 XCTestExpectation* blockExpectation = [self expectationWithDescription: @"callback occurs"];
183
184 XCTAssertEqual(errSecSuccess, _SecItemAddAndNotifyOnSync((__bridge CFDictionaryRef) query, NULL, ^(bool didSync, CFErrorRef error) {
185 XCTAssertTrue(didSync, "Item synced properly");
186 XCTAssertNil((__bridge NSError*)error, "No error syncing item");
187
188 [blockExpectation fulfill];
189 }), @"_SecItemAddAndNotifyOnSync succeeded");
190
191 [self waitForExpectationsWithTimeout:5.0 handler:nil];
192 }
193
194 - (void)testAddAndNotifyOnSyncFailure {
195 [self createAndSaveFakeKeyHierarchy: self.keychainZoneID]; // Make life easy for this test.
196
197 [self startCKKSSubsystem];
198 [self.keychainView waitForFetchAndIncomingQueueProcessing];
199
200 // Due to item UUID selection, this item will be added with UUID 50184A35-4480-E8BA-769B-567CF72F1EC0.
201 // Add it to CloudKit first!
202 CKRecord* ckr = [self createFakeRecord: self.keychainZoneID recordName:@"50184A35-4480-E8BA-769B-567CF72F1EC0"];
203 [self.keychainZone addToZone: ckr];
204
205
206 // Go for it!
207 [self expectCKAtomicModifyItemRecordsUpdateFailure: self.keychainZoneID];
208
209 NSMutableDictionary* query = [@{
210 (id)kSecClass : (id)kSecClassGenericPassword,
211 (id)kSecAttrAccessGroup : @"com.apple.security.ckks",
212 (id)kSecAttrAccessible: (id)kSecAttrAccessibleAfterFirstUnlock,
213 (id)kSecAttrAccount : @"testaccount",
214 (id)kSecAttrSynchronizable : (id)kCFBooleanTrue,
215 (id)kSecValueData : (id) [@"asdf" dataUsingEncoding:NSUTF8StringEncoding],
216 (id)kSecAttrSyncViewHint : self.keychainView.zoneName, // @ fake view hint for fake view
217 } mutableCopy];
218
219 XCTestExpectation* blockExpectation = [self expectationWithDescription: @"callback occurs"];
220
221 XCTAssertEqual(errSecSuccess, _SecItemAddAndNotifyOnSync((__bridge CFDictionaryRef) query, NULL, ^(bool didSync, CFErrorRef error) {
222 XCTAssertFalse(didSync, "Item did not sync (as expected)");
223 XCTAssertNotNil((__bridge NSError*)error, "error exists when item fails to sync");
224
225 [blockExpectation fulfill];
226 }), @"_SecItemAddAndNotifyOnSync succeeded");
227
228 [self waitForExpectationsWithTimeout:5.0 handler:nil];
229 [self waitForCKModifications];
230 }
231
232 - (void)testAddAndNotifyOnSyncLoggedOut {
233 // Test starts with nothing in database and the user logged out of CloudKit. We expect no CKKS operations.
234 self.accountStatus = CKAccountStatusNoAccount;
235 self.silentFetchesAllowed = false;
236 [self startCKKSSubsystem];
237
238 XCTAssertEqual(0, [self.keychainView.loggedOut wait:2*NSEC_PER_SEC], "CKKS should positively log out");
239
240 NSMutableDictionary* query = [@{
241 (id)kSecClass : (id)kSecClassGenericPassword,
242 (id)kSecAttrAccessGroup : @"com.apple.security.ckks",
243 (id)kSecAttrAccessible: (id)kSecAttrAccessibleAfterFirstUnlock,
244 (id)kSecAttrAccount : @"testaccount",
245 (id)kSecAttrSynchronizable : (id)kCFBooleanTrue,
246 (id)kSecValueData : (id) [@"asdf" dataUsingEncoding:NSUTF8StringEncoding],
247 } mutableCopy];
248
249 XCTestExpectation* blockExpectation = [self expectationWithDescription: @"callback occurs"];
250
251 XCTAssertEqual(errSecSuccess, _SecItemAddAndNotifyOnSync((__bridge CFDictionaryRef) query, NULL, ^(bool didSync, CFErrorRef error) {
252 XCTAssertFalse(didSync, "Item did not sync (with no iCloud account)");
253 XCTAssertNotNil((__bridge NSError*)error, "Error exists syncing item while logged out");
254
255 [blockExpectation fulfill];
256 }), @"_SecItemAddAndNotifyOnSync succeeded");
257
258 [self waitForExpectationsWithTimeout:5.0 handler:nil];
259 }
260
261 - (void)testAddAndNotifyOnSyncAccountStatusUnclear {
262 // Test starts with nothing in database, but CKKS hasn't been told we've logged out yet.
263 // We expect no CKKS operations.
264 self.accountStatus = CKAccountStatusNoAccount;
265 self.silentFetchesAllowed = false;
266
267 NSMutableDictionary* query = [@{
268 (id)kSecClass : (id)kSecClassGenericPassword,
269 (id)kSecAttrAccessGroup : @"com.apple.security.ckks",
270 (id)kSecAttrAccessible: (id)kSecAttrAccessibleAfterFirstUnlock,
271 (id)kSecAttrAccount : @"testaccount",
272 (id)kSecAttrSynchronizable : (id)kCFBooleanTrue,
273 (id)kSecValueData : (id) [@"asdf" dataUsingEncoding:NSUTF8StringEncoding],
274 } mutableCopy];
275
276 XCTestExpectation* blockExpectation = [self expectationWithDescription: @"callback occurs"];
277
278 XCTAssertEqual(errSecSuccess, _SecItemAddAndNotifyOnSync((__bridge CFDictionaryRef) query, NULL, ^(bool didSync, CFErrorRef error) {
279 XCTAssertFalse(didSync, "Item did not sync (with no iCloud account)");
280 XCTAssertNotNil((__bridge NSError*)error, "Error exists syncing item while logged out");
281
282 [blockExpectation fulfill];
283 }), @"_SecItemAddAndNotifyOnSync succeeded");
284
285 // And now, allow CKKS to discover we're logged out
286 [self startCKKSSubsystem];
287 XCTAssertEqual(0, [self.keychainView.loggedOut wait:2*NSEC_PER_SEC], "CKKS should positively log out");
288
289 [self waitForExpectationsWithTimeout:5.0 handler:nil];
290 }
291
292 - (void)testAddAndNotifyOnSyncBeforeKeyHierarchyReady {
293 // Test starts with a key hierarchy in cloudkit and the TLK having arrived
294 [self putFakeKeyHierarchyInCloudKit:self.keychainZoneID];
295 [self saveTLKMaterialToKeychain:self.keychainZoneID];
296 [self expectCKKSTLKSelfShareUpload:self.keychainZoneID];
297
298 // But block CloudKit fetches (so the key hierarchy won't be ready when we add this new item)
299 [self holdCloudKitFetches];
300
301 [self startCKKSSubsystem];
302 XCTAssertEqual(0, [self.keychainView.loggedIn wait:2*NSEC_PER_SEC], "CKKS should log in");
303 [self.keychainView.zoneSetupOperation waitUntilFinished];
304
305 NSMutableDictionary* query = [@{
306 (id)kSecClass : (id)kSecClassGenericPassword,
307 (id)kSecAttrAccessGroup : @"com.apple.security.ckks",
308 (id)kSecAttrAccessible: (id)kSecAttrAccessibleAfterFirstUnlock,
309 (id)kSecAttrAccount : @"testaccount",
310 (id)kSecAttrSynchronizable : (id)kCFBooleanTrue,
311 (id)kSecAttrSyncViewHint : self.keychainView.zoneName,
312 (id)kSecValueData : (id) [@"asdf" dataUsingEncoding:NSUTF8StringEncoding],
313 } mutableCopy];
314
315 XCTestExpectation* blockExpectation = [self expectationWithDescription: @"callback occurs"];
316
317 XCTAssertEqual(errSecSuccess, _SecItemAddAndNotifyOnSync((__bridge CFDictionaryRef) query, NULL, ^(bool didSync, CFErrorRef error) {
318 XCTAssertTrue(didSync, "Item synced");
319 XCTAssertNil((__bridge NSError*)error, "Shouldn't have received an error syncing item");
320
321 [blockExpectation fulfill];
322 }), @"_SecItemAddAndNotifyOnSync succeeded");
323
324 // We should be in the 'fetch' state, but no further
325 XCTAssertEqual(0, [self.keychainView.keyHierarchyConditions[SecCKKSZoneKeyStateFetch] wait:100*NSEC_PER_MSEC], @"Should have reached key state 'fetch', but no further");
326
327 // When we release the fetch, the callback should still fire and the item should upload
328 [self expectCKModifyItemRecords: 1 currentKeyPointerRecords: 1 zoneID:self.keychainZoneID];
329 [self releaseCloudKitFetchHold];
330 XCTAssertEqual(0, [self.keychainView.keyHierarchyConditions[SecCKKSZoneKeyStateReady] wait:5*NSEC_PER_SEC], @"Should have reached key state 'ready'");
331
332 // Verify that the item was written to CloudKit
333 OCMVerifyAllWithDelay(self.mockDatabase, 8);
334
335 [self waitForExpectationsWithTimeout:5.0 handler:nil];
336 }
337
338 - (void)testPCSUnencryptedFieldsAdd {
339 [self createAndSaveFakeKeyHierarchy: self.keychainZoneID]; // Make life easy for this test.
340
341 [self startCKKSSubsystem];
342 [self.keychainView waitForKeyHierarchyReadiness];
343
344 NSNumber* servIdentifier = @3;
345 NSData* publicKey = [@"asdfasdf" dataUsingEncoding:NSUTF8StringEncoding];
346 NSData* publicIdentity = [@"somedata" dataUsingEncoding:NSUTF8StringEncoding];
347
348 [self expectCKModifyItemRecords: 1 currentKeyPointerRecords: 1 zoneID:self.keychainZoneID
349 checkItem: [self checkPCSFieldsBlock:self.keychainZoneID
350 PCSServiceIdentifier:(NSNumber *)servIdentifier
351 PCSPublicKey:publicKey
352 PCSPublicIdentity:publicIdentity]];
353
354 NSMutableDictionary* query = [@{
355 (id)kSecClass : (id)kSecClassGenericPassword,
356 (id)kSecAttrAccessGroup : @"com.apple.security.ckks",
357 (id)kSecAttrAccessible: (id)kSecAttrAccessibleAfterFirstUnlock,
358 (id)kSecAttrAccount : @"testaccount",
359 (id)kSecAttrSynchronizable : (id)kCFBooleanTrue,
360 (id)kSecValueData : (id) [@"asdf" dataUsingEncoding:NSUTF8StringEncoding],
361 (id)kSecAttrDeriveSyncIDFromItemAttributes : (id)kCFBooleanTrue,
362 (id)kSecAttrPCSPlaintextServiceIdentifier : servIdentifier,
363 (id)kSecAttrPCSPlaintextPublicKey : publicKey,
364 (id)kSecAttrPCSPlaintextPublicIdentity : publicIdentity,
365 (id)kSecAttrSyncViewHint : self.keychainView.zoneName, // allows a CKKSScanOperation to find this item
366 } mutableCopy];
367
368 XCTAssertEqual(errSecSuccess, SecItemAdd((__bridge CFDictionaryRef) query, NULL), @"SecItemAdd succeeded");
369
370 // Verify that the item is written to CloudKit
371 OCMVerifyAllWithDelay(self.mockDatabase, 4);
372
373 CFTypeRef item = NULL;
374 query[(id)kSecValueData] = nil;
375 query[(id)kSecReturnAttributes] = @YES;
376 XCTAssertEqual(errSecSuccess, SecItemCopyMatching((__bridge CFDictionaryRef) query, &item), "item should still exist");
377
378 NSDictionary* itemAttributes = (NSDictionary*) CFBridgingRelease(item);
379 XCTAssertEqualObjects(itemAttributes[(id)kSecAttrPCSPlaintextServiceIdentifier], servIdentifier, "Service Identifier exists");
380 XCTAssertEqualObjects(itemAttributes[(id)kSecAttrPCSPlaintextPublicKey], publicKey, "public key exists");
381 XCTAssertEqualObjects(itemAttributes[(id)kSecAttrPCSPlaintextPublicIdentity], publicIdentity, "public identity exists");
382
383 // Find the item record in CloudKit. Since we're using kSecAttrDeriveSyncIDFromItemAttributes,
384 // the record ID is likely 50184A35-4480-E8BA-769B-567CF72F1EC0
385 [self waitForCKModifications];
386 CKRecordID* recordID = [[CKRecordID alloc] initWithRecordName: @"50184A35-4480-E8BA-769B-567CF72F1EC0" zoneID:self.keychainZoneID];
387 CKRecord* record = self.keychainZone.currentDatabase[recordID];
388 XCTAssertNotNil(record, "Found record in CloudKit at expected UUID");
389
390 XCTAssertEqualObjects(record[SecCKRecordPCSServiceIdentifier], servIdentifier, "Service identifier sent to cloudkit");
391 XCTAssertEqualObjects(record[SecCKRecordPCSPublicKey], publicKey, "public key sent to cloudkit");
392 XCTAssertEqualObjects(record[SecCKRecordPCSPublicIdentity], publicIdentity, "public identity sent to cloudkit");
393 }
394
395 - (void)testPCSUnencryptedFieldsModify {
396 [self createAndSaveFakeKeyHierarchy: self.keychainZoneID]; // Make life easy for this test.
397
398 [self startCKKSSubsystem];
399 [self.keychainView waitForKeyHierarchyReadiness];
400
401 NSNumber* servIdentifier = @3;
402 NSData* publicKey = [@"asdfasdf" dataUsingEncoding:NSUTF8StringEncoding];
403 NSData* publicIdentity = [@"somedata" dataUsingEncoding:NSUTF8StringEncoding];
404
405 [self expectCKModifyItemRecords: 1 currentKeyPointerRecords: 1 zoneID:self.keychainZoneID
406 checkItem: [self checkPCSFieldsBlock:self.keychainZoneID
407 PCSServiceIdentifier:(NSNumber *)servIdentifier
408 PCSPublicKey:publicKey
409 PCSPublicIdentity:publicIdentity]];
410
411 NSMutableDictionary* query = [@{
412 (id)kSecClass : (id)kSecClassGenericPassword,
413 (id)kSecAttrAccessGroup : @"com.apple.security.ckks",
414 (id)kSecAttrAccessible: (id)kSecAttrAccessibleAfterFirstUnlock,
415 (id)kSecAttrAccount : @"testaccount",
416 (id)kSecAttrSynchronizable : (id)kCFBooleanTrue,
417 (id)kSecValueData : (id) [@"asdf" dataUsingEncoding:NSUTF8StringEncoding],
418 (id)kSecAttrDeriveSyncIDFromItemAttributes : (id)kCFBooleanTrue,
419 (id)kSecAttrPCSPlaintextServiceIdentifier : servIdentifier,
420 (id)kSecAttrPCSPlaintextPublicKey : publicKey,
421 (id)kSecAttrPCSPlaintextPublicIdentity : publicIdentity,
422 (id)kSecAttrSyncViewHint : self.keychainView.zoneName, // allows a CKKSScanOperation to find this item
423 } mutableCopy];
424
425 XCTAssertEqual(errSecSuccess, SecItemAdd((__bridge CFDictionaryRef) query, NULL), @"SecItemAdd succeeded");
426
427 OCMVerifyAllWithDelay(self.mockDatabase, 8);
428 [self waitForCKModifications];
429
430 query[(id)kSecValueData] = nil;
431 query[(id)kSecAttrPCSPlaintextServiceIdentifier] = nil;
432 query[(id)kSecAttrPCSPlaintextPublicKey] = nil;
433 query[(id)kSecAttrPCSPlaintextPublicIdentity] = nil;
434
435 servIdentifier = @1;
436 publicKey = [@"new public key" dataUsingEncoding:NSUTF8StringEncoding];
437
438 NSNumber* newServiceIdentifier = @10;
439 NSData* newPublicKey = [@"new public key" dataUsingEncoding:NSUTF8StringEncoding];
440 NSData* newPublicIdentity = [@"new public identity" dataUsingEncoding:NSUTF8StringEncoding];
441
442 NSDictionary* update = @{
443 (id)kSecAttrPCSPlaintextServiceIdentifier : newServiceIdentifier,
444 (id)kSecAttrPCSPlaintextPublicKey : newPublicKey,
445 (id)kSecAttrPCSPlaintextPublicIdentity : newPublicIdentity,
446 };
447
448 [self expectCKModifyItemRecords: 1 currentKeyPointerRecords: 1 zoneID:self.keychainZoneID
449 checkItem: [self checkPCSFieldsBlock:self.keychainZoneID
450 PCSServiceIdentifier:(NSNumber *)newServiceIdentifier
451 PCSPublicKey:newPublicKey
452 PCSPublicIdentity:newPublicIdentity]];
453
454 XCTAssertEqual(errSecSuccess, SecItemUpdate((__bridge CFDictionaryRef) query, (__bridge CFDictionaryRef) update), @"SecItemUpdate succeeded");
455 OCMVerifyAllWithDelay(self.mockDatabase, 8);
456
457 CFTypeRef item = NULL;
458 query[(id)kSecValueData] = nil;
459 query[(id)kSecReturnAttributes] = @YES;
460 XCTAssertEqual(errSecSuccess, SecItemCopyMatching((__bridge CFDictionaryRef) query, &item), "item should still exist");
461
462 NSDictionary* itemAttributes = (NSDictionary*) CFBridgingRelease(item);
463 XCTAssertEqualObjects(itemAttributes[(id)kSecAttrPCSPlaintextServiceIdentifier], newServiceIdentifier, "Service Identifier exists");
464 XCTAssertEqualObjects(itemAttributes[(id)kSecAttrPCSPlaintextPublicKey], newPublicKey, "public key exists");
465 XCTAssertEqualObjects(itemAttributes[(id)kSecAttrPCSPlaintextPublicIdentity], newPublicIdentity, "public identity exists");
466
467 // Find the item record in CloudKit. Since we're using kSecAttrDeriveSyncIDFromItemAttributes,
468 // the record ID is likely 50184A35-4480-E8BA-769B-567CF72F1EC0
469 [self waitForCKModifications];
470 CKRecordID* recordID = [[CKRecordID alloc] initWithRecordName: @"50184A35-4480-E8BA-769B-567CF72F1EC0" zoneID:self.keychainZoneID];
471 CKRecord* record = self.keychainZone.currentDatabase[recordID];
472 XCTAssertNotNil(record, "Found record in CloudKit at expected UUID");
473
474 XCTAssertEqualObjects(record[SecCKRecordPCSServiceIdentifier], newServiceIdentifier, "Service identifier sent to cloudkit");
475 XCTAssertEqualObjects(record[SecCKRecordPCSPublicKey], newPublicKey, "public key sent to cloudkit");
476 XCTAssertEqualObjects(record[SecCKRecordPCSPublicIdentity], newPublicIdentity, "public identity sent to cloudkit");
477 }
478
479 // As of [<rdar://problem/32558310> CKKS: Re-authenticate PCSPublicFields], these fields are NOT server-modifiable. This test proves it.
480 - (void)testPCSUnencryptedFieldsServerModifyFail {
481 [self createAndSaveFakeKeyHierarchy: self.keychainZoneID]; // Make life easy for this test.
482
483 [self startCKKSSubsystem];
484 [self.keychainView waitForKeyHierarchyReadiness];
485
486 NSNumber* servIdentifier = @3;
487 NSData* publicKey = [@"asdfasdf" dataUsingEncoding:NSUTF8StringEncoding];
488 NSData* publicIdentity = [@"somedata" dataUsingEncoding:NSUTF8StringEncoding];
489
490 [self expectCKModifyItemRecords: 1 currentKeyPointerRecords: 1 zoneID:self.keychainZoneID
491 checkItem: [self checkPCSFieldsBlock:self.keychainZoneID
492 PCSServiceIdentifier:(NSNumber *)servIdentifier
493 PCSPublicKey:publicKey
494 PCSPublicIdentity:publicIdentity]];
495
496 NSMutableDictionary* query = [@{
497 (id)kSecClass : (id)kSecClassGenericPassword,
498 (id)kSecAttrAccessGroup : @"com.apple.security.ckks",
499 (id)kSecAttrAccessible: (id)kSecAttrAccessibleAfterFirstUnlock,
500 (id)kSecAttrAccount : @"testaccount",
501 (id)kSecAttrSynchronizable : (id)kCFBooleanTrue,
502 (id)kSecValueData : (id) [@"asdf" dataUsingEncoding:NSUTF8StringEncoding],
503 (id)kSecAttrDeriveSyncIDFromItemAttributes : (id)kCFBooleanTrue,
504 (id)kSecAttrPCSPlaintextServiceIdentifier : servIdentifier,
505 (id)kSecAttrPCSPlaintextPublicKey : publicKey,
506 (id)kSecAttrPCSPlaintextPublicIdentity : publicIdentity,
507 (id)kSecAttrSyncViewHint : self.keychainView.zoneName, // fake, for CKKSScanOperation
508 } mutableCopy];
509
510 XCTAssertEqual(errSecSuccess, SecItemAdd((__bridge CFDictionaryRef) query, NULL), @"SecItemAdd succeeded");
511
512 OCMVerifyAllWithDelay(self.mockDatabase, 4);
513 [self waitForCKModifications];
514
515 // Find the item record in CloudKit. Since we're using kSecAttrDeriveSyncIDFromItemAttributes,
516 // the record ID is likely 50184A35-4480-E8BA-769B-567CF72F1EC0
517 CKRecordID* recordID = [[CKRecordID alloc] initWithRecordName: @"50184A35-4480-E8BA-769B-567CF72F1EC0" zoneID:self.keychainZoneID];
518 CKRecord* record = self.keychainZone.currentDatabase[recordID];
519 XCTAssertNotNil(record, "Found record in CloudKit at expected UUID");
520
521 // Items are encrypted using encv2
522 XCTAssertEqualObjects(record[SecCKRecordEncryptionVersionKey], [NSNumber numberWithInteger:(int) CKKSItemEncryptionVersion2], "Uploaded using encv2");
523
524 if(!record) {
525 // Test has already failed; find the record just to be nice.
526 for(CKRecord* maybe in self.keychainZone.currentDatabase.allValues) {
527 if(maybe[SecCKRecordPCSServiceIdentifier] != nil) {
528 record = maybe;
529 }
530 }
531 }
532
533 NSNumber* newServiceIdentifier = @10;
534 NSData* newPublicKey = [@"new public key" dataUsingEncoding:NSUTF8StringEncoding];
535 NSData* newPublicIdentity = [@"new public identity" dataUsingEncoding:NSUTF8StringEncoding];
536
537 // Change the public key and public identity
538 record = [record copyWithZone: nil];
539 record[SecCKRecordPCSServiceIdentifier] = newServiceIdentifier;
540 record[SecCKRecordPCSPublicKey] = newPublicKey;
541 record[SecCKRecordPCSPublicIdentity] = newPublicIdentity;
542 [self.keychainZone addToZone: record];
543
544 // Trigger a notification
545 [self.keychainView notifyZoneChange:nil];
546 [self.keychainView waitForFetchAndIncomingQueueProcessing];
547
548 CFTypeRef item = NULL;
549 query[(id)kSecValueData] = nil;
550 query[(id)kSecAttrPCSPlaintextServiceIdentifier] = nil;
551 query[(id)kSecAttrPCSPlaintextPublicKey] = nil;
552 query[(id)kSecAttrPCSPlaintextPublicIdentity] = nil;
553 query[(id)kSecReturnAttributes] = @YES;
554 XCTAssertEqual(errSecSuccess, SecItemCopyMatching((__bridge CFDictionaryRef) query, &item), "item should still exist");
555
556 NSDictionary* itemAttributes = (NSDictionary*) CFBridgingRelease(item);
557 XCTAssertEqualObjects(itemAttributes[(id)kSecAttrPCSPlaintextServiceIdentifier], servIdentifier, "service identifier is not updated");
558 XCTAssertEqualObjects(itemAttributes[(id)kSecAttrPCSPlaintextPublicKey], publicKey, "public key not updated");
559 XCTAssertEqualObjects(itemAttributes[(id)kSecAttrPCSPlaintextPublicIdentity], publicIdentity, "public identity not updated");
560 }
561
562 -(void)testPCSUnencryptedFieldsRecieveUnauthenticatedFields {
563 [self createAndSaveFakeKeyHierarchy: self.keychainZoneID]; // Make life easy for this test.
564
565 [self startCKKSSubsystem];
566 [self.keychainView waitForKeyHierarchyReadiness];
567
568 NSNumber* servIdentifier = @3;
569 NSData* publicKey = [@"asdfasdf" dataUsingEncoding:NSUTF8StringEncoding];
570 NSData* publicIdentity = [@"somedata" dataUsingEncoding:NSUTF8StringEncoding];
571
572 NSError* error = nil;
573
574 // Manually encrypt an item
575 NSString* recordName = @"7B598D31-F9C5-481E-98AC-5A507ACB2D85";
576 CKRecordID* recordID = [[CKRecordID alloc] initWithRecordName:recordName zoneID:self.keychainZoneID];
577 NSDictionary* item = [self fakeRecordDictionary: @"account-delete-me" zoneID:self.keychainZoneID];
578 CKKSItem* cipheritem = [[CKKSItem alloc] initWithUUID:recordID.recordName
579 parentKeyUUID:self.keychainZoneKeys.classC.uuid
580 zoneID:recordID.zoneID];
581 CKKSKey* itemkey = [CKKSKey randomKeyWrappedByParent: self.keychainZoneKeys.classC error:&error];
582 XCTAssertNotNil(itemkey, "Got a key");
583 cipheritem.wrappedkey = itemkey.wrappedkey;
584 XCTAssertNotNil(cipheritem.wrappedkey, "Got a wrapped key");
585
586 cipheritem.encver = CKKSItemEncryptionVersion1;
587
588 // This item has the PCS public fields, but they are not authenticated
589 cipheritem.plaintextPCSServiceIdentifier = servIdentifier;
590 cipheritem.plaintextPCSPublicKey = publicKey;
591 cipheritem.plaintextPCSPublicIdentity = publicIdentity;
592
593 NSDictionary<NSString*, NSData*>* authenticatedData = [cipheritem makeAuthenticatedDataDictionaryUpdatingCKKSItem: nil encryptionVersion:CKKSItemEncryptionVersion1];
594 cipheritem.encitem = [CKKSItemEncrypter encryptDictionary:item key:itemkey.aessivkey authenticatedData:authenticatedData error:&error];
595 XCTAssertNil(error, "no error encrypting object");
596 XCTAssertNotNil(cipheritem.encitem, "Recieved ciphertext");
597
598 [self.keychainZone addToZone:[cipheritem CKRecordWithZoneID: recordID.zoneID]];
599
600 [self.keychainView waitForFetchAndIncomingQueueProcessing];
601
602 NSDictionary* query = @{(id)kSecClass: (id)kSecClassGenericPassword,
603 (id)kSecReturnAttributes: @YES,
604 (id)kSecAttrSynchronizable: @YES,
605 (id)kSecAttrAccount: @"account-delete-me",
606 (id)kSecMatchLimit: (id)kSecMatchLimitOne,
607 };
608 CFTypeRef cfresult = NULL;
609 XCTAssertEqual(errSecSuccess, SecItemCopyMatching((__bridge CFDictionaryRef) query, &cfresult), "Found synced item");
610
611 NSDictionary* result = CFBridgingRelease(cfresult);
612 XCTAssertEqualObjects(result[(id)kSecAttrPCSPlaintextServiceIdentifier], servIdentifier, "Received PCS service identifier");
613 XCTAssertEqualObjects(result[(id)kSecAttrPCSPlaintextPublicKey], publicKey, "Received PCS public key");
614 XCTAssertEqualObjects(result[(id)kSecAttrPCSPlaintextPublicIdentity], publicIdentity, "Received PCS public identity");
615 }
616
617 -(void)testPCSUnencryptedFieldsRecieveAuthenticatedFields {
618 [self createAndSaveFakeKeyHierarchy: self.keychainZoneID]; // Make life easy for this test.
619
620 [self startCKKSSubsystem];
621 [self.keychainView waitForKeyHierarchyReadiness];
622 [self.keychainView waitForOperationsOfClass:[CKKSIncomingQueueOperation class]];
623
624 NSNumber* servIdentifier = @3;
625 NSData* publicKey = [@"asdfasdf" dataUsingEncoding:NSUTF8StringEncoding];
626 NSData* publicIdentity = [@"somedata" dataUsingEncoding:NSUTF8StringEncoding];
627
628 NSError* error = nil;
629
630 // Manually encrypt an item
631 NSString* recordName = @"7B598D31-F9C5-481E-98AC-5A507ACB2D85";
632 CKRecordID* recordID = [[CKRecordID alloc] initWithRecordName:recordName zoneID:self.keychainZoneID];
633 NSDictionary* item = [self fakeRecordDictionary: @"account-delete-me" zoneID:self.keychainZoneID];
634 CKKSItem* cipheritem = [[CKKSItem alloc] initWithUUID:recordID.recordName
635 parentKeyUUID:self.keychainZoneKeys.classC.uuid
636 zoneID:recordID.zoneID];
637 CKKSKey* itemkey = [CKKSKey randomKeyWrappedByParent: self.keychainZoneKeys.classC error:&error];
638 XCTAssertNotNil(itemkey, "Got a key");
639 cipheritem.wrappedkey = itemkey.wrappedkey;
640 XCTAssertNotNil(cipheritem.wrappedkey, "Got a wrapped key");
641
642 cipheritem.encver = CKKSItemEncryptionVersion2;
643
644 // This item has the PCS public fields, and they are authenticated (since we're using v2)
645 cipheritem.plaintextPCSServiceIdentifier = servIdentifier;
646 cipheritem.plaintextPCSPublicKey = publicKey;
647 cipheritem.plaintextPCSPublicIdentity = publicIdentity;
648
649 // Use version 2, so PCS plaintext fields will be authenticated
650 NSMutableDictionary<NSString*, NSData*>* authenticatedData = [[cipheritem makeAuthenticatedDataDictionaryUpdatingCKKSItem: nil encryptionVersion:CKKSItemEncryptionVersion2] mutableCopy];
651
652 cipheritem.encitem = [CKKSItemEncrypter encryptDictionary:item key:itemkey.aessivkey authenticatedData:authenticatedData error:&error];
653 XCTAssertNil(error, "no error encrypting object");
654 XCTAssertNotNil(cipheritem.encitem, "Recieved ciphertext");
655
656 [self.keychainZone addToZone:[cipheritem CKRecordWithZoneID: recordID.zoneID]];
657
658 [self.keychainView waitForFetchAndIncomingQueueProcessing];
659
660 NSDictionary* query = @{(id)kSecClass: (id)kSecClassGenericPassword,
661 (id)kSecReturnAttributes: @YES,
662 (id)kSecAttrSynchronizable: @YES,
663 (id)kSecAttrAccount: @"account-delete-me",
664 (id)kSecMatchLimit: (id)kSecMatchLimitOne,
665 };
666 CFTypeRef cfresult = NULL;
667 XCTAssertEqual(errSecSuccess, SecItemCopyMatching((__bridge CFDictionaryRef) query, &cfresult), "Found synced item");
668
669 NSDictionary* result = CFBridgingRelease(cfresult);
670 XCTAssertEqualObjects(result[(id)kSecAttrPCSPlaintextServiceIdentifier], servIdentifier, "Received PCS service identifier");
671 XCTAssertEqualObjects(result[(id)kSecAttrPCSPlaintextPublicKey], publicKey, "Received PCS public key");
672 XCTAssertEqualObjects(result[(id)kSecAttrPCSPlaintextPublicIdentity], publicIdentity, "Received PCS public identity");
673
674 // Test that if this item is updated, it remains encrypted in v2
675 [self expectCKModifyItemRecords: 1 currentKeyPointerRecords: 1 zoneID:self.keychainZoneID
676 checkItem: [self checkPCSFieldsBlock:self.keychainZoneID
677 PCSServiceIdentifier:(NSNumber *)servIdentifier
678 PCSPublicKey:publicKey
679 PCSPublicIdentity:publicIdentity]];
680 [self updateGenericPassword:@"different password" account:@"account-delete-me"];
681
682 OCMVerifyAllWithDelay(self.mockDatabase, 4);
683 [self waitForCKModifications];
684
685 CKRecord* newRecord = self.keychainZone.currentDatabase[recordID];
686 XCTAssertEqualObjects(newRecord[SecCKRecordPCSServiceIdentifier], servIdentifier, "Didn't change service identifier");
687 XCTAssertEqualObjects(newRecord[SecCKRecordPCSPublicKey], publicKey, "Didn't change public key");
688 XCTAssertEqualObjects(newRecord[SecCKRecordPCSPublicIdentity], publicIdentity, "Didn't change public identity");
689 XCTAssertEqualObjects(newRecord[SecCKRecordEncryptionVersionKey], [NSNumber numberWithInteger:(int) CKKSItemEncryptionVersion2], "Uploaded using encv2");
690 }
691
692 -(void)testResetLocal {
693 // Test starts with nothing in database, but one in our fake CloudKit.
694 [self putFakeKeyHierarchyInCloudKit:self.keychainZoneID];
695 [self expectCKKSTLKSelfShareUpload:self.keychainZoneID];
696 [self saveTLKMaterialToKeychainSimulatingSOS:self.keychainZoneID];
697
698 // Spin up CKKS subsystem.
699 [self startCKKSSubsystem];
700
701 // We expect a single record to be uploaded
702 [self expectCKModifyItemRecords: 1 currentKeyPointerRecords: 1 zoneID:self.keychainZoneID checkItem: [self checkClassCBlock:self.keychainZoneID message:@"Object was encrypted under class C key in hierarchy"]];
703 [self addGenericPassword: @"data" account: @"account-delete-me"];
704 OCMVerifyAllWithDelay(self.mockDatabase, 8);
705
706 // After the local reset, we expect: a fetch, then nothing
707 self.silentFetchesAllowed = false;
708 [self expectCKFetch];
709
710 XCTestExpectation* resetExpectation = [self expectationWithDescription: @"local reset callback occurs"];
711 [self.injectedManager rpcResetLocal:nil reply:^(NSError* result) {
712 XCTAssertNil(result, "no error resetting local");
713 [resetExpectation fulfill];
714 }];
715 [self waitForExpectations:@[resetExpectation] timeout:8.0];
716
717 OCMVerifyAllWithDelay(self.mockDatabase, 8);
718
719 [self expectCKModifyItemRecords: 1 currentKeyPointerRecords: 1 zoneID:self.keychainZoneID checkItem: [self checkClassABlock:self.keychainZoneID message:@"Object was encrypted under class A key in hierarchy"]];
720 [self addGenericPassword:@"asdf"
721 account:@"account-class-A"
722 viewHint:nil
723 access:(id)kSecAttrAccessibleWhenUnlocked
724 expecting:errSecSuccess
725 message:@"Adding class A item"];
726 OCMVerifyAllWithDelay(self.mockDatabase, 8);
727 }
728
729 -(void)testResetLocalWhileLoggedOut {
730 // We're "logged in to" cloudkit but not in circle.
731 self.circleStatus = kSOSCCNotInCircle;
732 [self.accountStateTracker notifyCircleStatusChangeAndWaitForSignal];
733 self.silentFetchesAllowed = false;
734
735 // Test starts with local TLK and key hierarchy in our fake cloudkit
736 [self putFakeKeyHierarchyInCloudKit:self.keychainZoneID];
737 [self saveTLKMaterialToKeychainSimulatingSOS:self.keychainZoneID];
738
739 // Spin up CKKS subsystem.
740 [self startCKKSSubsystem];
741
742 XCTAssertEqual(0, [self.keychainView.loggedOut wait:500*NSEC_PER_MSEC], "Should have been told of a 'logout' event on startup");
743
744 NSData* changeTokenData = [[[NSUUID UUID] UUIDString] dataUsingEncoding:NSUTF8StringEncoding];
745 CKServerChangeToken* changeToken = [[CKServerChangeToken alloc] initWithData:changeTokenData];
746 [self.keychainView dispatchSync: ^bool{
747 CKKSZoneStateEntry* ckse = [CKKSZoneStateEntry state:self.keychainView.zoneName];
748 ckse.changeToken = changeToken;
749
750 NSError* error = nil;
751 [ckse saveToDatabase:&error];
752 XCTAssertNil(error, "No error saving new zone state to database");
753 return true;
754 }];
755
756 XCTestExpectation* resetExpectation = [self expectationWithDescription: @"local reset callback occurs"];
757 [self.injectedManager rpcResetLocal:nil reply:^(NSError* result) {
758 XCTAssertNil(result, "no error resetting local");
759 secnotice("ckks", "Received a rpcResetLocal callback");
760 [resetExpectation fulfill];
761 }];
762
763 [self waitForExpectations:@[resetExpectation] timeout:1.0];
764
765 [self.keychainView dispatchSync: ^bool{
766 CKKSZoneStateEntry* ckse = [CKKSZoneStateEntry state:self.keychainView.zoneName];
767 XCTAssertNotEqualObjects(changeToken, ckse.changeToken, "Change token is reset");
768 return true;
769 }];
770
771 // Now log in, and see what happens! It should re-fetch, pick up the old key hierarchy, and use it
772 [self expectCKKSTLKSelfShareUpload:self.keychainZoneID];
773 self.silentFetchesAllowed = true;
774 self.circleStatus = kSOSCCInCircle;
775 [self.accountStateTracker notifyCircleStatusChangeAndWaitForSignal];
776
777 [self expectCKModifyItemRecords: 1 currentKeyPointerRecords: 1 zoneID:self.keychainZoneID checkItem:[self checkClassABlock:self.keychainZoneID message:@"Object was encrypted under class A key in hierarchy"]];
778 [self addGenericPassword:@"asdf"
779 account:@"account-class-A"
780 viewHint:nil
781 access:(id)kSecAttrAccessibleWhenUnlocked
782 expecting:errSecSuccess
783 message:@"Adding class A item"];
784 OCMVerifyAllWithDelay(self.mockDatabase, 8);
785 }
786
787 -(void)testResetLocalMultipleTimes {
788 // Test starts with nothing in database, but one in our fake CloudKit.
789 [self putFakeKeyHierarchyInCloudKit:self.keychainZoneID];
790 [self expectCKKSTLKSelfShareUpload:self.keychainZoneID];
791 [self saveTLKMaterialToKeychainSimulatingSOS:self.keychainZoneID];
792
793 // Spin up CKKS subsystem.
794 [self startCKKSSubsystem];
795
796 // We expect a single record to be uploaded
797 XCTAssertEqual(0, [self.keychainView.keyHierarchyConditions[SecCKKSZoneKeyStateReady] wait:8*NSEC_PER_SEC], "CKKS entered 'ready'");
798 [self expectCKModifyItemRecords:1 currentKeyPointerRecords:1 zoneID:self.keychainZoneID
799 checkItem:[self checkClassCBlock:self.keychainZoneID message:@"Object was encrypted under class C key in hierarchy"]];
800 [self addGenericPassword: @"data" account: @"account-delete-me"];
801 OCMVerifyAllWithDelay(self.mockDatabase, 8);
802 [self waitForCKModifications];
803
804 // We're going to request a bunch of CloudKit resets, but hold them from finishing
805 [self holdCloudKitFetches];
806
807 XCTestExpectation* resetExpectation0 = [self expectationWithDescription: @"reset callback(0) occurs"];
808 XCTestExpectation* resetExpectation1 = [self expectationWithDescription: @"reset callback(1) occurs"];
809 XCTestExpectation* resetExpectation2 = [self expectationWithDescription: @"reset callback(2) occurs"];
810 [self.injectedManager rpcResetLocal:nil reply:^(NSError* result) {
811 XCTAssertNil(result, "should receive no error resetting local");
812 secnotice("ckksreset", "Received a rpcResetLocal(0) callback");
813 [resetExpectation0 fulfill];
814 }];
815 [self.injectedManager rpcResetLocal:nil reply:^(NSError* result) {
816 XCTAssertNil(result, "should receive no error resetting local");
817 secnotice("ckksreset", "Received a rpcResetLocal(1) callback");
818 [resetExpectation1 fulfill];
819 }];
820 [self.injectedManager rpcResetLocal:nil reply:^(NSError* result) {
821 XCTAssertNil(result, "should receive no error resetting local");
822 secnotice("ckksreset", "Received a rpcResetLocal(2) callback");
823 [resetExpectation2 fulfill];
824 }];
825
826 // After the reset(s), we expect no uploads. Let the resets flow!
827 [self releaseCloudKitFetchHold];
828 [self waitForExpectations:@[resetExpectation0, resetExpectation1, resetExpectation2] timeout:20];
829 XCTAssertEqual(0, [self.keychainView.keyHierarchyConditions[SecCKKSZoneKeyStateReady] wait:8*NSEC_PER_SEC], "CKKS entered 'ready'");
830
831 OCMVerifyAllWithDelay(self.mockDatabase, 8);
832
833 [self expectCKModifyItemRecords:1 currentKeyPointerRecords:1 zoneID:self.keychainZoneID
834 checkItem:[self checkClassABlock:self.keychainZoneID message:@"Object was encrypted under class A key in hierarchy"]];
835 [self addGenericPassword:@"asdf"
836 account:@"account-class-A"
837 viewHint:nil
838 access:(id)kSecAttrAccessibleWhenUnlocked
839 expecting:errSecSuccess
840 message:@"Adding class A item"];
841 OCMVerifyAllWithDelay(self.mockDatabase, 8);
842 }
843
844 -(void)testResetCloudKitZone {
845 self.silentZoneDeletesAllowed = true;
846
847 // Test starts with nothing in database, but one in our fake CloudKit.
848 [self putFakeKeyHierarchyInCloudKit:self.keychainZoneID];
849 [self expectCKKSTLKSelfShareUpload:self.keychainZoneID];
850 [self saveTLKMaterialToKeychainSimulatingSOS:self.keychainZoneID];
851
852 // Spin up CKKS subsystem.
853 [self startCKKSSubsystem];
854
855 // We expect a single record to be uploaded
856 [self expectCKModifyItemRecords: 1 currentKeyPointerRecords: 1 zoneID:self.keychainZoneID checkItem: [self checkClassCBlock:self.keychainZoneID message:@"Object was encrypted under class C key in hierarchy"]];
857 [self addGenericPassword: @"data" account: @"account-delete-me"];
858 OCMVerifyAllWithDelay(self.mockDatabase, 8);
859 [self waitForCKModifications];
860
861 // After the reset, we expect a key hierarchy upload, and then the class C item upload
862 [self expectCKModifyKeyRecords: 3 currentKeyPointerRecords: 3 tlkShareRecords: 1 zoneID:self.keychainZoneID];
863 [self expectCKModifyItemRecords: 1 currentKeyPointerRecords: 1 zoneID:self.keychainZoneID checkItem: [self checkClassCBlock:self.keychainZoneID message:@"Object was encrypted under class C key in hierarchy"]];
864
865 XCTestExpectation* resetExpectation = [self expectationWithDescription: @"reset callback occurs"];
866 [self.injectedManager rpcResetCloudKit:nil reply:^(NSError* result) {
867 XCTAssertNil(result, "no error resetting cloudkit");
868 secnotice("ckks", "Received a resetCloudKit callback");
869 [resetExpectation fulfill];
870 }];
871 [self waitForExpectations:@[resetExpectation] timeout:8.0];
872
873 OCMVerifyAllWithDelay(self.mockDatabase, 8);
874
875 [self expectCKModifyItemRecords: 1 currentKeyPointerRecords: 1 zoneID:self.keychainZoneID checkItem: [self checkClassABlock:self.keychainZoneID message:@"Object was encrypted under class A key in hierarchy"]];
876 [self addGenericPassword:@"asdf"
877 account:@"account-class-A"
878 viewHint:nil
879 access:(id)kSecAttrAccessibleWhenUnlocked
880 expecting:errSecSuccess
881 message:@"Adding class A item"];
882 OCMVerifyAllWithDelay(self.mockDatabase, 8);
883 }
884
885 - (void)testResetCloudKitZoneDuringWaitForTLK {
886 self.silentZoneDeletesAllowed = true;
887
888 // Test starts with nothing in database, but one in our fake CloudKit.
889 // No TLK, though!
890 [self putFakeKeyHierarchyInCloudKit:self.keychainZoneID];
891 [self putFakeDeviceStatusInCloudKit:self.keychainZoneID];
892
893 // Spin up CKKS subsystem.
894 [self startCKKSSubsystem];
895
896 // No records should be uploaded
897 [self addGenericPassword: @"data" account: @"account-delete-me"];
898
899 XCTAssertEqual(0, [self.keychainView.keyHierarchyConditions[SecCKKSZoneKeyStateWaitForTLK] wait:8*NSEC_PER_SEC], "CKKS should have entered waitfortlk");
900
901 // Restart CKKS to really get in the spirit of waitfortlk (and get a pending processOutgoingQueue operation going)
902 self.keychainView = [[CKKSViewManager manager] restartZone: self.keychainZoneID.zoneName];
903 XCTAssertEqual(0, [self.keychainView.keyHierarchyConditions[SecCKKSZoneKeyStateWaitForTLK] wait:8*NSEC_PER_SEC], "CKKS entered waitfortlk");
904
905 CKKSOutgoingQueueOperation* outgoingOp = [self.keychainView processOutgoingQueue:nil];
906 XCTAssertTrue([outgoingOp isPending], "outgoing queue processing should be on hold");
907
908 // Now, reset everything. The outgoingOp should get cancelled.
909 // We expect a key hierarchy upload, and then the class C item upload
910 [self expectCKModifyKeyRecords:3 currentKeyPointerRecords:3 tlkShareRecords:1 zoneID:self.keychainZoneID];
911 [self expectCKModifyItemRecords: 1 currentKeyPointerRecords: 1 zoneID:self.keychainZoneID
912 checkItem: [self checkClassCBlock:self.keychainZoneID message:@"Object was encrypted under class C key in hierarchy"]];
913
914 XCTestExpectation* resetExpectation = [self expectationWithDescription: @"reset callback occurs"];
915 [self.injectedManager rpcResetCloudKit:nil reply:^(NSError* result) {
916 XCTAssertNil(result, "no error resetting cloudkit");
917 [resetExpectation fulfill];
918 }];
919 [self waitForExpectations:@[resetExpectation] timeout:8.0];
920
921 XCTAssertTrue([outgoingOp isCancelled], "old stuck ProcessOutgoingQueue should be cancelled");
922 OCMVerifyAllWithDelay(self.mockDatabase, 8);
923
924 // And adding another item works too
925 [self expectCKModifyItemRecords: 1 currentKeyPointerRecords: 1 zoneID:self.keychainZoneID checkItem: [self checkClassABlock:self.keychainZoneID message:@"Object was encrypted under class A key in hierarchy"]];
926 [self addGenericPassword:@"asdf"
927 account:@"account-class-A"
928 viewHint:nil
929 access:(id)kSecAttrAccessibleWhenUnlocked
930 expecting:errSecSuccess
931 message:@"Adding class A item"];
932 OCMVerifyAllWithDelay(self.mockDatabase, 8);
933 }
934
935 /*
936 * This test doesn't work, since the resetLocal fails. CKKS gets back into waitfortlk
937 * but that isn't considered a successful resetLocal.
938 *
939 - (void)testResetLocalDuringWaitForTLK {
940 // Test starts with nothing in database, but one in our fake CloudKit.
941 // No TLK, though!
942 [self putFakeKeyHierarchyInCloudKit:self.keychainZoneID];
943
944 // Spin up CKKS subsystem.
945 [self startCKKSSubsystem];
946
947 // No records should be uploaded
948 [self addGenericPassword: @"data" account: @"account-delete-me"];
949
950 XCTAssertEqual(0, [self.keychainView.keyHierarchyConditions[SecCKKSZoneKeyStateWaitForTLK] wait:8*NSEC_PER_SEC], "CKKS should have entered waitfortlk");
951
952 // Restart CKKS to really get in the spirit of waitfortlk (and get a pending processOutgoingQueue operation going)
953 self.keychainView = [[CKKSViewManager manager] restartZone: self.keychainZoneID.zoneName];
954 XCTAssertEqual(0, [self.keychainView.keyHierarchyConditions[SecCKKSZoneKeyStateWaitForTLK] wait:8*NSEC_PER_SEC], "CKKS entered waitfortlk");
955
956 CKKSOutgoingQueueOperation* outgoingOp = [self.keychainView processOutgoingQueue:nil];
957 XCTAssertTrue([outgoingOp isPending], "outgoing queue processing should be on hold");
958
959 // Now, reset everything. The outgoingOp should get cancelled.
960 // We expect a key hierarchy upload, and then the class C item upload
961 [self expectCKModifyKeyRecords: 3 currentKeyPointerRecords: 3 tlkShareRecords:3 zoneID:self.keychainZoneID];
962 [self expectCKModifyItemRecords: 1 currentKeyPointerRecords: 1 zoneID:self.keychainZoneID
963 checkItem: [self checkClassCBlock:self.keychainZoneID message:@"Object was encrypted under class C key in hierarchy"]];
964
965 XCTestExpectation* resetExpectation = [self expectationWithDescription: @"reset callback occurs"];
966 [self.injectedManager rpcResetLocal:nil reply:^(NSError* result) {
967 XCTAssertNil(result, "no error resetting local");
968 [resetExpectation fulfill];
969 }];
970 [self waitForExpectations:@[resetExpectation] timeout:8.0];
971
972 XCTAssertTrue([outgoingOp isCancelled], "old stuck ProcessOutgoingQueue should be cancelled");
973 OCMVerifyAllWithDelay(self.mockDatabase, 8);
974
975 // And adding another item works too
976 [self expectCKModifyItemRecords: 1 currentKeyPointerRecords: 1 zoneID:self.keychainZoneID checkItem: [self checkClassABlock:self.keychainZoneID message:@"Object was encrypted under class A key in hierarchy"]];
977 [self addGenericPassword:@"asdf"
978 account:@"account-class-A"
979 viewHint:nil
980 access:(id)kSecAttrAccessibleWhenUnlocked
981 expecting:errSecSuccess
982 message:@"Adding class A item"];
983 OCMVerifyAllWithDelay(self.mockDatabase, 8);
984 }*/
985
986 -(void)testResetCloudKitZoneWhileLoggedOut {
987 self.silentZoneDeletesAllowed = true;
988
989 // We're "logged in to" cloudkit but not in circle.
990 self.circleStatus = kSOSCCNotInCircle;
991 [self.accountStateTracker notifyCircleStatusChangeAndWaitForSignal];
992 self.silentFetchesAllowed = false;
993
994 // Test starts with nothing in database, but one in our fake CloudKit.
995 [self putFakeKeyHierarchyInCloudKit:self.keychainZoneID];
996
997 // Spin up CKKS subsystem.
998 [self startCKKSSubsystem];
999
1000 CKRecord* ckr = [self createFakeRecord: self.keychainZoneID recordName:@"7B598D31-F9C5-481E-98AC-5A507ACB2D85"];
1001 [self.keychainZone addToZone: ckr];
1002
1003 XCTAssertNotNil(self.keychainZone.currentDatabase, "Zone exists");
1004 XCTAssertNotNil(self.keychainZone.currentDatabase[ckr.recordID], "An item exists in the fake zone");
1005
1006 XCTestExpectation* resetExpectation = [self expectationWithDescription: @"reset callback occurs"];
1007 [self.injectedManager rpcResetCloudKit:nil reply:^(NSError* result) {
1008 XCTAssertNil(result, "no error resetting cloudkit");
1009 secnotice("ckks", "Received a resetCloudKit callback");
1010 [resetExpectation fulfill];
1011 }];
1012 [self waitForExpectations:@[resetExpectation] timeout:1.0];
1013
1014 XCTAssertNil(self.keychainZone.currentDatabase, "No zone anymore!");
1015 OCMVerifyAllWithDelay(self.mockDatabase, 8);
1016
1017 // Now log in, and see what happens! It should create the zone again and upload a whole new key hierarchy
1018 self.silentFetchesAllowed = true;
1019 self.circleStatus = kSOSCCInCircle;
1020 [self.accountStateTracker notifyCircleStatusChangeAndWaitForSignal];
1021
1022 [self expectCKModifyKeyRecords: 3 currentKeyPointerRecords: 3 tlkShareRecords: 1 zoneID:self.keychainZoneID];
1023 OCMVerifyAllWithDelay(self.mockDatabase, 8);
1024
1025 [self expectCKModifyItemRecords: 1 currentKeyPointerRecords: 1 zoneID:self.keychainZoneID checkItem:[self checkClassABlock:self.keychainZoneID message:@"Object was encrypted under class A key in hierarchy"]];
1026 [self addGenericPassword:@"asdf"
1027 account:@"account-class-A"
1028 viewHint:nil
1029 access:(id)kSecAttrAccessibleWhenUnlocked
1030 expecting:errSecSuccess
1031 message:@"Adding class A item"];
1032 OCMVerifyAllWithDelay(self.mockDatabase, 8);
1033 }
1034
1035 - (void)testResetCloudKitZoneMultipleTimes {
1036 self.silentZoneDeletesAllowed = true;
1037
1038 // Test starts with nothing in database, but one in our fake CloudKit.
1039 [self putFakeKeyHierarchyInCloudKit:self.keychainZoneID];
1040 [self expectCKKSTLKSelfShareUpload:self.keychainZoneID];
1041 [self saveTLKMaterialToKeychainSimulatingSOS:self.keychainZoneID];
1042
1043 // Spin up CKKS subsystem.
1044 [self startCKKSSubsystem];
1045
1046 // We expect a single record to be uploaded
1047 XCTAssertEqual(0, [self.keychainView.keyHierarchyConditions[SecCKKSZoneKeyStateReady] wait:8*NSEC_PER_SEC], "CKKS entered 'ready'");
1048 [self expectCKModifyItemRecords:1 currentKeyPointerRecords:1 zoneID:self.keychainZoneID
1049 checkItem:[self checkClassCBlock:self.keychainZoneID message:@"Object was encrypted under class C key in hierarchy"]];
1050 [self addGenericPassword: @"data" account: @"account-delete-me"];
1051 OCMVerifyAllWithDelay(self.mockDatabase, 8);
1052 [self waitForCKModifications];
1053
1054 // We're going to request a bunch of CloudKit resets, but hold them from finishing
1055 [self holdCloudKitFetches];
1056
1057 XCTestExpectation* resetExpectation0 = [self expectationWithDescription: @"reset callback(0) occurs"];
1058 XCTestExpectation* resetExpectation1 = [self expectationWithDescription: @"reset callback(1) occurs"];
1059 XCTestExpectation* resetExpectation2 = [self expectationWithDescription: @"reset callback(2) occurs"];
1060 [self.injectedManager rpcResetCloudKit:nil reply:^(NSError* result) {
1061 XCTAssertNil(result, "should receive no error resetting cloudkit");
1062 secnotice("ckksreset", "Received a resetCloudKit(0) callback");
1063 [resetExpectation0 fulfill];
1064 }];
1065 [self.injectedManager rpcResetCloudKit:nil reply:^(NSError* result) {
1066 XCTAssertNil(result, "should receive no error resetting cloudkit");
1067 secnotice("ckksreset", "Received a resetCloudKit(1) callback");
1068 [resetExpectation1 fulfill];
1069 }];
1070 [self.injectedManager rpcResetCloudKit:nil reply:^(NSError* result) {
1071 XCTAssertNil(result, "should receive no error resetting cloudkit");
1072 secnotice("ckksreset", "Received a resetCloudKit(2) callback");
1073 [resetExpectation2 fulfill];
1074 }];
1075
1076 // After the reset(s), we expect a key hierarchy upload, and then the class C item upload
1077 [self expectCKModifyKeyRecords:3 currentKeyPointerRecords:3 tlkShareRecords:1 zoneID:self.keychainZoneID];
1078 [self expectCKModifyItemRecords:1 currentKeyPointerRecords:1 zoneID:self.keychainZoneID
1079 checkItem:[self checkClassCBlock:self.keychainZoneID message:@"Object was encrypted under class C key in hierarchy"]];
1080
1081 // And let the resets flow
1082 [self releaseCloudKitFetchHold];
1083 [self waitForExpectations:@[resetExpectation0, resetExpectation1, resetExpectation2] timeout:20];
1084 XCTAssertEqual(0, [self.keychainView.keyHierarchyConditions[SecCKKSZoneKeyStateReady] wait:8*NSEC_PER_SEC], "CKKS entered 'ready'");
1085
1086 OCMVerifyAllWithDelay(self.mockDatabase, 8);
1087
1088 [self expectCKModifyItemRecords:1 currentKeyPointerRecords:1 zoneID:self.keychainZoneID
1089 checkItem:[self checkClassABlock:self.keychainZoneID message:@"Object was encrypted under class A key in hierarchy"]];
1090 [self addGenericPassword:@"asdf"
1091 account:@"account-class-A"
1092 viewHint:nil
1093 access:(id)kSecAttrAccessibleWhenUnlocked
1094 expecting:errSecSuccess
1095 message:@"Adding class A item"];
1096 OCMVerifyAllWithDelay(self.mockDatabase, 8);
1097 }
1098
1099 - (void)testRPCFetchAndProcessWhileCloudKitNotResponding {
1100 [self createAndSaveFakeKeyHierarchy: self.keychainZoneID]; // Make life easy for this test.
1101 [self startCKKSSubsystem];
1102
1103 XCTAssertEqual(0, [self.keychainView.keyHierarchyConditions[SecCKKSZoneKeyStateReady] wait:8*NSEC_PER_SEC], "CKKS entered 'ready'");
1104 [self holdCloudKitFetches];
1105
1106 XCTestExpectation* callbackOccurs = [self expectationWithDescription:@"callback-occurs"];
1107 [self.ckksControl rpcFetchAndProcessChanges:nil reply:^(NSError * _Nullable error) {
1108 // done! we should have an underlying error of "fetch isn't working"
1109 XCTAssertNotNil(error, "Should have received an error attempting to fetch and process");
1110 NSError* underlying = error.userInfo[NSUnderlyingErrorKey];
1111 XCTAssertNotNil(underlying, "Should have received an underlying error");
1112 XCTAssertEqualObjects(underlying.domain, CKKSResultDescriptionErrorDomain, "Underlying error should be CKKSResultDescriptionErrorDomain");
1113 XCTAssertEqual(underlying.code, CKKSResultDescriptionPendingSuccessfulFetch, "Underlying error should be 'pending fetch'");
1114 [callbackOccurs fulfill];
1115 }];
1116
1117 [self waitForExpectations:@[callbackOccurs] timeout:20.0];
1118 [self releaseCloudKitFetchHold];
1119 OCMVerifyAllWithDelay(self.mockDatabase, 8);
1120 }
1121
1122 - (void)testRPCFetchAndProcessWhileCloudKitErroring {
1123 [self createAndSaveFakeKeyHierarchy: self.keychainZoneID]; // Make life easy for this test.
1124 [self startCKKSSubsystem];
1125
1126 XCTAssertEqual(0, [self.keychainView.keyHierarchyConditions[SecCKKSZoneKeyStateReady] wait:8*NSEC_PER_SEC], "CKKS entered 'ready'");
1127
1128 [self.keychainZone failNextFetchWith:[[CKPrettyError alloc] initWithDomain:CKErrorDomain
1129 code:CKErrorRequestRateLimited
1130 userInfo:@{CKErrorRetryAfterKey : [NSNumber numberWithInt:30]}]];
1131
1132 XCTestExpectation* callbackOccurs = [self expectationWithDescription:@"callback-occurs"];
1133 [self.ckksControl rpcFetchAndProcessChanges:nil reply:^(NSError * _Nullable error) {
1134 // done! we should have an underlying error of "fetch isn't working"
1135 XCTAssertNotNil(error, "Should have received an error attempting to fetch and process");
1136 NSError* underlying = error.userInfo[NSUnderlyingErrorKey];
1137 XCTAssertNotNil(underlying, "Should have received an underlying error");
1138 XCTAssertEqualObjects(underlying.domain, CKKSResultDescriptionErrorDomain, "Underlying error should be CKKSResultDescriptionErrorDomain");
1139 XCTAssertEqual(underlying.code, CKKSResultDescriptionPendingSuccessfulFetch, "Underlying error should be 'pending fetch'");
1140
1141 NSError* underunderlying = underlying.userInfo[NSUnderlyingErrorKey];
1142 XCTAssertNotNil(underunderlying, "Should have received another layer of underlying error");
1143 XCTAssertEqualObjects(underunderlying.domain, CKErrorDomain, "Underlying error should be CKErrorDomain");
1144 XCTAssertEqual(underunderlying.code, CKErrorRequestRateLimited, "Underlying error should be 'rate limited'");
1145
1146 [callbackOccurs fulfill];
1147 }];
1148
1149 [self waitForExpectations:@[callbackOccurs] timeout:20.0];
1150 OCMVerifyAllWithDelay(self.mockDatabase, 8);
1151 }
1152
1153 - (void)testRPCFetchAndProcessWhileInWaitForTLK {
1154 [self putFakeKeyHierarchyInCloudKit:self.keychainZoneID];
1155 [self putFakeDeviceStatusInCloudKit:self.keychainZoneID];
1156 [self startCKKSSubsystem];
1157
1158 XCTAssertEqual(0, [self.keychainView.keyHierarchyConditions[SecCKKSZoneKeyStateWaitForTLK] wait:8*NSEC_PER_SEC], "CKKS entered waitfortlk");
1159
1160 XCTestExpectation* callbackOccurs = [self expectationWithDescription:@"callback-occurs"];
1161 [self.ckksControl rpcFetchAndProcessChanges:nil reply:^(NSError * _Nullable error) {
1162 // done! we should have an underlying error of "fetch isn't working"
1163 XCTAssertNotNil(error, "Should have received an error attempting to fetch and process");
1164 NSError* underlying = error.userInfo[NSUnderlyingErrorKey];
1165 XCTAssertNotNil(underlying, "Should have received an underlying error");
1166 XCTAssertEqualObjects(underlying.domain, CKKSResultDescriptionErrorDomain, "Underlying error should be CKKSResultDescriptionErrorDomain");
1167 XCTAssertEqual(underlying.code, CKKSResultDescriptionPendingKeyReady, "Underlying error should be 'pending key ready'");
1168 [callbackOccurs fulfill];
1169 }];
1170
1171 [self waitForExpectations:@[callbackOccurs] timeout:20.0];
1172 OCMVerifyAllWithDelay(self.mockDatabase, 8);
1173 }
1174
1175 - (void)testRPCTLKMissingWhenMissing {
1176 // Bring CKKS up in waitfortlk
1177 [self putFakeKeyHierarchyInCloudKit:self.keychainZoneID];
1178 [self putFakeDeviceStatusInCloudKit:self.keychainZoneID];
1179 [self startCKKSSubsystem];
1180
1181 XCTAssertEqual(0, [self.keychainView.keyHierarchyConditions[SecCKKSZoneKeyStateWaitForTLK] wait:8*NSEC_PER_SEC], "CKKS entered waitfortlk");
1182
1183 XCTestExpectation* callbackOccurs = [self expectationWithDescription:@"callback-occurs"];
1184
1185 [self.ckksControl rpcTLKMissing:@"keychain" reply:^(bool missing) {
1186 XCTAssertTrue(missing, "TLKs should be missing");
1187 [callbackOccurs fulfill];
1188 }];
1189
1190 [self waitForExpectations:@[callbackOccurs] timeout:5.0];
1191
1192 OCMVerifyAllWithDelay(self.mockDatabase, 8);
1193 }
1194
1195 - (void)testRPCTLKMissingWhenFound {
1196 // Bring CKKS up in 'ready'
1197 [self putFakeKeyHierarchyInCloudKit:self.keychainZoneID];
1198 [self saveTLKMaterialToKeychain:self.keychainZoneID];
1199 [self expectCKKSTLKSelfShareUpload:self.keychainZoneID];
1200 [self startCKKSSubsystem];
1201
1202 XCTAssertEqual(0, [self.keychainView.keyHierarchyConditions[SecCKKSZoneKeyStateReady] wait:8*NSEC_PER_SEC], "CKKS entered 'ready''");
1203
1204 XCTestExpectation* callbackOccurs = [self expectationWithDescription:@"callback-occurs"];
1205
1206 [self.ckksControl rpcTLKMissing:@"keychain" reply:^(bool missing) {
1207 XCTAssertFalse(missing, "TLKs should not be missing");
1208 [callbackOccurs fulfill];
1209 }];
1210
1211 [self waitForExpectations:@[callbackOccurs] timeout:5.0];
1212
1213 OCMVerifyAllWithDelay(self.mockDatabase, 8);
1214 }
1215
1216 - (void)testRPCKnownBadStateWhenTLKsMissing {
1217 // Bring CKKS up in waitfortlk
1218 [self putFakeKeyHierarchyInCloudKit:self.keychainZoneID];
1219 [self putFakeDeviceStatusInCloudKit:self.keychainZoneID];
1220 [self startCKKSSubsystem];
1221
1222 XCTAssertEqual(0, [self.keychainView.keyHierarchyConditions[SecCKKSZoneKeyStateWaitForTLK] wait:8*NSEC_PER_SEC], "CKKS entered waitfortlk");
1223
1224 XCTestExpectation* callbackOccurs = [self expectationWithDescription:@"callback-occurs"];
1225
1226 [self.ckksControl rpcKnownBadState:@"keychain" reply:^(CKKSKnownBadState result) {
1227 XCTAssertEqual(result, CKKSKnownStateTLKsMissing, "TLKs should be missing");
1228 [callbackOccurs fulfill];
1229 }];
1230
1231 [self waitForExpectations:@[callbackOccurs] timeout:5.0];
1232
1233 OCMVerifyAllWithDelay(self.mockDatabase, 8);
1234 }
1235
1236 - (void)testRPCKnownBadStateWhenInWaitForUnlock {
1237 // Bring CKKS up in 'waitfortunlok'
1238 self.aksLockState = true;
1239 [self.lockStateTracker recheck];
1240 [self startCKKSSubsystem];
1241
1242 // Wait for the key hierarchy state machine to get stuck waiting for the unlock dependency. No uploads should occur.
1243 XCTAssertEqual(0, [self.keychainView.keyHierarchyConditions[SecCKKSZoneKeyStateWaitForUnlock] wait:8*NSEC_PER_SEC], @"Key state should get stuck in waitforunlock");
1244
1245 XCTestExpectation* callbackOccurs = [self expectationWithDescription:@"callback-occurs"];
1246
1247 [self.ckksControl rpcKnownBadState:@"keychain" reply:^(CKKSKnownBadState result) {
1248 XCTAssertEqual(result, CKKSKnownStateWaitForUnlock, "known state should be wait for unlock");
1249 [callbackOccurs fulfill];
1250 }];
1251
1252 [self waitForExpectations:@[callbackOccurs] timeout:5.0];
1253
1254 OCMVerifyAllWithDelay(self.mockDatabase, 8);
1255 }
1256
1257
1258 - (void)testRPCKnownBadStateWhenInGoodState {
1259 // Bring CKKS up in 'ready'
1260 [self putFakeKeyHierarchyInCloudKit:self.keychainZoneID];
1261 [self saveTLKMaterialToKeychain:self.keychainZoneID];
1262 [self expectCKKSTLKSelfShareUpload:self.keychainZoneID];
1263 [self startCKKSSubsystem];
1264
1265 XCTAssertEqual(0, [self.keychainView.keyHierarchyConditions[SecCKKSZoneKeyStateReady] wait:8*NSEC_PER_SEC], "CKKS entered 'ready''");
1266
1267 XCTestExpectation* callbackOccurs = [self expectationWithDescription:@"callback-occurs"];
1268
1269 [self.ckksControl rpcKnownBadState:@"keychain" reply:^(CKKSKnownBadState result) {
1270 XCTAssertEqual(result, CKKSKnownStatePossiblyGood, "known state should not be possibly-good");
1271 [callbackOccurs fulfill];
1272 }];
1273
1274 [self waitForExpectations:@[callbackOccurs] timeout:5.0];
1275
1276 OCMVerifyAllWithDelay(self.mockDatabase, 8);
1277 }
1278
1279 - (void)testRpcStatus {
1280 [self createAndSaveFakeKeyHierarchy: self.keychainZoneID]; // Make life easy for this test.
1281
1282 [self startCKKSSubsystem];
1283
1284 // Let things shake themselves out.
1285 OCMVerifyAllWithDelay(self.mockDatabase, 8);
1286 XCTAssertEqual(0, [self.keychainView.keyHierarchyConditions[SecCKKSZoneKeyStateReady] wait:8*NSEC_PER_SEC], "Key state should return to 'ready'");
1287 [self waitForCKModifications];
1288
1289 XCTestExpectation* callbackOccurs = [self expectationWithDescription:@"callback-occurs"];
1290 [self.ckksControl rpcStatus:@"keychain" reply:^(NSArray<NSDictionary*>* result, NSError* error) {
1291 XCTAssertNil(error, "should be no error fetching status for keychain");
1292
1293 // Ugly "global" hack
1294 XCTAssertEqual(result.count, 2u, "Should have received two result dictionaries back");
1295 NSDictionary* keychainStatus = result[1];
1296
1297 XCTAssertNotNil(keychainStatus, "Should have received at least one zone status back");
1298 XCTAssertEqualObjects(keychainStatus[@"view"], @"keychain", "Should have received status for the keychain view");
1299 XCTAssertEqualObjects(keychainStatus[@"keystate"], SecCKKSZoneKeyStateReady, "Should be in 'ready' status");
1300 [callbackOccurs fulfill];
1301 }];
1302
1303 [self waitForExpectations:@[callbackOccurs] timeout:5.0];
1304 }
1305
1306 - (void)testRpcStatusWaitsForAccountDetermination {
1307 [self createAndSaveFakeKeyHierarchy: self.keychainZoneID]; // Make life easy for this test.
1308
1309 // Set up the account state callbacks to happen in one second
1310 dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (1 * NSEC_PER_SEC)), dispatch_get_global_queue(QOS_CLASS_DEFAULT, 0), ^{
1311 // Let CKKS come up (simulating daemon starting due to RPC)
1312 [self startCKKSSubsystem];
1313 });
1314
1315 // Before CKKS figures out we're in an account, fire off the status RPC.
1316 XCTestExpectation* callbackOccurs = [self expectationWithDescription:@"callback-occurs"];
1317 [self.ckksControl rpcStatus:@"keychain" reply:^(NSArray<NSDictionary*>* result, NSError* error) {
1318 XCTAssertNil(error, "should be no error fetching status for keychain");
1319
1320 // Ugly "global" hack
1321 XCTAssertEqual(result.count, 2u, "Should have received two result dictionaries back");
1322 NSDictionary* keychainStatus = result[1];
1323
1324 XCTAssertNotNil(keychainStatus, "Should have received at least one zone status back");
1325 XCTAssertEqualObjects(keychainStatus[@"view"], @"keychain", "Should have received status for the keychain view");
1326 XCTAssertEqualObjects(keychainStatus[@"keystate"], SecCKKSZoneKeyStateReady, "Should be in 'ready' status");
1327 [callbackOccurs fulfill];
1328 }];
1329
1330 [self waitForExpectations:@[callbackOccurs] timeout:8.0];
1331 }
1332
1333 - (void)testRpcStatusIsFastDuringError {
1334 [self createAndSaveFakeKeyHierarchy: self.keychainZoneID]; // Make life easy for this test.
1335
1336 self.keychainFetchError = [NSError errorWithDomain:NSOSStatusErrorDomain code:errSecInternalError description:@"injected keychain failure"];
1337
1338 // Let CKKS come up; it should enter 'error'
1339 [self startCKKSSubsystem];
1340 XCTAssertEqual(0, [self.keychainView.keyHierarchyConditions[SecCKKSZoneKeyStateError] wait:8*NSEC_PER_SEC], "CKKS entered 'error'");
1341
1342 // Fire off the status RPC; it should return immediately
1343 XCTestExpectation* callbackOccurs = [self expectationWithDescription:@"callback-occurs"];
1344 [self.ckksControl rpcStatus:@"keychain" reply:^(NSArray<NSDictionary*>* result, NSError* error) {
1345 XCTAssertNil(error, "should be no error fetching status for keychain");
1346
1347 // Ugly "global" hack
1348 XCTAssertEqual(result.count, 2u, "Should have received two result dictionaries back");
1349 NSDictionary* keychainStatus = result[1];
1350
1351 XCTAssertNotNil(keychainStatus, "Should have received at least one zone status back");
1352 XCTAssertEqualObjects(keychainStatus[@"view"], @"keychain", "Should have received status for the keychain view");
1353 XCTAssertEqualObjects(keychainStatus[@"keystate"], SecCKKSZoneKeyStateError, "Should be in 'ready' status");
1354 [callbackOccurs fulfill];
1355 }];
1356
1357 [self waitForExpectations:@[callbackOccurs] timeout:1.0];
1358 }
1359
1360 @end
1361
1362 #endif // OCTAGON