นี่เป็นคำแนะนำว่าฉันจะแก้ไขปัญหานี้ได้อย่างไรในห้องสมุดการซื้อในแอปRMS ของฉัน ฉันจะอธิบายวิธีการตรวจสอบการทำธุรกรรมซึ่งรวมถึงการตรวจสอบใบเสร็จรับเงินทั้งหมด
อย่างรวดเร็ว
รับใบเสร็จและตรวจสอบการทำธุรกรรม ถ้ามันล้มเหลวรีเฟรชใบเสร็จรับเงินและลองอีกครั้ง สิ่งนี้ทำให้กระบวนการตรวจสอบแบบอะซิงโครนัสเป็นการรีเฟรชการรับแบบอะซิงโครนัส
จาก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];
}];
รับข้อมูลใบเสร็จรับเงิน
ใบเสร็จรับเงินอยู่ใน[[NSBundle mainBundle] appStoreReceiptURL]
และเป็นจริงคอนเทนเนอร์ PCKS7 ฉันดูดที่เข้ารหัสดังนั้นฉันใช้ OpenSSL เพื่อเปิดคอนเทนเนอร์นี้ เห็นได้ชัดว่าคนอื่น ๆ ได้ทำมันได้อย่างหมดจดด้วยกรอบระบบ
การเพิ่ม OpenSSL ให้กับโครงการของคุณไม่สำคัญ RMStore วิกิพีเดียจะช่วยให้
หากคุณเลือกใช้ 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 มันมีข้อมูลทั่วไปบางสาขาเพื่อการตรวจสอบ (เราจะมาถึงในภายหลัง) และข้อมูลเฉพาะของการซื้อในแอพที่ใช้บังคับแต่ละรายการ
อีกครั้ง OpenSSL มาช่วยเหลือเมื่อมันมาอ่าน ASN1 จาก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 เราได้ทำสิ่งนี้โดยปริยายแล้ว
- การตรวจสอบว่าใบเสร็จรับเงินนั้นลงนามโดย Apple สิ่งนี้ทำก่อนการแยกวิเคราะห์ใบเสร็จและจะมีรายละเอียดด้านล่าง
- การตรวจสอบว่าตัวระบุบันเดิลที่รวมอยู่ในใบเสร็จนั้นตรงกับตัวระบุบันเดิลของคุณ คุณควร hardcode ตัวบ่งชี้กลุ่มของคุณเนื่องจากมันไม่ได้เป็นเรื่องยากมากที่จะแก้ไขกลุ่มแอพของคุณและใช้ใบเสร็จรับเงินอื่น ๆ
- การตรวจสอบว่าเวอร์ชันของแอพที่รวมอยู่ในใบเสร็จรับเงินนั้นตรงกับตัวระบุเวอร์ชันแอปของคุณ คุณควรเข้ารหัสเวอร์ชันแอปด้วยเหตุผลเดียวกันกับที่ระบุไว้ด้านบน
- ตรวจสอบแฮชใบเสร็จรับเงินเพื่อให้แน่ใจว่าใบเสร็จนั้นสอดคล้องกับอุปกรณ์ปัจจุบัน
โค้ด 5 ขั้นตอนในระดับสูงจาก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;
}
มาเจาะลึกในขั้นตอนที่ 2 และ 5
การตรวจสอบลายเซ็นใบเสร็จรับเงิน
ย้อนกลับไปเมื่อเราดึงข้อมูลที่เรามองผ่านการตรวจสอบลายเซ็นใบเสร็จ ใบเสร็จรับเงินมีการเซ็นสัญญากับแอปเปิ้ลอิงค์ใบรับรองหลักซึ่งสามารถดาวน์โหลดได้จากแอปเปิ้ล Root Certificate Authority รหัสต่อไปนี้ใช้คอนเทนเนอร์ PKCS7 และใบรับรองรูทเป็นข้อมูลและตรวจสอบว่าตรงกันหรือไม่:
+ (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;
}
สิ่งนี้ทำเสร็จตั้งแต่ต้นก่อนที่จะมีการแยกวิเคราะห์ใบเสร็จ
การตรวจสอบแฮชใบเสร็จรับเงิน
แฮชที่รวมอยู่ในใบเสร็จคือ SHA1 ของรหัสอุปกรณ์ค่าทึบแสงบางอย่างรวมอยู่ในใบเสร็จและรหัสบันเดิล
นี่คือวิธีที่คุณจะตรวจสอบความถูกต้องของใบเสร็จรับเงินบน iOS จาก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];
}
และนั่นคือส่วนสำคัญของมัน ฉันอาจจะหายไปบางสิ่งบางอย่างที่นี่หรือที่นั่นดังนั้นฉันอาจกลับมาที่โพสต์นี้ในภายหลัง ในกรณีใด ๆ ฉันขอแนะนำให้ดูรหัสที่สมบูรณ์สำหรับรายละเอียดเพิ่มเติม