Skip to content

Instantly share code, notes, and snippets.

Embed
What would you like to do?
Mac App Store Receipt Validation Sample (Mac OS X 10.7)
//
// RVNReceiptValidation.m
//
// Created by Satoshi Numata on 12/06/30.
// Copyright (c) 2012 Sazameki and Satoshi Numata, Ph.D. All rights reserved.
//
// This sample shows how to write the Mac App Store receipt validation code.
// Replace kRVNBundleID and kRVNBundleVersion with your own ones.
//
// This sample is provided because the coding sample found in "Validating Mac App Store Receipts"
// is somehow out-of-date today and some functions are deprecated in Mac OS X 10.7.
// (cf. Validating Mac App Store Receipts: )
//
// You must want to make it much more robustness with some techniques, such as obfuscation
// with your "own" way. If you use and share the same codes with your friends, attackers
// will be able to make a special tool to patch application binaries so easily.
// Again, this sample gives you the very basic idea that which APIs can be used for the validation.
//
// Don't forget to add IOKit.framework and Security.framework to your project.
// The main() function should be replaced with the (commented out) main() code at the bottom of this sample.
// This sample assume that you are using Automatic Reference Counting for memory management.
//
// Have a nice Cocoa flavor, guys!!
//
#import "RVNReceiptValidation.h"
// RVNReceiptValidation.h contains only one function declaration:
// int RVNValidateAndRunApplication(int argc, char *argv[]);
#import <Foundation/Foundation.h>
#import <CommonCrypto/CommonDigest.h>
#import <Security/CMSDecoder.h>
#import <Security/SecAsn1Coder.h>
#import <Security/SecAsn1Templates.h>
#import <Security/SecRequirement.h>
#import <IOKit/IOKitLib.h>
static NSString *kRVNBundleID = @"jp.sazameki.MyGreatApp";
static NSString *kRVNBundleVersion = @"1.0";
typedef struct {
size_t length;
unsigned char *data;
} ASN1_Data;
typedef struct {
ASN1_Data type; // INTEGER
ASN1_Data version; // INTEGER
ASN1_Data value; // OCTET STRING
} RVNReceiptAttribute;
typedef struct {
RVNReceiptAttribute **attrs;
} RVNReceiptPayload;
// ASN.1 receipt attribute template
static const SecAsn1Template kReceiptAttributeTemplate[] = {
{ SEC_ASN1_SEQUENCE, 0, NULL, sizeof(RVNReceiptAttribute) },
{ SEC_ASN1_INTEGER, offsetof(RVNReceiptAttribute, type), NULL, 0 },
{ SEC_ASN1_INTEGER, offsetof(RVNReceiptAttribute, version), NULL, 0 },
{ SEC_ASN1_OCTET_STRING, offsetof(RVNReceiptAttribute, value), NULL, 0 },
{ 0, 0, NULL, 0 }
};
// ASN.1 receipt template set
static const SecAsn1Template kSetOfReceiptAttributeTemplate[] = {
{ SEC_ASN1_SET_OF, 0, kReceiptAttributeTemplate, sizeof(RVNReceiptPayload) },
{ 0, 0, NULL, 0 }
};
enum {
kRVNReceiptAttributeTypeBundleID = 2,
kRVNReceiptAttributeTypeApplicationVersion = 3,
kRVNReceiptAttributeTypeOpaqueValue = 4,
kRVNReceiptAttributeTypeSHA1Hash = 5,
kRVNReceiptAttributeTypeInAppPurchaseReceipt = 17,
kRVNReceiptAttributeTypeInAppQuantity = 1701,
kRVNReceiptAttributeTypeInAppProductID = 1702,
kRVNReceiptAttributeTypeInAppTransactionID = 1703,
kRVNReceiptAttributeTypeInAppPurchaseDate = 1704,
kRVNReceiptAttributeTypeInAppOriginalTransactionID = 1705,
kRVNReceiptAttributeTypeInAppOriginalPurchaseDate = 1706,
};
static NSString *kRVNReceiptInfoKeyBundleID = @"Bundle ID";
static NSString *kRVNReceiptInfoKeyBundleIDData = @"Bundle ID Data";
static NSString *kRVNReceiptInfoKeyApplicationVersion = @"Application Version";
static NSString *kRVNReceiptInfoKeyApplicationVersionData = @"Application Version Data";
static NSString *kRVNReceiptInfoKeyOpaqueValue = @"Opaque Value";
static NSString *kRVNReceiptInfoKeySHA1Hash = @"SHA-1 Hash";
static NSString *kRVNReceiptInfoKeyInAppPurchaseReceipt = @"In App Purchase Receipt";
static NSString *kRVNReceiptInfoKeyInAppProductID = @"In App Product ID";
static NSString *kRVNReceiptInfoKeyInAppTransactionID = @"In App Transaction ID";
static NSString *kRVNReceiptInfoKeyInAppOriginalTransactionID = @"In App Original Transaction ID";
static NSString *kRVNReceiptInfoKeyInAppPurchaseDate = @"In App Purchase Date";
static NSString *kRVNReceiptInfoKeyInAppOriginalPurchaseDate = @"In App Original Purchase Date";
static NSString *kRVNReceiptInfoKeyInAppQuantity = @"In App Quantity";
inline static void RVNCheckBundleIDAndVersion(void)
{
NSDictionary *bundleInfo = [[NSBundle mainBundle] infoDictionary];
NSString *bundleID = [bundleInfo valueForKey:@"CFBundleIdentifier"];
if (![bundleID isEqualToString:kRVNBundleID]) {
[NSException raise:@"MacAppStore Receipt Validation Error" format:@"Failed to check bundle ID.", nil];
}
NSString *bundleVersion = [bundleInfo valueForKey:@"CFBundleShortVersionString"];
if (![bundleVersion isEqualToString:kRVNBundleVersion]) {
[NSException raise:@"MacAppStore Receipt Validation Error" format:@"Failed to check bundle Version.", nil];
}
}
inline static void RVNCheckBundleSignature(void)
{
NSURL *bundleURL = [[NSBundle mainBundle] bundleURL];
SecStaticCodeRef staticCode = NULL;
OSStatus status = SecStaticCodeCreateWithPath((__bridge CFURLRef)bundleURL, kSecCSDefaultFlags, &staticCode);
if (status != errSecSuccess) {
[NSException raise:@"MacAppStore Receipt Validation Error" format:@"Failed to validate bundle signature: Create a static code", nil];
}
NSString *requirementText = @"anchor apple generic"; // For code signed by Apple
SecRequirementRef requirement = NULL;
status = SecRequirementCreateWithString((__bridge CFStringRef)requirementText, kSecCSDefaultFlags, &requirement);
if (status != errSecSuccess) {
if (staticCode) CFRelease(staticCode);
[NSException raise:@"MacAppStore Receipt Validation Error" format:@"Failed to validate bundle signature: Create a requirement", nil];
}
status = SecStaticCodeCheckValidity(staticCode, kSecCSDefaultFlags, requirement);
if (status != errSecSuccess) {
if (staticCode) CFRelease(staticCode);
if (requirement) CFRelease(requirement);
[NSException raise:@"MacAppStore Receipt Validation Error" format:@"Failed to validate bundle signature: Check the static code validity", nil];
}
if (staticCode) CFRelease(staticCode);
if (requirement) CFRelease(requirement);
}
inline static NSData *RVNGetReceiptData(void)
{
NSURL *receiptURL = [[NSBundle mainBundle] appStoreReceiptURL];
NSData *receiptData = [NSData dataWithContentsOfURL:receiptURL];
if (!receiptData) {
[NSException raise:@"MacAppStore Receipt Validation Error" format:@"Failed to fetch the MacAppStore receipt.", nil];
}
return receiptData;
}
inline static NSData *RVNDecodeReceiptData(NSData *receiptData)
{
CMSDecoderRef decoder = NULL;
SecPolicyRef policyRef = NULL;
SecTrustRef trustRef = NULL;
@try {
// Create a decoder
OSStatus status = CMSDecoderCreate(&decoder);
if (status) {
[NSException raise:@"MacAppStore Receipt Validation Error" format:@"Failed to decode receipt data: Create a decoder", nil];
}
// Decrypt the message (1)
status = CMSDecoderUpdateMessage(decoder, receiptData.bytes, receiptData.length);
if (status) {
[NSException raise:@"MacAppStore Receipt Validation Error" format:@"Failed to decode receipt data: Update message", nil];
}
// Decrypt the message (2)
status = CMSDecoderFinalizeMessage(decoder);
if (status) {
[NSException raise:@"MacAppStore Receipt Validation Error" format:@"Failed to decode receipt data: Finalize message", nil];
}
// Get the decrypted content
NSData *ret = nil;
CFDataRef dataRef = NULL;
status = CMSDecoderCopyContent(decoder, &dataRef);
if (status) {
[NSException raise:@"MacAppStore Receipt Validation Error" format:@"Failed to decode receipt data: Get decrypted content", nil];
}
ret = [NSData dataWithData:(__bridge NSData *)dataRef];
CFRelease(dataRef);
// Check the signature
size_t numSigners;
status = CMSDecoderGetNumSigners(decoder, &numSigners);
if (status) {
[NSException raise:@"MacAppStore Receipt Validation Error" format:@"Failed to check receipt signature: Get singer count", nil];
}
if (numSigners == 0) {
[NSException raise:@"MacAppStore Receipt Validation Error" format:@"Failed to check receipt signature: No signer found", nil];
}
policyRef = SecPolicyCreateBasicX509();
CMSSignerStatus signerStatus;
OSStatus certVerifyResult;
status = CMSDecoderCopySignerStatus(decoder, 0, policyRef, TRUE, &signerStatus, &trustRef, &certVerifyResult);
if (status) {
[NSException raise:@"MacAppStore Receipt Validation Error" format:@"Failed to check receipt signature: Get signer status", nil];
}
if (signerStatus != kCMSSignerValid) {
[NSException raise:@"MacAppStore Receipt Validation Error" format:@"Failed to check receipt signature: No valid signer", nil];
}
return ret;
} @catch (NSException *e) {
@throw e;
} @finally {
if (policyRef) CFRelease(policyRef);
if (trustRef) CFRelease(trustRef);
if (decoder) CFRelease(decoder);
}
}
inline static NSData *RVNGetASN1RawData(ASN1_Data asn1Data)
{
return [NSData dataWithBytes:asn1Data.data length:asn1Data.length];
}
inline static int RVNGetIntValueFromASN1Data(const ASN1_Data *asn1Data)
{
int ret = 0;
for (int i = 0; i < asn1Data->length; i++) {
ret = (ret << 8) | asn1Data->data[i];
}
return ret;
}
inline static NSNumber *RVNDecodeIntNumberFromASN1Data(SecAsn1CoderRef decoder, ASN1_Data srcData)
{
ASN1_Data asn1Data;
OSStatus status = SecAsn1Decode(decoder, srcData.data, srcData.length, kSecAsn1IntegerTemplate, &asn1Data);
if (status) {
[NSException raise:@"MacAppStore Receipt Validation Error" format:@"Failed to get receipt information: Decode integer value", nil];
}
return [NSNumber numberWithInt:RVNGetIntValueFromASN1Data(&asn1Data)];
}
inline static NSString *RVNDecodeUTF8StringFromASN1Data(SecAsn1CoderRef decoder, ASN1_Data srcData)
{
ASN1_Data asn1Data;
OSStatus status = SecAsn1Decode(decoder, srcData.data, srcData.length, kSecAsn1UTF8StringTemplate, &asn1Data);
if (status) {
[NSException raise:@"MacAppStore Receipt Validation Error" format:@"Failed to get receipt information: Decode UTF-8 string", nil];
}
return [[NSString alloc] initWithBytes:asn1Data.data length:asn1Data.length encoding:NSUTF8StringEncoding];
}
inline static NSDate *RVNDecodeDateFromASN1Data(SecAsn1CoderRef decoder, ASN1_Data srcData)
{
NSDateFormatter *dateFormatter = [[NSDateFormatter alloc] init];
[dateFormatter setDateFormat:@"yyyy-MM-ddTHH:mm:ssZ"];
ASN1_Data asn1Data;
OSStatus status = SecAsn1Decode(decoder, srcData.data, srcData.length, kSecAsn1IA5StringTemplate, &asn1Data);
if (status) {
[NSException raise:@"MacAppStore Receipt Validation Error" format:@"Failed to get receipt information: Decode date (IA5 string)", nil];
}
NSString *dateStr = [[NSString alloc] initWithBytes:asn1Data.data length:asn1Data.length encoding:NSASCIIStringEncoding];
return [dateFormatter dateFromString:dateStr];
}
inline static NSDictionary *RVNGetReceiptPayload(NSData *payloadData)
{
SecAsn1CoderRef asn1Decoder = NULL;
@try {
NSMutableDictionary *ret = [NSMutableDictionary dictionary];
// Create the ASN.1 parser
OSStatus status = SecAsn1CoderCreate(&asn1Decoder);
if (status) {
[NSException raise:@"MacAppStore Receipt Validation Error" format:@"Failed to get receipt information: Create ASN.1 decoder", nil];
}
// Decode the receipt payload
RVNReceiptPayload payload = { NULL };
status = SecAsn1Decode(asn1Decoder, payloadData.bytes, payloadData.length, kSetOfReceiptAttributeTemplate, &payload);
if (status) {
[NSException raise:@"MacAppStore Receipt Validation Error" format:@"Failed to get receipt information: Decode payload", nil];
}
// Fetch all attributes
RVNReceiptAttribute *anAttr;
for (int i = 0; (anAttr = payload.attrs[i]); i++) {
int type = RVNGetIntValueFromASN1Data(&anAttr->type);
switch (type) {
// UTF-8 String
case kRVNReceiptAttributeTypeBundleID:
[ret setValue:RVNDecodeUTF8StringFromASN1Data(asn1Decoder, anAttr->value) forKey:kRVNReceiptInfoKeyBundleID];
[ret setValue:RVNGetASN1RawData(anAttr->value) forKey:kRVNReceiptInfoKeyBundleIDData];
break;
case kRVNReceiptAttributeTypeApplicationVersion:
[ret setValue:RVNDecodeUTF8StringFromASN1Data(asn1Decoder, anAttr->value) forKey:kRVNReceiptInfoKeyApplicationVersion];
[ret setValue:RVNGetASN1RawData(anAttr->value) forKey:kRVNReceiptInfoKeyApplicationVersionData];
break;
case kRVNReceiptAttributeTypeInAppProductID:
[ret setValue:RVNDecodeUTF8StringFromASN1Data(asn1Decoder, anAttr->value) forKey:kRVNReceiptInfoKeyInAppProductID];
break;
case kRVNReceiptAttributeTypeInAppTransactionID:
[ret setValue:RVNDecodeUTF8StringFromASN1Data(asn1Decoder, anAttr->value) forKey:kRVNReceiptInfoKeyInAppTransactionID];
break;
case kRVNReceiptAttributeTypeInAppOriginalTransactionID:
[ret setValue:RVNDecodeUTF8StringFromASN1Data(asn1Decoder, anAttr->value) forKey:kRVNReceiptInfoKeyInAppOriginalTransactionID];
break;
// Purchase Date (As IA5 String (almost identical to the ASCII String))
case kRVNReceiptAttributeTypeInAppPurchaseDate:
[ret setValue:RVNDecodeDateFromASN1Data(asn1Decoder, anAttr->value) forKey:kRVNReceiptInfoKeyInAppPurchaseDate];
break;
case kRVNReceiptAttributeTypeInAppOriginalPurchaseDate:
[ret setValue:RVNDecodeDateFromASN1Data(asn1Decoder, anAttr->value) forKey:kRVNReceiptInfoKeyInAppOriginalPurchaseDate];
break;
// Quantity (Integer Value)
case kRVNReceiptAttributeTypeInAppQuantity:
[ret setValue:RVNDecodeIntNumberFromASN1Data(asn1Decoder, anAttr->value)
forKey:kRVNReceiptInfoKeyInAppQuantity];
break;
// Opaque Value (Octet Data)
case kRVNReceiptAttributeTypeOpaqueValue:
[ret setValue:RVNGetASN1RawData(anAttr->value) forKey:kRVNReceiptInfoKeyOpaqueValue];
break;
// SHA-1 Hash (Octet Data)
case kRVNReceiptAttributeTypeSHA1Hash:
[ret setValue:RVNGetASN1RawData(anAttr->value) forKey:kRVNReceiptInfoKeySHA1Hash];
break;
// In App Purchases Receipt
case kRVNReceiptAttributeTypeInAppPurchaseReceipt: {
NSMutableArray *inAppPurchases = [ret valueForKey:kRVNReceiptInfoKeyInAppPurchaseReceipt];
if (!inAppPurchases) {
inAppPurchases = [NSMutableArray array];
[ret setValue:inAppPurchases forKey:kRVNReceiptInfoKeyInAppPurchaseReceipt];
}
NSData *inAppData = [NSData dataWithBytes:anAttr->value.data length:anAttr->value.length];
NSDictionary *inAppInfo = RVNGetReceiptPayload(inAppData);
[inAppPurchases addObject:inAppInfo];
break;
}
// Otherwise
default:
break;
}
}
return ret;
} @catch (NSException *e) {
@throw e;
} @finally {
if (asn1Decoder) SecAsn1CoderRelease(asn1Decoder);
}
}
inline static NSData *RVLGetMacAddress(void)
{
mach_port_t masterPort;
kern_return_t result = IOMasterPort(MACH_PORT_NULL, &masterPort);
if (result != KERN_SUCCESS) {
return nil;
}
CFMutableDictionaryRef matchingDict = IOBSDNameMatching(masterPort, 0, "en0");
if (!matchingDict) {
return nil;
}
io_iterator_t iterator;
result = IOServiceGetMatchingServices(masterPort, matchingDict, &iterator);
if (result != KERN_SUCCESS) {
return nil;
}
CFDataRef macAddressDataRef = nil;
io_object_t aService;
while ((aService = IOIteratorNext(iterator)) != 0) {
io_object_t parentService;
result = IORegistryEntryGetParentEntry(aService, kIOServicePlane, &parentService);
if (result == KERN_SUCCESS) {
if (macAddressDataRef) CFRelease(macAddressDataRef);
macAddressDataRef = (CFDataRef)IORegistryEntryCreateCFProperty(parentService, (CFStringRef)@"IOMACAddress", kCFAllocatorDefault, 0);
IOObjectRelease(parentService);
}
IOObjectRelease(aService);
}
IOObjectRelease(iterator);
NSData *ret = nil;
if (macAddressDataRef) {
ret = [NSData dataWithData:(__bridge NSData *)macAddressDataRef];
CFRelease(macAddressDataRef);
}
return ret;
}
inline static void RVNCheckReceiptIDAndVersion(NSDictionary *receiptInfo)
{
NSString *bundleID = [receiptInfo valueForKey:kRVNReceiptInfoKeyBundleID];
if (![bundleID isEqualToString:kRVNBundleID]) {
[NSException raise:@"MacAppStore Receipt Validation Error" format:@"Failed to check receipt ID.", nil];
}
NSString *bundleVersion = [receiptInfo objectForKey:kRVNReceiptInfoKeyApplicationVersion];
if (![bundleVersion isEqualToString:kRVNBundleVersion]) {
[NSException raise:@"MacAppStore Receipt Validation Error" format:@"Failed to check receipt version.", nil];
}
}
inline static void RVNCheckReceiptHash(NSDictionary *receiptInfo)
{
NSData *macAddressData = RVLGetMacAddress();
if (!macAddressData) {
[NSException raise:@"MacAppStore Receipt Validation Error" format:@"Failed to get the primary MAC Address for checking receipt hash.", nil];
}
NSData *data1 = [receiptInfo valueForKey:kRVNReceiptInfoKeyOpaqueValue];
NSData *data2 = [receiptInfo valueForKey:kRVNReceiptInfoKeyBundleIDData];
NSMutableData *digestData = [NSMutableData dataWithData:macAddressData];
[digestData appendData:data1];
[digestData appendData:data2];
unsigned char digestBuffer[CC_SHA1_DIGEST_LENGTH];
CC_SHA1(digestData.bytes, (CC_LONG)digestData.length, digestBuffer);
NSData *hashData = [receiptInfo valueForKey:kRVNReceiptInfoKeySHA1Hash];
if (memcmp(digestBuffer, hashData.bytes, CC_SHA1_DIGEST_LENGTH) != 0) {
[NSException raise:@"MacAppStore Receipt Validation Error" format:@"Failed to check receipt hash.", nil];
}
}
int RVNValidateAndRunApplication(int argc, char *argv[])
{
@try {
///// Check the bundle information
RVNCheckBundleIDAndVersion();
RVNCheckBundleSignature();
///// Check the receipt information
NSData *receiptData = RVNGetReceiptData();
NSData *receiptDataDecoded = RVNDecodeReceiptData(receiptData);
NSDictionary *receiptInfo = RVNGetReceiptPayload(receiptDataDecoded);
#if DEBUG
NSLog(@"receiptInfo=%@", receiptInfo);
#endif
RVNCheckReceiptIDAndVersion(receiptInfo);
RVNCheckReceiptHash(receiptInfo);
return NSApplicationMain(argc, (const char **)argv);
} @catch (NSException *e) {
NSLog(@"%@", e.reason);
exit(173);
}
}
//int main(int argc, char *argv[])
//{
// @autoreleasepool {
// return RVNValidateAndRunApplication(argc, argv);
// }
//}
@KatkayApps

