A complete solution to LOCALLY validate an in-app receipts and bundle receipts on iOS 7
Asked Answered
D

3

168

I have read a lot of docs and code that in theory will validate an in-app and/or bundle receipt.

Given that my knowledge of SSL, certificates, encryption, etc., is nearly zero, all of the explanations I have read, like this promising one, I have found difficult to understand.

They say the explanations are incomplete because every person has to figure out how to do it, or the hackers will have an easy job creating a cracker app that can recognize and identify patterns and patch the application. OK, I agree with this up to a certain point. I think they could explain completely how to do it and put a warning saying "modify this method", "modify this other method", "obfuscate this variable", "change the name of this and that", etc.

Can some good soul out there be kind enough to explain how to LOCALLY validate, bundle receipts and in-app purchase receipts on iOS 7 as I am five years old (ok, make it 3), from top to bottom, clearly?

Thanks!!!


If you have a version working on your apps and your concerns are that hackers will see how you did it, simply change your sensitive methods before publishing here. Obfuscate strings, change the order of lines, change the way you do loops (from using for to block enumeration and vice-versa) and things like that. Obviously, every person that uses the code that may be posted here, has to do the same thing, not to risk being easily hacked.

Damselfly answered 13/11, 2013 at 0:55 Comment(9)
Fair warning: doing it locally makes it a hell of a lot easier to patch this function out of your application.Mateya
OK, I know, but the point here is to do things difficult and prevent automated cracking/patching. The question is that if a hacker really wants to crack your app he/she will do it, whatever method you use, local or remote. The idea is also to change it slightly every new version you release, to prevent automated patching again.Damselfly
I understand, I maintain Clutch ;-) changing it slightly every time won't really help if you can just NOP the check, or worse use Overdrive to intercept and automatically patch it.Mateya
@Mateya - one can NOP the check even if the verification is done on a server.Damselfly
I understand and share the frustration. I don't believe in security by obscurity, and many design decisions around the app receipt strike me as that. On the other hand, I'm glad that Apple is taking piracy seriously. For developers like me who use pay-as-you-go backends, effortless piracy is a real concern.Palliate
You do know the reason why there is no one single solution which has been posted online to handle local in-app receipts? It's because then everyone would use that code -- as it's very hard to code yourself -- and then it would be easy to crackDenney
sorry, but this is not excuse. The only thing the author has to do is to say DO NOT USE THE CODE AS IT IS. Without any example, it is impossible to understand this without being a rocket scientist.Damselfly
If you don't want to bother implementing DRM, don't bother with local verification. Just POST the receipt directly to Apple from your app, and they'll send it back to you again in an easily parsed JSON format. It's trivial for pirates to crack this, but if you're just transitioning to freemium and don't care about piracy, it's just a few lines of very easy code.Meagher
"change the name of this and that" demonstrates a misunderstanding of exactly what state your code will be in when a cracker tries to crack it. Security by obscurity addresses Murphy's Law by making it impossible for a novice programmer to ignore a "do not use as-is" warning, which let's face it, many novices would probably ignore. SBO has its own problems, but at the very least, if an app's security is crap, it's crap in a unique way that a cracker will need to spend time cracking.Meshed
P
151

Here's a walkthrough of how I solved this in my in-app purchase library RMStore. I will explain how to verify a transaction, which includes verifying the whole receipt.

At a glance

Get the receipt and verify the transaction. If it fails, refresh the receipt and try again. This makes the verification process asynchronous as refreshing the receipt is asynchronous.

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

Getting the receipt data

The receipt is in [[NSBundle mainBundle] appStoreReceiptURL] and is actually a PCKS7 container. I suck at cryptography so I used OpenSSL to open this container. Others apparently have done it purely with system frameworks.

Adding OpenSSL to your project is not trivial. The RMStore wiki should help.

If you opt to use OpenSSL to open the PKCS7 container, your code could look like this. 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;
}

We'll get into the details of the verification later.

Getting the receipt fields

