Kompletne rozwiązanie do LOKALNEGO sprawdzania potwierdzeń w aplikacji i paragonów w systemie iOS 7

160

Przeczytałem wiele dokumentów i kodu, które teoretycznie będą weryfikować potwierdzenie w aplikacji i / lub pakiet.

Biorąc pod uwagę, że moja wiedza na temat SSL, certyfikatów, szyfrowania itp. Jest prawie zerowa, wszystkie wyjaśnienia, które przeczytałem, takie jak to obiecujące , były trudne do zrozumienia.

Mówią, że wyjaśnienia są niekompletne, ponieważ każda osoba musi wymyślić, jak to zrobić, w przeciwnym razie hakerzy będą mieli łatwe zadanie stworzenia aplikacji do krakowania, która może rozpoznawać i identyfikować wzorce oraz łatać aplikację. OK, zgadzam się z tym do pewnego momentu. Myślę, że mogliby całkowicie wyjaśnić, jak to zrobić i umieścić ostrzeżenie o treściach „zmodyfikuj tę metodę”, „zmodyfikuj tę inną metodę”, „zaciemnij tę zmienną”, „zmień nazwę tego i tamtego” itp.

Czy jakaś dobra dusza może być na tyle miła, by wyjaśnić, jak LOKALNIE sprawdzać, pakować paragony i pokwitowania zakupów w aplikacji na iOS 7, ponieważ mam pięć lat (ok, zrób to 3), wyraźnie od góry do dołu?

Dzięki!!!


Jeśli masz wersję działającą w swoich aplikacjach i obawiasz się, że hakerzy zobaczą, jak to zrobiłeś, po prostu zmień wrażliwe metody przed opublikowaniem tutaj. Ukryj ciągi znaków, zmień kolejność linii, zmień sposób wykonywania pętli (od używania for do blokowania wyliczania i odwrotnie) i tym podobne. Oczywiście każda osoba korzystająca z zamieszczonego tutaj kodu musi zrobić to samo, aby nie ryzykować łatwego zhakowania.

kaczka
źródło
1
Uczciwe ostrzeżenie: robienie tego lokalnie znacznie ułatwia łatanie tej funkcji poza twoją aplikację.
NinjaLikesCheez
2
OK, wiem, ale chodzi o to, aby robić rzeczy trudne i zapobiegać automatycznemu łamaniu / łataniu. Pytanie jest takie, że jeśli haker naprawdę chce złamać twoją aplikację, zrobi to, niezależnie od używanej metody, lokalnej lub zdalnej. Chodzi również o to, aby nieznacznie go zmieniać przy każdej nowej wydanej wersji, aby zapobiec ponownemu automatycznemu aktualizowaniu.
Duck
4
@NinjaLikesCheez - można NOP sprawdzić, nawet jeśli weryfikacja jest wykonywana na serwerze.
Duck
14
przepraszam, ale to nie jest wymówka. Jedyne, co musi zrobić autor, to powiedzieć NIE UŻYWAJ KODU TAKIM, JAKIM JEST. Bez żadnego przykładu nie da się tego zrozumieć bez bycia naukowcem zajmującym się rakietami.
Duck,
3
Jeśli nie chcesz zawracać sobie głowy wdrażaniem DRM, nie przejmuj się lokalną weryfikacją. Po prostu OPUBLIKUJ pokwitowanie bezpośrednio z aplikacji Apple, a oni odeślą je z powrotem w łatwym do przeanalizowania formacie JSON. Dla piratów to trywialne, ale jeśli dopiero przechodzisz na freemium i nie przejmujesz się piractwem, to tylko kilka linijek bardzo prostego kodu.
Dan Fabulich

Odpowiedzi:

146

Oto przewodnik, w jaki sposób 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

Odbierz paragon i zweryfikuj transakcję. Jeśli to się nie powiedzie, odśwież potwierdzenie i spróbuj ponownie. Dzięki temu proces weryfikacji jest asynchroniczny, ponieważ odświeżanie potwierdzenia jest asynchroniczne.

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

