ios - iOS8 中的应用内购买收据验证

标签 ios objective-c iphone in-app-purchase

我们正在关注 http://www.raywenderlich.com/23266/in-app-purchases-in-ios-6-tutorial-consumables-and-receipt-validation 中的 raywenderlinch 教程

但是在我们的项目中导入 verificationController 类时出现了一些错误。现在,我正在使用 iOS8。

Error is: Implicit declaration of function 'checkReciptSecurity' is invalid in C99

我也在苹果开发者网站上搜索验证类的示例代码,没有找到他们的页面。

请给我您的解决方案或提供更新到 iOS8 的验证类的链接。

最佳答案

在 VerificationController.h 中放置这样的函数原型(prototype):

- (void)verifyPurchase:(SKPaymentTransaction *)transaction completionHandler:(VerifyCompletionHandler)completionHandler;
BOOL checkReceiptSecurity(NSString *purchase_info_string, NSString *signature_string, CFDateRef purchaseDate);

这样做的原因是调用函数 checkReceiptSecurity 的行号在函数声明之前。

您必须修改 VerificationController.m 文件代码。 我已将修改后的代码放在这里。

#import "VerificationController.h"
#import "NSData+Base64.h"

static VerificationController *singleton;

@implementation VerificationController {
    NSMutableDictionary * _completionHandlers;
}

+ (VerificationController *)sharedInstance
{
    if (singleton == nil)
    {
        singleton = [[VerificationController alloc] init];
    }
 return singleton;
}


 - (id)init
{
self = [super init];
if (self != nil)
{
    transactionsReceiptStorageDictionary = [[NSMutableDictionary alloc] init];
    _completionHandlers = [[NSMutableDictionary alloc] init];
   }
   return self;
}


- (NSDictionary *)dictionaryFromPlistData:(NSData *)data
{
NSError *error;
NSDictionary *dictionaryParsed = [NSPropertyListSerialization propertyListWithData:data
                                                                           options:NSPropertyListImmutable
                                                                            format:nil
                                                                             error:&error];
if (!dictionaryParsed)
{
    if (error)
    {
        NSLog(@"Error parsing plist");
    }
    return nil;
}
return dictionaryParsed;
}


- (NSDictionary *)dictionaryFromJSONData:(NSData *)data
{
NSError *error;
NSDictionary *dictionaryParsed = [NSJSONSerialization JSONObjectWithData:data
                                                                 options:0
                                                                   error:&error];
if (!dictionaryParsed)
{
    if (error)
    {
        NSLog(@"Error parsing dictionary");
    }
    return nil;
}
return dictionaryParsed;
}


#pragma mark Receipt Verification

// This method should be called once a transaction gets to the SKPaymentTransactionStatePurchased or    SKPaymentTransactionStateRestored state
// Call it with the SKPaymentTransaction.transactionReceipt
- (void)verifyPurchase:(SKPaymentTransaction *)transaction completionHandler:(VerifyCompletionHandler)completionHandler
{    
BOOL isOk = [self isTransactionAndItsReceiptValid:transaction];
if (!isOk)
{
    // There was something wrong with the transaction we got back, so no need to call verifyReceipt.
    NSLog(@"Invalid transacion");
    completionHandler(FALSE);
    return;
}

// The transaction looks ok, so start the verify process.

// Encode the receiptData for the itms receipt verification POST request.
NSString *jsonObjectString = [self encodeBase64:(uint8_t *)transaction.transactionReceipt.bytes
                                         length:transaction.transactionReceipt.length];

// Create the POST request payload.
NSString *payload = [NSString stringWithFormat:@"{\"receipt-data\" : \"%@\", \"password\" : \"%@\"}",
                     jsonObjectString, ITC_CONTENT_PROVIDER_SHARED_SECRET];

NSData *payloadData = [payload dataUsingEncoding:NSUTF8StringEncoding];

#warning Check for the correct itms verify receipt URL
// Use ITMS_SANDBOX_VERIFY_RECEIPT_URL while testing against the sandbox.
NSString *serverURL = ITMS_SANDBOX_VERIFY_RECEIPT_URL; //ITMS_PROD_VERIFY_RECEIPT_URL;

// Create the POST request to the server.
NSMutableURLRequest *request = [NSMutableURLRequest requestWithURL:[NSURL URLWithString:serverURL]];
[request setHTTPMethod:@"POST"];
[request setHTTPBody:payloadData];
NSURLConnection *conn = [[NSURLConnection alloc] initWithRequest:request delegate:self];

_completionHandlers[[NSValue valueWithNonretainedObject:conn]] = completionHandler;

[conn start];

// The transation receipt has not been validated yet.  That is done from the NSURLConnection callback.
}

