Voici un pas à pas de comment j'ai résolu ce problème dans mon achat dans l'application de la bibliothèque RMStore. Je vais vous expliquer comment vérifier une transaction, ce qui comprend la vérification de l'ensemble de réception.
D'un coup d'oeil
Obtenir de la réception et de vérifier la transaction. Si elle échoue, l'actualisation de la réception et essayez de nouveau. Cela rend le processus de vérification asynchrone comme l'actualisation de la réception est asynchrone.
De RMStoreAppReceiptVerificator:
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'obtention de la réception des données
La réception est en [[NSBundle mainBundle] appStoreReceiptURL]
et est en fait une PCKS7 conteneur. Je suce à la cryptographie j'ai donc utilisé OpenSSL pour ouvrir ce conteneur. D'autres ont apparemment fait uniquement avec des cadres du système.
L'ajout d'OpenSSL à votre projet n'est pas anodin. Le RMStore wiki devrait aider.
Si vous choisissez d'utiliser OpenSSL pour ouvrir la PKCS7 conteneur, votre code pourrait ressembler à ceci. De 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;
}
Nous allons entrer dans les détails de la vérification ultérieure.
L'obtention de la réception des champs
La réception est exprimé en ASN1 format. Il contient des renseignements généraux, certains domaines à des fins de vérification (nous y reviendrons) et des informations spécifiques applicables de chaque achat dans l'application.
Encore une fois, OpenSSL vient à la rescousse quand il s'agit de la lecture ASN1. De RMAppReceipt, en utilisant quelques méthodes d'aide:
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;
Obtenir les achats in-app
Chaque achat dans l'application est également en ASN1. Analyse il est très similaire à celle de l'analyse du général la réception de l'information.
De RMAppReceipt, en utilisant les mêmes méthodes d'aide:
[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;
}
}
}];
Il convient de noter que certains achats in-app, comme des consommables et non-renouvelables, les abonnements, n'apparaît qu'une fois dans la réception. Vous devez vérifier ces juste après l'achat (encore une fois, RMStore vous aide avec cette).
La vérification d'un coup d'oeil
Voilà, nous avons maintenant tous les champs de la réception et de tous ses achats dans l'application. Nous avons d'abord vérifier la réception elle-même, et il nous suffit alors de vérifier si la réception contient le produit de la transaction.
Ci-dessous est la méthode que nous avons rappelé au début. De 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;
}
Vérification de la réception
Vérification de la réception elle-même se résume à:
- Vérifier que le certificat est valide PKCS7 et ASN1. Nous l'avons fait implicitement déjà.
- Vérifier que la réception est signé par Apple. Cela a été fait avant l'analyse de la réception et sera détaillé ci-dessous.
- Vérifier que l'identifiant de lot inclus dans la réception correspond à votre identifiant de lot. Vous devez coder en dur l'identifiant de lot, comme il ne semble pas être très difficile de modifier votre app bundle et utiliser une autre réception.
- Vérifier que la version de l'application inclus dans la réception correspond à votre version de l'application de l'identificateur. Vous devez coder en dur la version de l'application, pour les mêmes raisons indiquées ci-dessus.
- De vérifier la réception de hachage pour s'assurer de la réception correspondent à l'appareil.
Les 5 étapes de code à un haut niveau, de 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;
}
Nous allons descendre dans les étapes 2 et 5.
La vérification de la signature
Lorsque nous avons extrait les données que nous avons jeté un coup d'oeil sur la réception de vérification de la signature. La réception est signé avec la société Apple Inc. Le Certificat racine, qui peut être téléchargé à partir de la Pomme de Certificat Racine de l'Autorité. Le code suivant le PKCS7 conteneur et le certificat racine que des données et vérifie si elles correspondent:
+ (BOOL)verifyPKCS7:(PKCS7*)container withCertificateData:(NSData*)certificateData
{ // Based on: https://developer.apple.com/library/ios/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 (), perhttp://www.openssl.org/docs/crypto/OpenSSL_add_all_algorithms.html
return result == verified;
}
Cela a été fait au tout début, avant la réception a été analysée.
Vérification de la réception de hachage
Le hachage inclus dans l'accusé de réception est un SHA1 de l'id de l'appareil, certains opaque la valeur est incluse dans la réception et l'id de lot.
Voici comment vous pouvez vérifier la réception de hachage sur iOS. De RMAppReceipt:
- (BOOL)verifyReceiptHash
{
// TODO: Getting the uuid in Mac is different. See: https://developer.apple.com/library/ios/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/ios/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];
}
Et c'est l'essentiel. J'ai peut-être raté quelque chose, ici ou là, je vais peut-être revenir à ce post plus tard. En tout cas, je recommande de la navigation sur le code complet pour plus de détails.