KeychainXCTest.m   [plain text]

/*
* Copyright (c) 2018 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@
*/

#import "KeychainXCTest.h"
#import "SecDbKeychainItem.h"
#import "SecdTestKeychainUtilities.h"
#import "CKKS.h"
#import "SecDbKeychainItemV7.h"
#import "SecDbKeychainMetadataKeyStore.h"
#import "SecDbBackupManager_Internal.h"
#import "SecAKSObjCWrappers.h"
#import "SecItemPriv.h"
#import "SecTaskPriv.h"
#import "server_security_helpers.h"
#import "SecItemServer.h"
#import "spi.h"
#import "SecDbKeychainSerializedItemV7.h"
#import "SecDbKeychainSerializedMetadata.h"
#import "SecDbKeychainSerializedSecretData.h"
#import "SecDbKeychainSerializedAKSWrappedKey.h"
#import "SecCDKeychain.h"
#import <utilities/SecCFWrappers.h>
#import <SecurityFoundation/SFEncryptionOperation.h>
#import <SecurityFoundation/SFCryptoServicesErrors.h>
#import <SecurityFoundation/SFKeychain.h>
#import <XCTest/XCTest.h>
#import <OCMock/OCMock.h>
#include <corecrypto/ccpbkdf2.h>
#include <corecrypto/ccsha2.h>
#include <corecrypto/ccaes.h>
#include <corecrypto/ccmode.h>
#include <corecrypto/ccwrap.h>
#include "CheckV12DevEnabled.h"

void* testlist = NULL;

// TODO: Switch to '1' closer to deployment, but leave at '0' for now to test not breaking people
static int testCheckV12DevEnabled(void) {
return 0;
}

@implementation KeychainXCTestFailureLogger
- (instancetype)init {
if((self = [super init])) {
}
return self;
}

- (void)testCase:(XCTestCase *)testCase didRecordIssue:(XCTIssue *)issue {
secnotice("keychainxctest", "XCTest failure: (%@)%@:%lu error: %@ -- %@\n%@",
testCase.name,
issue.sourceCodeContext.location.fileURL,
(unsigned long)issue.sourceCodeContext.location.lineNumber,
issue.compactDescription,
issue.detailedDescription,
issue.sourceCodeContext.callStack);
}
@end

#if USE_KEYSTORE

@interface SecDbKeychainItemV7 ()

+ (SFAESKeySpecifier*)keySpecifier;

@end

@interface FakeAKSRefKey : NSObject <SecAKSRefKey>
@end

@implementation FakeAKSRefKey {
SFAESKey* _key;
}

- (instancetype)initWithKeybag:(keybag_handle_t)keybag keyclass:(keyclass_t)keyclass
{
if (self = [super init]) {
_key = [[SFAESKey alloc] initRandomKeyWithSpecifier:[[SFAESKeySpecifier alloc] initWithBitSize:SFAESKeyBitSize256] error:nil];
}

return self;
}

- (instancetype)initWithBlob:(NSData*)blob keybag:(keybag_handle_t)keybag
{
if (self = [super init]) {
_key = [[SFAESKey alloc] initWithData:blob specifier:[[SFAESKeySpecifier alloc] initWithBitSize:SFAESKeyBitSize256] error:nil];
}

return self;
}

- (NSData*)wrappedDataForKey:(SFAESKey*)key
{
SFAuthenticatedEncryptionOperation* encryptionOperation = [[SFAuthenticatedEncryptionOperation alloc] initWithKeySpecifier:[[SFAESKeySpecifier alloc] initWithBitSize:SFAESKeyBitSize256]];
return [NSKeyedArchiver archivedDataWithRootObject:[encryptionOperation encrypt:key.keyData withKey:_key error:nil] requiringSecureCoding:YES error:nil];
}

- (SFAESKey*)keyWithWrappedData:(NSData*)wrappedKeyData
{
SFAESKeySpecifier* keySpecifier = [[SFAESKeySpecifier alloc] initWithBitSize:SFAESKeyBitSize256];
SFAuthenticatedEncryptionOperation* encryptionOperation = [[SFAuthenticatedEncryptionOperation alloc] initWithKeySpecifier:keySpecifier];
NSData* keyData = [encryptionOperation decrypt:[NSKeyedUnarchiver unarchivedObjectOfClass:[SFAuthenticatedCiphertext class] fromData:wrappedKeyData error:nil] withKey:_key error:nil];
return [[SFAESKey alloc] initWithData:keyData specifier:keySpecifier error:nil];
}

