다음은 인앱 구매 라이브러리 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];
}
그리고 그것은 그것의 요지입니다. 나는 여기 또는 저기에 뭔가 빠져있을 수 있으므로 나중에이 게시물로 돌아올 수 있습니다. 어쨌든 자세한 내용은 전체 코드를 찾아 보는 것이 좋습니다.