/*
 
 Owner: Leon von Tippelskirch <leon@boinx.com>
 Company: Boinx Software Ltd., Germany
 Projects: BXServices
 
 Abstract: Handles all additional functionality in the BXLicenseController for the Mac AppStore.
 
 */

#import "BXLicenseController.h"

#if defined(MAC_OS_X_VERSION_10_6) && (MAC_OS_X_VERSION_MAX_ALLOWED >= MAC_OS_X_VERSION_10_6)

#define APPSTORE_CHECK_FRACTION 10


#import <IOKit/IOKitLib.h> 
#import <Foundation/Foundation.h> 
#import <Security/Security.h> 
#import <Security/CSCommon.h> 
#import <Security/SecCode.h> 
#import <Security/SecStaticCode.h> 
#import <Security/SecCodeHost.h> 
#import <Security/SecRequirement.h> 
#import <CommonCrypto/CommonDigest.h> 
#include <openssl/pkcs7.h> 
#include <openssl/objects.h> 
#include <openssl/sha.h>


#pragma mark Function definition
// see the bottom of the file for the implementation
// All in the header in order to allow inline implementation
NS_INLINE CFDictionaryRef dictionaryWithAppStoreReceiptAtPath(NSString *path);
NS_INLINE CFDataRef copyMacAddress(void);
NS_INLINE BOOL isAppleSignedBinary(NSURL *filePath);
NS_INLINE BOOL checkForValidReceipt();
NS_INLINE BOOL isAppStore();
NS_INLINE void exitWithInvalidCert(void);

#endif

@interface BXLicenseControllerAppStore : BXLicenseController
{
}


#if defined(MAC_OS_X_VERSION_10_6) && (MAC_OS_X_VERSION_MAX_ALLOWED >= MAC_OS_X_VERSION_10_6)


// overridden functions
- (BOOL)hasLicense;
- (BOOL)needsToRegister;
- (BOOL)isLicensed;
- (int)licenseStatus;
- (BOOL)isAppStore;

#endif

@end

#if defined(MAC_OS_X_VERSION_10_6) && (MAC_OS_X_VERSION_MAX_ALLOWED >= MAC_OS_X_VERSION_10_6)

#pragma mark C Function implementation

