Đây là hướng dẫn về cách tôi giải quyết vấn đề này trong thư viện mua hàng trong ứng dụng RMStore . Tôi sẽ giải thích cách xác minh một giao dịch, bao gồm xác minh toàn bộ biên lai.
Trong nháy mắt
Nhận biên lai và xác minh giao dịch. Nếu thất bại, hãy làm mới hóa đơn và thử lại. Điều này làm cho quá trình xác minh không đồng bộ khi làm mới hóa đơn không đồng bộ.
Từ RMStoreAppReceiptVerifier :
RMAppReceipt *receipt = [RMAppReceipt bundleReceipt];
const BOOL verified = [self verifyTransaction:transaction inReceipt:receipt success:successBlock failure:nil]; // failureBlock is nil intentionally. See below.
if (verified) return;
// Apple recommends to refresh the receipt if validation fails on iOS
[[RMStore defaultStore] refreshReceiptOnSuccess:^{
RMAppReceipt *receipt = [RMAppReceipt bundleReceipt];
[self verifyTransaction:transaction inReceipt:receipt success:successBlock failure:failureBlock];
} failure:^(NSError *error) {
[self failWithBlock:failureBlock error:error];
}];
Lấy dữ liệu nhận
Biên lai là trong [[NSBundle mainBundle] appStoreReceiptURL]
và thực sự là một container PCKS7. Tôi hút mật mã vì vậy tôi đã sử dụng OpenSSL để mở container này. Những người khác dường như đã làm điều đó hoàn toàn với khung hệ thống .
Thêm OpenSSL vào dự án của bạn không phải là chuyện nhỏ. Các wiki RMStore nên giúp đỡ.
Nếu bạn chọn sử dụng OpenSSL để mở bộ chứa PKCS7, mã của bạn có thể trông như thế này. Từ 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;
}
Chúng ta sẽ đi vào chi tiết xác minh sau.
Lấy các trường nhận
Biên lai được thể hiện dưới định dạng ASN1. Nó chứa thông tin chung, một số trường cho mục đích xác minh (chúng ta sẽ đến đó sau) và thông tin cụ thể của từng giao dịch mua trong ứng dụng.
Một lần nữa, OpenSSL đến cứu nguy khi đọc ASN1. Từ RMAppReceipt , sử dụng một vài phương thức trợ giúp:
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;
Nhận mua hàng trong ứng dụng
Mỗi lần mua trong ứng dụng cũng bằng ASN1. Phân tích cú pháp nó rất giống với phân tích thông tin nhận chung.
Từ RMAppReceipt , sử dụng các phương thức trợ giúp tương tự:
[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;
}
}
}];
Cần lưu ý rằng một số giao dịch mua trong ứng dụng, chẳng hạn như hàng tiêu dùng và đăng ký không thể gia hạn, sẽ chỉ xuất hiện một lần trong biên lai. Bạn nên xác minh những điều này ngay sau khi mua hàng (một lần nữa, RMStore giúp bạn điều này).
Xác minh trong nháy mắt
Bây giờ chúng tôi đã nhận được tất cả các lĩnh vực từ biên lai và tất cả các giao dịch mua trong ứng dụng. Đầu tiên chúng tôi xác minh hóa đơn, và sau đó chúng tôi chỉ cần kiểm tra xem biên nhận có chứa sản phẩm của giao dịch hay không.
Dưới đây là phương pháp mà chúng tôi đã gọi lại từ đầu. Từ 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;
}
Xác nhận hóa đơn
Xác minh hóa đơn tự rút xuống:
- Kiểm tra xem biên nhận có hợp lệ PKCS7 và ASN1 không. Chúng tôi đã thực hiện điều này ngầm.
- Xác minh rằng biên nhận được ký bởi Apple. Điều này đã được thực hiện trước khi phân tích hóa đơn và sẽ được chi tiết dưới đây.
- Kiểm tra xem số nhận dạng gói có trong biên nhận có tương ứng với số nhận dạng gói của bạn không. Bạn nên mã hóa số nhận dạng gói của mình, vì dường như không khó để sửa đổi gói ứng dụng của bạn và sử dụng một số biên lai khác.
- Kiểm tra xem phiên bản ứng dụng có trong biên lai có tương ứng với số nhận dạng phiên bản ứng dụng của bạn không. Bạn nên mã hóa phiên bản ứng dụng, vì những lý do tương tự được nêu ở trên.
- Kiểm tra băm nhận để đảm bảo biên nhận tương ứng với thiết bị hiện tại.
5 bước trong mã ở mức cao, từ RMStoreAppReceiptVerificator :
- (BOOL)verifyAppReceipt:(RMAppReceipt*)receipt
{
// Steps 1 & 2 were done while parsing the receipt
if (!receipt) return NO;
// Step 3
if (![receipt.bundleIdentifier isEqualToString:self.bundleIdentifier]) return NO;
// Step 4
if (![receipt.appVersion isEqualToString:self.bundleVersion]) return NO;
// Step 5
if (![receipt verifyReceiptHash]) return NO;
return YES;
}
Hãy đi sâu vào bước 2 và 5.
Xác nhận chữ ký nhận
Quay lại khi chúng tôi trích xuất dữ liệu, chúng tôi liếc qua xác minh chữ ký nhận. Biên lai được ký với Chứng chỉ gốc của Apple Inc., có thể được tải xuống từ Cơ quan cấp chứng chỉ gốc của Apple . Đoạn mã sau lấy bộ chứa PKCS7 và chứng chỉ gốc làm dữ liệu và kiểm tra xem chúng có khớp không:
+ (BOOL)verifyPKCS7:(PKCS7*)container withCertificateData:(NSData*)certificateData
{ // Based on: https://developer.apple.com/library/content/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
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(); // Balances OpenSSL_add_all_digests (), per http://www.openssl.org/docs/crypto/OpenSSL_add_all_algorithms.html
return result == verified;
}
Điều này đã được thực hiện lại từ đầu, trước khi nhận được phân tích cú pháp.
Xác minh băm nhận
Giá trị băm có trong biên nhận là SHA1 của id thiết bị, một số giá trị mờ bao gồm trong hóa đơn và id gói.
Đây là cách bạn sẽ xác minh băm nhận trên iOS. Từ RMAppReceipt :
- (BOOL)verifyReceiptHash
{
// TODO: Getting the uuid in Mac is different. See: https://developer.apple.com/library/content/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/content/releasenotes/General/ValidateAppStoreReceipt/Chapters/ValidateLocally.html#//apple_ref/doc/uid/TP40010573-CH1-SW5
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];
}
Và đó là ý chính của nó. Tôi có thể đang thiếu một cái gì đó ở đây hoặc ở đó, vì vậy tôi có thể quay lại bài này sau. Trong mọi trường hợp, tôi khuyên bạn nên duyệt mã hoàn chỉnh để biết thêm chi tiết.