KeychainAPITests.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 "SecItemPriv.h"
#include "SecItemInternal.h"
#import "SecItemServer.h"
#import "spi.h"
#import "SecDbKeychainSerializedItemV7.h"
#import "SecDbKeychainSerializedMetadata.h"
#import "SecDbKeychainSerializedSecretData.h"
#import "SecDbKeychainSerializedAKSWrappedKey.h"
#import <utilities/SecCFWrappers.h>
#import <SecurityFoundation/SFEncryptionOperation.h>
#import <XCTest/XCTest.h>
#import <OCMock/OCMock.h>
#include <dispatch/dispatch.h>
#include <utilities/SecDb.h>
#include <sys/stat.h>
#include <utilities/SecFileLocations.h>
#include "der_plist.h"
#import "SecItemRateLimit_tests.h"
#include "ipc/server_security_helpers.h"
#include <Security/SecEntitlements.h>
#include "keychain/securityd/SecItemDb.h"

#if USE_KEYSTORE

@interface KeychainAPITests : KeychainXCTest
@end

@implementation KeychainAPITests

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

- (NSString*)nameOfTest
{
return [self.name componentsSeparatedByCharactersInSet:[NSCharacterSet characterSetWithCharactersInString:@" ]"]][1];
}

- (void)setUp
{
[super setUp];
// KeychainXCTest already sets up keychain with custom test-named directory
}

- (void)testReturnValuesInSecItemUpdate
{
NSDictionary* addQuery = @{ (id)kSecClass : (id)kSecClassGenericPassword,
(id)kSecValueData : [@"password" dataUsingEncoding:NSUTF8StringEncoding],
(id)kSecAttrAccount : @"TestAccount",
(id)kSecAttrService : @"TestService",
(id)kSecUseDataProtectionKeychain : @(YES),
(id)kSecReturnAttributes : @(YES)
};

NSDictionary* updateQueryWithNoReturn = @{ (id)kSecClass : (id)kSecClassGenericPassword,
(id)kSecAttrAccount : @"TestAccount",
(id)kSecAttrService : @"TestService",
(id)kSecUseDataProtectionKeychain : @(YES)
};

CFTypeRef result = NULL;

// Add the item
XCTAssertEqual(SecItemAdd((__bridge CFDictionaryRef)addQuery, &result), errSecSuccess, @"Should have succeeded in adding test item to keychain");
XCTAssertNotNil((__bridge id)result, @"Should have received a dictionary back from SecItemAdd");
CFReleaseNull(result);

// And we can update the item
XCTAssertEqual(SecItemUpdate((__bridge CFDictionaryRef)updateQueryWithNoReturn, (__bridge CFDictionaryRef)@{(id)kSecValueData: [@"otherpassword" dataUsingEncoding:NSUTF8StringEncoding]}), errSecSuccess, "failed to update item with clean update query");

// great, a normal update works
// now let's do updates with various queries which include return parameters to ensure they succeed on macOS and throw errors on iOS.
// this is a status-quo compromise between changing iOS match macOS (which has lamé no-op characteristics) and changing macOS to match iOS, which risks breaking existing clients

#if TARGET_OS_OSX
NSMutableDictionary* updateQueryWithReturnAttributes = updateQueryWithNoReturn.mutableCopy;
updateQueryWithReturnAttributes[(id)kSecReturnAttributes] = @(YES);
XCTAssertEqual(SecItemUpdate((__bridge CFDictionaryRef)updateQueryWithReturnAttributes, (__bridge CFDictionaryRef)@{(id)kSecValueData: [@"return-attributes" dataUsingEncoding:NSUTF8StringEncoding]}), errSecSuccess, "failed to update item with return attributes query");

NSMutableDictionary* updateQueryWithReturnData = updateQueryWithNoReturn.mutableCopy;
updateQueryWithReturnAttributes[(id)kSecReturnData] = @(YES);
XCTAssertEqual(SecItemUpdate((__bridge CFDictionaryRef)updateQueryWithReturnData, (__bridge CFDictionaryRef)@{(id)kSecValueData: [@"return-data" dataUsingEncoding:NSUTF8StringEncoding]}), errSecSuccess, "failed to update item with return data query");

NSMutableDictionary* updateQueryWithReturnRef = updateQueryWithNoReturn.mutableCopy;
updateQueryWithReturnAttributes[(id)kSecReturnRef] = @(YES);
XCTAssertEqual(SecItemUpdate((__bridge CFDictionaryRef)updateQueryWithReturnRef, (__bridge CFDictionaryRef)@{(id)kSecValueData: [@"return-ref" dataUsingEncoding:NSUTF8StringEncoding]}), errSecSuccess, "failed to update item with return ref query");

NSMutableDictionary* updateQueryWithReturnPersistentRef = updateQueryWithNoReturn.mutableCopy;
updateQueryWithReturnAttributes[(id)kSecReturnPersistentRef] = @(YES);
XCTAssertEqual(SecItemUpdate((__bridge CFDictionaryRef)updateQueryWithReturnPersistentRef, (__bridge CFDictionaryRef)@{(id)kSecValueData: [@"return-persistent-ref" dataUsingEncoding:NSUTF8StringEncoding]}), errSecSuccess, "failed to update item with return persistent ref query");
#else
NSMutableDictionary* updateQueryWithReturnAttributes = updateQueryWithNoReturn.mutableCopy;
updateQueryWithReturnAttributes[(id)kSecReturnAttributes] = @(YES);
XCTAssertEqual(SecItemUpdate((__bridge CFDictionaryRef)updateQueryWithReturnAttributes, (__bridge CFDictionaryRef)@{(id)kSecValueData: [@"return-attributes" dataUsingEncoding:NSUTF8StringEncoding]}), errSecParam, "failed to generate error updating item with return attributes query");

NSMutableDictionary* updateQueryWithReturnData = updateQueryWithNoReturn.mutableCopy;
updateQueryWithReturnData[(id)kSecReturnData] = @(YES);
XCTAssertEqual(SecItemUpdate((__bridge CFDictionaryRef)updateQueryWithReturnData, (__bridge CFDictionaryRef)@{(id)kSecValueData: [@"return-data" dataUsingEncoding:NSUTF8StringEncoding]}), errSecParam, "failed to generate error updating item with return data query");

NSMutableDictionary* updateQueryWithReturnRef = updateQueryWithNoReturn.mutableCopy;
updateQueryWithReturnRef[(id)kSecReturnRef] = @(YES);
XCTAssertEqual(SecItemUpdate((__bridge CFDictionaryRef)updateQueryWithReturnRef, (__bridge CFDictionaryRef)@{(id)kSecValueData: [@"return-ref" dataUsingEncoding:NSUTF8StringEncoding]}), errSecParam, "failed to generate error updating item with return ref query");

NSMutableDictionary* updateQueryWithReturnPersistentRef = updateQueryWithNoReturn.mutableCopy;
updateQueryWithReturnPersistentRef[(id)kSecReturnPersistentRef] = @(YES);
XCTAssertEqual(SecItemUpdate((__bridge CFDictionaryRef)updateQueryWithReturnPersistentRef, (__bridge CFDictionaryRef)@{(id)kSecValueData: [@"return-persistent-ref" dataUsingEncoding:NSUTF8StringEncoding]}), errSecParam, "failed to generate error updating item with return persistent ref query");
#endif
}