This comment has been minimized.

Copy link

@KatkayApps KatkayApps commented May 29, 2014

Fantastic. Thanks. Do you plan to create something similar to iOS?

@agilemicro

This comment has been minimized.

Copy link

@agilemicro agilemicro commented Aug 20, 2014

I am trying to use this code with a Cocoa app targeting OSX 10.8. My app terminates before the RVNValidateAndRunApplication function is called. I am unable to see any NSLog entires, nor can I catch the termination in Xcode debugger. Do I need to do something different in 10.8 ? Thanks!1

@patr1ck

This comment has been minimized.

Copy link

@patr1ck patr1ck commented Nov 24, 2014

Hi! Thanks for sharing this code.

Question: Regardless of how you were to use this code in your "own" way, isn't it still a bad idea to rely on cryptographic checks from a dynamic library like CommonCrypto? While it's certainly simpler than having to build and link a static OpenSSL in your project, don't you run the risk of having CommonCrypto patched by a hacked system?

It's easy to see how a malicious user (or jailbreaker, if you prefer) could modify CMSDecoderCopySignerStatus() to always set signerStatus to kCMSSignerValid and render the above code (or any variation of it, across multiple apps) ineffectual.

I'm not a cryptography expert, so maybe I'm missing something here. I'd love to know what you think. Thanks!

