İşte bunu uygulama içi satın alma kitaplığım RMStore'da nasıl çözdüğümün bir yolu . Tüm makbuzun doğrulanmasını içeren bir işlemin nasıl doğrulanacağını açıklayacağım.
Bir bakışta
Makbuzu alın ve işlemi doğrulayın. Başarısız olursa, makbuzu yenileyin ve tekrar deneyin. Bu, makbuzu yenilemek eşzamansız olduğundan doğrulama işlemini eşzamansız hale getirir.
Gönderen 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];
}];
Makbuz verilerinin alınması
Makbuz içinde [[NSBundle mainBundle] appStoreReceiptURL]
ve aslında bir PCKS7 kapsayıcısıdır. Bu kapsayıcıyı açmak için OpenSSL kullandım. Diğerleri görünüşe göre bunu tamamen sistem çerçeveleri ile yapmışlardır .
Projenize OpenSSL eklemek önemsiz değil. RMStore wiki yardımcı olmalıdır.
PKCS7 kapsayıcısını açmak için OpenSSL kullanmayı seçerseniz, kodunuz şöyle görünebilir. Gönderen 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;
}
Doğrulamanın ayrıntılarına daha sonra gireceğiz.
Makbuz alanlarını alma
Makbuz ASN1 biçiminde ifade edilir. Genel bilgiler, doğrulama amaçlı bazı alanlar (daha sonra geleceğiz) ve her bir uygulama içi satın alma işlemiyle ilgili belirli bilgiler içerir.
Yine, ASSS1 okuma söz konusu olduğunda OpenSSL kurtarmaya geliyor. Gönderen RMAppReceipt , birkaç yardımcı yöntemler kullanılarak:
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;
Uygulama içi satın alma işlemlerini alma
Her uygulama içi satın alma işlemi de ASN1'de yapılır. Ayrıştırmak, genel makbuz bilgilerini ayrıştırmaktan çok benzer.
Gönderen RMAppReceipt , aynı yardımcı yöntemler kullanılarak:
[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;
}
}
}];
Sarf malzemeleri ve yenilenemeyen abonelikler gibi bazı uygulama içi satın alımların makbuzda yalnızca bir kez görüneceğine dikkat edilmelidir. Bunları satın aldıktan hemen sonra doğrulamanız gerekir (yine RMStore size bu konuda yardımcı olur).
Bir bakışta doğrulama
Şimdi tüm alanları makbuzdan ve tüm uygulama içi satın alımlarından aldık. İlk önce makbuzun kendisini doğrularız ve sonra makbuzun işlemin ürününü içerip içermediğini kontrol ederiz.
Başlangıçta geri çağırdığımız yöntem aşağıdadır. Gönderen 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;
}
Makbuzu doğrulama
Makbuzun kendisinin doğrulanması aşağıdakilerle ilgilidir:
- Makbuzun geçerli PKCS7 ve ASN1 olup olmadığını kontrol edin. Bunu dolaylı olarak yaptık.
- Makbuzun Apple tarafından imzalandığını doğrulama. Bu makbuz ayrıştırılmadan önce yapıldı ve aşağıda ayrıntılandırılacaktır.
- Makbuzdaki paket kimliğinin paket kimliğinize uygun olup olmadığını kontrol edin. Uygulama paketinizi değiştirmek ve başka bir makbuz kullanmak çok zor görünmediğinden, paket tanımlayıcınızı kodlamalısınız.
- Makbuzdaki uygulama sürümünün uygulama sürümü tanımlayıcınıza karşılık geldiğini kontrol edin. Yukarıda belirtilen nedenlerle uygulama sürümünü kodlamalısınız.
- Makbuzun mevcut cihaza karşılık geldiğinden emin olmak için makbuz karmasını kontrol edin.
RMStoreAppReceiptVerificator'dan yüksek düzeyde kodda 5 adım :
- (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;
}
2. ve 5. adımlara geçelim.
Makbuz imzasını doğrulama
Verileri çıkardığımızda makbuz imzası doğrulamasına baktık. Makbuz, Apple Root Certificate Authority'den indirilebilen Apple Inc. Root Certificate ile imzalanmıştır . Aşağıdaki kod, PKCS7 kapsayıcısını ve kök sertifikayı veri olarak alır ve eşleşip eşleşmediğini denetler:
+ (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;
}
Bu, başlangıçta, makbuz ayrıştırılmadan önce yapıldı.
Makbuz karmasını doğrulama
Makbuzdaki karma, cihaz kimliğinin bir SHA1'i, makbuzda bulunan bazı opak değer ve paket kimliğidir.
İOS'ta makbuz karmasını bu şekilde doğrularsınız. Gönderen 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];
}
Ve bu onun özü. Burada veya orada bir şeyler eksik olabilir, bu yüzden daha sonra bu gönderiye geri dönebilirim. Her durumda, daha fazla ayrıntı için kodun tamamına göz atmanızı öneririm.