CloudKitKeychainSyncingMockXCTest.m [plain text]
/*
* Copyright (c) 2017 Apple Inc. All Rights Reserved.
*
* @APPLE_LICENSE_HEADER_START@
*
* This file contains Original Code and/or Modifications of Original Code
* as defined in and that are subject to the Apple Public Source License
* Version 2.0 (the 'License'). You may not use this file except in
* compliance with the License. Please obtain a copy of the License at
* http://www.opensource.apple.com/apsl/ and read it before using this
* file.
*
* The Original Code and all software distributed under the License are
* distributed on an 'AS IS' basis, WITHOUT WARRANTY OF ANY KIND, EITHER
* EXPRESS OR IMPLIED, AND APPLE HEREBY DISCLAIMS ALL SUCH WARRANTIES,
* INCLUDING WITHOUT LIMITATION, ANY WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE, QUIET ENJOYMENT OR NON-INFRINGEMENT.
* Please see the License for the specific language governing rights and
* limitations under the License.
*
* @APPLE_LICENSE_HEADER_END@
*/
#if OCTAGON
#import <OCMock/OCMock.h>
#import "keychain/ckks/tests/CloudKitMockXCTest.h"
#import "keychain/ckks/tests/CloudKitKeychainSyncingMockXCTest.h"
#import <securityd/SecItemServer.h>
#import <securityd/SecItemDb.h>
#import "keychain/ckks/CKKS.h"
#import "keychain/ckks/CKKSKeychainView.h"
#import "keychain/ckks/CKKSCurrentKeyPointer.h"
#import "keychain/ckks/CKKSItemEncrypter.h"
#import "keychain/ckks/CKKSKey.h"
#import "keychain/ckks/CKKSOutgoingQueueEntry.h"
#import "keychain/ckks/CKKSIncomingQueueEntry.h"
#import "keychain/ckks/CKKSSynchronizeOperation.h"
#import "keychain/ckks/CKKSViewManager.h"
#import "keychain/ckks/CKKSZoneStateEntry.h"
#import "keychain/ckks/CKKSManifest.h"
#import "Security/SecureObjectSync/SOSAccount.h"
@implementation ZoneKeys
@end
// No tests here, just helper functions
@implementation CloudKitKeychainSyncingMockXCTest
- (void)setUp {
// Need to convince your tests to set these, no matter what the on-disk pist says? Uncomment.
(void)[CKKSManifest shouldSyncManifests]; // perfrom initialization
SecCKKSSetSyncManifests(false);
SecCKKSSetEnforceManifests(false);
// Check that your environment is set up correctly
XCTAssertFalse([CKKSManifest shouldSyncManifests], "Manifests syncing is disabled");
XCTAssertFalse([CKKSManifest shouldEnforceManifests], "Manifests enforcement is disabled");
[super setUp];
self.keys = [[NSMutableDictionary alloc] init];
// Fake out whether class A keys can be loaded from the keychain.
self.mockCKKSKey = OCMClassMock([CKKSKey class]);
__weak __typeof(self) weakSelf = self;
OCMStub([self.mockCKKSKey loadKeyMaterialFromKeychain:[OCMArg checkWithBlock:^BOOL(CKKSKey* key) {
__strong __typeof(self) strongSelf = weakSelf;
return ([key.keyclass isEqualToString: SecCKKSKeyClassA] || [key.keyclass isEqualToString: SecCKKSKeyClassTLK]) && strongSelf.aksLockState;
}]
resave:[OCMArg anyPointer]
error:[OCMArg anyObjectRef]]).andCall(self, @selector(handleLockLoadKeyMaterialFromKeychain:resave:error:));
OCMStub([self.mockCKKSKey saveKeyMaterialToKeychain:[OCMArg checkWithBlock:^BOOL(CKKSKey* key) {
__strong __typeof(self) strongSelf = weakSelf;
return ([key.keyclass isEqualToString: SecCKKSKeyClassA] || [key.keyclass isEqualToString: SecCKKSKeyClassTLK]) && strongSelf.aksLockState;
}]
stashTLK:[OCMArg anyObjectRef]
error:[OCMArg anyObjectRef]]
).andCall(self, @selector(handleLockSaveKeyMaterialToKeychain:stashTLK:error:));
}
- (void)tearDown {
[self.mockCKKSKey stopMocking];
self.mockCKKSKey = nil;
[super tearDown];
self.keys = nil;
}
- (void)createClassCItemAndWaitForUpload:(CKRecordZoneID*)zoneID account:(NSString*)account {
[self expectCKModifyItemRecords:1
currentKeyPointerRecords:1
zoneID:zoneID
checkItem:[self checkClassCBlock:zoneID message:@"Object was encrypted under class C key in hierarchy"]];
[self addGenericPassword: @"data" account: account];
OCMVerifyAllWithDelay(self.mockDatabase, 8);
}
- (void)createClassAItemAndWaitForUpload:(CKRecordZoneID*)zoneID account:(NSString*)account {
[self expectCKModifyItemRecords:1
currentKeyPointerRecords:1
zoneID:zoneID
checkItem: [self checkClassABlock:zoneID message:@"Object was encrypted under class A key in hierarchy"]];
[self addGenericPassword:@"asdf"
account:account
viewHint:nil
access:(id)kSecAttrAccessibleWhenUnlocked
expecting:errSecSuccess
message:@"Adding class A item"];
OCMVerifyAllWithDelay(self.mockDatabase, 8);
}
// Helpers to handle keychain 'locked' loading
-(bool)handleLockLoadKeyMaterialFromKeychain:(CKKSKey*)key resave:(bool*)resavePtr error:(NSError * __autoreleasing *) error {
XCTAssertTrue(self.aksLockState, "Failing a read only when keychain is locked");
if(error) {
*error = [NSError errorWithDomain:@"securityd" code:errSecInteractionNotAllowed userInfo:nil];
}
return false;
}
-(bool)handleLockSaveKeyMaterialToKeychain:(CKKSKey*)key stashTLK:(bool)stashTLK error:(NSError * __autoreleasing *) error {
XCTAssertTrue(self.aksLockState, "Failing a write only when keychain is locked");
if(error) {
*error = [NSError errorWithDomain:@"securityd" code:errSecInteractionNotAllowed userInfo:nil];
}
return false;
}
- (ZoneKeys*)createFakeKeyHierarchy: (CKRecordZoneID*)zoneID oldTLK:(CKKSKey*) oldTLK {
if(self.keys[zoneID]) {
// Already created. Skip.
return self.keys[zoneID];
}
NSError* error = nil;
ZoneKeys* zonekeys = [[ZoneKeys alloc] init];
zonekeys.tlk = [self fakeTLK:zoneID];
[zonekeys.tlk CKRecordWithZoneID: zoneID]; // no-op here, but memoize in the object
zonekeys.currentTLKPointer = [[CKKSCurrentKeyPointer alloc] initForClass: SecCKKSKeyClassTLK currentKeyUUID: zonekeys.tlk.uuid zoneID:zoneID encodedCKRecord: nil];
[zonekeys.currentTLKPointer CKRecordWithZoneID: zoneID];
if(oldTLK) {
zonekeys.rolledTLK = oldTLK;
[zonekeys.rolledTLK wrapUnder: zonekeys.tlk error:&error];
XCTAssertNotNil(zonekeys.rolledTLK, "Created a rolled TLK");
XCTAssertNil(error, "No error creating rolled TLK");
}
zonekeys.classA = [CKKSKey randomKeyWrappedByParent: zonekeys.tlk keyclass:SecCKKSKeyClassA error:&error];
XCTAssertNotNil(zonekeys.classA, "make Class A key");
zonekeys.classA.currentkey = true;
[zonekeys.classA CKRecordWithZoneID: zoneID];
zonekeys.currentClassAPointer = [[CKKSCurrentKeyPointer alloc] initForClass: SecCKKSKeyClassA currentKeyUUID: zonekeys.classA.uuid zoneID:zoneID encodedCKRecord: nil];
[zonekeys.currentClassAPointer CKRecordWithZoneID: zoneID];
zonekeys.classC = [CKKSKey randomKeyWrappedByParent: zonekeys.tlk keyclass:SecCKKSKeyClassC error:&error];
XCTAssertNotNil(zonekeys.classC, "make Class C key");
zonekeys.classC.currentkey = true;
[zonekeys.classC CKRecordWithZoneID: zoneID];
zonekeys.currentClassCPointer = [[CKKSCurrentKeyPointer alloc] initForClass: SecCKKSKeyClassC currentKeyUUID: zonekeys.classC.uuid zoneID:zoneID encodedCKRecord: nil];
[zonekeys.currentClassCPointer CKRecordWithZoneID: zoneID];
self.keys[zoneID] = zonekeys;
return zonekeys;
}
- (void)saveFakeKeyHierarchyToLocalDatabase: (CKRecordZoneID*)zoneID {
NSError* error = nil;
ZoneKeys* zonekeys = [self createFakeKeyHierarchy: zoneID oldTLK:nil];
[zonekeys.tlk saveToDatabase:&error];
XCTAssertNil(error, "TLK saved to database successfully");
[zonekeys.classA saveToDatabase:&error];
XCTAssertNil(error, "Class A key saved to database successfully");
[zonekeys.classC saveToDatabase:&error];
XCTAssertNil(error, "Class C key saved to database successfully");
[zonekeys.currentTLKPointer saveToDatabase:&error];
XCTAssertNil(error, "Current TLK pointer saved to database successfully");
[zonekeys.currentClassAPointer saveToDatabase:&error];
XCTAssertNil(error, "Current Class A pointer saved to database successfully");
[zonekeys.currentClassCPointer saveToDatabase:&error];
XCTAssertNil(error, "Current Class C pointer saved to database successfully");
}
- (void)putFakeKeyHierarchyInCloudKit: (CKRecordZoneID*)zoneID {
ZoneKeys* zonekeys = [self createFakeKeyHierarchy: zoneID oldTLK:nil];
[self.zones[zoneID] addToZone: zonekeys.tlk zoneID: zoneID];
[self.zones[zoneID] addToZone: zonekeys.classA zoneID: zoneID];
[self.zones[zoneID] addToZone: zonekeys.classC zoneID: zoneID];
[self.zones[zoneID] addToZone: zonekeys.currentTLKPointer zoneID: zoneID];
[self.zones[zoneID] addToZone: zonekeys.currentClassAPointer zoneID: zoneID];
[self.zones[zoneID] addToZone: zonekeys.currentClassCPointer zoneID: zoneID];
if(zonekeys.rolledTLK) {
[self.zones[zoneID] addToZone: zonekeys.rolledTLK zoneID: zoneID];
}
}
- (void)rollFakeKeyHierarchyInCloudKit: (CKRecordZoneID*)zoneID {
ZoneKeys* zonekeys = self.keys[zoneID];
self.keys[zoneID] = nil;
CKKSKey* oldTLK = zonekeys.tlk;
[self createFakeKeyHierarchy: zoneID oldTLK:oldTLK];
[self putFakeKeyHierarchyInCloudKit: zoneID];
}
- (void)saveTLKMaterialToKeychainSimulatingSOS: (CKRecordZoneID*)zoneID {
XCTAssertNotNil(self.keys[zoneID].tlk, "Have a TLK to save for zone
__block CFErrorRef cferror = NULL;
kc_with_dbt(true, &cferror, ^bool (SecDbConnectionRef dbt) {
bool ok = kc_transaction_type(dbt, kSecDbExclusiveRemoteSOSTransactionType, &cferror, ^bool {
NSError* error = nil;
[self.keys[zoneID].tlk saveKeyMaterialToKeychain: false error:&error];
XCTAssertNil(error, @"Saved TLK material to keychain");
return true;
});
return ok;
});
XCTAssertNil( (__bridge NSError*)cferror, @"no error with transaction");
CFReleaseNull(cferror);
}
static SOSFullPeerInfoRef SOSCreateFullPeerInfoFromName(CFStringRef name, SecKeyRef* outSigningKey, SecKeyRef* outOctagonSigningKey, CFErrorRef *error)
{
SOSFullPeerInfoRef result = NULL;
SecKeyRef publicKey = NULL;
CFDictionaryRef gestalt = NULL;
*outSigningKey = GeneratePermanentFullECKey(256, name, error);
*outOctagonSigningKey = GeneratePermanentFullECKey(384, name, error);
gestalt = SOSCreatePeerGestaltFromName(name);
result = SOSFullPeerInfoCreate(NULL, gestalt, NULL, *outSigningKey, *outOctagonSigningKey, error);
CFReleaseNull(gestalt);
CFReleaseNull(publicKey);
return result;
}
- (NSMutableArray<NSData *>*) SOSPiggyICloudIdentities
{
SecKeyRef signingKey = NULL;
SecKeyRef octagonSigningKey = NULL;
NSMutableArray<NSData *>* icloudidentities = [NSMutableArray array];
SOSFullPeerInfoRef fpi = SOSCreateFullPeerInfoFromName(CFSTR("Test Peer"), &signingKey, &octagonSigningKey, NULL);
NSData *data = CFBridgingRelease(SOSPeerInfoCopyData(SOSFullPeerInfoGetPeerInfo(fpi), NULL));
if (data)
[icloudidentities addObject:data];
CFReleaseNull(signingKey);
CFReleaseNull(octagonSigningKey);
return icloudidentities;
}
static CFDictionaryRef SOSCreatePeerGestaltFromName(CFStringRef name)
{
return CFDictionaryCreateForCFTypes(kCFAllocatorDefault,
kPIUserDefinedDeviceNameKey, name,
NULL);
}
-(NSMutableDictionary*)SOSPiggyBackCopyFromKeychain
{
__block NSMutableDictionary *piggybackdata = [[NSMutableDictionary alloc] init];
__block CFErrorRef cferror = NULL;
kc_with_dbt(true, &cferror, ^bool (SecDbConnectionRef dbt) {
bool ok = kc_transaction_type(dbt, kSecDbExclusiveRemoteSOSTransactionType, &cferror, ^bool {
piggybackdata[@"idents"] = [self SOSPiggyICloudIdentities];
piggybackdata[@"tlk"] = SOSAccountGetAllTLKs();
return true;
});
return ok;
});
XCTAssertNil( (__bridge NSError*)cferror, @"no error with transaction");
CFReleaseNull(cferror);
return piggybackdata;
}
- (void)SOSPiggyBackAddToKeychain:(NSDictionary*)piggydata{
__block CFErrorRef cferror = NULL;
kc_with_dbt(true, &cferror, ^bool (SecDbConnectionRef dbt) {
bool ok = kc_transaction_type(dbt, kSecDbExclusiveRemoteSOSTransactionType, &cferror, ^bool {
NSError* error = nil;
NSArray* icloudidentities = piggydata[@"idents"];
NSArray* tlk = piggydata[@"tlk"];
SOSPiggyBackAddToKeychain(icloudidentities, tlk);
XCTAssertNil(error, @"Saved TLK-piggy material to keychain");
return true;
});
return ok;
});
XCTAssertNil( (__bridge NSError*)cferror, @"no error with transaction");
CFReleaseNull(cferror);
}
- (void)saveTLKMaterialToKeychain: (CKRecordZoneID*)zoneID {
NSError* error = nil;
XCTAssertNotNil(self.keys[zoneID].tlk, "Have a TLK to save for zone [self.keys[zoneID].tlk saveKeyMaterialToKeychain:&error];
XCTAssertNil(error, @"Saved TLK material to keychain");
}
- (void)deleteTLKMaterialFromKeychain: (CKRecordZoneID*)zoneID {
NSError* error = nil;
XCTAssertNotNil(self.keys[zoneID].tlk, "Have a TLK to save for zone [self.keys[zoneID].tlk deleteKeyMaterialFromKeychain:&error];
XCTAssertNil(error, @"Saved TLK material to keychain");
}
- (void)saveClassKeyMaterialToKeychain: (CKRecordZoneID*)zoneID {
NSError* error = nil;
XCTAssertNotNil(self.keys[zoneID].classA, "Have a Class A to save for zone [self.keys[zoneID].classA saveKeyMaterialToKeychain:&error];
XCTAssertNil(error, @"Saved Class A material to keychain");
XCTAssertNotNil(self.keys[zoneID].classC, "Have a Class C to save for zone [self.keys[zoneID].classC saveKeyMaterialToKeychain:&error];
XCTAssertNil(error, @"Saved Class C material to keychain");
}
- (void)createAndSaveFakeKeyHierarchy: (CKRecordZoneID*)zoneID {
[self saveFakeKeyHierarchyToLocalDatabase: zoneID];
[self putFakeKeyHierarchyInCloudKit: zoneID];
[self saveTLKMaterialToKeychain: zoneID];
[self saveClassKeyMaterialToKeychain: zoneID];
}
// Override our base class here:
- (void)expectCKModifyKeyRecords: (NSUInteger) expectedNumberOfRecords currentKeyPointerRecords: (NSUInteger) expectedCurrentKeyRecords zoneID: (CKRecordZoneID*) zoneID {
__weak __typeof(self) weakSelf = self;
[self expectCKModifyRecords: @{
SecCKRecordIntermediateKeyType: [NSNumber numberWithUnsignedInteger: expectedNumberOfRecords],
SecCKRecordCurrentKeyType: [NSNumber numberWithUnsignedInteger: expectedCurrentKeyRecords]}
deletedRecordTypeCounts:nil
zoneID:zoneID
checkModifiedRecord:nil
runAfterModification:^{
__strong __typeof(weakSelf) strongSelf = weakSelf;
XCTAssertNotNil(strongSelf, "self exists");
// Reach into our cloudkit database and extract the keys
CKRecordID* currentTLKPointerID = [[CKRecordID alloc] initWithRecordName:SecCKKSKeyClassTLK zoneID:zoneID];
CKRecordID* currentClassAPointerID = [[CKRecordID alloc] initWithRecordName:SecCKKSKeyClassA zoneID:zoneID];
CKRecordID* currentClassCPointerID = [[CKRecordID alloc] initWithRecordName:SecCKKSKeyClassC zoneID:zoneID];
ZoneKeys* zonekeys = strongSelf.keys[zoneID];
if(!zonekeys) {
zonekeys = [[ZoneKeys alloc] init];
strongSelf.keys[zoneID] = zonekeys;
}
XCTAssertNotNil(strongSelf.zones[zoneID].currentDatabase[currentTLKPointerID], "Have a currentTLKPointer");
XCTAssertNotNil(strongSelf.zones[zoneID].currentDatabase[currentClassAPointerID], "Have a currentClassAPointer");
XCTAssertNotNil(strongSelf.zones[zoneID].currentDatabase[currentClassCPointerID], "Have a currentClassCPointer");
XCTAssertNotNil(strongSelf.zones[zoneID].currentDatabase[currentTLKPointerID][SecCKRecordParentKeyRefKey], "Have a currentTLKPointer parent");
XCTAssertNotNil(strongSelf.zones[zoneID].currentDatabase[currentClassAPointerID][SecCKRecordParentKeyRefKey], "Have a currentClassAPointer parent");
XCTAssertNotNil(strongSelf.zones[zoneID].currentDatabase[currentClassCPointerID][SecCKRecordParentKeyRefKey], "Have a currentClassCPointer parent");
XCTAssertNotNil([strongSelf.zones[zoneID].currentDatabase[currentTLKPointerID][SecCKRecordParentKeyRefKey] recordID].recordName, "Have a currentTLKPointer parent UUID");
XCTAssertNotNil([strongSelf.zones[zoneID].currentDatabase[currentClassAPointerID][SecCKRecordParentKeyRefKey] recordID].recordName, "Have a currentClassAPointer parent UUID");
XCTAssertNotNil([strongSelf.zones[zoneID].currentDatabase[currentClassCPointerID][SecCKRecordParentKeyRefKey] recordID].recordName, "Have a currentClassCPointer parent UUID");
zonekeys.currentTLKPointer = [[CKKSCurrentKeyPointer alloc] initWithCKRecord: strongSelf.zones[zoneID].currentDatabase[currentTLKPointerID]];
zonekeys.currentClassAPointer = [[CKKSCurrentKeyPointer alloc] initWithCKRecord: strongSelf.zones[zoneID].currentDatabase[currentClassAPointerID]];
zonekeys.currentClassCPointer = [[CKKSCurrentKeyPointer alloc] initWithCKRecord: strongSelf.zones[zoneID].currentDatabase[currentClassCPointerID]];
XCTAssertNotNil(zonekeys.currentTLKPointer.currentKeyUUID, "Have a currentTLKPointer current UUID");
XCTAssertNotNil(zonekeys.currentClassAPointer.currentKeyUUID, "Have a currentClassAPointer current UUID");
XCTAssertNotNil(zonekeys.currentClassCPointer.currentKeyUUID, "Have a currentClassCPointer current UUID");
CKRecordID* currentTLKID = [[CKRecordID alloc] initWithRecordName:zonekeys.currentTLKPointer.currentKeyUUID zoneID:zoneID];
CKRecordID* currentClassAID = [[CKRecordID alloc] initWithRecordName:zonekeys.currentClassAPointer.currentKeyUUID zoneID:zoneID];
CKRecordID* currentClassCID = [[CKRecordID alloc] initWithRecordName:zonekeys.currentClassCPointer.currentKeyUUID zoneID:zoneID];
zonekeys.tlk = [[CKKSKey alloc] initWithCKRecord: strongSelf.zones[zoneID].currentDatabase[currentTLKID]];
zonekeys.classA = [[CKKSKey alloc] initWithCKRecord: strongSelf.zones[zoneID].currentDatabase[currentClassAID]];
zonekeys.classC = [[CKKSKey alloc] initWithCKRecord: strongSelf.zones[zoneID].currentDatabase[currentClassCID]];
XCTAssertNotNil(zonekeys.tlk, "Have the current TLK");
XCTAssertNotNil(zonekeys.classA, "Have the current Class A key");
XCTAssertNotNil(zonekeys.classC, "Have the current Class C key");
}];
}
- (void)checkNoCKKSData: (CKKSKeychainView*) view {
// Test that there are no items in the database
[view dispatchSync:^bool{
NSError* error = nil;
NSArray<CKKSMirrorEntry*>* ckmes = [CKKSMirrorEntry all: view.zoneID error:&error];
XCTAssertNil(error, "No error fetching CKMEs");
XCTAssertEqual(ckmes.count, 0ul, "No CKMirrorEntries");
NSArray<CKKSOutgoingQueueEntry*>* oqes = [CKKSOutgoingQueueEntry all: view.zoneID error:&error];
XCTAssertNil(error, "No error fetching OQEs");
XCTAssertEqual(oqes.count, 0ul, "No OutgoingQueueEntries");
NSArray<CKKSIncomingQueueEntry*>* iqes = [CKKSIncomingQueueEntry all: view.zoneID error:&error];
XCTAssertNil(error, "No error fetching IQEs");
XCTAssertEqual(iqes.count, 0ul, "No IncomingQueueEntries");
NSArray<CKKSKey*>* keys = [CKKSKey all: view.zoneID error:&error];
XCTAssertNil(error, "No error fetching keys");
XCTAssertEqual(keys.count, 0ul, "No CKKSKeys");
NSArray<CKKSDeviceStateEntry*>* deviceStates = [CKKSDeviceStateEntry allInZone:view.zoneID error:&error];
XCTAssertNil(error, "should be no error fetching device states");
XCTAssertEqual(deviceStates.count, 0ul, "No Device State entries");
return false;
}];
}
- (BOOL (^) (CKRecord*)) checkClassABlock: (CKRecordZoneID*) zoneID message:(NSString*) message {
__weak __typeof(self) weakSelf = self;
return ^BOOL(CKRecord* record) {
__strong __typeof(weakSelf) strongSelf = weakSelf;
XCTAssertNotNil(strongSelf, "self exists");
ZoneKeys* zoneKeys = strongSelf.keys[zoneID];
XCTAssertNotNil(zoneKeys, "Have zone keys for XCTAssertEqualObjects([record[SecCKRecordParentKeyRefKey] recordID].recordName, zoneKeys.classA.uuid, " return [[record[SecCKRecordParentKeyRefKey] recordID].recordName isEqual: zoneKeys.classA.uuid];
};
}
- (BOOL (^) (CKRecord*)) checkClassCBlock: (CKRecordZoneID*) zoneID message:(NSString*) message {
__weak __typeof(self) weakSelf = self;
return ^BOOL(CKRecord* record) {
__strong __typeof(weakSelf) strongSelf = weakSelf;
XCTAssertNotNil(strongSelf, "self exists");
ZoneKeys* zoneKeys = strongSelf.keys[zoneID];
XCTAssertNotNil(zoneKeys, "Have zone keys for XCTAssertEqualObjects([record[SecCKRecordParentKeyRefKey] recordID].recordName, zoneKeys.classC.uuid, " return [[record[SecCKRecordParentKeyRefKey] recordID].recordName isEqual: zoneKeys.classC.uuid];
};
}
- (BOOL (^) (CKRecord*)) checkPasswordBlock:(CKRecordZoneID*)zoneID
account:(NSString*)account
password:(NSString*)password {
__weak __typeof(self) weakSelf = self;
return ^BOOL(CKRecord* record) {
__strong __typeof(weakSelf) strongSelf = weakSelf;
XCTAssertNotNil(strongSelf, "self exists");
ZoneKeys* zoneKeys = strongSelf.keys[zoneID];
XCTAssertNotNil(zoneKeys, "Have zone keys for XCTAssertNotNil([record[SecCKRecordParentKeyRefKey] recordID].recordName, "Have a wrapping key");
CKKSKey* key = nil;
if([[record[SecCKRecordParentKeyRefKey] recordID].recordName isEqualToString: zoneKeys.classC.uuid]) {
key = zoneKeys.classC;
} else if([[record[SecCKRecordParentKeyRefKey] recordID].recordName isEqualToString: zoneKeys.classA.uuid]) {
key = zoneKeys.classA;
}
XCTAssertNotNil(key, "Found a key via UUID");
CKKSMirrorEntry* ckme = [[CKKSMirrorEntry alloc] initWithCKRecord: record];
NSError* error = nil;
NSDictionary* dict = [CKKSItemEncrypter decryptItemToDictionary:ckme.item error:&error];
XCTAssertNil(error, "No error decrypting item");
XCTAssertEqualObjects(account, dict[(id)kSecAttrAccount], "Account matches");
XCTAssertEqualObjects([password dataUsingEncoding:NSUTF8StringEncoding], dict[(id)kSecValueData], "Password matches");
return YES;
};
}
- (NSDictionary*)fakeRecordDictionary:(NSString*) account zoneID:(CKRecordZoneID*)zoneID {
NSError* error = nil;
/* Basically: @{
@"acct" : @"account-delete-me",
@"agrp" : @"com.apple.security.sos",
@"cdat" : @"2016-12-21 03:33:25 +0000",
@"class" : @"genp",
@"mdat" : @"2016-12-21 03:33:25 +0000",
@"musr" : [[NSData alloc] init],
@"pdmn" : @"ak",
@"sha1" : [[NSData alloc] initWithBase64EncodedString: @"C3VWONaOIj8YgJjk/xwku4By1CY=" options:0],
@"svce" : @"",
@"tomb" : [NSNumber numberWithInt: 0],
@"v_Data" : [@"data" dataUsingEncoding: NSUTF8StringEncoding],
};
TODO: this should be binary encoded instead of expanded, but the plist encoder should handle it fine */
NSData* itemdata = [[NSData alloc] initWithBase64EncodedString:@"PD94bWwgdmVyc2lvbj0iMS4wIiBlbmNvZGluZz0iVVRGLTgiPz4KPCFET0NUWVBFIHBsaXN0IFBVQkxJQyAiLS8vQXBwbGUvL0RURCBQTElTVCAxLjAvL0VOIiAiaHR0cDovL3d3dy5hcHBsZS5jb20vRFREcy9Qcm9wZXJ0eUxpc3QtMS4wLmR0ZCI+CjxwbGlzdCB2ZXJzaW9uPSIxLjAiPgo8ZGljdD4KCTxrZXk+YWNjdDwva2V5PgoJPHN0cmluZz5hY2NvdW50LWRlbGV0ZS1tZTwvc3RyaW5nPgoJPGtleT5hZ3JwPC9rZXk+Cgk8c3RyaW5nPmNvbS5hcHBsZS5zZWN1cml0eS5zb3M8L3N0cmluZz4KCTxrZXk+Y2RhdDwva2V5PgoJPGRhdGU+MjAxNi0xMi0yMVQwMzozMzoyNVo8L2RhdGU+Cgk8a2V5PmNsYXNzPC9rZXk+Cgk8c3RyaW5nPmdlbnA8L3N0cmluZz4KCTxrZXk+bWRhdDwva2V5PgoJPGRhdGU+MjAxNi0xMi0yMVQwMzozMzoyNVo8L2RhdGU+Cgk8a2V5Pm11c3I8L2tleT4KCTxkYXRhPgoJPC9kYXRhPgoJPGtleT5wZG1uPC9rZXk+Cgk8c3RyaW5nPmFrPC9zdHJpbmc+Cgk8a2V5PnNoYTE8L2tleT4KCTxkYXRhPgoJQzNWV09OYU9JajhZZ0pqay94d2t1NEJ5MUNZPQoJPC9kYXRhPgoJPGtleT5zdmNlPC9rZXk+Cgk8c3RyaW5nPjwvc3RyaW5nPgoJPGtleT50b21iPC9rZXk+Cgk8aW50ZWdlcj4wPC9pbnRlZ2VyPgoJPGtleT52X0RhdGE8L2tleT4KCTxkYXRhPgoJWkdGMFlRPT0KCTwvZGF0YT4KPC9kaWN0Pgo8L3BsaXN0Pgo=" options:0];
NSMutableDictionary * item = [[NSPropertyListSerialization propertyListWithData:itemdata
options:0
format:nil
error:&error] mutableCopy];
// Fix up dictionary
item[@"agrp"] = @"com.apple.security.ckks";
item[@"vwht"] = @"keychain";
XCTAssertNil(error, "no error interpreting data as item");
XCTAssertNotNil(item, "interpreted data as item");
if(zoneID && ![zoneID.zoneName isEqualToString:@"keychain"]) {
[item setObject: zoneID.zoneName forKey: (__bridge id) kSecAttrSyncViewHint];
}
if(account) {
[item setObject: account forKey: (__bridge id) kSecAttrAccount];
}
return item;
}
- (CKRecord*)createFakeRecord: (CKRecordZoneID*)zoneID recordName:(NSString*)recordName {
return [self createFakeRecord: zoneID recordName:recordName withAccount: nil key:nil];
}
- (CKRecord*)createFakeRecord: (CKRecordZoneID*)zoneID recordName:(NSString*)recordName withAccount: (NSString*) account {
return [self createFakeRecord: zoneID recordName:recordName withAccount:account key:nil];
}
- (CKRecord*)createFakeRecord: (CKRecordZoneID*)zoneID recordName:(NSString*)recordName withAccount: (NSString*) account key:(CKKSKey*)key {
NSDictionary* item = [self fakeRecordDictionary: account zoneID:zoneID];
CKRecordID* ckrid = [[CKRecordID alloc] initWithRecordName:recordName zoneID:zoneID];
if(key) {
return [self newRecord: ckrid withNewItemData:item key:key];
} else {
return [self newRecord: ckrid withNewItemData:item];
}
}
- (CKRecord*)newRecord: (CKRecordID*) recordID withNewItemData:(NSDictionary*) dictionary {
ZoneKeys* zonekeys = self.keys[recordID.zoneID];
XCTAssertNotNil(zonekeys, "Have zone keys for zone");
XCTAssertNotNil(zonekeys.classC, "Have class C key for zone");
return [self newRecord:recordID withNewItemData:dictionary key:zonekeys.classC];
}
- (CKRecord*)newRecord: (CKRecordID*) recordID withNewItemData:(NSDictionary*) dictionary key:(CKKSKey*)key {
NSError* error = nil;
CKKSItem* cipheritem = [CKKSItemEncrypter encryptCKKSItem:[[CKKSItem alloc] initWithUUID:recordID.recordName
parentKeyUUID:key.uuid
zoneID:recordID.zoneID]
dataDictionary:dictionary
updatingCKKSItem:nil
parentkey:key
error:&error];
XCTAssertNil(error, "encrypted item with class c key");
XCTAssertNotNil(cipheritem, "Have an encrypted item");
CKRecord* ckr = [cipheritem CKRecordWithZoneID: recordID.zoneID];
XCTAssertNotNil(ckr, "Created a CKRecord");
return ckr;
}
- (NSDictionary*)decryptRecord: (CKRecord*) record {
CKKSItem* item = [[CKKSItem alloc] initWithCKRecord: record];
NSError* error = nil;
NSDictionary* ret = [CKKSItemEncrypter decryptItemToDictionary: item error:&error];
XCTAssertNil(error);
XCTAssertNotNil(ret);
return ret;
}
- (void)addGenericPassword: (NSString*) password account: (NSString*) account viewHint: (NSString*) viewHint access: (NSString*) access expecting: (OSStatus) status message: (NSString*) message {
NSMutableDictionary* query = [@{
(id)kSecClass : (id)kSecClassGenericPassword,
(id)kSecAttrAccessGroup : @"com.apple.security.ckks",
(id)kSecAttrAccessible: access,
(id)kSecAttrAccount : account,
(id)kSecAttrSynchronizable : (id)kCFBooleanTrue,
(id)kSecValueData : (id) [password dataUsingEncoding:NSUTF8StringEncoding],
} mutableCopy];
if(viewHint) {
query[(id)kSecAttrSyncViewHint] = viewHint;
} else {
// Fake it as 'keychain'. This lets CKKSScanLocalItemsOperation for the test-only 'keychain' view find items which would normally not have a view hint.
query[(id)kSecAttrSyncViewHint] = @"keychain";
}
XCTAssertEqual(status, SecItemAdd((__bridge CFDictionaryRef) query, NULL), @"}
- (void)addGenericPassword: (NSString*) password account: (NSString*) account expecting: (OSStatus) status message: (NSString*) message {
[self addGenericPassword:password account:account viewHint:nil access:(id)kSecAttrAccessibleAfterFirstUnlock expecting:errSecSuccess message:message];
}
- (void)addGenericPassword: (NSString*) password account: (NSString*) account {
[self addGenericPassword:password account:account viewHint:nil access:(id)kSecAttrAccessibleAfterFirstUnlock expecting:errSecSuccess message:@"Add item to keychain"];
}
- (void)addGenericPassword: (NSString*) password account: (NSString*) account viewHint:(NSString*)viewHint {
[self addGenericPassword:password account:account viewHint:viewHint access:(id)kSecAttrAccessibleAfterFirstUnlock expecting:errSecSuccess message:@"Add item to keychain with a viewhint"];
}
- (void)updateGenericPassword: (NSString*) newPassword account: (NSString*)account {
NSDictionary* query = @{
(id)kSecClass : (id)kSecClassGenericPassword,
(id)kSecAttrAccessGroup : @"com.apple.security.ckks",
(id)kSecAttrAccount : account,
(id)kSecAttrSynchronizable : (id)kCFBooleanTrue,
};
NSDictionary* update = @{
(id)kSecValueData : (id) [newPassword dataUsingEncoding:NSUTF8StringEncoding],
};
XCTAssertEqual(errSecSuccess, SecItemUpdate((__bridge CFDictionaryRef) query, (__bridge CFDictionaryRef) update), @"Updating item }
- (void)updateAccountOfGenericPassword:(NSString*)newAccount
account:(NSString*)account {
NSDictionary* query = @{
(id)kSecClass : (id)kSecClassGenericPassword,
(id)kSecAttrAccessGroup : @"com.apple.security.ckks",
(id)kSecAttrAccount : account,
(id)kSecAttrSynchronizable : (id)kCFBooleanTrue,
};
NSDictionary* update = @{
(id)kSecAttrAccount : (id) newAccount,
};
XCTAssertEqual(errSecSuccess, SecItemUpdate((__bridge CFDictionaryRef) query, (__bridge CFDictionaryRef) update), @"Updating item }
- (void)deleteGenericPassword: (NSString*) account {
NSDictionary* query = @{
(id)kSecClass : (id)kSecClassGenericPassword,
(id)kSecAttrAccount : account,
(id)kSecAttrSynchronizable : (id)kCFBooleanTrue,
};
XCTAssertEqual(errSecSuccess, SecItemDelete((__bridge CFDictionaryRef) query), @"Deleting item }
- (void)findGenericPassword: (NSString*) account expecting: (OSStatus) status {
NSDictionary *query = @{(id)kSecClass : (id)kSecClassGenericPassword,
(id)kSecAttrAccessGroup : @"com.apple.security.ckks",
(id)kSecAttrAccount : account,
(id)kSecAttrSynchronizable : (id)kCFBooleanTrue,
(id)kSecMatchLimit : (id)kSecMatchLimitOne,
};
XCTAssertEqual(status, SecItemCopyMatching((__bridge CFDictionaryRef) query, NULL), "Finding item }
- (void)checkGenericPassword: (NSString*) password account: (NSString*) account {
NSDictionary *query = @{(id)kSecClass : (id)kSecClassGenericPassword,
(id)kSecAttrAccessGroup : @"com.apple.security.ckks",
(id)kSecAttrAccount : account,
(id)kSecAttrSynchronizable : (id)kCFBooleanTrue,
(id)kSecMatchLimit : (id)kSecMatchLimitOne,
(id)kSecReturnData : (id)kCFBooleanTrue,
};
CFTypeRef result = NULL;
XCTAssertEqual(errSecSuccess, SecItemCopyMatching((__bridge CFDictionaryRef) query, &result), "Finding item XCTAssertNotNil((__bridge id)result, "Received an item");
NSString* storedPassword = [[NSString alloc] initWithData: (__bridge NSData*) result encoding: NSUTF8StringEncoding];
XCTAssertNotNil(storedPassword, "Password parsed as a password");
XCTAssertEqualObjects(storedPassword, password, "Stored password matches received password");
}
-(XCTestExpectation*)expectChangeForView:(NSString*)view {
NSString* notification = [NSString stringWithFormat: @"com.apple.security.view-change. return [self expectationForNotification:notification object:nil handler:^BOOL(NSNotification * _Nonnull nsnotification) {
secnotice("ckks", "Got a notification for return YES;
}];
}
- (void)checkNSyncableTLKsInKeychain:(size_t)n {
NSDictionary *query = @{(id)kSecClass : (id)kSecClassInternetPassword,
(id)kSecAttrAccessGroup : @"com.apple.security.ckks",
(id)kSecAttrSynchronizable : (id)kCFBooleanTrue,
(id)kSecAttrDescription: SecCKKSKeyClassTLK,
(id)kSecMatchLimit : (id)kSecMatchLimitAll,
(id)kSecReturnAttributes : (id)kCFBooleanTrue,
};
CFTypeRef result = NULL;
if(n == 0) {
XCTAssertEqual(errSecItemNotFound, SecItemCopyMatching((__bridge CFDictionaryRef) query, &result), "Should have found no TLKs");
} else {
XCTAssertEqual(errSecSuccess, SecItemCopyMatching((__bridge CFDictionaryRef) query, &result), "Should have found TLKs");
NSArray* items = (NSArray*) CFBridgingRelease(result);
XCTAssertEqual(items.count, n, "Should have received }
}
@end
#endif // OCTAGON