Uzyskanie danych do odbioru

Pokwitowanie znajduje się [[NSBundle mainBundle] appStoreReceiptURL]i jest w rzeczywistości kontenerem PCKS7. Ssam w kryptografii, więc użyłem OpenSSL do otwarcia tego kontenera. Inni najwyraźniej zrobili to wyłącznie za pomocą ram 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ć następująco. Od 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;
}

Szczegóły weryfikacji omówimy później.

Pobieranie pól paragonu

Pokwitowanie jest wyrażone w formacie ASN1. Zawiera ogólne informacje, niektóre pola do celów weryfikacji (do tego dojdziemy później) oraz szczegółowe informacje o każdym odpowiednim zakupie w aplikacji.

Ponownie, OpenSSL przychodzi z pomocą, jeśli chodzi o czytanie 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;

Pobieranie zakupów w aplikacji

Każdy zakup w aplikacji jest również w ASN1. Analizowanie go jest bardzo podobne do analizowania ogólnych informacji o potwierdzeniu.

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

Należy zauważyć, że niektóre zakupy w aplikacji, takie jak materiały eksploatacyjne i nieodnawialne subskrypcje, pojawią się na paragonie tylko raz. Powinieneś je zweryfikować zaraz po zakupie (ponownie RMStore pomaga w tym).

Weryfikacja w skrócie

Teraz mamy wszystkie pola z paragonu i wszystkie zakupy w aplikacji. Najpierw weryfikujemy sam paragon, a potem po prostu sprawdzamy, czy na paragonie znajduje się 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 odbioru

Sama weryfikacja paragonu sprowadza się do:

  1. Sprawdzanie, czy paragon jest ważny PKCS7 i ASN1. Zrobiliśmy to już w sposób dorozumiany.
  2. Weryfikacja, czy pokwitowanie jest podpisane przez Apple. Zostało to zrobione przed przeanalizowaniem potwierdzenia i zostanie szczegółowo opisane poniżej.
  3. Sprawdzenie, czy identyfikator paczki na paragonie odpowiada Twojemu identyfikatorowi paczki. Powinieneś zakodować na stałe swój identyfikator pakietu, ponieważ zmodyfikowanie pakietu aplikacji i użycie innego potwierdzenia nie wydaje się trudne.
  4. Sprawdzanie, czy wersja aplikacji zawarta na potwierdzeniu odpowiada identyfikatorowi wersji aplikacji. Powinieneś zakodować wersję aplikacji na stałe z tych samych powodów, które wskazano powyżej.
  5. Sprawdź skrót potwierdzenia, aby upewnić się, że pokwitowanie jest zgodne z bieżącym urządzeniem.

5 kroków w kodzie na wysokim poziomie z 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 szczegółów w krokach 2 i 5.

Weryfikacja podpisu odbioru

Kiedy już wyodrębniliśmy dane, przejrzeliśmy weryfikację podpisu paragonu. Potwierdzenie jest podpisane głównym certyfikatem Apple Inc., który można pobrać z głównego urzędu certyfikacji Apple . Poniższy kod pobiera kontener PKCS7 i certyfikat główny jako dane i sprawdza, czy są zgodne:

+ (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;
}

Zrobiono to na początku, zanim parsowano paragon.

Weryfikacja skrótu potwierdzenia

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

W ten sposób zweryfikujesz skrót potwierdzenia na iOS. Od 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 to jest sedno tego. Być może brakuje mi czegoś tu lub tam, więc mogę wrócić do tego postu później. W każdym razie zalecamy przejrzenie całego kodu, aby uzyskać więcej informacji.