@raymo960

This comment has been minimized.

Copy link

@raymo960 raymo960 commented Jul 28, 2015

This is great code and a great help to this very confusing topic. However, I am not sure I understand the point of RVNCheckReceiptIDAndVersion where it checks the hard coded value for the bundle ID and version against what is in the plist file. This seems erroneous and would require the hard coded version to be updated every time the version is updated (in the plist). Instead, why not just get the bundle id and version out of the plist and compare with the receipt. I have made this change in my version of these sources but curious to know if I am missing something.
Again, thanks for the great code.

@IngmarStein

This comment has been minimized.

Copy link

@IngmarStein IngmarStein commented Sep 20, 2015

Has anyone figured out a way to do this on iOS without OpenSSL? The Cryptographic Message Syntax API still seems to be non-public on that platform…

@aonez

This comment has been minimized.

Copy link

@aonez aonez commented Nov 21, 2018

@raymo960 where's your edit?

@BrunoVandekerkhove

This comment has been minimized.

Copy link

@BrunoVandekerkhove BrunoVandekerkhove commented Feb 4, 2020

More generic (RFC 3339) date string conversion code provided by Apple here :

- (NSString *)userVisibleDateTimeStringForRFC3339DateTimeString:(NSString *)rfc3339DateTimeString {
/*
Returns a user-visible date time string that corresponds to the specified
RFC 3339 date time string. Note that this does not handle all possible
RFC 3339 date time strings, just one of the most common styles.
*/
 
NSDateFormatter *rfc3339DateFormatter = [[NSDateFormatter alloc] init];
NSLocale *enUSPOSIXLocale = [[NSLocale alloc] initWithLocaleIdentifier:@"en_US_POSIX"];
 
[rfc3339DateFormatter setLocale:enUSPOSIXLocale];
[rfc3339DateFormatter setDateFormat:@"yyyy'-'MM'-'dd'T'HH':'mm':'ss'Z'"];
[rfc3339DateFormatter setTimeZone:[NSTimeZone timeZoneForSecondsFromGMT:0]];
 
// Convert the RFC 3339 date time string to an NSDate.
NSDate *date = [rfc3339DateFormatter dateFromString:rfc3339DateTimeString];
 
NSString *userVisibleDateTimeString;
if (date != nil) {
// Convert the date object to a user-visible date string.
NSDateFormatter *userVisibleDateFormatter = [[NSDateFormatter alloc] init];
assert(userVisibleDateFormatter != nil);
 
[userVisibleDateFormatter setDateStyle:NSDateFormatterShortStyle];
[userVisibleDateFormatter setTimeStyle:NSDateFormatterShortStyle];
 
userVisibleDateTimeString = [userVisibleDateFormatter stringFromDate:date];
}
return userVisibleDateTimeString;
}
@helje5