- (void)testBadTypeInParams
{
NSMutableDictionary *attrs = @{
(id)kSecClass: (id)kSecClassGenericPassword,
(id)kSecUseDataProtectionKeychain: @YES,
(id)kSecAttrLabel: @"testentry",
}.mutableCopy;

SecItemDelete((CFDictionaryRef)attrs);
XCTAssertEqual(errSecSuccess, SecItemAdd((CFDictionaryRef)attrs, NULL));
XCTAssertEqual(errSecSuccess, SecItemDelete((CFDictionaryRef)attrs));

// We try to fool SecItem API with unexpected type of kSecAttrAccessControl attribute in query and it should not crash.
attrs[(id)kSecAttrAccessControl] = @"string, no SecAccessControlRef!";
XCTAssertEqual(errSecParam, SecItemAdd((CFDictionaryRef)attrs, NULL));
XCTAssertEqual(errSecParam, SecItemDelete((CFDictionaryRef)attrs));
}

- (BOOL)passInternalAttributeToKeychainAPIsWithKey:(id)key value:(id)value {
NSDictionary* badquery = @{
(id)kSecClass : (id)kSecClassGenericPassword,
(id)kSecAttrService : @"AppClipTestService",
(id)kSecUseDataProtectionKeychain : @YES,
(id)kSecValueData : [@"password" dataUsingEncoding:NSUTF8StringEncoding],
key : value,
};
NSDictionary* badupdate = @{key : value};

NSDictionary* okquery = @{
(id)kSecClass : (id)kSecClassGenericPassword,
(id)kSecAttrService : @"AppClipTestService",
(id)kSecUseDataProtectionKeychain : @YES,
(id)kSecValueData : [@"password" dataUsingEncoding:NSUTF8StringEncoding],
};
NSDictionary* okupdate = @{(id)kSecAttrService : @"DifferentService"};

if (SecItemAdd((__bridge CFDictionaryRef)badquery, NULL) != errSecParam) {
XCTFail("SecItemAdd did not return errSecParam");
return NO;
}
if (SecItemCopyMatching((__bridge CFDictionaryRef)badquery, NULL) != errSecParam) {
XCTFail("SecItemCopyMatching did not return errSecParam");
return NO;
}
if (SecItemUpdate((__bridge CFDictionaryRef)badquery, (__bridge CFDictionaryRef)okupdate) != errSecParam) {
XCTFail("SecItemUpdate with bad query did not return errSecParam");
return NO;
}
if (SecItemUpdate((__bridge CFDictionaryRef)okquery, (__bridge CFDictionaryRef)badupdate) != errSecParam) {
XCTFail("SecItemUpdate with bad update did not return errSecParam");
return NO;
}
if (SecItemDelete((__bridge CFDictionaryRef)badquery) != errSecParam) {
XCTFail("SecItemDelete did not return errSecParam");
return NO;
}
return YES;
}

// Expand this, rdar://problem/59297616
- (void)testNotAllowedToPassInternalAttributes {
XCTAssert([self passInternalAttributeToKeychainAPIsWithKey:(__bridge NSString*)kSecAttrAppClipItem value:@YES], @"Expect errSecParam for 'clip' attribute");
}

#pragma mark - Corruption Tests

const uint8_t keychain_data[] = {
0x62, 0x70, 0x6c, 0x69, 0x73, 0x74, 0x30, 0x30, 0xd2, 0x01, 0x02, 0x03,
0x04, 0x5f, 0x10, 0x1b, 0x4e, 0x53, 0x57, 0x69, 0x6e, 0x64, 0x6f, 0x77,
0x20, 0x46, 0x72, 0x61, 0x6d, 0x65, 0x20, 0x50, 0x72, 0x6f, 0x63, 0x65,
0x73, 0x73, 0x50, 0x61, 0x6e, 0x65, 0x6c, 0x5f, 0x10, 0x1d, 0x4e, 0x53,
0x57, 0x69, 0x6e, 0x64, 0x6f, 0x77, 0x20, 0x46, 0x72, 0x61, 0x6d, 0x65,
0x20, 0x41, 0x62, 0x6f, 0x75, 0x74, 0x20, 0x54, 0x68, 0x69, 0x73, 0x20,
0x4d, 0x61, 0x63, 0x5f, 0x10, 0x1c, 0x32, 0x38, 0x20, 0x33, 0x37, 0x33,
0x20, 0x33, 0x34, 0x36, 0x20, 0x32, 0x39, 0x30, 0x20, 0x30, 0x20, 0x30,
0x20, 0x31, 0x34, 0x34, 0x30, 0x20, 0x38, 0x37, 0x38, 0x20, 0x5f, 0x10,
0x1d, 0x35, 0x36, 0x38, 0x20, 0x33, 0x39, 0x35, 0x20, 0x33, 0x30, 0x37,
0x20, 0x33, 0x37, 0x39, 0x20, 0x30, 0x20, 0x30, 0x20, 0x31, 0x34, 0x34,
0x30, 0x20, 0x38, 0x37, 0x38, 0x20, 0x08, 0x0d, 0x2b, 0x4b, 0x6a, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x01, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x05, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x8a
};

