]> git.saurik.com Git - apple/security.git/blob - keychain/ckks/tests/CloudKitKeychainSyncingMockXCTest.m
Security-59754.41.1.tar.gz
[apple/security.git] / keychain / ckks / tests / CloudKitKeychainSyncingMockXCTest.m
1 /*
2 * Copyright (c) 2017 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 #pragma clang diagnostic push
27 #pragma clang diagnostic ignored "-Wquoted-include-in-framework-header"
28 #import <OCMock/OCMock.h>
29 #pragma clang diagnostic pop
30
31 #import "keychain/ckks/tests/CloudKitMockXCTest.h"
32 #import "keychain/ckks/tests/CloudKitKeychainSyncingMockXCTest.h"
33
34 #import "keychain/securityd/SecItemSchema.h"
35 #import "keychain/securityd/SecItemServer.h"
36 #import "keychain/securityd/SecItemDb.h"
37
38 #import "keychain/ckks/CKKS.h"
39 #import "keychain/ckks/CKKSKeychainView.h"
40 #import "keychain/ckks/CKKSCurrentKeyPointer.h"
41 #import "keychain/ckks/CKKSItemEncrypter.h"
42 #import "keychain/ckks/CKKSKey.h"
43 #import "keychain/ckks/CKKSOutgoingQueueEntry.h"
44 #import "keychain/ckks/CKKSIncomingQueueEntry.h"
45 #import "keychain/ckks/CKKSSynchronizeOperation.h"
46 #import "keychain/ckks/CKKSViewManager.h"
47 #import "keychain/ckks/CKKSZoneStateEntry.h"
48 #import "keychain/ckks/CKKSManifest.h"
49 #import "keychain/ckks/CKKSPeer.h"
50 #import "keychain/categories/NSError+UsefulConstructors.h"
51
52 #import "keychain/ot/OTDefines.h"
53
54 #import "tests/secdmockaks/mockaks.h"
55
56 #pragma clang diagnostic push
57 #pragma clang diagnostic ignored "-Wdeprecated-declarations"
58 #import "keychain/SecureObjectSync/SOSAccount.h"
59 #pragma clang diagnostic pop
60
61 @implementation ZoneKeys
62 - (instancetype)initLoadingRecordsFromZone:(FakeCKZone*)zone {
63 if((self = [super init])) {
64 CKRecordID* currentTLKPointerID = [[CKRecordID alloc] initWithRecordName:SecCKKSKeyClassTLK zoneID:zone.zoneID];
65 CKRecordID* currentClassAPointerID = [[CKRecordID alloc] initWithRecordName:SecCKKSKeyClassA zoneID:zone.zoneID];
66 CKRecordID* currentClassCPointerID = [[CKRecordID alloc] initWithRecordName:SecCKKSKeyClassC zoneID:zone.zoneID];
67
68 CKRecord* currentTLKPointerRecord = zone.currentDatabase[currentTLKPointerID];
69 CKRecord* currentClassAPointerRecord = zone.currentDatabase[currentClassAPointerID];
70 CKRecord* currentClassCPointerRecord = zone.currentDatabase[currentClassCPointerID];
71
72 self.currentTLKPointer = currentTLKPointerRecord ? [[CKKSCurrentKeyPointer alloc] initWithCKRecord: currentTLKPointerRecord] : nil;
73 self.currentClassAPointer = currentClassAPointerRecord ? [[CKKSCurrentKeyPointer alloc] initWithCKRecord: currentClassAPointerRecord] : nil;
74 self.currentClassCPointer = currentClassCPointerRecord ? [[CKKSCurrentKeyPointer alloc] initWithCKRecord: currentClassCPointerRecord] : nil;
75
76 CKRecordID* currentTLKID = self.currentTLKPointer.currentKeyUUID ? [[CKRecordID alloc] initWithRecordName:self.currentTLKPointer.currentKeyUUID zoneID:zone.zoneID] : nil;
77 CKRecordID* currentClassAID = self.currentClassAPointer.currentKeyUUID ? [[CKRecordID alloc] initWithRecordName:self.currentClassAPointer.currentKeyUUID zoneID:zone.zoneID] : nil;
78 CKRecordID* currentClassCID = self.currentClassCPointer.currentKeyUUID ? [[CKRecordID alloc] initWithRecordName:self.currentClassCPointer.currentKeyUUID zoneID:zone.zoneID] : nil;
79
80 CKRecord* currentTLKRecord = currentTLKID ? zone.currentDatabase[currentTLKID] : nil;
81 CKRecord* currentClassARecord = currentClassAID ? zone.currentDatabase[currentClassAID] : nil;
82 CKRecord* currentClassCRecord = currentClassCID ? zone.currentDatabase[currentClassCID] : nil;
83
84 self.tlk = currentTLKRecord ? [[CKKSKey alloc] initWithCKRecord: currentTLKRecord] : nil;
85 self.classA = currentClassARecord ? [[CKKSKey alloc] initWithCKRecord: currentClassARecord] : nil;
86 self.classC = currentClassCRecord ? [[CKKSKey alloc] initWithCKRecord: currentClassCRecord] : nil;
87 }
88 return self;
89 }
90
91 @end
92
93 // No tests here, just helper functions
94 @implementation CloudKitKeychainSyncingMockXCTest
95
96 - (void)setUp {
97 // Need to convince your tests to set these, no matter what the on-disk plist says? Uncomment.
98 (void)[CKKSManifest shouldSyncManifests]; // perfrom initialization
99 SecCKKSSetSyncManifests(false);
100 SecCKKSSetEnforceManifests(false);
101
102 // Check that your environment is set up correctly
103 XCTAssertFalse([CKKSManifest shouldSyncManifests], "Manifests syncing is disabled");
104 XCTAssertFalse([CKKSManifest shouldEnforceManifests], "Manifests enforcement is disabled");
105
106 // Use our superclass to create a fake keychain
107 [super setUp];
108
109 self.automaticallyBeginCKKSViewCloudKitOperation = true;
110 self.suggestTLKUpload = OCMClassMock([CKKSNearFutureScheduler class]);
111 OCMStub([self.suggestTLKUpload trigger]);
112
113 self.requestPolicyCheck = OCMClassMock([CKKSNearFutureScheduler class]);
114 OCMStub([self.requestPolicyCheck trigger]);
115
116 // If a subclass wants to fill these in before calling setUp, fine.
117 self.ckksZones = self.ckksZones ?: [NSMutableSet set];
118 self.ckksViews = self.ckksViews ?: [NSMutableSet set];
119 self.keys = self.keys ?: [[NSMutableDictionary alloc] init];
120
121 [SecMockAKS reset];
122
123 // Set up a remote peer with no keys
124 self.remoteSOSOnlyPeer = [[CKKSSOSPeer alloc] initWithSOSPeerID:@"remote-peer-with-no-keys"
125 encryptionPublicKey:nil
126 signingPublicKey:nil
127 viewList:self.managedViewList];
128 NSMutableSet<id<CKKSSOSPeerProtocol>>* currentPeers = [NSMutableSet setWithObject:self.remoteSOSOnlyPeer];
129 self.mockSOSAdapter.trustedPeers = currentPeers;
130
131 // Fake out whether class A keys can be loaded from the keychain.
132 self.mockCKKSKeychainBackedKey = OCMClassMock([CKKSKeychainBackedKey class]);
133 __weak __typeof(self) weakSelf = self;
134 BOOL (^shouldFailKeychainQuery)(NSDictionary* query) = ^BOOL(NSDictionary* query) {
135 __strong __typeof(self) strongSelf = weakSelf;
136 return !!strongSelf.keychainFetchError;
137 };
138
139 OCMStub([self.mockCKKSKeychainBackedKey setKeyMaterialInKeychain:[OCMArg checkWithBlock:shouldFailKeychainQuery] error:[OCMArg anyObjectRef]]
140 ).andCall(self, @selector(handleFailedSetKeyMaterialInKeychain:error:));
141
142 OCMStub([self.mockCKKSKeychainBackedKey queryKeyMaterialInKeychain:[OCMArg checkWithBlock:shouldFailKeychainQuery] error:[OCMArg anyObjectRef]]
143 ).andCall(self, @selector(handleFailedLoadKeyMaterialFromKeychain:error:));
144
145 // Bring up a fake CKKSControl object
146 id mockConnection = OCMPartialMock([[NSXPCConnection alloc] init]);
147 OCMStub([mockConnection remoteObjectProxyWithErrorHandler:[OCMArg any]]).andCall(self, @selector(injectedManager));
148 self.ckksControl = [[CKKSControl alloc] initWithConnection:mockConnection];
149 XCTAssertNotNil(self.ckksControl, "Should have received control object");
150 }
151
152 - (void)tearDown {
153 // Make sure the key state machines won't continue
154 for(CKKSKeychainView* view in self.ckksViews) {
155 [view.stateMachine haltOperation];
156 }
157 [self.ckksViews removeAllObjects];
158
159 [super tearDown];
160 self.keys = nil;
161
162 [self.mockCKKSKeychainBackedKey stopMocking];
163 self.mockCKKSKeychainBackedKey = nil;
164
165 self.remoteSOSOnlyPeer = nil;
166 }
167
168 - (void)startCKKSSubsystem
169 {
170 [super startCKKSSubsystem];
171 if(self.mockSOSAdapter.circleStatus == kSOSCCInCircle) {
172 [self beginSOSTrustedOperationForAllViews];
173 } else {
174 [self endSOSTrustedOperationForAllViews];
175 }
176 }
177
178 - (void)beginSOSTrustedOperationForAllViews {
179 for(CKKSKeychainView* view in self.ckksViews) {
180 [self beginSOSTrustedViewOperation:view];
181 }
182 }
183
184 - (void)beginSOSTrustedViewOperation:(CKKSKeychainView*)view
185 {
186 if(self.automaticallyBeginCKKSViewCloudKitOperation) {
187 [view beginCloudKitOperation];
188 }
189
190 [view beginTrustedOperation:@[self.mockSOSAdapter]
191 suggestTLKUpload:self.suggestTLKUpload
192 requestPolicyCheck:self.requestPolicyCheck];
193 }
194
195 - (void)endSOSTrustedOperationForAllViews {
196 for(CKKSKeychainView* view in self.ckksViews) {
197 [self endSOSTrustedViewOperation:view];
198 }
199 }
200
201 - (void)endSOSTrustedViewOperation:(CKKSKeychainView*)view
202 {
203 if(self.automaticallyBeginCKKSViewCloudKitOperation) {
204 [view beginCloudKitOperation];
205 }
206 [view endTrustedOperation];
207 }
208
209 - (void)verifyDatabaseMocks {
210 OCMVerifyAllWithDelay(self.mockDatabase, 20);
211 [self waitForCKModifications];
212 }
213
214 - (void)createClassCItemAndWaitForUpload:(CKRecordZoneID*)zoneID account:(NSString*)account {
215 [self expectCKModifyItemRecords:1
216 currentKeyPointerRecords:1
217 zoneID:zoneID
218 checkItem:[self checkClassCBlock:zoneID message:@"Object was encrypted under class C key in hierarchy"]];
219 [self addGenericPassword: @"data" account: account];
220 OCMVerifyAllWithDelay(self.mockDatabase, 20);
221 }
222
223 - (void)createClassAItemAndWaitForUpload:(CKRecordZoneID*)zoneID account:(NSString*)account {
224 [self expectCKModifyItemRecords:1
225 currentKeyPointerRecords:1
226 zoneID:zoneID
227 checkItem: [self checkClassABlock:zoneID message:@"Object was encrypted under class A key in hierarchy"]];
228 [self addGenericPassword:@"asdf"
229 account:account
230 viewHint:nil
231 access:(id)kSecAttrAccessibleWhenUnlocked
232 expecting:errSecSuccess
233 message:@"Adding class A item"];
234 OCMVerifyAllWithDelay(self.mockDatabase, 20);
235 }
236
237 // Helpers to handle 'failed' keychain loading and saving
238 - (bool)handleFailedLoadKeyMaterialFromKeychain:(NSDictionary*)query error:(NSError * __autoreleasing *) error {
239 NSAssert(self.keychainFetchError != nil, @"must have a keychain error to error with");
240
241 if(error) {
242 *error = self.keychainFetchError;
243 }
244 return false;
245 }
246
247 - (bool)handleFailedSetKeyMaterialInKeychain:(NSDictionary*)query error:(NSError * __autoreleasing *) error {
248 NSAssert(self.keychainFetchError != nil, @"must have a keychain error to error with");
249
250 if(error) {
251 *error = self.keychainFetchError;
252 }
253 return false;
254 }
255
256
257 - (ZoneKeys*)createFakeKeyHierarchy: (CKRecordZoneID*)zoneID oldTLK:(CKKSKey*) oldTLK {
258 if(self.keys[zoneID]) {
259 // Already created. Skip.
260 return self.keys[zoneID];
261 }
262
263 NSError* error = nil;
264
265 ZoneKeys* zonekeys = [[ZoneKeys alloc] init];
266 zonekeys.viewName = zoneID.zoneName;
267
268 zonekeys.tlk = [self fakeTLK:zoneID];
269 [zonekeys.tlk CKRecordWithZoneID: zoneID]; // no-op here, but memoize in the object
270 zonekeys.currentTLKPointer = [[CKKSCurrentKeyPointer alloc] initForClass: SecCKKSKeyClassTLK currentKeyUUID: zonekeys.tlk.uuid zoneID:zoneID encodedCKRecord: nil];
271 [zonekeys.currentTLKPointer CKRecordWithZoneID: zoneID];
272
273 if(oldTLK) {
274 zonekeys.rolledTLK = oldTLK;
275 [zonekeys.rolledTLK wrapUnder: zonekeys.tlk error:&error];
276 XCTAssertNotNil(zonekeys.rolledTLK, "Created a rolled TLK");
277 XCTAssertNil(error, "No error creating rolled TLK");
278 }
279
280 zonekeys.classA = [CKKSKey randomKeyWrappedByParent: zonekeys.tlk keyclass:SecCKKSKeyClassA error:&error];
281 XCTAssertNotNil(zonekeys.classA, "make Class A key");
282 zonekeys.classA.currentkey = true;
283 [zonekeys.classA CKRecordWithZoneID: zoneID];
284 zonekeys.currentClassAPointer = [[CKKSCurrentKeyPointer alloc] initForClass: SecCKKSKeyClassA currentKeyUUID: zonekeys.classA.uuid zoneID:zoneID encodedCKRecord: nil];
285 [zonekeys.currentClassAPointer CKRecordWithZoneID: zoneID];
286
287 zonekeys.classC = [CKKSKey randomKeyWrappedByParent: zonekeys.tlk keyclass:SecCKKSKeyClassC error:&error];
288 XCTAssertNotNil(zonekeys.classC, "make Class C key");
289 zonekeys.classC.currentkey = true;
290 [zonekeys.classC CKRecordWithZoneID: zoneID];
291 zonekeys.currentClassCPointer = [[CKKSCurrentKeyPointer alloc] initForClass: SecCKKSKeyClassC currentKeyUUID: zonekeys.classC.uuid zoneID:zoneID encodedCKRecord: nil];
292 [zonekeys.currentClassCPointer CKRecordWithZoneID: zoneID];
293
294 self.keys[zoneID] = zonekeys;
295 return zonekeys;
296 }
297
298 - (void)saveFakeKeyHierarchyToLocalDatabase: (CKRecordZoneID*)zoneID {
299 ZoneKeys* zonekeys = [self createFakeKeyHierarchy: zoneID oldTLK:nil];
300
301 [CKKSSQLDatabaseObject performCKKSTransaction:^CKKSDatabaseTransactionResult {
302 NSError* error = nil;
303
304 [zonekeys.tlk saveToDatabase:&error];
305 XCTAssertNil(error, "TLK saved to database successfully");
306
307 [zonekeys.classA saveToDatabase:&error];
308 XCTAssertNil(error, "Class A key saved to database successfully");
309
310 [zonekeys.classC saveToDatabase:&error];
311 XCTAssertNil(error, "Class C key saved to database successfully");
312
313 [zonekeys.currentTLKPointer saveToDatabase:&error];
314 XCTAssertNil(error, "Current TLK pointer saved to database successfully");
315
316 [zonekeys.currentClassAPointer saveToDatabase:&error];
317 XCTAssertNil(error, "Current Class A pointer saved to database successfully");
318
319 [zonekeys.currentClassCPointer saveToDatabase:&error];
320 XCTAssertNil(error, "Current Class C pointer saved to database successfully");
321
322 return CKKSDatabaseTransactionCommit;
323 }];
324 }
325
326 - (void)putFakeDeviceStatusInCloudKit:(CKRecordZoneID*)zoneID zonekeys:(ZoneKeys*)zonekeys {
327 // SOS peer IDs are written bare, missing the CKKSSOSPeerPrefix. Strip it here.
328 NSString* peerID = self.remoteSOSOnlyPeer.peerID;
329 if([peerID hasPrefix:CKKSSOSPeerPrefix]) {
330 peerID = [peerID substringFromIndex:CKKSSOSPeerPrefix.length];
331 }
332
333 CKKSDeviceStateEntry* dse = [[CKKSDeviceStateEntry alloc] initForDevice:self.remoteSOSOnlyPeer.peerID
334 osVersion:@"faux-version"
335 lastUnlockTime:nil
336 octagonPeerID:nil
337 octagonStatus:nil
338 circlePeerID:peerID
339 circleStatus:kSOSCCInCircle
340 keyState:SecCKKSZoneKeyStateReady
341 currentTLKUUID:zonekeys.tlk.uuid
342 currentClassAUUID:zonekeys.classA.uuid
343 currentClassCUUID:zonekeys.classC.uuid
344 zoneID:zoneID
345 encodedCKRecord:nil];
346 [self.zones[zoneID] addToZone:dse zoneID:zoneID];
347 }
348
349 - (void)putFakeDeviceStatusInCloudKit:(CKRecordZoneID*)zoneID {
350 [self putFakeDeviceStatusInCloudKit:zoneID zonekeys:self.keys[zoneID]];
351 }
352
353 - (void)putFakeOctagonOnlyDeviceStatusInCloudKit:(CKRecordZoneID*)zoneID zonekeys:(ZoneKeys*)zonekeys {
354 CKKSDeviceStateEntry* dse = [[CKKSDeviceStateEntry alloc] initForDevice:self.remoteSOSOnlyPeer.peerID
355 osVersion:@"faux-version"
356 lastUnlockTime:nil
357 octagonPeerID:@"octagon-fake-peer-id"
358 octagonStatus:[[OTCliqueStatusWrapper alloc] initWithStatus:CliqueStatusIn]
359 circlePeerID:nil
360 circleStatus:kSOSCCError
361 keyState:SecCKKSZoneKeyStateReady
362 currentTLKUUID:zonekeys.tlk.uuid
363 currentClassAUUID:zonekeys.classA.uuid
364 currentClassCUUID:zonekeys.classC.uuid
365 zoneID:zoneID
366 encodedCKRecord:nil];
367 [self.zones[zoneID] addToZone:dse zoneID:zoneID];
368 }
369
370 - (void)putFakeOctagonOnlyDeviceStatusInCloudKit:(CKRecordZoneID*)zoneID {
371 [self putFakeOctagonOnlyDeviceStatusInCloudKit:zoneID zonekeys:self.keys[zoneID]];
372 }
373
374 - (void)putFakeKeyHierarchyInCloudKit: (CKRecordZoneID*)zoneID {
375 ZoneKeys* zonekeys = [self createFakeKeyHierarchy: zoneID oldTLK:nil];
376 XCTAssertNotNil(zonekeys, "failed to create fake key hierarchy for zoneID=%@", zoneID);
377 XCTAssertNil(zonekeys.error, "should have no error creating a zonekeys");
378
379 secnotice("fake-cloudkit", "new fake hierarchy: %@", zonekeys);
380
381 FakeCKZone* zone = self.zones[zoneID];
382 XCTAssertNotNil(zone, "failed to find zone %@", zoneID);
383
384 dispatch_sync(zone.queue, ^{
385 [zone _onqueueAddToZone:zonekeys.tlk zoneID:zoneID];
386 [zone _onqueueAddToZone:zonekeys.classA zoneID:zoneID];
387 [zone _onqueueAddToZone:zonekeys.classC zoneID:zoneID];
388
389 [zone _onqueueAddToZone:zonekeys.currentTLKPointer zoneID:zoneID];
390 [zone _onqueueAddToZone:zonekeys.currentClassAPointer zoneID:zoneID];
391 [zone _onqueueAddToZone:zonekeys.currentClassCPointer zoneID:zoneID];
392
393 if(zonekeys.rolledTLK) {
394 [zone _onqueueAddToZone:zonekeys.rolledTLK zoneID:zoneID];
395 }
396 });
397 }
398
399 - (void)rollFakeKeyHierarchyInCloudKit: (CKRecordZoneID*)zoneID {
400 ZoneKeys* zonekeys = self.keys[zoneID];
401 self.keys[zoneID] = nil;
402
403 CKKSKey* oldTLK = zonekeys.tlk;
404 NSError* error = nil;
405 [oldTLK ensureKeyLoaded:&error];
406 XCTAssertNil(error, "shouldn't error ensuring that the oldTLK has its key material");
407
408 [self createFakeKeyHierarchy: zoneID oldTLK:oldTLK];
409 [self putFakeKeyHierarchyInCloudKit: zoneID];
410 }
411
412 - (void)ensureZoneDeletionAllowed:(FakeCKZone*)zone {
413 [super ensureZoneDeletionAllowed:zone];
414
415 // Here's a hack: if we're deleting this zone, also drop the keys that used to be in it
416 self.keys[zone.zoneID] = nil;
417 }
418
419 - (NSArray<CKRecord*>*)putKeySetInCloudKit:(CKKSCurrentKeySet*)keyset
420 {
421 XCTAssertNotNil(keyset.tlk, "Should have a TLK to put a key set in CloudKit");
422 CKRecordZoneID* zoneID = keyset.tlk.zoneID;
423 XCTAssertNotNil(zoneID, "Should have a zoneID to put a key set in CloudKit");
424
425 ZoneKeys* zonekeys = self.keys[zoneID];
426 XCTAssertNil(zonekeys, "Should not already have zone keys when putting keyset in cloudkit");
427 zonekeys = [[ZoneKeys alloc] initForZoneName:zoneID.zoneName];
428
429 FakeCKZone* zone = self.zones[zoneID];
430 // Cuttlefish makes this for you, but for now assert if there's an issue
431 XCTAssertNotNil(zone, "Should already have a fakeckzone before putting a keyset in it");
432
433 __block NSMutableArray<CKRecord*>* newRecords = [NSMutableArray array];
434 dispatch_sync(zone.queue, ^{
435 [newRecords addObject:[zone _onqueueAddToZone:keyset.tlk zoneID:zoneID]];
436 [newRecords addObject:[zone _onqueueAddToZone:keyset.classA zoneID:zoneID]];
437 [newRecords addObject:[zone _onqueueAddToZone:keyset.classC zoneID:zoneID]];
438
439 [newRecords addObject:[zone _onqueueAddToZone:keyset.currentTLKPointer zoneID:zoneID]];
440 [newRecords addObject:[zone _onqueueAddToZone:keyset.currentClassAPointer zoneID:zoneID]];
441 [newRecords addObject:[zone _onqueueAddToZone:keyset.currentClassCPointer zoneID:zoneID]];
442
443 // TODO handle a rolled TLK
444 //if(zonekeys.rolledTLK) {
445 // [zone _onqueueAddToZone:zonekeys.rolledTLK zoneID:zoneID];
446 //}
447
448 zonekeys.tlk = keyset.tlk;
449 zonekeys.classA = keyset.classA;
450 zonekeys.classC = keyset.classC;
451 self.keys[zoneID] = zonekeys;
452
453 // Octagon uploads the pending TLKshares, not all of them
454 for(CKKSTLKShareRecord* tlkshare in keyset.pendingTLKShares) {
455 [newRecords addObject:[zone _onqueueAddToZone:tlkshare zoneID:zoneID]];
456 }
457 });
458
459 return newRecords;
460 }
461
462 - (void)performOctagonTLKUpload:(NSSet<CKKSKeychainView*>*)views
463 {
464 [self performOctagonTLKUpload:views afterUpload:nil];
465 }
466
467 - (void)performOctagonTLKUpload:(NSSet<CKKSKeychainView*>*)views afterUpload:(void (^_Nullable)(void))afterUpload
468 {
469 NSMutableArray<CKKSResultOperation<CKKSKeySetProviderOperationProtocol>*>* keysetOps = [NSMutableArray array];
470
471 for(CKKSKeychainView* view in views) {
472 XCTAssertEqual(0, [view.keyHierarchyConditions[SecCKKSZoneKeyStateWaitForTLKCreation] wait:40*NSEC_PER_SEC], @"key state should enter 'waitfortlkcreation' (view %@)", view);
473 [keysetOps addObject: [view findKeySet:NO]];
474 }
475
476 // Now that we've kicked them all off, wait for them to resolve
477 for(CKKSKeychainView* view in views) {
478 XCTAssertEqual(0, [view.keyHierarchyConditions[SecCKKSZoneKeyStateWaitForTLKUpload] wait:40*NSEC_PER_SEC], @"key state should enter 'waitfortlkupload'");
479 }
480
481 NSMutableArray<CKRecord*>* keyHierarchyRecords = [NSMutableArray array];
482
483 for(CKKSResultOperation<CKKSKeySetProviderOperationProtocol>* keysetOp in keysetOps) {
484 // Wait until finished is usually a bad idea. We could rip this out into an operation if we'd like.
485 [keysetOp waitUntilFinished];
486 XCTAssertNil(keysetOp.error, "Should be no error fetching keyset from CKKS");
487
488 NSArray<CKRecord*>* records = [self putKeySetInCloudKit:keysetOp.keyset];
489 [keyHierarchyRecords addObjectsFromArray:records];
490 }
491
492 if(afterUpload) {
493 afterUpload();
494 }
495
496 // Tell our views about our shiny new records!
497 for(CKKSKeychainView* view in views) {
498 [view receiveTLKUploadRecords: keyHierarchyRecords];
499 }
500 }
501
502 - (void)saveTLKMaterialToKeychainSimulatingSOS: (CKRecordZoneID*)zoneID {
503
504 XCTAssertNotNil(self.keys[zoneID].tlk, "Have a TLK to save for zone %@", zoneID);
505
506 __block CFErrorRef cferror = NULL;
507 kc_with_dbt(true, &cferror, ^bool (SecDbConnectionRef dbt) {
508 bool ok = kc_transaction_type(dbt, kSecDbExclusiveRemoteSOSTransactionType, &cferror, ^bool {
509 NSError* error = nil;
510 [self.keys[zoneID].tlk saveKeyMaterialToKeychain: false error:&error];
511 XCTAssertNil(error, @"Saved TLK material to keychain");
512 return true;
513 });
514 return ok;
515 });
516
517 XCTAssertNil( (__bridge NSError*)cferror, @"no error with transaction");
518 CFReleaseNull(cferror);
519 }
520 static SOSFullPeerInfoRef SOSCreateFullPeerInfoFromName(CFStringRef name,
521 SecKeyRef* outSigningKey,
522 SecKeyRef* outOctagonSigningKey,
523 SecKeyRef* outOctagonEncryptionKey,
524 CFErrorRef *error)
525 {
526 SOSFullPeerInfoRef result = NULL;
527 SecKeyRef publicKey = NULL;
528 CFDictionaryRef gestalt = NULL;
529
530 *outSigningKey = GeneratePermanentFullECKey(256, name, error);
531
532 *outOctagonSigningKey = GeneratePermanentFullECKey(384, name, error);
533 *outOctagonEncryptionKey = GeneratePermanentFullECKey(384, name, error);
534
535 gestalt = SOSCreatePeerGestaltFromName(name);
536
537 result = SOSFullPeerInfoCreate(NULL, gestalt, NULL, *outSigningKey,
538 *outOctagonSigningKey, *outOctagonEncryptionKey,
539 error);
540
541 CFReleaseNull(gestalt);
542 CFReleaseNull(publicKey);
543
544 return result;
545 }
546 - (NSMutableArray<NSData *>*) SOSPiggyICloudIdentities
547 {
548 SecKeyRef signingKey = NULL;
549 SecKeyRef octagonSigningKey = NULL;
550 SecKeyRef octagonEncryptionKey = NULL;
551 NSMutableArray<NSData *>* icloudidentities = [NSMutableArray array];
552
553 SOSFullPeerInfoRef fpi = SOSCreateFullPeerInfoFromName(CFSTR("Test Peer"), &signingKey, &octagonSigningKey, &octagonEncryptionKey, NULL);
554
555 NSData *data = CFBridgingRelease(SOSPeerInfoCopyData(SOSFullPeerInfoGetPeerInfo(fpi), NULL));
556 CFReleaseNull(fpi);
557 if (data)
558 [icloudidentities addObject:data];
559
560 CFReleaseNull(signingKey);
561 CFReleaseNull(octagonSigningKey);
562 CFReleaseNull(octagonEncryptionKey);
563
564 return icloudidentities;
565 }
566 static CFDictionaryRef SOSCreatePeerGestaltFromName(CFStringRef name)
567 {
568 return CFDictionaryCreateForCFTypes(kCFAllocatorDefault,
569 kPIUserDefinedDeviceNameKey, name,
570 NULL);
571 }
572 -(NSMutableDictionary*)SOSPiggyBackCopyFromKeychain
573 {
574 __block NSMutableDictionary *piggybackdata = [[NSMutableDictionary alloc] init];
575 __block CFErrorRef cferror = NULL;
576 kc_with_dbt(true, &cferror, ^bool (SecDbConnectionRef dbt) {
577 bool ok = kc_transaction_type(dbt, kSecDbExclusiveRemoteSOSTransactionType, &cferror, ^bool {
578 piggybackdata[@"idents"] = [self SOSPiggyICloudIdentities];
579 piggybackdata[@"tlk"] = SOSAccountGetAllTLKs();
580
581 return true;
582 });
583 return ok;
584 });
585
586 XCTAssertNil( (__bridge NSError*)cferror, @"no error with transaction");
587 CFReleaseNull(cferror);
588 return piggybackdata;
589 }
590
591 - (void)SOSPiggyBackAddToKeychain:(NSDictionary*)piggydata{
592 __block CFErrorRef cferror = NULL;
593 kc_with_dbt(true, &cferror, ^bool (SecDbConnectionRef dbt) {
594 bool ok = kc_transaction_type(dbt, kSecDbExclusiveRemoteSOSTransactionType, &cferror, ^bool {
595 NSError* error = nil;
596 NSArray* icloudidentities = piggydata[@"idents"];
597 NSArray* tlk = piggydata[@"tlk"];
598
599 SOSPiggyBackAddToKeychain(icloudidentities, tlk);
600
601 XCTAssertNil(error, @"Saved TLK-piggy material to keychain");
602 return true;
603 });
604 return ok;
605 });
606
607 XCTAssertNil( (__bridge NSError*)cferror, @"no error with transaction");
608 CFReleaseNull(cferror);
609 }
610
611 - (void)saveTLKMaterialToKeychain: (CKRecordZoneID*)zoneID {
612 NSError* error = nil;
613 XCTAssertNotNil(self.keys[zoneID].tlk, "Have a TLK to save for zone %@", zoneID);
614
615 // Don't make the stashed local copy of the TLK
616 [self.keys[zoneID].tlk saveKeyMaterialToKeychain:false error:&error];
617 XCTAssertNil(error, @"Saved TLK material to keychain");
618 }
619
620 - (void)deleteTLKMaterialFromKeychain: (CKRecordZoneID*)zoneID {
621 NSError* error = nil;
622 XCTAssertNotNil(self.keys[zoneID].tlk, "Have a TLK to save for zone %@", zoneID);
623 [self.keys[zoneID].tlk deleteKeyMaterialFromKeychain:&error];
624 XCTAssertNil(error, @"Saved TLK material to keychain");
625 }
626
627 - (void)saveClassKeyMaterialToKeychain: (CKRecordZoneID*)zoneID {
628 NSError* error = nil;
629 XCTAssertNotNil(self.keys[zoneID].classA, "Have a Class A to save for zone %@", zoneID);
630 [self.keys[zoneID].classA saveKeyMaterialToKeychain:&error];
631 XCTAssertNil(error, @"Saved Class A material to keychain");
632
633 XCTAssertNotNil(self.keys[zoneID].classC, "Have a Class C to save for zone %@", zoneID);
634 [self.keys[zoneID].classC saveKeyMaterialToKeychain:&error];
635 XCTAssertNil(error, @"Saved Class C material to keychain");
636 }
637
638
639 - (void)putTLKShareInCloudKit:(CKKSKey*)key
640 from:(id<CKKSSelfPeer>)sharingPeer
641 to:(id<CKKSPeer>)receivingPeer
642 zoneID:(CKRecordZoneID*)zoneID
643 {
644 NSError* error = nil;
645 CKKSTLKShareRecord* share = [CKKSTLKShareRecord share:key
646 as:sharingPeer
647 to:receivingPeer
648 epoch:-1
649 poisoned:0
650 error:&error];
651 XCTAssertNil(error, "Should have been no error sharing a CKKSKey");
652 XCTAssertNotNil(share, "Should be able to create a share");
653
654 CKRecord* shareRecord = [share CKRecordWithZoneID: zoneID];
655 XCTAssertNotNil(shareRecord, "Should have been able to create a CKRecord");
656
657 FakeCKZone* zone = self.zones[zoneID];
658 XCTAssertNotNil(zone, "Should have a zone to put a TLKShare in");
659 [zone addToZone:shareRecord];
660
661 ZoneKeys* keys = self.keys[zoneID];
662 XCTAssertNotNil(keys, "Have a zonekeys object for this zone");
663 keys.tlkShares = keys.tlkShares ? [keys.tlkShares arrayByAddingObject:share] : @[share];
664 }
665
666 - (void)putTLKSharesInCloudKit:(CKKSKey*)key
667 from:(id<CKKSSelfPeer>)sharingPeer
668 zoneID:(CKRecordZoneID*)zoneID
669 {
670 NSSet* peers = [self.mockSOSAdapter.trustedPeers setByAddingObject:self.mockSOSAdapter.selfPeer];
671
672 for(id<CKKSPeer> peer in peers) {
673 // Can only send to peers with encryption keys
674 if(peer.publicEncryptionKey) {
675 [self putTLKShareInCloudKit:key from:sharingPeer to:peer zoneID:zoneID];
676 }
677 }
678 }
679
680 - (void)putSelfTLKSharesInCloudKit:(CKRecordZoneID*)zoneID {
681 CKKSKey* tlk = self.keys[zoneID].tlk;
682 XCTAssertNotNil(tlk, "Should have a TLK for zone %@", zoneID);
683 [self putTLKSharesInCloudKit:tlk from:self.mockSOSAdapter.selfPeer zoneID:zoneID];
684 }
685
686 - (void)saveTLKSharesInLocalDatabase:(CKRecordZoneID*)zoneID {
687 ZoneKeys* keys = self.keys[zoneID];
688 XCTAssertNotNil(keys, "Have a zonekeys object for this zone");
689
690 [CKKSSQLDatabaseObject performCKKSTransaction:^CKKSDatabaseTransactionResult{
691 for(CKKSTLKShareRecord* share in keys.tlkShares) {
692 NSError* error = nil;
693 [share saveToDatabase:&error];
694 XCTAssertNil(error, "Shouldn't have been an error saving a TLKShare to the database");
695 }
696
697 return CKKSDatabaseTransactionCommit;
698 }];
699 }
700
701 - (void)createAndSaveFakeKeyHierarchy: (CKRecordZoneID*)zoneID {
702 // Put in CloudKit first, so the records on-disk will have the right change tags
703 [self putFakeKeyHierarchyInCloudKit: zoneID];
704 [self saveFakeKeyHierarchyToLocalDatabase: zoneID];
705 [self saveTLKMaterialToKeychain: zoneID];
706 [self saveClassKeyMaterialToKeychain: zoneID];
707
708 [self putSelfTLKSharesInCloudKit:zoneID];
709 [self saveTLKSharesInLocalDatabase:zoneID];
710 }
711
712 // Override our base class here, but only for Keychain Views
713
714 - (void)expectCKModifyRecords:(NSDictionary<NSString*, NSNumber*>*)expectedRecordTypeCounts
715 deletedRecordTypeCounts:(NSDictionary<NSString*, NSNumber*>*)expectedDeletedRecordTypeCounts
716 zoneID:(CKRecordZoneID*)zoneID
717 checkModifiedRecord:(BOOL (^)(CKRecord*))checkRecord
718 runAfterModification:(void (^) (void))afterModification
719 {
720
721 void (^newAfterModification)(void) = afterModification;
722 if([self.ckksZones containsObject:zoneID]) {
723 __weak __typeof(self) weakSelf = self;
724 newAfterModification = ^{
725 __strong __typeof(weakSelf) strongSelf = weakSelf;
726 XCTAssertNotNil(strongSelf, "self exists");
727
728 // Reach into our cloudkit database and extract the keys
729 CKRecordID* currentTLKPointerID = [[CKRecordID alloc] initWithRecordName:SecCKKSKeyClassTLK zoneID:zoneID];
730 CKRecordID* currentClassAPointerID = [[CKRecordID alloc] initWithRecordName:SecCKKSKeyClassA zoneID:zoneID];
731 CKRecordID* currentClassCPointerID = [[CKRecordID alloc] initWithRecordName:SecCKKSKeyClassC zoneID:zoneID];
732
733 ZoneKeys* zonekeys = strongSelf.keys[zoneID];
734 if(!zonekeys) {
735 zonekeys = [[ZoneKeys alloc] init];
736 strongSelf.keys[zoneID] = zonekeys;
737 }
738
739 XCTAssertNotNil(strongSelf.zones[zoneID].currentDatabase[currentTLKPointerID], "Have a currentTLKPointer");
740 XCTAssertNotNil(strongSelf.zones[zoneID].currentDatabase[currentClassAPointerID], "Have a currentClassAPointer");
741 XCTAssertNotNil(strongSelf.zones[zoneID].currentDatabase[currentClassCPointerID], "Have a currentClassCPointer");
742 XCTAssertNotNil(strongSelf.zones[zoneID].currentDatabase[currentTLKPointerID][SecCKRecordParentKeyRefKey], "Have a currentTLKPointer parent");
743 XCTAssertNotNil(strongSelf.zones[zoneID].currentDatabase[currentClassAPointerID][SecCKRecordParentKeyRefKey], "Have a currentClassAPointer parent");
744 XCTAssertNotNil(strongSelf.zones[zoneID].currentDatabase[currentClassCPointerID][SecCKRecordParentKeyRefKey], "Have a currentClassCPointer parent");
745 XCTAssertNotNil([strongSelf.zones[zoneID].currentDatabase[currentTLKPointerID][SecCKRecordParentKeyRefKey] recordID].recordName, "Have a currentTLKPointer parent UUID");
746 XCTAssertNotNil([strongSelf.zones[zoneID].currentDatabase[currentClassAPointerID][SecCKRecordParentKeyRefKey] recordID].recordName, "Have a currentClassAPointer parent UUID");
747 XCTAssertNotNil([strongSelf.zones[zoneID].currentDatabase[currentClassCPointerID][SecCKRecordParentKeyRefKey] recordID].recordName, "Have a currentClassCPointer parent UUID");
748
749 zonekeys.currentTLKPointer = [[CKKSCurrentKeyPointer alloc] initWithCKRecord: strongSelf.zones[zoneID].currentDatabase[currentTLKPointerID]];
750 zonekeys.currentClassAPointer = [[CKKSCurrentKeyPointer alloc] initWithCKRecord: strongSelf.zones[zoneID].currentDatabase[currentClassAPointerID]];
751 zonekeys.currentClassCPointer = [[CKKSCurrentKeyPointer alloc] initWithCKRecord: strongSelf.zones[zoneID].currentDatabase[currentClassCPointerID]];
752
753 XCTAssertNotNil(zonekeys.currentTLKPointer.currentKeyUUID, "Have a currentTLKPointer current UUID");
754 XCTAssertNotNil(zonekeys.currentClassAPointer.currentKeyUUID, "Have a currentClassAPointer current UUID");
755 XCTAssertNotNil(zonekeys.currentClassCPointer.currentKeyUUID, "Have a currentClassCPointer current UUID");
756
757 CKRecordID* currentTLKID = [[CKRecordID alloc] initWithRecordName:zonekeys.currentTLKPointer.currentKeyUUID zoneID:zoneID];
758 CKRecordID* currentClassAID = [[CKRecordID alloc] initWithRecordName:zonekeys.currentClassAPointer.currentKeyUUID zoneID:zoneID];
759 CKRecordID* currentClassCID = [[CKRecordID alloc] initWithRecordName:zonekeys.currentClassCPointer.currentKeyUUID zoneID:zoneID];
760
761 zonekeys.tlk = [[CKKSKey alloc] initWithCKRecord: strongSelf.zones[zoneID].currentDatabase[currentTLKID]];
762 zonekeys.classA = [[CKKSKey alloc] initWithCKRecord: strongSelf.zones[zoneID].currentDatabase[currentClassAID]];
763 zonekeys.classC = [[CKKSKey alloc] initWithCKRecord: strongSelf.zones[zoneID].currentDatabase[currentClassCID]];
764
765 XCTAssertNotNil(zonekeys.tlk, "Have the current TLK");
766 XCTAssertNotNil(zonekeys.classA, "Have the current Class A key");
767 XCTAssertNotNil(zonekeys.classC, "Have the current Class C key");
768
769 NSMutableArray<CKKSTLKShareRecord*>* shares = [NSMutableArray array];
770 for(CKRecordID* recordID in strongSelf.zones[zoneID].currentDatabase.allKeys) {
771 if([recordID.recordName hasPrefix: [CKKSTLKShareRecord ckrecordPrefix]]) {
772 CKKSTLKShareRecord* share = [[CKKSTLKShareRecord alloc] initWithCKRecord:strongSelf.zones[zoneID].currentDatabase[recordID]];
773 XCTAssertNotNil(share, "Should be able to parse a CKKSTLKShare CKRecord into a CKKSTLKShare");
774 [shares addObject:share];
775 }
776 }
777 zonekeys.tlkShares = shares;
778
779 if(afterModification) {
780 afterModification();
781 }
782 };
783 }
784
785 [super expectCKModifyRecords:expectedRecordTypeCounts
786 deletedRecordTypeCounts:expectedDeletedRecordTypeCounts
787 zoneID:zoneID
788 checkModifiedRecord:checkRecord
789 runAfterModification:newAfterModification];
790 }
791
792 - (void)expectCKReceiveSyncKeyHierarchyError:(CKRecordZoneID*)zoneID {
793
794 __weak __typeof(self) weakSelf = self;
795 [[self.mockDatabase expect] addOperation:[OCMArg checkWithBlock:^BOOL(id obj) {
796 __strong __typeof(weakSelf) strongSelf = weakSelf;
797 XCTAssertNotNil(strongSelf, "self exists");
798
799 __block bool rejected = false;
800 if ([obj isKindOfClass:[CKModifyRecordsOperation class]]) {
801 CKModifyRecordsOperation *op = (CKModifyRecordsOperation *)obj;
802
803 if(!op.atomic) {
804 // We only care about atomic operations
805 return NO;
806 }
807
808 // We want to only match zone updates pertaining to this zone
809 for(CKRecord* record in op.recordsToSave) {
810 if(![record.recordID.zoneID isEqual: zoneID]) {
811 return NO;
812 }
813 }
814
815 // we oly want to match updates that are updating a class C or class A CKP
816 bool updatingClassACKP = false;
817 bool updatingClassCCKP = false;
818 for(CKRecord* record in op.recordsToSave) {
819 if([record.recordID.recordName isEqualToString:SecCKKSKeyClassA]) {
820 updatingClassACKP = true;
821 }
822 if([record.recordID.recordName isEqualToString:SecCKKSKeyClassC]) {
823 updatingClassCCKP = true;
824 }
825 }
826
827 if(!updatingClassACKP && !updatingClassCCKP) {
828 return NO;
829 }
830
831 FakeCKZone* zone = strongSelf.zones[zoneID];
832 XCTAssertNotNil(zone, "Should have a zone for these records");
833
834 // We only want to match if the synckeys aren't pointing correctly
835
836 ZoneKeys* zonekeys = [[ZoneKeys alloc] initLoadingRecordsFromZone:zone];
837
838 XCTAssertNotNil(zonekeys.currentTLKPointer, "Have a currentTLKPointer");
839 XCTAssertNotNil(zonekeys.currentClassAPointer, "Have a currentClassAPointer");
840 XCTAssertNotNil(zonekeys.currentClassCPointer, "Have a currentClassCPointer");
841
842 XCTAssertNotNil(zonekeys.tlk, "Have the current TLK");
843 XCTAssertNotNil(zonekeys.classA, "Have the current Class A key");
844 XCTAssertNotNil(zonekeys.classC, "Have the current Class C key");
845
846 // Ensure that either the Class A synckey or the class C synckey do not immediately wrap to the current TLK
847 bool classALinkBroken = ![zonekeys.classA.parentKeyUUID isEqualToString:zonekeys.tlk.uuid];
848 bool classCLinkBroken = ![zonekeys.classC.parentKeyUUID isEqualToString:zonekeys.tlk.uuid];
849
850 // Neither synckey link is broken. Don't match this operation.
851 if(!classALinkBroken && !classCLinkBroken) {
852 return NO;
853 }
854
855 NSMutableDictionary<CKRecordID*, NSError*>* failedRecords = [[NSMutableDictionary alloc] init];
856
857 @synchronized(zone.currentDatabase) {
858 for(CKRecord* record in op.recordsToSave) {
859 if(classALinkBroken && [record.recordID.recordName isEqualToString:SecCKKSKeyClassA]) {
860 failedRecords[record.recordID] = [strongSelf ckInternalServerExtensionError:CKKSServerUnexpectedSyncKeyInChain description:@"synckey record: current classA synckey does not point to current tlk synckey"];
861 rejected = true;
862 }
863 if(classCLinkBroken && [record.recordID.recordName isEqualToString:SecCKKSKeyClassC]) {
864 failedRecords[record.recordID] = [strongSelf ckInternalServerExtensionError:CKKSServerUnexpectedSyncKeyInChain description:@"synckey record: current classC synckey does not point to current tlk synckey"];
865 rejected = true;
866 }
867 }
868 }
869
870 if(rejected) {
871 [strongSelf rejectWrite: op failedRecords:failedRecords];
872 }
873 }
874 return rejected ? YES : NO;
875 }]];
876 }
877
878 - (void)checkNoCKKSData: (CKKSKeychainView*) view {
879 // Test that there are no items in the database
880 [view dispatchSyncWithReadOnlySQLTransaction:^{
881 NSError* error = nil;
882 NSArray<CKKSMirrorEntry*>* ckmes = [CKKSMirrorEntry all: view.zoneID error:&error];
883 XCTAssertNil(error, "No error fetching CKMEs");
884 XCTAssertEqual(ckmes.count, 0ul, "No CKMirrorEntries");
885
886 NSArray<CKKSOutgoingQueueEntry*>* oqes = [CKKSOutgoingQueueEntry all: view.zoneID error:&error];
887 XCTAssertNil(error, "No error fetching OQEs");
888 XCTAssertEqual(oqes.count, 0ul, "No OutgoingQueueEntries");
889
890 NSArray<CKKSIncomingQueueEntry*>* iqes = [CKKSIncomingQueueEntry all: view.zoneID error:&error];
891 XCTAssertNil(error, "No error fetching IQEs");
892 XCTAssertEqual(iqes.count, 0ul, "No IncomingQueueEntries");
893
894 NSArray<CKKSKey*>* keys = [CKKSKey all: view.zoneID error:&error];
895 XCTAssertNil(error, "No error fetching keys");
896 XCTAssertEqual(keys.count, 0ul, "No CKKSKeys");
897
898 NSArray<CKKSDeviceStateEntry*>* deviceStates = [CKKSDeviceStateEntry allInZone:view.zoneID error:&error];
899 XCTAssertNil(error, "should be no error fetching device states");
900 XCTAssertEqual(deviceStates.count, 0ul, "No Device State entries");
901 }];
902 }
903
904 - (BOOL (^) (CKRecord*)) checkClassABlock: (CKRecordZoneID*) zoneID message:(NSString*) message {
905 __weak __typeof(self) weakSelf = self;
906 return ^BOOL(CKRecord* record) {
907 __strong __typeof(weakSelf) strongSelf = weakSelf;
908 XCTAssertNotNil(strongSelf, "self exists");
909
910 ZoneKeys* zoneKeys = strongSelf.keys[zoneID];
911 XCTAssertNotNil(zoneKeys, "Have zone keys for %@", zoneID);
912 XCTAssertEqualObjects([record[SecCKRecordParentKeyRefKey] recordID].recordName, zoneKeys.classA.uuid, "%@", message);
913 return [[record[SecCKRecordParentKeyRefKey] recordID].recordName isEqual: zoneKeys.classA.uuid];
914 };
915 }
916
917 - (BOOL (^) (CKRecord*)) checkClassCBlock: (CKRecordZoneID*) zoneID message:(NSString*) message {
918 __weak __typeof(self) weakSelf = self;
919 return ^BOOL(CKRecord* record) {
920 __strong __typeof(weakSelf) strongSelf = weakSelf;
921 XCTAssertNotNil(strongSelf, "self exists");
922
923 ZoneKeys* zoneKeys = strongSelf.keys[zoneID];
924 XCTAssertNotNil(zoneKeys, "Have zone keys for %@", zoneID);
925 XCTAssertEqualObjects([record[SecCKRecordParentKeyRefKey] recordID].recordName, zoneKeys.classC.uuid, "%@", message);
926 return [[record[SecCKRecordParentKeyRefKey] recordID].recordName isEqual: zoneKeys.classC.uuid];
927 };
928 }
929
930 - (BOOL (^) (CKRecord*)) checkPasswordBlock:(CKRecordZoneID*)zoneID
931 account:(NSString*)account
932 password:(NSString*)password {
933 __weak __typeof(self) weakSelf = self;
934 return ^BOOL(CKRecord* record) {
935 __strong __typeof(weakSelf) strongSelf = weakSelf;
936 XCTAssertNotNil(strongSelf, "self exists");
937
938 ZoneKeys* zoneKeys = strongSelf.keys[zoneID];
939 XCTAssertNotNil(zoneKeys, "Have zone keys for %@", zoneID);
940 XCTAssertNotNil([record[SecCKRecordParentKeyRefKey] recordID].recordName, "Have a wrapping key");
941
942 CKKSKey* key = nil;
943 if([[record[SecCKRecordParentKeyRefKey] recordID].recordName isEqualToString: zoneKeys.classC.uuid]) {
944 key = zoneKeys.classC;
945 } else if([[record[SecCKRecordParentKeyRefKey] recordID].recordName isEqualToString: zoneKeys.classA.uuid]) {
946 key = zoneKeys.classA;
947 }
948 XCTAssertNotNil(key, "Found a key via UUID");
949
950 CKKSMirrorEntry* ckme = [[CKKSMirrorEntry alloc] initWithCKRecord: record];
951
952 NSError* error = nil;
953 NSDictionary* dict = [CKKSItemEncrypter decryptItemToDictionary:ckme.item error:&error];
954 XCTAssertNil(error, "No error decrypting item");
955 XCTAssertEqualObjects(account, dict[(id)kSecAttrAccount], "Account matches");
956 XCTAssertEqualObjects([password dataUsingEncoding:NSUTF8StringEncoding], dict[(id)kSecValueData], "Password matches");
957 return YES;
958 };
959 }
960
961 - (NSDictionary*)fakeRecordDictionary:(NSString*) account zoneID:(CKRecordZoneID*)zoneID {
962 NSError* error = nil;
963
964 /* Basically: @{
965 @"acct" : @"account-delete-me",
966 @"agrp" : @"com.apple.security.sos",
967 @"cdat" : @"2016-12-21 03:33:25 +0000",
968 @"class" : @"genp",
969 @"mdat" : @"2016-12-21 03:33:25 +0000",
970 @"musr" : [[NSData alloc] init],
971 @"pdmn" : @"ak",
972 @"sha1" : [[NSData alloc] initWithBase64EncodedString: @"C3VWONaOIj8YgJjk/xwku4By1CY=" options:0],
973 @"svce" : @"",
974 @"tomb" : [NSNumber numberWithInt: 0],
975 @"v_Data" : [@"data" dataUsingEncoding: NSUTF8StringEncoding],
976 };
977 TODO: this should be binary encoded instead of expanded, but the plist encoder should handle it fine */
978 NSData* itemdata = [[NSData alloc] initWithBase64EncodedString:@"PD94bWwgdmVyc2lvbj0iMS4wIiBlbmNvZGluZz0iVVRGLTgiPz4KPCFET0NUWVBFIHBsaXN0IFBVQkxJQyAiLS8vQXBwbGUvL0RURCBQTElTVCAxLjAvL0VOIiAiaHR0cDovL3d3dy5hcHBsZS5jb20vRFREcy9Qcm9wZXJ0eUxpc3QtMS4wLmR0ZCI+CjxwbGlzdCB2ZXJzaW9uPSIxLjAiPgo8ZGljdD4KCTxrZXk+YWNjdDwva2V5PgoJPHN0cmluZz5hY2NvdW50LWRlbGV0ZS1tZTwvc3RyaW5nPgoJPGtleT5hZ3JwPC9rZXk+Cgk8c3RyaW5nPmNvbS5hcHBsZS5zZWN1cml0eS5zb3M8L3N0cmluZz4KCTxrZXk+Y2RhdDwva2V5PgoJPGRhdGU+MjAxNi0xMi0yMVQwMzozMzoyNVo8L2RhdGU+Cgk8a2V5PmNsYXNzPC9rZXk+Cgk8c3RyaW5nPmdlbnA8L3N0cmluZz4KCTxrZXk+bWRhdDwva2V5PgoJPGRhdGU+MjAxNi0xMi0yMVQwMzozMzoyNVo8L2RhdGU+Cgk8a2V5Pm11c3I8L2tleT4KCTxkYXRhPgoJPC9kYXRhPgoJPGtleT5wZG1uPC9rZXk+Cgk8c3RyaW5nPmFrPC9zdHJpbmc+Cgk8a2V5PnNoYTE8L2tleT4KCTxkYXRhPgoJQzNWV09OYU9JajhZZ0pqay94d2t1NEJ5MUNZPQoJPC9kYXRhPgoJPGtleT5zdmNlPC9rZXk+Cgk8c3RyaW5nPjwvc3RyaW5nPgoJPGtleT50b21iPC9rZXk+Cgk8aW50ZWdlcj4wPC9pbnRlZ2VyPgoJPGtleT52X0RhdGE8L2tleT4KCTxkYXRhPgoJWkdGMFlRPT0KCTwvZGF0YT4KPC9kaWN0Pgo8L3BsaXN0Pgo=" options:0];
979 NSMutableDictionary * item = [[NSPropertyListSerialization propertyListWithData:itemdata
980 options:0
981 format:nil
982 error:&error] mutableCopy];
983 // Fix up dictionary
984 item[@"agrp"] = @"com.apple.security.ckks";
985 item[@"vwht"] = @"keychain";
986 XCTAssertNil(error, "no error interpreting data as item");
987 XCTAssertNotNil(item, "interpreted data as item");
988
989 if(zoneID && ![zoneID.zoneName isEqualToString:@"keychain"]) {
990 [item setObject: zoneID.zoneName forKey: (__bridge id) kSecAttrSyncViewHint];
991 }
992
993 if(account) {
994 [item setObject: account forKey: (__bridge id) kSecAttrAccount];
995 }
996 return item;
997 }
998
999
1000 - (CKRecord*)createFakeTombstoneRecord:(CKRecordZoneID*)zoneID recordName:(NSString*)recordName account:(NSString*)account {
1001 NSMutableDictionary* item = [[self fakeRecordDictionary:account zoneID:zoneID] mutableCopy];
1002 item[@"tomb"] = @YES;
1003
1004 CKRecordID* ckrid = [[CKRecordID alloc] initWithRecordName:recordName zoneID:zoneID];
1005 return [self newRecord:ckrid withNewItemData:item];
1006 }
1007
1008 - (CKRecord*)createFakeRecord: (CKRecordZoneID*)zoneID recordName:(NSString*)recordName {
1009 return [self createFakeRecord: zoneID recordName:recordName withAccount: nil key:nil];
1010 }
1011
1012 - (CKRecord*)createFakeRecord: (CKRecordZoneID*)zoneID recordName:(NSString*)recordName withAccount: (NSString*) account {
1013 return [self createFakeRecord: zoneID recordName:recordName withAccount:account key:nil];
1014 }
1015
1016 - (CKRecord*)createFakeRecord: (CKRecordZoneID*)zoneID recordName:(NSString*)recordName withAccount: (NSString*) account key:(CKKSKey*)key {
1017 NSMutableDictionary* item = [[self fakeRecordDictionary: account zoneID:zoneID] mutableCopy];
1018
1019 // class c items should be class c
1020 if([key.keyclass isEqualToString:SecCKKSKeyClassC]) {
1021 item[(__bridge NSString*)kSecAttrAccessible] = @"ck";
1022 }
1023
1024 CKRecordID* ckrid = [[CKRecordID alloc] initWithRecordName:recordName zoneID:zoneID];
1025 if(key) {
1026 return [self newRecord: ckrid withNewItemData:item key:key];
1027 } else {
1028 return [self newRecord: ckrid withNewItemData:item];
1029 }
1030 }
1031
1032 - (CKRecord*)newRecord: (CKRecordID*) recordID withNewItemData:(NSDictionary*) dictionary {
1033 ZoneKeys* zonekeys = self.keys[recordID.zoneID];
1034 XCTAssertNotNil(zonekeys, "Have zone keys for zone");
1035 XCTAssertNotNil(zonekeys.classC, "Have class C key for zone");
1036
1037 return [self newRecord:recordID withNewItemData:dictionary key:zonekeys.classC];
1038 }
1039
1040 - (CKKSItem*)newItem:(CKRecordID*)recordID withNewItemData:(NSDictionary*)dictionary key:(CKKSKey*)key {
1041 NSError* error = nil;
1042 CKKSItem* cipheritem = [CKKSItemEncrypter encryptCKKSItem:[[CKKSItem alloc] initWithUUID:recordID.recordName
1043 parentKeyUUID:key.uuid
1044 zoneID:recordID.zoneID]
1045 dataDictionary:dictionary
1046 updatingCKKSItem:nil
1047 parentkey:key
1048 error:&error];
1049 XCTAssertNil(error, "encrypted item with class c key");
1050 XCTAssertNotNil(cipheritem, "Have an encrypted item");
1051
1052 return cipheritem;
1053 }
1054
1055 - (CKRecord*)newRecord: (CKRecordID*) recordID withNewItemData:(NSDictionary*) dictionary key:(CKKSKey*)key {
1056 CKKSItem* item = [self newItem:recordID withNewItemData:dictionary key:key];
1057
1058 CKRecord* ckr = [item CKRecordWithZoneID: recordID.zoneID];
1059 XCTAssertNotNil(ckr, "Created a CKRecord");
1060 return ckr;
1061 }
1062
1063 - (void)addItemToCloudKitZone:(NSDictionary*)itemDict recordName:(NSString*)recordName zoneID:(CKRecordZoneID*)zoneID
1064 {
1065 FakeCKZone* zone = self.zones[zoneID];
1066 XCTAssertNotNil(zone, "Should have a zone for %@", zoneID);
1067
1068 CKRecordID* ckrid = [[CKRecordID alloc] initWithRecordName:recordName zoneID:zoneID];
1069 CKRecord* record = [self newRecord:ckrid withNewItemData:itemDict];
1070
1071 [zone addToZone:record];
1072 }
1073
1074 - (NSDictionary*)decryptRecord: (CKRecord*) record {
1075 CKKSItem* item = [[CKKSItem alloc] initWithCKRecord: record];
1076
1077 NSError* error = nil;
1078
1079 NSDictionary* ret = [CKKSItemEncrypter decryptItemToDictionary: item error:&error];
1080 XCTAssertNil(error);
1081 XCTAssertNotNil(ret);
1082 return ret;
1083 }
1084
1085 - (BOOL)addGenericPassword:(NSString*)password
1086 account:(NSString*)account
1087 access:(NSString*)access
1088 viewHint:(NSString* _Nullable)viewHint
1089 accessGroup:(NSString* _Nullable)accessGroup
1090 expecting:(OSStatus)status
1091 message:(NSString*)message
1092 {
1093 NSMutableDictionary* query = [@{
1094 (id)kSecClass : (id)kSecClassGenericPassword,
1095 (id)kSecAttrAccessible: (id)access,
1096 (id)kSecAttrAccount : account,
1097 (id)kSecAttrSynchronizable : (id)kCFBooleanTrue,
1098 (id)kSecValueData : (id) [password dataUsingEncoding:NSUTF8StringEncoding],
1099 } mutableCopy];
1100
1101 query[(id)kSecAttrAccessGroup] = accessGroup ?: @"com.apple.security.ckks";
1102
1103 if(viewHint) {
1104 query[(id)kSecAttrSyncViewHint] = viewHint;
1105 } else {
1106 // Fake it as 'keychain'. This lets CKKSScanLocalItemsOperation for the test-only 'keychain' view find items which would normally not have a view hint.
1107 query[(id)kSecAttrSyncViewHint] = @"keychain";
1108 }
1109
1110 XCTAssertEqual(status, SecItemAdd((__bridge CFDictionaryRef) query, NULL), @"%@", message);
1111 }
1112
1113 - (void)addGenericPassword: (NSString*) password account: (NSString*) account viewHint: (NSString*) viewHint access: (NSString*) access expecting: (OSStatus) status message: (NSString*) message {
1114 [self addGenericPassword:password
1115 account:account
1116 access:access
1117 viewHint:viewHint
1118 accessGroup:nil
1119 expecting:status
1120 message:message];
1121 }
1122
1123 - (void)addGenericPassword: (NSString*) password account: (NSString*) account expecting: (OSStatus) status message: (NSString*) message {
1124 [self addGenericPassword:password account:account viewHint:nil access:(id)kSecAttrAccessibleAfterFirstUnlock expecting:errSecSuccess message:message];
1125
1126 }
1127
1128 - (void)addGenericPassword: (NSString*) password account: (NSString*) account {
1129 [self addGenericPassword:password account:account viewHint:nil access:(id)kSecAttrAccessibleAfterFirstUnlock expecting:errSecSuccess message:@"Add item to keychain"];
1130 }
1131
1132 - (void)addGenericPassword: (NSString*) password account: (NSString*) account viewHint:(NSString*)viewHint {
1133 [self addGenericPassword:password account:account viewHint:viewHint access:(id)kSecAttrAccessibleAfterFirstUnlock expecting:errSecSuccess message:@"Add item to keychain with a viewhint"];
1134 }
1135
1136
1137 - (void)addGenericPassword:(NSString*)password account:(NSString*)account accessGroup:(NSString*)accessGroup
1138 {
1139 [self addGenericPassword:password
1140 account:account
1141 access:(id)kSecAttrAccessibleAfterFirstUnlock
1142 viewHint:nil
1143 accessGroup:accessGroup
1144 expecting:errSecSuccess
1145 message:@"Add item to keychain with an access group"];
1146 }
1147
1148 - (void)updateGenericPassword: (NSString*) newPassword account: (NSString*)account {
1149 NSDictionary* query = @{
1150 (id)kSecClass : (id)kSecClassGenericPassword,
1151 (id)kSecAttrAccessGroup : @"com.apple.security.ckks",
1152 (id)kSecAttrAccount : account,
1153 (id)kSecAttrSynchronizable : (id)kCFBooleanTrue,
1154 };
1155
1156 NSDictionary* update = @{
1157 (id)kSecValueData : (id) [newPassword dataUsingEncoding:NSUTF8StringEncoding],
1158 };
1159 XCTAssertEqual(errSecSuccess, SecItemUpdate((__bridge CFDictionaryRef) query, (__bridge CFDictionaryRef) update), @"Updating item %@ to %@", account, newPassword);
1160 }
1161
1162 - (void)updateAccountOfGenericPassword:(NSString*)newAccount
1163 account:(NSString*)account {
1164 NSDictionary* query = @{
1165 (id)kSecClass : (id)kSecClassGenericPassword,
1166 (id)kSecAttrAccessGroup : @"com.apple.security.ckks",
1167 (id)kSecAttrAccount : account,
1168 (id)kSecAttrSynchronizable : (id)kCFBooleanTrue,
1169 };
1170
1171 NSDictionary* update = @{
1172 (id)kSecAttrAccount : (id) newAccount,
1173 };
1174 XCTAssertEqual(errSecSuccess, SecItemUpdate((__bridge CFDictionaryRef) query, (__bridge CFDictionaryRef) update), @"Updating item %@ to account %@", account, newAccount);
1175 }
1176
1177 - (void)deleteGenericPassword: (NSString*) account {
1178 NSDictionary* query = @{
1179 (id)kSecClass : (id)kSecClassGenericPassword,
1180 (id)kSecAttrAccount : account,
1181 (id)kSecAttrSynchronizable : (id)kCFBooleanTrue,
1182 };
1183
1184 XCTAssertEqual(errSecSuccess, SecItemDelete((__bridge CFDictionaryRef) query), @"Deleting item %@", account);
1185 }
1186
1187 - (void)deleteGenericPasswordWithoutTombstones:(NSString*)account {
1188 NSDictionary* query = @{
1189 (id)kSecClass : (id)kSecClassGenericPassword,
1190 (id)kSecAttrAccount : account,
1191 (id)kSecAttrSynchronizable : (id)kCFBooleanTrue,
1192 (id)kSecUseTombstones: @NO,
1193 };
1194
1195 XCTAssertEqual(errSecSuccess, SecItemDelete((__bridge CFDictionaryRef) query), @"Deleting item %@", account);
1196 }
1197
1198 - (void)findGenericPassword: (NSString*) account expecting: (OSStatus) status {
1199 NSDictionary *query = @{(id)kSecClass : (id)kSecClassGenericPassword,
1200 (id)kSecAttrAccessGroup : @"com.apple.security.ckks",
1201 (id)kSecAttrAccount : account,
1202 (id)kSecAttrSynchronizable : (id)kCFBooleanTrue,
1203 (id)kSecMatchLimit : (id)kSecMatchLimitOne,
1204 };
1205 XCTAssertEqual(status, SecItemCopyMatching((__bridge CFDictionaryRef) query, NULL), "Finding item %@", account);
1206 }
1207
1208 - (void)checkGenericPassword: (NSString*) password account: (NSString*) account {
1209 NSDictionary *query = @{(id)kSecClass : (id)kSecClassGenericPassword,
1210 (id)kSecAttrAccessGroup : @"com.apple.security.ckks",
1211 (id)kSecAttrAccount : account,
1212 (id)kSecAttrSynchronizable : (id)kCFBooleanTrue,
1213 (id)kSecMatchLimit : (id)kSecMatchLimitOne,
1214 (id)kSecReturnData : (id)kCFBooleanTrue,
1215 };
1216 CFTypeRef result = NULL;
1217
1218 XCTAssertEqual(errSecSuccess, SecItemCopyMatching((__bridge CFDictionaryRef) query, &result), "Item %@ should exist", account);
1219 XCTAssertNotNil((__bridge id)result, "Should have received an item");
1220
1221 NSString* storedPassword = [[NSString alloc] initWithData: (__bridge NSData*) result encoding: NSUTF8StringEncoding];
1222 XCTAssertNotNil(storedPassword, "Password should parse as a UTF8 password");
1223
1224 XCTAssertEqualObjects(storedPassword, password, "Stored password should match received password");
1225 }
1226
1227 - (void)checkGenericPasswordStoredUUID:(NSString*)uuid account:(NSString*)account {
1228 NSDictionary* queryAttributes = @{(id)kSecClass: (id)kSecClassGenericPassword,
1229 (id)kSecAttrAccessGroup : @"com.apple.security.ckks",
1230 (id)kSecAttrAccount : account,
1231 (id)kSecAttrSynchronizable: @(YES),
1232 (id)kSecReturnAttributes : @(YES),
1233 (id)kSecReturnData : (id)kCFBooleanTrue,
1234 };
1235
1236 __block CFErrorRef cferror = nil;
1237 Query *q = query_create_with_limit( (__bridge CFDictionaryRef)queryAttributes, NULL, kSecMatchUnlimited, NULL, &cferror);
1238 XCTAssertNil((__bridge id)cferror, "Should be no error creating query");
1239 CFReleaseNull(cferror);
1240
1241 __block size_t count = 0;
1242
1243 bool ok = kc_with_dbt(true, &cferror, ^(SecDbConnectionRef dbt) {
1244 return SecDbItemQuery(q, NULL, dbt, &cferror, ^(SecDbItemRef item, bool *stop) {
1245 count += 1;
1246
1247 NSString* itemUUID = (NSString*) CFBridgingRelease(CFRetain(SecDbItemGetValue(item, &v10itemuuid, &cferror)));
1248 XCTAssertEqualObjects(uuid, itemUUID, "Item uuid should match expectation");
1249 });
1250 });
1251
1252 XCTAssertTrue(ok, "query should have been successful");
1253 XCTAssertNil((__bridge id)cferror, "Should be no error performing query");
1254 CFReleaseNull(cferror);
1255
1256 XCTAssertEqual(count, 1, "Should have processed one item");
1257 }
1258
1259 - (void)setGenericPasswordStoredUUID:(NSString*)uuid account:(NSString*)account {
1260 NSDictionary* queryAttributes = @{(id)kSecClass: (id)kSecClassGenericPassword,
1261 (id)kSecAttrAccessGroup : @"com.apple.security.ckks",
1262 (id)kSecAttrAccount : account,
1263 (id)kSecAttrSynchronizable: @(YES),
1264 (id)kSecReturnAttributes : @(YES),
1265 (id)kSecReturnData : (id)kCFBooleanTrue,
1266 };
1267
1268 __block CFErrorRef cferror = nil;
1269 Query *q = query_create_with_limit( (__bridge CFDictionaryRef)queryAttributes, NULL, kSecMatchUnlimited, NULL, &cferror);
1270 XCTAssertNil((__bridge id)cferror, "Should be no error creating query");
1271 CFReleaseNull(cferror);
1272
1273 __block size_t count = 0;
1274
1275 bool ok = kc_with_dbt(true, &cferror, ^(SecDbConnectionRef dbt) {
1276 return SecDbItemQuery(q, NULL, dbt, &cferror, ^(SecDbItemRef item, bool *stop) {
1277 count += 1;
1278
1279 NSDictionary* updates = @{(id) kSecAttrUUID: uuid};
1280
1281 SecDbItemRef new_item = SecDbItemCopyWithUpdates(item, (__bridge CFDictionaryRef)updates, &cferror);
1282 XCTAssertTrue(new_item != NULL, "Should be able to create a new item");
1283
1284 bool updateOk = kc_transaction_type(dbt, kSecDbExclusiveRemoteCKKSTransactionType, &cferror, ^{
1285 return SecDbItemUpdate(item, new_item, dbt, kCFBooleanFalse, q->q_uuid_from_primary_key, &cferror);
1286 });
1287 XCTAssertTrue(updateOk, "Should be able to update item");
1288
1289 return;
1290 });
1291 });
1292
1293 XCTAssertTrue(ok, "query should have been successful");
1294 XCTAssertNil((__bridge id)cferror, "Should be no error performing query");
1295 CFReleaseNull(cferror);
1296
1297 XCTAssertEqual(count, 1, "Should have processed one item");
1298 }
1299
1300 -(XCTestExpectation*)expectChangeForView:(NSString*)view {
1301 NSString* notification = [NSString stringWithFormat: @"com.apple.security.view-change.%@", view];
1302 return [self expectationForNotification:notification object:nil handler:^BOOL(NSNotification * _Nonnull nsnotification) {
1303 ckksnotice_global("ckks", "Got a notification for %@: %@", notification, nsnotification);
1304 return YES;
1305 }];
1306 }
1307
1308 - (void)expectCKKSTLKSelfShareUpload:(CKRecordZoneID*)zoneID {
1309 [self expectCKModifyKeyRecords:0 currentKeyPointerRecords:0 tlkShareRecords:1 zoneID:zoneID];
1310 }
1311
1312 - (void)checkNSyncableTLKsInKeychain:(size_t)n {
1313 NSDictionary *query = @{(id)kSecClass : (id)kSecClassInternetPassword,
1314 (id)kSecAttrAccessGroup : @"com.apple.security.ckks",
1315 (id)kSecAttrSynchronizable : (id)kCFBooleanTrue,
1316 (id)kSecAttrDescription: SecCKKSKeyClassTLK,
1317 (id)kSecMatchLimit : (id)kSecMatchLimitAll,
1318 (id)kSecReturnAttributes : (id)kCFBooleanTrue,
1319 };
1320 CFTypeRef result = NULL;
1321
1322 if(n == 0) {
1323 XCTAssertEqual(errSecItemNotFound, SecItemCopyMatching((__bridge CFDictionaryRef) query, &result), "Should have found no TLKs");
1324 } else {
1325 XCTAssertEqual(errSecSuccess, SecItemCopyMatching((__bridge CFDictionaryRef) query, &result), "Should have found TLKs");
1326 NSArray* items = (NSArray*) CFBridgingRelease(result);
1327
1328 XCTAssertEqual(items.count, n, "Should have received %lu items", (unsigned long)n);
1329 }
1330 }
1331
1332 @end
1333
1334 #endif // OCTAGON