// sample code from https://devforums.apple.com/thread/73391?tstart=0
// Returns autoreleased object
NS_INLINE CFDictionaryRef dictionaryWithAppStoreReceiptAtPath(NSString *path) 
{
	CFDictionaryRef returnValue = NULL;
	
	NSString *kReceiptBundleIdentifer = @"BundleIdentifier";
	NSString *kReceiptBundleIdentiferData = @"BundleIdentifierData";
	NSString *kReceiptVersion = @"Version";
	NSString *kReceiptOpaqueValue = @"OpaqueValue";
	NSString *kReceiptHash = @"Hash";
	
    enum ATTRIBUTES 
	{
        ATTR_START = 1,
        BUNDLE_ID,
        VERSION,
        OPAQUE_VALUE,
        HASH,
        ATTR_END
    };
    
    // Expected input is a PKCS7 container with signed data containing
    // an ASN.1 SET of SEQUENCE structures. Each SEQUENCE contains
    // two INTEGERS and an OCTET STRING.
    
    FILE *fp = fopen([path fileSystemRepresentation], "rb");
    if (fp != NULL)
	{
#if BX_SUPPRESS_DEPRECATION_WARNINGS_WITHOUT_SUITABLE_REPLACEMENTS
#pragma clang diagnostic push
#pragma clang diagnostic ignored "-Wdeprecated-declarations"
#endif
		PKCS7 *p7 = d2i_PKCS7_fp(fp, NULL);
#if BX_SUPPRESS_DEPRECATION_WARNINGS_WITHOUT_SUITABLE_REPLACEMENTS
#pragma clang diagnostic pop
#endif
		
		fclose(fp);
		
#if BX_SUPPRESS_DEPRECATION_WARNINGS_WITHOUT_SUITABLE_REPLACEMENTS
#pragma clang diagnostic push
#pragma clang diagnostic ignored "-Wdeprecated-declarations"
#endif
		if (PKCS7_type_is_signed(p7)) 
#if BX_SUPPRESS_DEPRECATION_WARNINGS_WITHOUT_SUITABLE_REPLACEMENTS
#pragma clang diagnostic pop
#endif
		{
#if BX_SUPPRESS_DEPRECATION_WARNINGS_WITHOUT_SUITABLE_REPLACEMENTS
#pragma clang diagnostic push
#pragma clang diagnostic ignored "-Wdeprecated-declarations"
#endif
			if (PKCS7_type_is_data(p7->d.sign->contents)) 
#if BX_SUPPRESS_DEPRECATION_WARNINGS_WITHOUT_SUITABLE_REPLACEMENTS
#pragma clang diagnostic pop
#endif
			{
				ASN1_OCTET_STRING *octets = p7->d.sign->contents->d.data;   
				const unsigned char *p = octets->data;
				const unsigned char *end = p + octets->length;
				
				int type = 0;
				int xclass = 0;
				long length = 0;
				
#if BX_SUPPRESS_DEPRECATION_WARNINGS_WITHOUT_SUITABLE_REPLACEMENTS
#pragma clang diagnostic push
#pragma clang diagnostic ignored "-Wdeprecated-declarations"
#endif
				ASN1_get_object(&p, &length, &type, &xclass, end - p);
#if BX_SUPPRESS_DEPRECATION_WARNINGS_WITHOUT_SUITABLE_REPLACEMENTS
#pragma clang diagnostic pop
#endif
				if (type == V_ASN1_SET) 
				{
					NSMutableDictionary *info = [NSMutableDictionary dictionary];
					
					while (p < end) 
					{
#if BX_SUPPRESS_DEPRECATION_WARNINGS_WITHOUT_SUITABLE_REPLACEMENTS
#pragma clang diagnostic push
#pragma clang diagnostic ignored "-Wdeprecated-declarations"
#endif
						ASN1_get_object(&p, &length, &type, &xclass, end - p);
#if BX_SUPPRESS_DEPRECATION_WARNINGS_WITHOUT_SUITABLE_REPLACEMENTS
#pragma clang diagnostic pop
#endif
						if (type != V_ASN1_SEQUENCE)
							break;
						
						const unsigned char *seq_end = p + length;
						
						int attr_type = 0;
//						int attr_version = 0;
						
						// Attribute type
#if BX_SUPPRESS_DEPRECATION_WARNINGS_WITHOUT_SUITABLE_REPLACEMENTS
#pragma clang diagnostic push
#pragma clang diagnostic ignored "-Wdeprecated-declarations"
#endif
						ASN1_get_object(&p, &length, &type, &xclass, seq_end - p);
#if BX_SUPPRESS_DEPRECATION_WARNINGS_WITHOUT_SUITABLE_REPLACEMENTS
#pragma clang diagnostic pop
#endif
						if (type == V_ASN1_INTEGER && length == 1) 
						{
							attr_type = p[0];
						}
						p += length;
						
						// Attribute version
#if BX_SUPPRESS_DEPRECATION_WARNINGS_WITHOUT_SUITABLE_REPLACEMENTS
#pragma clang diagnostic push
#pragma clang diagnostic ignored "-Wdeprecated-declarations"
#endif
						ASN1_get_object(&p, &length, &type, &xclass, seq_end - p);
#if BX_SUPPRESS_DEPRECATION_WARNINGS_WITHOUT_SUITABLE_REPLACEMENTS
#pragma clang diagnostic pop
#endif
						if (type == V_ASN1_INTEGER && length == 1) 
						{
//							attr_version = p[0];
						}
						p += length;
						
						// Only parse attributes we're interested in
						if (attr_type > ATTR_START && attr_type < ATTR_END) {
							NSString *key = nil;
							
#if BX_SUPPRESS_DEPRECATION_WARNINGS_WITHOUT_SUITABLE_REPLACEMENTS
#pragma clang diagnostic push
#pragma clang diagnostic ignored "-Wdeprecated-declarations"
#endif
							ASN1_get_object(&p, &length, &type, &xclass, seq_end - p);
#if BX_SUPPRESS_DEPRECATION_WARNINGS_WITHOUT_SUITABLE_REPLACEMENTS
#pragma clang diagnostic pop
#endif
							if (type == V_ASN1_OCTET_STRING) 
							{
								// Bytes
								if (attr_type == BUNDLE_ID || attr_type == OPAQUE_VALUE || attr_type == HASH) {
									NSData *data = [NSData dataWithBytes:p length:length];
									
									switch (attr_type) 
									{
										case BUNDLE_ID:
											// This is included for hash generation
											key = kReceiptBundleIdentiferData;
											break;
										case OPAQUE_VALUE:
											key = kReceiptOpaqueValue;
											break;
										case HASH:
											key = kReceiptHash;
											break;
									}
									
									[info setObject:data forKey:key];
								}
								
								// Strings
								if (attr_type == BUNDLE_ID || attr_type == VERSION) 
								{
									int str_type = 0;
									long str_length = 0;
									const unsigned char *str_p = p;
#if BX_SUPPRESS_DEPRECATION_WARNINGS_WITHOUT_SUITABLE_REPLACEMENTS
#pragma clang diagnostic push
#pragma clang diagnostic ignored "-Wdeprecated-declarations"
#endif
									ASN1_get_object(&str_p, &str_length, &str_type, &xclass, seq_end - str_p);
#if BX_SUPPRESS_DEPRECATION_WARNINGS_WITHOUT_SUITABLE_REPLACEMENTS
#pragma clang diagnostic pop
#endif
									if (str_type == V_ASN1_UTF8STRING)
									{
										NSString *string = [[[NSString alloc] initWithBytes:str_p
																					 length:str_length
																				   encoding:NSUTF8StringEncoding] autorelease];
										
										switch (attr_type) 
										{
											case BUNDLE_ID:
												key = kReceiptBundleIdentifer;
												break;
											case VERSION:
												key = kReceiptVersion;
												break;
										}
										
										[info setObject:string forKey:key];
									}
								}
							}
							p += length;
						}
						
						// Skip any remaining fields in this SEQUENCE
						while (p < seq_end) 
						{
#if BX_SUPPRESS_DEPRECATION_WARNINGS_WITHOUT_SUITABLE_REPLACEMENTS
#pragma clang diagnostic push
#pragma clang diagnostic ignored "-Wdeprecated-declarations"
#endif
							ASN1_get_object(&p, &length, &type, &xclass, seq_end - p);
#if BX_SUPPRESS_DEPRECATION_WARNINGS_WITHOUT_SUITABLE_REPLACEMENTS
#pragma clang diagnostic pop
#endif
							p += length;
						}
					}
					
					returnValue = (CFDictionaryRef) info;
				}
			}
		}
		PKCS7_free(p7);
    }
    return returnValue;
}


