Kompletne rozwiązanie do lokalnego sprawdzania paragonów w aplikacji i paragonów w systemie iOS 7

Przeczytałem wiele dokumentów i kodu, które teoretycznie potwierdzą potwierdzenie w aplikacji i/lub pakiecie.

Biorąc pod uwagę, że moja znajomość SSL, certyfikatów, szyfrowania itp., jest prawie zerowe, wszystkie wyjaśnienia, które przeczytałem, Jak to obiecujące , trudno mi było zrozumieć.

Mówią, że wyjaśnienia są niekompletne, ponieważ każda osoba musi wymyślić, jak to zrobić, albo hakerzy będą mieli łatwą pracę tworząc aplikację cracker, która może rozpoznać i identyfikuj wzorce i łataj aplikację. OK, zgadzam się z tym do pewnego momentu. Myślę, że mogliby dokładnie wyjaśnić, jak to zrobić i umieścić Ostrzeżenie: "modify this method", "modify this other method", "obfuscate this variable", "change the name of this and that", itd.

Czy jakaś dobra dusza może być na tyle uprzejma, aby wyjaśnić jak lokalnie zweryfikować paragony i paragony zakupu w aplikacji na iOS 7 Jak mam pięć lat( ok, zrób to 3), od góry do dno?

Dzięki!!!


Jeśli masz wersję działającą na twoich aplikacjach i masz obawy, że hakerzy zobaczą, jak to zrobiłeś, po prostu zmień wrażliwe metody przed opublikowaniem tutaj. Zaciemniaj łańcuchy, zmieniaj kolejność linii, zmieniaj sposób wykonywania pętli (od używania for do blokowania wyliczania i vice-versa) i tego typu rzeczy. Oczywiście, każda osoba, która używa kodu, który może być zamieszczony tutaj, musi zrobić to samo, aby nie ryzykować, że będzie łatwo zhakowany.

Author: smileBot, 2013-11-13

3 answers

W tym artykule omówiłem sposób, w jaki rozwiązałem ten problem w mojej bibliotece zakupów w aplikacji RMStore. Wyjaśnię, jak zweryfikować transakcję, która obejmuje weryfikację całego paragonu.

W skrócie

Zdobądź Paragon i zweryfikuj transakcję. Jeśli się nie powiedzie, odśwież Paragon i spróbuj ponownie. To sprawia, że proces weryfikacji jest asynchroniczny, ponieważ odświeżanie paragonu jest asynchroniczne.

From 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];
}];
[[13]}Getting the dane rachunku

Paragon jest w [[NSBundle mainBundle] appStoreReceiptURL] i jest w rzeczywistości kontenerem PCKS7. Jestem beznadziejny w kryptografii, więc użyłem OpenSSL do otwarcia tego kontenera. Inni najwyraźniej zrobili to wyłącznie za pomocą frameworków systemowych .

Dodanie OpenSSL do twojego projektu nie jest trywialne. rmstore wiki powinno pomóc.

Jeśli zdecydujesz się użyć OpenSSL do otwarcia kontenera PKCS7, Twój kod może wyglądać tak. From 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;
}

Wejdziemy do szczegóły weryfikacji później.

Otrzymywanie pól paragonu

Paragon jest wyrażony w formacie ASN1. Zawiera ona informacje ogólne, niektóre pola do celów weryfikacji (do tego wrócimy później) oraz szczegółowe informacje dotyczące każdego zakupu w aplikacji.

Ponownie, OpenSSL przychodzi na ratunek, jeśli chodzi o odczyt ASN1. Z RMAppReceipt, używając kilku metod pomocniczych:

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;
[13]} pobieranie zakupów w aplikacji

Każdy zakup w aplikacji jest również w ASN1. Parsowanie jest bardzo podobne do parsowania ogólnych informacji o odbiorze.

Z RMAppReceipt, używając tych samych metod pomocniczych:

[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;
        }
    }
}]; 
[9]} należy zauważyć, że niektóre zakupy w aplikacji, takie jak materiały eksploatacyjne i nieodnawialne subskrypcje, pojawią się tylko raz na paragonie. Należy je zweryfikować zaraz po zakupie (ponownie pomaga w tym RMStore).

Weryfikacja w skrócie

Teraz mamy wszystkie pola z paragonu i wszystkich zakupów w aplikacji. Najpierw weryfikujemy sam paragon, a następnie po prostu sprawdzamy, czy Paragon zawiera produkt transakcji.

Poniżej znajduje się metoda, którą wywołaliśmy na początku. Z 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;
}

Weryfikacja paragonu

Weryfikacja samego paragonu sprowadza się do:

  1. sprawdzenie, czy paragon jest ważny PKCS7 i ASN1. Zrobiliśmy to bezwarunkowo już.
  2. sprawdzanie, czy paragon jest podpisany przez Apple. Zostało to zrobione przed analizą paragonu i zostanie szczegółowo opisane poniżej.
  3. sprawdzenie, czy identyfikator pakietu zawarty w paragonie odpowiada identyfikatorowi pakietu. Należy zakodować identyfikator pakietu na twardo, ponieważ modyfikowanie pakietu aplikacji i korzystanie z innego paragonu nie wydaje się być trudne.
  4. sprawdzenie, czy wersja aplikacji dołączona do paragonu odpowiada identyfikatorowi wersji aplikacji. Należy hardcode wersji aplikacji, z tych samych powodów wskazanych powyżej.
  5. Sprawdź Skrót paragonu, aby upewnić się, że Paragon odpowiada aktualnemu urządzeniu.

5 kroków kodu na wysokim poziomie, od 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;
}
Przejdźmy do kroków 2 i 5.

Weryfikacja podpisu paragonu

Kiedy wyodrębniliśmy dane, przejrzeliśmy weryfikację podpisu paragonu. Paragon jest podpisany Apple Inc. Certyfikat główny, który można pobrać z Apple Root Certificate Authority . Poniższy kod pobiera kontener PKCS7 i certyfikat główny jako dane i sprawdza, czy pasują:
+ (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;
}
Zostało to zrobione na początku, zanim Paragon został przetworzony.

Weryfikacja paragonu

Hash zawarty w paragonie to SHA1 identyfikatora urządzenia, pewna nieprzezroczysta wartość zawarta w paragonie i identyfikator pakietu.

Oto jak możesz zweryfikować hash paragonu na iOS. From 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];
}
I w tym tkwi sedno sprawy. Może mi czegoś brakuje tu lub tam, więc może wrócę do tego posta później. W każdym razie, polecam przeglądanie całego kodu, aby uzyskać więcej szczegółów.
 137
Author: hpique,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2018-01-10 05:25:18

Dziwi mnie, że nikt tu nie wspomniał o Receigen. Jest to narzędzie, które automatycznie generuje ukryty kod weryfikacyjny paragonu, za każdym razem inny; obsługuje Zarówno interfejs GUI, jak i operacje w wierszu poleceń. Gorąco polecam.

(nie związany z Receigen, po prostu szczęśliwy użytkownik.)

Używam Rakefile jak ten do automatycznego receigen receigen (bo trzeba to zrobić przy każdej zmianie wersji) kiedy wpisuję 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
 10
Author: Andrey Tarantsov,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2015-11-03 21:02:24

Uwaga: nie zaleca się tego typu weryfikacji po stronie klienta

To jest wersja Swift 4 do walidacji paragonu zakupu w aplikacji...

Pozwala utworzyć enum, aby reprezentować możliwe błędy walidacji paragonu

enum ReceiptValidationError: Error {
    case receiptNotFound
    case jsonResponseIsNotValid(description: String)
    case notBought
    case expired
}

Następnie stwórzmy funkcję, która waliduje Paragon, wyświetli błąd, jeśli nie jest w stanie go zweryfikować.