The receipt is expressed in ASN1 format. It contains general information, some fields for verification purposes (we'll come to that later) and specific information of each applicable in-app purchase.

Again, OpenSSL comes to the rescue when it comes to reading ASN1. From RMAppReceipt, using a few helper methods:

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;

Getting the in-app purchases

Each in-app purchase is also in ASN1. Parsing it is very similar than parsing the general receipt information.

From RMAppReceipt, using the same helper methods:

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

It should be noted that certain in-app purchases, such as consumables and non-renewable subscriptions, will appear only once in the receipt. You should verify these right after the purchase (again, RMStore helps you with this).

Verification at a glance

Now we got all the fields from the receipt and all its in-app purchases. First we verify the receipt itself, and then we simply check if the receipt contains the product of the transaction.

Below is the method that we called back at the beginning. From 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;
}

Verifying the receipt

Verifying the receipt itself boils down to:

  1. Checking that the receipt is valid PKCS7 and ASN1. We have done this implicitly already.
  2. Verifying that the receipt is signed by Apple. This was done before parsing the receipt and will be detailed below.
  3. Checking that the bundle identifier included in the receipt corresponds to your bundle identifier. You should hardcode your bundle identifier, as it doesn't seem to be very difficult to modify your app bundle and use some other receipt.
  4. Checking that the app version included in the receipt corresponds to your app version identifier. You should hardcode the app version, for the same reasons indicated above.
  5. Check the receipt hash to make sure the receipt correspond to the current device.

The 5 steps in code at a high-level, from 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;
}

Let's drill-down into steps 2 and 5.

Verifying the receipt signature

Back when we extracted the data we glanced over the receipt signature verification. The receipt is signed with the Apple Inc. Root Certificate, which can be downloaded from Apple Root Certificate Authority. The following code takes the PKCS7 container and the root certificate as data and checks if they match:

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

This was done back at the beginning, before the receipt was parsed.

Verifying the receipt hash

The hash included in the receipt is a SHA1 of the device id, some opaque value included in the receipt and the bundle id.

This is how you would verify the receipt hash on 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];
}

And that's the gist of it. I might be missing something here or there, so I might come back to this post later. In any case, I recommend browsing the complete code for more details.

Palliate answered 18/11, 2013 at 3:5 Comment(27)
Security disclaimer: using open source code makes your app more vulnerable. If security is a concern, you might want to use RMStore and the above code only as a guide.Palliate
It would be fantastic if in the future you get rid of OpenSSL and make your library compact by using just system frameworks.Damselfly
@RubberDuck See github.com/robotmedia/RMStore/issues/16. Feel free to chime in, or contribute. :)Palliate
contribute, me? this is why I have asked this question and gave a bounty, because my knowledge of encryption, SSL, OpenSSL, etc., is zero.Damselfly
@RubberDuck I had zero knowledge of OpenSSL until this. Who knows, you might even like it. :PPalliate
@RubberDuck I believe he is validating the receipt against Apple's server directly from the App. This is not recommended by Apple as it's relatively simple to hack.Palliate
as far as I see, he retrieves the bundle receipt locally, encodes it as 64bit string, sends it to apple and receives asynchronously a response dictionary that contains everything. Do you say this is easy to hack?Damselfly
@RubberDuck Yes, the response is the weak link. Check out the WWDC session video about this topic. asciiwwdc.com/2013/sessions/308Palliate
do you say that because the response is not encrypted, right?Damselfly
It's susceptible to a Man In The Middle Attack, where the request and/or response can be intercepted and modified. Eg, the request could be redirected to a 3rd party server, and a false response could be returned, tricking the app into thinking a product was purchased, when it was not, and enabling the functionality for free.Buck
I'm not an expert, but it seems that one has to call PKCS7_type_is_signed() to check that its type is signed before accessing p7->d.sign?Crine
@Crine Most likely. As mentioned, the code above doesn't check if the receipt was signed by Apple. I still have to add that part to RMStore.Palliate
There is a bug in line number 233 of github.com/robotmedia/RMStore/blob/master/RMStore/Optional/… It should be if (certificateData || [self verifyPCKS7:p7 withCertificateData:certificateData]) There should not be a "!"Interdiction
@Interdiction That's intentional. If you don't provide a certificate, RMStore assumes that you don't care about verifying the receipt with the certificate.Palliate
Should the term PCKS7 in this Answer be PKCS7, as in PKCS?Agamemnon
@BasilBourque Yep, I just changed it. Thanks for catching that!Palliate
@Palliate : May I test it sandbox mode? My app is not live till now. What to do if I want to test receipt validation while app is under development?Lucila
To get started with this, how do I add the Apple OpenSSL framework to a swift project? I can't find the correct module. Yes, it's not secure and I should add a static library, but that's step 2. Thanks.Gaal
refreshReceiptOnSuccess will ask the user for it's iTunes password, so I'd like to avoid that and only show it if really necessary. What would be the user-friendliest solution to show it only if really necessary? Only to users who really purchased something once? Not to users who never purchased anything...Equiponderance
Any implementation like this with CommonCrypto framework?Kamerman
Can you comment please are there any ways for hackers to easily hack this solution on jailbroken devices? We think about adding this solution for jailbroken users. We afraid that on jailbroken devices hackers will hack this solution.Physiotherapy
The thing that I find missing is how to run the Receipt Validation prior to triggering the popup 'Thank You - Your transaction has been successful'. What I am after is to get the receipt, run validation, and then and only then, show this popup. Thoughts?Harmonyharmotome
@Physiotherapy There is no 100% protection against hackers. But you can use various techniques for detecting if the app is running on a jailbroken device and then refuse to run (or deny access to content).Agamemnon
Is there a way to test with receipt samples that fail a specific step? For example, generate a receipt sample that passes everything but fails at step 3 (comparing the bundle identifiers), and then generating another one that fails at verifying the receipt hash.Bui
@Palliate Your disclaimer is absolutely incorrect. Open source code tends to be more secure because anyone can read it for themselves and confirm that it is correct (or not). "Security through obscurity" is no security at all.Upbow
If my app is so good that someone is actually going to bother reverse engineering and attacking my subscription payment mechanism, then I'll be extremely happy :p Honestly, is all this worth preventing the 0.0001% of users who would actually do this? As long as it isn't super-trivial to hack, for most developers that will be enoughGilgai
@Palliate I see you are validating against the root cert but why are you not validating against the intermediate cert? Would be interested in seeing how this would look like in code. Apple says to validate against both root and intermediate: developer.apple.com/documentation/appstorereceipts/…Schleicher
S
13