NS_INLINE CFDataRef copyMacAddress(void)
{
    kern_return_t             kernResult = 0;
    mach_port_t               master_port = 0;
    CFMutableDictionaryRef    matchingDict = NULL;
    io_iterator_t             iterator = 0;
    io_object_t               service = 0;
    CFDataRef                 macAddress = nil;
	
    kernResult = IOMasterPort(MACH_PORT_NULL, &master_port);
    if (kernResult == KERN_SUCCESS) 
	{
		matchingDict = IOBSDNameMatching(master_port, 0, "en0");
		if(matchingDict) 
		{
			// IOServiceGetMatchingServices consumes one retainCount of matchingDict, see docu!
			kernResult = IOServiceGetMatchingServices(master_port, matchingDict, &iterator);
			if (kernResult == KERN_SUCCESS) 
			{
				while((service = IOIteratorNext(iterator)) != 0)
				{
					io_object_t        parentService = 0;
					
					kernResult = IORegistryEntryGetParentEntry(service, kIOServicePlane, &parentService);
					if(kernResult == KERN_SUCCESS)
					{
						if(macAddress) 
						{
							CFRelease(macAddress);
						}
						
						macAddress = (CFDataRef)IORegistryEntryCreateCFProperty(parentService, CFSTR("IOMACAddress"), kCFAllocatorDefault, 0);
						IOObjectRelease(parentService);
					}
					
					IOObjectRelease(service);
				}
			}
			IOObjectRelease(iterator);
		}
//		BXCFRelease(matchingDict); // Don't release dict here, see above.
	}
	
    return macAddress;
}


NS_INLINE void exitWithInvalidCert(void)
{
	// There should be better exit could here
	exit(173);
	[(id <NSObject>)0x00001234 autorelease]; // crash if exit fails
}


extern BOOL sSecSemaphore; //@"Please don't steal software!";

// these are inline c functions in order to provide a better scatterment of the functions inside the code
NS_INLINE BOOL isAppleSignedBinary(NSURL *filePath)
{
	BOOL result = NO;
	
	while (! __sync_bool_compare_and_swap (&sSecSemaphore, NO, YES))
	{
		usleep(10);
	}	
	
	SecStaticCodeRef staticCode = NULL;
	SecRequirementRef appleAncorReq  = NULL;
	
	OSStatus status = SecStaticCodeCreateWithPath( (CFURLRef)filePath,  
												  kSecCSDefaultFlags,
												  &staticCode );
	if (status == noErr)
	{
		status = SecRequirementCreateWithString( CFSTR("anchor apple generic") ,  
												kSecCSDefaultFlags,
												&appleAncorReq );
		
		if (status == noErr)
		{
			status = SecStaticCodeCheckValidity(staticCode,
												kSecCSDefaultFlags,  
												appleAncorReq);
			
			if (status == noErr)
			{
				result = YES;
			}	
		}
		
		BXCFRelease(appleAncorReq);
		BXCFRelease(staticCode);
	}
	sSecSemaphore = NO;
	
	return result;
}