func validateReceipt() throws {
    guard let appStoreReceiptURL = Bundle.main.appStoreReceiptURL, FileManager.default.fileExists(atPath: appStoreReceiptURL.path) else {
        throw ReceiptValidationError.receiptNotFound
    }

    let receiptData = try! Data(contentsOf: appStoreReceiptURL, options: .alwaysMapped)
    let receiptString = receiptData.base64EncodedString()
    let jsonObjectBody = ["receipt-data" : receiptString, "password" : <#String#>]

    #if DEBUG
    let url = URL(string: "https://sandbox.itunes.apple.com/verifyReceipt")!
    #else
    let url = URL(string: "https://buy.itunes.apple.com/verifyReceipt")!
    #endif

    var request = URLRequest(url: url)
    request.httpMethod = "POST"
    request.httpBody = try! JSONSerialization.data(withJSONObject: jsonObjectBody, options: .prettyPrinted)

    let semaphore = DispatchSemaphore(value: 0)

    var validationError : ReceiptValidationError?

    let task = URLSession.shared.dataTask(with: request) { data, response, error in
        guard let data = data, let httpResponse = response as? HTTPURLResponse, error == nil, httpResponse.statusCode == 200 else {
            validationError = ReceiptValidationError.jsonResponseIsNotValid(description: error?.localizedDescription ?? "")
            semaphore.signal()
            return
        }
        guard let jsonResponse = (try? JSONSerialization.jsonObject(with: data, options: JSONSerialization.ReadingOptions.mutableContainers)) as? [AnyHashable: Any] else {
            validationError = ReceiptValidationError.jsonResponseIsNotValid(description: "Unable to parse json")
            semaphore.signal()
            return
        }
        guard let expirationDate = self.expirationDate(jsonResponse: jsonResponse, forProductId: <#String#>) else {
            validationError = ReceiptValidationError.notBought
            semaphore.signal()
            return
        }

        let currentDate = Date()
        if currentDate > expirationDate {
            validationError = ReceiptValidationError.expired
        }

        semaphore.signal()
    }
    task.resume()

    semaphore.wait()

    if let validationError = validationError {
        throw validationError
    }
}

Użyjmy tej funkcji pomocniczej, aby uzyskać wygaśnięcie Data konkretnego produktu. Funkcja otrzymuje odpowiedź JSON i identyfikator produktu. Odpowiedź JSON może zawierać wiele informacji o paragonach dla różnych produktów, więc pobiera ostatnie informacje o podanym parametrze.

func expirationDate(jsonResponse: [AnyHashable: Any], forProductId productId :String) -> Date? {
    guard let receiptInfo = (jsonResponse["latest_receipt_info"] as? [[AnyHashable: Any]]) else {
        return nil
    }

    let filteredReceipts = receiptInfo.filter{ return ($0["product_id"] as? String) == productId }

    guard let lastReceipt = filteredReceipts.last else {
        return nil
    }

    let formatter = DateFormatter()
    formatter.dateFormat = "yyyy-MM-dd HH:mm:ss VV"

    if let expiresString = lastReceipt["expires_date"] as? String {
        return formatter.date(from: expiresString)
    }

    return nil
}

Teraz możesz wywołać tę funkcję i obsłużyć możliwe przypadki błędów

do {
    try validateReceipt()
    // The receipt is valid 
    print("Receipt is valid")
} catch ReceiptValidationError.receiptNotFound {
    // There is no receipt on the device 
} catch ReceiptValidationError.jsonResponseIsNotValid(let description) {
    // unable to parse the json 
    print(description)
} catch ReceiptValidationError.notBought {
    // the subscription hasn't being purchased 
} catch ReceiptValidationError.expired {
    // the subscription is expired 
} catch {
    print("Unexpected error: \(error).")
}

Możesz uzyskać hasło z App Store Connect. https://developer.apple.com otwórz ten link kliknij na

  • 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

Skopiuj ten klucz i wklej w pole Hasło.

Nadzieję, że to pomoże dla każdego, kto chce, że w wersji swift.

 3
Author: APK APPS,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2018-07-17 14:37:10