이론적으로 인앱 및 / 또는 번들 영수증의 유효성을 검사하는 많은 문서와 코드를 읽었습니다.
SSL, 인증서, 암호화 등에 대한 나의 지식이 거의 제로라고 가정 할 때, 이 유망한 설명 과 같이 읽은 모든 설명은 이해하기 어렵다는 것을 알게되었습니다.
그들은 모든 사람이 어떻게해야하는지 설명해야하기 때문에 설명이 불완전하다고 말합니다. 그렇지 않으면 해커가 패턴을 인식하고 식별하고 응용 프로그램을 패치 할 수있는 크래커 앱을 쉽게 만들 수 있습니다. 좋아, 나는 특정 시점까지 이것에 동의합니다. 나는 그들이 어떻게 해야하는지 완전히 설명하고 “이 방법 수정”, “이 다른 방법 수정”, “이 변수를 난독 화하다”, “이 이름과 그 이름 변경”등의 경고를 넣을 수 있다고 생각합니다.
iOS 7 에서 5 세 (OK, 3)로 위아래에서 명확하게 LOCALLY 유효성 검사, 번들 영수증 및 인앱 구매 영수증을 확인 하는 방법을 충분히 설명 할 수 있을까요?
감사!!!
앱에서 작동하는 버전이 있고 해커가 자신의 방식을 알게 될 우려가있는 경우 여기에 게시하기 전에 민감한 방법을 변경하면됩니다. 문자열을 난독 화하고, 줄 순서를 변경하고, 루프 사용 방식을 변경합니다 (for 사용에서 열거 및 그 반대로). 분명히, 여기에 게시 된 코드를 사용하는 모든 사람은 쉽게 해킹 당할 위험이 없도록 동일한 작업을 수행해야합니다.
답변
다음은 인앱 구매 라이브러리 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 컨테이너를 열도록 선택하면 코드는 다음과 같습니다. 에서 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;
}
영수증 확인
영수증 자체를 확인하면 다음과 같이 요약됩니다.
- 영수증이 유효한 PKCS7 및 ASN1인지 확인 우리는 이것을 이미 암시 적으로 수행했습니다.
- Apple이 영수증에 서명했는지 확인 이는 영수증을 파싱하기 전에 수행되었으며 아래에 자세히 설명되어 있습니다.
- 영수증에 포함 된 번들 식별자가 번들 식별자에 해당하는지 확인 번들 번들 식별자는 앱 번들을 수정하고 다른 영수증을 사용하는 것이 어렵지 않은 것처럼 보이기 때문에 하드 코딩해야합니다.
- 영수증에 포함 된 앱 버전이 앱 버전 식별자와 일치하는지 확인합니다. 위에 표시된 것과 동일한 이유로 앱 버전을 하드 코딩해야합니다.
- 영수증이 현재 장치와 일치하는지 확인 해시를 확인하십시오.
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 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;
}
영수증을 파싱하기 전에 처음부터 다시 수행했습니다.
영수증 해시 확인
영수증에 포함 된 해시는 장치 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
해당 키를 복사하여 비밀번호 필드에 붙여 넣습니다.
이것이 빠른 버전으로 원하는 모든 사람들에게 도움이되기를 바랍니다.