]> git.saurik.com Git - apple/security.git/blob - keychain/ckks/tests/CKKSTests+API.m
Security-58286.31.2.tar.gz
[apple/security.git] / keychain / ckks / tests / CKKSTests+API.m
1 /*
2 * Copyright (c) 2016 Apple Inc. All Rights Reserved.
3 *
4 * @APPLE_LICENSE_HEADER_START@
5 *
6 * This file contains Original Code and/or Modifications of Original Code
7 * as defined in and that are subject to the Apple Public Source License
8 * Version 2.0 (the 'License'). You may not use this file except in
9 * compliance with the License. Please obtain a copy of the License at
10 * http://www.opensource.apple.com/apsl/ and read it before using this
11 * file.
12 *
13 * The Original Code and all software distributed under the License are
14 * distributed on an 'AS IS' basis, WITHOUT WARRANTY OF ANY KIND, EITHER
15 * EXPRESS OR IMPLIED, AND APPLE HEREBY DISCLAIMS ALL SUCH WARRANTIES,
16 * INCLUDING WITHOUT LIMITATION, ANY WARRANTIES OF MERCHANTABILITY,
17 * FITNESS FOR A PARTICULAR PURPOSE, QUIET ENJOYMENT OR NON-INFRINGEMENT.
18 * Please see the License for the specific language governing rights and
19 * limitations under the License.
20 *
21 * @APPLE_LICENSE_HEADER_END@
22 */
23
24 #if OCTAGON
25
26 #import <CloudKit/CloudKit.h>
27 #import <XCTest/XCTest.h>
28 #import <OCMock/OCMock.h>
29
30 #include <Security/SecItemPriv.h>
31 #include <Security/SecEntitlements.h>
32 #include <ipc/server_security_helpers.h>
33 #import <Foundation/NSXPCConnection_Private.h>
34
35 #import "keychain/ckks/tests/CloudKitMockXCTest.h"
36 #import "keychain/ckks/tests/CloudKitKeychainSyncingMockXCTest.h"
37 #import "keychain/ckks/CKKS.h"
38 #import "keychain/ckks/CKKSItem.h"
39 #import "keychain/ckks/CKKSItemEncrypter.h"
40 #import "keychain/ckks/CKKSKey.h"
41 #import "keychain/ckks/CKKSViewManager.h"
42 #import "keychain/ckks/CKKSZoneStateEntry.h"
43
44 #import "keychain/ckks/CKKSControl.h"
45
46 #import "keychain/ckks/tests/MockCloudKit.h"
47 #import "keychain/ckks/tests/CKKSTests.h"
48 #import "keychain/ckks/tests/CKKSTests+API.h"
49
50 @implementation CloudKitKeychainSyncingTests (APITests)
51
52 - (void)testSecuritydClientBringup {
53 CFErrorRef cferror = nil;
54 xpc_endpoint_t endpoint = SecCreateSecuritydXPCServerEndpoint(&cferror);
55 XCTAssertNil((__bridge id)cferror, "No error creating securityd endpoint");
56 XCTAssertNotNil(endpoint, "Received securityd endpoint");
57
58 NSXPCInterface *interface = [NSXPCInterface interfaceWithProtocol:@protocol(SecuritydXPCProtocol)];
59 [SecuritydXPCClient configureSecuritydXPCProtocol: interface];
60 XCTAssertNotNil(interface, "Received a configured CKKS interface");
61
62 NSXPCListenerEndpoint *listenerEndpoint = [[NSXPCListenerEndpoint alloc] init];
63 [listenerEndpoint _setEndpoint:endpoint];
64
65 NSXPCConnection* connection = [[NSXPCConnection alloc] initWithListenerEndpoint:listenerEndpoint];
66 XCTAssertNotNil(connection , "Received an active connection");
67
68 connection.remoteObjectInterface = interface;
69 }
70
71 -(NSMutableDictionary*)pcsAddItemQuery:(NSString*)account
72 data:(NSData*)data
73 serviceIdentifier:(NSNumber*)serviceIdentifier
74 publicKey:(NSData*)publicKey
75 publicIdentity:(NSData*)publicIdentity
76 {
77 return [@{
78 (id)kSecClass : (id)kSecClassGenericPassword,
79 (id)kSecReturnPersistentRef: @YES,
80 (id)kSecReturnAttributes: @YES,
81 (id)kSecAttrAccessGroup : @"com.apple.security.ckks",
82 (id)kSecAttrAccessible: (id)kSecAttrAccessibleAfterFirstUnlock,
83 (id)kSecAttrAccount : account,
84 (id)kSecAttrSynchronizable : (id)kCFBooleanTrue,
85 (id)kSecValueData : data,
86 (id)kSecAttrDeriveSyncIDFromItemAttributes : (id)kCFBooleanTrue,
87 (id)kSecAttrPCSPlaintextServiceIdentifier : serviceIdentifier,
88 (id)kSecAttrPCSPlaintextPublicKey : publicKey,
89 (id)kSecAttrPCSPlaintextPublicIdentity : publicIdentity,
90 } mutableCopy];
91 }
92
93 -(NSDictionary*)pcsAddItem:(NSString*)account
94 data:(NSData*)data
95 serviceIdentifier:(NSNumber*)serviceIdentifier
96 publicKey:(NSData*)publicKey
97 publicIdentity:(NSData*)publicIdentity
98 expectingSync:(bool)expectingSync
99 {
100 NSMutableDictionary* query = [self pcsAddItemQuery:account
101 data:data
102 serviceIdentifier:(NSNumber*)serviceIdentifier
103 publicKey:(NSData*)publicKey
104 publicIdentity:(NSData*)publicIdentity];
105 CFTypeRef result = NULL;
106 XCTestExpectation* syncExpectation = [self expectationWithDescription: @"callback occurs"];
107
108 XCTAssertEqual(errSecSuccess, _SecItemAddAndNotifyOnSync((__bridge CFDictionaryRef) query, &result, ^(bool didSync, CFErrorRef error) {
109 if(expectingSync) {
110 XCTAssertTrue(didSync, "Item synced");
111 XCTAssertNil((__bridge NSError*)error, "No error syncing item");
112 } else {
113 XCTAssertFalse(didSync, "Item did not sync");
114 XCTAssertNotNil((__bridge NSError*)error, "Error syncing item");
115 }
116
117 [syncExpectation fulfill];
118 }), @"_SecItemAddAndNotifyOnSync succeeded");
119
120 // Verify that the item was written to CloudKit
121 OCMVerifyAllWithDelay(self.mockDatabase, 8);
122
123 // In real code, you'd need to wait for the _SecItemAddAndNotifyOnSync callback to succeed before proceeding
124 [self waitForExpectations:@[syncExpectation] timeout:8.0];
125
126 return (NSDictionary*) CFBridgingRelease(result);
127 }
128
129
130 - (void)testAddAndNotifyOnSync {
131 [self createAndSaveFakeKeyHierarchy: self.keychainZoneID]; // Make life easy for this test.
132
133 [self expectCKModifyItemRecords: 1 currentKeyPointerRecords: 1 zoneID:self.keychainZoneID];
134 [self startCKKSSubsystem];
135
136 // Let things shake themselves out.
137 [self.keychainView waitForKeyHierarchyReadiness];
138 [self waitForCKModifications];
139
140 NSMutableDictionary* query = [@{
141 (id)kSecClass : (id)kSecClassGenericPassword,
142 (id)kSecAttrAccessGroup : @"com.apple.security.ckks",
143 (id)kSecAttrAccessible: (id)kSecAttrAccessibleAfterFirstUnlock,
144 (id)kSecAttrAccount : @"testaccount",
145 (id)kSecAttrSynchronizable : (id)kCFBooleanTrue,
146 (id)kSecValueData : (id) [@"asdf" dataUsingEncoding:NSUTF8StringEncoding],
147 } mutableCopy];
148
149 XCTestExpectation* blockExpectation = [self expectationWithDescription: @"callback occurs"];
150
151 XCTAssertEqual(errSecSuccess, _SecItemAddAndNotifyOnSync((__bridge CFDictionaryRef) query, NULL, ^(bool didSync, CFErrorRef error) {
152 XCTAssertTrue(didSync, "Item synced properly");
153 XCTAssertNil((__bridge NSError*)error, "No error syncing item");
154
155 [blockExpectation fulfill];
156 }), @"_SecItemAddAndNotifyOnSync succeeded");
157
158 [self waitForExpectationsWithTimeout:5.0 handler:nil];
159 }
160
161 - (void)testAddAndNotifyOnSyncFailure {
162 [self createAndSaveFakeKeyHierarchy: self.keychainZoneID]; // Make life easy for this test.
163
164 [self startCKKSSubsystem];
165 [self.keychainView waitForFetchAndIncomingQueueProcessing];
166
167 // Due to item UUID selection, this item will be added with UUID DD7C2F9B-B22D-3B90-C299-E3B48174BFA3.
168 // Add it to CloudKit first!
169 CKRecord* ckr = [self createFakeRecord: self.keychainZoneID recordName:@"DD7C2F9B-B22D-3B90-C299-E3B48174BFA3"];
170 [self.keychainZone addToZone: ckr];
171
172
173 // Go for it!
174 [self expectCKAtomicModifyItemRecordsUpdateFailure: self.keychainZoneID];
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 XCTAssertFalse(didSync, "Item did not sync (as expected)");
189 XCTAssertNotNil((__bridge NSError*)error, "error exists when item fails to sync");
190
191 [blockExpectation fulfill];
192 }), @"_SecItemAddAndNotifyOnSync succeeded");
193
194 [self waitForExpectationsWithTimeout:5.0 handler:nil];
195 [self waitForCKModifications];
196 }
197
198 - (void)testAddAndNotifyOnSyncLoggedOut {
199 // Test starts with nothing in database and the user logged out of CloudKit. We expect no CKKS operations.
200 self.accountStatus = CKAccountStatusNoAccount;
201 self.silentFetchesAllowed = false;
202 [self startCKKSSubsystem];
203
204 NSMutableDictionary* query = [@{
205 (id)kSecClass : (id)kSecClassGenericPassword,
206 (id)kSecAttrAccessGroup : @"com.apple.security.ckks",
207 (id)kSecAttrAccessible: (id)kSecAttrAccessibleAfterFirstUnlock,
208 (id)kSecAttrAccount : @"testaccount",
209 (id)kSecAttrSynchronizable : (id)kCFBooleanTrue,
210 (id)kSecValueData : (id) [@"asdf" dataUsingEncoding:NSUTF8StringEncoding],
211 } mutableCopy];
212
213 XCTestExpectation* blockExpectation = [self expectationWithDescription: @"callback occurs"];
214
215 XCTAssertEqual(errSecSuccess, _SecItemAddAndNotifyOnSync((__bridge CFDictionaryRef) query, NULL, ^(bool didSync, CFErrorRef error) {
216 XCTAssertFalse(didSync, "Item did not sync (with no iCloud account)");
217 XCTAssertNotNil((__bridge NSError*)error, "Error exists syncing item while logged out");
218
219 [blockExpectation fulfill];
220 }), @"_SecItemAddAndNotifyOnSync succeeded");
221
222 [self waitForExpectationsWithTimeout:5.0 handler:nil];
223 }
224
225 - (void)testAddAndNotifyOnSyncBeforeKeyHierarchyReady {
226 // Test starts with a key hierarchy in cloudkit and the TLK having arrived
227 [self putFakeKeyHierarchyInCloudKit:self.keychainZoneID];
228 [self saveTLKMaterialToKeychain:self.keychainZoneID];
229
230 // But block CloudKit fetches (so the key hierarchy won't be ready when we add this new item)
231 [self holdCloudKitFetches];
232
233 [self startCKKSSubsystem];
234 [self.keychainView.viewSetupOperation waitUntilFinished];
235
236 NSMutableDictionary* query = [@{
237 (id)kSecClass : (id)kSecClassGenericPassword,
238 (id)kSecAttrAccessGroup : @"com.apple.security.ckks",
239 (id)kSecAttrAccessible: (id)kSecAttrAccessibleAfterFirstUnlock,
240 (id)kSecAttrAccount : @"testaccount",
241 (id)kSecAttrSynchronizable : (id)kCFBooleanTrue,
242 (id)kSecAttrSyncViewHint : self.keychainView.zoneName,
243 (id)kSecValueData : (id) [@"asdf" dataUsingEncoding:NSUTF8StringEncoding],
244 } mutableCopy];
245
246 XCTestExpectation* blockExpectation = [self expectationWithDescription: @"callback occurs"];
247
248 XCTAssertEqual(errSecSuccess, _SecItemAddAndNotifyOnSync((__bridge CFDictionaryRef) query, NULL, ^(bool didSync, CFErrorRef error) {
249 XCTAssertTrue(didSync, "Item synced");
250 XCTAssertNil((__bridge NSError*)error, "Shouldn't have received an error syncing item");
251
252 [blockExpectation fulfill];
253 }), @"_SecItemAddAndNotifyOnSync succeeded");
254
255 // We should be in the 'initialized' state, but no further
256 XCTAssertEqual(0, [self.keychainView.keyHierarchyConditions[SecCKKSZoneKeyStateInitialized] wait:100*NSEC_PER_MSEC], @"Should have reached key state 'initialized', but no further");
257
258 // When we release the fetch, the callback should still fire and the item should upload
259 [self expectCKModifyItemRecords: 1 currentKeyPointerRecords: 1 zoneID:self.keychainZoneID];
260 [self releaseCloudKitFetchHold];
261 XCTAssertEqual(0, [self.keychainView.keyHierarchyConditions[SecCKKSZoneKeyStateReady] wait:5*NSEC_PER_SEC], @"Should have reached key state 'ready'");
262
263 // Verify that the item was written to CloudKit
264 OCMVerifyAllWithDelay(self.mockDatabase, 8);
265
266 [self waitForExpectationsWithTimeout:5.0 handler:nil];
267 }
268
269 - (BOOL (^) (CKRecord*)) checkPCSFieldsBlock: (CKRecordZoneID*) zoneID
270 PCSServiceIdentifier:(NSNumber*)servIdentifier
271 PCSPublicKey:(NSData*)publicKey
272 PCSPublicIdentity:(NSData*)publicIdentity
273 {
274 __weak __typeof(self) weakSelf = self;
275 return ^BOOL(CKRecord* record) {
276 __strong __typeof(weakSelf) strongSelf = weakSelf;
277 XCTAssertNotNil(strongSelf, "self exists");
278
279 XCTAssert([record[SecCKRecordPCSServiceIdentifier] isEqual: servIdentifier], "PCS Service identifier matches input");
280 XCTAssert([record[SecCKRecordPCSPublicKey] isEqual: publicKey], "PCS Public Key matches input");
281 XCTAssert([record[SecCKRecordPCSPublicIdentity] isEqual: publicIdentity], "PCS Public Identity matches input");
282
283 if([record[SecCKRecordPCSServiceIdentifier] isEqual: servIdentifier] &&
284 [record[SecCKRecordPCSPublicKey] isEqual: publicKey] &&
285 [record[SecCKRecordPCSPublicIdentity] isEqual: publicIdentity]) {
286 return YES;
287 } else {
288 return NO;
289 }
290 };
291 }
292
293 - (void)testPCSUnencryptedFieldsAdd {
294
295 [self createAndSaveFakeKeyHierarchy: self.keychainZoneID]; // Make life easy for this test.
296
297 [self startCKKSSubsystem];
298 [self.keychainView waitUntilAllOperationsAreFinished];
299
300 NSNumber* servIdentifier = @3;
301 NSData* publicKey = [@"asdfasdf" dataUsingEncoding:NSUTF8StringEncoding];
302 NSData* publicIdentity = [@"somedata" dataUsingEncoding:NSUTF8StringEncoding];
303
304 [self expectCKModifyItemRecords: 1 currentKeyPointerRecords: 1 zoneID:self.keychainZoneID
305 checkItem: [self checkPCSFieldsBlock:self.keychainZoneID
306 PCSServiceIdentifier:(NSNumber *)servIdentifier
307 PCSPublicKey:publicKey
308 PCSPublicIdentity:publicIdentity]];
309
310 NSMutableDictionary* query = [@{
311 (id)kSecClass : (id)kSecClassGenericPassword,
312 (id)kSecAttrAccessGroup : @"com.apple.security.ckks",
313 (id)kSecAttrAccessible: (id)kSecAttrAccessibleAfterFirstUnlock,
314 (id)kSecAttrAccount : @"testaccount",
315 (id)kSecAttrSynchronizable : (id)kCFBooleanTrue,
316 (id)kSecValueData : (id) [@"asdf" dataUsingEncoding:NSUTF8StringEncoding],
317 (id)kSecAttrDeriveSyncIDFromItemAttributes : (id)kCFBooleanTrue,
318 (id)kSecAttrPCSPlaintextServiceIdentifier : servIdentifier,
319 (id)kSecAttrPCSPlaintextPublicKey : publicKey,
320 (id)kSecAttrPCSPlaintextPublicIdentity : publicIdentity,
321 } mutableCopy];
322
323 XCTAssertEqual(errSecSuccess, SecItemAdd((__bridge CFDictionaryRef) query, NULL), @"SecItemAdd succeeded");
324
325 // Verify that the item is written to CloudKit
326 OCMVerifyAllWithDelay(self.mockDatabase, 4);
327
328 CFTypeRef item = NULL;
329 query[(id)kSecValueData] = nil;
330 query[(id)kSecReturnAttributes] = @YES;
331 XCTAssertEqual(errSecSuccess, SecItemCopyMatching((__bridge CFDictionaryRef) query, &item), "item should still exist");
332
333 NSDictionary* itemAttributes = (NSDictionary*) CFBridgingRelease(item);
334 XCTAssertEqualObjects(itemAttributes[(id)kSecAttrPCSPlaintextServiceIdentifier], servIdentifier, "Service Identifier exists");
335 XCTAssertEqualObjects(itemAttributes[(id)kSecAttrPCSPlaintextPublicKey], publicKey, "public key exists");
336 XCTAssertEqualObjects(itemAttributes[(id)kSecAttrPCSPlaintextPublicIdentity], publicIdentity, "public identity exists");
337
338 // Find the item record in CloudKit. Since we're using kSecAttrDeriveSyncIDFromItemAttributes,
339 // the record ID is likely DD7C2F9B-B22D-3B90-C299-E3B48174BFA3
340 [self waitForCKModifications];
341 CKRecordID* recordID = [[CKRecordID alloc] initWithRecordName: @"DD7C2F9B-B22D-3B90-C299-E3B48174BFA3" zoneID:self.keychainZoneID];
342 CKRecord* record = self.keychainZone.currentDatabase[recordID];
343 XCTAssertNotNil(record, "Found record in CloudKit at expected UUID");
344
345 XCTAssertEqualObjects(record[SecCKRecordPCSServiceIdentifier], servIdentifier, "Service identifier sent to cloudkit");
346 XCTAssertEqualObjects(record[SecCKRecordPCSPublicKey], publicKey, "public key sent to cloudkit");
347 XCTAssertEqualObjects(record[SecCKRecordPCSPublicIdentity], publicIdentity, "public identity sent to cloudkit");
348 }
349
350 - (void)testPCSUnencryptedFieldsModify {
351 [self createAndSaveFakeKeyHierarchy: self.keychainZoneID]; // Make life easy for this test.
352
353 [self startCKKSSubsystem];
354 [self.keychainView waitUntilAllOperationsAreFinished];
355
356 NSNumber* servIdentifier = @3;
357 NSData* publicKey = [@"asdfasdf" dataUsingEncoding:NSUTF8StringEncoding];
358 NSData* publicIdentity = [@"somedata" dataUsingEncoding:NSUTF8StringEncoding];
359
360 [self expectCKModifyItemRecords: 1 currentKeyPointerRecords: 1 zoneID:self.keychainZoneID
361 checkItem: [self checkPCSFieldsBlock:self.keychainZoneID
362 PCSServiceIdentifier:(NSNumber *)servIdentifier
363 PCSPublicKey:publicKey
364 PCSPublicIdentity:publicIdentity]];
365
366 NSMutableDictionary* query = [@{
367 (id)kSecClass : (id)kSecClassGenericPassword,
368 (id)kSecAttrAccessGroup : @"com.apple.security.ckks",
369 (id)kSecAttrAccessible: (id)kSecAttrAccessibleAfterFirstUnlock,
370 (id)kSecAttrAccount : @"testaccount",
371 (id)kSecAttrSynchronizable : (id)kCFBooleanTrue,
372 (id)kSecValueData : (id) [@"asdf" dataUsingEncoding:NSUTF8StringEncoding],
373 (id)kSecAttrDeriveSyncIDFromItemAttributes : (id)kCFBooleanTrue,
374 (id)kSecAttrPCSPlaintextServiceIdentifier : servIdentifier,
375 (id)kSecAttrPCSPlaintextPublicKey : publicKey,
376 (id)kSecAttrPCSPlaintextPublicIdentity : publicIdentity,
377 } mutableCopy];
378
379 XCTAssertEqual(errSecSuccess, SecItemAdd((__bridge CFDictionaryRef) query, NULL), @"SecItemAdd succeeded");
380
381 OCMVerifyAllWithDelay(self.mockDatabase, 8);
382 [self waitForCKModifications];
383
384 query[(id)kSecValueData] = nil;
385 query[(id)kSecAttrPCSPlaintextServiceIdentifier] = nil;
386 query[(id)kSecAttrPCSPlaintextPublicKey] = nil;
387 query[(id)kSecAttrPCSPlaintextPublicIdentity] = nil;
388
389 servIdentifier = @1;
390 publicKey = [@"new public key" dataUsingEncoding:NSUTF8StringEncoding];
391
392 NSNumber* newServiceIdentifier = @10;
393 NSData* newPublicKey = [@"new public key" dataUsingEncoding:NSUTF8StringEncoding];
394 NSData* newPublicIdentity = [@"new public identity" dataUsingEncoding:NSUTF8StringEncoding];
395
396 NSDictionary* update = @{
397 (id)kSecAttrPCSPlaintextServiceIdentifier : newServiceIdentifier,
398 (id)kSecAttrPCSPlaintextPublicKey : newPublicKey,
399 (id)kSecAttrPCSPlaintextPublicIdentity : newPublicIdentity,
400 };
401
402 [self expectCKModifyItemRecords: 1 currentKeyPointerRecords: 1 zoneID:self.keychainZoneID
403 checkItem: [self checkPCSFieldsBlock:self.keychainZoneID
404 PCSServiceIdentifier:(NSNumber *)newServiceIdentifier
405 PCSPublicKey:newPublicKey
406 PCSPublicIdentity:newPublicIdentity]];
407
408 XCTAssertEqual(errSecSuccess, SecItemUpdate((__bridge CFDictionaryRef) query, (__bridge CFDictionaryRef) update), @"SecItemUpdate succeeded");
409 OCMVerifyAllWithDelay(self.mockDatabase, 8);
410
411 CFTypeRef item = NULL;
412 query[(id)kSecValueData] = nil;
413 query[(id)kSecReturnAttributes] = @YES;
414 XCTAssertEqual(errSecSuccess, SecItemCopyMatching((__bridge CFDictionaryRef) query, &item), "item should still exist");
415
416 NSDictionary* itemAttributes = (NSDictionary*) CFBridgingRelease(item);
417 XCTAssertEqualObjects(itemAttributes[(id)kSecAttrPCSPlaintextServiceIdentifier], newServiceIdentifier, "Service Identifier exists");
418 XCTAssertEqualObjects(itemAttributes[(id)kSecAttrPCSPlaintextPublicKey], newPublicKey, "public key exists");
419 XCTAssertEqualObjects(itemAttributes[(id)kSecAttrPCSPlaintextPublicIdentity], newPublicIdentity, "public identity exists");
420
421 // Find the item record in CloudKit. Since we're using kSecAttrDeriveSyncIDFromItemAttributes,
422 // the record ID is likely DD7C2F9B-B22D-3B90-C299-E3B48174BFA3
423 [self waitForCKModifications];
424 CKRecordID* recordID = [[CKRecordID alloc] initWithRecordName: @"DD7C2F9B-B22D-3B90-C299-E3B48174BFA3" zoneID:self.keychainZoneID];
425 CKRecord* record = self.keychainZone.currentDatabase[recordID];
426 XCTAssertNotNil(record, "Found record in CloudKit at expected UUID");
427
428 XCTAssertEqualObjects(record[SecCKRecordPCSServiceIdentifier], newServiceIdentifier, "Service identifier sent to cloudkit");
429 XCTAssertEqualObjects(record[SecCKRecordPCSPublicKey], newPublicKey, "public key sent to cloudkit");
430 XCTAssertEqualObjects(record[SecCKRecordPCSPublicIdentity], newPublicIdentity, "public identity sent to cloudkit");
431 }
432
433 // As of [<rdar://problem/32558310> CKKS: Re-authenticate PCSPublicFields], these fields are NOT server-modifiable. This test proves it.
434 - (void)testPCSUnencryptedFieldsServerModifyFail {
435 [self createAndSaveFakeKeyHierarchy: self.keychainZoneID]; // Make life easy for this test.
436
437 [self startCKKSSubsystem];
438 [self.keychainView waitForKeyHierarchyReadiness];
439
440 NSNumber* servIdentifier = @3;
441 NSData* publicKey = [@"asdfasdf" dataUsingEncoding:NSUTF8StringEncoding];
442 NSData* publicIdentity = [@"somedata" dataUsingEncoding:NSUTF8StringEncoding];
443
444 [self expectCKModifyItemRecords: 1 currentKeyPointerRecords: 1 zoneID:self.keychainZoneID
445 checkItem: [self checkPCSFieldsBlock:self.keychainZoneID
446 PCSServiceIdentifier:(NSNumber *)servIdentifier
447 PCSPublicKey:publicKey
448 PCSPublicIdentity:publicIdentity]];
449
450 NSMutableDictionary* query = [@{
451 (id)kSecClass : (id)kSecClassGenericPassword,
452 (id)kSecAttrAccessGroup : @"com.apple.security.ckks",
453 (id)kSecAttrAccessible: (id)kSecAttrAccessibleAfterFirstUnlock,
454 (id)kSecAttrAccount : @"testaccount",
455 (id)kSecAttrSynchronizable : (id)kCFBooleanTrue,
456 (id)kSecValueData : (id) [@"asdf" dataUsingEncoding:NSUTF8StringEncoding],
457 (id)kSecAttrDeriveSyncIDFromItemAttributes : (id)kCFBooleanTrue,
458 (id)kSecAttrPCSPlaintextServiceIdentifier : servIdentifier,
459 (id)kSecAttrPCSPlaintextPublicKey : publicKey,
460 (id)kSecAttrPCSPlaintextPublicIdentity : publicIdentity,
461 } mutableCopy];
462
463 XCTAssertEqual(errSecSuccess, SecItemAdd((__bridge CFDictionaryRef) query, NULL), @"SecItemAdd succeeded");
464
465 OCMVerifyAllWithDelay(self.mockDatabase, 4);
466 [self waitForCKModifications];
467
468 // Find the item record in CloudKit. Since we're using kSecAttrDeriveSyncIDFromItemAttributes,
469 // the record ID is likely DD7C2F9B-B22D-3B90-C299-E3B48174BFA3
470 CKRecordID* recordID = [[CKRecordID alloc] initWithRecordName: @"DD7C2F9B-B22D-3B90-C299-E3B48174BFA3" zoneID:self.keychainZoneID];
471 CKRecord* record = self.keychainZone.currentDatabase[recordID];
472 XCTAssertNotNil(record, "Found record in CloudKit at expected UUID");
473
474 // Items are encrypted using encv2
475 XCTAssertEqualObjects(record[SecCKRecordEncryptionVersionKey], [NSNumber numberWithInteger:(int) CKKSItemEncryptionVersion2], "Uploaded using encv2");
476
477 if(!record) {
478 // Test has already failed; find the record just to be nice.
479 for(CKRecord* maybe in self.keychainZone.currentDatabase.allValues) {
480 if(maybe[SecCKRecordPCSServiceIdentifier] != nil) {
481 record = maybe;
482 }
483 }
484 }
485
486 NSNumber* newServiceIdentifier = @10;
487 NSData* newPublicKey = [@"new public key" dataUsingEncoding:NSUTF8StringEncoding];
488 NSData* newPublicIdentity = [@"new public identity" dataUsingEncoding:NSUTF8StringEncoding];
489
490 // Change the public key and public identity
491 record = [record copyWithZone: nil];
492 record[SecCKRecordPCSServiceIdentifier] = newServiceIdentifier;
493 record[SecCKRecordPCSPublicKey] = newPublicKey;
494 record[SecCKRecordPCSPublicIdentity] = newPublicIdentity;
495 [self.keychainZone addToZone: record];
496
497 // Trigger a notification
498 [self.keychainView notifyZoneChange:nil];
499 [self.keychainView waitForFetchAndIncomingQueueProcessing];
500
501 CFTypeRef item = NULL;
502 query[(id)kSecValueData] = nil;
503 query[(id)kSecAttrPCSPlaintextServiceIdentifier] = nil;
504 query[(id)kSecAttrPCSPlaintextPublicKey] = nil;
505 query[(id)kSecAttrPCSPlaintextPublicIdentity] = nil;
506 query[(id)kSecReturnAttributes] = @YES;
507 XCTAssertEqual(errSecSuccess, SecItemCopyMatching((__bridge CFDictionaryRef) query, &item), "item should still exist");
508
509 NSDictionary* itemAttributes = (NSDictionary*) CFBridgingRelease(item);
510 XCTAssertEqualObjects(itemAttributes[(id)kSecAttrPCSPlaintextServiceIdentifier], servIdentifier, "service identifier is not updated");
511 XCTAssertEqualObjects(itemAttributes[(id)kSecAttrPCSPlaintextPublicKey], publicKey, "public key not updated");
512 XCTAssertEqualObjects(itemAttributes[(id)kSecAttrPCSPlaintextPublicIdentity], publicIdentity, "public identity not updated");
513 }
514
515 -(void)testPCSUnencryptedFieldsRecieveUnauthenticatedFields {
516 [self createAndSaveFakeKeyHierarchy: self.keychainZoneID]; // Make life easy for this test.
517
518 [self startCKKSSubsystem];
519 [self.keychainView waitForKeyHierarchyReadiness];
520
521 NSNumber* servIdentifier = @3;
522 NSData* publicKey = [@"asdfasdf" dataUsingEncoding:NSUTF8StringEncoding];
523 NSData* publicIdentity = [@"somedata" dataUsingEncoding:NSUTF8StringEncoding];
524
525 NSError* error = nil;
526
527 // Manually encrypt an item
528 NSString* recordName = @"7B598D31-F9C5-481E-98AC-5A507ACB2D85";
529 CKRecordID* recordID = [[CKRecordID alloc] initWithRecordName:recordName zoneID:self.keychainZoneID];
530 NSDictionary* item = [self fakeRecordDictionary: @"account-delete-me" zoneID:self.keychainZoneID];
531 CKKSItem* cipheritem = [[CKKSItem alloc] initWithUUID:recordID.recordName
532 parentKeyUUID:self.keychainZoneKeys.classC.uuid
533 zoneID:recordID.zoneID];
534 CKKSKey* itemkey = [CKKSKey randomKeyWrappedByParent: self.keychainZoneKeys.classC error:&error];
535 XCTAssertNotNil(itemkey, "Got a key");
536 cipheritem.wrappedkey = itemkey.wrappedkey;
537 XCTAssertNotNil(cipheritem.wrappedkey, "Got a wrapped key");
538
539 cipheritem.encver = CKKSItemEncryptionVersion1;
540
541 // This item has the PCS public fields, but they are not authenticated
542 cipheritem.plaintextPCSServiceIdentifier = servIdentifier;
543 cipheritem.plaintextPCSPublicKey = publicKey;
544 cipheritem.plaintextPCSPublicIdentity = publicIdentity;
545
546 NSDictionary<NSString*, NSData*>* authenticatedData = [cipheritem makeAuthenticatedDataDictionaryUpdatingCKKSItem: nil encryptionVersion:CKKSItemEncryptionVersion1];
547 cipheritem.encitem = [CKKSItemEncrypter encryptDictionary:item key:itemkey.aessivkey authenticatedData:authenticatedData error:&error];
548 XCTAssertNil(error, "no error encrypting object");
549 XCTAssertNotNil(cipheritem.encitem, "Recieved ciphertext");
550
551 [self.keychainZone addToZone:[cipheritem CKRecordWithZoneID: recordID.zoneID]];
552
553 [self.keychainView waitForFetchAndIncomingQueueProcessing];
554
555 NSDictionary* query = @{(id)kSecClass: (id)kSecClassGenericPassword,
556 (id)kSecReturnAttributes: @YES,
557 (id)kSecAttrSynchronizable: @YES,
558 (id)kSecAttrAccount: @"account-delete-me",
559 (id)kSecMatchLimit: (id)kSecMatchLimitOne,
560 };
561 CFTypeRef cfresult = NULL;
562 XCTAssertEqual(errSecSuccess, SecItemCopyMatching((__bridge CFDictionaryRef) query, &cfresult), "Found synced item");
563
564 NSDictionary* result = CFBridgingRelease(cfresult);
565 XCTAssertEqualObjects(result[(id)kSecAttrPCSPlaintextServiceIdentifier], servIdentifier, "Received PCS service identifier");
566 XCTAssertEqualObjects(result[(id)kSecAttrPCSPlaintextPublicKey], publicKey, "Received PCS public key");
567 XCTAssertEqualObjects(result[(id)kSecAttrPCSPlaintextPublicIdentity], publicIdentity, "Received PCS public identity");
568 }
569
570 -(void)testPCSUnencryptedFieldsRecieveAuthenticatedFields {
571 [self createAndSaveFakeKeyHierarchy: self.keychainZoneID]; // Make life easy for this test.
572
573 [self startCKKSSubsystem];
574 [self.keychainView waitForKeyHierarchyReadiness];
575
576 NSNumber* servIdentifier = @3;
577 NSData* publicKey = [@"asdfasdf" dataUsingEncoding:NSUTF8StringEncoding];
578 NSData* publicIdentity = [@"somedata" dataUsingEncoding:NSUTF8StringEncoding];
579
580 NSError* error = nil;
581
582 // Manually encrypt an item
583 NSString* recordName = @"7B598D31-F9C5-481E-98AC-5A507ACB2D85";
584 CKRecordID* recordID = [[CKRecordID alloc] initWithRecordName:recordName zoneID:self.keychainZoneID];
585 NSDictionary* item = [self fakeRecordDictionary: @"account-delete-me" zoneID:self.keychainZoneID];
586 CKKSItem* cipheritem = [[CKKSItem alloc] initWithUUID:recordID.recordName
587 parentKeyUUID:self.keychainZoneKeys.classC.uuid
588 zoneID:recordID.zoneID];
589 CKKSKey* itemkey = [CKKSKey randomKeyWrappedByParent: self.keychainZoneKeys.classC error:&error];
590 XCTAssertNotNil(itemkey, "Got a key");
591 cipheritem.wrappedkey = itemkey.wrappedkey;
592 XCTAssertNotNil(cipheritem.wrappedkey, "Got a wrapped key");
593
594 cipheritem.encver = CKKSItemEncryptionVersion2;
595
596 // This item has the PCS public fields, and they are authenticated (since we're using v2)
597 cipheritem.plaintextPCSServiceIdentifier = servIdentifier;
598 cipheritem.plaintextPCSPublicKey = publicKey;
599 cipheritem.plaintextPCSPublicIdentity = publicIdentity;
600
601 // Use version 2, so PCS plaintext fields will be authenticated
602 NSMutableDictionary<NSString*, NSData*>* authenticatedData = [[cipheritem makeAuthenticatedDataDictionaryUpdatingCKKSItem: nil encryptionVersion:CKKSItemEncryptionVersion2] mutableCopy];
603
604 cipheritem.encitem = [CKKSItemEncrypter encryptDictionary:item key:itemkey.aessivkey authenticatedData:authenticatedData error:&error];
605 XCTAssertNil(error, "no error encrypting object");
606 XCTAssertNotNil(cipheritem.encitem, "Recieved ciphertext");
607
608 [self.keychainZone addToZone:[cipheritem CKRecordWithZoneID: recordID.zoneID]];
609
610 [self.keychainView waitForFetchAndIncomingQueueProcessing];
611
612 NSDictionary* query = @{(id)kSecClass: (id)kSecClassGenericPassword,
613 (id)kSecReturnAttributes: @YES,
614 (id)kSecAttrSynchronizable: @YES,
615 (id)kSecAttrAccount: @"account-delete-me",
616 (id)kSecMatchLimit: (id)kSecMatchLimitOne,
617 };
618 CFTypeRef cfresult = NULL;
619 XCTAssertEqual(errSecSuccess, SecItemCopyMatching((__bridge CFDictionaryRef) query, &cfresult), "Found synced item");
620
621 NSDictionary* result = CFBridgingRelease(cfresult);
622 XCTAssertEqualObjects(result[(id)kSecAttrPCSPlaintextServiceIdentifier], servIdentifier, "Received PCS service identifier");
623 XCTAssertEqualObjects(result[(id)kSecAttrPCSPlaintextPublicKey], publicKey, "Received PCS public key");
624 XCTAssertEqualObjects(result[(id)kSecAttrPCSPlaintextPublicIdentity], publicIdentity, "Received PCS public identity");
625
626 // Test that if this item is updated, it remains encrypted in v2
627 [self expectCKModifyItemRecords: 1 currentKeyPointerRecords: 1 zoneID:self.keychainZoneID
628 checkItem: [self checkPCSFieldsBlock:self.keychainZoneID
629 PCSServiceIdentifier:(NSNumber *)servIdentifier
630 PCSPublicKey:publicKey
631 PCSPublicIdentity:publicIdentity]];
632 [self updateGenericPassword:@"different password" account:@"account-delete-me"];
633
634 OCMVerifyAllWithDelay(self.mockDatabase, 4);
635 [self waitForCKModifications];
636
637 CKRecord* newRecord = self.keychainZone.currentDatabase[recordID];
638 XCTAssertEqualObjects(newRecord[SecCKRecordPCSServiceIdentifier], servIdentifier, "Didn't change service identifier");
639 XCTAssertEqualObjects(newRecord[SecCKRecordPCSPublicKey], publicKey, "Didn't change public key");
640 XCTAssertEqualObjects(newRecord[SecCKRecordPCSPublicIdentity], publicIdentity, "Didn't change public identity");
641 XCTAssertEqualObjects(newRecord[SecCKRecordEncryptionVersionKey], [NSNumber numberWithInteger:(int) CKKSItemEncryptionVersion2], "Uploaded using encv2");
642 }
643
644 -(void)testResetLocal {
645 // Test starts with nothing in database, but one in our fake CloudKit.
646 [self putFakeKeyHierarchyInCloudKit:self.keychainZoneID];
647 [self saveTLKMaterialToKeychainSimulatingSOS:self.keychainZoneID];
648
649 // Spin up CKKS subsystem.
650 [self startCKKSSubsystem];
651
652 // We expect a single record to be uploaded
653 [self expectCKModifyItemRecords: 1 currentKeyPointerRecords: 1 zoneID:self.keychainZoneID checkItem: [self checkClassCBlock:self.keychainZoneID message:@"Object was encrypted under class C key in hierarchy"]];
654 [self addGenericPassword: @"data" account: @"account-delete-me"];
655 OCMVerifyAllWithDelay(self.mockDatabase, 8);
656
657 // After the local reset, we expect: a fetch, then nothing
658 self.silentFetchesAllowed = false;
659 [self expectCKFetch];
660
661 dispatch_semaphore_t resetSemaphore = dispatch_semaphore_create(0);
662 [self.injectedManager rpcResetLocal:nil reply:^(NSError* result) {
663 XCTAssertNil(result, "no error resetting local");
664 secnotice("ckks", "Received a rpcResetLocal callback");
665 dispatch_semaphore_signal(resetSemaphore);
666 }];
667
668 XCTAssertEqual(0, dispatch_semaphore_wait(resetSemaphore, 4*NSEC_PER_SEC), "Semaphore wait did not time out");
669
670 OCMVerifyAllWithDelay(self.mockDatabase, 8);
671
672 [self expectCKModifyItemRecords: 1 currentKeyPointerRecords: 1 zoneID:self.keychainZoneID checkItem: [self checkClassABlock:self.keychainZoneID message:@"Object was encrypted under class A key in hierarchy"]];
673 [self addGenericPassword:@"asdf"
674 account:@"account-class-A"
675 viewHint:nil
676 access:(id)kSecAttrAccessibleWhenUnlocked
677 expecting:errSecSuccess
678 message:@"Adding class A item"];
679 OCMVerifyAllWithDelay(self.mockDatabase, 8);
680 }
681
682 -(void)testResetLocalWhileLoggedOut {
683 // We're "logged in to" cloudkit but not in circle.
684 self.circleStatus = kSOSCCNotInCircle;
685 [self.accountStateTracker notifyCircleStatusChangeAndWaitForSignal];
686 self.silentFetchesAllowed = false;
687
688 // Test starts with local TLK and key hierarhcy in our fake cloudkit
689 [self putFakeKeyHierarchyInCloudKit:self.keychainZoneID];
690 [self saveTLKMaterialToKeychainSimulatingSOS:self.keychainZoneID];
691
692 // Spin up CKKS subsystem.
693 [self startCKKSSubsystem];
694
695 NSData* changeTokenData = [[[NSUUID UUID] UUIDString] dataUsingEncoding:NSUTF8StringEncoding];
696 CKServerChangeToken* changeToken = [[CKServerChangeToken alloc] initWithData:changeTokenData];
697 [self.keychainView dispatchSync: ^bool{
698 CKKSZoneStateEntry* ckse = [CKKSZoneStateEntry state:self.keychainView.zoneName];
699 ckse.changeToken = changeToken;
700
701 NSError* error = nil;
702 [ckse saveToDatabase:&error];
703 XCTAssertNil(error, "No error saving new zone state to database");
704 }];
705
706 dispatch_semaphore_t resetSemaphore = dispatch_semaphore_create(0);
707 [self.injectedManager rpcResetLocal:nil reply:^(NSError* result) {
708 XCTAssertNil(result, "no error resetting cloudkit");
709 secnotice("ckks", "Received a rpcResetLocal callback");
710 dispatch_semaphore_signal(resetSemaphore);
711 }];
712
713 XCTAssertEqual(0, dispatch_semaphore_wait(resetSemaphore, 400*NSEC_PER_SEC), "Semaphore wait did not time out");
714
715 [self.keychainView dispatchSync: ^bool{
716 CKKSZoneStateEntry* ckse = [CKKSZoneStateEntry state:self.keychainView.zoneName];
717 XCTAssertNotEqualObjects(changeToken, ckse.changeToken, "Change token is reset");
718 }];
719
720 // Now log in, and see what happens! It should re-fetch, pick up the old key hierarchy, and use it
721 self.silentFetchesAllowed = true;
722 self.circleStatus = kSOSCCInCircle;
723 [self.accountStateTracker notifyCircleStatusChangeAndWaitForSignal];
724
725 [self expectCKModifyItemRecords: 1 currentKeyPointerRecords: 1 zoneID:self.keychainZoneID checkItem:[self checkClassABlock:self.keychainZoneID message:@"Object was encrypted under class A key in hierarchy"]];
726 [self addGenericPassword:@"asdf"
727 account:@"account-class-A"
728 viewHint:nil
729 access:(id)kSecAttrAccessibleWhenUnlocked
730 expecting:errSecSuccess
731 message:@"Adding class A item"];
732 OCMVerifyAllWithDelay(self.mockDatabase, 8);
733 }
734
735 -(void)testResetCloudKitZone {
736 // Test starts with nothing in database, but one in our fake CloudKit.
737 [self putFakeKeyHierarchyInCloudKit:self.keychainZoneID];
738 [self saveTLKMaterialToKeychainSimulatingSOS:self.keychainZoneID];
739
740 // Spin up CKKS subsystem.
741 [self startCKKSSubsystem];
742
743 // We expect a single record to be uploaded
744 [self expectCKModifyItemRecords: 1 currentKeyPointerRecords: 1 zoneID:self.keychainZoneID checkItem: [self checkClassCBlock:self.keychainZoneID message:@"Object was encrypted under class C key in hierarchy"]];
745 [self addGenericPassword: @"data" account: @"account-delete-me"];
746 OCMVerifyAllWithDelay(self.mockDatabase, 8);
747
748 // We expect a key hierarchy upload, and then the class C item upload
749 [self expectCKModifyKeyRecords: 3 currentKeyPointerRecords: 3 tlkShareRecords: 1 zoneID:self.keychainZoneID];
750 [self expectCKModifyItemRecords: 1 currentKeyPointerRecords: 1 zoneID:self.keychainZoneID checkItem: [self checkClassCBlock:self.keychainZoneID message:@"Object was encrypted under class C key in hierarchy"]];
751
752 dispatch_semaphore_t resetSemaphore = dispatch_semaphore_create(0);
753 [self.injectedManager rpcResetCloudKit:nil reply:^(NSError* result) {
754 XCTAssertNil(result, "no error resetting cloudkit");
755 secnotice("ckks", "Received a resetCloudKit callback");
756 dispatch_semaphore_signal(resetSemaphore);
757 }];
758
759 XCTAssertEqual(0, dispatch_semaphore_wait(resetSemaphore, 4*NSEC_PER_SEC), "Semaphore wait did not time out");
760
761 OCMVerifyAllWithDelay(self.mockDatabase, 8);
762
763 [self expectCKModifyItemRecords: 1 currentKeyPointerRecords: 1 zoneID:self.keychainZoneID checkItem: [self checkClassABlock:self.keychainZoneID message:@"Object was encrypted under class A key in hierarchy"]];
764 [self addGenericPassword:@"asdf"
765 account:@"account-class-A"
766 viewHint:nil
767 access:(id)kSecAttrAccessibleWhenUnlocked
768 expecting:errSecSuccess
769 message:@"Adding class A item"];
770 OCMVerifyAllWithDelay(self.mockDatabase, 8);
771 }
772
773 - (void)testResetCloudKitZoneDuringWaitForTLK {
774 // Test starts with nothing in database, but one in our fake CloudKit.
775 // No TLK, though!
776 [self putFakeKeyHierarchyInCloudKit:self.keychainZoneID];
777
778 // Spin up CKKS subsystem.
779 [self startCKKSSubsystem];
780
781 // No records should be uploaded
782 [self addGenericPassword: @"data" account: @"account-delete-me"];
783
784 XCTAssertEqual(0, [self.keychainView.keyHierarchyConditions[SecCKKSZoneKeyStateWaitForTLK] wait:8*NSEC_PER_SEC], "CKKS should have entered waitfortlk");
785
786 // Restart CKKS to really get in the spirit of waitfortlk (and get a pending processOutgoingQueue operation going)
787 self.keychainView = [[CKKSViewManager manager] restartZone: self.keychainZoneID.zoneName];
788 XCTAssertEqual(0, [self.keychainView.keyHierarchyConditions[SecCKKSZoneKeyStateWaitForTLK] wait:8*NSEC_PER_SEC], "CKKS entered waitfortlk");
789
790 CKKSOutgoingQueueOperation* outgoingOp = [self.keychainView processOutgoingQueue:nil];
791 XCTAssertTrue([outgoingOp isPending], "outgoing queue processing should be on hold");
792
793 // Now, reset everything. The outgoingOp should get cancelled.
794 // We expect a key hierarchy upload, and then the class C item upload
795 [self expectCKModifyKeyRecords: 3 currentKeyPointerRecords: 3 tlkShareRecords:3 zoneID:self.keychainZoneID];
796 [self expectCKModifyItemRecords: 1 currentKeyPointerRecords: 1 zoneID:self.keychainZoneID
797 checkItem: [self checkClassCBlock:self.keychainZoneID message:@"Object was encrypted under class C key in hierarchy"]];
798
799 XCTestExpectation* resetExpectation = [self expectationWithDescription: @"reset callback occurs"];
800 [self.injectedManager rpcResetCloudKit:nil reply:^(NSError* result) {
801 XCTAssertNil(result, "no error resetting cloudkit");
802 [resetExpectation fulfill];
803 }];
804 [self waitForExpectations:@[resetExpectation] timeout:8.0];
805
806 XCTAssertTrue([outgoingOp isCancelled], "old stuck ProcessOutgoingQueue should be cancelled");
807 OCMVerifyAllWithDelay(self.mockDatabase, 8);
808
809 // And adding another item works too
810 [self expectCKModifyItemRecords: 1 currentKeyPointerRecords: 1 zoneID:self.keychainZoneID checkItem: [self checkClassABlock:self.keychainZoneID message:@"Object was encrypted under class A key in hierarchy"]];
811 [self addGenericPassword:@"asdf"
812 account:@"account-class-A"
813 viewHint:nil
814 access:(id)kSecAttrAccessibleWhenUnlocked
815 expecting:errSecSuccess
816 message:@"Adding class A item"];
817 OCMVerifyAllWithDelay(self.mockDatabase, 8);
818 }
819
820 /*
821 * This test doesn't work, since the resetLocal fails. CKKS gets back into waitfortlk
822 * but that isn't considered a successful resetLocal.
823 *
824 - (void)testResetLocalDuringWaitForTLK {
825 // Test starts with nothing in database, but one in our fake CloudKit.
826 // No TLK, though!
827 [self putFakeKeyHierarchyInCloudKit:self.keychainZoneID];
828
829 // Spin up CKKS subsystem.
830 [self startCKKSSubsystem];
831
832 // No records should be uploaded
833 [self addGenericPassword: @"data" account: @"account-delete-me"];
834
835 XCTAssertEqual(0, [self.keychainView.keyHierarchyConditions[SecCKKSZoneKeyStateWaitForTLK] wait:8*NSEC_PER_SEC], "CKKS should have entered waitfortlk");
836
837 // Restart CKKS to really get in the spirit of waitfortlk (and get a pending processOutgoingQueue operation going)
838 self.keychainView = [[CKKSViewManager manager] restartZone: self.keychainZoneID.zoneName];
839 XCTAssertEqual(0, [self.keychainView.keyHierarchyConditions[SecCKKSZoneKeyStateWaitForTLK] wait:8*NSEC_PER_SEC], "CKKS entered waitfortlk");
840
841 CKKSOutgoingQueueOperation* outgoingOp = [self.keychainView processOutgoingQueue:nil];
842 XCTAssertTrue([outgoingOp isPending], "outgoing queue processing should be on hold");
843
844 // Now, reset everything. The outgoingOp should get cancelled.
845 // We expect a key hierarchy upload, and then the class C item upload
846 [self expectCKModifyKeyRecords: 3 currentKeyPointerRecords: 3 tlkShareRecords:3 zoneID:self.keychainZoneID];
847 [self expectCKModifyItemRecords: 1 currentKeyPointerRecords: 1 zoneID:self.keychainZoneID
848 checkItem: [self checkClassCBlock:self.keychainZoneID message:@"Object was encrypted under class C key in hierarchy"]];
849
850 XCTestExpectation* resetExpectation = [self expectationWithDescription: @"reset callback occurs"];
851 [self.injectedManager rpcResetLocal:nil reply:^(NSError* result) {
852 XCTAssertNil(result, "no error resetting local");
853 [resetExpectation fulfill];
854 }];
855 [self waitForExpectations:@[resetExpectation] timeout:8.0];
856
857 XCTAssertTrue([outgoingOp isCancelled], "old stuck ProcessOutgoingQueue should be cancelled");
858 OCMVerifyAllWithDelay(self.mockDatabase, 8);
859
860 // And adding another item works too
861 [self expectCKModifyItemRecords: 1 currentKeyPointerRecords: 1 zoneID:self.keychainZoneID checkItem: [self checkClassABlock:self.keychainZoneID message:@"Object was encrypted under class A key in hierarchy"]];
862 [self addGenericPassword:@"asdf"
863 account:@"account-class-A"
864 viewHint:nil
865 access:(id)kSecAttrAccessibleWhenUnlocked
866 expecting:errSecSuccess
867 message:@"Adding class A item"];
868 OCMVerifyAllWithDelay(self.mockDatabase, 8);
869 }*/
870
871 -(void)testResetCloudKitZoneWhileLoggedOut {
872 // We're "logged in to" cloudkit but not in circle.
873 self.circleStatus = kSOSCCNotInCircle;
874 [self.accountStateTracker notifyCircleStatusChangeAndWaitForSignal];
875 self.silentFetchesAllowed = false;
876
877 // Test starts with nothing in database, but one in our fake CloudKit.
878 [self putFakeKeyHierarchyInCloudKit:self.keychainZoneID];
879
880 // Spin up CKKS subsystem.
881 [self startCKKSSubsystem];
882
883 [self.keychainView.viewSetupOperation waitUntilFinished];
884 // Reset setup, since that's the most likely state to be in (33866282)
885 [self.keychainView resetSetup];
886
887 CKRecord* ckr = [self createFakeRecord: self.keychainZoneID recordName:@"7B598D31-F9C5-481E-98AC-5A507ACB2D85"];
888 [self.keychainZone addToZone: ckr];
889
890 XCTAssertNotNil(self.keychainZone.currentDatabase, "Zone exists");
891 XCTAssertNotNil(self.keychainZone.currentDatabase[ckr.recordID], "An item exists in the fake zone");
892
893 dispatch_semaphore_t resetSemaphore = dispatch_semaphore_create(0);
894 [self.injectedManager rpcResetCloudKit:nil reply:^(NSError* result) {
895 XCTAssertNil(result, "no error resetting cloudkit");
896 secnotice("ckks", "Received a resetCloudKit callback");
897 dispatch_semaphore_signal(resetSemaphore);
898 }];
899
900 XCTAssertEqual(0, dispatch_semaphore_wait(resetSemaphore, 400*NSEC_PER_SEC), "Semaphore wait did not time out");
901
902 XCTAssertNil(self.keychainZone.currentDatabase, "No zone anymore!");
903 OCMVerifyAllWithDelay(self.mockDatabase, 8);
904
905 // Now log in, and see what happens! It should create the zone again and upload a whole new key hierarchy
906 self.silentFetchesAllowed = true;
907 self.circleStatus = kSOSCCInCircle;
908 [self.accountStateTracker notifyCircleStatusChangeAndWaitForSignal];
909
910 [self expectCKModifyKeyRecords: 3 currentKeyPointerRecords: 3 tlkShareRecords: 1 zoneID:self.keychainZoneID];
911 OCMVerifyAllWithDelay(self.mockDatabase, 8);
912
913 [self expectCKModifyItemRecords: 1 currentKeyPointerRecords: 1 zoneID:self.keychainZoneID checkItem:[self checkClassABlock:self.keychainZoneID message:@"Object was encrypted under class A key in hierarchy"]];
914 [self addGenericPassword:@"asdf"
915 account:@"account-class-A"
916 viewHint:nil
917 access:(id)kSecAttrAccessibleWhenUnlocked
918 expecting:errSecSuccess
919 message:@"Adding class A item"];
920 OCMVerifyAllWithDelay(self.mockDatabase, 8);
921 }
922
923 - (void)testRPCTLKMissingWhenMissing {
924 // Bring CKKS up in waitfortlk
925 [self putFakeKeyHierarchyInCloudKit:self.keychainZoneID];
926 [self startCKKSSubsystem];
927
928 XCTAssertEqual(0, [self.keychainView.keyHierarchyConditions[SecCKKSZoneKeyStateWaitForTLK] wait:8*NSEC_PER_SEC], "CKKS entered waitfortlk");
929
930 XCTestExpectation* callbackOccurs = [self expectationWithDescription:@"callback-occurs"];
931
932 [self.ckksControl rpcTLKMissing:@"keychain" reply:^(bool missing) {
933 XCTAssertTrue(missing, "TLKs should be missing");
934 [callbackOccurs fulfill];
935 }];
936
937 [self waitForExpectations:@[callbackOccurs] timeout:5.0];
938 }
939
940 - (void)testRPCTLKMissingWhenFound {
941 // Bring CKKS up in waitfortlk
942 [self putFakeKeyHierarchyInCloudKit:self.keychainZoneID];
943 [self saveTLKMaterialToKeychain:self.keychainZoneID];
944 [self startCKKSSubsystem];
945
946 XCTAssertEqual(0, [self.keychainView.keyHierarchyConditions[SecCKKSZoneKeyStateReady] wait:8*NSEC_PER_SEC], "CKKS entered 'ready''");
947
948 XCTestExpectation* callbackOccurs = [self expectationWithDescription:@"callback-occurs"];
949
950 [self.ckksControl rpcTLKMissing:@"keychain" reply:^(bool missing) {
951 XCTAssertFalse(missing, "TLKs should not be missing");
952 [callbackOccurs fulfill];
953 }];
954
955 [self waitForExpectations:@[callbackOccurs] timeout:5.0];
956 }
957
958 @end
959
960 #endif // OCTAGON