Hier ist eine exemplarische Vorgehensweise, wie ich dies in meiner In-App- Kaufbibliothek RMStore gelöst habe . Ich werde erklären, wie eine Transaktion überprüft wird, einschließlich der Überprüfung des gesamten Belegs.
Auf einen Blick
Holen Sie sich die Quittung und überprüfen Sie die Transaktion. Wenn dies fehlschlägt, aktualisieren Sie die Quittung und versuchen Sie es erneut. Dies macht den Überprüfungsprozess asynchron, da das Aktualisieren des Belegs asynchron ist.
Von 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];
}];
Abrufen der Belegdaten
Die Quittung ist in [[NSBundle mainBundle] appStoreReceiptURL]
und ist eigentlich ein PCKS7-Container. Ich lutsche an Kryptographie, also habe ich OpenSSL verwendet, um diesen Container zu öffnen. Andere haben es anscheinend nur mit System-Frameworks gemacht .
Das Hinzufügen von OpenSSL zu Ihrem Projekt ist nicht trivial. Das RMStore-Wiki sollte helfen.
Wenn Sie OpenSSL zum Öffnen des PKCS7-Containers verwenden, könnte Ihr Code folgendermaßen aussehen. Von 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;
}
Wir werden später auf die Details der Überprüfung eingehen.
Abrufen der Belegfelder
Die Quittung wird im ASN1-Format ausgedrückt. Es enthält allgemeine Informationen, einige Felder zu Überprüfungszwecken (wir werden später darauf zurückkommen) und spezifische Informationen zu jedem anwendbaren In-App-Kauf.
Auch hier hilft OpenSSL beim Lesen von ASN1. Von RMAppReceipt mit einigen Hilfsmethoden :
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;
In-App-Käufe abrufen
Jeder In-App-Kauf erfolgt ebenfalls in ASN1. Das Parsen ist dem Parsen der allgemeinen Beleginformationen sehr ähnlich.
In RMAppReceipt mit denselben Hilfsmethoden :
[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;
}
}
}];
Es ist zu beachten, dass bestimmte In-App-Käufe wie Verbrauchsmaterialien und nicht erneuerbare Abonnements nur einmal in der Quittung erscheinen. Sie sollten diese direkt nach dem Kauf überprüfen (auch hier hilft Ihnen RMStore).
Überprüfung auf einen Blick
Jetzt haben wir alle Felder von der Quittung und allen In-App-Käufen erhalten. Zuerst überprüfen wir die Quittung selbst und dann prüfen wir einfach, ob die Quittung das Produkt der Transaktion enthält.
Unten ist die Methode, die wir am Anfang zurückgerufen haben. Von 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;
}
Überprüfung der Quittung
Die Überprüfung der Quittung selbst läuft auf Folgendes hinaus:
- Überprüfen, ob die Quittung gültig ist PKCS7 und ASN1. Wir haben dies implizit bereits getan.
- Überprüfen, ob die Quittung von Apple signiert ist. Dies wurde vor dem Parsen der Quittung durchgeführt und wird unten detailliert beschrieben.
- Überprüfen Sie, ob die in der Quittung enthaltene Bundle-ID Ihrer Bundle-ID entspricht. Sie sollten Ihre Bundle-ID fest codieren, da es nicht sehr schwierig zu sein scheint, Ihr App-Bundle zu ändern und eine andere Quittung zu verwenden.
- Überprüfen Sie, ob die in der Quittung enthaltene App-Version Ihrer App-Versionskennung entspricht. Sie sollten die App-Version aus den oben genannten Gründen fest codieren.
- Überprüfen Sie den Beleg-Hash, um sicherzustellen, dass der Beleg dem aktuellen Gerät entspricht.
Die 5 Schritte im Code auf hoher Ebene von 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;
}
Lassen Sie uns einen Drilldown in die Schritte 2 und 5 durchführen.
Überprüfung der Quittungssignatur
Als wir die Daten extrahierten, warfen wir einen Blick auf die Bestätigung der Quittungssignatur. Die Quittung ist mit dem Apple Inc.-Stammzertifikat signiert, das von der Apple Root Certificate Authority heruntergeladen werden kann . Der folgende Code verwendet den PKCS7-Container und das Stammzertifikat als Daten und prüft, ob sie übereinstimmen:
+ (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;
}
Dies wurde zu Beginn durchgeführt, bevor die Quittung analysiert wurde.
Überprüfen des Quittungs-Hash
Der in der Quittung enthaltene Hash ist ein SHA1 der Geräte-ID, ein undurchsichtiger Wert in der Quittung und die Bundle-ID.
Auf diese Weise würden Sie den Beleg-Hash unter iOS überprüfen. Von 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];
}
Und das ist der Kern davon. Möglicherweise fehlt mir hier oder da etwas, sodass ich später auf diesen Beitrag zurückkommen kann. In jedem Fall empfehle ich, den vollständigen Code nach weiteren Details zu durchsuchen.