NS_INLINE BOOL checkForValidReceipt()
{
	BOOL returnValue = NO;
	NSString *receiptPath = [[[NSBundle bundleForClass:[[NSApp delegate] class]] bundlePath] stringByAppendingString:@"/Contents/_MASReceipt/receipt"];
//	NSString *receiptPath = @"/tmp/receipt";
	
	// If there is no receipt, verification fails.
	if ([[NSFileManager defaultManager] fileExistsAtPath:receiptPath])
	{
		// If the receipt is not properly signed by Apple, verification fails.
//		if (isAppleSignedBinary([NSURL fileURLWithPath:receiptPath]))
		{
			NSDictionary *receiptDictionary = (NSDictionary *)dictionaryWithAppStoreReceiptAtPath(receiptPath);
			
			//If the bundle identifier in the receipt does not match the value for CFBundleIdentifier in the Info.plist file, verification fails.
			//If the version identifier string in the receipt does not match the value for CFBundleShortVersionString in the Info.plist file, verification fails.
//#pragma warning read out the right Identifier
			NSDictionary *infoDictionary = [[NSBundle bundleForClass:[[NSApp delegate] class]] infoDictionary];
			NSString *infoPlistBundleIdentifier = (NSString *)[infoDictionary objectForKey:@"CFBundleIdentifier"];
			NSString *infoPlistVersion = (NSString *)[infoDictionary objectForKey:@"CFBundleShortVersionString"];
			
			if (receiptDictionary != nil &&
				[[receiptDictionary objectForKey:@"BundleIdentifier"] isEqualTo:infoPlistBundleIdentifier] &&
				[[receiptDictionary objectForKey:@"Version"] isEqualTo:infoPlistVersion])
			{				
				// get Mac Address
				CFDataRef macAddress = copyMacAddress();
				if (macAddress != NULL)
				{
					//Concatenate the bytes of the computer’s GUID, the opaque value (the attribute of type 4), 
					//and the bundle identifier from the receipt. Compute the SHA-1 hash. If the result does not match the hash in the receipt, verification fails.
//					CFRelease(macAddress);
//					const UInt8 dummyMacAddress[6] = { 0x00, 0x17, 0xf2, 0xc4, 0xbc, 0xc0 };
//					macAddress = CFDataCreate(NULL, dummyMacAddress, 6);

					// SHA1 Hash
					
					NSMutableData *input = [NSMutableData data];
					[input appendData:(NSData *)macAddress];
					[input appendData:[receiptDictionary objectForKey:@"OpaqueValue"]];
					[input appendData:[receiptDictionary objectForKey:@"BundleIdentifierData"]];
					
					NSMutableData *hash = [NSMutableData dataWithLength:CC_SHA1_DIGEST_LENGTH];
					CC_SHA1((unsigned char *)[input bytes], [input length], (unsigned char *)[hash mutableBytes]);
					
					CFRelease(macAddress);


					if ([hash isEqualToData:[receiptDictionary objectForKey:@"Hash"]]) 
					{
						returnValue = YES;
					}
					else
					{
						exitWithInvalidCert();
					}
				}
				else
				{
					exitWithInvalidCert();
				}
			}
			else
			{
				exitWithInvalidCert();
			}
		}
//		else
//		{
//			exitWithInvalidCert();
//		}
	}
	else
	{
//		BXLog(@"Receipt not signed");
		exitWithInvalidCert();
	}
	
	return returnValue;
}


// 1 == AppStore, 2 == Not Appstore, 0 == undefined
static int sHasAppStoreChecked = 0;

NS_INLINE BOOL isAppStore()
{
	if ([NSApp runningOnSnowLeopardOrHigher])
	{
		// If it isn't already checked
		// only do appstore verification on a random occasion
		if (sHasAppStoreChecked == 0 ||
			(random() % APPSTORE_CHECK_FRACTION) == (random() % APPSTORE_CHECK_FRACTION))
		{
			if (sHasAppStoreChecked != 2)
			{
				// first check if the app store file is still there
				NSString *appstoreCheckFilename = [[[NSBundle bundleForClass:[[NSApp delegate] class]] bundlePath] stringByAppendingString:@"/Contents/Resources/AppStore"];	// this file is created during build process by a jenkins script.
				
				if ([[NSFileManager defaultManager] fileExistsAtPath:[appstoreCheckFilename stringByExpandingTildeInPath]])
				{
					sHasAppStoreChecked = 1;
					
					if (isAppleSignedBinary([[NSBundle bundleForClass:[[NSApp delegate] class]] executableURL]))
					{
						checkForValidReceipt();
					}
					else
					{
						// AppStore binary is modified
						BXLog(@"AppStore binary is modified. Exiting...");
						exit(-1);
					}
				}
				else
				{
					sHasAppStoreChecked = 2;
				}
			}
		}
	}
	
	return (sHasAppStoreChecked == 1);
}

#else

NS_INLINE BOOL isAppStore()
{
	return NO;
}

#endif

