Полное решение для ЛОКАЛЬНОЙ проверки поступлений в приложении и пакетов квитанций на iOS 7

Я прочитал много документов и кода, которые теоретически подтвердят получение в приложении и/или пакет.

Учитывая, что мои знания SSL, сертификатов, шифрования и т.д. почти равны нулю, все объяснения, которые я прочитал, похожи на этот многообещающий вариант, у меня есть было трудно понять.

Говорят, что объяснения неполны, потому что каждый человек должен выяснить, как это сделать, или у хакеров будет легкая работа, создающая приложение для взлома, которое может распознавать и идентифицировать шаблоны и исправлять приложение. Хорошо, я согласен с этим до определенного момента. Я думаю, что они могли бы полностью объяснить, как это сделать, и предупредили, что "измените этот метод", "измените этот другой метод", "обфускайте эту переменную", "измените имя этого и того" и т.д.

Может ли какая-то хорошая душа быть достаточно любезной, чтобы объяснить , как ЛОКАЛЬНО проверять, связывать квитанции и покупки в приложении на iOS 7, поскольку мне пять лет (ок, сделайте это 3), сверху вниз, ясно?

Спасибо!!!


Если у вас есть версия, работающая над вашими приложениями, и ваши проблемы состоят в том, что хакеры увидят, как вы это сделали, просто измените свои чувствительные методы перед публикацией здесь. Обфускайте строки, измените порядок строк, измените способ выполнения циклов (от использования для блокировки перечисления и наоборот) и тому подобное. Очевидно, что каждый человек, который использует код, который может быть размещен здесь, должен делать то же самое, а не рисковать легко взломать.

Ответ 1

Вот обзор того, как я решил это в своей библиотеке покупок в приложении RMStore. Я объясню, как проверить транзакцию, которая включает проверку всей квитанции.

Краткий обзор

Получить квитанцию ​​и проверить транзакцию. Если это не удается, обновите квитанцию ​​и повторите попытку. Это делает процесс проверки асинхронным, так как обновление квитанции является асинхронным.

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

Получение данных квитанции

Квитанция находится в [[NSBundle mainBundle] appStoreReceiptURL] и фактически является контейнером PCKS7. Я сосать криптографию, поэтому я использовал OpenSSL, чтобы открыть этот контейнер. Другие, по-видимому, делали это исключительно с системными рамками.

Добавление OpenSSL к вашему проекту не является тривиальным. RMStore wiki должен помочь.

Если вы решили использовать OpenSSL для открытия контейнера PKCS7, ваш код может выглядеть так. Из 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;
}

Далее мы рассмотрим детали проверки.

Получение полей квитанций

Квитанция выражается в формате ASN1. Он содержит общую информацию, некоторые поля для целей проверки (мы придем к этому позже) и конкретную информацию о каждой применимой покупке в приложении.

Опять же, OpenSSL приходит на помощь, когда дело доходит до чтения ASN1. Из 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;

Получение покупок в приложении

Каждая покупка в приложении также находится в ASN1. Анализ очень похож, чем анализ общей информации о получении.

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

Следует отметить, что некоторые покупки в приложении, такие как расходные материалы и невозобновляемые подписки, будут отображаться только один раз в квитанции. Вы должны проверить это сразу после покупки (опять же, RMStore поможет вам в этом).

Проверка с первого взгляда

Теперь мы получили все поля из квитанции и все его покупки в приложении. Сначала мы проверяем сам квитанцию, а затем мы просто проверяем, содержит ли квитанция продукт транзакции.

Ниже приведен метод, который мы перезвонили в начале. Из 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;
}

Проверка получения