dispatch_semaphore_t sema = NULL;

// The real corruption exit handler should xpc_transaction_exit_clean,
// let's be certain it does not. Also make sure exit handler gets called at all
static void SecDbTestCorruptionHandler(void)
{
dispatch_semaphore_signal(sema);
}

- (void)testCorruptionHandler {
__security_simulatecrash_enable(false);

SecDbCorruptionExitHandler = SecDbTestCorruptionHandler;
sema = dispatch_semaphore_create(0);

// Test teardown will want to delete this keychain. Make sure it knows where to look...
NSString* corruptedKeychainPath = [NSString stringWithFormat:@"%@-bad", [self nameOfTest]];

if(self.keychainDirectoryPrefix) {
XCTAssertTrue(secd_test_teardown_delete_temp_keychain([self.keychainDirectoryPrefix UTF8String]), "Should be able to delete the temp keychain");
}

self.keychainDirectoryPrefix = corruptedKeychainPath;

secd_test_setup_temp_keychain([corruptedKeychainPath UTF8String], ^{
CFStringRef keychain_path_cf = __SecKeychainCopyPath();

CFStringPerformWithCString(keychain_path_cf, ^(const char *keychain_path) {
int fd = open(keychain_path, O_RDWR | O_CREAT | O_TRUNC, 0644);
XCTAssert(fd > -1, "Could not open fd to write keychain: %{darwin.errno}d", errno);

size_t written = write(fd, keychain_data, sizeof(keychain_data));
XCTAssertEqual(written, sizeof(keychain_data), "Write garbage to disk, got %lu instead of %lu: %{darwin.errno}d", written, sizeof(keychain_data), errno);
XCTAssertEqual(close(fd), 0, "Close keychain file failed: %{darwin.errno}d", errno);
});

CFReleaseNull(keychain_path_cf);
});

NSDictionary* query = @{(id)kSecClass : (id)kSecClassGenericPassword,
(id)kSecAttrAccount : @"TestAccount",
(id)kSecAttrService : @"TestService",
(id)kSecUseDataProtectionKeychain : @(YES),
(id)kSecReturnAttributes : @(YES)
};

CFTypeRef result = NULL;
// Real keychain should xpc_transaction_exit_clean() after this, but we nerfed it
XCTAssertEqual(SecItemCopyMatching((__bridge CFDictionaryRef)query, &result), errSecNotAvailable, "Expected badness from corrupt keychain");
XCTAssertEqual(dispatch_semaphore_wait(sema, dispatch_time(DISPATCH_TIME_NOW, 1 * NSEC_PER_SEC)), 0, "Timed out waiting for corruption exit handler");

sema = NULL;
SecDbResetCorruptionExitHandler();
CFReleaseNull(result);

NSString* markerpath = [NSString stringWithFormat:@"%@-iscorrupt", CFBridgingRelease(__SecKeychainCopyPath())];
struct stat info = {};
XCTAssertEqual(stat([markerpath UTF8String], &info), 0, "Unable to stat corruption marker: %{darwin.errno}d", errno);
}

- (void)testRecoverFromCorruption {
__security_simulatecrash_enable(false);

// Setup does a reset, but that doesn't create the db yet so let's sneak in first
__block struct stat before = {};
WithPathInKeychainDirectory(CFSTR("keychain-2.db"), ^(const char *filename) {
FILE* file = fopen(filename, "w");
XCTAssert(file != NULL, "Didn't get a FILE pointer");
fclose(file);
XCTAssertEqual(stat(filename, &before), 0, "Unable to stat newly created file");
});

WithPathInKeychainDirectory(CFSTR("keychain-2.db-iscorrupt"), ^(const char *filename) {
FILE* file = fopen(filename, "w");
XCTAssert(file != NULL, "Didn't get a FILE pointer");
fclose(file);
});

NSMutableDictionary* query = [@{(id)kSecClass : (id)kSecClassGenericPassword,
(id)kSecValueData : [@"password" dataUsingEncoding:NSUTF8StringEncoding],
(id)kSecAttrAccount : @"TestAccount",
(id)kSecAttrService : @"TestService",
(id)kSecUseDataProtectionKeychain : @(YES),
(id)kSecReturnAttributes : @(YES)
} mutableCopy];
CFTypeRef result = NULL;
XCTAssertEqual(SecItemAdd((__bridge CFDictionaryRef)query, &result), errSecSuccess, @"Should have added item to keychain");
XCTAssertNotNil((__bridge id)result, @"Should have received a dictionary back from SecItemAdd");
CFReleaseNull(result);

query[(id)kSecValueData] = nil;
XCTAssertEqual(SecItemCopyMatching((__bridge CFDictionaryRef)query, &result), errSecSuccess, @"Should have found item in keychain");
XCTAssertNotNil((__bridge id)result, @"Should have received a dictionary back from SecItemCopyMatching");
CFReleaseNull(result);

XCTAssertEqual(SecItemDelete((__bridge CFDictionaryRef)query), errSecSuccess, @"Should have deleted item from keychain");

WithPathInKeychainDirectory(CFSTR("keychain-2.db-iscorrupt"), ^(const char *filename) {
struct stat markerinfo = {};
XCTAssertNotEqual(stat(filename, &markerinfo), 0, "Expected not to find corruption marker after killing keychain");
});

__block struct stat after = {};
WithPathInKeychainDirectory(CFSTR("keychain-2.db"), ^(const char *filename) {
FILE* file = fopen(filename, "w");
XCTAssert(file != NULL, "Didn't get a FILE pointer");
fclose(file);
XCTAssertEqual(stat(filename, &after), 0, "Unable to stat newly created file");
});

if (before.st_birthtimespec.tv_sec == after.st_birthtimespec.tv_sec) {
XCTAssertLessThan(before.st_birthtimespec.tv_nsec, after.st_birthtimespec.tv_nsec, "db was not deleted and recreated");
} else {
XCTAssertLessThan(before.st_birthtimespec.tv_sec, after.st_birthtimespec.tv_sec, "db was not deleted and recreated");
}
}