// Check the validity of the receipt.  If it checks out then also ensure the transaction is something
// we haven't seen before and then decode and save the purchaseInfo from the receipt for later receipt validation.
- (BOOL)isTransactionAndItsReceiptValid:(SKPaymentTransaction *)transaction
{
if (!(transaction && transaction.transactionReceipt && [transaction.transactionReceipt length] > 0))
{
    // Transaction is not valid.
    return NO;
}

// Pull the purchase-info out of the transaction receipt, decode it, and save it for later so
// it can be cross checked with the verifyReceipt.
NSDictionary *receiptDict       = [self dictionaryFromPlistData:transaction.transactionReceipt];
NSString *transactionPurchaseInfo = [receiptDict objectForKey:@"purchase-info"];
NSString *decodedPurchaseInfo   = [self decodeBase64:transactionPurchaseInfo length:nil];
NSDictionary *purchaseInfoDict  = [self dictionaryFromPlistData:[decodedPurchaseInfo dataUsingEncoding:NSUTF8StringEncoding]];

NSString *transactionId         = [purchaseInfoDict objectForKey:@"transaction-id"];
NSString *purchaseDateString    = [purchaseInfoDict objectForKey:@"purchase-date"];
NSString *signature             = [receiptDict objectForKey:@"signature"];

// Convert the string into a date
NSDateFormatter *dateFormat = [[NSDateFormatter alloc] init];
[dateFormat setDateFormat:@"yyyy-MM-dd HH:mm:ss z"];

NSDate *purchaseDate = [dateFormat dateFromString:[purchaseDateString stringByReplacingOccurrencesOfString:@"Etc/" withString:@""]];


if (![self isTransactionIdUnique:transactionId])
{
    // We've seen this transaction before.
    // Had [transactionsReceiptStorageDictionary objectForKey:transactionId]
    // Got purchaseInfoDict
    return NO;
}

// Check the authenticity of the receipt response/signature etc.

BOOL result = checkReceiptSecurity(transactionPurchaseInfo, signature,
                                   (__bridge CFDateRef)(purchaseDate));

if (!result)
{
    return NO;
}

// Ensure the transaction itself is legit
if (![self doTransactionDetailsMatchPurchaseInfo:transaction withPurchaseInfo:purchaseInfoDict])
{
    return NO;
}

// Make a note of the fact that we've seen the transaction id already
[self saveTransactionId:transactionId];

// Save the transaction receipt's purchaseInfo in the transactionsReceiptStorageDictionary.
[transactionsReceiptStorageDictionary setObject:purchaseInfoDict forKey:transactionId];

return YES;
}

// Make sure the transaction details actually match the purchase info
- (BOOL)doTransactionDetailsMatchPurchaseInfo:(SKPaymentTransaction *)transaction withPurchaseInfo:(NSDictionary *)purchaseInfoDict

{
if (!transaction || !purchaseInfoDict)
{
    return NO;
}

int failCount = 0;

if (![transaction.payment.productIdentifier isEqualToString:[purchaseInfoDict objectForKey:@"product-id"]])
{

    failCount++;
}

if (transaction.payment.quantity != [[purchaseInfoDict objectForKey:@"quantity"] intValue])
{
    failCount++;
}

if (![transaction.transactionIdentifier isEqualToString:[purchaseInfoDict objectForKey:@"transaction-id"]])
{
    failCount++;
}

// Optionally check the bid and bvrs match this app's current bundle ID and bundle version.
// Optionally check the requestData.
// Optionally check the dates.

if (failCount != 0)
{
    return NO;
}

// The transaction and its signed content seem ok.
return YES;
}



