ios - आईओएस 7 पर इन-ऐप रसीदें और बंडल रसीदों को वैध रूप से सत्यापित करने का एक पूरा समाधान



iphone in-app-purchase (2)

मुझे आश्चर्य है कि यहां कोई भी रेसीजन का उल्लेख नहीं करता है। यह एक ऐसा उपकरण है जो स्वचालित रूप से obfuscated रसीद सत्यापन कोड उत्पन्न करता है, हर बार एक अलग; यह जीयूआई और कमांड लाइन ऑपरेशन दोनों का समर्थन करता है। अत्यधिक सिफारिशित।

(रिसीजन से संबद्ध नहीं, बस एक खुश उपयोगकर्ता।)

जब मैं 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*<string>(.*?)</string>!
      $1.strip
    else
      nil
    end
  end
end

मैंने बहुत सारे दस्तावेज़ और कोड पढ़े हैं जो सिद्धांत में इन-एप और / या बंडल रसीद को मान्य करेंगे।

यह देखते हुए कि एसएसएल, सर्टिफिकेट, एन्क्रिप्शन इत्यादि का मेरा ज्ञान लगभग शून्य है, मैंने जो स्पष्टीकरण पढ़ा है, वैसे ही , इस वादा करने वाले एक को , मुझे समझना मुश्किल हो गया है।

वे कहते हैं कि स्पष्टीकरण अधूरे हैं क्योंकि प्रत्येक व्यक्ति को यह पता लगाना होगा कि इसे कैसे किया जाए, या हैकर्स के पास एक क्रैकर ऐप बनाने में आसान काम होगा जो पैटर्न को पहचान और पहचान सकता है और एप्लिकेशन को पैच कर सकता है। ठीक है, मैं इसके साथ एक निश्चित बिंदु से सहमत हूं। मुझे लगता है कि वे पूरी तरह से समझा सकते हैं कि यह कैसे करें और "इस विधि को संशोधित करें" कहकर चेतावनी दें, "इस अन्य विधि को संशोधित करें", "इस चर को obfuscate", "इसका नाम बदलें और" आदि।

क्या कुछ अच्छी आत्मा वहां बता सकती है कि आईओएस 7 पर स्थानीय रूप से वैध, बंडल रसीदें और इन-ऐप खरीद रसीदों को कैसे सत्यापित किया जाए, क्योंकि मैं पांच साल का हूं (ठीक है, इसे 3 बनाओ), ऊपर से नीचे तक स्पष्ट रूप से?

धन्यवाद!!!

यदि आपके पास अपने ऐप्स पर एक संस्करण काम कर रहा है और आपकी चिंताओं यह है कि हैकर्स देखेंगे कि आपने यह कैसे किया है, तो यहां प्रकाशित करने से पहले बस अपने संवेदनशील तरीकों को बदलें। स्ट्रिंग्स को ऑब्फस्केट करें, लाइनों के क्रम को बदलें, जिस तरह से आप लूप करते हैं (गणित को अवरुद्ध करने के लिए उपयोग करके और इसके विपरीत) और उस तरह की चीजें बदलें। जाहिर है, यहां पोस्ट किए जा सकने वाले कोड का उपयोग करने वाले प्रत्येक व्यक्ति को एक ही चीज़ करना है, आसानी से हैक होने का जोखिम नहीं है।


Answer #1

यहां मेरी इन-ऐप खरीद लाइब्रेरी RMStore में इसे हल करने का RMStore । मैं समझाऊंगा कि लेनदेन को कैसे सत्यापित किया जाए, जिसमें संपूर्ण रसीद को सत्यापित करना शामिल है।

एक नजर में

रसीद प्राप्त करें और लेनदेन की पुष्टि करें। यदि यह विफल रहता है, तो रसीद रीफ्रेश करें और पुनः प्रयास करें। यह सत्यापन प्रक्रिया को अतुल्यकालिक बनाता है क्योंकि रसीद को रीफ्रेश करना असीमित है।

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] और वास्तव में एक पीसीकेएस 7 कंटेनर है। मैं क्रिप्टोग्राफी में चूसता हूं इसलिए मैंने इस कंटेनर को खोलने के लिए ओपनएसएसएल का इस्तेमाल किया। दूसरों ने स्पष्ट रूप से सिस्टम ढांचे के साथ इसे पूरी तरह से किया है

