Berikut ini adalah langkah-langkah bagaimana saya memecahkan ini di perpustakaan pembelian dalam aplikasi RMStore saya . Saya akan menjelaskan cara memverifikasi transaksi, yang mencakup memverifikasi seluruh tanda terima.
Sekilas
Dapatkan tanda terima dan verifikasi transaksi. Jika gagal, segarkan tanda terima dan coba lagi. Ini membuat proses verifikasi tidak sinkron seperti menyegarkan tanda terima tidak sinkron.
Dari 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];
}];
Mendapatkan data tanda terima
Tanda terima ada di dalam [[NSBundle mainBundle] appStoreReceiptURL]
dan sebenarnya adalah wadah PCKS7. Saya payah pada kriptografi jadi saya menggunakan OpenSSL untuk membuka wadah ini. Yang lain rupanya telah melakukannya murni dengan kerangka kerja sistem .
Menambahkan OpenSSL ke proyek Anda bukanlah hal sepele. The RMStore wiki harus membantu.
Jika Anda memilih untuk menggunakan OpenSSL untuk membuka wadah PKCS7, kode Anda bisa seperti ini. Dari 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;
}
Kami akan masuk ke detail verifikasi nanti.
Mendapatkan bidang tanda terima
Tanda terima dinyatakan dalam format ASN1. Ini berisi informasi umum, beberapa bidang untuk keperluan verifikasi (kami akan membahasnya nanti) dan informasi spesifik dari setiap pembelian dalam aplikasi yang berlaku.
Sekali lagi, OpenSSL datang untuk menyelamatkan ketika datang untuk membaca ASN1. Dari RMAppReceipt , menggunakan beberapa metode pembantu:
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;
Mendapatkan pembelian dalam aplikasi
Setiap pembelian dalam aplikasi juga dalam ASN1. Mem-parsingnya sangat mirip dengan menguraikan informasi penerimaan umum.
Dari RMAppReceipt , menggunakan metode pembantu yang sama:
[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;
}
}
}];
Perlu dicatat bahwa pembelian dalam aplikasi tertentu, seperti barang habis pakai dan langganan tidak terbarukan, akan muncul hanya sekali dalam tanda terima. Anda harus memverifikasi ini tepat setelah pembelian (sekali lagi, RMStore membantu Anda dengan ini).
Sekilas tentang verifikasi
Sekarang kami mendapatkan semua bidang dari tanda terima dan semua pembelian dalam aplikasi. Pertama kita memverifikasi tanda terima itu sendiri, dan kemudian kita hanya memeriksa apakah tanda terima itu mengandung produk transaksi.
Di bawah ini adalah metode yang kami panggil kembali di awal. Dari 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;
}
Memverifikasi tanda terima
Memverifikasi tanda terima itu sendiri bermuara ke:
- Memeriksa bahwa tanda terima valid PKCS7 dan ASN1. Kami telah melakukan ini secara implisit.
- Memverifikasi bahwa tanda terima ditandatangani oleh Apple. Ini dilakukan sebelum menguraikan tanda terima dan akan dirinci di bawah.
- Memeriksa apakah pengidentifikasi bundel yang disertakan dalam tanda terima sesuai dengan pengidentifikasi bundel Anda. Anda harus membuat hardcode pengenal bundel Anda, karena sepertinya tidak terlalu sulit untuk memodifikasi bundel aplikasi Anda dan menggunakan beberapa tanda terima lainnya.
- Memeriksa bahwa versi aplikasi yang termasuk dalam tanda terima sesuai dengan pengenal versi aplikasi Anda. Anda harus membuat hardcode versi aplikasi, untuk alasan yang sama yang ditunjukkan di atas.
- Periksa hash tanda terima untuk memastikan tanda terima sesuai dengan perangkat saat ini.
5 langkah dalam kode di tingkat tinggi, dari 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;
}
Mari kita telusuri langkah 2 dan 5.
Memverifikasi tanda tangan tanda terima
Kembali ketika kami mengekstraksi data, kami melirik verifikasi tanda tangan tanda terima. Tanda terima ditandatangani dengan Apple Inc. Root Certificate, yang dapat diunduh dari Apple Root Certificate Authority . Kode berikut mengambil wadah PKCS7 dan sertifikat root sebagai data dan memeriksa jika cocok:
+ (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;
}
Ini dilakukan kembali di awal, sebelum tanda terima diuraikan.
Memverifikasi hash tanda terima
Hash yang termasuk dalam tanda terima adalah SHA1 dari id perangkat, beberapa nilai buram yang termasuk dalam tanda terima dan bundel id.
Ini adalah bagaimana Anda akan memverifikasi tanda terima pada iOS. Dari 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];
}
Dan itulah intinya. Saya mungkin kehilangan sesuatu di sini atau di sana, jadi saya mungkin kembali ke posting ini nanti. Bagaimanapun, saya sarankan menelusuri kode lengkap untuk lebih jelasnya.