This comment has been minimized.

Copy link

@helje5 helje5 commented Nov 7, 2020

Might be an OK Swift port, might contain a lot of non-sense:

import Foundation
import CommonCrypto

struct AppStoreReceipt {
  
  var bundleID     : String?
  var bundleIDData : Data?
  var appVersion   : String?
  
  var sha1Hash     : Data?
  var opaqueValue  : Data?

  /// To detect whether the app was purchased as paid or subscription app
  /// in case the app switches models.
  var originalAppVersion : String?

  struct IAP { // really a transaction, can be multiple for subscriptions!
    var productID              : String?
    var quantity               : Int?
    var transactionID          : String?
    var purchaseDate           : Date?
    
    /// First in-app purchase for a subscription
    var originalTransactionID  : String?
    var originalPurchaseDate   : Date?
    
    /// If this is in the future, it is active. You need to filter all the
    /// transactions for a given originalTransactionID, and then check whether
    /// there is an active one.
    /// Note: If there is none, request a refresh receipt to make sure!
    ///       (SKReceiptRefreshRequest on iOS, macOS: exit(173))
    var subscriptionExpiryDate : Date?
  }
  var inAppPurchases = [ IAP ]()
  
  func transactions(for id: String) -> [ IAP ] {
    return inAppPurchases.filter { id == $0.originalTransactionID }
  }