अपनी परियोजना में ओपनएसएसएल जोड़ना तुच्छ नहीं है। आरएमएसटोर विकी मदद करनी चाहिए।

यदि आप पीकेसीएस 7 कंटेनर खोलने के लिए ओपनएसएसएल का उपयोग करना चुनते हैं, तो आपका कोड इस तरह दिख सकता है। 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;
}

हम बाद में सत्यापन के विवरण में शामिल होंगे।

रसीद फ़ील्ड प्राप्त करना

रसीद एएसएन 1 प्रारूप में व्यक्त की जाती है। इसमें सामान्य जानकारी होती है, सत्यापन उद्देश्यों के लिए कुछ फ़ील्ड (हम बाद में आते हैं) और प्रत्येक लागू इन-एप खरीद की विशिष्ट जानकारी।

फिर, ओएसएसएस 1 पढ़ने के लिए ओपनएसएसएल बचाव के लिए आता है। 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;

इन-ऐप खरीदारी प्राप्त करना

प्रत्येक इन-एप खरीद एएसएन 1 में भी है। इसे सामान्य रसीद जानकारी को पार्स करने से बहुत समान है।

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

यह ध्यान दिया जाना चाहिए कि कुछ इन-एप खरीद, जैसे कि उपभोग्य सामग्रियों और गैर नवीकरणीय सदस्यता, रसीद में केवल एक बार दिखाई देंगी। आपको खरीद के बाद इन अधिकारों को सत्यापित करना चाहिए (फिर से, आरएमएसटोर आपको इससे मदद करता है)।

एक नज़र में सत्यापन

अब हमें रसीद और उसके सभी इन-ऐप खरीद से सभी फ़ील्ड मिल गए हैं। सबसे पहले हम रसीद को स्वयं सत्यापित करते हैं, और फिर हम जांच करते हैं कि रसीद में लेनदेन का उत्पाद शामिल है या नहीं।

नीचे वह तरीका है जिसे हमने शुरुआत में वापस बुलाया था। 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;
}

रसीद की पुष्टि

रसीद को सत्यापित करना स्वयं को उबालता है:

  1. यह जांचना कि रसीद वैध पीकेसीएस 7 और एएसएन 1 है। हमने यह पहले से ही किया है।
  2. यह सत्यापित करना कि रसीद पर ऐप्पल द्वारा हस्ताक्षर किए गए हैं। यह रसीद को पार्स करने से पहले किया गया था और नीचे विस्तृत किया जाएगा।
  3. यह जांच कर रहा है कि रसीद में शामिल बंडल पहचानकर्ता आपके बंडल पहचानकर्ता से मेल खाता है। आपको अपने बंडल पहचानकर्ता को हार्डकोड करना चाहिए, क्योंकि ऐसा लगता है कि आपके ऐप बंडल को संशोधित करना और कुछ अन्य रसीदों का उपयोग करना बहुत मुश्किल नहीं लगता है।
  4. यह जांचना कि रसीद में शामिल ऐप संस्करण आपके ऐप संस्करण पहचानकर्ता से मेल खाता है। उपर्युक्त संकेतों के लिए आपको ऐप संस्करण को हार्डकोड करना चाहिए।
  5. यह सुनिश्चित करने के लिए रसीद हैश की जांच करें कि रसीद वर्तमान डिवाइस से मेल खाती है।

RMStoreAppReceiptVerificator से उच्च स्तर पर कोड में 5 चरण:

- (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 में ड्रिल-डाउन करें।

रसीद हस्ताक्षर की पुष्टि

वापस जब हमने डेटा निकाला तो हमने रसीद हस्ताक्षर सत्यापन पर देखा। रसीद को ऐप्पल इंक रूट सर्टिफिकेट के साथ हस्ताक्षरित किया गया है, जिसे ऐप्पल रूट सर्टिफिकेट अथॉरिटी से डाउनलोड किया जा सकता है। निम्न कोड 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 है, रसीद और बंडल आईडी में शामिल कुछ अपारदर्शी मूल्य।

इस प्रकार आप आईओएस पर रसीद हैश को सत्यापित करेंगे। 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];
}

और यह इसका सारांश है। मुझे यहां या वहां कुछ याद आ रहा है, इसलिए मैं बाद में इस पोस्ट पर वापस आ सकता हूं। किसी भी मामले में, मैं अधिक जानकारी के लिए पूर्ण कोड ब्राउज़ करने की सलाह देता हूं।





storekit