提问人:Duck 提问时间:11/13/2013 最后编辑:SmileBotDuck 更新时间:4/5/2021 访问量:58580
在 iOS 7 上本地验证应用内收据和捆绑收据的完整解决方案
A complete solution to LOCALLY validate an in-app receipts and bundle receipts on iOS 7
问:
我读过很多文档和代码,理论上可以验证应用内和/或捆绑收据。
鉴于我对SSL,证书,加密等的了解几乎为零,我读过的所有解释,就像这个有前途的解释一样,我发现很难理解。
他们说这些解释是不完整的,因为每个人都必须弄清楚如何去做,否则黑客将很容易创建一个可以识别和识别模式并修补应用程序的破解应用程序。好的,在某种程度上我同意这一点。我认为他们可以完全解释如何做到这一点,并发出警告,说“修改此方法”、“修改此其他方法”、“混淆此变量”、“更改此和该名称”等。
一些好心人可以解释为什么在我五岁的时候(好吧,让它成为 3 岁),从上到下,清楚地解释如何在 iOS 7 上本地验证、捆绑收据和应用内购买收据?
谢谢!!!
如果你有一个版本在你的应用程序上运行,并且你担心黑客会看到你是如何做到的,只需在发布之前更改你的敏感方法即可。对字符串进行模糊处理,更改行的顺序,更改循环的方式(从使用 for 到阻止枚举,反之亦然)等等。显然,每个使用可能在此处发布的代码的人都必须做同样的事情,而不是冒着被黑客入侵的风险。
答:
以下是我如何在应用内购买库 RMStore 中解决此问题的演练。我将解释如何验证交易,包括验证整个收据。
一目了然
获取收据并验证交易。如果失败,请刷新收据,然后重试。这使得验证过程是异步的,因为刷新收据是异步的。
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];
}];
获取收据数据
收据在 PCKS7 容器中,实际上是一个 PCKS7 容器。我不喜欢密码学,所以我使用OpenSSL来打开这个容器。其他人显然纯粹是用系统框架来做到这一点的。[[NSBundle mainBundle] appStoreReceiptURL]
将 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;
}
验证收据
验证收据本身归结为:
- 检查收据是否有效 PKCS7 和 ASN1。我们已经隐含地这样做了。
- 验证收据是否由 Apple 签名。这是在解析收据之前完成的,下面将详细介绍。
- 检查收据中包含的捆绑标识符是否与您的捆绑标识符相对应。您应该对捆绑包标识符进行硬编码,因为修改您的 app bundle 并使用其他一些收据似乎并不困难。
- 检查收据中包含的应用版本是否与您的应用版本标识符相对应。出于上述相同原因,您应该对应用版本进行硬编码。
- 检查收据哈希,确保收据与当前设备相对应。
RMStoreAppReceiptVerificator 中的高级代码中的 5 个步骤: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;
}
让我们深入研究步骤 2 和 5。
验证收据签名
当我们提取数据时,我们浏览了收据签名验证。收据使用 Apple Inc. 根证书签名,该证书可从 Apple 根证书颁发机构下载。以下代码将 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];
}
这就是它的要点。我可能在这里或那里遗漏了一些东西,所以我可能会稍后再回到这篇文章。无论如何,我建议浏览完整的代码以获取更多详细信息。
评论
我很惊讶没有人在这里提到 Receigen。它是一种自动生成混淆收据验证码的工具,每次都不同;它支持GUI和命令行操作。强烈推荐。
(不隶属于 Receigen,只是一个快乐的用户。
当我键入时,我使用这样的 Rakefile 自动重新运行 Receigen(因为它需要在每次版本更改时完成):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
评论
注意:不建议在客户端进行此类验证
这是一个 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
复制该密钥并粘贴到密码字段中。
评论