- (BOOL)isTransactionIdUnique:(NSString *)transactionId
{
NSString *transactionDictionary = KNOWN_TRANSACTIONS_KEY;
// Save the transactionId to the standardUserDefaults so we can check against that later
NSUserDefaults *defaults = [NSUserDefaults standardUserDefaults];
[defaults synchronize];

if (![defaults objectForKey:transactionDictionary])
{
    [defaults setObject:[[NSMutableDictionary alloc] init] forKey:transactionDictionary];
    [defaults synchronize];
}

if (![[defaults objectForKey:transactionDictionary] objectForKey:transactionId])
{
    return YES;
}
// The transaction already exists in the defaults.
return NO;
}


- (void)saveTransactionId:(NSString *)transactionId
{
// Save the transactionId to the standardUserDefaults so we can check against that later
// If dictionary exists already then retrieve it and add new transactionID
// Regardless save transactionID to dictionary which gets saved to NSUserDefaults
NSUserDefaults *defaults = [NSUserDefaults standardUserDefaults];
NSString *transactionDictionary = KNOWN_TRANSACTIONS_KEY;
NSMutableDictionary *dictionary = [NSMutableDictionary dictionaryWithDictionary:
                                   [defaults objectForKey:transactionDictionary]];
if (!dictionary)
{
    dictionary = [[NSMutableDictionary alloc] initWithObjectsAndKeys:[NSNumber numberWithInt:1], transactionId, nil];
} else {
    [dictionary setObject:[NSNumber numberWithInt:1] forKey:transactionId];
}
[defaults setObject:dictionary forKey:transactionDictionary];
[defaults synchronize];

}


- (BOOL)doesTransactionInfoMatchReceipt:(NSString*) receiptString
{
// Convert the responseString into a dictionary and pull out the receipt data.
NSDictionary *verifiedReceiptDictionary = [self dictionaryFromJSONData:[receiptString dataUsingEncoding:NSUTF8StringEncoding]];

// Check the status of the verifyReceipt call
id status = [verifiedReceiptDictionary objectForKey:@"status"];
if (!status)
{
    return NO;
}
int verifyReceiptStatus = [status integerValue];
// 21006 = This receipt is valid but the subscription has expired.
if (0 != verifyReceiptStatus && 21006 != verifyReceiptStatus)
{
    return NO;
}

// The receipt is valid, so checked the receipt specifics now.

NSDictionary *verifiedReceiptReceiptDictionary  = [verifiedReceiptDictionary objectForKey:@"receipt"];
NSString *verifiedReceiptUniqueIdentifier       = [verifiedReceiptReceiptDictionary objectForKey:@"unique_identifier"];
NSString *transactionIdFromVerifiedReceipt      = [verifiedReceiptReceiptDictionary objectForKey:@"transaction_id"];

// Get the transaction's receipt data from the transactionsReceiptStorageDictionary
NSDictionary *purchaseInfoFromTransaction = [transactionsReceiptStorageDictionary objectForKey:transactionIdFromVerifiedReceipt];

if (!purchaseInfoFromTransaction)
{
    // We didn't find a receipt for this transaction.
    return NO;
}


// NOTE: Instead of counting errors you could just return early.
int failCount = 0;

// Verify all the receipt specifics to ensure everything matches up as expected
if (![[verifiedReceiptReceiptDictionary objectForKey:@"bid"]
      isEqualToString:[purchaseInfoFromTransaction objectForKey:@"bid"]])
{
    failCount++;
}

if (![[verifiedReceiptReceiptDictionary objectForKey:@"product_id"]
      isEqualToString:[purchaseInfoFromTransaction objectForKey:@"product-id"]])
{
    failCount++;
}

if (![[verifiedReceiptReceiptDictionary objectForKey:@"quantity"]
      isEqualToString:[purchaseInfoFromTransaction objectForKey:@"quantity"]])
{
    failCount++;
}

if (![[verifiedReceiptReceiptDictionary objectForKey:@"item_id"]
      isEqualToString:[purchaseInfoFromTransaction objectForKey:@"item-id"]])
{
    failCount++;
}

if ([[UIDevice currentDevice] respondsToSelector:NSSelectorFromString(@"identifierForVendor")]) // iOS 6?
{
#if IS_IOS6_AWARE
    // iOS 6 (or later)
    NSString *localIdentifier                   = [[[UIDevice currentDevice] identifierForVendor] UUIDString];
    NSString *purchaseInfoUniqueVendorId        = [purchaseInfoFromTransaction objectForKey:@"unique-vendor-identifier"];
    NSString *verifiedReceiptVendorIdentifier   = [verifiedReceiptReceiptDictionary objectForKey:@"unique_vendor_identifier"];


    if(verifiedReceiptVendorIdentifier)
    {
        if (![purchaseInfoUniqueVendorId isEqualToString:verifiedReceiptVendorIdentifier]
            || ![purchaseInfoUniqueVendorId isEqualToString:localIdentifier])
        {
            // Comment this line out to test in the Simulator.
            failCount++;
        }
    }
#endif
} else {
    // Pre iOS 6 
//        NSString *localIdentifier           = [UIDevice currentDevice].uniqueIdentifier;
//        NSString *purchaseInfoUniqueId      = [purchaseInfoFromTransaction objectForKey:@"unique-identifier"];
//        if (![purchaseInfoUniqueId isEqualToString:verifiedReceiptUniqueIdentifier]
//            || ![purchaseInfoUniqueId isEqualToString:localIdentifier])
//        {
//            // Comment this line out to test in the Simulator.
//            failCount++;
//        }        
}


// Do addition time checks for the transaction and receipt.

if(failCount != 0)
{
    return NO;
}

return YES;
}


