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