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