- (void)testInetBinaryFields {
NSData* note = [@"OBVIOUS_NOTES_DATA" dataUsingEncoding:NSUTF8StringEncoding];
NSData* history = [@"OBVIOUS_HISTORY_DATA" dataUsingEncoding:NSUTF8StringEncoding];
NSData* client0 = [@"OBVIOUS_CLIENT0_DATA" dataUsingEncoding:NSUTF8StringEncoding];
NSData* client1 = [@"OBVIOUS_CLIENT1_DATA" dataUsingEncoding:NSUTF8StringEncoding];
NSData* client2 = [@"OBVIOUS_CLIENT2_DATA" dataUsingEncoding:NSUTF8StringEncoding];
NSData* client3 = [@"OBVIOUS_CLIENT3_DATA" dataUsingEncoding:NSUTF8StringEncoding];

NSData* originalPassword = [@"asdf" dataUsingEncoding:NSUTF8StringEncoding];
NSMutableDictionary* query = [@{
(id)kSecClass : (id)kSecClassInternetPassword,
(id)kSecAttrAccessible : (id)kSecAttrAccessibleWhenUnlocked,
(id)kSecUseDataProtectionKeychain : @YES,
(id)kSecAttrDescription : @"desc",
(id)kSecAttrServer : @"server",
(id)kSecAttrAccount : @"test-account",
(id)kSecValueData : originalPassword,
(id)kSecDataInetExtraNotes : note,
(id)kSecDataInetExtraHistory : history,
(id)kSecDataInetExtraClientDefined0 : client0,
(id)kSecDataInetExtraClientDefined1 : client1,
(id)kSecDataInetExtraClientDefined2 : client2,
(id)kSecDataInetExtraClientDefined3 : client3,

(id)kSecReturnAttributes : @YES,
} mutableCopy];

CFTypeRef cfresult = nil;
XCTAssertEqual(SecItemAdd((__bridge CFDictionaryRef)query, &cfresult), errSecSuccess, "Should be able to add an item using new binary fields");
NSDictionary* result = (NSDictionary*)CFBridgingRelease(cfresult);
XCTAssertNotNil(result, "Should have some sort of result");

XCTAssertNil(result[(id)kSecDataInetExtraNotes], "Notes field should not be returned as an attribute from add");
XCTAssertNil(result[(id)kSecDataInetExtraHistory], "Notes field should not be returned as an attribute from add");

NSDictionary* queryFind = @{
(id)kSecClass : (id)kSecClassInternetPassword,
(id)kSecUseDataProtectionKeychain : @YES,
(id)kSecAttrAccount : @"test-account",
};

NSMutableDictionary* queryFindOneWithJustAttributes = [[NSMutableDictionary alloc] initWithDictionary:queryFind];
queryFindOneWithJustAttributes[(id)kSecReturnAttributes] = @YES;

NSMutableDictionary* queryFindAllWithJustAttributes = [[NSMutableDictionary alloc] initWithDictionary:queryFindOneWithJustAttributes];
queryFindAllWithJustAttributes[(id)kSecMatchLimit] = (id)kSecMatchLimitAll;

NSDictionary* queryFindOneWithAttributesAndData = @{
(id)kSecClass : (id)kSecClassInternetPassword,
(id)kSecUseDataProtectionKeychain : @YES,
(id)kSecReturnAttributes : @YES,
(id)kSecReturnData: @YES,
(id)kSecAttrAccount : @"test-account",
};

NSDictionary* queryFindAllWithAttributesAndData = @{
(id)kSecClass : (id)kSecClassInternetPassword,
(id)kSecUseDataProtectionKeychain : @YES,
(id)kSecReturnAttributes : @YES,
(id)kSecReturnData: @YES,
(id)kSecMatchLimit : (id)kSecMatchLimitAll,
(id)kSecAttrAccount : @"test-account",
};

/* Copy with a single record limite, but with attributes only */
XCTAssertEqual(SecItemCopyMatching((__bridge CFDictionaryRef)queryFindOneWithJustAttributes, &cfresult), errSecSuccess, "Should be able to find an item");

result = (NSDictionary*)CFBridgingRelease(cfresult);
XCTAssertNotNil(result, "Should have some sort of result");

XCTAssertNil(result[(id)kSecDataInetExtraNotes], "Notes field should not be returned as an attribute from copymatching when finding a single item");
XCTAssertNil(result[(id)kSecDataInetExtraHistory], "Notes field should not be returned as an attribute from copymatching when finding a single item");
XCTAssertNil(result[(id)kSecDataInetExtraClientDefined0], "ClientDefined0 field should not be returned as an attribute from copymatching when finding a single item");
XCTAssertNil(result[(id)kSecDataInetExtraClientDefined1], "ClientDefined1 field should not be returned as an attribute from copymatching when finding a single item");
XCTAssertNil(result[(id)kSecDataInetExtraClientDefined2], "ClientDefined2 field should not be returned as an attribute from copymatching when finding a single item");
XCTAssertNil(result[(id)kSecDataInetExtraClientDefined3], "ClientDefined3 field should not be returned as an attribute from copymatching when finding a single item");

/* Copy with no limit, but with attributes only */
XCTAssertEqual(SecItemCopyMatching((__bridge CFDictionaryRef)queryFindAllWithJustAttributes, &cfresult), errSecSuccess, "Should be able to find an item");
NSArray* arrayResult = (NSArray*)CFBridgingRelease(cfresult);
XCTAssertNotNil(arrayResult, "Should have some sort of result");
XCTAssertTrue([arrayResult isKindOfClass:[NSArray class]], "Should have received an array back from copymatching");
XCTAssertEqual(arrayResult.count, 1, "Array should have one element");

result = arrayResult[0];
XCTAssertNil(result[(id)kSecDataInetExtraNotes], "Notes field should not be returned as an attribute from copymatching when finding all items");
XCTAssertNil(result[(id)kSecDataInetExtraHistory], "Notes field should not be returned as an attribute from copymatching when finding all items");
XCTAssertNil(result[(id)kSecDataInetExtraClientDefined0], "ClientDefined0 field should not be returned as an attribute from copymatching when finding all items");
XCTAssertNil(result[(id)kSecDataInetExtraClientDefined1], "ClientDefined1 field should not be returned as an attribute from copymatching when finding all items");
XCTAssertNil(result[(id)kSecDataInetExtraClientDefined2], "ClientDefined2 field should not be returned as an attribute from copymatching when finding all items");
XCTAssertNil(result[(id)kSecDataInetExtraClientDefined3], "ClientDefined3 field should not be returned as an attribute from copymatching when finding all items");

/* Copy with single-record limit, but with attributes and data */
XCTAssertEqual(SecItemCopyMatching((__bridge CFDictionaryRef)queryFindOneWithAttributesAndData, &cfresult), errSecSuccess, "Should be able to find an item");
result = (NSDictionary*)CFBridgingRelease(cfresult);
XCTAssertNotNil(result, "Should have some sort of result");

XCTAssertEqualObjects(note, result[(id)kSecDataInetExtraNotes], "Notes field should be returned as data");
XCTAssertEqualObjects(history, result[(id)kSecDataInetExtraHistory], "History field should be returned as data");
XCTAssertEqualObjects(client0, result[(id)kSecDataInetExtraClientDefined0], "Client Defined 0 field should be returned as data");
XCTAssertEqualObjects(client1, result[(id)kSecDataInetExtraClientDefined1], "Client Defined 1 field should be returned as data");
XCTAssertEqualObjects(client2, result[(id)kSecDataInetExtraClientDefined2], "Client Defined 2 field should be returned as data");
XCTAssertEqualObjects(client3, result[(id)kSecDataInetExtraClientDefined3], "Client Defined 3 field should be returned as data");

/* Copy with no limit, but with attributes and data */
XCTAssertEqual(SecItemCopyMatching((__bridge CFDictionaryRef)queryFindAllWithAttributesAndData, &cfresult), errSecSuccess, "Should be able to find an item");
arrayResult = (NSArray*)CFBridgingRelease(cfresult);
XCTAssertNotNil(arrayResult, "Should have some sort of result");
XCTAssertTrue([arrayResult isKindOfClass:[NSArray class]], "Should have received an array back from copymatching");
XCTAssertEqual(arrayResult.count, 1, "Array should have one element");
result = arrayResult[0];
XCTAssertEqualObjects(note, result[(id)kSecDataInetExtraNotes], "Notes field should be returned as data");
XCTAssertEqualObjects(history, result[(id)kSecDataInetExtraHistory], "History field should be returned as data");
XCTAssertEqualObjects(client0, result[(id)kSecDataInetExtraClientDefined0], "Client Defined 0 field should be returned as data");
XCTAssertEqualObjects(client1, result[(id)kSecDataInetExtraClientDefined1], "Client Defined 1 field should be returned as data");
XCTAssertEqualObjects(client2, result[(id)kSecDataInetExtraClientDefined2], "Client Defined 2 field should be returned as data");
XCTAssertEqualObjects(client3, result[(id)kSecDataInetExtraClientDefined3], "Client Defined 3 field should be returned as data");

/* Copy just looking for the password */
XCTAssertEqual(SecItemCopyMatching((__bridge CFDictionaryRef)@{
(id)kSecClass : (id)kSecClassInternetPassword,
(id)kSecUseDataProtectionKeychain : @YES,
(id)kSecReturnData: @YES,
(id)kSecAttrAccount : @"test-account",
}, &cfresult), errSecSuccess, "Should be able to find an item");

NSData* password = (NSData*)CFBridgingRelease(cfresult);
XCTAssertNotNil(password, "Should have some sort of password");
XCTAssertTrue([password isKindOfClass:[NSData class]], "Password is a data");
XCTAssertEqualObjects(originalPassword, password, "Should still be able to fetch the original password");

NSData* newHistoryContents = [@"gone" dataUsingEncoding:NSUTF8StringEncoding];

NSDictionary* updateQuery = @{
(id)kSecDataInetExtraHistory : newHistoryContents,
};

XCTAssertEqual(SecItemUpdate((__bridge CFDictionaryRef)queryFind, (__bridge CFDictionaryRef)updateQuery), errSecSuccess, "Should be able to update a history field");

// And find it again
XCTAssertEqual(SecItemCopyMatching((__bridge CFDictionaryRef)queryFindOneWithAttributesAndData, &cfresult), errSecSuccess, "Should be able to find an item");
result = (NSDictionary*)CFBridgingRelease(cfresult);
XCTAssertNotNil(result, "Should have some sort of result");

XCTAssertEqualObjects(note, result[(id)kSecDataInetExtraNotes], "Notes field should be returned as data");
XCTAssertEqualObjects(newHistoryContents, result[(id)kSecDataInetExtraHistory], "History field should be updated");
XCTAssertEqualObjects(client0, result[(id)kSecDataInetExtraClientDefined0], "Client Defined 0 field should be returned as data");
XCTAssertEqualObjects(client1, result[(id)kSecDataInetExtraClientDefined1], "Client Defined 1 field should be returned as data");
XCTAssertEqualObjects(client2, result[(id)kSecDataInetExtraClientDefined2], "Client Defined 2 field should be returned as data");
XCTAssertEqualObjects(client3, result[(id)kSecDataInetExtraClientDefined3], "Client Defined 3 field should be returned as data");
}