  init() {}
  
  init(contentsOf data: Data) throws {
    let coder = try SecAsn1CoderRef.make()
    self = try coder.decode(data)
  }
}

extension AppStoreReceipt {
  
  enum ReceiptError : Swift.Error {
    case noPrimaryMacAddress
    case missingData
    case invalidHashSize
    case hashMismatch
    case couldNotGetMacAddress(kern_return_t)
  }
  
  func checkReceiptHash() throws {
    
    func getPrimaryMacAddress() -> Data? {
      var masterPort = mach_port_t()
      var status = IOMasterPort(mach_port_t(MACH_PORT_NULL), &masterPort)
      guard status == KERN_SUCCESS else { return nil }
      
      guard let matchingDict = IOBSDNameMatching(masterPort, 0, "en0") else {
        return nil
      }
      
      var iterator = io_iterator_t()
      status = IOServiceGetMatchingServices(masterPort, matchingDict, &iterator)
      guard status == KERN_SUCCESS else { return nil }
      defer { IOObjectRelease(iterator) }

      var macAddressData : Data?
      
      while true {
        let service = IOIteratorNext(iterator)
        guard service != 0 else { break }
        defer { IOObjectRelease(service) }
        
        var parentService = io_object_t()
        let result = IORegistryEntryGetParentEntry(service, kIOServicePlane,
                                                   &parentService)
        guard result == KERN_SUCCESS else { continue }
        
        // TBD: release old ref
        let dataRef = IORegistryEntryCreateCFProperty(
            parentService, "IOMACAddress" as CFString, kCFAllocatorDefault, 0)
        
        guard let cfRef = dataRef?.takeRetainedValue() else { continue }
        guard let data = cfRef as? Data                else { continue }
        macAddressData = data
      }
      
      return macAddressData
    }
    
    guard let macAddressData = getPrimaryMacAddress() else {
      // no primary MAC address
      throw ReceiptError.noPrimaryMacAddress
    }
    guard let opaqueValue = opaqueValue, let bundleID = bundleIDData,
          let sha1Hash = sha1Hash else {
      throw ReceiptError.missingData
    }
    assert(sha1Hash.count == CC_SHA1_DIGEST_LENGTH)
    guard sha1Hash.count == CC_SHA1_DIGEST_LENGTH else {
      throw ReceiptError.invalidHashSize
    }

    let digestBuffer = UnsafeMutableBufferPointer<UInt8>
                         .allocate(capacity: Int(CC_SHA1_DIGEST_LENGTH))
    defer { digestBuffer.deallocate() }
    
    var ctx = CC_SHA1_CTX()
    CC_SHA1_Init(&ctx)
    _ = macAddressData.withUnsafeBytes {
      CC_SHA1_Update(&ctx, $0, CC_LONG(macAddressData.count))
    }
    _ = opaqueValue.withUnsafeBytes {
      CC_SHA1_Update(&ctx, $0, CC_LONG(opaqueValue.count))
    }
    _ = bundleID.withUnsafeBytes {
      CC_SHA1_Update(&ctx, $0, CC_LONG(bundleID.count))
    }
    CC_SHA1_Final(digestBuffer.baseAddress, &ctx)
    
    guard sha1Hash.elementsEqual(digestBuffer) else {
      #if false
      print("Stored Hash:  ", sha1Hash)
      print("Computed Hash:", digestBuffer)
      print("BundleID:     ", bundleID, self.bundleID ?? "-")
      #if true
      for i in 0..<sha1Hash.count {
        print("  [\(i)]:", sha1Hash[i], digestBuffer[i])
      }
      #endif
      #endif
      throw ReceiptError.hashMismatch
    }
  }
  
}

