J'ai lu beaucoup de documents et de codes qui, en théorie, valideront un reçu intégré à l'application et/ou un paquet.
Étant donné que ma connaissance de SSL, des certificats, du cryptage, etc. est presque nulle, toutes les explications que j'ai lues, comme celle-ci est prometteuse , j'ai trouvé difficile à comprendre.
Ils expliquent que les explications sont incomplètes, car chaque personne doit trouver le moyen de le faire. Sinon, les pirates auront la tâche facile de créer une application de piratage capable de reconnaître et d’identifier les schémas et de corriger l’application. OK, je suis d'accord avec cela jusqu'à un certain point. Je pense qu'ils pourraient expliquer complètement comment faire cela et mettre un avertissement disant "modifiez cette méthode", "modifiez cette autre méthode", "masquer cette variable", "changez le nom de ceci et cela", etc.
Une bonne âme peut-elle avoir la gentillesse d’expliquer comment valider LOCALEMENT, regrouper les reçus et les reçus d’achats intégrés sur iOS 7 alors que je suis dans cinq ans vieux (ok, fais-en 3), de haut en bas, clairement?
Merci!!!
Si vous avez une version qui fonctionne sur vos applications et que vous craignez que les pirates informatiques ne voient comment vous y êtes parvenu, il vous suffit de modifier vos méthodes sensibles avant de publier ici. Brouiller les chaînes, changer l'ordre des lignes, changer votre façon de faire des boucles (d'utiliser pour pour bloquer l'énumération et vice-versa) et ainsi de suite. De toute évidence, chaque personne qui utilise le code qui peut être affiché ici doit faire la même chose, sans risquer d’être facilement piratée.
Voici un exemple de la façon dont j'ai résolu ce problème dans ma bibliothèque d'achats intégrés RMStore . Je vais expliquer comment vérifier une transaction, ce qui inclut la vérification de tout le reçu.
Obtenez le reçu et vérifiez la transaction. Si cela échoue, actualisez le reçu et réessayez. Cela rend le processus de vérification asynchrone car l'actualisation de l'accusé de réception est asynchrone.
De 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];
}];
Le reçu est en [[NSBundle mainBundle] appStoreReceiptURL]
et est en fait un conteneur PCKS7. J'adore la cryptographie et j'ai donc utilisé OpenSSL pour ouvrir ce conteneur. D'autres l'ont apparemment fait uniquement avec des cadres système .
Ajouter OpenSSL à votre projet n’est pas une mince affaire. Le wiki RMStore devrait vous aider.
Si vous choisissez d'utiliser OpenSSL pour ouvrir le conteneur PKCS7, votre code pourrait ressembler à ceci. De 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;
}
Nous entrerons dans les détails de la vérification plus tard.
Le reçu est exprimé au format ASN1. Il contient des informations générales, des champs à des fins de vérification (nous y reviendrons plus tard) et des informations spécifiques sur chaque achat intégré appliqué.
Encore une fois, OpenSSL vient à la rescousse quand il s’agit de lire ASN1. De RMAppReceipt , à l'aide de quelques méthodes d'assistance:
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;
Chaque achat intégré est également en ASN1. Son analyse est très similaire à celle des informations de réception générales.
De RMAppReceipt , en utilisant les mêmes méthodes d'assistance:
[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;
}
}
}];
Il convient de noter que certains achats intégrés, tels que les consommables et les abonnements non renouvelables, n'apparaîtront qu'une seule fois sur le reçu. Vous devriez les vérifier juste après l'achat (encore une fois, RMStore vous aide à cela).
Nous avons maintenant tous les champs du reçu et de tous ses achats intégrés. Nous vérifions d'abord le reçu lui-même, puis nous vérifions simplement si le reçu contient le produit de la transaction.
Ci-dessous, la méthode que nous avons rappelée au début. De 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;
}
La vérification du reçu lui-même revient à:
Les 5 étapes du code à un niveau élevé, à partir de 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;
}
Explorons les étapes 2 et 5.
Lorsque nous avons extrait les données, nous avons survolé la vérification de la signature du reçu. Le reçu est signé avec le certificat racine Apple Inc., qui peut être téléchargé à partir de Autorité de certification racine Apple . Le code suivant prend le conteneur PKCS7 et le certificat racine en tant que données et vérifie s'ils correspondent:
+ (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;
}
Cela a été fait au début, avant que le reçu ne soit analysé.
Le hachage inclus dans le reçu est un SHA1 de l'ID de périphérique, une valeur opaque incluse dans le reçu et l'ID de l'ensemble.
Voici comment vérifier le hachage de réception sur iOS. De 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];
}
Et c'est l'essentiel. Il se peut que je manque quelque chose ici ou là, alors je pourrais revenir à ce post plus tard. Dans tous les cas, je vous recommande de parcourir le code complet pour plus de détails.
Je suis surpris que personne ne mentionne Receigen ici. C'est un outil qui génère automatiquement un code de validation des reçus masqués, différent à chaque fois; il supporte à la fois l'interface graphique et les opérations en ligne de commande. Hautement recommandé.
(Non affilié à Receigen, juste un utilisateur heureux.)
J'utilise un fichier Rakefile comme celui-ci pour réexécuter automatiquement Receigen (car cela doit être fait à chaque changement de version) lorsque je tape 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
Remarque: il n'est pas recommandé d'effectuer ce type de vérification côté client.
Ceci est une version Swift 4 pour la validation du reçu d'achat intégré à l'application ...
Permet de créer une énumération pour représenter les erreurs possibles de la validation de la réception
enum ReceiptValidationError: Error {
case receiptNotFound
case jsonResponseIsNotValid(description: String)
case notBought
case expired
}
Ensuite, créons la fonction qui valide le reçu, une erreur sera renvoyée s’il ne parvient pas à le valider.
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
}
}
Utilisons cette fonction d'assistance pour obtenir la date d'expiration d'un produit spécifique. La fonction reçoit une réponse JSON et un identifiant de produit. La réponse JSON peut contenir plusieurs informations de réception pour différents produits. Elle obtient donc les dernières informations du paramètre spécifié.
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
}
Vous pouvez maintenant appeler cette fonction et gérer les erreurs possibles
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).")
}
Vous pouvez obtenir un mot de passe depuis l'App Store Connect.
https://developer.Apple.com
ouvrez ce lien cliquez sur
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
Copiez cette clé et collez-la dans le champ du mot de passe.
J'espère que cela aidera pour tous ceux qui le souhaitent dans la version Swift version.