Проверка самой квитанции сводится к:

  • Проверка того, что квитанция действительна PKCS7 и ASN1. Мы уже сделали это неявно.
  • Проверка того, что квитанция подписана Apple. Это было сделано до разбора чека и будет подробно описано ниже.
  • Проверка того, что идентификатор пакета, включенный в квитанцию, соответствует идентификатору вашего пакета. Вы должны жестко закодировать свой идентификатор пакета, так как не очень сложно изменить комплект приложений и использовать другую квитанцию.
  • Проверка того, что версия приложения, включенная в квитанцию, соответствует идентификатору версии вашего приложения. Вы должны жестко указать версию приложения по тем же причинам, что указаны выше.
  • Проверьте хэш хэш, чтобы убедиться, что квитанция соответствует текущему устройству.

5 шагов в коде на высоком уровне, 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;
}

Перейдите к шагам 2 и 5.

Проверка подписи квитанции

Назад, когда мы извлекли данные, которые мы просмотрели по проверке подписи квитанции. Квитанция подписывается с корневым сертификатом Apple Inc., который можно загрузить из Apple Root Certificate Authority. Следующий код принимает контейнер 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 идентификатора устройства, некоторое непрозрачное значение, включенное в квитанцию ​​и идентификатор пакета.

Вот как вы можете проверить хеш получения на iOS. Из 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];
}

И это суть этого. Мне может быть что-то не хватает здесь или там, поэтому я могу вернуться к этому сообщению позже. В любом случае, я рекомендую просмотреть полный код для более подробной информации.

Ответ 2

Я удивлен, что никто не упоминал Receigen здесь. Это инструмент, который автоматически генерирует обфускацию кода проверки чека, каждый раз каждый раз; он поддерживает как графический интерфейс, так и работу в командной строке. Очень рекомендуется.

(Не связан с Receigen, просто счастливым пользователем.)

Я использую такой Rakefile, чтобы автоматически перезапускать Receigen (потому что это нужно делать при каждом изменении версии), когда я набираю 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

Ответ 3

Привет. Это версия Swift 3 для проверки получения покупки в приложении.

Вызвать receiptValidation() функцию из вашего AppDelegate или из того места, где вы хотите все это.

    func receiptValidation() {
    if let appStoreReceiptURL = Bundle.main.appStoreReceiptURL,
        FileManager.default.fileExists(atPath: appStoreReceiptURL.path) {
        do {
            let receiptData = try Data(contentsOf: appStoreReceiptURL, options: .alwaysMapped)
            let receiptString = receiptData.base64EncodedString(options: [])
            let dict = ["receipt-data" : receiptString, "password" : "**************************"] as [String : Any]
            do {
                let jsonData = try JSONSerialization.data(withJSONObject: dict, options: .prettyPrinted)
                //This Url for original Account
                //let url : String = "https://buy.itunes.apple.com/verifyReceipt"
                //This Url for Sandbox Testing Account 
                let url : String = "https://sandbox.itunes.apple.com/verifyReceipt"
                if let sandboxURL = Foundation.URL(string: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<String, 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=\(String(describing: error))")
                        }
                    }
                    task.resume()
                } else {
                }
            }
            catch {
            }
        }
        catch {
        }
    }
}

Теперь у нас есть другая функция, получающая дату из квитанции expirationDateFromResponse(). Эта функция будет находиться в одном контроллере или в 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! String) as NSDate!
        formatter.dateStyle = .medium
        let stringOutput = formatter.string(from: expirationDate as Date)
        formatter.dateFormat = "yyyy-MM-dd HH:mm:ss"
        let date = formatter.string(from: expirationDate as Date)
        print("Date=\(date)")
        self.iosNextBillingDateEntry(date)
        UserDefaults.standard.set(stringOutput, forKey: "PLAN_EXP_DATE")
        return expirationDate

    } else {
        return nil

    }
}

Теперь у нас есть другая функция для получения локального времени, если вам это требуется.

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
}

Пароль, который вы получите из магазина Apple. https://developer.apple.com открыть эту ссылку нажмите

  • 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

Скопируйте этот ключ и вставьте его в поле пароля.

Надеюсь, это поможет каждому, кто хочет этого в быстрой версии.