// When this test starts failing, hopefully rdar://problem/60332379 got fixed
- (void)testBadDateCausesDERDecodeValidationError {
// Wonky time calculation hastily stolen from SecGregorianDateGetAbsoluteTime and tweaked
// As of right now this causes CFCalendarDecomposeAbsoluteTime with Zulu calendar to give a seemingly incorrect date which then causes DER date validation issues
CFAbsoluteTime absTime = (CFAbsoluteTime)(((-(1902 * 365) + -38) * 24 + 0) * 60 + -1) * 60 + 1;
absTime -= 0.0004; // Just to make sure the nanoseconds keep getting encoded/decoded properly
CFDateRef date = CFDateCreate(NULL, absTime);

CFErrorRef error = NULL;
size_t plistSize = der_sizeof_plist(date, &error);
XCTAssert(error == NULL);
XCTAssertGreaterThan(plistSize, 0);

// Encode without repair does not validate dates because that changes behavior I do not want to fiddle with
uint8_t* der = calloc(1, plistSize);
uint8_t* der_end = der + plistSize;
uint8_t* result = der_encode_plist(date, &error, der, der_end);
XCTAssert(error == NULL);
XCTAssertEqual(der, result);

// ...but decoding does and will complain
CFPropertyListRef decoded = NULL;
XCTAssert(der_decode_plist(NULL, &decoded, &error, der, der_end) == NULL);
XCTAssert(error != NULL);
XCTAssertEqual(CFErrorGetDomain(error), kCFErrorDomainOSStatus);
NSString* description = CFBridgingRelease(CFErrorCopyDescription(error));
XCTAssert([description containsString:@"Invalid date"]);

CFReleaseNull(error);
free(der);
}