I'm surprised nobody mentioned Receigen here. It's a tool that automatically generates obfuscated receipt validation code, a different one each time; it supports both GUI and command-line operation. Highly recommended.

(Not affiliated with Receigen, just a happy user.)

I use a Rakefile like this to automatically rerun Receigen (because it needs to be done on every version change) when I type 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
Samoyed answered 3/11, 2015 at 21:2 Comment(6)
For those who is interested in Receigen, this a paid solution, which is available on App Store for 29.99$. Although it has not been updated since September 2014.Kershner
True, the lack of updates is very alarming. However it still works; FWIW, I'm using it in my apps.Samoyed
Check your app in instruments for leaks, with Receigen I get them a lot.Verenaverene
Receigen is the cutting edge, but yes it's a shame it seems to have been dropped.Figwort
Looks like it is not dropped yet. Updated three weeks ago!Bamberg
Yay! Yes, Receigen has risen from the ashes, and I confirm that I've successfully used it in a new project. It even generates Swift code now.Samoyed
M
6

Note: It's not recommend to do this type of verification in the client side

This is a Swift 4 version for validation of in-app-purchase receipt...

Lets create an enum to represent the possible errors of the receipt validation

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

Then let's create the function that validates the receipt, it will throws an error if it's unable to validate it.

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

Let's use this helper function, to get the expiration date of a specific product. The function receives a JSON response and a product id. The JSON response can contain multiple receipts info for different products, so it get the last info for the specified parameter.

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
}

Now you can call this function and handle of the possible error cases

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).")
}

You can get a Password from the App Store Connect. https://developer.apple.com open this link click on

  • 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

Copy that key and paste into the password field.

Marjoram answered 8/6, 2017 at 5:10 Comment(6)
You should never use the Apple validation URL from your device. It should only be used from your server. This was mentioned in the WWDC sessions.Footer
What would happen if user delete the apps or not open a long time? Is your expiry date calculation working fine?Lanchow
Then you need to keep validation on server side.Marjoram
As @pechar said, you should never do this. Please add it to the top of your answer. See WWDC session at 36:32 => developer.apple.com/videos/play/wwdc2016/702Bookerbookie
I don't understand why is it not secure to send the receipt data directly from device. Would anyone be able to explain?Unsound
@Unsound Because of possible Man-in-the-Middle attacks. You cannot control any side of the connection as described here: developer.apple.com/documentation/storekit/in-app_purchase/…Gereld

© 2022 - 2024 — McMap. All rights reserved.