Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Support OS X #191

Open
wants to merge 23 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 3 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -274,11 +274,12 @@ RMStore requires iOS 5.0 or above and ARC.

RMStore is in initial development and its public API should not be considered stable. Future enhancements will include:

* [Better OS X support](https://github.com/robotmedia/RMStore/issues/4)
* Tests for OS X
* Example app for OS X

##License

Copyright 2013-2014 [Robot Media SL](http://www.robotmedia.net)
Copyright 2013-2016 [Robot Media SL](http://www.robotmedia.net)

Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
Expand Down
16 changes: 7 additions & 9 deletions RMStore.podspec
Original file line number Diff line number Diff line change
@@ -1,16 +1,18 @@
Pod::Spec.new do |s|
s.name = 'RMStore'
s.version = '0.7.1'
s.version = '0.8.0'
s.license = 'Apache 2.0'
s.summary = 'A lightweight iOS library for In-App Purchases that adds blocks and notifications to StoreKit, plus verification, persistence and downloads.'
s.homepage = 'https://github.com/robotmedia/RMStore'
s.author = 'Hermes Pique'
s.social_media_url = 'https://twitter.com/hpique'
s.source = { :git => 'https://github.com/robotmedia/RMStore.git', :tag => "v#{s.version}" }
s.platform = :ios, '7.0'
s.frameworks = 'StoreKit'
s.requires_arc = true
s.default_subspec = 'Core'

s.ios.deployment_target = '7.0'
s.osx.deployment_target = '10.7'

s.subspec 'Core' do |core|
core.source_files = 'RMStore/*.{h,m}'
Expand All @@ -29,14 +31,10 @@ Pod::Spec.new do |s|

s.subspec 'AppReceiptVerifier' do |arv|
arv.dependency 'RMStore/Core'
arv.platform = :ios, '7.0'
arv.source_files = 'RMStore/Optional/RMStoreAppReceiptVerifier.{h,m}', 'RMStore/Optional/RMAppReceipt.{h,m}'
arv.dependency 'OpenSSL', '~> 1.0'
end

s.subspec 'TransactionReceiptVerifier' do |trv|
trv.dependency 'RMStore/Core'
trv.source_files = 'RMStore/Optional/RMStoreTransactionReceiptVerifier.{h,m}'
arv.dependency 'OpenSSL-Universal', '~> 1.0'
arv.osx.frameworks = 'Security', 'IOKit'
arv.resources = 'RMStore/Optional/AppleIncRootCertificate.cer'
end

end
226 changes: 214 additions & 12 deletions RMStore.xcodeproj/project.pbxproj

Large diffs are not rendered by default.

1 change: 0 additions & 1 deletion RMStore/Optional/RMAppReceipt.h
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,6 @@

/** Represents the app receipt.
*/
__attribute__((availability(ios,introduced=7.0)))
@interface RMAppReceipt : NSObject

/** The app’s bundle identifier.
Expand Down
197 changes: 172 additions & 25 deletions RMStore/Optional/RMAppReceipt.m
Original file line number Diff line number Diff line change
Expand Up @@ -19,12 +19,24 @@
//

#import "RMAppReceipt.h"
#import <UIKit/UIKit.h>
#import <openssl/pkcs7.h>
#import <openssl/objects.h>
#import <openssl/sha.h>
#import <openssl/x509.h>

#if TARGET_OS_IPHONE
#import <UIKit/UIKit.h>
#elif TARGET_OS_MAC
#import <IOKit/IOKitLib.h>
#import <Security/SecKeychainItem.h>
#endif

#if DEBUG
#define RMAppReceiptLog(...) NSLog(@"RMAppReceipt: %@", [NSString stringWithFormat:__VA_ARGS__]);
#else
#define RMAppReceiptLog(...)
#endif

// From https://developer.apple.com/library/ios/releasenotes/General/ValidateAppStoreReceipt/Chapters/ReceiptFields.html#//apple_ref/doc/uid/TP40010573-CH106-SW1
NSInteger const RMAppReceiptASN1TypeBundleIdentifier = 2;
NSInteger const RMAppReceiptASN1TypeAppVersion = 3;
Expand Down Expand Up @@ -101,6 +113,114 @@ static int RMASN1ReadInteger(const uint8_t **pp, long omax)
return RMASN1ReadString(pp, omax, V_ASN1_IA5STRING, NSASCIIStringEncoding);
}

#if TARGET_OS_MAC && !TARGET_OS_IPHONE

// Returns a CFData object, containing the computer's GUID.
static CFDataRef CopyMACAddressData()
{
kern_return_t kernResult;
mach_port_t master_port;
CFMutableDictionaryRef matchingDict;
io_iterator_t iterator;
io_object_t service;
CFDataRef macAddress = nil;

kernResult = IOMasterPort(MACH_PORT_NULL, &master_port);
if (kernResult != KERN_SUCCESS) {
RMAppReceiptLog(@"IOMasterPort returned %d", kernResult);
return nil;
}

matchingDict = IOBSDNameMatching(master_port, 0, "en0");
if (!matchingDict) {
RMAppReceiptLog(@"IOBSDNameMatching returned empty dictionary");
return nil;
}

kernResult = IOServiceGetMatchingServices(master_port, matchingDict, &iterator);
if (kernResult != KERN_SUCCESS) {
RMAppReceiptLog(@"IOServiceGetMatchingServices returned %d", kernResult);
return nil;
}

while((service = IOIteratorNext(iterator)) != 0) {
io_object_t parentService;

kernResult = IORegistryEntryGetParentEntry(service, kIOServicePlane,
&parentService);
if (kernResult == KERN_SUCCESS) {
if (macAddress) CFRelease(macAddress);

macAddress = (CFDataRef) IORegistryEntryCreateCFProperty(parentService,
CFSTR("IOMACAddress"), kCFAllocatorDefault, 0);
IOObjectRelease(parentService);
} else {
RMAppReceiptLog(@"IORegistryEntryGetParentEntry returned %d", kernResult);
}

IOObjectRelease(service);
}
IOObjectRelease(iterator);

return macAddress;
}

static inline SecCertificateRef AppleRootCAFromKeychain( void )
{
SecKeychainRef roots = NULL;
SecKeychainSearchRef search = NULL;
SecCertificateRef cert = NULL;
BOOL cfReleaseKeychain = YES;

// there's a GC bug with this guy it seems
OSStatus err = SecKeychainOpen( "/System/Library/Keychains/SystemRootCertificates.keychain", &roots );

if ( err != noErr )
{
CFStringRef errStr = SecCopyErrorMessageString( err, NULL );
RMAppReceiptLog( @"Error: %d (%@)", err, errStr );
CFRelease( errStr );
return NULL;
}

SecKeychainAttribute labelAttr = { .tag = kSecLabelItemAttr, .length = 13, .data = (void *)"Apple Root CA" };
SecKeychainAttributeList attrs = { .count = 1, .attr = &labelAttr };

err = SecKeychainSearchCreateFromAttributes( roots, kSecCertificateItemClass, &attrs, &search );
if ( err != noErr )
{
CFStringRef errStr = SecCopyErrorMessageString( err, NULL );
RMAppReceiptLog( @"Error: %d (%@)", err, errStr );
CFRelease( errStr );
if ( cfReleaseKeychain )
CFRelease( roots );
return NULL;
}

SecKeychainItemRef item = NULL;
err = SecKeychainSearchCopyNext( search, &item );
if ( err != noErr )
{
CFStringRef errStr = SecCopyErrorMessageString( err, NULL );
RMAppReceiptLog( @"Error: %d (%@)", err, errStr );
CFRelease( errStr );
if ( cfReleaseKeychain )
CFRelease( roots );

return NULL;
}

cert = (SecCertificateRef)item;
CFRelease( search );

if ( cfReleaseKeychain )
CFRelease( roots );

return ( cert );
}

#endif

static NSURL *_appleRootCertificateURL = nil;

@implementation RMAppReceipt
Expand All @@ -117,17 +237,17 @@ - (instancetype)initWithASN1Data:(NSData*)asn1Data
switch (type)
{
case RMAppReceiptASN1TypeBundleIdentifier:
_bundleIdentifierData = data;
_bundleIdentifier = RMASN1ReadUTF8String(&s, length);
self->_bundleIdentifierData = data;
self->_bundleIdentifier = RMASN1ReadUTF8String(&s, length);
break;
case RMAppReceiptASN1TypeAppVersion:
_appVersion = RMASN1ReadUTF8String(&s, length);
self->_appVersion = RMASN1ReadUTF8String(&s, length);
break;
case RMAppReceiptASN1TypeOpaqueValue:
_opaqueValue = data;
self->_opaqueValue = data;
break;
case RMAppReceiptASN1TypeHash:
_receiptHash = data;
self->_receiptHash = data;
break;
case RMAppReceiptASN1TypeInAppPurchaseReceipt:
{
Expand All @@ -136,12 +256,12 @@ - (instancetype)initWithASN1Data:(NSData*)asn1Data
break;
}
case RMAppReceiptASN1TypeOriginalAppVersion:
_originalAppVersion = RMASN1ReadUTF8String(&s, length);
self->_originalAppVersion = RMASN1ReadUTF8String(&s, length);
break;
case RMAppReceiptASN1TypeExpirationDate:
{
NSString *string = RMASN1ReadIA5SString(&s, length);
_expirationDate = [RMAppReceipt formatRFC3339String:string];
self->_expirationDate = [RMAppReceipt formatRFC3339String:string];
break;
}
}
Expand Down Expand Up @@ -180,13 +300,19 @@ -(BOOL)containsActiveAutoRenewableSubscriptionOfProductIdentifier:(NSString *)pr
- (BOOL)verifyReceiptHash
{
// TODO: Getting the uuid in Mac is different. See: https://developer.apple.com/library/ios/releasenotes/General/ValidateAppStoreReceipt/Chapters/ValidateLocally.html#//apple_ref/doc/uid/TP40010573-CH1-SW5
NSUUID *uuid = [UIDevice currentDevice].identifierForVendor;
unsigned char uuidBytes[16];
[uuid getUUIDBytes:uuidBytes];

// Order taken from: https://developer.apple.com/library/ios/releasenotes/General/ValidateAppStoreReceipt/Chapters/ValidateLocally.html#//apple_ref/doc/uid/TP40010573-CH1-SW5

NSMutableData *data = [NSMutableData data];
#if TARGET_OS_IPHONE
NSUUID *uuid = [UIDevice currentDevice].identifierForVendor;
unsigned char uuidBytes[16];
[uuid getUUIDBytes:uuidBytes];
[data appendBytes:uuidBytes length:sizeof(uuidBytes)];
#elif TARGET_OS_MAC
[data appendData:(__bridge NSData * _Nonnull)(CopyMACAddressData())];
#endif

[data appendData:self.opaqueValue];
[data appendData:self.bundleIdentifierData];

Expand Down Expand Up @@ -228,10 +354,30 @@ + (NSData*)dataFromPCKS7Path:(NSString*)path

if (!p7) return nil;

NSData *data;
NSData *certificateData = nil;
#if TARGET_OS_IPHONE
NSURL *certificateURL = _appleRootCertificateURL ? : [[NSBundle mainBundle] URLForResource:@"AppleIncRootCertificate" withExtension:@"cer"];
NSData *certificateData = [NSData dataWithContentsOfURL:certificateURL];
if (!certificateData || [self verifyPCKS7:p7 withCertificateData:certificateData])
certificateData = [NSData dataWithContentsOfURL:certificateURL];
#elif TARGET_OS_MAC
if (_appleRootCertificateURL)
{
certificateData = [NSData dataWithContentsOfURL:_appleRootCertificateURL];
}
else
{
// get the Apple root CA from http://www.apple.com/certificateauthority and load it into b_X509
//NSData * root = [NSData dataWithContentsOfURL: [NSURL URLWithString: @"http://www.apple.com/certificateauthority/AppleComputerRootCertificate.cer"]];
SecCertificateRef cert = AppleRootCAFromKeychain();
NSAssert(cert != NULL, @"Failed to load Apple Root CA from keychain");
certificateData = CFBridgingRelease(SecCertificateCopyData(cert));
CFRelease(cert);
}
#endif

NSAssert(certificateData != nil, @"Certificate AppleRootCA is missed, add it to the bundle (ios) or provide access to keychain (osx) or specify url `setAppleRootCertificateURL`");

NSData *data = nil;
if (certificateData && [self verifyPKCS7:p7 withCertificateData:certificateData])
{
struct pkcs7_st *contents = p7->d.sign->contents;
if (PKCS7_type_is_data(contents))
Expand All @@ -244,8 +390,9 @@ + (NSData*)dataFromPCKS7Path:(NSString*)path
return data;
}

+ (BOOL)verifyPCKS7:(PKCS7*)container withCertificateData:(NSData*)certificateData
{ // Based on: https://developer.apple.com/library/ios/releasenotes/General/ValidateAppStoreReceipt/Chapters/ValidateLocally.html#//apple_ref/doc/uid/TP40010573-CH1-SW17
+ (BOOL)verifyPKCS7:(PKCS7*)container withCertificateData:(NSData*)certificateData
{
// Based on: https://developer.apple.com/library/ios/releasenotes/General/ValidateAppStoreReceipt/Chapters/ValidateLocally.html#//apple_ref/doc/uid/TP40010573-CH1-SW17
static int verified = 1;
int result = 0;
OpenSSL_add_all_digests(); // Required for PKCS7_verify to work
Expand Down Expand Up @@ -336,42 +483,42 @@ - (instancetype)initWithASN1Data:(NSData*)asn1Data
switch (type)
{
case RMAppReceiptASN1TypeQuantity:
_quantity = RMASN1ReadInteger(&p, length);
self->_quantity = RMASN1ReadInteger(&p, length);
break;
case RMAppReceiptASN1TypeProductIdentifier:
_productIdentifier = RMASN1ReadUTF8String(&p, length);
self->_productIdentifier = RMASN1ReadUTF8String(&p, length);
break;
case RMAppReceiptASN1TypeTransactionIdentifier:
_transactionIdentifier = RMASN1ReadUTF8String(&p, length);
self->_transactionIdentifier = RMASN1ReadUTF8String(&p, length);
break;
case RMAppReceiptASN1TypePurchaseDate:
{
NSString *string = RMASN1ReadIA5SString(&p, length);
_purchaseDate = [RMAppReceipt formatRFC3339String:string];
self->_purchaseDate = [RMAppReceipt formatRFC3339String:string];
break;
}
case RMAppReceiptASN1TypeOriginalTransactionIdentifier:
_originalTransactionIdentifier = RMASN1ReadUTF8String(&p, length);
self->_originalTransactionIdentifier = RMASN1ReadUTF8String(&p, length);
break;
case RMAppReceiptASN1TypeOriginalPurchaseDate:
{
NSString *string = RMASN1ReadIA5SString(&p, length);
_originalPurchaseDate = [RMAppReceipt formatRFC3339String:string];
self->_originalPurchaseDate = [RMAppReceipt formatRFC3339String:string];
break;
}
case RMAppReceiptASN1TypeSubscriptionExpirationDate:
{
NSString *string = RMASN1ReadIA5SString(&p, length);
_subscriptionExpirationDate = [RMAppReceipt formatRFC3339String:string];
self->_subscriptionExpirationDate = [RMAppReceipt formatRFC3339String:string];
break;
}
case RMAppReceiptASN1TypeWebOrderLineItemID:
_webOrderLineItemID = RMASN1ReadInteger(&p, length);
self->_webOrderLineItemID = RMASN1ReadInteger(&p, length);
break;
case RMAppReceiptASN1TypeCancellationDate:
{
NSString *string = RMASN1ReadIA5SString(&p, length);
_cancellationDate = [RMAppReceipt formatRFC3339String:string];
self->_cancellationDate = [RMAppReceipt formatRFC3339String:string];
break;
}
}
Expand Down
1 change: 0 additions & 1 deletion RMStore/Optional/RMStoreAppReceiptVerifier.h
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,6 @@
/**
Reference implementation of an app receipt verifier. If security is a concern you might want to avoid using a verifier whose code is open source.
*/
__attribute__((availability(ios,introduced=7.0)))
@interface RMStoreAppReceiptVerifier : NSObject<RMStoreReceiptVerifier>

/**
Expand Down
8 changes: 4 additions & 4 deletions RMStore/Optional/RMStoreAppReceiptVerifier.m
Original file line number Diff line number Diff line change
Expand Up @@ -24,8 +24,8 @@
@implementation RMStoreAppReceiptVerifier

- (void)verifyTransaction:(SKPaymentTransaction*)transaction
success:(void (^)())successBlock
failure:(void (^)(NSError *error))failureBlock
success:(void (^)(void))successBlock
failure:(void (^)(NSError *error))failureBlock
{
RMAppReceipt *receipt = [RMAppReceipt bundleReceipt];
const BOOL verified = [self verifyTransaction:transaction inReceipt:receipt success:successBlock failure:nil]; // failureBlock is nil intentionally. See below.
Expand Down Expand Up @@ -87,8 +87,8 @@ - (BOOL)verifyAppReceipt:(RMAppReceipt*)receipt

- (BOOL)verifyTransaction:(SKPaymentTransaction*)transaction
inReceipt:(RMAppReceipt*)receipt
success:(void (^)())successBlock
failure:(void (^)(NSError *error))failureBlock
success:(void (^)(void))successBlock
failure:(void (^)(NSError *error))failureBlock
{
const BOOL receiptVerified = [self verifyAppReceipt:receipt];
if (!receiptVerified)
Expand Down
Loading