- (NSData*)refKeyBlob
{
return _key.keyData;
}

@end

@implementation SFKeychainServerFakeConnection {
NSArray* _fakeAccessGroups;
}

- (void)setFakeAccessGroups:(NSArray*)fakeAccessGroups
{
_fakeAccessGroups = fakeAccessGroups.copy;
}

- (NSArray*)clientAccessGroups
{
return _fakeAccessGroups ?: @[@"com.apple.token"];
}

@end

@implementation KeychainXCTest {
id _keychainPartialMock;
CFArrayRef _originalAccessGroups;
bool _simcrashenabled;
}

static KeychainXCTestFailureLogger* _testFailureLoggerVariable;

@synthesize keychainPartialMock = _keychainPartialMock;

+ (void)setUp
{
[super setUp];
SecCKKSDisable();

self.testFailureLogger = [[KeychainXCTestFailureLogger alloc] init];
[[XCTestObservationCenter sharedTestObservationCenter] addTestObserver:self.testFailureLogger];

// Do not want test code to be allowed to init real keychain!
secd_test_setup_temp_keychain("keychaintestthrowaway", NULL);
securityd_init(NULL);
}

- (void)setUp
{
_simcrashenabled = __security_simulatecrash_enabled();
__security_simulatecrash_enable(false);

[super setUp];

self.lockState = LockStateUnlocked;
self.allowDecryption = true;
self.didAKSDecrypt = NO;
self.simulateRolledAKSKey = NO;

secnotice("keychainxctest", "Beginning test %@", self.name);

self.keyclassUsedForAKSDecryption = 0;

self.keySpecifier = [[SFAESKeySpecifier alloc] initWithBitSize:SFAESKeyBitSize256];
[self setNewFakeAKSKey:[NSData dataWithBytes:"1234567890123456789012345678901" length:32]];

self.mockSecDbKeychainItemV7 = OCMClassMock([SecDbKeychainItemV7 class]);
[[[self.mockSecDbKeychainItemV7 stub] andCall:@selector(decryptionOperation) onObject:self] decryptionOperation];

self.mockSecAKSObjCWrappers = OCMClassMock([SecAKSObjCWrappers class]);
[[[[self.mockSecAKSObjCWrappers stub] andCall:@selector(fakeAKSEncryptWithKeybag:keyclass:plaintext:outKeyclass:ciphertext:error:) onObject:self] ignoringNonObjectArgs] aksEncryptWithKeybag:0 keyclass:0 plaintext:[OCMArg any] outKeyclass:NULL ciphertext:[OCMArg any] error:NULL];
[[[[self.mockSecAKSObjCWrappers stub] andCall:@selector(fakeAKSDecryptWithKeybag:keyclass:ciphertext:outKeyclass:plaintext:error:) onObject:self] ignoringNonObjectArgs] aksDecryptWithKeybag:0 keyclass:0 ciphertext:[OCMArg any] outKeyclass:NULL plaintext:[OCMArg any] error:NULL];

// bring back with <rdar://problem/37523001>
// [[[self.mockSecDbKeychainItemV7 stub] andCall:@selector(isKeychainUnlocked) onObject:self] isKeychainUnlocked];

id refKeyMock = OCMClassMock([SecAKSRefKey class]);
[[[refKeyMock stub] andCall:@selector(alloc) onObject:[FakeAKSRefKey class]] alloc];

checkV12DevEnabled = testCheckV12DevEnabled;
NSArray* partsOfName = [self.name componentsSeparatedByCharactersInSet:[NSCharacterSet characterSetWithCharactersInString:@" ]"]];
self.keychainDirectoryPrefix = partsOfName[1];

// Calls SecKeychainDbReset which also resets metadata keys and backup manager
secd_test_setup_temp_keychain([self.keychainDirectoryPrefix UTF8String], NULL);

_originalAccessGroups = SecAccessGroupsGetCurrent();
SecResetLocalSecuritydXPCFakeEntitlements();
}