#pragma mark NSURLConnectionDelegate (for the verifyReceipt connection)

- (void)connection:(NSURLConnection *)connection didFailWithError:(NSError *)error {

NSLog(@"Connection failure: %@", error);

VerifyCompletionHandler completionHandler = _completionHandlers[[NSValue valueWithNonretainedObject:connection]];
[_completionHandlers removeObjectForKey:[NSValue valueWithNonretainedObject:connection]];
completionHandler(FALSE);

}
- (void)connection:(NSURLConnection *)connection didReceiveData:(NSData *)data
{
NSString *responseString = [[NSString alloc] initWithData:data encoding:NSUTF8StringEncoding];

BOOL isOk = [self doesTransactionInfoMatchReceipt:responseString];

VerifyCompletionHandler completionHandler = _completionHandlers[[NSValue valueWithNonretainedObject:connection]];
[_completionHandlers removeObjectForKey:[NSValue valueWithNonretainedObject:connection]];
if (isOk)
{
    //Validation suceeded. Unlock content here.
    NSLog(@"Validation successful");
    completionHandler(TRUE);

} else {
    NSLog(@"Validation failed");
    completionHandler(FALSE);
}
}
- (void)connection:(NSURLConnection *)connection willSendRequestForAuthenticationChallenge:(NSURLAuthenticationChallenge *)challenge
{
if ([[[challenge protectionSpace] authenticationMethod] isEqualToString:NSURLAuthenticationMethodServerTrust])
{
    SecTrustRef trust = [[challenge protectionSpace] serverTrust];
    NSError *error = nil;
    BOOL didUseCredential = NO;
    BOOL isTrusted = [self validateTrust:trust error:&error];
    if (isTrusted)
    {
        NSURLCredential *trust_credential = [NSURLCredential credentialForTrust:trust];
        if (trust_credential)
        {
            [[challenge sender] useCredential:trust_credential forAuthenticationChallenge:challenge];
            didUseCredential = YES;
        }
    }
    if (!didUseCredential)
    {
        [[challenge sender] cancelAuthenticationChallenge:challenge];
    }
} else {
    [[challenge sender] performDefaultHandlingForAuthenticationChallenge:challenge];
}
}

// NOTE: These are needed for 4.x (as willSendRequestForAuthenticationChallenge: is not supported)
- (BOOL)connection:(NSURLConnection *)connection canAuthenticateAgainstProtectionSpace:(NSURLProtectionSpace *)protectionSpace
{
return [[protectionSpace authenticationMethod] isEqualToString:NSURLAuthenticationMethodServerTrust];
}
- (void)connection:(NSURLConnection *)connection didReceiveAuthenticationChallenge:(NSURLAuthenticationChallenge *)challenge
{
if ([[[challenge protectionSpace] authenticationMethod] isEqualToString:NSURLAuthenticationMethodServerTrust])
    {
    SecTrustRef trust = [[challenge protectionSpace] serverTrust];
    NSError *error = nil;
    BOOL didUseCredential = NO;
    BOOL isTrusted = [self validateTrust:trust error:&error];
    if (isTrusted)
    {
        NSURLCredential *credential = [NSURLCredential credentialForTrust:trust];
        if (credential)
        {
            [[challenge sender] useCredential:credential forAuthenticationChallenge:challenge];
            didUseCredential = YES;
        }
    }
    if (! didUseCredential) {
        [[challenge sender] cancelAuthenticationChallenge:challenge];
    }
} else {
    [[challenge sender] performDefaultHandlingForAuthenticationChallenge:challenge];
}
}


