]> git.saurik.com Git - apple/security.git/blob - keychain/ckks/tests/CKKSTests+API.m
ec3659ff2f2527d6b5487c7c9497a09251f709d1
[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/tests/MockCloudKit.h"
45
46 #import "keychain/ckks/tests/CKKSTests.h"
47 #import "keychain/ckks/tests/CKKSTests+API.h"
48
49 @implementation CloudKitKeychainSyncingTests (APITests)
50
51 - (void)testSecuritydClientBringup {
52 CFErrorRef cferror = nil;
53 xpc_endpoint_t endpoint = SecCreateSecuritydXPCServerEndpoint(&cferror);
54 XCTAssertNil((__bridge id)cferror, "No error creating securityd endpoint");
55 XCTAssertNotNil(endpoint, "Received securityd endpoint");
56
57 NSXPCInterface *interface = [NSXPCInterface interfaceWithProtocol:@protocol(SecuritydXPCProtocol)];
58 [SecuritydXPCClient configureSecuritydXPCProtocol: interface];
59 XCTAssertNotNil(interface, "Received a configured CKKS interface");
60
61 NSXPCListenerEndpoint *listenerEndpoint = [[NSXPCListenerEndpoint alloc] init];
62 [listenerEndpoint _setEndpoint:endpoint];
63
64 NSXPCConnection* connection = [[NSXPCConnection alloc] initWithListenerEndpoint:listenerEndpoint];
65 XCTAssertNotNil(connection , "Received an active connection");
66
67 connection.remoteObjectInterface = interface;
68 }
69
70 -(NSMutableDictionary*)pcsAddItemQuery:(NSString*)account
71 data:(NSData*)data
72 serviceIdentifier:(NSNumber*)serviceIdentifier
73 publicKey:(NSData*)publicKey
74 publicIdentity:(NSData*)publicIdentity
75 {
76 return [@{
77 (id)kSecClass : (id)kSecClassGenericPassword,
78 (id)kSecReturnPersistentRef: @YES,
79 (id)kSecReturnAttributes: @YES,
80 (id)kSecAttrAccessGroup : @"com.apple.security.ckks",
81 (id)kSecAttrAccessible: (id)kSecAttrAccessibleAfterFirstUnlock,
82 (id)kSecAttrAccount : account,
83 (id)kSecAttrSynchronizable : (id)kCFBooleanTrue,
84 (id)kSecValueData : data,
85 (id)kSecAttrDeriveSyncIDFromItemAttributes : (id)kCFBooleanTrue,
86 (id)kSecAttrPCSPlaintextServiceIdentifier : serviceIdentifier,
87 (id)kSecAttrPCSPlaintextPublicKey : publicKey,
88 (id)kSecAttrPCSPlaintextPublicIdentity : publicIdentity,
89 } mutableCopy];
90 }
91
92 -(NSDictionary*)pcsAddItem:(NSString*)account
93 data:(NSData*)data
94 serviceIdentifier:(NSNumber*)serviceIdentifier
95 publicKey:(NSData*)publicKey
96 publicIdentity:(NSData*)publicIdentity
97 expectingSync:(bool)expectingSync
98 {
99 NSMutableDictionary* query = [self pcsAddItemQuery:account
100 data:data
101 serviceIdentifier:(NSNumber*)serviceIdentifier
102 publicKey:(NSData*)publicKey
103 publicIdentity:(NSData*)publicIdentity];
104 CFTypeRef result = NULL;
105 XCTestExpectation* syncExpectation = [self expectationWithDescription: @"callback occurs"];
106
107 XCTAssertEqual(errSecSuccess, _SecItemAddAndNotifyOnSync((__bridge CFDictionaryRef) query, &result, ^(bool didSync, CFErrorRef error) {
108 if(expectingSync) {
109 XCTAssertTrue(didSync, "Item synced");
110 XCTAssertNil((__bridge NSError*)error, "No error syncing item");
111 } else {
112 XCTAssertFalse(didSync, "Item did not sync");
113 XCTAssertNotNil((__bridge NSError*)error, "Error syncing item");
114 }
115
116 [syncExpectation fulfill];
117 }), @"_SecItemAddAndNotifyOnSync succeeded");
118
119 // Verify that the item was written to CloudKit
120 OCMVerifyAllWithDelay(self.mockDatabase, 8);
121
122 // In real code, you'd need to wait for the _SecItemAddAndNotifyOnSync callback to succeed before proceeding
123 [self waitForExpectations:@[syncExpectation] timeout:8.0];
124
125 return (NSDictionary*) CFBridgingRelease(result);
126 }
127
128
129 - (void)testAddAndNotifyOnSync {
130 [self createAndSaveFakeKeyHierarchy: self.keychainZoneID]; // Make life easy for this test.
131
132 [self expectCKModifyItemRecords: 1 currentKeyPointerRecords: 1 zoneID:self.keychainZoneID];
133 [self startCKKSSubsystem];
134
135 // Let things shake themselves out.
136 [self.keychainView waitForKeyHierarchyReadiness];
137 [self waitForCKModifications];
138
139 NSMutableDictionary* query = [@{
140 (id)kSecClass : (id)kSecClassGenericPassword,
141 (id)kSecAttrAccessGroup : @"com.apple.security.ckks",
142 (id)kSecAttrAccessible: (id)kSecAttrAccessibleAfterFirstUnlock,
143 (id)kSecAttrAccount : @"testaccount",
144 (id)kSecAttrSynchronizable : (id)kCFBooleanTrue,
145 (id)kSecValueData : (id) [@"asdf" dataUsingEncoding:NSUTF8StringEncoding],
146 } mutableCopy];
147
148 XCTestExpectation* blockExpectation = [self expectationWithDescription: @"callback occurs"];
149
150 XCTAssertEqual(errSecSuccess, _SecItemAddAndNotifyOnSync((__bridge CFDictionaryRef) query, NULL, ^(bool didSync, CFErrorRef error) {
151 XCTAssertTrue(didSync, "Item synced properly");
152 XCTAssertNil((__bridge NSError*)error, "No error syncing item");
153
154 [blockExpectation fulfill];
155 }), @"_SecItemAddAndNotifyOnSync succeeded");
156
157 [self waitForExpectationsWithTimeout:5.0 handler:nil];
158 }
159
160 - (void)testAddAndNotifyOnSyncFailure {
161 [self createAndSaveFakeKeyHierarchy: self.keychainZoneID]; // Make life easy for this test.
162
163 [self startCKKSSubsystem];
164 [self.keychainView waitForFetchAndIncomingQueueProcessing];
165
166 // Due to item UUID selection, this item will be added with UUID DD7C2F9B-B22D-3B90-C299-E3B48174BFA3.
167 // Add it to CloudKit first!
168 CKRecord* ckr = [self createFakeRecord: self.keychainZoneID recordName:@"DD7C2F9B-B22D-3B90-C299-E3B48174BFA3"];
169 [self.keychainZone addToZone: ckr];
170
171
172 // Go for it!
173 [self expectCKAtomicModifyItemRecordsUpdateFailure: self.keychainZoneID];
174
175 NSMutableDictionary* query = [@{
176 (id)kSecClass : (id)kSecClassGenericPassword,
177 (id)kSecAttrAccessGroup : @"com.apple.security.ckks",
178 (id)kSecAttrAccessible: (id)kSecAttrAccessibleAfterFirstUnlock,
179 (id)kSecAttrAccount : @"testaccount",
180 (id)kSecAttrSynchronizable : (id)kCFBooleanTrue,
181 (id)kSecValueData : (id) [@"asdf" dataUsingEncoding:NSUTF8StringEncoding],
182 } mutableCopy];
183
184 XCTestExpectation* blockExpectation = [self expectationWithDescription: @"callback occurs"];
185
186 XCTAssertEqual(errSecSuccess, _SecItemAddAndNotifyOnSync((__bridge CFDictionaryRef) query, NULL, ^(bool didSync, CFErrorRef error) {
187 XCTAssertFalse(didSync, "Item did not sync (as expected)");
188 XCTAssertNotNil((__bridge NSError*)error, "error exists when item fails to sync");
189
190 [blockExpectation fulfill];
191 }), @"_SecItemAddAndNotifyOnSync succeeded");
192
193 [self waitForExpectationsWithTimeout:5.0 handler:nil];
194 [self waitForCKModifications];
195 }
196
197 - (void)testAddAndNotifyOnSyncLoggedOut {
198 // Test starts with nothing in database and the user logged out of CloudKit. We expect no CKKS operations.
199 self.accountStatus = CKAccountStatusNoAccount;
200 self.silentFetchesAllowed = false;
201 [self startCKKSSubsystem];
202
203 NSMutableDictionary* query = [@{
204 (id)kSecClass : (id)kSecClassGenericPassword,
205 (id)kSecAttrAccessGroup : @"com.apple.security.ckks",
206 (id)kSecAttrAccessible: (id)kSecAttrAccessibleAfterFirstUnlock,
207 (id)kSecAttrAccount : @"testaccount",
208 (id)kSecAttrSynchronizable : (id)kCFBooleanTrue,
209 (id)kSecValueData : (id) [@"asdf" dataUsingEncoding:NSUTF8StringEncoding],
210 } mutableCopy];
211
212 XCTestExpectation* blockExpectation = [self expectationWithDescription: @"callback occurs"];
213
214 XCTAssertEqual(errSecSuccess, _SecItemAddAndNotifyOnSync((__bridge CFDictionaryRef) query, NULL, ^(bool didSync, CFErrorRef error) {
215 XCTAssertFalse(didSync, "Item did not sync (with no iCloud account)");
216 XCTAssertNotNil((__bridge NSError*)error, "Error exists syncing item while logged out");
217
218 [blockExpectation fulfill];
219 }), @"_SecItemAddAndNotifyOnSync succeeded");
220
221 [self waitForExpectationsWithTimeout:5.0 handler:nil];
222 }
223
224 - (BOOL (^) (CKRecord*)) checkPCSFieldsBlock: (CKRecordZoneID*) zoneID
225 PCSServiceIdentifier:(NSNumber*)servIdentifier
226 PCSPublicKey:(NSData*)publicKey
227 PCSPublicIdentity:(NSData*)publicIdentity
228 {
229 __weak __typeof(self) weakSelf = self;
230 return ^BOOL(CKRecord* record) {
231 __strong __typeof(weakSelf) strongSelf = weakSelf;
232 XCTAssertNotNil(strongSelf, "self exists");
233
234 XCTAssert([record[SecCKRecordPCSServiceIdentifier] isEqual: servIdentifier], "PCS Service identifier matches input");
235 XCTAssert([record[SecCKRecordPCSPublicKey] isEqual: publicKey], "PCS Public Key matches input");
236 XCTAssert([record[SecCKRecordPCSPublicIdentity] isEqual: publicIdentity], "PCS Public Identity matches input");
237
238 if([record[SecCKRecordPCSServiceIdentifier] isEqual: servIdentifier] &&
239 [record[SecCKRecordPCSPublicKey] isEqual: publicKey] &&
240 [record[SecCKRecordPCSPublicIdentity] isEqual: publicIdentity]) {
241 return YES;
242 } else {
243 return NO;
244 }
245 };
246 }
247
248 - (void)testPCSUnencryptedFieldsAdd {
249
250 [self createAndSaveFakeKeyHierarchy: self.keychainZoneID]; // Make life easy for this test.
251
252 [self startCKKSSubsystem];
253 [self.keychainView waitUntilAllOperationsAreFinished];
254
255 NSNumber* servIdentifier = @3;
256 NSData* publicKey = [@"asdfasdf" dataUsingEncoding:NSUTF8StringEncoding];
257 NSData* publicIdentity = [@"somedata" dataUsingEncoding:NSUTF8StringEncoding];
258
259 [self expectCKModifyItemRecords: 1 currentKeyPointerRecords: 1 zoneID:self.keychainZoneID
260 checkItem: [self checkPCSFieldsBlock:self.keychainZoneID
261 PCSServiceIdentifier:(NSNumber *)servIdentifier
262 PCSPublicKey:publicKey
263 PCSPublicIdentity:publicIdentity]];
264
265 NSMutableDictionary* query = [@{
266 (id)kSecClass : (id)kSecClassGenericPassword,
267 (id)kSecAttrAccessGroup : @"com.apple.security.ckks",
268 (id)kSecAttrAccessible: (id)kSecAttrAccessibleAfterFirstUnlock,
269 (id)kSecAttrAccount : @"testaccount",
270 (id)kSecAttrSynchronizable : (id)kCFBooleanTrue,
271 (id)kSecValueData : (id) [@"asdf" dataUsingEncoding:NSUTF8StringEncoding],
272 (id)kSecAttrDeriveSyncIDFromItemAttributes : (id)kCFBooleanTrue,
273 (id)kSecAttrPCSPlaintextServiceIdentifier : servIdentifier,
274 (id)kSecAttrPCSPlaintextPublicKey : publicKey,
275 (id)kSecAttrPCSPlaintextPublicIdentity : publicIdentity,
276 } mutableCopy];
277
278 XCTAssertEqual(errSecSuccess, SecItemAdd((__bridge CFDictionaryRef) query, NULL), @"SecItemAdd succeeded");
279
280 // Verify that the item is written to CloudKit
281 OCMVerifyAllWithDelay(self.mockDatabase, 4);
282
283 CFTypeRef item = NULL;
284 query[(id)kSecValueData] = nil;
285 query[(id)kSecReturnAttributes] = @YES;
286 XCTAssertEqual(errSecSuccess, SecItemCopyMatching((__bridge CFDictionaryRef) query, &item), "item should still exist");
287
288 NSDictionary* itemAttributes = (NSDictionary*) CFBridgingRelease(item);
289 XCTAssertEqualObjects(itemAttributes[(id)kSecAttrPCSPlaintextServiceIdentifier], servIdentifier, "Service Identifier exists");
290 XCTAssertEqualObjects(itemAttributes[(id)kSecAttrPCSPlaintextPublicKey], publicKey, "public key exists");
291 XCTAssertEqualObjects(itemAttributes[(id)kSecAttrPCSPlaintextPublicIdentity], publicIdentity, "public identity exists");
292
293 // Find the item record in CloudKit. Since we're using kSecAttrDeriveSyncIDFromItemAttributes,
294 // the record ID is likely DD7C2F9B-B22D-3B90-C299-E3B48174BFA3
295 [self waitForCKModifications];
296 CKRecordID* recordID = [[CKRecordID alloc] initWithRecordName: @"DD7C2F9B-B22D-3B90-C299-E3B48174BFA3" zoneID:self.keychainZoneID];
297 CKRecord* record = self.keychainZone.currentDatabase[recordID];
298 XCTAssertNotNil(record, "Found record in CloudKit at expected UUID");
299
300 XCTAssertEqualObjects(record[SecCKRecordPCSServiceIdentifier], servIdentifier, "Service identifier sent to cloudkit");
301 XCTAssertEqualObjects(record[SecCKRecordPCSPublicKey], publicKey, "public key sent to cloudkit");
302 XCTAssertEqualObjects(record[SecCKRecordPCSPublicIdentity], publicIdentity, "public identity sent to cloudkit");
303 }
304
305 - (void)testPCSUnencryptedFieldsModify {
306 [self createAndSaveFakeKeyHierarchy: self.keychainZoneID]; // Make life easy for this test.
307
308 [self startCKKSSubsystem];
309 [self.keychainView waitUntilAllOperationsAreFinished];
310
311 NSNumber* servIdentifier = @3;
312 NSData* publicKey = [@"asdfasdf" dataUsingEncoding:NSUTF8StringEncoding];
313 NSData* publicIdentity = [@"somedata" dataUsingEncoding:NSUTF8StringEncoding];
314
315 [self expectCKModifyItemRecords: 1 currentKeyPointerRecords: 1 zoneID:self.keychainZoneID
316 checkItem: [self checkPCSFieldsBlock:self.keychainZoneID
317 PCSServiceIdentifier:(NSNumber *)servIdentifier
318 PCSPublicKey:publicKey
319 PCSPublicIdentity:publicIdentity]];
320
321 NSMutableDictionary* query = [@{
322 (id)kSecClass : (id)kSecClassGenericPassword,
323 (id)kSecAttrAccessGroup : @"com.apple.security.ckks",
324 (id)kSecAttrAccessible: (id)kSecAttrAccessibleAfterFirstUnlock,
325 (id)kSecAttrAccount : @"testaccount",
326 (id)kSecAttrSynchronizable : (id)kCFBooleanTrue,
327 (id)kSecValueData : (id) [@"asdf" dataUsingEncoding:NSUTF8StringEncoding],
328 (id)kSecAttrDeriveSyncIDFromItemAttributes : (id)kCFBooleanTrue,
329 (id)kSecAttrPCSPlaintextServiceIdentifier : servIdentifier,
330 (id)kSecAttrPCSPlaintextPublicKey : publicKey,
331 (id)kSecAttrPCSPlaintextPublicIdentity : publicIdentity,
332 } mutableCopy];
333
334 XCTAssertEqual(errSecSuccess, SecItemAdd((__bridge CFDictionaryRef) query, NULL), @"SecItemAdd succeeded");
335
336 OCMVerifyAllWithDelay(self.mockDatabase, 8);
337 [self waitForCKModifications];
338
339 query[(id)kSecValueData] = nil;
340 query[(id)kSecAttrPCSPlaintextServiceIdentifier] = nil;
341 query[(id)kSecAttrPCSPlaintextPublicKey] = nil;
342 query[(id)kSecAttrPCSPlaintextPublicIdentity] = nil;
343
344 servIdentifier = @1;
345 publicKey = [@"new public key" dataUsingEncoding:NSUTF8StringEncoding];
346
347 NSNumber* newServiceIdentifier = @10;
348 NSData* newPublicKey = [@"new public key" dataUsingEncoding:NSUTF8StringEncoding];
349 NSData* newPublicIdentity = [@"new public identity" dataUsingEncoding:NSUTF8StringEncoding];
350
351 NSDictionary* update = @{
352 (id)kSecAttrPCSPlaintextServiceIdentifier : newServiceIdentifier,
353 (id)kSecAttrPCSPlaintextPublicKey : newPublicKey,
354 (id)kSecAttrPCSPlaintextPublicIdentity : newPublicIdentity,
355 };
356
357 [self expectCKModifyItemRecords: 1 currentKeyPointerRecords: 1 zoneID:self.keychainZoneID
358 checkItem: [self checkPCSFieldsBlock:self.keychainZoneID
359 PCSServiceIdentifier:(NSNumber *)newServiceIdentifier
360 PCSPublicKey:newPublicKey
361 PCSPublicIdentity:newPublicIdentity]];
362
363 XCTAssertEqual(errSecSuccess, SecItemUpdate((__bridge CFDictionaryRef) query, (__bridge CFDictionaryRef) update), @"SecItemUpdate succeeded");
364 OCMVerifyAllWithDelay(self.mockDatabase, 8);
365
366 CFTypeRef item = NULL;
367 query[(id)kSecValueData] = nil;
368 query[(id)kSecReturnAttributes] = @YES;
369 XCTAssertEqual(errSecSuccess, SecItemCopyMatching((__bridge CFDictionaryRef) query, &item), "item should still exist");
370
371 NSDictionary* itemAttributes = (NSDictionary*) CFBridgingRelease(item);
372 XCTAssertEqualObjects(itemAttributes[(id)kSecAttrPCSPlaintextServiceIdentifier], newServiceIdentifier, "Service Identifier exists");
373 XCTAssertEqualObjects(itemAttributes[(id)kSecAttrPCSPlaintextPublicKey], newPublicKey, "public key exists");
374 XCTAssertEqualObjects(itemAttributes[(id)kSecAttrPCSPlaintextPublicIdentity], newPublicIdentity, "public identity exists");
375
376 // Find the item record in CloudKit. Since we're using kSecAttrDeriveSyncIDFromItemAttributes,
377 // the record ID is likely DD7C2F9B-B22D-3B90-C299-E3B48174BFA3
378 [self waitForCKModifications];
379 CKRecordID* recordID = [[CKRecordID alloc] initWithRecordName: @"DD7C2F9B-B22D-3B90-C299-E3B48174BFA3" zoneID:self.keychainZoneID];
380 CKRecord* record = self.keychainZone.currentDatabase[recordID];
381 XCTAssertNotNil(record, "Found record in CloudKit at expected UUID");
382
383 XCTAssertEqualObjects(record[SecCKRecordPCSServiceIdentifier], newServiceIdentifier, "Service identifier sent to cloudkit");
384 XCTAssertEqualObjects(record[SecCKRecordPCSPublicKey], newPublicKey, "public key sent to cloudkit");
385 XCTAssertEqualObjects(record[SecCKRecordPCSPublicIdentity], newPublicIdentity, "public identity sent to cloudkit");
386 }
387
388 // As of [<rdar://problem/32558310> CKKS: Re-authenticate PCSPublicFields], these fields are NOT server-modifiable. This test proves it.
389 - (void)testPCSUnencryptedFieldsServerModifyFail {
390 [self createAndSaveFakeKeyHierarchy: self.keychainZoneID]; // Make life easy for this test.
391
392 [self startCKKSSubsystem];
393 [self.keychainView waitForKeyHierarchyReadiness];
394
395 NSNumber* servIdentifier = @3;
396 NSData* publicKey = [@"asdfasdf" dataUsingEncoding:NSUTF8StringEncoding];
397 NSData* publicIdentity = [@"somedata" dataUsingEncoding:NSUTF8StringEncoding];
398
399 [self expectCKModifyItemRecords: 1 currentKeyPointerRecords: 1 zoneID:self.keychainZoneID
400 checkItem: [self checkPCSFieldsBlock:self.keychainZoneID
401 PCSServiceIdentifier:(NSNumber *)servIdentifier
402 PCSPublicKey:publicKey
403 PCSPublicIdentity:publicIdentity]];
404
405 NSMutableDictionary* query = [@{
406 (id)kSecClass : (id)kSecClassGenericPassword,
407 (id)kSecAttrAccessGroup : @"com.apple.security.ckks",
408 (id)kSecAttrAccessible: (id)kSecAttrAccessibleAfterFirstUnlock,
409 (id)kSecAttrAccount : @"testaccount",
410 (id)kSecAttrSynchronizable : (id)kCFBooleanTrue,
411 (id)kSecValueData : (id) [@"asdf" dataUsingEncoding:NSUTF8StringEncoding],
412 (id)kSecAttrDeriveSyncIDFromItemAttributes : (id)kCFBooleanTrue,
413 (id)kSecAttrPCSPlaintextServiceIdentifier : servIdentifier,
414 (id)kSecAttrPCSPlaintextPublicKey : publicKey,
415 (id)kSecAttrPCSPlaintextPublicIdentity : publicIdentity,
416 } mutableCopy];
417
418 XCTAssertEqual(errSecSuccess, SecItemAdd((__bridge CFDictionaryRef) query, NULL), @"SecItemAdd succeeded");
419
420 OCMVerifyAllWithDelay(self.mockDatabase, 4);
421 [self waitForCKModifications];
422
423 // Find the item record in CloudKit. Since we're using kSecAttrDeriveSyncIDFromItemAttributes,
424 // the record ID is likely DD7C2F9B-B22D-3B90-C299-E3B48174BFA3
425 CKRecordID* recordID = [[CKRecordID alloc] initWithRecordName: @"DD7C2F9B-B22D-3B90-C299-E3B48174BFA3" zoneID:self.keychainZoneID];
426 CKRecord* record = self.keychainZone.currentDatabase[recordID];
427 XCTAssertNotNil(record, "Found record in CloudKit at expected UUID");
428
429 // Items are encrypted using encv2
430 XCTAssertEqualObjects(record[SecCKRecordEncryptionVersionKey], [NSNumber numberWithInteger:(int) CKKSItemEncryptionVersion2], "Uploaded using encv2");
431
432 if(!record) {
433 // Test has already failed; find the record just to be nice.
434 for(CKRecord* maybe in self.keychainZone.currentDatabase.allValues) {
435 if(maybe[SecCKRecordPCSServiceIdentifier] != nil) {
436 record = maybe;
437 }
438 }
439 }
440
441 NSNumber* newServiceIdentifier = @10;
442 NSData* newPublicKey = [@"new public key" dataUsingEncoding:NSUTF8StringEncoding];
443 NSData* newPublicIdentity = [@"new public identity" dataUsingEncoding:NSUTF8StringEncoding];
444
445 // Change the public key and public identity
446 record = [record copyWithZone: nil];
447 record[SecCKRecordPCSServiceIdentifier] = newServiceIdentifier;
448 record[SecCKRecordPCSPublicKey] = newPublicKey;
449 record[SecCKRecordPCSPublicIdentity] = newPublicIdentity;
450 [self.keychainZone addToZone: record];
451
452 // Trigger a notification
453 [self.keychainView notifyZoneChange:nil];
454 [self.keychainView waitForFetchAndIncomingQueueProcessing];
455
456 CFTypeRef item = NULL;
457 query[(id)kSecValueData] = nil;
458 query[(id)kSecAttrPCSPlaintextServiceIdentifier] = nil;
459 query[(id)kSecAttrPCSPlaintextPublicKey] = nil;
460 query[(id)kSecAttrPCSPlaintextPublicIdentity] = nil;
461 query[(id)kSecReturnAttributes] = @YES;
462 XCTAssertEqual(errSecSuccess, SecItemCopyMatching((__bridge CFDictionaryRef) query, &item), "item should still exist");
463
464 NSDictionary* itemAttributes = (NSDictionary*) CFBridgingRelease(item);
465 XCTAssertEqualObjects(itemAttributes[(id)kSecAttrPCSPlaintextServiceIdentifier], servIdentifier, "service identifier is not updated");
466 XCTAssertEqualObjects(itemAttributes[(id)kSecAttrPCSPlaintextPublicKey], publicKey, "public key not updated");
467 XCTAssertEqualObjects(itemAttributes[(id)kSecAttrPCSPlaintextPublicIdentity], publicIdentity, "public identity not updated");
468 }
469
470 -(void)testPCSUnencryptedFieldsRecieveUnauthenticatedFields {
471 [self createAndSaveFakeKeyHierarchy: self.keychainZoneID]; // Make life easy for this test.
472
473 [self startCKKSSubsystem];
474 [self.keychainView waitForKeyHierarchyReadiness];
475
476 NSNumber* servIdentifier = @3;
477 NSData* publicKey = [@"asdfasdf" dataUsingEncoding:NSUTF8StringEncoding];
478 NSData* publicIdentity = [@"somedata" dataUsingEncoding:NSUTF8StringEncoding];
479
480 NSError* error = nil;
481
482 // Manually encrypt an item
483 NSString* recordName = @"7B598D31-F9C5-481E-98AC-5A507ACB2D85";
484 CKRecordID* recordID = [[CKRecordID alloc] initWithRecordName:recordName zoneID:self.keychainZoneID];
485 NSDictionary* item = [self fakeRecordDictionary: @"account-delete-me" zoneID:self.keychainZoneID];
486 CKKSItem* cipheritem = [[CKKSItem alloc] initWithUUID:recordID.recordName
487 parentKeyUUID:self.keychainZoneKeys.classC.uuid
488 zoneID:recordID.zoneID];
489 CKKSKey* itemkey = [CKKSKey randomKeyWrappedByParent: self.keychainZoneKeys.classC error:&error];
490 XCTAssertNotNil(itemkey, "Got a key");
491 cipheritem.wrappedkey = itemkey.wrappedkey;
492 XCTAssertNotNil(cipheritem.wrappedkey, "Got a wrapped key");
493
494 cipheritem.encver = CKKSItemEncryptionVersion1;
495
496 // This item has the PCS public fields, but they are not authenticated
497 cipheritem.plaintextPCSServiceIdentifier = servIdentifier;
498 cipheritem.plaintextPCSPublicKey = publicKey;
499 cipheritem.plaintextPCSPublicIdentity = publicIdentity;
500
501 NSDictionary<NSString*, NSData*>* authenticatedData = [cipheritem makeAuthenticatedDataDictionaryUpdatingCKKSItem: nil encryptionVersion:CKKSItemEncryptionVersion1];
502 cipheritem.encitem = [CKKSItemEncrypter encryptDictionary:item key:itemkey.aessivkey authenticatedData:authenticatedData error:&error];
503 XCTAssertNil(error, "no error encrypting object");
504 XCTAssertNotNil(cipheritem.encitem, "Recieved ciphertext");
505
506 [self.keychainZone addToZone:[cipheritem CKRecordWithZoneID: recordID.zoneID]];
507
508 [self.keychainView waitForFetchAndIncomingQueueProcessing];
509
510 NSDictionary* query = @{(id)kSecClass: (id)kSecClassGenericPassword,
511 (id)kSecReturnAttributes: @YES,
512 (id)kSecAttrSynchronizable: @YES,
513 (id)kSecAttrAccount: @"account-delete-me",
514 (id)kSecMatchLimit: (id)kSecMatchLimitOne,
515 };
516 CFTypeRef cfresult = NULL;
517 XCTAssertEqual(errSecSuccess, SecItemCopyMatching((__bridge CFDictionaryRef) query, &cfresult), "Found synced item");
518
519 NSDictionary* result = CFBridgingRelease(cfresult);
520 XCTAssertEqualObjects(result[(id)kSecAttrPCSPlaintextServiceIdentifier], servIdentifier, "Received PCS service identifier");
521 XCTAssertEqualObjects(result[(id)kSecAttrPCSPlaintextPublicKey], publicKey, "Received PCS public key");
522 XCTAssertEqualObjects(result[(id)kSecAttrPCSPlaintextPublicIdentity], publicIdentity, "Received PCS public identity");
523 }
524
525 -(void)testPCSUnencryptedFieldsRecieveAuthenticatedFields {
526 [self createAndSaveFakeKeyHierarchy: self.keychainZoneID]; // Make life easy for this test.
527
528 [self startCKKSSubsystem];
529 [self.keychainView waitForKeyHierarchyReadiness];
530
531 NSNumber* servIdentifier = @3;
532 NSData* publicKey = [@"asdfasdf" dataUsingEncoding:NSUTF8StringEncoding];
533 NSData* publicIdentity = [@"somedata" dataUsingEncoding:NSUTF8StringEncoding];
534
535 NSError* error = nil;
536
537 // Manually encrypt an item
538 NSString* recordName = @"7B598D31-F9C5-481E-98AC-5A507ACB2D85";
539 CKRecordID* recordID = [[CKRecordID alloc] initWithRecordName:recordName zoneID:self.keychainZoneID];
540 NSDictionary* item = [self fakeRecordDictionary: @"account-delete-me" zoneID:self.keychainZoneID];
541 CKKSItem* cipheritem = [[CKKSItem alloc] initWithUUID:recordID.recordName
542 parentKeyUUID:self.keychainZoneKeys.classC.uuid
543 zoneID:recordID.zoneID];
544 CKKSKey* itemkey = [CKKSKey randomKeyWrappedByParent: self.keychainZoneKeys.classC error:&error];
545 XCTAssertNotNil(itemkey, "Got a key");
546 cipheritem.wrappedkey = itemkey.wrappedkey;
547 XCTAssertNotNil(cipheritem.wrappedkey, "Got a wrapped key");
548
549 cipheritem.encver = CKKSItemEncryptionVersion2;
550
551 // This item has the PCS public fields, and they are authenticated (since we're using v2)
552 cipheritem.plaintextPCSServiceIdentifier = servIdentifier;
553 cipheritem.plaintextPCSPublicKey = publicKey;
554 cipheritem.plaintextPCSPublicIdentity = publicIdentity;
555
556 // Use version 2, so PCS plaintext fields will be authenticated
557 NSMutableDictionary<NSString*, NSData*>* authenticatedData = [[cipheritem makeAuthenticatedDataDictionaryUpdatingCKKSItem: nil encryptionVersion:CKKSItemEncryptionVersion2] mutableCopy];
558
559 cipheritem.encitem = [CKKSItemEncrypter encryptDictionary:item key:itemkey.aessivkey authenticatedData:authenticatedData error:&error];
560 XCTAssertNil(error, "no error encrypting object");
561 XCTAssertNotNil(cipheritem.encitem, "Recieved ciphertext");
562
563 [self.keychainZone addToZone:[cipheritem CKRecordWithZoneID: recordID.zoneID]];
564
565 [self.keychainView waitForFetchAndIncomingQueueProcessing];
566
567 NSDictionary* query = @{(id)kSecClass: (id)kSecClassGenericPassword,
568 (id)kSecReturnAttributes: @YES,
569 (id)kSecAttrSynchronizable: @YES,
570 (id)kSecAttrAccount: @"account-delete-me",
571 (id)kSecMatchLimit: (id)kSecMatchLimitOne,
572 };
573 CFTypeRef cfresult = NULL;
574 XCTAssertEqual(errSecSuccess, SecItemCopyMatching((__bridge CFDictionaryRef) query, &cfresult), "Found synced item");
575
576 NSDictionary* result = CFBridgingRelease(cfresult);
577 XCTAssertEqualObjects(result[(id)kSecAttrPCSPlaintextServiceIdentifier], servIdentifier, "Received PCS service identifier");
578 XCTAssertEqualObjects(result[(id)kSecAttrPCSPlaintextPublicKey], publicKey, "Received PCS public key");
579 XCTAssertEqualObjects(result[(id)kSecAttrPCSPlaintextPublicIdentity], publicIdentity, "Received PCS public identity");
580
581 // Test that if this item is updated, it remains encrypted in v2
582 [self expectCKModifyItemRecords: 1 currentKeyPointerRecords: 1 zoneID:self.keychainZoneID
583 checkItem: [self checkPCSFieldsBlock:self.keychainZoneID
584 PCSServiceIdentifier:(NSNumber *)servIdentifier
585 PCSPublicKey:publicKey
586 PCSPublicIdentity:publicIdentity]];
587 [self updateGenericPassword:@"different password" account:@"account-delete-me"];
588
589 OCMVerifyAllWithDelay(self.mockDatabase, 4);
590 [self waitForCKModifications];
591
592 CKRecord* newRecord = self.keychainZone.currentDatabase[recordID];
593 XCTAssertEqualObjects(newRecord[SecCKRecordPCSServiceIdentifier], servIdentifier, "Didn't change service identifier");
594 XCTAssertEqualObjects(newRecord[SecCKRecordPCSPublicKey], publicKey, "Didn't change public key");
595 XCTAssertEqualObjects(newRecord[SecCKRecordPCSPublicIdentity], publicIdentity, "Didn't change public identity");
596 XCTAssertEqualObjects(newRecord[SecCKRecordEncryptionVersionKey], [NSNumber numberWithInteger:(int) CKKSItemEncryptionVersion2], "Uploaded using encv2");
597 }
598
599 -(void)testResetLocal {
600 // Test starts with nothing in database, but one in our fake CloudKit.
601 [self putFakeKeyHierarchyInCloudKit:self.keychainZoneID];
602 [self saveTLKMaterialToKeychainSimulatingSOS:self.keychainZoneID];
603
604 // Spin up CKKS subsystem.
605 [self startCKKSSubsystem];
606
607 // We expect a single record to be uploaded
608 [self expectCKModifyItemRecords: 1 currentKeyPointerRecords: 1 zoneID:self.keychainZoneID checkItem: [self checkClassCBlock:self.keychainZoneID message:@"Object was encrypted under class C key in hierarchy"]];
609 [self addGenericPassword: @"data" account: @"account-delete-me"];
610 OCMVerifyAllWithDelay(self.mockDatabase, 8);
611
612 // After the local reset, we expect: a fetch, then nothing
613 self.silentFetchesAllowed = false;
614 [self expectCKFetch];
615
616 dispatch_semaphore_t resetSemaphore = dispatch_semaphore_create(0);
617 [self.injectedManager rpcResetLocal:nil reply:^(NSError* result) {
618 XCTAssertNil(result, "no error resetting local");
619 secnotice("ckks", "Received a rpcResetLocal callback");
620 dispatch_semaphore_signal(resetSemaphore);
621 }];
622
623 XCTAssertEqual(0, dispatch_semaphore_wait(resetSemaphore, 4*NSEC_PER_SEC), "Semaphore wait did not time out");
624
625 OCMVerifyAllWithDelay(self.mockDatabase, 8);
626
627 [self expectCKModifyItemRecords: 1 currentKeyPointerRecords: 1 zoneID:self.keychainZoneID checkItem: [self checkClassABlock:self.keychainZoneID message:@"Object was encrypted under class A key in hierarchy"]];
628 [self addGenericPassword:@"asdf"
629 account:@"account-class-A"
630 viewHint:nil
631 access:(id)kSecAttrAccessibleWhenUnlocked
632 expecting:errSecSuccess
633 message:@"Adding class A item"];
634 OCMVerifyAllWithDelay(self.mockDatabase, 8);
635 }
636
637 -(void)testResetLocalWhileLoggedOut {
638 // We're "logged in to" cloudkit but not in circle.
639 self.circleStatus = kSOSCCNotInCircle;
640 [self.accountStateTracker notifyCircleStatusChangeAndWaitForSignal];
641 self.silentFetchesAllowed = false;
642
643 // Test starts with local TLK and key hierarhcy in our fake cloudkit
644 [self putFakeKeyHierarchyInCloudKit:self.keychainZoneID];
645 [self saveTLKMaterialToKeychainSimulatingSOS:self.keychainZoneID];
646
647 // Spin up CKKS subsystem.
648 [self startCKKSSubsystem];
649
650 NSData* changeTokenData = [[[NSUUID UUID] UUIDString] dataUsingEncoding:NSUTF8StringEncoding];
651 CKServerChangeToken* changeToken = [[CKServerChangeToken alloc] initWithData:changeTokenData];
652 [self.keychainView dispatchSync: ^bool{
653 CKKSZoneStateEntry* ckse = [CKKSZoneStateEntry state:self.keychainView.zoneName];
654 ckse.changeToken = changeToken;
655
656 NSError* error = nil;
657 [ckse saveToDatabase:&error];
658 XCTAssertNil(error, "No error saving new zone state to database");
659 }];
660
661 dispatch_semaphore_t resetSemaphore = dispatch_semaphore_create(0);
662 [self.injectedManager rpcResetLocal:nil reply:^(NSError* result) {
663 XCTAssertNil(result, "no error resetting cloudkit");
664 secnotice("ckks", "Received a rpcResetLocal callback");
665 dispatch_semaphore_signal(resetSemaphore);
666 }];
667
668 XCTAssertEqual(0, dispatch_semaphore_wait(resetSemaphore, 400*NSEC_PER_SEC), "Semaphore wait did not time out");
669
670 [self.keychainView dispatchSync: ^bool{
671 CKKSZoneStateEntry* ckse = [CKKSZoneStateEntry state:self.keychainView.zoneName];
672 XCTAssertNotEqualObjects(changeToken, ckse.changeToken, "Change token is reset");
673 }];
674
675 // Now log in, and see what happens! It should re-fetch, pick up the old key hierarchy, and use it
676 self.silentFetchesAllowed = true;
677 self.circleStatus = kSOSCCInCircle;
678 [self.accountStateTracker notifyCircleStatusChangeAndWaitForSignal];
679
680 [self expectCKModifyItemRecords: 1 currentKeyPointerRecords: 1 zoneID:self.keychainZoneID checkItem:[self checkClassABlock:self.keychainZoneID message:@"Object was encrypted under class A key in hierarchy"]];
681 [self addGenericPassword:@"asdf"
682 account:@"account-class-A"
683 viewHint:nil
684 access:(id)kSecAttrAccessibleWhenUnlocked
685 expecting:errSecSuccess
686 message:@"Adding class A item"];
687 OCMVerifyAllWithDelay(self.mockDatabase, 8);
688 }
689
690 -(void)testResetCloudKitZone {
691 // Test starts with nothing in database, but one in our fake CloudKit.
692 [self putFakeKeyHierarchyInCloudKit:self.keychainZoneID];
693 [self saveTLKMaterialToKeychainSimulatingSOS:self.keychainZoneID];
694
695 // Spin up CKKS subsystem.
696 [self startCKKSSubsystem];
697
698 // We expect a single record to be uploaded
699 [self expectCKModifyItemRecords: 1 currentKeyPointerRecords: 1 zoneID:self.keychainZoneID checkItem: [self checkClassCBlock:self.keychainZoneID message:@"Object was encrypted under class C key in hierarchy"]];
700 [self addGenericPassword: @"data" account: @"account-delete-me"];
701 OCMVerifyAllWithDelay(self.mockDatabase, 8);
702
703 // We expect a key hierarchy upload, and then the class C item upload
704 [self expectCKModifyKeyRecords: 3 currentKeyPointerRecords: 3 zoneID:self.keychainZoneID];
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
707 dispatch_semaphore_t resetSemaphore = dispatch_semaphore_create(0);
708 [self.injectedManager rpcResetCloudKit:nil reply:^(NSError* result) {
709 XCTAssertNil(result, "no error resetting cloudkit");
710 secnotice("ckks", "Received a resetCloudKit callback");
711 dispatch_semaphore_signal(resetSemaphore);
712 }];
713
714 XCTAssertEqual(0, dispatch_semaphore_wait(resetSemaphore, 4*NSEC_PER_SEC), "Semaphore wait did not time out");
715
716 OCMVerifyAllWithDelay(self.mockDatabase, 8);
717
718 [self expectCKModifyItemRecords: 1 currentKeyPointerRecords: 1 zoneID:self.keychainZoneID checkItem: [self checkClassABlock:self.keychainZoneID message:@"Object was encrypted under class A key in hierarchy"]];
719 [self addGenericPassword:@"asdf"
720 account:@"account-class-A"
721 viewHint:nil
722 access:(id)kSecAttrAccessibleWhenUnlocked
723 expecting:errSecSuccess
724 message:@"Adding class A item"];
725 OCMVerifyAllWithDelay(self.mockDatabase, 8);
726 }
727
728 -(void)testResetCloudKitZoneWhileLoggedOut {
729 // We're "logged in to" cloudkit but not in circle.
730 self.circleStatus = kSOSCCNotInCircle;
731 [self.accountStateTracker notifyCircleStatusChangeAndWaitForSignal];
732 self.silentFetchesAllowed = false;
733
734 // Test starts with nothing in database, but one in our fake CloudKit.
735 [self putFakeKeyHierarchyInCloudKit:self.keychainZoneID];
736
737 // Spin up CKKS subsystem.
738 [self startCKKSSubsystem];
739
740 CKRecord* ckr = [self createFakeRecord: self.keychainZoneID recordName:@"7B598D31-F9C5-481E-98AC-5A507ACB2D85"];
741 [self.keychainZone addToZone: ckr];
742
743 XCTAssertNotNil(self.keychainZone.currentDatabase, "Zone exists");
744 XCTAssertNotNil(self.keychainZone.currentDatabase[ckr.recordID], "An item exists in the fake zone");
745
746 dispatch_semaphore_t resetSemaphore = dispatch_semaphore_create(0);
747 [self.injectedManager rpcResetCloudKit:nil reply:^(NSError* result) {
748 XCTAssertNil(result, "no error resetting cloudkit");
749 secnotice("ckks", "Received a resetCloudKit callback");
750 dispatch_semaphore_signal(resetSemaphore);
751 }];
752
753 XCTAssertEqual(0, dispatch_semaphore_wait(resetSemaphore, 400*NSEC_PER_SEC), "Semaphore wait did not time out");
754
755 XCTAssertNil(self.keychainZone.currentDatabase, "No zone anymore!");
756 OCMVerifyAllWithDelay(self.mockDatabase, 8);
757
758 // Now log in, and see what happens! It should create the zone again and upload a whole new key hierarchy
759 self.silentFetchesAllowed = true;
760 self.circleStatus = kSOSCCInCircle;
761 [self.accountStateTracker notifyCircleStatusChangeAndWaitForSignal];
762
763 [self expectCKModifyKeyRecords: 3 currentKeyPointerRecords: 3 zoneID:self.keychainZoneID];
764 OCMVerifyAllWithDelay(self.mockDatabase, 8);
765
766 [self expectCKModifyItemRecords: 1 currentKeyPointerRecords: 1 zoneID:self.keychainZoneID checkItem:[self checkClassABlock:self.keychainZoneID message:@"Object was encrypted under class A key in hierarchy"]];
767 [self addGenericPassword:@"asdf"
768 account:@"account-class-A"
769 viewHint:nil
770 access:(id)kSecAttrAccessibleWhenUnlocked
771 expecting:errSecSuccess
772 message:@"Adding class A item"];
773 OCMVerifyAllWithDelay(self.mockDatabase, 8);
774 }
775
776 @end
777
778 #endif // OCTAGON