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