- (void)tearDown
{
[self.mockSecDbKeychainItemV7 stopMocking];
[self.mockSecAKSObjCWrappers stopMocking];
[self resetEntitlements];
SecAccessGroupsSetCurrent(_originalAccessGroups);
__security_simulatecrash_enable(_simcrashenabled);

if(self.keychainDirectoryPrefix) {
XCTAssertTrue(secd_test_teardown_delete_temp_keychain([self.keychainDirectoryPrefix UTF8String]), "Should be able to delete the temp keychain");
} else {
XCTFail("Should have had a keychain directory to remove");
}

secnotice("keychainxctest", "Ending test %@", self.name);

[super tearDown];
}

+ (void)tearDown {
secd_test_teardown_delete_temp_keychain("keychaintestthrowaway");
SecResetLocalSecuritydXPCFakeEntitlements();
[super tearDown];

[[XCTestObservationCenter sharedTestObservationCenter] removeTestObserver:self.testFailureLogger];
}

+ (KeychainXCTestFailureLogger*)testFailureLogger {
return _testFailureLoggerVariable;
}

+ (void)setTestFailureLogger:(KeychainXCTestFailureLogger*)logger {
_testFailureLoggerVariable = logger;
}

- (bool)isKeychainUnlocked
{
return self.lockState == LockStateUnlocked;
}

- (id)decryptionOperation
{
return self.allowDecryption ? [[SFAuthenticatedEncryptionOperation alloc] initWithKeySpecifier:[SecDbKeychainItemV7 keySpecifier]] : nil;
}

- (bool)setNewFakeAKSKey:(NSData*)newKeyData
{
self.fakeAKSKey = newKeyData;
return true;
}

- (NSData*)wrapKey:(NSData*)plaintextKey withKey:(NSData*)wrappingKey
{
const struct ccmode_ecb *ecb_mode = ccaes_ecb_encrypt_mode();
ccecb_ctx_decl(ccecb_context_size(ecb_mode), key);
NSMutableData* wrappedKey = [NSMutableData dataWithLength:ccwrap_wrapped_size(plaintextKey.length)];

ccecb_init(ecb_mode, key, wrappingKey.length, wrappingKey.bytes);

size_t obytes = 0;
int wrap_status = ccwrap_auth_encrypt(ecb_mode, key, plaintextKey.length, plaintextKey.bytes,
&obytes, wrappedKey.mutableBytes);
if (wrap_status == 0) {
assert(obytes == wrappedKey.length);
} else {
wrappedKey = nil;
}

ccecb_ctx_clear(ccecb_context_size(ecb_mode), key);
return wrappedKey;
}

- (NSData*)unwrapKey:(NSData*)ciphertextKey withKey:(NSData*)wrappingKey
{
const struct ccmode_ecb *ecb_mode = ccaes_ecb_decrypt_mode();
ccecb_ctx_decl(ccecb_context_size(ecb_mode), key);
NSMutableData *unwrappedKey = [NSMutableData dataWithLength:ccwrap_unwrapped_size(ciphertextKey.length)];

ccecb_init(ecb_mode, key, wrappingKey.length, wrappingKey.bytes);

size_t obytes = 0;
int status = ccwrap_auth_decrypt(ecb_mode, key, ciphertextKey.length, ciphertextKey.bytes,
&obytes, unwrappedKey.mutableBytes);
if (status == 0) {
assert(obytes == (size_t)[unwrappedKey length]);
} else {
unwrappedKey = nil;
}

ccecb_ctx_clear(ccecb_context_size(ecb_mode), key);
return unwrappedKey;
}

- (bool)fakeAKSEncryptWithKeybag:(keybag_handle_t)keybag
keyclass:(keyclass_t)keyclass
plaintext:(NSData*)plaintext
outKeyclass:(keyclass_t*)outKeyclass
ciphertext:(NSMutableData*)ciphertextOut
error:(NSError**)error
{
if (self.lockState == LockStateLockedAndDisallowAKS) {
if (error) {
*error = [NSError errorWithDomain:(__bridge NSString *)kSecErrorDomain code:errSecInteractionNotAllowed userInfo:NULL];
}
return false;
}

if (keybag == KEYBAG_DEVICE) {
XCTAssertLessThanOrEqual(ciphertextOut.length, APPLE_KEYSTORE_MAX_SYM_WRAPPED_KEY_LEN);
} else { // this'll do for now: assume non-device bags are asymmetric backup bags
XCTAssertLessThanOrEqual(ciphertextOut.length, APPLE_KEYSTORE_MAX_ASYM_WRAPPED_KEY_LEN);
}

NSData* wrappedKey = [self wrapKey:plaintext withKey:self.fakeAKSKey];
if (ciphertextOut.length >= wrappedKey.length) {
memcpy(ciphertextOut.mutableBytes, wrappedKey.bytes, wrappedKey.length);
ciphertextOut.length = wrappedKey.length; // simulate ks_crypt behavior
if (self.simulateRolledAKSKey && outKeyclass) {
*outKeyclass = keyclass | (key_class_last + 1);
} else if (outKeyclass) {
*outKeyclass = keyclass;
}
return true;
} else {
XCTFail(@"output buffer too small for wrapped key");
return false;
}
}

