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