2 * Copyright (c) 2017 Apple Inc. All Rights Reserved.
4 * @APPLE_LICENSE_HEADER_START@
6 * This file contains Original Code and/or Modifications of Original Code
7 * as defined in and that are subject to the Apple Public Source License
8 * Version 2.0 (the 'License'). You may not use this file except in
9 * compliance with the License. Please obtain a copy of the License at
10 * http://www.opensource.apple.com/apsl/ and read it before using this
13 * The Original Code and all software distributed under the License are
14 * distributed on an 'AS IS' basis, WITHOUT WARRANTY OF ANY KIND, EITHER
15 * EXPRESS OR IMPLIED, AND APPLE HEREBY DISCLAIMS ALL SUCH WARRANTIES,
16 * INCLUDING WITHOUT LIMITATION, ANY WARRANTIES OF MERCHANTABILITY,
17 * FITNESS FOR A PARTICULAR PURPOSE, QUIET ENJOYMENT OR NON-INFRINGEMENT.
18 * Please see the License for the specific language governing rights and
19 * limitations under the License.
21 * @APPLE_LICENSE_HEADER_END@
26 #import <OCMock/OCMock.h>
28 #import "keychain/ckks/tests/CloudKitMockXCTest.h"
29 #import "keychain/ckks/tests/CloudKitKeychainSyncingMockXCTest.h"
31 #import <securityd/SecItemServer.h>
32 #import <securityd/SecItemDb.h>
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"
47 #import "keychain/ot/OTDefines.h"
49 #pragma clang diagnostic push
50 #pragma clang diagnostic ignored "-Wdeprecated-declarations"
51 #import "Security/SecureObjectSync/SOSAccount.h"
52 #pragma clang diagnostic pop
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];
61 CKRecord* currentTLKPointerRecord = zone.currentDatabase[currentTLKPointerID];
62 CKRecord* currentClassAPointerRecord = zone.currentDatabase[currentClassAPointerID];
63 CKRecord* currentClassCPointerRecord = zone.currentDatabase[currentClassCPointerID];
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;
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;
73 CKRecord* currentTLKRecord = currentTLKID ? zone.currentDatabase[currentTLKID] : nil;
74 CKRecord* currentClassARecord = currentClassAID ? zone.currentDatabase[currentClassAID] : nil;
75 CKRecord* currentClassCRecord = currentClassCID ? zone.currentDatabase[currentClassCID] : nil;
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;
86 // No tests here, just helper functions
87 @implementation CloudKitKeychainSyncingMockXCTest
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);
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");
100 self.ckksZones = [NSMutableSet set];
101 self.keys = [[NSMutableDictionary alloc] init];
103 // Fake out whether class A keys can be loaded from the keychain.
104 self.mockCKKSKey = OCMClassMock([CKKSKey class]);
105 __weak __typeof(self) weakSelf = self;
106 BOOL (^shouldFailKeychainQuery)(NSDictionary* query) = ^BOOL(NSDictionary* query) {
107 __strong __typeof(self) strongSelf = weakSelf;
108 NSString* description = query[(id)kSecAttrDescription];
109 bool isTLK = [description isEqualToString: SecCKKSKeyClassTLK] ||
110 [description isEqualToString: [SecCKKSKeyClassTLK stringByAppendingString: @"-nonsync"]] ||
111 [description isEqualToString: [SecCKKSKeyClassTLK stringByAppendingString: @"-piggy"]];
112 bool isClassA = [description isEqualToString: SecCKKSKeyClassA];
114 return ((isTLK || isClassA) && strongSelf.aksLockState) || self.keychainFetchError;
117 OCMStub([self.mockCKKSKey setKeyMaterialInKeychain:[OCMArg checkWithBlock:shouldFailKeychainQuery] error:[OCMArg anyObjectRef]]
118 ).andCall(self, @selector(handleLockSetKeyMaterialInKeychain:error:));
120 OCMStub([self.mockCKKSKey queryKeyMaterialInKeychain:[OCMArg checkWithBlock:shouldFailKeychainQuery] error:[OCMArg anyObjectRef]]
121 ).andCall(self, @selector(handleLockLoadKeyMaterialFromKeychain:error:));
123 // Fake out SOS peers
124 self.currentSelfPeer = [[CKKSSOSSelfPeer alloc] initWithSOSPeerID:@"local-peer"
125 encryptionKey:[[SFECKeyPair alloc] initRandomKeyPairWithSpecifier:[[SFECKeySpecifier alloc] initWithCurve:SFEllipticCurveNistp384]]
126 signingKey:[[SFECKeyPair alloc] initRandomKeyPairWithSpecifier:[[SFECKeySpecifier alloc] initWithCurve:SFEllipticCurveNistp384]]];
128 // One trusted non-self peer, but it doesn't have any Octagon keys. Your test can change this if it wants.
129 // However, note that [self putFakeDeviceStatusInCloudKit:] will likely not do what you want.
130 self.remoteSOSOnlyPeer = [[CKKSSOSPeer alloc] initWithSOSPeerID:@"remote-peer-with-no-keys"
131 encryptionPublicKey:nil
132 signingPublicKey:nil];
133 self.currentPeers = [NSMutableSet set];
134 [self.currentPeers addObject:self.remoteSOSOnlyPeer];
136 OCMStub([self.mockCKKSViewManager currentSOSSelf:[OCMArg anyObjectRef]]).andCall(self, @selector(currentSOSSelf:));
137 OCMStub([self.mockCKKSViewManager fetchTrustedPeers:[OCMArg anyObjectRef]]).andCall(self, @selector(fetchTrustedPeers:));
139 // Bring up a fake CKKSControl object
140 id mockConnection = OCMPartialMock([[NSXPCConnection alloc] init]);
141 OCMStub([mockConnection remoteObjectProxyWithErrorHandler:[OCMArg any]]).andCall(self, @selector(injectedManager));
142 self.ckksControl = [[CKKSControl alloc] initWithConnection:mockConnection];
143 XCTAssertNotNil(self.ckksControl, "Should have received control object");
147 [self.mockCKKSKey stopMocking];
148 self.mockCKKSKey = nil;
153 self.currentSelfPeer = nil;
154 self.currentPeers = nil;
157 - (id<CKKSSelfPeer> _Nullable)currentSOSSelf:(NSError**)error {
158 if(self.currentSelfPeerError) {
160 *error = self.currentSelfPeerError;
163 } else if(self.aksLockState) {
165 *error = [NSError errorWithDomain:(__bridge NSString*)kSecErrorDomain code:errSecInteractionNotAllowed userInfo:nil];
169 return self.currentSelfPeer;
173 - (NSSet<id<CKKSPeer>>*)fetchTrustedPeers:(NSError* __autoreleasing *)error {
174 if(self.currentPeersError) {
176 *error = self.currentPeersError;
181 // Trusted Peers include ourselves, but as a CKKSSOSPeer object instead of a self peer
182 CKKSSOSPeer* s = [[CKKSSOSPeer alloc] initWithSOSPeerID:self.currentSelfPeer.peerID
183 encryptionPublicKey:self.currentSelfPeer.publicEncryptionKey
184 signingPublicKey:self.currentSelfPeer.publicSigningKey];
186 return [self.currentPeers setByAddingObject: s];
189 - (void)createClassCItemAndWaitForUpload:(CKRecordZoneID*)zoneID account:(NSString*)account {
190 [self expectCKModifyItemRecords:1
191 currentKeyPointerRecords:1
193 checkItem:[self checkClassCBlock:zoneID message:@"Object was encrypted under class C key in hierarchy"]];
194 [self addGenericPassword: @"data" account: account];
195 OCMVerifyAllWithDelay(self.mockDatabase, 8);
198 - (void)createClassAItemAndWaitForUpload:(CKRecordZoneID*)zoneID account:(NSString*)account {
199 [self expectCKModifyItemRecords:1
200 currentKeyPointerRecords:1
202 checkItem: [self checkClassABlock:zoneID message:@"Object was encrypted under class A key in hierarchy"]];
203 [self addGenericPassword:@"asdf"
206 access:(id)kSecAttrAccessibleWhenUnlocked
207 expecting:errSecSuccess
208 message:@"Adding class A item"];
209 OCMVerifyAllWithDelay(self.mockDatabase, 8);
212 // Helpers to handle 'locked' keychain loading and saving
213 -(bool)handleLockLoadKeyMaterialFromKeychain:(NSDictionary*)query error:(NSError * __autoreleasing *) error {
214 if(self.keychainFetchError) {
216 *error = self.keychainFetchError;
221 // I think the behavior is: errSecItemNotFound if the item doesn't exist, otherwise errSecInteractionNotAllowed.
222 XCTAssertTrue(self.aksLockState, "Failing a read when keychain is locked");
224 OSStatus status = SecItemCopyMatching((__bridge CFDictionaryRef)query, NULL);
225 if(status == errSecItemNotFound) {
227 *error = [NSError errorWithDomain:@"securityd" code:status userInfo:nil];
231 *error = [NSError errorWithDomain:@"securityd" code:errSecInteractionNotAllowed userInfo:nil];
237 -(bool)handleLockSetKeyMaterialInKeychain:(NSDictionary*)query error:(NSError * __autoreleasing *) error {
238 if(self.keychainFetchError) {
240 *error = self.keychainFetchError;
245 XCTAssertTrue(self.aksLockState, "Failing a write only when keychain is locked");
247 *error = [NSError errorWithDomain:@"securityd" code:errSecInteractionNotAllowed userInfo:nil];
253 - (ZoneKeys*)createFakeKeyHierarchy: (CKRecordZoneID*)zoneID oldTLK:(CKKSKey*) oldTLK {
254 if(self.keys[zoneID]) {
255 // Already created. Skip.
256 return self.keys[zoneID];
259 NSError* error = nil;
261 ZoneKeys* zonekeys = [[ZoneKeys alloc] init];
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];
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");
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];
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];
289 self.keys[zoneID] = zonekeys;
293 - (void)saveFakeKeyHierarchyToLocalDatabase: (CKRecordZoneID*)zoneID {
294 NSError* error = nil;
295 ZoneKeys* zonekeys = [self createFakeKeyHierarchy: zoneID oldTLK:nil];
297 [zonekeys.tlk saveToDatabase:&error];
298 XCTAssertNil(error, "TLK saved to database successfully");
300 [zonekeys.classA saveToDatabase:&error];
301 XCTAssertNil(error, "Class A key saved to database successfully");
303 [zonekeys.classC saveToDatabase:&error];
304 XCTAssertNil(error, "Class C key saved to database successfully");
306 [zonekeys.currentTLKPointer saveToDatabase:&error];
307 XCTAssertNil(error, "Current TLK pointer saved to database successfully");
309 [zonekeys.currentClassAPointer saveToDatabase:&error];
310 XCTAssertNil(error, "Current Class A pointer saved to database successfully");
312 [zonekeys.currentClassCPointer saveToDatabase:&error];
313 XCTAssertNil(error, "Current Class C pointer saved to database successfully");
316 - (void)putFakeDeviceStatusInCloudKit:(CKRecordZoneID*)zoneID zonekeys:(ZoneKeys*)zonekeys {
317 CKKSDeviceStateEntry* dse = [[CKKSDeviceStateEntry alloc] initForDevice:self.remoteSOSOnlyPeer.peerID
318 osVersion:@"faux-version"
320 circlePeerID:self.remoteSOSOnlyPeer.peerID
321 circleStatus:kSOSCCInCircle
322 keyState:SecCKKSZoneKeyStateReady
323 currentTLKUUID:zonekeys.tlk.uuid
324 currentClassAUUID:zonekeys.classA.uuid
325 currentClassCUUID:zonekeys.classC.uuid
327 encodedCKRecord:nil];
328 [self.zones[zoneID] addToZone:dse zoneID:zoneID];
331 - (void)putFakeDeviceStatusInCloudKit:(CKRecordZoneID*)zoneID {
332 [self putFakeDeviceStatusInCloudKit:zoneID zonekeys:self.keys[zoneID]];
335 - (void)putFakeKeyHierarchyInCloudKit: (CKRecordZoneID*)zoneID {
336 ZoneKeys* zonekeys = [self createFakeKeyHierarchy: zoneID oldTLK:nil];
338 [self.zones[zoneID] addToZone: zonekeys.tlk zoneID: zoneID];
339 [self.zones[zoneID] addToZone: zonekeys.classA zoneID: zoneID];
340 [self.zones[zoneID] addToZone: zonekeys.classC zoneID: zoneID];
342 [self.zones[zoneID] addToZone: zonekeys.currentTLKPointer zoneID: zoneID];
343 [self.zones[zoneID] addToZone: zonekeys.currentClassAPointer zoneID: zoneID];
344 [self.zones[zoneID] addToZone: zonekeys.currentClassCPointer zoneID: zoneID];
346 if(zonekeys.rolledTLK) {
347 [self.zones[zoneID] addToZone: zonekeys.rolledTLK zoneID: zoneID];
351 - (void)rollFakeKeyHierarchyInCloudKit: (CKRecordZoneID*)zoneID {
352 ZoneKeys* zonekeys = self.keys[zoneID];
353 self.keys[zoneID] = nil;
355 CKKSKey* oldTLK = zonekeys.tlk;
356 NSError* error = nil;
357 [oldTLK ensureKeyLoaded:&error];
358 XCTAssertNil(error, "shouldn't error ensuring that the oldTLK has its key material");
360 [self createFakeKeyHierarchy: zoneID oldTLK:oldTLK];
361 [self putFakeKeyHierarchyInCloudKit: zoneID];
364 - (void)saveTLKMaterialToKeychainSimulatingSOS: (CKRecordZoneID*)zoneID {
366 XCTAssertNotNil(self.keys[zoneID].tlk, "Have a TLK to save for zone %@", zoneID);
368 __block CFErrorRef cferror = NULL;
369 kc_with_dbt(true, &cferror, ^bool (SecDbConnectionRef dbt) {
370 bool ok = kc_transaction_type(dbt, kSecDbExclusiveRemoteSOSTransactionType, &cferror, ^bool {
371 NSError* error = nil;
372 [self.keys[zoneID].tlk saveKeyMaterialToKeychain: false error:&error];
373 XCTAssertNil(error, @"Saved TLK material to keychain");
379 XCTAssertNil( (__bridge NSError*)cferror, @"no error with transaction");
380 CFReleaseNull(cferror);
382 static SOSFullPeerInfoRef SOSCreateFullPeerInfoFromName(CFStringRef name,
383 SecKeyRef* outSigningKey,
384 SecKeyRef* outOctagonSigningKey,
385 SecKeyRef* outOctagonEncryptionKey,
388 SOSFullPeerInfoRef result = NULL;
389 SecKeyRef publicKey = NULL;
390 CFDictionaryRef gestalt = NULL;
392 *outSigningKey = GeneratePermanentFullECKey(256, name, error);
394 *outOctagonSigningKey = GeneratePermanentFullECKey(384, name, error);
395 *outOctagonEncryptionKey = GeneratePermanentFullECKey(384, name, error);
397 gestalt = SOSCreatePeerGestaltFromName(name);
399 result = SOSFullPeerInfoCreate(NULL, gestalt, NULL, *outSigningKey,
400 *outOctagonSigningKey, *outOctagonEncryptionKey,
403 CFReleaseNull(gestalt);
404 CFReleaseNull(publicKey);
408 - (NSMutableArray<NSData *>*) SOSPiggyICloudIdentities
410 SecKeyRef signingKey = NULL;
411 SecKeyRef octagonSigningKey = NULL;
412 SecKeyRef octagonEncryptionKey = NULL;
413 NSMutableArray<NSData *>* icloudidentities = [NSMutableArray array];
415 SOSFullPeerInfoRef fpi = SOSCreateFullPeerInfoFromName(CFSTR("Test Peer"), &signingKey, &octagonSigningKey, &octagonEncryptionKey, NULL);
417 NSData *data = CFBridgingRelease(SOSPeerInfoCopyData(SOSFullPeerInfoGetPeerInfo(fpi), NULL));
420 [icloudidentities addObject:data];
422 CFReleaseNull(signingKey);
423 CFReleaseNull(octagonSigningKey);
424 CFReleaseNull(octagonEncryptionKey);
426 return icloudidentities;
428 static CFDictionaryRef SOSCreatePeerGestaltFromName(CFStringRef name)
430 return CFDictionaryCreateForCFTypes(kCFAllocatorDefault,
431 kPIUserDefinedDeviceNameKey, name,
434 -(NSMutableDictionary*)SOSPiggyBackCopyFromKeychain
436 __block NSMutableDictionary *piggybackdata = [[NSMutableDictionary alloc] init];
437 __block CFErrorRef cferror = NULL;
438 kc_with_dbt(true, &cferror, ^bool (SecDbConnectionRef dbt) {
439 bool ok = kc_transaction_type(dbt, kSecDbExclusiveRemoteSOSTransactionType, &cferror, ^bool {
440 piggybackdata[@"idents"] = [self SOSPiggyICloudIdentities];
441 piggybackdata[@"tlk"] = SOSAccountGetAllTLKs();
448 XCTAssertNil( (__bridge NSError*)cferror, @"no error with transaction");
449 CFReleaseNull(cferror);
450 return piggybackdata;
452 - (void)SOSPiggyBackAddToKeychain:(NSDictionary*)piggydata{
453 __block CFErrorRef cferror = NULL;
454 kc_with_dbt(true, &cferror, ^bool (SecDbConnectionRef dbt) {
455 bool ok = kc_transaction_type(dbt, kSecDbExclusiveRemoteSOSTransactionType, &cferror, ^bool {
456 NSError* error = nil;
457 NSArray* icloudidentities = piggydata[@"idents"];
458 NSArray* tlk = piggydata[@"tlk"];
460 SOSPiggyBackAddToKeychain(icloudidentities, tlk);
462 XCTAssertNil(error, @"Saved TLK-piggy material to keychain");
468 XCTAssertNil( (__bridge NSError*)cferror, @"no error with transaction");
469 CFReleaseNull(cferror);
472 - (void)saveTLKMaterialToKeychain: (CKRecordZoneID*)zoneID {
473 NSError* error = nil;
474 XCTAssertNotNil(self.keys[zoneID].tlk, "Have a TLK to save for zone %@", zoneID);
476 // Don't make the stashed local copy of the TLK
477 [self.keys[zoneID].tlk saveKeyMaterialToKeychain:false error:&error];
478 XCTAssertNil(error, @"Saved TLK material to keychain");
481 - (void)deleteTLKMaterialFromKeychain: (CKRecordZoneID*)zoneID {
482 NSError* error = nil;
483 XCTAssertNotNil(self.keys[zoneID].tlk, "Have a TLK to save for zone %@", zoneID);
484 [self.keys[zoneID].tlk deleteKeyMaterialFromKeychain:&error];
485 XCTAssertNil(error, @"Saved TLK material to keychain");
488 - (void)saveClassKeyMaterialToKeychain: (CKRecordZoneID*)zoneID {
489 NSError* error = nil;
490 XCTAssertNotNil(self.keys[zoneID].classA, "Have a Class A to save for zone %@", zoneID);
491 [self.keys[zoneID].classA saveKeyMaterialToKeychain:&error];
492 XCTAssertNil(error, @"Saved Class A material to keychain");
494 XCTAssertNotNil(self.keys[zoneID].classC, "Have a Class C to save for zone %@", zoneID);
495 [self.keys[zoneID].classC saveKeyMaterialToKeychain:&error];
496 XCTAssertNil(error, @"Saved Class C material to keychain");
500 - (void)putTLKShareInCloudKit:(CKKSKey*)key
501 from:(CKKSSOSSelfPeer*)sharingPeer
502 to:(id<CKKSPeer>)receivingPeer
503 zoneID:(CKRecordZoneID*)zoneID
505 NSError* error = nil;
506 CKKSTLKShare* share = [CKKSTLKShare share:key
512 XCTAssertNil(error, "Should have been no error sharing a CKKSKey");
513 XCTAssertNotNil(share, "Should be able to create a share");
515 CKRecord* shareRecord = [share CKRecordWithZoneID: zoneID];
516 XCTAssertNotNil(shareRecord, "Should have been able to create a CKRecord");
518 FakeCKZone* zone = self.zones[zoneID];
519 XCTAssertNotNil(zone, "Should have a zone to put a TLKShare in");
520 [zone addToZone:shareRecord];
522 ZoneKeys* keys = self.keys[zoneID];
523 XCTAssertNotNil(keys, "Have a zonekeys object for this zone");
524 keys.tlkShares = keys.tlkShares ? [keys.tlkShares arrayByAddingObject:share] : @[share];
527 - (void)putTLKSharesInCloudKit:(CKKSKey*)key
528 from:(CKKSSOSSelfPeer*)sharingPeer
529 zoneID:(CKRecordZoneID*)zoneID
531 NSSet* peers = [self.currentPeers setByAddingObject:self.currentSelfPeer];
533 for(id<CKKSPeer> peer in peers) {
534 // Can only send to peers with encryption keys
535 if(peer.publicEncryptionKey) {
536 [self putTLKShareInCloudKit:key from:sharingPeer to:peer zoneID:zoneID];
541 - (void)putSelfTLKSharesInCloudKit:(CKRecordZoneID*)zoneID {
542 CKKSKey* tlk = self.keys[zoneID].tlk;
543 XCTAssertNotNil(tlk, "Should have a TLK for zone %@", zoneID);
544 [self putTLKSharesInCloudKit:tlk from:self.currentSelfPeer zoneID:zoneID];
547 - (void)saveTLKSharesInLocalDatabase:(CKRecordZoneID*)zoneID {
548 ZoneKeys* keys = self.keys[zoneID];
549 XCTAssertNotNil(keys, "Have a zonekeys object for this zone");
551 for(CKKSTLKShare* share in keys.tlkShares) {
552 NSError* error = nil;
553 [share saveToDatabase:&error];
554 XCTAssertNil(error, "Shouldn't have been an error saving a TLKShare to the database");
558 - (void)createAndSaveFakeKeyHierarchy: (CKRecordZoneID*)zoneID {
559 // Put in CloudKit first, so the records on-disk will have the right change tags
560 [self putFakeKeyHierarchyInCloudKit: zoneID];
561 [self saveFakeKeyHierarchyToLocalDatabase: zoneID];
562 [self saveTLKMaterialToKeychain: zoneID];
563 [self saveClassKeyMaterialToKeychain: zoneID];
565 [self putSelfTLKSharesInCloudKit:zoneID];
566 [self saveTLKSharesInLocalDatabase:zoneID];
569 // Override our base class here, but only for Keychain Views
571 - (void)expectCKModifyRecords:(NSDictionary<NSString*, NSNumber*>*)expectedRecordTypeCounts
572 deletedRecordTypeCounts:(NSDictionary<NSString*, NSNumber*>*)expectedDeletedRecordTypeCounts
573 zoneID:(CKRecordZoneID*)zoneID
574 checkModifiedRecord:(BOOL (^)(CKRecord*))checkRecord
575 runAfterModification:(void (^) ())afterModification
578 void (^newAfterModification)() = afterModification;
579 if([self.ckksZones containsObject:zoneID]) {
580 __weak __typeof(self) weakSelf = self;
581 newAfterModification = ^{
582 __strong __typeof(weakSelf) strongSelf = weakSelf;
583 XCTAssertNotNil(strongSelf, "self exists");
585 // Reach into our cloudkit database and extract the keys
586 CKRecordID* currentTLKPointerID = [[CKRecordID alloc] initWithRecordName:SecCKKSKeyClassTLK zoneID:zoneID];
587 CKRecordID* currentClassAPointerID = [[CKRecordID alloc] initWithRecordName:SecCKKSKeyClassA zoneID:zoneID];
588 CKRecordID* currentClassCPointerID = [[CKRecordID alloc] initWithRecordName:SecCKKSKeyClassC zoneID:zoneID];
590 ZoneKeys* zonekeys = strongSelf.keys[zoneID];
592 zonekeys = [[ZoneKeys alloc] init];
593 strongSelf.keys[zoneID] = zonekeys;
596 XCTAssertNotNil(strongSelf.zones[zoneID].currentDatabase[currentTLKPointerID], "Have a currentTLKPointer");
597 XCTAssertNotNil(strongSelf.zones[zoneID].currentDatabase[currentClassAPointerID], "Have a currentClassAPointer");
598 XCTAssertNotNil(strongSelf.zones[zoneID].currentDatabase[currentClassCPointerID], "Have a currentClassCPointer");
599 XCTAssertNotNil(strongSelf.zones[zoneID].currentDatabase[currentTLKPointerID][SecCKRecordParentKeyRefKey], "Have a currentTLKPointer parent");
600 XCTAssertNotNil(strongSelf.zones[zoneID].currentDatabase[currentClassAPointerID][SecCKRecordParentKeyRefKey], "Have a currentClassAPointer parent");
601 XCTAssertNotNil(strongSelf.zones[zoneID].currentDatabase[currentClassCPointerID][SecCKRecordParentKeyRefKey], "Have a currentClassCPointer parent");
602 XCTAssertNotNil([strongSelf.zones[zoneID].currentDatabase[currentTLKPointerID][SecCKRecordParentKeyRefKey] recordID].recordName, "Have a currentTLKPointer parent UUID");
603 XCTAssertNotNil([strongSelf.zones[zoneID].currentDatabase[currentClassAPointerID][SecCKRecordParentKeyRefKey] recordID].recordName, "Have a currentClassAPointer parent UUID");
604 XCTAssertNotNil([strongSelf.zones[zoneID].currentDatabase[currentClassCPointerID][SecCKRecordParentKeyRefKey] recordID].recordName, "Have a currentClassCPointer parent UUID");
606 zonekeys.currentTLKPointer = [[CKKSCurrentKeyPointer alloc] initWithCKRecord: strongSelf.zones[zoneID].currentDatabase[currentTLKPointerID]];
607 zonekeys.currentClassAPointer = [[CKKSCurrentKeyPointer alloc] initWithCKRecord: strongSelf.zones[zoneID].currentDatabase[currentClassAPointerID]];
608 zonekeys.currentClassCPointer = [[CKKSCurrentKeyPointer alloc] initWithCKRecord: strongSelf.zones[zoneID].currentDatabase[currentClassCPointerID]];
610 XCTAssertNotNil(zonekeys.currentTLKPointer.currentKeyUUID, "Have a currentTLKPointer current UUID");
611 XCTAssertNotNil(zonekeys.currentClassAPointer.currentKeyUUID, "Have a currentClassAPointer current UUID");
612 XCTAssertNotNil(zonekeys.currentClassCPointer.currentKeyUUID, "Have a currentClassCPointer current UUID");
614 CKRecordID* currentTLKID = [[CKRecordID alloc] initWithRecordName:zonekeys.currentTLKPointer.currentKeyUUID zoneID:zoneID];
615 CKRecordID* currentClassAID = [[CKRecordID alloc] initWithRecordName:zonekeys.currentClassAPointer.currentKeyUUID zoneID:zoneID];
616 CKRecordID* currentClassCID = [[CKRecordID alloc] initWithRecordName:zonekeys.currentClassCPointer.currentKeyUUID zoneID:zoneID];
618 zonekeys.tlk = [[CKKSKey alloc] initWithCKRecord: strongSelf.zones[zoneID].currentDatabase[currentTLKID]];
619 zonekeys.classA = [[CKKSKey alloc] initWithCKRecord: strongSelf.zones[zoneID].currentDatabase[currentClassAID]];
620 zonekeys.classC = [[CKKSKey alloc] initWithCKRecord: strongSelf.zones[zoneID].currentDatabase[currentClassCID]];
622 XCTAssertNotNil(zonekeys.tlk, "Have the current TLK");
623 XCTAssertNotNil(zonekeys.classA, "Have the current Class A key");
624 XCTAssertNotNil(zonekeys.classC, "Have the current Class C key");
626 NSMutableArray<CKKSTLKShare*>* shares = [NSMutableArray array];
627 for(CKRecordID* recordID in strongSelf.zones[zoneID].currentDatabase.allKeys) {
628 if([recordID.recordName hasPrefix: [CKKSTLKShare ckrecordPrefix]]) {
629 CKKSTLKShare* share = [[CKKSTLKShare alloc] initWithCKRecord:strongSelf.zones[zoneID].currentDatabase[recordID]];
630 XCTAssertNotNil(share, "Should be able to parse a CKKSTLKShare CKRecord into a CKKSTLKShare");
631 [shares addObject:share];
634 zonekeys.tlkShares = shares;
636 if(afterModification) {
642 [super expectCKModifyRecords:expectedRecordTypeCounts
643 deletedRecordTypeCounts:expectedDeletedRecordTypeCounts
645 checkModifiedRecord:checkRecord
646 runAfterModification:newAfterModification];
649 - (void)expectCKReceiveSyncKeyHierarchyError:(CKRecordZoneID*)zoneID {
651 __weak __typeof(self) weakSelf = self;
652 [[self.mockDatabase expect] addOperation:[OCMArg checkWithBlock:^BOOL(id obj) {
653 __strong __typeof(weakSelf) strongSelf = weakSelf;
654 XCTAssertNotNil(strongSelf, "self exists");
656 __block bool rejected = false;
657 if ([obj isKindOfClass:[CKModifyRecordsOperation class]]) {
658 CKModifyRecordsOperation *op = (CKModifyRecordsOperation *)obj;
661 // We only care about atomic operations
665 // We want to only match zone updates pertaining to this zone
666 for(CKRecord* record in op.recordsToSave) {
667 if(![record.recordID.zoneID isEqual: zoneID]) {
672 // we oly want to match updates that are updating a class C or class A CKP
673 bool updatingClassACKP = false;
674 bool updatingClassCCKP = false;
675 for(CKRecord* record in op.recordsToSave) {
676 if([record.recordID.recordName isEqualToString:SecCKKSKeyClassA]) {
677 updatingClassACKP = true;
679 if([record.recordID.recordName isEqualToString:SecCKKSKeyClassC]) {
680 updatingClassCCKP = true;
684 if(!updatingClassACKP && !updatingClassCCKP) {
688 FakeCKZone* zone = strongSelf.zones[zoneID];
689 XCTAssertNotNil(zone, "Should have a zone for these records");
691 // We only want to match if the synckeys aren't pointing correctly
693 ZoneKeys* zonekeys = [[ZoneKeys alloc] initLoadingRecordsFromZone:zone];
695 XCTAssertNotNil(zonekeys.currentTLKPointer, "Have a currentTLKPointer");
696 XCTAssertNotNil(zonekeys.currentClassAPointer, "Have a currentClassAPointer");
697 XCTAssertNotNil(zonekeys.currentClassCPointer, "Have a currentClassCPointer");
699 XCTAssertNotNil(zonekeys.tlk, "Have the current TLK");
700 XCTAssertNotNil(zonekeys.classA, "Have the current Class A key");
701 XCTAssertNotNil(zonekeys.classC, "Have the current Class C key");
703 // Ensure that either the Class A synckey or the class C synckey do not immediately wrap to the current TLK
704 bool classALinkBroken = ![zonekeys.classA.parentKeyUUID isEqualToString:zonekeys.tlk.uuid];
705 bool classCLinkBroken = ![zonekeys.classC.parentKeyUUID isEqualToString:zonekeys.tlk.uuid];
707 // Neither synckey link is broken. Don't match this operation.
708 if(!classALinkBroken && !classCLinkBroken) {
712 NSMutableDictionary<CKRecordID*, NSError*>* failedRecords = [[NSMutableDictionary alloc] init];
714 @synchronized(zone.currentDatabase) {
715 for(CKRecord* record in op.recordsToSave) {
716 if(classALinkBroken && [record.recordID.recordName isEqualToString:SecCKKSKeyClassA]) {
717 failedRecords[record.recordID] = [strongSelf ckInternalServerExtensionError:CKKSServerUnexpectedSyncKeyInChain description:@"synckey record: current classA synckey does not point to current tlk synckey"];
720 if(classCLinkBroken && [record.recordID.recordName isEqualToString:SecCKKSKeyClassC]) {
721 failedRecords[record.recordID] = [strongSelf ckInternalServerExtensionError:CKKSServerUnexpectedSyncKeyInChain description:@"synckey record: current classC synckey does not point to current tlk synckey"];
728 [strongSelf rejectWrite: op failedRecords:failedRecords];
731 return rejected ? YES : NO;
735 - (void)checkNoCKKSData: (CKKSKeychainView*) view {
736 // Test that there are no items in the database
737 [view dispatchSync:^bool{
738 NSError* error = nil;
739 NSArray<CKKSMirrorEntry*>* ckmes = [CKKSMirrorEntry all: view.zoneID error:&error];
740 XCTAssertNil(error, "No error fetching CKMEs");
741 XCTAssertEqual(ckmes.count, 0ul, "No CKMirrorEntries");
743 NSArray<CKKSOutgoingQueueEntry*>* oqes = [CKKSOutgoingQueueEntry all: view.zoneID error:&error];
744 XCTAssertNil(error, "No error fetching OQEs");
745 XCTAssertEqual(oqes.count, 0ul, "No OutgoingQueueEntries");
747 NSArray<CKKSIncomingQueueEntry*>* iqes = [CKKSIncomingQueueEntry all: view.zoneID error:&error];
748 XCTAssertNil(error, "No error fetching IQEs");
749 XCTAssertEqual(iqes.count, 0ul, "No IncomingQueueEntries");
751 NSArray<CKKSKey*>* keys = [CKKSKey all: view.zoneID error:&error];
752 XCTAssertNil(error, "No error fetching keys");
753 XCTAssertEqual(keys.count, 0ul, "No CKKSKeys");
755 NSArray<CKKSDeviceStateEntry*>* deviceStates = [CKKSDeviceStateEntry allInZone:view.zoneID error:&error];
756 XCTAssertNil(error, "should be no error fetching device states");
757 XCTAssertEqual(deviceStates.count, 0ul, "No Device State entries");
762 - (BOOL (^) (CKRecord*)) checkClassABlock: (CKRecordZoneID*) zoneID message:(NSString*) message {
763 __weak __typeof(self) weakSelf = self;
764 return ^BOOL(CKRecord* record) {
765 __strong __typeof(weakSelf) strongSelf = weakSelf;
766 XCTAssertNotNil(strongSelf, "self exists");
768 ZoneKeys* zoneKeys = strongSelf.keys[zoneID];
769 XCTAssertNotNil(zoneKeys, "Have zone keys for %@", zoneID);
770 XCTAssertEqualObjects([record[SecCKRecordParentKeyRefKey] recordID].recordName, zoneKeys.classA.uuid, "%@", message);
771 return [[record[SecCKRecordParentKeyRefKey] recordID].recordName isEqual: zoneKeys.classA.uuid];
775 - (BOOL (^) (CKRecord*)) checkClassCBlock: (CKRecordZoneID*) zoneID message:(NSString*) message {
776 __weak __typeof(self) weakSelf = self;
777 return ^BOOL(CKRecord* record) {
778 __strong __typeof(weakSelf) strongSelf = weakSelf;
779 XCTAssertNotNil(strongSelf, "self exists");
781 ZoneKeys* zoneKeys = strongSelf.keys[zoneID];
782 XCTAssertNotNil(zoneKeys, "Have zone keys for %@", zoneID);
783 XCTAssertEqualObjects([record[SecCKRecordParentKeyRefKey] recordID].recordName, zoneKeys.classC.uuid, "%@", message);
784 return [[record[SecCKRecordParentKeyRefKey] recordID].recordName isEqual: zoneKeys.classC.uuid];
788 - (BOOL (^) (CKRecord*)) checkPasswordBlock:(CKRecordZoneID*)zoneID
789 account:(NSString*)account
790 password:(NSString*)password {
791 __weak __typeof(self) weakSelf = self;
792 return ^BOOL(CKRecord* record) {
793 __strong __typeof(weakSelf) strongSelf = weakSelf;
794 XCTAssertNotNil(strongSelf, "self exists");
796 ZoneKeys* zoneKeys = strongSelf.keys[zoneID];
797 XCTAssertNotNil(zoneKeys, "Have zone keys for %@", zoneID);
798 XCTAssertNotNil([record[SecCKRecordParentKeyRefKey] recordID].recordName, "Have a wrapping key");
801 if([[record[SecCKRecordParentKeyRefKey] recordID].recordName isEqualToString: zoneKeys.classC.uuid]) {
802 key = zoneKeys.classC;
803 } else if([[record[SecCKRecordParentKeyRefKey] recordID].recordName isEqualToString: zoneKeys.classA.uuid]) {
804 key = zoneKeys.classA;
806 XCTAssertNotNil(key, "Found a key via UUID");
808 CKKSMirrorEntry* ckme = [[CKKSMirrorEntry alloc] initWithCKRecord: record];
810 NSError* error = nil;
811 NSDictionary* dict = [CKKSItemEncrypter decryptItemToDictionary:ckme.item error:&error];
812 XCTAssertNil(error, "No error decrypting item");
813 XCTAssertEqualObjects(account, dict[(id)kSecAttrAccount], "Account matches");
814 XCTAssertEqualObjects([password dataUsingEncoding:NSUTF8StringEncoding], dict[(id)kSecValueData], "Password matches");
819 - (NSDictionary*)fakeRecordDictionary:(NSString*) account zoneID:(CKRecordZoneID*)zoneID {
820 NSError* error = nil;
823 @"acct" : @"account-delete-me",
824 @"agrp" : @"com.apple.security.sos",
825 @"cdat" : @"2016-12-21 03:33:25 +0000",
827 @"mdat" : @"2016-12-21 03:33:25 +0000",
828 @"musr" : [[NSData alloc] init],
830 @"sha1" : [[NSData alloc] initWithBase64EncodedString: @"C3VWONaOIj8YgJjk/xwku4By1CY=" options:0],
832 @"tomb" : [NSNumber numberWithInt: 0],
833 @"v_Data" : [@"data" dataUsingEncoding: NSUTF8StringEncoding],
835 TODO: this should be binary encoded instead of expanded, but the plist encoder should handle it fine */
836 NSData* itemdata = [[NSData alloc] initWithBase64EncodedString:@"PD94bWwgdmVyc2lvbj0iMS4wIiBlbmNvZGluZz0iVVRGLTgiPz4KPCFET0NUWVBFIHBsaXN0IFBVQkxJQyAiLS8vQXBwbGUvL0RURCBQTElTVCAxLjAvL0VOIiAiaHR0cDovL3d3dy5hcHBsZS5jb20vRFREcy9Qcm9wZXJ0eUxpc3QtMS4wLmR0ZCI+CjxwbGlzdCB2ZXJzaW9uPSIxLjAiPgo8ZGljdD4KCTxrZXk+YWNjdDwva2V5PgoJPHN0cmluZz5hY2NvdW50LWRlbGV0ZS1tZTwvc3RyaW5nPgoJPGtleT5hZ3JwPC9rZXk+Cgk8c3RyaW5nPmNvbS5hcHBsZS5zZWN1cml0eS5zb3M8L3N0cmluZz4KCTxrZXk+Y2RhdDwva2V5PgoJPGRhdGU+MjAxNi0xMi0yMVQwMzozMzoyNVo8L2RhdGU+Cgk8a2V5PmNsYXNzPC9rZXk+Cgk8c3RyaW5nPmdlbnA8L3N0cmluZz4KCTxrZXk+bWRhdDwva2V5PgoJPGRhdGU+MjAxNi0xMi0yMVQwMzozMzoyNVo8L2RhdGU+Cgk8a2V5Pm11c3I8L2tleT4KCTxkYXRhPgoJPC9kYXRhPgoJPGtleT5wZG1uPC9rZXk+Cgk8c3RyaW5nPmFrPC9zdHJpbmc+Cgk8a2V5PnNoYTE8L2tleT4KCTxkYXRhPgoJQzNWV09OYU9JajhZZ0pqay94d2t1NEJ5MUNZPQoJPC9kYXRhPgoJPGtleT5zdmNlPC9rZXk+Cgk8c3RyaW5nPjwvc3RyaW5nPgoJPGtleT50b21iPC9rZXk+Cgk8aW50ZWdlcj4wPC9pbnRlZ2VyPgoJPGtleT52X0RhdGE8L2tleT4KCTxkYXRhPgoJWkdGMFlRPT0KCTwvZGF0YT4KPC9kaWN0Pgo8L3BsaXN0Pgo=" options:0];
837 NSMutableDictionary * item = [[NSPropertyListSerialization propertyListWithData:itemdata
840 error:&error] mutableCopy];
842 item[@"agrp"] = @"com.apple.security.ckks";
843 item[@"vwht"] = @"keychain";
844 XCTAssertNil(error, "no error interpreting data as item");
845 XCTAssertNotNil(item, "interpreted data as item");
847 if(zoneID && ![zoneID.zoneName isEqualToString:@"keychain"]) {
848 [item setObject: zoneID.zoneName forKey: (__bridge id) kSecAttrSyncViewHint];
852 [item setObject: account forKey: (__bridge id) kSecAttrAccount];
857 - (CKRecord*)createFakeRecord: (CKRecordZoneID*)zoneID recordName:(NSString*)recordName {
858 return [self createFakeRecord: zoneID recordName:recordName withAccount: nil key:nil];
861 - (CKRecord*)createFakeRecord: (CKRecordZoneID*)zoneID recordName:(NSString*)recordName withAccount: (NSString*) account {
862 return [self createFakeRecord: zoneID recordName:recordName withAccount:account key:nil];
865 - (CKRecord*)createFakeRecord: (CKRecordZoneID*)zoneID recordName:(NSString*)recordName withAccount: (NSString*) account key:(CKKSKey*)key {
866 NSDictionary* item = [self fakeRecordDictionary: account zoneID:zoneID];
868 CKRecordID* ckrid = [[CKRecordID alloc] initWithRecordName:recordName zoneID:zoneID];
870 return [self newRecord: ckrid withNewItemData:item key:key];
872 return [self newRecord: ckrid withNewItemData:item];
876 - (CKRecord*)newRecord: (CKRecordID*) recordID withNewItemData:(NSDictionary*) dictionary {
877 ZoneKeys* zonekeys = self.keys[recordID.zoneID];
878 XCTAssertNotNil(zonekeys, "Have zone keys for zone");
879 XCTAssertNotNil(zonekeys.classC, "Have class C key for zone");
881 return [self newRecord:recordID withNewItemData:dictionary key:zonekeys.classC];
884 - (CKKSItem*)newItem:(CKRecordID*)recordID withNewItemData:(NSDictionary*)dictionary key:(CKKSKey*)key {
885 NSError* error = nil;
886 CKKSItem* cipheritem = [CKKSItemEncrypter encryptCKKSItem:[[CKKSItem alloc] initWithUUID:recordID.recordName
887 parentKeyUUID:key.uuid
888 zoneID:recordID.zoneID]
889 dataDictionary:dictionary
893 XCTAssertNil(error, "encrypted item with class c key");
894 XCTAssertNotNil(cipheritem, "Have an encrypted item");
899 - (CKRecord*)newRecord: (CKRecordID*) recordID withNewItemData:(NSDictionary*) dictionary key:(CKKSKey*)key {
900 CKKSItem* item = [self newItem:recordID withNewItemData:dictionary key:key];
902 CKRecord* ckr = [item CKRecordWithZoneID: recordID.zoneID];
903 XCTAssertNotNil(ckr, "Created a CKRecord");
907 - (NSDictionary*)decryptRecord: (CKRecord*) record {
908 CKKSItem* item = [[CKKSItem alloc] initWithCKRecord: record];
910 NSError* error = nil;
912 NSDictionary* ret = [CKKSItemEncrypter decryptItemToDictionary: item error:&error];
914 XCTAssertNotNil(ret);
918 - (void)addGenericPassword: (NSString*) password account: (NSString*) account viewHint: (NSString*) viewHint access: (NSString*) access expecting: (OSStatus) status message: (NSString*) message {
919 NSMutableDictionary* query = [@{
920 (id)kSecClass : (id)kSecClassGenericPassword,
921 (id)kSecAttrAccessGroup : @"com.apple.security.ckks",
922 (id)kSecAttrAccessible: access,
923 (id)kSecAttrAccount : account,
924 (id)kSecAttrSynchronizable : (id)kCFBooleanTrue,
925 (id)kSecValueData : (id) [password dataUsingEncoding:NSUTF8StringEncoding],
929 query[(id)kSecAttrSyncViewHint] = viewHint;
931 // Fake it as 'keychain'. This lets CKKSScanLocalItemsOperation for the test-only 'keychain' view find items which would normally not have a view hint.
932 query[(id)kSecAttrSyncViewHint] = @"keychain";
935 XCTAssertEqual(status, SecItemAdd((__bridge CFDictionaryRef) query, NULL), @"%@", message);
938 - (void)addGenericPassword: (NSString*) password account: (NSString*) account expecting: (OSStatus) status message: (NSString*) message {
939 [self addGenericPassword:password account:account viewHint:nil access:(id)kSecAttrAccessibleAfterFirstUnlock expecting:errSecSuccess message:message];
943 - (void)addGenericPassword: (NSString*) password account: (NSString*) account {
944 [self addGenericPassword:password account:account viewHint:nil access:(id)kSecAttrAccessibleAfterFirstUnlock expecting:errSecSuccess message:@"Add item to keychain"];
947 - (void)addGenericPassword: (NSString*) password account: (NSString*) account viewHint:(NSString*)viewHint {
948 [self addGenericPassword:password account:account viewHint:viewHint access:(id)kSecAttrAccessibleAfterFirstUnlock expecting:errSecSuccess message:@"Add item to keychain with a viewhint"];
951 - (void)updateGenericPassword: (NSString*) newPassword account: (NSString*)account {
952 NSDictionary* query = @{
953 (id)kSecClass : (id)kSecClassGenericPassword,
954 (id)kSecAttrAccessGroup : @"com.apple.security.ckks",
955 (id)kSecAttrAccount : account,
956 (id)kSecAttrSynchronizable : (id)kCFBooleanTrue,
959 NSDictionary* update = @{
960 (id)kSecValueData : (id) [newPassword dataUsingEncoding:NSUTF8StringEncoding],
962 XCTAssertEqual(errSecSuccess, SecItemUpdate((__bridge CFDictionaryRef) query, (__bridge CFDictionaryRef) update), @"Updating item %@ to %@", account, newPassword);
965 - (void)updateAccountOfGenericPassword:(NSString*)newAccount
966 account:(NSString*)account {
967 NSDictionary* query = @{
968 (id)kSecClass : (id)kSecClassGenericPassword,
969 (id)kSecAttrAccessGroup : @"com.apple.security.ckks",
970 (id)kSecAttrAccount : account,
971 (id)kSecAttrSynchronizable : (id)kCFBooleanTrue,
974 NSDictionary* update = @{
975 (id)kSecAttrAccount : (id) newAccount,
977 XCTAssertEqual(errSecSuccess, SecItemUpdate((__bridge CFDictionaryRef) query, (__bridge CFDictionaryRef) update), @"Updating item %@ to account %@", account, newAccount);
980 - (void)deleteGenericPassword: (NSString*) account {
981 NSDictionary* query = @{
982 (id)kSecClass : (id)kSecClassGenericPassword,
983 (id)kSecAttrAccount : account,
984 (id)kSecAttrSynchronizable : (id)kCFBooleanTrue,
987 XCTAssertEqual(errSecSuccess, SecItemDelete((__bridge CFDictionaryRef) query), @"Deleting item %@", account);
990 - (void)findGenericPassword: (NSString*) account expecting: (OSStatus) status {
991 NSDictionary *query = @{(id)kSecClass : (id)kSecClassGenericPassword,
992 (id)kSecAttrAccessGroup : @"com.apple.security.ckks",
993 (id)kSecAttrAccount : account,
994 (id)kSecAttrSynchronizable : (id)kCFBooleanTrue,
995 (id)kSecMatchLimit : (id)kSecMatchLimitOne,
997 XCTAssertEqual(status, SecItemCopyMatching((__bridge CFDictionaryRef) query, NULL), "Finding item %@", account);
1000 - (void)checkGenericPassword: (NSString*) password account: (NSString*) account {
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 (id)kSecReturnData : (id)kCFBooleanTrue,
1008 CFTypeRef result = NULL;
1010 XCTAssertEqual(errSecSuccess, SecItemCopyMatching((__bridge CFDictionaryRef) query, &result), "Item %@ should exist", account);
1011 XCTAssertNotNil((__bridge id)result, "Should have received an item");
1013 NSString* storedPassword = [[NSString alloc] initWithData: (__bridge NSData*) result encoding: NSUTF8StringEncoding];
1014 XCTAssertNotNil(storedPassword, "Password should parse as a UTF8 password");
1016 XCTAssertEqualObjects(storedPassword, password, "Stored password should match received password");
1019 -(XCTestExpectation*)expectChangeForView:(NSString*)view {
1020 NSString* notification = [NSString stringWithFormat: @"com.apple.security.view-change.%@", view];
1021 return [self expectationForNotification:notification object:nil handler:^BOOL(NSNotification * _Nonnull nsnotification) {
1022 secnotice("ckks", "Got a notification for %@: %@", notification, nsnotification);
1027 - (void)expectCKKSTLKSelfShareUpload:(CKRecordZoneID*)zoneID {
1028 [self expectCKModifyKeyRecords:0 currentKeyPointerRecords:0 tlkShareRecords:1 zoneID:zoneID];
1031 - (void)checkNSyncableTLKsInKeychain:(size_t)n {
1032 NSDictionary *query = @{(id)kSecClass : (id)kSecClassInternetPassword,
1033 (id)kSecAttrAccessGroup : @"com.apple.security.ckks",
1034 (id)kSecAttrSynchronizable : (id)kCFBooleanTrue,
1035 (id)kSecAttrDescription: SecCKKSKeyClassTLK,
1036 (id)kSecMatchLimit : (id)kSecMatchLimitAll,
1037 (id)kSecReturnAttributes : (id)kCFBooleanTrue,
1039 CFTypeRef result = NULL;
1042 XCTAssertEqual(errSecItemNotFound, SecItemCopyMatching((__bridge CFDictionaryRef) query, &result), "Should have found no TLKs");
1044 XCTAssertEqual(errSecSuccess, SecItemCopyMatching((__bridge CFDictionaryRef) query, &result), "Should have found TLKs");
1045 NSArray* items = (NSArray*) CFBridgingRelease(result);
1047 XCTAssertEqual(items.count, n, "Should have received %lu items", n);