// When this test starts failing, hopefully rdar://problem/60332379 got fixed
- (void)testBadDateWithDEREncodingRepairProducesDefaultValue {
// Wonky time calculation hastily stolen from SecGregorianDateGetAbsoluteTime and tweaked
// As of right now this causes CFCalendarDecomposeAbsoluteTime with Zulu calendar to give a seemingly incorrect date which then causes DER date validation issues
CFAbsoluteTime absTime = (CFAbsoluteTime)(((-(1902 * 365) + -38) * 24 + 0) * 60 + -1) * 60 + 1;
absTime -= 0.0004; // Just to make sure the nanoseconds keep getting encoded/decoded properly
CFDateRef date = CFDateCreate(NULL, absTime);

CFErrorRef error = NULL;
size_t plistSize = der_sizeof_plist(date, &error);
XCTAssert(error == NULL);
XCTAssertGreaterThan(plistSize, 0);

uint8_t* der = calloc(1, plistSize);
uint8_t* der_end = der + plistSize;
uint8_t* encoderesult = der_encode_plist_repair(date, &error, true, der, der_end);
XCTAssert(error == NULL);
XCTAssertEqual(der, encoderesult);

CFPropertyListRef decoded = NULL;
const uint8_t* decoderesult = der_decode_plist(NULL, &decoded, &error, der, der_end);
XCTAssertEqual(der_end, decoderesult);
XCTAssertEqual(CFGetTypeID(decoded), CFDateGetTypeID());
XCTAssertEqualWithAccuracy(CFDateGetAbsoluteTime(decoded), 0, 60 * 60 * 24);
}

- (void)testContainersWithBadDateWithDEREncodingRepairProducesDefaultValue {
// Wonky time calculation hastily stolen from SecGregorianDateGetAbsoluteTime and tweaked
// As of right now this causes CFCalendarDecomposeAbsoluteTime with Zulu calendar to give a seemingly incorrect date which then causes DER date validation issues
CFAbsoluteTime absTime = (CFAbsoluteTime)(((-(1902 * 365) + -38) * 24 + 0) * 60 + -1) * 60 + 1;
absTime -= 0.0004;
CFDateRef date = CFDateCreate(NULL, absTime);

NSDictionary* dict = @{
@"dateset": [NSSet setWithObject:(__bridge id)date],
@"datearray": @[(__bridge id)date],
};

CFErrorRef error = NULL;
size_t plistSize = der_sizeof_plist((__bridge CFTypeRef)dict, &error);
XCTAssertNil((__bridge NSError*)error, "Should be no error checking the size of the plist");
XCTAssertGreaterThan(plistSize, 0);

uint8_t* der = calloc(1, plistSize);
uint8_t* der_end = der + plistSize;
uint8_t* encoderesult = der_encode_plist_repair((__bridge CFTypeRef)dict, &error, true, der, der_end);
XCTAssertNil((__bridge NSError*)error, "Should be no error encoding the plist");
XCTAssertEqual(der, encoderesult);

CFPropertyListRef decoded = NULL;
const uint8_t* decoderesult = der_decode_plist(NULL, &decoded, &error, der, der_end);
XCTAssertNil((__bridge NSError*)error, "Should be no error decoding the plist");
XCTAssertEqual(der_end, decoderesult);

XCTAssertNotNil((__bridge NSDictionary*)decoded, "Should have decoded some dictionary");
if(decoded == nil) {
return;
}

XCTAssertEqual(CFGetTypeID(decoded), CFDictionaryGetTypeID());
CFDictionaryRef decodedCFDictionary = decoded;

{
CFSetRef decodedCFSet = CFDictionaryGetValue(decodedCFDictionary, CFSTR("dateset"));
XCTAssertNotNil((__bridge NSSet*)decodedCFSet, "Should have some CFSet");

if(decodedCFSet != NULL) {
XCTAssertEqual(CFGetTypeID(decodedCFSet), CFSetGetTypeID());
XCTAssertEqual(CFSetGetCount(decodedCFSet), 1, "Should have one item in set");

__block bool dateprocessed = false;
CFSetForEach(decodedCFSet, ^(const void *value) {
XCTAssertEqual(CFGetTypeID(value), CFDateGetTypeID());
XCTAssertEqualWithAccuracy(CFDateGetAbsoluteTime(value), 0, 60 * 60 * 24);
dateprocessed = true;
});

XCTAssertTrue(dateprocessed, "Should have processed at least one date in the set");
}
}

{
CFArrayRef decodedCFArray = CFDictionaryGetValue(decodedCFDictionary, CFSTR("datearray"));
XCTAssertNotNil((__bridge NSArray*)decodedCFArray, "Should have some CFArray");

if(decodedCFArray != NULL) {
XCTAssertEqual(CFGetTypeID(decodedCFArray), CFArrayGetTypeID());
XCTAssertEqual(CFArrayGetCount(decodedCFArray), 1, "Should have one item in array");

__block bool dateprocessed = false;
CFArrayForEach(decodedCFArray, ^(const void *value) {
XCTAssertEqual(CFGetTypeID(value), CFDateGetTypeID());
XCTAssertEqualWithAccuracy(CFDateGetAbsoluteTime(value), 0, 60 * 60 * 24);
dateprocessed = true;
});

XCTAssertTrue(dateprocessed, "Should have processed at least one date in the array");
}
}

CFReleaseNull(decoded);
}

