Une solution complète pour VALIDER LOCALEMENT un reçu de l'application et des reçus groupés sur iOS 7

J'ai lu beaucoup de docs et de code qui, en théorie, vont valider un reçu in-app et / ou bundle.

Étant donné que ma connaissance du SSL, des certificates, du encryption, etc., est presque nulle, toutes les explications que j'ai lues, comme celle prometteuse , m'ont paru difficiles à comprendre.

Ils disent que les explications sont incomplètes parce que chaque personne doit comprendre comment le faire, ou les pirates auront un travail facile à créer une application de piratage qui peut reconnaître et identifier les templates et patcher l'application. OK, je suis d'accord avec ça 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 cela", etc.

Est-ce qu'une bonne âme peut être assez aimable pour expliquer comment valider LOCALEMENT, regrouper les reçus et les reçus d'achat embeddeds sur iOS 7 depuis que j'ai cinq ans (ok, fais-en 3), de haut en bas, clairement?

Merci!!!


Si vous avez une version qui fonctionne sur vos applications et vos préoccupations sont que les pirates vont voir comment vous l'avez fait, il suffit de changer vos methods sensibles avant de publier ici. Obscurcir les strings, changer l'ordre des lignes, changer la façon dont vous faites des loops (d'utiliser pour bloquer l'énumération et vice-versa) et des choses comme ça. Évidemment, toute personne qui utilise le code qui peut être posté ici, doit faire la même chose, pour ne pas risquer d'être facilement piraté.

    Voici une description de la façon dont j'ai résolu ce problème dans ma bibliothèque d'achat embeddede à l'application RMStore . Je vais vous expliquer comment vérifier une transaction, ce qui comprend la vérification de l'set du reçu.

    En un coup d'oeil

    Obtenez le reçu et vérifiez la transaction. En cas d'échec, actualisez le reçu et réessayez. Cela rend le process de vérification asynchronous car l'actualisation du ticket est asynchronous.

    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]; }]; 

    Obtenir datatables de réception

    Le reçu est dans [[NSBundle mainBundle] appStoreReceiptURL] et est en fait un conteneur PCKS7. Je crains la cryptography, j'ai donc utilisé OpenSSL pour ouvrir ce conteneur. D'autres l'ont apparemment fait uniquement avec des frameworks de système .

    L'ajout d'OpenSSL à votre projet n'est pas sortingvial. Le wiki RMStore devrait aider.

    Si vous choisissez d'utiliser OpenSSL pour ouvrir le conteneur PKCS7, votre code pourrait ressembler à ceci. De RMAppReceipt :

     + (NSData*)dataFromPKCS7Path:(NSSsortingng*)path { const char *cpath = [[path ssortingngByStandardizingPath] 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 *certificateeURL = [[NSBundle mainBundle] URLForResource:@"AppleIncRootCertificate" withExtension:@"cer"]; NSData *certificateeData = [NSData dataWithContentsOfURL:certificateeURL]; if ([self verifyPKCS7:p7 withCertificateData:certificateeData]) { 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 entrerons dans les détails de la vérification plus tard.

    Obtenir les champs de réception

    Le reçu est exprimé en format ASN1. Il contient des informations générales, certains champs à des fins de vérification (nous y reviendrons plus tard) et des informations spécifiques sur chaque achat in-app applicable.

    Encore une fois, OpenSSL vient à la rescousse quand il s'agit de lire ASN1. À partir de RMAppReceipt , en utilisant quelques methods d'assistance:

     NSMutableArray *purchases = [NSMutableArray array]; [RMAppReceipt enumerateASN1Atsortingbutes: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 = RMASN1ReadUTF8Ssortingng(&s, length); break; case RMAppReceiptASN1TypeAppVersion: _appVersion = RMASN1ReadUTF8Ssortingng(&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 = RMASN1ReadUTF8Ssortingng(&s, length); break; case RMAppReceiptASN1TypeExpirationDate: { NSSsortingng *ssortingng = RMASN1ReadIA5SSsortingng(&s, length); _expirationDate = [RMAppReceipt formatRFC3339Ssortingng:ssortingng]; break; } } }]; _inAppPurchases = purchases; 

    Obtenir les achats via l'application

    Chaque achat embedded est également en ASN1. L'parsing est très similaire à l'parsing des informations de réception générales.

    À partir de RMAppReceipt , en utilisant les mêmes methods d'assistance:

     [RMAppReceipt enumerateASN1Atsortingbutes: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 = RMASN1ReadUTF8Ssortingng(&p, length); break; case RMAppReceiptASN1TypeTransactionIdentifier: _transactionIdentifier = RMASN1ReadUTF8Ssortingng(&p, length); break; case RMAppReceiptASN1TypePurchaseDate: { NSSsortingng *ssortingng = RMASN1ReadIA5SSsortingng(&p, length); _purchaseDate = [RMAppReceipt formatRFC3339Ssortingng:ssortingng]; break; } case RMAppReceiptASN1TypeOriginalTransactionIdentifier: _originalTransactionIdentifier = RMASN1ReadUTF8Ssortingng(&p, length); break; case RMAppReceiptASN1TypeOriginalPurchaseDate: { NSSsortingng *ssortingng = RMASN1ReadIA5SSsortingng(&p, length); _originalPurchaseDate = [RMAppReceipt formatRFC3339Ssortingng:ssortingng]; break; } case RMAppReceiptASN1TypeSubscriptionExpirationDate: { NSSsortingng *ssortingng = RMASN1ReadIA5SSsortingng(&p, length); _subscriptionExpirationDate = [RMAppReceipt formatRFC3339Ssortingng:ssortingng]; break; } case RMAppReceiptASN1TypeWebOrderLineItemID: _webOrderLineItemID = RMASN1ReadInteger(&p, length); break; case RMAppReceiptASN1TypeCancellationDate: { NSSsortingng *ssortingng = RMASN1ReadIA5SSsortingng(&p, length); _cancellationDate = [RMAppReceipt formatRFC3339Ssortingng:ssortingng]; break; } } }]; 

    Il est à noter que certains achats embeddeds, tels que les consommables et les abonnements non renouvelables, n'apparaîtront qu'une seule fois dans le reçu. Vous devriez les vérifier juste après l'achat (encore une fois, RMStore vous aide avec ceci).

    Vérification en un coup d'œil

    Nous avons maintenant tous les champs du reçu et tous ses achats embeddeds. Nous vérifions d'abord le reçu lui-même, puis nous vérifions simplement si le reçu contient le produit de la transaction.

    Voici la méthode que nous avons callbackée 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:NSLocalizedSsortingng(@"The app receipt failed verification", @"")]; return NO; } SKPayment *payment = transaction.payment; const BOOL transactionVerified = [receipt containsInAppPurchaseOfProductIdentifier:payment.productIdentifier]; if (!transactionVerified) { [self failWithBlock:failureBlock message:NSLocalizedSsortingng(@"The app receipt doest not contain the given product", @"")]; return NO; } if (successBlock) { successBlock(); } return YES; } 

    Vérification du reçu

    La vérification du reçu se résume à:

    1. Vérifier que le reçu est valide PKCS7 et ASN1. Nous l'avons déjà fait implicitement.
    2. Vérifier que le reçu est signé par Apple. Cela a été fait avant d'parsingr le reçu et sera détaillé ci-dessous.
    3. Vérifier que l'identifiant de package inclus dans le reçu correspond à votre identifiant de package. Vous devriez coder en dur votre identifiant de bundle, car il ne semble pas très difficile de modifier votre bundle d'applications et d'utiliser d'autres reçus.
    4. Vérifier que la version de l'application incluse dans le reçu correspond à l'identifiant de la version de l'application. Vous devez coder en dur la version de l'application, pour les mêmes raisons que celles indiquées ci-dessus.
    5. Vérifiez le hachage de la facture pour vous assurer que le reçu correspond à l'appareil actuel.

    Les 5 étapes du code à un niveau élevé, à partir de RMStoreAppReceiptVerificator :

     - (BOOL)verifyAppReceipt:(RMAppReceipt*)receipt { // Steps 1 & 2 were done while parsing the receipt if (!receipt) return NO; // Step 3 if (![receipt.bundleIdentifier isEqualToSsortingng:self.bundleIdentifier]) return NO; // Step 4 if (![receipt.appVersion isEqualToSsortingng:self.bundleVersion]) return NO; // Step 5 if (![receipt verifyReceiptHash]) return NO; return YES; } 

    Descendons aux étapes 2 et 5.

    Vérification de la signature du reçu

    À l'époque où nous avons extrait datatables, nous avons jeté un coup d'œil sur la vérification de la signature des reçus. Le reçu est signé avec le certificate racine d'Apple Inc., qui peut être téléchargé à partir de l' autorité de certificateion Apple Root . Le code suivant prend le conteneur PKCS7 et le certificate racine en tant que données et vérifie s'ils correspondent:

     + (BOOL)verifyPKCS7:(PKCS7*)container withCertificateData:(NSData*)certificateeData { // 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 *certificateeBytes = (uint8_t *)(certificateeData.bytes); X509 *certificatee = d2i_X509(NULL, &certificateeBytes, (long)certificateeData.length); if (certificatee) { X509_STORE_add_cert(store, certificatee); BIO *payload = BIO_new(BIO_s_mem()); result = PKCS7_verify(container, NULL, store, NULL, payload, 0); BIO_free(payload); X509_free(certificatee); } } 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; } 

    Cela a été fait au début, avant que le reçu a été analysé.

    Vérification du hachage de la facture

    Le hachage inclus dans le reçu est un SHA1 de l'identifiant de l'appareil, une certaine valeur opaque étant incluse dans le reçu et l'identifiant du package.

    C'est ainsi que vous vérifieriez le hachage de la facture sur iOS. De 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]; } 

    Et c'est l'essentiel. Je pourrais manquer quelque chose ici ou là, donc je pourrais revenir à ce post plus tard. Dans tous les cas, je recommand de parcourir le code complet pour plus de détails.

    Je suis surpris que personne n'ait mentionné Receigen ici. C'est un outil qui génère automatiquement un code de validation de reçu obfusqué, un code différent à chaque fois; il supporte à la fois l'interface graphique et le fonctionnement en command line. Hautement recommandé.

    (Non affilié avec Receigen, juste un user heureux.)

    J'utilise un Rakefile comme celui-ci pour réexécuter automatiquement Receigen (car il doit être fait à chaque changement de version) quand je tape rake receigen :

     desc "Regenerate App Store Receipt validation code using Receigen app (which must be already installed)" task :receigen do # TODO: modify these to match your app bundle_id = 'com.example.YourBundleIdentifierHere' output_file = File.join(__dir__, 'SomeDir/ReceiptValidation.h') version = PList.get(File.join(__dir__, 'YourProjectFolder/Info.plist'), 'CFBundleVersion') command = %Q</Applications/Receigen.app/Contents/MacOS/Receigen --identifier #{bundle_id} --version #{version} --os ios --prefix ReceiptValidation --success callblock --failure callblock> puts "#{command} > #{output_file}" data = `#{command}` File.open(output_file, 'w') { |f| f.write(data) } end module PList def self.get file_name, key if File.read(file_name) =~ %r!<key>#{Regexp.escape(key)}</key>\s*<ssortingng>(.*?)</ssortingng>! $1.ssortingp else nil end end end 

    Bonjour C'est la version 3 de Swift pour la validation du reçu d'achat in-app …

    Appelez la fonction receiptValidation() partir de votre AppDelegate ou de l'endroit où vous voulez tout cela.

      func receiptValidation() { if let appStoreReceiptURL = Bundle.main.appStoreReceiptURL, FileManager.default.fileExists(atPath: appStoreReceiptURL.path) { do { let receiptData = try Data(contentsOf: appStoreReceiptURL, options: .alwaysMapped) let receiptSsortingng = receiptData.base64EncodedSsortingng(options: []) let dict = ["receipt-data" : receiptSsortingng, "password" : "**************************"] as [Ssortingng : Any] do { let jsonData = try JSONSerialization.data(withJSONObject: dict, options: .prettyPrinted) //This Url for original Account //let url : Ssortingng = "https://buy.itunes.apple.com/verifyReceipt" //This Url for Sandbox Testing Account let url : Ssortingng = "https://sandbox.itunes.apple.com/verifyReceipt" if let sandboxURL = Foundation.URL(ssortingng:url) { var request = URLRequest(url: sandboxURL) request.httpMethod = "POST" request.httpBody = jsonData let session = URLSession(configuration: URLSessionConfiguration.default) let task = session.dataTask(with: request) { data, response, error in if let receivedData = data, let httpResponse = response as? HTTPURLResponse, error == nil, httpResponse.statusCode == 200 { do { if let jsonResponse = try JSONSerialization.jsonObject(with: receivedData, options: JSONSerialization.ReadingOptions.mutableContainers) as? Dictionary<Ssortingng, AnyObject> { if let expirationDate: NSDate = self.expirationDateFromResponse(jsonResponse: jsonResponse as NSDictionary) { let currentDate = self.getCurrentLocalDateApp() if currentDate > expirationDate as Date { self.downgrade("1") }else{ } } } else { } } catch { } }else { print("Error=\(Ssortingng(describing: error))") } } task.resume() } else { } } catch { } } catch { } } } 

    Maintenant, nous avons une autre fonction get une date à partir de la réception expirationDateFromResponse() . Cette fonction serait dans le même controller ou dans l' AppDelegate

     func expirationDateFromResponse(jsonResponse: NSDictionary) -> NSDate? { if let receiptInfo: NSArray = jsonResponse["latest_receipt_info"] as? NSArray { let lastReceipt = receiptInfo.lastObject as! NSDictionary let formatter = DateFormatter() formatter.dateFormat = "yyyy-MM-dd HH:mm:ss VV" let expirationDate: NSDate = formatter.date(from: lastReceipt["expires_date"] as! Ssortingng) as NSDate! formatter.dateStyle = .medium let ssortingngOutput = formatter.ssortingng(from: expirationDate as Date) formatter.dateFormat = "yyyy-MM-dd HH:mm:ss" let date = formatter.ssortingng(from: expirationDate as Date) print("Date=\(date)") self.iosNextBillingDateEntry(date) UserDefaults.standard.set(ssortingngOutput, forKey: "PLAN_EXP_DATE") return expirationDate } else { return nil } } 

    Maintenant, nous avons une autre fonction pour get la date locale si cela vous est demandé.

     func getCurrentLocalDateApp()-> Date { var now = Date() var nowComponents = DateComponents() let calendar = Calendar.current nowComponents.year = (Calendar.current as NSCalendar).component(NSCalendar.Unit.year, from: now) nowComponents.month = (Calendar.current as NSCalendar).component(NSCalendar.Unit.month, from: now) nowComponents.day = (Calendar.current as NSCalendar).component(NSCalendar.Unit.day, from: now) nowComponents.hour = (Calendar.current as NSCalendar).component(NSCalendar.Unit.hour, from: now) nowComponents.minute = (Calendar.current as NSCalendar).component(NSCalendar.Unit.minute, from: now) nowComponents.second = (Calendar.current as NSCalendar).component(NSCalendar.Unit.second, from: now) nowComponents.timeZone = TimeZone(abbreviation: "VV") now = calendar.date(from: nowComponents)! return now } 

    Le mot de passe que vous obtiendrez de l'Apple Store. https://developer.apple.com ouvrir ce lien cliquer sur

    • Account tab
    • Do Sign in
    • Open iTune Connect
    • Open My App
    • Open Feature Tab
    • Open In App Purchase
    • Click at the right side on 'View Shared Secret'
    • At the bottom you will get a secrete key

    Copiez cette key et collez-la dans le champ du mot de passe.

    J'espère que cela aidera pour tous ceux qui veulent cela en version rapide.