167 votes

Une solution complète pour valider un reçus en application et regrouper les recettes sur iOS 7 localement

J'ai lu beaucoup de docs et du code qui, en théorie, permettra de valider dans l'application et/ou faisceau de réception.

Étant donné que mes connaissances en SSL, certificats, cryptage, etc., est près de zéro, toutes les explications que j'ai lu, comme cette promesse, je l'ai trouvé difficile à comprendre.

Ils disent que les explications sont incomplètes parce que chaque personne doit trouver comment le faire, ou les pirates ont une tâche facile de créer un cracker app qui permet de reconnaître et d'identifier les modèles et les patch de l'application. OK, je suis d'accord avec ce que jusqu'à un certain point. Je pense qu'ils pourraient expliquer complètement comment le faire et mettre un avertissement en disant "modifier cette méthode", "modifier cette autre méthode", "obscurcir cette variable", "changer le nom de ceci et de cela", etc.

Peut quelque bonne âme y être assez aimable pour expliquer comment valider LOCALEMENT, paquet de recettes et d'achat dans l'application des recettes sur iOS 7 , comme je suis en cinq ans (ok, en faire 3), de haut en bas, clairement?

Merci!!!


Si vous avez une version de travail sur vos applications et de vos préoccupations, que les pirates de voir comment vous l'avez fait, il suffit de changer vos méthodes sensibles avant de les publier ici. Masquer des chaînes, modifier l'ordre des lignes, changer la façon de faire des boucles (de l'utiliser pour bloquer l'énumération et vice-versa) et des choses comme ça. Évidemment, chaque personne qui utilise le code qui peut être posté ici, est de faire la même chose, ne pas risquer d'être facilement piraté.

150voto

hpique Points 23090

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 à:

  1. Vérifier que le certificat est valide PKCS7 et ASN1. Nous l'avons fait implicitement déjà.
  2. 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.
  3. 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.
  4. 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.
  5. 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.

14voto

timschmitz Points 551

J’ai eu la chance avec RMStore : https://github.com/robotmedia/RMStore. Allez à la section « Référence verificators. »

Prograide.com

Prograide est une communauté de développeurs qui cherche à élargir la connaissance de la programmation au-delà de l'anglais.
Pour cela nous avons les plus grands doutes résolus en français et vous pouvez aussi poser vos propres questions ou résoudre celles des autres.

Powered by:

X