- (bool)fakeAKSDecryptWithKeybag:(keybag_handle_t)keybag
keyclass:(keyclass_t)keyclass
ciphertext:(NSData*)ciphertextIn
outKeyclass:(keyclass_t*)outKeyclass
plaintext:(NSMutableData*)plaintext
error:(NSError**)error
{
if (self.lockState == LockStateLockedAndDisallowAKS) {
if (error) {
*error = [NSError errorWithDomain:(__bridge NSString *)kSecErrorDomain code:errSecInteractionNotAllowed userInfo:NULL];
}
return false;
}

if (self.simulateRolledAKSKey && keyclass < key_class_last) {
// let's make decryption fail like it would if this were an old metadata key entry made with a generational AKS key, but we didn't store that info in the database
return false;
}

self.keyclassUsedForAKSDecryption = keyclass;
NSData* unwrappedKey = [self unwrapKey:ciphertextIn withKey:self.fakeAKSKey];
if (unwrappedKey && plaintext.length >= unwrappedKey.length) {
memcpy(plaintext.mutableBytes, unwrappedKey.bytes, unwrappedKey.length);
plaintext.length = unwrappedKey.length; // simulate ks_crypt behavior
self.didAKSDecrypt = YES;
return true;
} else if (unwrappedKey) {
XCTFail(@"output buffer too small for unwrapped key");
return false;
} else {
if (error && !self.simulateRolledAKSKey && keyclass > key_class_last) {
// for this case we want to simulate what happens when we try decrypting with a rolled keyclass on a device which has never been rolled, which is it ends up with a NotPermitted error from AKS which the security layer translates as locked keybag
*error = [NSError errorWithDomain:NSOSStatusErrorDomain code:errSecInteractionNotAllowed userInfo:nil];
}
else if (error) {
*error = [NSError errorWithDomain:NSOSStatusErrorDomain code:errSecDecode userInfo:nil];
}
return false;
}
}

- (NSData*)getDatabaseKeyDataWithError:(NSError**)error
{
if (_lockState == LockStateUnlocked) {
return [NSData dataWithBytes:"1234567890123456789012345678901" length:32];
}
else {
if (error) {
// <rdar://problem/38972671> add SFKeychainErrorDeviceLocked
*error = [NSError errorWithDomain:SFKeychainErrorDomain code:SFKeychainErrorFailedToCommunicateWithServer userInfo:nil];
}
return nil;
}
}

// Mock SecTask entitlement retrieval API, so that we can test access group entitlement parsing code in SecTaskCopyAccessGroups()
static NSDictionary *currentEntitlements = nil;
static BOOL currentEntitlementsValidated = YES;

CFTypeRef SecTaskCopyValueForEntitlement(SecTaskRef task, CFStringRef entitlement, CFErrorRef *error) {
id value = currentEntitlements[(__bridge id)entitlement];
if (value == nil && error != NULL) {
*error = (CFErrorRef)CFBridgingRetain([NSError errorWithDomain:NSPOSIXErrorDomain code:EINVAL userInfo:nil]);
}
return CFBridgingRetain(value);
}

Boolean SecTaskEntitlementsValidated(SecTaskRef task) {
return currentEntitlementsValidated;
}

- (void)setEntitlements:(NSDictionary<NSString *, id> *)entitlements validated:(BOOL)validated {
currentEntitlements = entitlements;
currentEntitlementsValidated = validated;
id task = CFBridgingRelease(SecTaskCreateFromSelf(kCFAllocatorDefault));
NSArray *currentAccessGroups = CFBridgingRelease(SecTaskCopyAccessGroups((__bridge SecTaskRef)task));
SecAccessGroupsSetCurrent((__bridge CFArrayRef)currentAccessGroups); // SetCurrent retains the access groups
}

- (void)resetEntitlements {
currentEntitlements = nil;
currentEntitlementsValidated = YES;
}

@end

#endif