我已经阅读了很多文档和代码,理论上可以验证应用内和/或捆绑收据。
鉴于我对SSL、证书、加密等的了解几乎为零,我读过的所有解释,like this promising one,我发现很难理解。
他们说,解释是不完整的,因为每个人都必须弄清楚如何做到这一点,否则黑客将很容易创建一个黑客应用程序,可以识别和识别模式,并修补应用程序。好吧,我在一定程度上同意这一点。我认为他们可以完全解释如何做到这一点,并放置一个警告,写着“修改这个方法”,“修改另一个方法”,“混淆这个变量”,“更改这个和那个的名称”,等等。
有没有一些好心人能从上到下清楚地解释一下,在我5岁的时候,如何在iOS 7上进行本地验证,捆绑收据和应用内购买收据(好吧,让它变成3)?
谢谢!
如果你有一个在你的应用程序上运行的版本,你担心黑客会看到你是如何做到的,只需在这里发布之前更改你的敏感方法即可。混淆字符串,改变行的顺序,改变循环的方式(从使用for到块枚举,反之亦然)以及诸如此类的事情。显然,使用这里可能发布的代码的每个人都必须做同样的事情,而不是冒着被轻易黑客攻击的风险。
发布于 2013-11-18 11:05:57
下面是我如何在我的应用内购买库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];
}];
获取回单数据
收据是[[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格式表示。它包含一般信息、一些用于验证目的的字段(我们将在稍后讨论)以及每个应用程序内购买的特定信息。
再一次,当涉及到阅读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会帮助您完成此操作)。
验证一目了然
现在我们得到了收据中的所有字段和它的所有应用内购买。首先我们验证收据本身,然后简单地检查收据是否包含交易的产品。
下面是我们在开始回调的方法。来自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;
}
验证回执
验证收据本身归结为:
来自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;
}
让我们深入到步骤2和5。
验证回执签名
当我们提取数据时,我们浏览了一下收据签名验证。收据上有苹果公司的根证书签名,该证书可以从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、包括在收据中的某个不透明值和捆绑id的SHA1。
这就是在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];
}
这就是它的要点。我可能在这里或那里遗漏了一些东西,所以我可能会稍后再回来看看这篇文章。在任何情况下,我都建议您浏览完整的代码以了解更多细节。
发布于 2015-11-04 05:02:24
我很惊讶这里没有人提到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
发布于 2017-06-08 13:10:32
注意:不建议在客户端进行这种类型的验证
这是一个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).")
}
您可以从应用商店连接获取密码。
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
复制密钥并将其粘贴到密码字段中。
https://stackoverflow.com/questions/19943183
复制相似问题