#pragma mark
#pragma mark NSURLConnection - Trust validation

- (BOOL)validateTrust:(SecTrustRef)trust error:(NSError **)error
{

// Include some Security framework SPIs
extern CFStringRef kSecTrustInfoExtendedValidationKey;
extern CFDictionaryRef SecTrustCopyInfo(SecTrustRef trust);

BOOL trusted = NO;
SecTrustResultType trust_result;
if ((noErr == SecTrustEvaluate(trust, &trust_result)) && (trust_result == kSecTrustResultUnspecified))
{
    NSDictionary *trust_info = (__bridge_transfer NSDictionary *)SecTrustCopyInfo(trust);
    id hasEV = [trust_info objectForKey:(__bridge NSString *)kSecTrustInfoExtendedValidationKey];
    trusted =  [hasEV isKindOfClass:[NSValue class]] && [hasEV boolValue];
}

if (trust)
{
    if (!trusted && error)
    {
        *error = [NSError errorWithDomain:@"kSecTrustError" code:(NSInteger)trust_result userInfo:nil];
    }
    return trusted;
}
return NO;
}
#pragma mark
#pragma mark Base 64 encoding

- (NSString *)encodeBase64:(const uint8_t *)input length:(NSInteger)length
{
NSData * data = [NSData dataWithBytes:input length:length];
return [data base64EncodedString];
}
- (NSString *)decodeBase64:(NSString *)input length:(NSInteger *)length
{
NSData * data = [NSData dataFromBase64String:input];
return [[NSString alloc] initWithData:data encoding:NSUTF8StringEncoding];
}

char* base64_encode(const void* buf, size_t size) {
size_t outputLength;
return NewBase64Encode(buf, size, NO, &outputLength);
}
void * base64_decode(const char* s, size_t * data_len)
{
 return NewBase64Decode(s, strlen(s), data_len);
}
@end
#pragma mark
#pragma mark Check Receipt signature
#include <CommonCrypto/CommonDigest.h>
#include <Security/Security.h>
#include <AssertMacros.h>
unsigned int iTS_intermediate_der_len = 1039;

unsigned char iTS_intermediate_der[] = {
put the hexacode here
};

