在 iOS 7 上本地验证应用内收据和捆绑收据的完整解决方案
声明:本页面是StackOverFlow热门问题的中英对照翻译,遵循CC BY-SA 4.0协议,如果您需要使用它,必须同样遵循CC BY-SA许可,注明原文地址和作者信息,同时你必须将它归于原作者(不是我):StackOverFlow
原文地址: http://stackoverflow.com/questions/19943183/
Warning: these are provided under cc-by-sa 4.0 license. You are free to use/share it, But you must attribute it to the original authors (not me):
StackOverFlow
A complete solution to LOCALLY validate an in-app receipts and bundle receipts on iOS 7
提问by DuckDucking
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.
鉴于我对 SSL、证书、加密等方面的知识几乎为零,我读过的所有解释,比如这个有前途的解释,我都觉得难以理解。
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 7as I am five years old (ok, make it 3), from top to bottom, clearly?
有没有好心人可以解释一下如何在 iOS 7 上本地验证、捆绑收据和应用内购买收据,因为我 5 岁(好吧,让它 3),从上到下,清楚地?
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.
如果您的应用程序有一个版本,并且您担心黑客会看到您是如何做到的,只需在此处发布之前更改您的敏感方法。混淆字符串,改变行的顺序,改变你做循环的方式(从使用 for 到阻塞枚举,反之亦然)等等。显然,每个使用可能被张贴在这里的代码的人都必须做同样的事情,不要冒被轻易黑客攻击的风险。
采纳答案by hpique
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.
这是我如何在我的应用程序内购买库RMStore 中解决此问题的演练。我将解释如何验证交易,包括验证整个收据。
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.
收据在[[NSBundle mainBundle] appStoreReceiptURL]
并且实际上是一个 PCKS7 容器。我不擅长密码学,所以我使用 OpenSSL 打开这个容器。其他人显然是纯粹使用系统框架来完成的。
Adding OpenSSL to your project is not trivial. The RMStore wikishould help.
将 OpenSSL 添加到您的项目并非易事。该RMStore维基应该有所帮助。
If you opt to use OpenSSL to open the PKCS7 container, your code could look like this. From RMAppReceipt:
如果您选择使用 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;
}
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.
收据以 ASN1 格式表示。它包含一般信息、一些用于验证目的的字段(我们稍后会谈到)以及每个适用的应用程序内购买的特定信息。
Again, OpenSSL comes to the rescue when it comes to reading ASN1. From RMAppReceipt, using a few helper methods:
同样,在读取 ASN1 时,OpenSSL 会派上用场。从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;
Getting the in-app purchases
获取应用内购买
Each in-app purchase is also in ASN1. Parsing it is very similar than parsing the general receipt information.
每个应用内购买也在 ASN1 中。解析它与解析一般收据信息非常相似。
From RMAppReceipt, using the same helper methods:
从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;
}
}
}];
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).
需要注意的是,某些应用内购买,例如消耗品和不可再生订阅,只会在收据中出现一次。您应该在购买后立即验证这些(同样,RMStore 可以帮助您)。
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:
下面是我们一开始回调的方法。从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:
验证收据本身归结为:
- Checking that the receipt is valid PKCS7 and ASN1. We have done this implicitly already.
- Verifying that the receipt is signed by Apple. This was done before parsing the receipt and will be detailed below.
- 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.
- 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.
- Check the receipt hash to make sure the receipt correspond to the current device.
- 检查收据是否有效 PKCS7 和 ASN1。我们已经隐含地做到了这一点。
- 验证收据是否由 Apple 签名。这是在解析收据之前完成的,将在下面详细说明。
- 检查收据中包含的捆绑标识符是否与您的捆绑标识符相对应。您应该对您的包标识符进行硬编码,因为修改您的应用包并使用其他收据似乎并不难。
- 检查收据中包含的应用程序版本是否与您的应用程序版本标识符相对应。出于与上述相同的原因,您应该对应用程序版本进行硬编码。
- 检查收据哈希以确保收据对应于当前设备。
The 5 steps in code at a high-level, from RMStoreAppReceiptVerificator:
来自RMStoreAppReceiptVerificator的高级代码的 5 个步骤:
- (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.
让我们深入了解第 2 步和第 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:
当我们提取数据时,我们浏览了收据签名验证。收据上有 Apple Inc. Root Certificate 签名,该证书可从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;
}
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.
收据中包含的哈希是设备 ID 的 SHA1、收据中包含的一些不透明值和捆绑包 ID。
This is how you would verify the receipt hash on iOS. From RMAppReceipt:
这是您在 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];
}
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.
这就是它的要点。我可能在这里或那里遗漏了一些东西,所以我可能会在稍后回到这篇文章。无论如何,我建议浏览完整代码以获取更多详细信息。
回答by Andrey Tarantsov
I'm surprised nobody mentioned Receigenhere. 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.
我很惊讶这里没有人提到Receigen。它是一种自动生成混淆收据验证码的工具,每次都不同;它支持 GUI 和命令行操作。强烈推荐。
(Not affiliated with Receigen, just a happy user.)
(不隶属于 Receigen,只是一个快乐的用户。)
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
:
当我输入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>!
.strip
else
nil
end
end
end
回答by Pushpendra
Note: It's not recommend to do this type of verification in the client side
注意:不建议在客户端进行此类验证
This is a Swift 4version for validation of in-app-purchase receipt...
这是用于验证应用内购买收据的Swift 4版本...
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.
让我们使用这个辅助函数来获取特定产品的到期日期。该函数接收 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 (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).")
}
["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
现在您可以调用此函数并处理可能的错误情况
##代码##You can get a Passwordfrom the App Store Connect.
https://developer.apple.com
open this link click on
您可以从 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
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.
复制该密钥并粘贴到密码字段中。
Hope this will help for every on who wants that in swift version.
希望这对每个想要 swift 版本的人都有帮助。