2 * Copyright (c) 2017 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>
31 #include <Security/SecItemPriv.h>
33 #import "keychain/ckks/tests/CloudKitMockXCTest.h"
34 #import "keychain/ckks/tests/CloudKitKeychainSyncingMockXCTest.h"
35 #import "keychain/ckks/tests/CKKSTests+MultiZone.h"
37 #import "keychain/ckks/CKKS.h"
38 #import "keychain/ckks/CKKSKeychainView.h"
39 #import "keychain/ckks/CKKSCurrentKeyPointer.h"
40 #import "keychain/ckks/CKKSItemEncrypter.h"
41 #import "keychain/ckks/CKKSKey.h"
42 #import "keychain/ckks/CKKSOutgoingQueueEntry.h"
43 #import "keychain/ckks/CKKSIncomingQueueEntry.h"
44 #import "keychain/ckks/CKKSSynchronizeOperation.h"
45 #import "keychain/ckks/CKKSViewManager.h"
46 #import "keychain/ckks/CKKSZoneStateEntry.h"
47 #import "keychain/ckks/CKKSManifest.h"
49 #import "keychain/ckks/tests/MockCloudKit.h"
51 #pragma clang diagnostic push
52 #pragma clang diagnostic ignored "-Wdeprecated-declarations"
53 #include <Security/SecureObjectSync/SOSCloudCircle.h>
54 #include "keychain/SecureObjectSync/SOSAccountPriv.h"
55 #include "keychain/SecureObjectSync/SOSAccount.h"
56 #include "keychain/SecureObjectSync/SOSInternal.h"
57 #include "keychain/SecureObjectSync/SOSFullPeerInfo.h"
58 #pragma clang diagnostic pop
60 #include <Security/SecKey.h>
61 #include <Security/SecKeyPriv.h>
62 #pragma clang diagnostic pop
64 @interface CloudKitKeychainSyncingSOSIntegrationTests : CloudKitKeychainSyncingMultiZoneTestsBase
67 @implementation CloudKitKeychainSyncingSOSIntegrationTests
69 - (void)testFindManateePiggyTLKs {
70 [self saveFakeKeyHierarchyToLocalDatabase:self.manateeZoneID];
71 [self saveTLKMaterialToKeychain:self.manateeZoneID];
73 [self startCKKSSubsystem];
75 NSDictionary* piggyTLKs = [self SOSPiggyBackCopyFromKeychain];
77 [self deleteTLKMaterialFromKeychain:self.manateeZoneID];
79 [self SOSPiggyBackAddToKeychain:piggyTLKs];
82 [self.manateeZoneKeys.tlk loadKeyMaterialFromKeychain:&error];
83 XCTAssertNil(error, "No error loading tlk from piggy contents");
86 - (void)testFindPiggyTLKs {
87 [self putFakeKeyHierachiesInCloudKit];
88 [self putFakeDeviceStatusesInCloudKit];
89 [self saveTLKsToKeychain];
91 [self startCKKSSubsystem];
93 NSDictionary* piggyTLKs = [self SOSPiggyBackCopyFromKeychain];
95 [self deleteTLKMaterialsFromKeychain];
97 [self SOSPiggyBackAddToKeychain:piggyTLKs];
100 [self.manateeZoneKeys.tlk loadKeyMaterialFromKeychain:&error];
101 XCTAssertNil(error, "No error loading manatee tlk from piggy contents");
103 [self.engramZoneKeys.tlk loadKeyMaterialFromKeychain:&error];
104 XCTAssertNil(error, "No error loading engram tlk from piggy contents");
106 [self.autoUnlockZoneKeys.tlk loadKeyMaterialFromKeychain:&error];
107 XCTAssertNil(error, "No error loading AutoUnlock tlk from piggy contents");
109 [self.healthZoneKeys.tlk loadKeyMaterialFromKeychain:&error];
110 XCTAssertNil(error, "No error loading Health tlk from piggy contents");
112 [self.applepayZoneKeys.tlk loadKeyMaterialFromKeychain:&error];
113 XCTAssertNil(error, "No error loading ApplePay tlk from piggy contents");
115 [self.homeZoneKeys.tlk loadKeyMaterialFromKeychain:&error];
116 XCTAssertNil(error, "No error loading Home tlk from piggy contents");
119 -(void)testPiggybackingData{
120 [self putFakeKeyHierachiesInCloudKit];
121 [self saveTLKsToKeychain];
123 for(CKRecordZoneID* zoneID in self.ckksZones) {
124 [self expectCKKSTLKSelfShareUpload:zoneID];
126 [self startCKKSSubsystem];
128 [self waitForKeyHierarchyReadinesses];
130 OCMVerifyAllWithDelay(self.mockDatabase, 20);
133 * Pull data from keychain and view manager
136 NSDictionary* piggydata = [self SOSPiggyBackCopyFromKeychain];
137 NSArray<NSData *>* icloudidentities = piggydata[@"idents"];
138 NSArray<NSDictionary *>* tlks = piggydata[@"tlk"];
140 XCTAssertEqual([tlks count], [[self.injectedManager viewList] count], "TLKs not same as views");
142 XCTAssertNotNil(tlks, "tlks not set");
143 XCTAssertNotEqual([tlks count], (NSUInteger)0, "0 tlks");
144 XCTAssertNotNil(icloudidentities, "idents not set");
145 XCTAssertNotEqual([icloudidentities count], (NSUInteger)0, "0 icloudidentities");
147 NSData *initial = SOSPiggyCreateInitialSyncData(icloudidentities, tlks);
149 XCTAssertNotNil(initial, "Initial not set");
150 XCTAssertNotEqual((int)[initial length], 0, "initial sync data is greater than 0");
153 * Check that they make it accross
156 const uint8_t* der = [initial bytes];
157 const uint8_t *der_end = der + [initial length];
159 NSDictionary *result = SOSPiggyCopyInitialSyncData(&der, der_end);
160 XCTAssertNotNil(result, "Initial not set");
161 NSArray *copiedTLKs = result[@"tlks"];
162 XCTAssertNotNil(copiedTLKs, "tlks not set");
163 XCTAssertEqual([copiedTLKs count], 5u, "piggybacking should have gotten 5 TLKs across (but we have more than that elsewhere)");
165 NSArray *copiediCloudidentities = result[@"idents"];
166 XCTAssertNotNil(copiediCloudidentities, "idents not set");
167 XCTAssertEqual([copiediCloudidentities count], [icloudidentities count], "ident count not same");
171 - (void)testPiggybackingTLKRequest {
172 [self putFakeKeyHierachiesInCloudKit];
173 [self saveTLKsToKeychain];
175 for(CKRecordZoneID* zoneID in self.ckksZones) {
176 [self expectCKKSTLKSelfShareUpload:zoneID];
178 [self startCKKSSubsystem];
179 [self waitForKeyHierarchyReadinesses];
180 OCMVerifyAllWithDelay(self.mockDatabase, 20);
182 // The "tlk request" piggybacking session calls SOSAccountCopyInitialSyncData
183 __block CFErrorRef cferror = NULL;
184 NSData* piggybackingData = (NSData*) CFBridgingRelease(SOSAccountCopyInitialSyncData(nil, kSOSInitialSyncFlagTLKsRequestOnly, &cferror));
186 XCTAssertEqual(cferror, NULL, "Should have no error fetching only the TLKs");
187 XCTAssertNotNil(piggybackingData, "Should have received some sync data");
189 const uint8_t* der = [piggybackingData bytes];
190 const uint8_t *der_end = der + [piggybackingData length];
192 NSDictionary *result = SOSPiggyCopyInitialSyncData(&der, der_end);
193 XCTAssertNotNil(result, "Should be able to parse the piggybacking data");
195 NSArray *copiedTLKs = result[@"tlks"];
196 XCTAssertNotNil(copiedTLKs, "should have some tlks");
197 XCTAssertEqual([copiedTLKs count], 1u, "piggybacking should have gotten 1 TLK");
198 XCTAssertEqualObjects(copiedTLKs[0][@"srvr"], @"Passwords", "should have the passwords TLK only");
199 NSData* keyData = copiedTLKs[0][@"v_Data"];
200 XCTAssertNotNil(keyData, "Should have some key material");
201 XCTAssertEqual([keyData length], 64, "Key material should be 64 bytes");
203 NSArray *copiediCloudidentities = result[@"idents"];
204 XCTAssertNotNil(copiediCloudidentities, "idents not set");
205 XCTAssertEqual([copiediCloudidentities count], 0, "Should have no icloud identities");
208 -(void)testVerifyTLKSorting {
209 char key[32*2] = {0};
210 NSArray<NSDictionary *> *tlks = @[
212 @"acct" : @"11111111",
213 @"srvr" : @"Manatee",
214 @"v_Data" : [NSData dataWithBytes:key length:sizeof(key)],
218 @"acct" : @"55555555",
220 @"v_Data" : [NSData dataWithBytes:key length:sizeof(key)],
223 @"acct" : @"22222222",
225 @"v_Data" : [NSData dataWithBytes:key length:sizeof(key)],
229 @"acct" : @"44444444",
230 @"srvr" : @"Manatee",
231 @"v_Data" : [NSData dataWithBytes:key length:sizeof(key)],
234 @"acct" : @"33333333",
236 @"v_Data" : [NSData dataWithBytes:key length:sizeof(key)],
240 @"acct" : @"66666666",
242 @"v_Data" : [NSData dataWithBytes:key length:sizeof(key)],
247 NSArray<NSDictionary *>* sortedTLKs = SOSAccountSortTLKS(tlks);
248 XCTAssertNotNil(sortedTLKs, "sortedTLKs not set");
250 // Home gets sorted into the middle, as the other Health and Manatee TLKs aren't 'authoritative'
251 NSArray<NSString *> *expectedOrder = @[ @"11111111", @"22222222", @"33333333", @"66666666", @"44444444", @"55555555"];
252 [sortedTLKs enumerateObjectsUsingBlock:^(NSDictionary *tlk, NSUInteger idx, BOOL * _Nonnull stop) {
253 NSString *uuid = tlk[@"acct"];
254 XCTAssertEqualObjects(uuid, expectedOrder[idx], "wrong order");
259 - (void)testAcceptExistingPiggyKeyHierarchy {
260 // Test starts with no keys in CKKS database, but one in our fake CloudKit.
261 // Test also begins with the TLK having arrived in the local keychain (via SOS)
262 [self putFakeKeyHierachiesInCloudKit];
263 [self saveTLKsToKeychain];
264 NSDictionary* piggyTLKS = [self SOSPiggyBackCopyFromKeychain];
265 [self SOSPiggyBackAddToKeychain:piggyTLKS];
266 [self deleteTLKMaterialsFromKeychain];
268 // The CKKS subsystem should write a TLK Share for each view
269 for(CKRecordZoneID* zoneID in self.ckksZones) {
270 [self expectCKKSTLKSelfShareUpload:zoneID];
273 // Spin up CKKS subsystem.
274 [self startCKKSSubsystem];
276 [self.manateeView waitForKeyHierarchyReadiness];
278 OCMVerifyAllWithDelay(self.mockDatabase, 20);
280 // Verify that there are three local keys, and three local current key records
281 __weak __typeof(self) weakSelf = self;
282 [self.manateeView dispatchSyncWithReadOnlySQLTransaction:^{
283 __strong __typeof(weakSelf) strongSelf = weakSelf;
284 XCTAssertNotNil(strongSelf, "self exists");
286 NSError* error = nil;
288 NSArray<CKKSKey*>* keys = [CKKSKey localKeys:strongSelf.manateeZoneID error:&error];
289 XCTAssertNil(error, "no error fetching keys");
290 XCTAssertEqual(keys.count, 3u, "Three keys in local database");
292 NSArray<CKKSCurrentKeyPointer*>* currentkeys = [CKKSCurrentKeyPointer all:strongSelf.manateeZoneID error:&error];
293 XCTAssertNil(error, "no error fetching current keys");
294 XCTAssertEqual(currentkeys.count, 3u, "Three current key pointers in local database");
296 // Ensure that the manatee syncable TLK is created from a piggy
297 NSDictionary* query = @{
298 (id)kSecClass : (id)kSecClassInternetPassword,
299 (id)kSecUseDataProtectionKeychain : @YES,
300 (id)kSecAttrAccessGroup : @"com.apple.security.ckks",
301 (id)kSecAttrDescription: SecCKKSKeyClassTLK,
302 (id)kSecAttrAccount: strongSelf.manateeZoneKeys.tlk.uuid,
303 (id)kSecAttrServer: strongSelf.manateeZoneID.zoneName,
304 (id)kSecAttrSynchronizable: @YES,
305 (id)kSecReturnAttributes: @YES,
306 (id)kSecReturnData: @YES,
308 CFTypeRef result = nil;
309 XCTAssertEqual(errSecSuccess, SecItemCopyMatching((__bridge CFDictionaryRef)query, &result), "Found a syncable TLK");
310 XCTAssertNotNil((__bridge id) result, "Received a result from SecItemCopyMatching");
311 CFReleaseNull(result);
315 -(void)testAcceptExistingAndUsePiggyKeyHierarchy {
316 // Test starts with nothing in database, but one in our fake CloudKit.
317 [self putFakeKeyHierachiesInCloudKit];
318 [self putFakeDeviceStatusesInCloudKit];
319 [self saveTLKsToKeychain];
320 NSDictionary* piggyData = [self SOSPiggyBackCopyFromKeychain];
321 [self deleteTLKMaterialsFromKeychain];
323 // Spin up CKKS subsystem.
324 [self startCKKSSubsystem];
326 // The CKKS subsystem should not try to write anything to the CloudKit database.
327 XCTAssertEqual(0, [self.manateeView.keyHierarchyConditions[SecCKKSZoneKeyStateWaitForTLK] wait:20*NSEC_PER_SEC], "CKKS entered waitfortlk");
329 OCMVerifyAllWithDelay(self.mockDatabase, 20);
331 // Now, save the TLKs to the keychain (to simulate them coming in later via piggybacking).
332 for(CKRecordZoneID* zoneID in self.ckksZones) {
333 [self expectCKKSTLKSelfShareUpload:zoneID];
336 [self SOSPiggyBackAddToKeychain:piggyData];
337 [self waitForKeyHierarchyReadinesses];
339 // We expect a single record to be uploaded for each key class
340 [self expectCKModifyItemRecords: 1 currentKeyPointerRecords: 1 zoneID:self.manateeZoneID checkItem: [self checkClassCBlock:self.manateeZoneID message:@"Object was encrypted under class C key in hierarchy"]];
341 [self addGenericPassword: @"data" account: @"account-delete-me-manatee" viewHint:(id)kSecAttrViewHintManatee];
343 OCMVerifyAllWithDelay(self.mockDatabase, 20);
345 [self expectCKModifyItemRecords: 1 currentKeyPointerRecords: 1 zoneID:self.manateeZoneID checkItem: [self checkClassABlock:self.manateeZoneID message:@"Object was encrypted under class A key in hierarchy"]];
347 [self addGenericPassword:@"asdf"
348 account:@"account-class-A"
349 viewHint:(id)kSecAttrViewHintManatee
350 access:(id)kSecAttrAccessibleWhenUnlocked
351 expecting:errSecSuccess
352 message:@"Adding class A item"];
353 OCMVerifyAllWithDelay(self.mockDatabase, 20);