以下是我在我的应用内购买库
RMStore中解决此问题的步骤说明。我将解释如何验证交易,包括整个收据的验证。
一览:
获取收据并验证交易。如果失败,请刷新收据并重试。这使得验证过程是异步的,因为刷新收据是异步的。
从
RMStoreAppReceiptVerifier中:
RMAppReceipt *receipt = [RMAppReceipt bundleReceipt];
const BOOL verified = [self verifyTransaction:transaction inReceipt:receipt success:successBlock failure:nil];
if (verified) return;
[[RMStore defaultStore] refreshReceiptOnSuccess:^{
RMAppReceipt *receipt = [RMAppReceipt bundleReceipt];
[self verifyTransaction:transaction inReceipt:receipt success:successBlock failure:failureBlock];
} failure:^(NSError *error) {
[self failWithBlock:failureBlock error:error];
}];
获取收据数据
收据位于[[NSBundle mainBundle] appStoreReceiptURL]
,实际上是一个PCKS7容器。我不擅长密码学,所以我使用了OpenSSL来打开这个容器。其他人似乎纯粹使用system frameworks就做到了这一点。
将OpenSSL添加到您的项目中并不容易。RMStore wiki应该会有帮助。
如果您选择使用OpenSSL来打开PKCS7容器,则您的代码可能如下所示。来自RMAppReceipt:
+ (NSData*)dataFromPKCS7Path:(NSString*)path
{
const char *cpath = [[path stringByStandardizingPath] fileSystemRepresentation];
FILE *fp = fopen(cpath, "rb");
if (!fp) return nil;
PKCS7 *p7 = d2i_PKCS7_fp(fp, NULL);
fclose(fp);
if (!p7) return nil;
NSData *data;
NSURL *certificateURL = [[NSBundle mainBundle] URLForResource:@"AppleIncRootCertificate" withExtension:@"cer"];
NSData *certificateData = [NSData dataWithContentsOfURL:certificateURL];
if ([self verifyPKCS7:p7 withCertificateData:certificateData])
{
struct pkcs7_st *contents = p7->d.sign->contents;
if (PKCS7_type_is_data(contents))
{
ASN1_OCTET_STRING *octets = contents->d.data;
data = [NSData dataWithBytes:octets->data length:octets->length];
}
}
PKCS7_free(p7);
return data;
}
我们稍后会详细介绍验证的细节。
获取收据字段
收据以ASN1格式表示。它包含一般信息,一些用于验证目的的字段(稍后我们会讲到),以及每个适用的应用内购买的特定信息。
同样地,当涉及到读取ASN1时,OpenSSL可以提供帮助。使用RMAppReceipt中的一些辅助方法:
NSMutableArray *purchases = [NSMutableArray array];
[RMAppReceipt enumerateASN1Attributes:asn1Data.bytes length:asn1Data.length usingBlock:^(NSData *data, int type) {
const uint8_t *s = data.bytes;
const NSUInteger length = data.length;
switch (type)
{
case RMAppReceiptASN1TypeBundleIdentifier:
_bundleIdentifierData = data;
_bundleIdentifier = RMASN1ReadUTF8String(&s, length);
break;
case RMAppReceiptASN1TypeAppVersion:
_appVersion = RMASN1ReadUTF8String(&s, length);
break;
case RMAppReceiptASN1TypeOpaqueValue:
_opaqueValue = data;
break;
case RMAppReceiptASN1TypeHash:
_hash = data;
break;
case RMAppReceiptASN1TypeInAppPurchaseReceipt:
{
RMAppReceiptIAP *purchase = [[RMAppReceiptIAP alloc] initWithASN1Data:data];
[purchases addObject:purchase];
break;
}
case RMAppReceiptASN1TypeOriginalAppVersion:
_originalAppVersion = RMASN1ReadUTF8String(&s, length);
break;
case RMAppReceiptASN1TypeExpirationDate:
{
NSString *string = RMASN1ReadIA5SString(&s, length);
_expirationDate = [RMAppReceipt formatRFC3339String:string];
break;
}
}
}];
_inAppPurchases = purchases;
获取应用内购买
每个应用内购买也都是ASN1格式的。解析它与解析一般收据信息非常相似。
从RMAppReceipt中使用相同的辅助方法:
[RMAppReceipt enumerateASN1Attributes:asn1Data.bytes length:asn1Data.length usingBlock:^(NSData *data, int type) {
const uint8_t *p = data.bytes;
const NSUInteger length = data.length;
switch (type)
{
case RMAppReceiptASN1TypeQuantity:
_quantity = RMASN1ReadInteger(&p, length);
break;
case RMAppReceiptASN1TypeProductIdentifier:
_productIdentifier = RMASN1ReadUTF8String(&p, length);
break;
case RMAppReceiptASN1TypeTransactionIdentifier:
_transactionIdentifier = RMASN1ReadUTF8String(&p, length);
break;
case RMAppReceiptASN1TypePurchaseDate:
{
NSString *string = RMASN1ReadIA5SString(&p, length);
_purchaseDate = [RMAppReceipt formatRFC3339String:string];
break;
}
case RMAppReceiptASN1TypeOriginalTransactionIdentifier:
_originalTransactionIdentifier = RMASN1ReadUTF8String(&p, length);
break;
case RMAppReceiptASN1TypeOriginalPurchaseDate:
{
NSString *string = RMASN1ReadIA5SString(&p, length);
_originalPurchaseDate = [RMAppReceipt formatRFC3339String:string];
break;
}
case RMAppReceiptASN1TypeSubscriptionExpirationDate:
{
NSString *string = RMASN1ReadIA5SString(&p, length);
_subscriptionExpirationDate = [RMAppReceipt formatRFC3339String:string];
break;
}
case RMAppReceiptASN1TypeWebOrderLineItemID:
_webOrderLineItemID = RMASN1ReadInteger(&p, length);
break;
case RMAppReceiptASN1TypeCancellationDate:
{
NSString *string = RMASN1ReadIA5SString(&p, length);
_cancellationDate = [RMAppReceipt formatRFC3339String:string];
break;
}
}
}];
请注意,某些应用内购买(例如可消耗品和非续订订阅)将仅在收据中出现一次。您应该在购买后立即验证这些内容(再次使用RMStore帮助您完成此操作)。
一目了然的验证
现在我们已经获得了收据和所有应用内购买的所有字段。首先,我们验证收据本身,然后简单地检查收据是否包含交易产品。
下面是我们在开始时调用的方法。来自
RMStoreAppReceiptVerificator:
- (BOOL)verifyTransaction:(SKPaymentTransaction*)transaction
inReceipt:(RMAppReceipt*)receipt
success:(void (^)())successBlock
failure:(void (^)(NSError *error))failureBlock
{
const BOOL receiptVerified = [self verifyAppReceipt:receipt];
if (!receiptVerified)
{
[self failWithBlock:failureBlock message:NSLocalizedString(@"The app receipt failed verification", @"")];
return NO;
}
SKPayment *payment = transaction.payment;
const BOOL transactionVerified = [receipt containsInAppPurchaseOfProductIdentifier:payment.productIdentifier];
if (!transactionVerified)
{
[self failWithBlock:failureBlock message:NSLocalizedString(@"The app receipt doest not contain the given product", @"")];
return NO;
}
if (successBlock)
{
successBlock();
}
return YES;
}
验证收据
验证收据本身包括以下内容:
- 检查收据是否是有效的PKCS7和ASN1格式。我们已经隐式地完成了这一步。
- 验证收据是否由苹果签名。在解析收据之前已经完成了此操作,并将在下面详细说明。
- 检查收据中包含的捆绑标识符是否与您的捆绑标识符相对应。您应该硬编码您的捆绑标识符,因为很容易修改您的应用程序捆绑并使用其他收据。
- 检查收据中包含的应用程序版本是否与您的应用程序版本标识符相对应。出于上述原因,您应该硬编码应用程序版本。
- 检查收据哈希值,以确保收据与当前设备相对应。
从RMStoreAppReceiptVerificator的代码高层次上看,有5个步骤:
- (BOOL)verifyAppReceipt:(RMAppReceipt*)receipt
{
if (!receipt) return NO;
if (![receipt.bundleIdentifier isEqualToString:self.bundleIdentifier]) return NO;
if (![receipt.appVersion isEqualToString:self.bundleVersion]) return NO;
if (![receipt verifyReceiptHash]) return NO;
return YES;
}
让我们深入了解步骤2和5。
验证收据签名
在提取数据时,我们忽略了收据签名验证。该收据使用苹果公司根证书进行签名,可从Apple Root Certificate Authority下载。以下代码将PKCS7容器和根证书作为数据,并检查它们是否匹配:
+ (BOOL)verifyPKCS7:(PKCS7*)container withCertificateData:(NSData*)certificateData
{
static int verified = 1;
int result = 0;
OpenSSL_add_all_digests();
X509_STORE *store = X509_STORE_new();
if (store)
{
const uint8_t *certificateBytes = (uint8_t *)(certificateData.bytes);
X509 *certificate = d2i_X509(NULL, &certificateBytes, (long)certificateData.length);
if (certificate)
{
X509_STORE_add_cert(store, certificate);
BIO *payload = BIO_new(BIO_s_mem());
result = PKCS7_verify(container, NULL, store, NULL, payload, 0);
BIO_free(payload);
X509_free(certificate);
}
}
X509_STORE_free(store);
EVP_cleanup();
return result == verified;
}
在解析收据之前,这是在开始时完成的。
验证收据哈希
收据中包含的哈希是设备ID、收据中包含的一些不透明值和捆绑标识符的SHA1。
以下是在iOS上验证收据哈希的方法。从RMAppReceipt:
- (BOOL)verifyReceiptHash
{
NSUUID *uuid = [[UIDevice currentDevice] identifierForVendor];
unsigned char uuidBytes[16];
[uuid getUUIDBytes:uuidBytes];
NSMutableData *data = [NSMutableData data];
[data appendBytes:uuidBytes length:sizeof(uuidBytes)];
[data appendData:self.opaqueValue];
[data appendData:self.bundleIdentifierData];
NSMutableData *expectedHash = [NSMutableData dataWithLength:SHA_DIGEST_LENGTH];
SHA1(data.bytes, data.length, expectedHash.mutableBytes);
return [expectedHash isEqualToData:self.hash];
}
这就是要点。可能会有一些细节我没有涉及到,所以我之后可能会回来修改这篇文章。无论如何,我建议浏览完整的代码以获取更多细节。