一个完整的解决方案,可以在iOS 7上验证应用内收据和捆绑收据

时间:2013-11-13 00:55:16

标签: ios iphone in-app-purchase storekit

我已经阅读了很多文档和代码,理论上这些文档和代码将验证应用内和/或捆绑收据。

鉴于我对SSL,证书,加密等知识几乎为零,我读过的所有解释like this promising one,我发现很难理解。

他们说解释是不完整的,因为每个人都必须弄清楚如何去做,或者黑客可以轻松创建一个能够识别和识别模式并修补应用程序的破解程序应用程序。好的,我同意这一点。我认为他们可以完全解释如何做到并发出警告说"修改这种方法","修改这个其他方法","混淆这个变量",& #34;更改此名称和#34;等等。

在我五岁的时候,有一些善良的灵魂可以解释如何在iOS 7上进行本地验证,捆绑收据和应用内购买收据(好吧,让它成为3) ,从上到下,显然?

感谢!!!


如果你的应用程序有一个版本,而你的担心是黑客会看到你是如何做到的,那么只需在发布之前更改你的敏感方法。混淆字符串,改变行的顺序,改变循环的方式(从使用到阻止枚举,反之亦然)等等。显然,每个使用可能在此处发布的代码的人都必须做同样的事情,而不是冒着容易被黑客攻击的风险。

3 个答案:

答案 0 :(得分:140)

以下是我在应用内购买库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],实际上是PCKS7容器。我吮吸密码学所以我用OpenSSL打开这个容器。其他人显然是纯粹用system frameworks完成的。

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

验证收据

验证收据本身归结为:

  1. 检查收据是否有效PKCS7和ASN1。我们已经隐含地这样做了。
  2. 验证收据是否由Apple签署。这是在解析收据之前完成的,详情如下。
  3. 检查收据中包含的捆绑包标识符是否与您的捆绑包标识符相对应。您应该对您的软件包标识符进行硬编码,因为修改您的应用程序包并使用其他收据似乎并不是很困难。
  4. 检查收据中包含的应用版本是否与您的应用版本标识符相对应。您应该对应用版本进行硬编码,原因与上述相同。
  5. 检查收据哈希,确保收据与当前设备相对应。
  6. 高级代码中的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;
    }
    

    这是在解析收据之前在开始时完成的。

    验证收据哈希

    收据中包含的哈希值是设备ID的SHA1,收据中包含的一些不透明值以及包ID。

    这是验证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];
    }
    

    这就是它的要点。我可能会在这里或那里遗漏一些东西,所以我稍后可能会回到这篇文章。无论如何,我建议浏览完整的代码以获取更多详细信息。

答案 1 :(得分:13)

我很惊讶没有人在这里提到Receigen。它是一种自动生成混淆收据验证码的工具,每次都有不同的验证码;它支持GUI和命令行操作。强烈推荐。

(与Receigen无关,只是一个快乐的用户。)

当我输入rake receigen时,我使用这样的Rakefile自动重新运行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

答案 2 :(得分:4)

  

注意:不建议在客户端进行此类验证

这是用于验证应用内购买收据的 Swift 4 版本...

让我们创建一个枚举来表示收据验证的可能错误

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

然后让我们创建验证收据的功能,如果收件无法验证,则会抛出错误。

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

让我们使用此辅助函数来获取特定产品的到期日期。该函数接收JSON响应和产品ID。 JSON响应可以包含不同产品的多个收据信息,因此它获取指定参数的最后信息。

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
}

现在您可以调用此函数并处理可能的错误情况

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

您可以从App Store Connect获取密码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
  

复制该密钥并粘贴到密码字段中。

希望这对于那些想要快速版本的人来说会有所帮助。