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