- (void)testSecItemCopyMatchingWithBadDateInItem {
// Wonky time calculation hastily stolen from SecGregorianDateGetAbsoluteTime and tweaked
// As of right now this causes CFCalendarDecomposeAbsoluteTime with Zulu calendar to give a seemingly incorrect date which then causes DER date validation issues
CFAbsoluteTime absTime = (CFAbsoluteTime)(((-(1902 * 365) + -38) * 24 + 0) * 60 + -1) * 60 + 1;
absTime -= 0.0004;
CFDateRef date = CFDateCreate(NULL, absTime);

NSDictionary* addQuery = @{
//(id)kSecClass : (id)kSecClassGenericPassword,
(id)kSecAttrCreationDate : (__bridge id)date,
(id)kSecAttrModificationDate : (__bridge id)date,

(id)kSecValueData : [@"password" dataUsingEncoding:NSUTF8StringEncoding],
(id)kSecAttrAccount : @"TestAccount",
(id)kSecAttrService : @"TestService",
(id)kSecAttrAccessGroup : @"com.apple.security.securityd",

(id)kSecAttrAccessible : @"ak",
(id)kSecAttrTombstone : [NSNumber numberWithInt: 0],
(id)kSecAttrMultiUser : [[NSData alloc] init],
};

__block CFErrorRef cferror = NULL;
kc_with_dbt(true, &cferror, ^bool(SecDbConnectionRef dbt) {
return kc_transaction_type(dbt, kSecDbExclusiveTransactionType, &cferror, ^bool {
SecDbItemRef item = SecDbItemCreateWithAttributes(NULL, kc_class_with_name(kSecClassGenericPassword), (__bridge CFDictionaryRef)addQuery, KEYBAG_DEVICE, &cferror);

bool ret = SecDbItemInsert(item, dbt, false, &cferror);

XCTAssertTrue(ret, "Should be able to add an item");
CFReleaseNull(item);

return ret;
});
});

NSDictionary* findQuery = @{
(id)kSecClass : (id)kSecClassGenericPassword,
(id)kSecUseDataProtectionKeychain : @YES,
(id)kSecAttrAccount : @"TestAccount",
(id)kSecAttrService : @"TestService",
(id)kSecAttrAccessGroup : @"com.apple.security.securityd",

(id)kSecReturnAttributes : @YES,
(id)kSecReturnData: @YES,
(id)kSecMatchLimit: (id)kSecMatchLimitAll,
};

CFTypeRef result = NULL;
XCTAssertEqual(SecItemCopyMatching((__bridge CFDictionaryRef)findQuery, &result), errSecItemNotFound, @"Should not be able to find our misdated item");

// This is a bit of a mystery: how do these items have a bad date that's not in the item?

CFReleaseNull(result);
CFReleaseNull(date);
}

- (void)testSecItemCopyMatchingWithBadDateInSQLColumn {
NSDictionary* addQuery = @{
(id)kSecClass : (id)kSecClassGenericPassword,

(id)kSecValueData : [@"password" dataUsingEncoding:NSUTF8StringEncoding],
(id)kSecAttrAccount : @"TestAccount",
(id)kSecAttrService : @"TestService",
(id)kSecAttrAccessGroup : @"com.apple.security.securityd",

(id)kSecAttrAccessible : @"ak",
(id)kSecAttrService : @"",

(id)kSecUseDataProtectionKeychain: @YES,
};

OSStatus status = SecItemAdd((__bridge CFDictionaryRef)addQuery, NULL);
XCTAssertEqual(status, errSecSuccess, "Should be able to add an item to the keychain");

// Modify the cdat/mdat columns in the keychain db
__block CFErrorRef cferror = NULL;
kc_with_dbt(true, &cferror, ^bool(SecDbConnectionRef dbt) {
return kc_transaction_type(dbt, kSecDbExclusiveTransactionType, &cferror, ^bool {
CFErrorRef updateError = NULL;

// Magic number extracted from testSecItemCopyMatchingWithBadDateInItem
SecDbExec(dbt,
(__bridge CFStringRef)@"UPDATE genp SET cdat = -59984755259.0004, mdat = -59984755259.0004;",
&updateError);

XCTAssertNil((__bridge NSError*)updateError, "Should be no error updating the table");
CFReleaseNull(updateError);

return true;
});
});

// Can we find the item?
NSDictionary* findQuery = @{
(id)kSecClass : (id)kSecClassGenericPassword,
(id)kSecUseDataProtectionKeychain : @YES,
(id)kSecAttrAccount : @"TestAccount",
(id)kSecAttrService : @"TestService",
(id)kSecAttrAccessGroup : @"com.apple.security.securityd",

(id)kSecReturnAttributes : @YES,
(id)kSecReturnData: @YES,
(id)kSecMatchLimit: (id)kSecMatchLimitAll,
};

CFTypeRef result = NULL;
XCTAssertEqual(SecItemCopyMatching((__bridge CFDictionaryRef)findQuery, &result), errSecSuccess, @"Should be able to find our misdated item");

CFReleaseNull(result);
}