fileprivate enum ASN1 {
  
  // use hardcoded values for bundleID/version (WWDC 2017:305 ~12m)
  // easier to switch out a Info.plist than a binary.
  
  enum AttributeType : Int {
    
    // top level attributes
    case bundleID                   = 2
    case applicationVersion         = 3
    
    /// Hash of the bundleID, deviceID and opaqueValue (4)
    case sha1Hash                   = 5
    case opaqueValue                = 4

    case iapReceipt                 = 17
    
    /// To detect whether the app was purchased as paid or subscription app
    /// in case the app switches models.
    case originalApplicationVersion = 19
    
    // those are within the iapReceipt
    case inAppQuantity               = 1701
    case inAppProductID              = 1702
    case inAppTransactionID          = 1703
    case inAppPurchaseDate           = 1704
    case inAppOriginalTransactionID  = 1705
    case inAppOriginalPurchaseDate   = 1706
    
    case inAppSubscriptionExpiryDate = 1708
  }
  
  struct Data {
    let count : size_t
    let data  : UnsafePointer<UInt8>?
    
    var intValue : Int {
      guard let data = data else { return 0 }
      var ret = 0
      for i in 0..<count {
        ret = (ret << 8) | Int(data[i])
      }
      return ret
    }
    
    var asUnsafeBufferPointer : UnsafeBufferPointer<UInt8> {
      return UnsafeBufferPointer(start: data, count: count)
    }
    
    var dataValue : Foundation.Data? {
      guard let data = data else { return nil }
      return Foundation.Data(bytes: data, count: count)
    }
  }

  struct ReceiptAttribute {
    let type    : Data // Int
    let version : Data // Int
    let value   : Data // Data
  }
  struct ReceiptPayload {
    var attrs : UnsafePointer<UnsafePointer<ASN1.ReceiptAttribute>?>? = nil
  }
}

enum IAPValidationError : Swift.Error {
  case invalidBundleID
  case invalidBundleSignature
  case missingReceiptURL
  case loadError(Swift.Error)
  
  case couldNotCreateStaticCode
  case couldNotCreateRequirement
  
  // TBD: wrap decoder errors?
  case noSigners
  case invalidSigner
  case decoderError(Swift.Error)
}

extension Bundle {
  // Note: In Swift we don't need inline statics? because static.
  
  @inline(__always)
  func checkBundleIDAndVersion() throws {
    // here he checks a static string against the
    // CFBundleIdentifier/CFBundleShortVersionString in the mainBundle.info
    // TBD
  }
  
  @inline(__always)
  func checkBundleSignature() throws {
    let requirementText = "anchor apple generic" // For code signed by Apple
    
    let staticCode  = try SecStaticCode .make(with: bundleURL)
    let requirement = try SecRequirement.make(with: requirementText)
    try staticCode.checkValidity(of: requirement)
  }
  
  @inline(__always) func loadReceiptData(from url: URL? = nil) throws -> Data {
    guard let receiptURL = url ?? self.appStoreReceiptURL else {
      throw IAPValidationError.missingReceiptURL
    }
    
    func decodeReceiptData(_ receiptData: Data) throws -> Data {
      do {
        let decoder = try CMSDecoder.make()
        let data    = try decoder.decode(receiptData)
        
        // check the signature
        let numSigners = try decoder.getNumberOfSigners()
        guard numSigners > 0 else {
          throw IAPValidationError.noSigners
        }
        
        let signerStatus = try decoder.getSignerStatus()
        guard signerStatus == .valid else {
          throw IAPValidationError.invalidSigner
        }
        
        return data
      }
      catch let error as IAPValidationError { throw error }
      catch { throw IAPValidationError.decoderError(error) }
    }
    
    do {
      let data = try Data(contentsOf: receiptURL)
      return try decodeReceiptData(data)
    }
    catch { throw IAPValidationError.loadError(error) }
  }
  
}


import Security.SecAsn1Coder
import Security.SecAsn1Templates

// SEC_ASN1_MAY_STREAM  0x40000 (encoding only I think)

fileprivate let kSecAsn1IA5StringTemplate =
  UnsafePointer<SecAsn1Template>.make(from: [
    // TBD: SEC_ASN1_MAY_STREAM?
    SecAsn1Template(kind: .init(SEC_ASN1_IA5_STRING), offset: 0, sub: .null,
                    size: .init(MemoryLayout<ASN1.Data>.stride))
    ]
)
fileprivate let kSecAsn1UTF8StringTemplate =
  UnsafePointer<SecAsn1Template>.make(from: [
    SecAsn1Template(kind: .init(SEC_ASN1_UTF8_STRING), offset: 0, sub: .null,
                    size: .init(MemoryLayout<ASN1.Data>.stride))
  ]
)
fileprivate let kSecAsn1IntegerTemplate =
  UnsafePointer<SecAsn1Template>.make(from: [
    SecAsn1Template(kind: .init(SEC_ASN1_INTEGER), offset: 0, sub: .null,
                    size: .init(MemoryLayout<ASN1.Data>.stride))
    ]
)
fileprivate let kSetOfReceiptAttributeTemplate =
  UnsafePointer<SecAsn1Template>.make(from: [
    SecAsn1Template(setOf: ASN1.ReceiptPayload.self,
                    item: [
                      SecAsn1Template(sequenceOf: ASN1.ReceiptAttribute.self),
                      SecAsn1Template(kind: SEC_ASN1_INTEGER,
                                      field: \ASN1.ReceiptAttribute.type),
                      SecAsn1Template(kind: SEC_ASN1_INTEGER,
                                      field: \ASN1.ReceiptAttribute.version),
                      SecAsn1Template(kind: SEC_ASN1_OCTET_STRING,
                                      field: \ASN1.ReceiptAttribute.value),
                      SecAsn1Template.EOL
                    ]),
    SecAsn1Template.EOL
 ]
)


// MARK: - ASN1 Helpers

fileprivate extension SecAsn1Template {
  static let EOL = SecAsn1Template(kind: 0, offset: 0, sub: .null, size: 0)
  
  init<T>(kind: Int32, field: PartialKeyPath<T>) {
    self.init(kind:   .init(kind),
              offset: .init(MemoryLayout<T>.offset(of: field)!),
              sub:    .null, size: 0)
  }
  init<T>(sequenceOf type: T.Type) {
    self.init(kind: .init(SEC_ASN1_SEQUENCE),
              offset: 0, sub: .null,
              size: .init(MemoryLayout<T>.stride))
  }
  init<T>(setOf type: T.Type, item: [ SecAsn1Template ]) {
    self.init(kind: .init(SEC_ASN1_SET_OF),
              offset: 0, sub: UnsafeRawPointer.make(from: item),
              size: .init(MemoryLayout<UnsafePointer<T>>.stride))
  }
}

