在iOS 7上本地validation应用内收据和捆绑收据的完整解决scheme
我已经阅读了许多文档和代码,理论上将validation应用程序和/或捆绑收据。
鉴于我对SSL,证书,encryption等方面的知识几乎为零,所有我已经读过的解释, 就像这个有希望的解释一样 ,我发现很难理解。
他们说这些解释是不完整的,因为每个人都必须弄清楚如何去做,或者黑客可以轻松创build一个可以识别和识别模式并修补应用程序的破解程序。 好的,我同意这一点。 我想他们可以完全解释如何做,并发出一个警告,说“修改这个方法”,“修改这个方法”,“混淆这个variables”,“改变这个和那个名字”等等。
能不能有一个好的灵魂来解释如何在iOS 7上本地validation,绑定收据和应用内购买收据,因为我从五岁(从上到下)显然是自上而下的?
谢谢!!!
如果你的应用有一个版本,而你的担心是黑客会看到你是如何做到的,那么只需要在这里发布之前改变你的敏感方法。 混淆string,改变行的顺序,改变你做循环的方式(从使用到阻止枚举,反之亦然)以及类似的东西。 很明显,每个使用可能在这里发布的代码的人都必须做同样的事情,而不是冒险被轻易入侵。
以下是我在应用内购买库RMStore中解决此问题的演练。 我将解释如何validation交易,其中包括validation整个收据。
乍看上去
获取收据并validation交易。 如果失败,请刷新收据并重试。 这使得validation过程asynchronous,因为刷新收据是asynchronous的。
来自RMStoreAppReceiptVerificator :
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 wiki应该有所帮助。
如果你select使用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; }
我们稍后会详细介绍validation的细节。
获取收据字段
收据以ASN1格式表示。 它包含一般信息,一些用于validation的字段(我们将在稍后介绍)以及每个适用的应用程序内购买的具体信息。
同样,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中。 parsing它与parsing一般收据信息非常相似。
从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帮助您)。
validation一目了然
现在我们从收据和所有的应用程序内购买了所有的领域。 首先我们validation收据本身,然后检查收据是否包含交易的产品。
下面是我们在开始callback的方法。 来自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; }
validation收据
validation收据本身归结为:
- 检查收据是否有效PKCS7和ASN1。 我们已经隐含地做了这个。
- validation收据是由Apple签署的。 这是在parsing收据之前完成的,下面会详细介绍。
- 检查包含在收据中的包标识符是否与您的包标识符相对应。 您应该对您的包标识符进行硬编码,因为修改您的应用程序包并使用其他收据似乎不是很困难。
- 检查收据中包含的应用程序版本是否与您的应用程序版本标识符相对应。 您应该硬编码的应用程序版本,出于上述相同的原因。
- 检查收据散列以确保收据对应于当前设备。
来自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。
validation收据签名
当我们提取数据时,我们浏览了收据签名validation。 收据用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; }
这是在收据被parsing之前的一开始完成的。
validation收据散列
包含在收据中的散列是设备ID的SHA1,包含在收据中的一些不透明值以及包ID。
这就是你将如何validationiOS上的收据散列。 从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]; }
这就是它的要点。 我可能会在这里或那里丢失一些东西,所以我可能会在以后回到这个post。 无论如何,我build议浏览完整的代码以获取更多详细信息。
我很惊讶没有人在这里提到Receigen 。 这是一个自动生成混淆收据validation码的工具,每次都有一个不同的validation码。 它支持GUI和命令行操作。 强烈推荐。
(不隶属于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
嗨这是用于validation应用内购买收据的Swift 3版本…
从您的AppDelegate
调用receiptValidation()
函数或从您想要的所有位置调用。
func receiptValidation() { if let appStoreReceiptURL = Bundle.main.appStoreReceiptURL, FileManager.default.fileExists(atPath: appStoreReceiptURL.path) { do { let receiptData = try Data(contentsOf: appStoreReceiptURL, options: .alwaysMapped) let receiptString = receiptData.base64EncodedString(options: []) let dict = ["receipt-data" : receiptString, "password" : "**************************"] as [String : Any] do { let jsonData = try JSONSerialization.data(withJSONObject: dict, options: .prettyPrinted) //This Url for original Account //let url : String = "https://buy.itunes.apple.com/verifyReceipt" //This Url for Sandbox Testing Account let url : String = "https://sandbox.itunes.apple.com/verifyReceipt" if let sandboxURL = Foundation.URL(string:url) { var request = URLRequest(url: sandboxURL) request.httpMethod = "POST" request.httpBody = jsonData let session = URLSession(configuration: URLSessionConfiguration.default) let task = session.dataTask(with: request) { data, response, error in if let receivedData = data, let httpResponse = response as? HTTPURLResponse, error == nil, httpResponse.statusCode == 200 { do { if let jsonResponse = try JSONSerialization.jsonObject(with: receivedData, options: JSONSerialization.ReadingOptions.mutableContainers) as? Dictionary<String, AnyObject> { if let expirationDate: NSDate = self.expirationDateFromResponse(jsonResponse: jsonResponse as NSDictionary) { let currentDate = self.getCurrentLocalDateApp() if currentDate > expirationDate as Date { self.downgrade("1") }else{ } } } else { } } catch { } }else { print("Error=\(String(describing: error))") } } task.resume() } else { } } catch { } } catch { } } }
现在我们有另外一个函数从收据expirationDateFromResponse()
获取一个date。 这个函数会在同一个控制器或AppDelegate
func expirationDateFromResponse(jsonResponse: NSDictionary) -> NSDate? { if let receiptInfo: NSArray = jsonResponse["latest_receipt_info"] as? NSArray { let lastReceipt = receiptInfo.lastObject as! NSDictionary let formatter = DateFormatter() formatter.dateFormat = "yyyy-MM-dd HH:mm:ss VV" let expirationDate: NSDate = formatter.date(from: lastReceipt["expires_date"] as! String) as NSDate! formatter.dateStyle = .medium let stringOutput = formatter.string(from: expirationDate as Date) formatter.dateFormat = "yyyy-MM-dd HH:mm:ss" let date = formatter.string(from: expirationDate as Date) print("Date=\(date)") self.iosNextBillingDateEntry(date) UserDefaults.standard.set(stringOutput, forKey: "PLAN_EXP_DATE") return expirationDate } else { return nil } }
现在我们有另一个获取本地date时间的function,如果您需要的话。
func getCurrentLocalDateApp()-> Date { var now = Date() var nowComponents = DateComponents() let calendar = Calendar.current nowComponents.year = (Calendar.current as NSCalendar).component(NSCalendar.Unit.year, from: now) nowComponents.month = (Calendar.current as NSCalendar).component(NSCalendar.Unit.month, from: now) nowComponents.day = (Calendar.current as NSCalendar).component(NSCalendar.Unit.day, from: now) nowComponents.hour = (Calendar.current as NSCalendar).component(NSCalendar.Unit.hour, from: now) nowComponents.minute = (Calendar.current as NSCalendar).component(NSCalendar.Unit.minute, from: now) nowComponents.second = (Calendar.current as NSCalendar).component(NSCalendar.Unit.second, from: now) nowComponents.timeZone = TimeZone(abbreviation: "VV") now = calendar.date(from: nowComponents)! return now }
您将从苹果商店获得的密码 。
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
复制该密钥并粘贴到密码字段中。
希望这将有助于迅速版本上的每一个谁想要的。