2 * Copyright (c) 2016 Apple Inc. All Rights Reserved.
4 * @APPLE_LICENSE_HEADER_START@
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
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.
21 * @APPLE_LICENSE_HEADER_END@
26 #import <CloudKit/CloudKit.h>
27 #import <XCTest/XCTest.h>
28 #import <OCMock/OCMock.h>
30 #include <Security/SecItemPriv.h>
31 #include <Security/SecEntitlements.h>
32 #include <ipc/server_security_helpers.h>
33 #import <Foundation/NSXPCConnection_Private.h>
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"
44 #import "keychain/ckks/tests/MockCloudKit.h"
46 #import "keychain/ckks/tests/CKKSTests.h"
47 #import "keychain/ckks/tests/CKKSTests+API.h"
49 @implementation CloudKitKeychainSyncingTests (APITests)
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");
57 NSXPCInterface *interface = [NSXPCInterface interfaceWithProtocol:@protocol(SecuritydXPCProtocol)];
58 [SecuritydXPCClient configureSecuritydXPCProtocol: interface];
59 XCTAssertNotNil(interface, "Received a configured CKKS interface");
61 NSXPCListenerEndpoint *listenerEndpoint = [[NSXPCListenerEndpoint alloc] init];
62 [listenerEndpoint _setEndpoint:endpoint];
64 NSXPCConnection* connection = [[NSXPCConnection alloc] initWithListenerEndpoint:listenerEndpoint];
65 XCTAssertNotNil(connection , "Received an active connection");
67 connection.remoteObjectInterface = interface;
70 -(NSMutableDictionary*)pcsAddItemQuery:(NSString*)account
72 serviceIdentifier:(NSNumber*)serviceIdentifier
73 publicKey:(NSData*)publicKey
74 publicIdentity:(NSData*)publicIdentity
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,
92 -(NSDictionary*)pcsAddItem:(NSString*)account
94 serviceIdentifier:(NSNumber*)serviceIdentifier
95 publicKey:(NSData*)publicKey
96 publicIdentity:(NSData*)publicIdentity
97 expectingSync:(bool)expectingSync
99 NSMutableDictionary* query = [self pcsAddItemQuery:account
101 serviceIdentifier:(NSNumber*)serviceIdentifier
102 publicKey:(NSData*)publicKey
103 publicIdentity:(NSData*)publicIdentity];
104 CFTypeRef result = NULL;
105 XCTestExpectation* syncExpectation = [self expectationWithDescription: @"callback occurs"];
107 XCTAssertEqual(errSecSuccess, _SecItemAddAndNotifyOnSync((__bridge CFDictionaryRef) query, &result, ^(bool didSync, CFErrorRef error) {
109 XCTAssertTrue(didSync, "Item synced");
110 XCTAssertNil((__bridge NSError*)error, "No error syncing item");
112 XCTAssertFalse(didSync, "Item did not sync");
113 XCTAssertNotNil((__bridge NSError*)error, "Error syncing item");
116 [syncExpectation fulfill];
117 }), @"_SecItemAddAndNotifyOnSync succeeded");
119 // Verify that the item was written to CloudKit
120 OCMVerifyAllWithDelay(self.mockDatabase, 8);
122 // In real code, you'd need to wait for the _SecItemAddAndNotifyOnSync callback to succeed before proceeding
123 [self waitForExpectations:@[syncExpectation] timeout:8.0];
125 return (NSDictionary*) CFBridgingRelease(result);
129 - (void)testAddAndNotifyOnSync {
130 [self createAndSaveFakeKeyHierarchy: self.keychainZoneID]; // Make life easy for this test.
132 [self expectCKModifyItemRecords: 1 currentKeyPointerRecords: 1 zoneID:self.keychainZoneID];
133 [self startCKKSSubsystem];
135 // Let things shake themselves out.
136 [self.keychainView waitForKeyHierarchyReadiness];
137 [self waitForCKModifications];
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],
148 XCTestExpectation* blockExpectation = [self expectationWithDescription: @"callback occurs"];
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");
154 [blockExpectation fulfill];
155 }), @"_SecItemAddAndNotifyOnSync succeeded");
157 [self waitForExpectationsWithTimeout:5.0 handler:nil];
160 - (void)testAddAndNotifyOnSyncFailure {
161 [self createAndSaveFakeKeyHierarchy: self.keychainZoneID]; // Make life easy for this test.
163 [self startCKKSSubsystem];
164 [self.keychainView waitForFetchAndIncomingQueueProcessing];
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];
173 [self expectCKAtomicModifyItemRecordsUpdateFailure: self.keychainZoneID];
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],
184 XCTestExpectation* blockExpectation = [self expectationWithDescription: @"callback occurs"];
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");
190 [blockExpectation fulfill];
191 }), @"_SecItemAddAndNotifyOnSync succeeded");
193 [self waitForExpectationsWithTimeout:5.0 handler:nil];
194 [self waitForCKModifications];
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];
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],
212 XCTestExpectation* blockExpectation = [self expectationWithDescription: @"callback occurs"];
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");
218 [blockExpectation fulfill];
219 }), @"_SecItemAddAndNotifyOnSync succeeded");
221 [self waitForExpectationsWithTimeout:5.0 handler:nil];
224 - (BOOL (^) (CKRecord*)) checkPCSFieldsBlock: (CKRecordZoneID*) zoneID
225 PCSServiceIdentifier:(NSNumber*)servIdentifier
226 PCSPublicKey:(NSData*)publicKey
227 PCSPublicIdentity:(NSData*)publicIdentity
229 __weak __typeof(self) weakSelf = self;
230 return ^BOOL(CKRecord* record) {
231 __strong __typeof(weakSelf) strongSelf = weakSelf;
232 XCTAssertNotNil(strongSelf, "self exists");
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");
238 if([record[SecCKRecordPCSServiceIdentifier] isEqual: servIdentifier] &&
239 [record[SecCKRecordPCSPublicKey] isEqual: publicKey] &&
240 [record[SecCKRecordPCSPublicIdentity] isEqual: publicIdentity]) {
248 - (void)testPCSUnencryptedFieldsAdd {
250 [self createAndSaveFakeKeyHierarchy: self.keychainZoneID]; // Make life easy for this test.
252 [self startCKKSSubsystem];
253 [self.keychainView waitUntilAllOperationsAreFinished];
255 NSNumber* servIdentifier = @3;
256 NSData* publicKey = [@"asdfasdf" dataUsingEncoding:NSUTF8StringEncoding];
257 NSData* publicIdentity = [@"somedata" dataUsingEncoding:NSUTF8StringEncoding];
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]];
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,
278 XCTAssertEqual(errSecSuccess, SecItemAdd((__bridge CFDictionaryRef) query, NULL), @"SecItemAdd succeeded");
280 // Verify that the item is written to CloudKit
281 OCMVerifyAllWithDelay(self.mockDatabase, 4);
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");
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");
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");
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");
305 - (void)testPCSUnencryptedFieldsModify {
306 [self createAndSaveFakeKeyHierarchy: self.keychainZoneID]; // Make life easy for this test.
308 [self startCKKSSubsystem];
309 [self.keychainView waitUntilAllOperationsAreFinished];
311 NSNumber* servIdentifier = @3;
312 NSData* publicKey = [@"asdfasdf" dataUsingEncoding:NSUTF8StringEncoding];
313 NSData* publicIdentity = [@"somedata" dataUsingEncoding:NSUTF8StringEncoding];
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]];
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,
334 XCTAssertEqual(errSecSuccess, SecItemAdd((__bridge CFDictionaryRef) query, NULL), @"SecItemAdd succeeded");
336 OCMVerifyAllWithDelay(self.mockDatabase, 8);
337 [self waitForCKModifications];
339 query[(id)kSecValueData] = nil;
340 query[(id)kSecAttrPCSPlaintextServiceIdentifier] = nil;
341 query[(id)kSecAttrPCSPlaintextPublicKey] = nil;
342 query[(id)kSecAttrPCSPlaintextPublicIdentity] = nil;
345 publicKey = [@"new public key" dataUsingEncoding:NSUTF8StringEncoding];
347 NSNumber* newServiceIdentifier = @10;
348 NSData* newPublicKey = [@"new public key" dataUsingEncoding:NSUTF8StringEncoding];
349 NSData* newPublicIdentity = [@"new public identity" dataUsingEncoding:NSUTF8StringEncoding];
351 NSDictionary* update = @{
352 (id)kSecAttrPCSPlaintextServiceIdentifier : newServiceIdentifier,
353 (id)kSecAttrPCSPlaintextPublicKey : newPublicKey,
354 (id)kSecAttrPCSPlaintextPublicIdentity : newPublicIdentity,
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]];
363 XCTAssertEqual(errSecSuccess, SecItemUpdate((__bridge CFDictionaryRef) query, (__bridge CFDictionaryRef) update), @"SecItemUpdate succeeded");
364 OCMVerifyAllWithDelay(self.mockDatabase, 8);
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");
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");
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");
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");
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.
392 [self startCKKSSubsystem];
393 [self.keychainView waitForKeyHierarchyReadiness];
395 NSNumber* servIdentifier = @3;
396 NSData* publicKey = [@"asdfasdf" dataUsingEncoding:NSUTF8StringEncoding];
397 NSData* publicIdentity = [@"somedata" dataUsingEncoding:NSUTF8StringEncoding];
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]];
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,
418 XCTAssertEqual(errSecSuccess, SecItemAdd((__bridge CFDictionaryRef) query, NULL), @"SecItemAdd succeeded");
420 OCMVerifyAllWithDelay(self.mockDatabase, 4);
421 [self waitForCKModifications];
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");
429 // Items are encrypted using encv2
430 XCTAssertEqualObjects(record[SecCKRecordEncryptionVersionKey], [NSNumber numberWithInteger:(int) CKKSItemEncryptionVersion2], "Uploaded using encv2");
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) {
441 NSNumber* newServiceIdentifier = @10;
442 NSData* newPublicKey = [@"new public key" dataUsingEncoding:NSUTF8StringEncoding];
443 NSData* newPublicIdentity = [@"new public identity" dataUsingEncoding:NSUTF8StringEncoding];
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];
452 // Trigger a notification
453 [self.keychainView notifyZoneChange:nil];
454 [self.keychainView waitForFetchAndIncomingQueueProcessing];
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");
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");
470 -(void)testPCSUnencryptedFieldsRecieveUnauthenticatedFields {
471 [self createAndSaveFakeKeyHierarchy: self.keychainZoneID]; // Make life easy for this test.
473 [self startCKKSSubsystem];
474 [self.keychainView waitForKeyHierarchyReadiness];
476 NSNumber* servIdentifier = @3;
477 NSData* publicKey = [@"asdfasdf" dataUsingEncoding:NSUTF8StringEncoding];
478 NSData* publicIdentity = [@"somedata" dataUsingEncoding:NSUTF8StringEncoding];
480 NSError* error = nil;
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");
494 cipheritem.encver = CKKSItemEncryptionVersion1;
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;
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");
506 [self.keychainZone addToZone:[cipheritem CKRecordWithZoneID: recordID.zoneID]];
508 [self.keychainView waitForFetchAndIncomingQueueProcessing];
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,
516 CFTypeRef cfresult = NULL;
517 XCTAssertEqual(errSecSuccess, SecItemCopyMatching((__bridge CFDictionaryRef) query, &cfresult), "Found synced item");
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");
525 -(void)testPCSUnencryptedFieldsRecieveAuthenticatedFields {
526 [self createAndSaveFakeKeyHierarchy: self.keychainZoneID]; // Make life easy for this test.
528 [self startCKKSSubsystem];
529 [self.keychainView waitForKeyHierarchyReadiness];
531 NSNumber* servIdentifier = @3;
532 NSData* publicKey = [@"asdfasdf" dataUsingEncoding:NSUTF8StringEncoding];
533 NSData* publicIdentity = [@"somedata" dataUsingEncoding:NSUTF8StringEncoding];
535 NSError* error = nil;
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");
549 cipheritem.encver = CKKSItemEncryptionVersion2;
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;
556 // Use version 2, so PCS plaintext fields will be authenticated
557 NSMutableDictionary<NSString*, NSData*>* authenticatedData = [[cipheritem makeAuthenticatedDataDictionaryUpdatingCKKSItem: nil encryptionVersion:CKKSItemEncryptionVersion2] mutableCopy];
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");
563 [self.keychainZone addToZone:[cipheritem CKRecordWithZoneID: recordID.zoneID]];
565 [self.keychainView waitForFetchAndIncomingQueueProcessing];
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,
573 CFTypeRef cfresult = NULL;
574 XCTAssertEqual(errSecSuccess, SecItemCopyMatching((__bridge CFDictionaryRef) query, &cfresult), "Found synced item");
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");
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"];
589 OCMVerifyAllWithDelay(self.mockDatabase, 4);
590 [self waitForCKModifications];
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");
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];
604 // Spin up CKKS subsystem.
605 [self startCKKSSubsystem];
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);
612 // After the local reset, we expect: a fetch, then nothing
613 self.silentFetchesAllowed = false;
614 [self expectCKFetch];
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);
623 XCTAssertEqual(0, dispatch_semaphore_wait(resetSemaphore, 4*NSEC_PER_SEC), "Semaphore wait did not time out");
625 OCMVerifyAllWithDelay(self.mockDatabase, 8);
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"
631 access:(id)kSecAttrAccessibleWhenUnlocked
632 expecting:errSecSuccess
633 message:@"Adding class A item"];
634 OCMVerifyAllWithDelay(self.mockDatabase, 8);
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;
643 // Test starts with local TLK and key hierarhcy in our fake cloudkit
644 [self putFakeKeyHierarchyInCloudKit:self.keychainZoneID];
645 [self saveTLKMaterialToKeychainSimulatingSOS:self.keychainZoneID];
647 // Spin up CKKS subsystem.
648 [self startCKKSSubsystem];
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;
656 NSError* error = nil;
657 [ckse saveToDatabase:&error];
658 XCTAssertNil(error, "No error saving new zone state to database");
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);
668 XCTAssertEqual(0, dispatch_semaphore_wait(resetSemaphore, 400*NSEC_PER_SEC), "Semaphore wait did not time out");
670 [self.keychainView dispatchSync: ^bool{
671 CKKSZoneStateEntry* ckse = [CKKSZoneStateEntry state:self.keychainView.zoneName];
672 XCTAssertNotEqualObjects(changeToken, ckse.changeToken, "Change token is reset");
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];
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"
684 access:(id)kSecAttrAccessibleWhenUnlocked
685 expecting:errSecSuccess
686 message:@"Adding class A item"];
687 OCMVerifyAllWithDelay(self.mockDatabase, 8);
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];
695 // Spin up CKKS subsystem.
696 [self startCKKSSubsystem];
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);
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"]];
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);
714 XCTAssertEqual(0, dispatch_semaphore_wait(resetSemaphore, 4*NSEC_PER_SEC), "Semaphore wait did not time out");
716 OCMVerifyAllWithDelay(self.mockDatabase, 8);
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"
722 access:(id)kSecAttrAccessibleWhenUnlocked
723 expecting:errSecSuccess
724 message:@"Adding class A item"];
725 OCMVerifyAllWithDelay(self.mockDatabase, 8);
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;
734 // Test starts with nothing in database, but one in our fake CloudKit.
735 [self putFakeKeyHierarchyInCloudKit:self.keychainZoneID];
737 // Spin up CKKS subsystem.
738 [self startCKKSSubsystem];
740 CKRecord* ckr = [self createFakeRecord: self.keychainZoneID recordName:@"7B598D31-F9C5-481E-98AC-5A507ACB2D85"];
741 [self.keychainZone addToZone: ckr];
743 XCTAssertNotNil(self.keychainZone.currentDatabase, "Zone exists");
744 XCTAssertNotNil(self.keychainZone.currentDatabase[ckr.recordID], "An item exists in the fake zone");
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);
753 XCTAssertEqual(0, dispatch_semaphore_wait(resetSemaphore, 400*NSEC_PER_SEC), "Semaphore wait did not time out");
755 XCTAssertNil(self.keychainZone.currentDatabase, "No zone anymore!");
756 OCMVerifyAllWithDelay(self.mockDatabase, 8);
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];
763 [self expectCKModifyKeyRecords: 3 currentKeyPointerRecords: 3 zoneID:self.keychainZoneID];
764 OCMVerifyAllWithDelay(self.mockDatabase, 8);
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"
770 access:(id)kSecAttrAccessibleWhenUnlocked
771 expecting:errSecSuccess
772 message:@"Adding class A item"];
773 OCMVerifyAllWithDelay(self.mockDatabase, 8);