BOOL checkReceiptSecurity(NSString *purchase_info_string, NSString *signature_string, CFDateRef purchaseDate)
{
BOOL valid = NO;
SecCertificateRef leaf = NULL, intermediate = NULL;
SecTrustRef trust = NULL;
SecPolicyRef policy = SecPolicyCreateBasicX509();

NSData *certificate_data;
NSArray *anchors;

/*
 Parse inputs:
 purchase_info_string and signature_string are base64 encoded JSON blobs that need to
 be decoded.
 */

require([purchase_info_string canBeConvertedToEncoding:NSASCIIStringEncoding] &&
        [signature_string canBeConvertedToEncoding:NSASCIIStringEncoding], outLabel);

size_t purchase_info_length;
uint8_t *purchase_info_bytes = base64_decode([purchase_info_string cStringUsingEncoding:NSASCIIStringEncoding],
                                             &purchase_info_length);

size_t signature_length;
uint8_t *signature_bytes = base64_decode([signature_string cStringUsingEncoding:NSASCIIStringEncoding],
                                         &signature_length);

require(purchase_info_bytes && signature_bytes, outLabel);

/*
 Binary format looks as follows:

 RECEIPTVERSION | SIGNATURE | CERTIFICATE SIZE | CERTIFICATE
 1 byte           128         4 bytes
 big endian

 Extract version, signature and certificate(s).
 Check receipt version == 2.
 Sanity check that signature is 128 bytes.
 Sanity check certificate size <= remaining payload data.
 */

#pragma pack(push, 1)
struct signature_blob {
    uint8_t version;
    uint8_t signature[128];
    uint32_t cert_len;
    uint8_t certificate[];
} *signature_blob_ptr = (struct signature_blob *)signature_bytes;
#pragma pack(pop)
uint32_t certificate_len;

/*
 Make sure the signature blob is long enough to safely extract the version and
 cert_len fields, then perform a sanity check on the fields.
 */
require(signature_length > offsetof(struct signature_blob, certificate), outLabel);
require(signature_blob_ptr->version == 2, outLabel);
certificate_len = ntohl(signature_blob_ptr->cert_len);

require(signature_length - offsetof(struct signature_blob, certificate) >= certificate_len, outLabel);

/*
 Validate certificate chains back to valid receipt signer; policy approximation for now
 set intermediate as a trust anchor; current intermediate lapses in 2016.
 */

certificate_data = [NSData dataWithBytes:signature_blob_ptr->certificate length:certificate_len];
require(leaf = SecCertificateCreateWithData(NULL, (__bridge CFDataRef) certificate_data), outLabel);

certificate_data = [NSData dataWithBytes:iTS_intermediate_der length:iTS_intermediate_der_len];
require(intermediate = SecCertificateCreateWithData(NULL, (__bridge CFDataRef) certificate_data), outLabel);

anchors = [NSArray arrayWithObject:(__bridge id)intermediate];
require(anchors, outLabel);

require_noerr(SecTrustCreateWithCertificates(leaf, policy, &trust), outLabel);
require_noerr(SecTrustSetAnchorCertificates(trust, (__bridge CFArrayRef) anchors), outLabel);

if (purchaseDate)
{
    require_noerr(SecTrustSetVerifyDate(trust, purchaseDate), outLabel);
}

SecTrustResultType trust_result;
require_noerr(SecTrustEvaluate(trust, &trust_result), outLabel);
require(trust_result == kSecTrustResultUnspecified, outLabel);

require(2 == SecTrustGetCertificateCount(trust), outLabel);

/*
 Chain is valid, use leaf key to verify signature on receipt by
 calculating SHA1(version|purchaseInfo)
 */

CC_SHA1_CTX sha1_ctx;
uint8_t to_be_verified_data[CC_SHA1_DIGEST_LENGTH];

CC_SHA1_Init(&sha1_ctx);
CC_SHA1_Update(&sha1_ctx, &signature_blob_ptr->version, sizeof(signature_blob_ptr->version));
CC_SHA1_Update(&sha1_ctx, purchase_info_bytes, purchase_info_length);
CC_SHA1_Final(to_be_verified_data, &sha1_ctx);

SecKeyRef receipt_signing_key = SecTrustCopyPublicKey(trust);
require(receipt_signing_key, outLabel);
require_noerr(SecKeyRawVerify(receipt_signing_key, kSecPaddingPKCS1SHA1,
                              to_be_verified_data, sizeof(to_be_verified_data),
                              signature_blob_ptr->signature, sizeof(signature_blob_ptr->signature)),
              outLabel);

/*
 Optional:  Verify that the receipt certificate has the 1.2.840.113635.100.6.5.1 Null OID

 The signature is a 1024-bit RSA signature.
 */

valid = YES;

outLabel:
if (leaf) CFRelease(leaf);
if (intermediate) CFRelease(intermediate);
if (trust) CFRelease(trust);
if (policy) CFRelease(policy);

return valid;
}

关于ios - iOS8 中的应用内购买收据验证,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/26890349/

相关文章:

ios - CIPixellate滤镜中kCIInputCenterKey参数控制什么?

ios - Xcode 9 - 无法发出预编译 header

ios - UITableViewCell 没有正确更新 swift 2.2

ios - xib拒绝展示

objective-c - EXC_BAD_ACCESS 访问结构体变量时出现运行时异常->Objective C

iphone - 缩放图像非常慢 - 帮助

ios - 从 iPhone 检索最后几张照片

objective-c - Objective-C中强弱的区别

iphone - 在警报中显示当前 View Controller 的名称

ios - UIScrollView 无法滚动?