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

我读过很多文档和代码,理论上它们可以验证应用内收据和/或捆绑收据。

鉴于我对 SSL、证书、加密等的知识几乎为零,所有我读过的解释,就像这个有前途的,我发现很难理解。

他们说,解释是不完整的,因为每个人都必须弄清楚如何做到这一点,否则黑客将很容易创建一个破解应用程序,可以识别和识别模式,并修补应用程序。好吧,在某种程度上我同意这一点。我认为他们可以完全解释如何做到这一点,并放置一个警告说: “修改这个方法”,“修改这个其他方法”,“混淆这个变量”,“改变这个和那个的名称”,等等。

有没有一些好心人能够像我五岁那年一样,从头到尾解释一下 如何在 iOS7上本地验证、捆绑收据和应用内购买收据呢?

谢谢! ! !


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

57743 次浏览

下面是我如何在应用程序内购买库 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 打开这个容器。其他人显然已经做到了这一点,纯粹与 系统架构

将 OpenSSL 添加到项目中并非易事,RMStore 维基百科应该会有所帮助。

如果您选择使用 OpenSSL 打开 PKCS7容器,那么您的代码可能是这样的:

+ (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格式表示。它包含一般信息,一些用于验证的字段(我们将在后面讨论) ,以及每个适用于应用程序内购买的具体信息。

在阅读 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;

进行应用内购买

每个应用内购买也在 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 可以帮助您)。

一目了然

现在我们有了收据上的所有字段以及所有应用内购买记录。首先我们验证收据本身,然后我们简单地检查收据是否包含交易的产品。

下面是我们在开始时调用的方法:

- (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. 验证收据是否由苹果公司签署。这是在解析收据之前完成的,详情如下。
  3. 检查收据中包含的 bundle 标识符是否与 bundle 标识符对应。您应该硬编码您的捆绑包标识符,因为修改您的应用程序捆绑包并使用其他收据似乎并不是很困难。
  4. 检查收据中包含的应用程序版本是否对应于您的应用程序版本标识符。你应该硬编码的应用程序版本,出于同样的原因上面指出。
  5. 检查收据散列以确保收据与当前设备对应。

代码中的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步。

验证收据签名

当我们提取数据的时候,我们浏览了一下收据签名验证。收据上有苹果公司的签名。根证书,可从 苹果根证书管理局下载。下面的代码以 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 上验证收据散列的方法:

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

这就是重点。我可能在这里或那里遗漏了一些东西,所以我可能稍后再来看这篇文章。无论如何,我建议浏览完整的代码以了解更多细节。

我很惊讶这里竟然没人提到 收件人。它是一个自动生成模糊收据验证代码的工具,每次都不同; 它支持 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

注意: 不推荐在客户端进行这种类型的验证

这是一个 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
}
}

让我们使用这个 helper 函数来获取特定产品的过期日期。该函数接收一个 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).")
}

您可以从应用程序商店连接获得一个 密码

  • 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

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