Una solución completa para validar LOCALMENTE los recibos integrados en la aplicación y agrupar los recibos en iOS 7


He leído muchos documentos y código que en teoría validarán un recibo in-app y/o bundle.

Dado que mi conocimiento de SSL, certificados, cifrado, etc. es casi cero, todas las explicaciones que he leído, como este prometedor, he encontrado difícil de entender.

Dicen que las explicaciones están incompletas porque cada persona tiene que averiguar cómo hacerlo, o los hackers tendrán un trabajo fácil creando una aplicación de cracker que pueda reconocer y identificar patrones y parchear la aplicación. OK, estoy de acuerdo con esto hasta cierto punto. Creo que podrían explicar completamente cómo hacerlo y poner una advertencia diciendo "modificar este método", "modificar este otro método", "ofuscar esta variable", "cambiar el nombre de esto y aquello", etc.

¿Puede algún alma buena por ahí ser lo suficientemente amable para explicar cómo validar LOCALMENTE, agrupar recibos y recibos de compra en la aplicación en iOS 7 como tengo cinco años (ok, que sea 3), de arriba a abajo abajo, ¿está claro?

Gracias!!!


Si tiene una versión que funciona en sus aplicaciones y sus preocupaciones son que los hackers verán cómo lo hizo, simplemente cambie sus métodos sensibles antes de publicar aquí. Ofuscar cadenas, cambiar el orden de las líneas, cambiar la forma de hacer bucles (de usar para para bloquear la enumeración y viceversa) y cosas por el estilo. Obviamente, cada persona que utiliza el código que se puede publicar aquí, tiene que hacer lo mismo, no correr el riesgo de ser fácilmente hackear.

Author: smileBot, 2013-11-13

3 answers

Aquí hay un tutorial de cómo resolví esto en mi biblioteca de compras in-app RMStore. Explicaré cómo verificar una transacción, lo que incluye verificar todo el recibo.

De un vistazo

Obtenga el recibo y verifique la transacción. Si falla, actualice el recibo e inténtelo de nuevo. Esto hace que el proceso de verificación sea asíncrono, ya que actualizar el recibo es asíncrono.

De Rmstoreapreceiptverifier :

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];
}];

Conseguir el datos de recepción

El recibo está en [[NSBundle mainBundle] appStoreReceiptURL] y en realidad es un contenedor PCKS7. Apesto en criptografía así que usé OpenSSL para abrir este contenedor. Otros aparentemente lo han hecho puramente con marcos de sistema.

Agregar OpenSSL a su proyecto no es trivial. El wiki de RMStore debería ayudar.

Si opta por usar OpenSSL para abrir el contenedor PKCS7, su código podría verse así. From 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;
}

Vamos a entrar en el detalles de la verificación más adelante.

Obteniendo los campos de recibo

El recibo se expresa en formato ASN1. Contiene información general, algunos campos para fines de verificación (hablaremos de eso más adelante) e información específica de cada compra dentro de la aplicación aplicable.

De nuevo, OpenSSL viene al rescate cuando se trata de leer ASN1. Desde RMAppReceipt , usando algunos métodos auxiliares:

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;

Obteniendo las compras in-app

Cada la compra en la aplicación también está en ASN1. El análisis es muy similar al análisis de la información general de recepción.

De RMAppReceipt, usando los mismos métodos auxiliares:

[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;
        }
    }
}]; 

Debe tenerse en cuenta que ciertas compras en la aplicación, como consumibles y suscripciones no renovables, aparecerán solo una vez en el recibo. Usted debe verificar estos inmediatamente después de la compra (de nuevo, RMStore le ayuda con esto).

Verificación de un vistazo

Ahora tenemos todos los campos del recibo y todas sus compras in-app. Primero verificamos el recibo en sí, y luego simplemente verificamos si el recibo contiene el producto de la transacción.

A continuación se muestra el método que llamamos al principio. Desde 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;
}

Verificando el recibo

Verificar el recibo en sí se reduce a:

  1. Comprobando que el recibo es válido PKCS7 y ASN1. Hemos hecho esto implícitamente ya.
  2. Verificando que el recibo está firmado por Apple. Esto se hizo antes de analizar el recibo y se detallará a continuación.
  3. Comprobar que el identificador de paquete incluido en el recibo corresponde a su identificador de paquete. Debe codificar su identificador de paquete, ya que no parece ser muy difícil modificar su paquete de aplicaciones y usar algún otro recibo.
  4. Comprobar que la versión de la aplicación incluida en el recibo corresponde a su identificador de versión de la aplicación. Debe codificar la versión de la aplicación, por las mismas razones indicadas anteriormente.
  5. Compruebe el hash de recibo para asegurarse de que el recibo corresponde al dispositivo actual.

Los 5 pasos en el código en un nivel alto, desde 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;
}

Vamos a profundizar en los pasos 2 y 5.

Verificando la firma de recepción

Cuando extraímos los datos echamos un vistazo a la verificación de la firma del recibo. El recibo está firmado con the Apple Inc. Certificado raíz, que se puede descargar desde Autoridad de Certificación Raíz de Apple . El siguiente código toma el contenedor PKCS7 y el certificado raíz como datos y comprueba si coinciden:

+ (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;
}

Esto se hizo al principio, antes de que se analizara el recibo.

Verificando el hash de recepción

El hash incluido en el recibo es un SHA1 del id del dispositivo, algún valor opaco incluido en el recibo y el id del paquete.

Así es como verificarías el hash de recibo en iOS. From 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];
}

Y eso es lo esencial. Puede que me esté perdiendo algo aquí o allá, así que podría volver a este post más tarde. En cualquier caso, recomiendo navegar por el código completo para más detalles.

 137
Author: hpique,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/ajaxhispano.com/template/agent.layouts/content.php on line 61
2018-01-10 05:25:18

Me sorprende que nadie haya mencionado Receigen aquí. Es una herramienta que genera automáticamente código de validación de recibos ofuscado, uno diferente cada vez; soporta tanto GUI como operaciones de línea de comandos. Muy recomendable.

(No afiliado con Receigen, solo un usuario feliz.)

Uso un Rakefile como este para volver a ejecutar Receigen automáticamente (porque debe hacerse en cada cambio de versión) cuando escribo 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
 10
Author: Andrey Tarantsov,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/ajaxhispano.com/template/agent.layouts/content.php on line 61
2015-11-03 21:02:24

Nota: No se recomienda hacer este tipo de verificación en el lado del cliente

Esta es una versión Swift 4 para la validación del recibo de compra en la aplicación...

Vamos a crear una enumeración para representar los posibles errores de la validación de recibos

enum ReceiptValidationError: Error {
    case receiptNotFound
    case jsonResponseIsNotValid(description: String)
    case notBought
    case expired
}

Entonces vamos a crear la función que valida el recibo, lanzará un error si no puede validarlo.

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
    }
}

Usemos esta función auxiliar, para obtener la caducidad fecha de un producto específico. La función recibe una respuesta JSON y un id de producto. La respuesta JSON puede contener información de varios recibos para diferentes productos, por lo que obtiene la última información para el parámetro especificado.

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
}

Ahora puede llamar a esta función y manejar los posibles casos de error

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).")
}

Puedes obtener una contraseña desde App Store Connect. https://developer.apple.com abra este enlace haga clic en

  • 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

Copie esa clave y péguela en el campo de contraseña.

Espero que esto ayude a todos los que quieran eso en la versión swift.

 3
Author: APK APPS,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/ajaxhispano.com/template/agent.layouts/content.php on line 61
2018-07-17 14:37:10