- (void)testDurableWriteAPI
{
NSDictionary* addQuery = @{
(id)kSecClass : (id)kSecClassGenericPassword,
(id)kSecValueData : [@"password" dataUsingEncoding:NSUTF8StringEncoding],
(id)kSecAttrAccount : @"TestAccount",
(id)kSecAttrService : @"TestService",
(id)kSecUseDataProtectionKeychain : @(YES),
(id)kSecReturnAttributes : @(YES),
};

NSDictionary* updateQuery = @{
(id)kSecClass : (id)kSecClassGenericPassword,
(id)kSecAttrAccount : @"TestAccount",
(id)kSecAttrService : @"TestService",
(id)kSecUseDataProtectionKeychain : @(YES),
};

CFTypeRef result = NULL;

// Add the item
XCTAssertEqual(SecItemAdd((__bridge CFDictionaryRef)addQuery, &result), errSecSuccess, @"Should have succeeded in adding test item to keychain");
XCTAssertNotNil((__bridge id)result, @"Should have received a dictionary back from SecItemAdd");
CFReleaseNull(result);

// Using the API without the entitlement should fail
CFErrorRef cferror = NULL;
XCTAssertEqual(SecItemPersistKeychainWritesAtHighPerformanceCost(&cferror), errSecMissingEntitlement, @"Should not be able to persist keychain writes without the entitlement");
XCTAssertNotNil((__bridge NSError*)cferror, "Should be an error persisting keychain writes without the entitlement");
CFReleaseNull(cferror);

// But with the entitlement, you're good
SecResetLocalSecuritydXPCFakeEntitlements();
SecAddLocalSecuritydXPCFakeEntitlement(kSecEntitlementPrivatePerformanceImpactingAPI, kCFBooleanTrue);

XCTAssertEqual(SecItemPersistKeychainWritesAtHighPerformanceCost(&cferror), errSecSuccess, @"Should be able to persist keychain writes");
XCTAssertNil((__bridge NSError*)cferror, "Should be no error persisting keychain writes");

// And we can update the item
XCTAssertEqual(SecItemUpdate((__bridge CFDictionaryRef)updateQuery,
(__bridge CFDictionaryRef)@{
(id)kSecValueData: [@"otherpassword" dataUsingEncoding:NSUTF8StringEncoding],
}),
errSecSuccess, "should be able to update item with clean update query");

XCTAssertEqual(SecItemPersistKeychainWritesAtHighPerformanceCost(&cferror), errSecSuccess, @"Should be able to persist keychain writes after an update");
XCTAssertNil((__bridge NSError*)cferror, "Should be no error persisting keychain writes");

XCTAssertEqual(SecItemDelete((__bridge CFDictionaryRef)updateQuery), errSecSuccess, "Should be able to delete item");
XCTAssertEqual(SecItemPersistKeychainWritesAtHighPerformanceCost(&cferror), errSecSuccess, @"Should be able to persist keychain writes after a delete");
XCTAssertNil((__bridge NSError*)cferror, "Should be no error persisting keychain writes");
}

#pragma mark - SecItemRateLimit

// This is not super accurate in BATS, so put some margin around what you need
- (void)sleepAlternativeForXCTest:(double)interval
{
dispatch_semaphore_t localsema = dispatch_semaphore_create(0);
dispatch_after(dispatch_time(DISPATCH_TIME_NOW, NSEC_PER_SEC * interval), dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_HIGH, 0), ^{
dispatch_semaphore_signal(localsema);
});
dispatch_semaphore_wait(localsema, DISPATCH_TIME_FOREVER);
}

- (void)testSecItemRateLimitTimePasses {
SecItemRateLimit* rl = [SecItemRateLimit getStaticRateLimit];
[rl forceEnabled: true];

for (int idx = 0; idx < rl.roCapacity; ++idx) {
XCTAssertTrue(isReadOnlyAPIRateWithinLimits());
}

for (int idx = 0; idx < rl.rwCapacity; ++idx) {
XCTAssertTrue(isModifyingAPIRateWithinLimits());
}

[self sleepAlternativeForXCTest: 2];
XCTAssertTrue(isReadOnlyAPIRateWithinLimits());
XCTAssertTrue(isModifyingAPIRateWithinLimits());

[SecItemRateLimit resetStaticRateLimit];
}

- (void)testSecItemRateLimitResetAfterExceed {
SecItemRateLimit* rl = [SecItemRateLimit getStaticRateLimit];
[rl forceEnabled: true];

for (int idx = 0; idx < rl.roCapacity; ++idx) {
XCTAssertTrue(isReadOnlyAPIRateWithinLimits());
}
XCTAssertFalse(isReadOnlyAPIRateWithinLimits());
XCTAssertTrue(isReadOnlyAPIRateWithinLimits());

for (int idx = 0; idx < rl.rwCapacity; ++idx) {
XCTAssertTrue(isModifyingAPIRateWithinLimits());
}
XCTAssertFalse(isModifyingAPIRateWithinLimits());
XCTAssertTrue(isModifyingAPIRateWithinLimits());

[SecItemRateLimit resetStaticRateLimit];
}

- (void)testSecItemRateLimitMultiplier {
SecItemRateLimit* rl = [SecItemRateLimit getStaticRateLimit];
[rl forceEnabled: true];

int ro_iterations_before = 0;
for (; ro_iterations_before < rl.roCapacity; ++ro_iterations_before) {
XCTAssertTrue(isReadOnlyAPIRateWithinLimits());
}
XCTAssertFalse(isReadOnlyAPIRateWithinLimits());

int rw_iterations_before = 0;
for (; rw_iterations_before < rl.rwCapacity; ++rw_iterations_before) {
XCTAssertTrue(isModifyingAPIRateWithinLimits());
}
XCTAssertFalse(isModifyingAPIRateWithinLimits());

int ro_iterations_after = 0;
for (; ro_iterations_after < rl.roCapacity; ++ro_iterations_after) {
XCTAssertTrue(isReadOnlyAPIRateWithinLimits());
}
XCTAssertFalse(isReadOnlyAPIRateWithinLimits());

int rw_iterations_after = 0;
for (; rw_iterations_after < rl.rwCapacity; ++rw_iterations_after) {
XCTAssertTrue(isModifyingAPIRateWithinLimits());
}
XCTAssertFalse(isModifyingAPIRateWithinLimits());

XCTAssertEqualWithAccuracy(rl.limitMultiplier * ro_iterations_before, ro_iterations_after, 1);
XCTAssertEqualWithAccuracy(rl.limitMultiplier * rw_iterations_before, rw_iterations_after, 1);
[SecItemRateLimit resetStaticRateLimit];
}

// We stipulate that this test is run on an internal release.
// If this were a platform binary limits would be enforced, but it should not be so they should not.
- (void)testSecItemRateLimitInternalPlatformBinariesOnly {
SecItemRateLimit* rl = [SecItemRateLimit getStaticRateLimit];

for (int idx = 0; idx < 3 * MAX(rl.roCapacity, rl.rwCapacity); ++idx) {
XCTAssertTrue(isReadOnlyAPIRateWithinLimits());
XCTAssertTrue(isModifyingAPIRateWithinLimits());
}

[SecItemRateLimit resetStaticRateLimit];
}

@end

#endif