hpique
źródło
2
Zastrzeżenie dotyczące bezpieczeństwa: używanie otwartego kodu źródłowego sprawia, że ​​Twoja aplikacja jest bardziej podatna na ataki. Jeśli zależy Ci na bezpieczeństwie, możesz chcieć użyć RMStore i powyższego kodu tylko jako przewodnika.
hpique
6
Byłoby fantastycznie, gdybyś w przyszłości pozbył się OpenSSL i zmniejszył swoją bibliotekę, używając tylko struktur systemowych.
Duck
2
@RubberDuck Zobacz github.com/robotmedia/RMStore/issues/16 . Zapraszam do przyłączenia się lub wniesienia wkładu. :)
hpique
1
@RubberDuck Do tego momentu nie miałem żadnej wiedzy na temat OpenSSL. Kto wie, może ci się nawet spodobać. : P
hpique
2
Jest podatny na atak Man In The Middle, w którym żądanie i / lub odpowiedź mogą zostać przechwycone i zmodyfikowane. Na przykład żądanie może zostać przekierowane na serwer innej firmy i może zostać zwrócona fałszywa odpowiedź, nakłaniając aplikację do myślenia, że ​​produkt został zakupiony, gdy nie był, i włączając tę ​​funkcjonalność za darmo.
Jasarien
13

Dziwię się, że nikt nie wspomniał tutaj o Receigenie . Jest to narzędzie, które automatycznie generuje zaciemniony kod weryfikacji paragonu, za każdym razem inny; obsługuje zarówno GUI, jak i działanie z linii poleceń. Wysoce rekomendowane.

(Nie jest powiązany z Receigen, tylko zadowolony użytkownik.)

Używam Rakefile takiego jak ten, aby automatycznie ponownie uruchomić Receigen (ponieważ trzeba to zrobić przy każdej zmianie wersji), kiedy piszę 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
Andrey Tarantsov
źródło
1
Dla zainteresowanych Receigenem jest to płatne rozwiązanie, które jest dostępne w App Store za 29,99 $. Chociaż nie był aktualizowany od września 2014 r.
DevGansta
To prawda, że ​​brak aktualizacji jest bardzo niepokojący. Jednak nadal działa; FWIW, używam go w moich aplikacjach.
Andrey Tarantsov
Sprawdź swoją aplikację w instrumentach pod kątem wycieków, z Receigen dostaję ich dużo.
wielebny
Receigen jest nowatorski, ale tak, szkoda, że ​​został upuszczony.
Fattie
1
Wygląda na to, że nie został jeszcze upuszczony. Zaktualizowano trzy tygodnie temu!
Oleg Korzhukov
2

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

To jest wersja Swift 4 do sprawdzania potwierdzenia zakupu w aplikacji ...

Pozwala utworzyć wyliczenie reprezentujące możliwe błędy weryfikacji potwierdzenia

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

Następnie stwórzmy funkcję, która waliduje paragon, zgłosi błąd, jeśli nie będzie 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ć datę ważności konkretnego produktu. Funkcja otrzymuje odpowiedź JSON i identyfikator produktu. Odpowiedź JSON może zawierać wiele informacji o potwierdzeniach dla różnych produktów, więc pobiera ostatnie informacje dla określonego parametru.

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.comotwórz ten link kliknij

  • 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 polu hasła.

Mam nadzieję, że pomoże to każdemu, kto tego chce w szybkiej wersji.

Pushpendra
źródło
19
Nigdy nie należy używać adresu URL weryfikacji Apple z urządzenia. Powinien być używany tylko z twojego serwera. Wspomniano o tym podczas sesji WWDC.
pechar
Co by się stało, gdyby użytkownik usunął aplikacje lub nie otwierał ich przez długi czas? Czy obliczanie daty ważności działa prawidłowo?
karthikeyan
Następnie musisz zachować walidację po stronie serwera.
Pushpendra
1
Jak powiedział @pechar, nigdy nie powinieneś tego robić. Dodaj go na początku swojej odpowiedzi. Zobacz sesję WWDC pod adresem 36:32 => developer.apple.com/videos/play/wwdc2016/702
cicerocamargo
Nie rozumiem, dlaczego przesyłanie danych paragonu bezpośrednio z urządzenia nie jest bezpieczne. Czy ktoś byłby w stanie to wyjaśnić?
Koh