fileprivate extension SecAsn1CoderRef {
  
  enum ASN1CoderError : Swift.Error {
    case couldNotCreate(OSStatus)
    case couldNotDecode(OSStatus)
    case fieldIsNull
    case fieldValueInvalid
  }

  @inline(__always) static func make() throws -> SecAsn1CoderRef {
    var value : SecAsn1CoderRef?
    let status = SecAsn1CoderCreate(&value)
    guard status == errSecSuccess, let result = value else {
      throw ASN1CoderError.couldNotCreate(status)
    }
    return result
  }
  
  func decodePayload(_ data: Data) throws
       -> UnsafePointer<UnsafePointer<ASN1.ReceiptAttribute>?>
  {
    var payload = ASN1.ReceiptPayload()
    
    let status : OSStatus = data.withUnsafeBytes {
      ( ptr ) -> OSStatus in
      return SecAsn1Decode(self, ptr, data.count,
                           kSetOfReceiptAttributeTemplate,
                           &payload)
    }
    
    guard status == errSecSuccess, let cursor = payload.attrs else {
      throw ASN1CoderError.couldNotDecode(status)
    }
    return cursor
  }

  @inline(__always) func decode(_ data: Data) throws -> AppStoreReceipt.IAP {
    var cursor = try decodePayload(data)
    var iap    = AppStoreReceipt.IAP()
    
    // walk the attributes
    while let attr = cursor.pointee?.pointee {
      cursor = cursor.advanced(by: 1)
      
      guard let type = ASN1.AttributeType(rawValue: attr.type.intValue) else {
        //print("unexpected attribute type:", attr.type.intValue)
        continue
      }
      
      switch type {
        case .bundleID, .applicationVersion, .opaqueValue, .sha1Hash,
             .iapReceipt, .originalApplicationVersion:
          assertionFailure("unexpected type in IAP: \(type)")

        case .inAppProductID:
          iap.productID = try decodeString(from: attr.value)
        case .inAppTransactionID:
          iap.transactionID = try decodeString(from: attr.value)
        case .inAppOriginalTransactionID:
          iap.originalTransactionID = try decodeString(from:attr.value)
        
        // Purchase Date (As IA5 String (almost identical to the ASCII String))
        case .inAppPurchaseDate:
          iap.purchaseDate = try decodeDate(from: attr.value)
        case .inAppOriginalPurchaseDate:
          iap.originalPurchaseDate = try decodeDate(from: attr.value)

        case .inAppQuantity:
          iap.quantity = try decodeInt(from: attr.value)
        
        case .inAppSubscriptionExpiryDate:
          iap.subscriptionExpiryDate = try decodeDate(from: attr.value)
      }
    }
    return iap
  }
  
  @inline(__always) func decode(_ data: Data) throws -> AppStoreReceipt {
    var cursor  = try decodePayload(data)
    var receipt = AppStoreReceipt()
    
    // walk the attributes
    while let attr = cursor.pointee?.pointee {
      cursor = cursor.advanced(by: 1)
      
      guard let type = ASN1.AttributeType(rawValue: attr.type.intValue) else {
        continue
      }
      
      switch type {
        case .bundleID:
          receipt.bundleIDData = attr.value.dataValue
          receipt.bundleID     = try decodeString(from: attr.value)
        case .opaqueValue: receipt.opaqueValue = attr.value.dataValue
        case .sha1Hash:    receipt.sha1Hash    = attr.value.dataValue
        case .applicationVersion:
          receipt.appVersion = try decodeString(from: attr.value)
        case .originalApplicationVersion:
          receipt.originalAppVersion = try decodeString(from: attr.value)

        case .iapReceipt:
          guard let data = attr.value.dataValue else {
            throw ASN1CoderError.fieldIsNull
          }
          receipt.inAppPurchases.append(try decode(data))

        case .inAppProductID, .inAppTransactionID, .inAppOriginalTransactionID,
             .inAppPurchaseDate, .inAppOriginalPurchaseDate, .inAppQuantity,
             .inAppSubscriptionExpiryDate:
          assertionFailure("unexpected type in receipt: \(type)")
        
      }
    }

    return receipt
  }
  
  func decode(_ srcData: ASN1.Data,
              using template: UnsafePointer<SecAsn1Template>) throws
       -> ASN1.Data
  {
    guard let srcDataPtr = srcData.data, srcData.count > 0 else {
      throw ASN1CoderError.fieldIsNull
    }
    
    var asn1Data = ASN1.Data(count: 0, data: nil)
    let status = SecAsn1Decode(self, srcDataPtr, srcData.count,
                               template, &asn1Data)
    guard status == errSecSuccess else {
      throw ASN1CoderError.couldNotDecode(status)
    }
    
    return asn1Data
  }

  @inline(__always)
  func decodeInt(from srcData: ASN1.Data) throws -> Int {
    let asn1Data = try decode(srcData, using: kSecAsn1IntegerTemplate)
    return asn1Data.intValue
  }

  @inline(__always)
  func decodeString(from srcData: ASN1.Data) throws -> String {
    let asn1Data = try decode(srcData, using: kSecAsn1UTF8StringTemplate)
    return String(decoding: asn1Data.asUnsafeBufferPointer, as: UTF8.self)
  }

  @inline(__always)
  func decodeDate(from srcData: ASN1.Data) throws -> Date? {
    let asn1Data = try decode(srcData, using: kSecAsn1IA5StringTemplate)
    guard asn1Data.count > 0 else {
      // Happens for inAppSubscriptionExpiryDate. I think this may be right,
      // it is just not set. (though a little weird)
      return nil
    }
    
    // In here because we only use it a few times
    let ia5DateFormatter : DateFormatter = {
      let locale = Locale(identifier: "en_US_POSIX")
      let df     = DateFormatter()
      df.locale  = locale
      df.dateFormat = "yyyy'-'MM'-'dd'T'HH':'mm':'ss'Z'"
      df.timeZone   = TimeZone(secondsFromGMT: 0)
      return df
    }()
    /* 2020-10-08:
     * https://twitter.com/depth42/status/1314179656870965254
     * 2) They used to look like "2020-10-03T07:12:34Z". Now they added
     *    millisceonds like in "2020-10-03T07:12:34.567Z". Apple's specification
     *    only states that dates follow RFC 3339, which does not specify if
     *    there should be milliseconds or not.
     */    
    let ia5DateFormatterMS : DateFormatter = {
      let locale = Locale(identifier: "en_US_POSIX")
      let df     = DateFormatter()
      df.locale  = locale
      df.dateFormat = "yyyy'-'MM'-'dd'T'HH':'mm':'ss'.'SSS'Z'"
      df.timeZone   = TimeZone(secondsFromGMT: 0)
      return df
    }()
    let s = String(decoding: asn1Data.asUnsafeBufferPointer, as: UTF8.self)
    if let date = ia5DateFormatterMS.date(from: s) { return date }
    if let date = ia5DateFormatter  .date(from: s) { return date }
    
    // 2019-01-04T23:51:15Z
    assertionFailure("invalid date string: \(s)")
    throw ASN1CoderError.fieldIsNull // FIXME
  }
}


// MARK: - Sec helpers

fileprivate let kSecCSDefaultFlags : SecCSFlags = []

fileprivate extension SecStaticCode {

  @inline(__always)
  func checkValidity(of requirement: SecRequirement) throws {
    let status = SecStaticCodeCheckValidity(self, kSecCSDefaultFlags,
                                            requirement)
    guard status == errSecSuccess else {
      throw IAPValidationError.invalidBundleSignature
    }
  }

  @inline(__always)
  static func make(with url: URL) throws -> SecStaticCode {
    var staticCode : SecStaticCode?
    
    let status = SecStaticCodeCreateWithPath(url as CFURL,
                                             kSecCSDefaultFlags, &staticCode)
    guard status == errSecSuccess, let result = staticCode else {
      throw IAPValidationError.couldNotCreateStaticCode
    }
    return result
  }
}

fileprivate extension SecRequirement {

  @inline(__always)
  static func make(with text: String) throws -> SecRequirement {
    var requirement : SecRequirement?
    let status = SecRequirementCreateWithString(text as CFString,
                                                kSecCSDefaultFlags,
                                                &requirement)
    guard status == errSecSuccess, let result = requirement else {
      throw IAPValidationError.couldNotCreateRequirement
    }
    return result
  }
}

fileprivate extension CMSDecoder {
  
  enum CMSDecoderError : Swift.Error {
    case couldNotCreate(OSStatus)
    case couldNotDecode(OSStatus)
    case couldNotGetSignerCount(OSStatus)
    case couldNotGetSignerStatus(OSStatus, verificationStatus: OSStatus)
  }

  @inline(__always) static func make() throws -> CMSDecoder {
    var decoder : CMSDecoder?
    let status  = CMSDecoderCreate(&decoder)
    guard status == errSecSuccess, let result = decoder else {
      throw CMSDecoderError.couldNotCreate(status)
    }
    return result
  }
  
  // MARK: - Signature
  
  @inline(__always) func getNumberOfSigners() throws -> Int {
    var count = 0
    let status = CMSDecoderGetNumSigners(self, &count)
    guard status == errSecSuccess else {
      throw CMSDecoderError.couldNotGetSignerCount(status)
    }
    return count
  }
  
  @inline(__always)
  func getSignerStatus(for index: Int = 0, evaluateTrust: Bool = true) throws
       -> CMSSignerStatus
  {
    let policy           = SecPolicyCreateBasicX509()
    var certVerifyResult : OSStatus = 0
    var signerStatus     = CMSSignerStatus.invalidCert
    var trust            : SecTrust?
    
    let status = CMSDecoderCopySignerStatus(self, index, policy, evaluateTrust,
                                            &signerStatus, &trust,
                                            &certVerifyResult)
    guard status == errSecSuccess else {
      throw CMSDecoderError
        .couldNotGetSignerStatus(status, verificationStatus: certVerifyResult)
    }
    
    return signerStatus
  }
  
  // MARK: - Decoding

  @inline(__always) func decode(_ data: Data) throws -> Data {
    try updateMessage(with: data)
    try finalize()
    return try getContent()
  }
  
  @inline(__always) private func updateMessage(with data: Data) throws {
    let status = data.withUnsafeBytes { ptr in
      CMSDecoderUpdateMessage(self, ptr, data.count)
    }
    guard status == errSecSuccess else {
      throw CMSDecoderError.couldNotDecode(status)
    }
  }
  
  @inline(__always) private func finalize() throws {
    let status = CMSDecoderFinalizeMessage(self)
    guard status == errSecSuccess else {
      throw CMSDecoderError.couldNotDecode(status)
    }
  }
  
  @inline(__always) private func getContent() throws -> Data {
    var data   : CFData?
    let status = CMSDecoderCopyContent(self, &data)
    guard status == errSecSuccess, let result = data else {
      throw CMSDecoderError.couldNotDecode(status)
    }
    return result as Data
  }
}

// MARK: - Pointer Helpers

fileprivate extension UnsafeMutableBufferPointer {
  static func make<T>(from array: [ T ]) -> UnsafeMutableBufferPointer<T> {
    let bp = UnsafeMutableBufferPointer<T>.allocate(capacity: array.count)
    for i in 0..<array.count {
      bp[i] = array[i]
    }
    return bp
  }
}
fileprivate extension UnsafeBufferPointer {
  static func make<T>(from array: [ T ]) -> UnsafeBufferPointer<T> {
    let bp = UnsafeMutableBufferPointer<T>.make(from: array)
    return UnsafeBufferPointer<T>(bp)
  }
}
fileprivate extension UnsafePointer {
  static func make<T>(from array: [ T ]) -> UnsafePointer<T> {
    let bp = UnsafeMutableBufferPointer<T>.make(from: array)
    return UnsafePointer<T>(bp.baseAddress!)
  }
}
fileprivate extension UnsafeRawPointer {
  static func make<T>(from array: [ T ]) -> UnsafeRawPointer {
    let bp = UnsafeMutableBufferPointer<T>.make(from: array)
    return UnsafeRawPointer(bp.baseAddress!)
  }
}

fileprivate extension UnsafeRawPointer {
  // just to please the API ...
  static let `null` : UnsafeRawPointer = {
    struct Wrap { var value : UInt64 = 0; }
    var value = Wrap()
    return unsafeBitCast(value, to: UnsafeRawPointer.self)
  }()
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment