Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Apple Pay Payment Token Decryption in Java

Tags:

java

applepay

I am trying to decrypt the data field inside an Apple Pay Payment Token using ECC algorithm in Java on the server side. How can I implement it?

I've been searching for such an implementation in Java for quite a while and I couldn't find one. Instead, I've found an implementation using Bouncy Castle C# library to decrypt the token: https://github.com/chengbo/ApplePayAndroidPayDecryption Although there is also an Bouncy Castle library in Java, but I've found some difference in the implementation between the C# one and the Java one, which lead to the failure of decryption when I try to code in accordance with the C# implementation above. I've generated my certificate in Apple Dev Center and I'm quite sure that the certificate file needed during the process of decrytion is correct. Has anyone succeeded in decrypting the token in Java? Any help is appreciated, Thanks a lot!

Here is the key part of the return message from Apple when I did a test payment:

passKit={"version":"EC_v1","data":"AK7UZehTHQRXYzgPCD5ijZfloc9ZfUjAutl+7v/83V7U6YjsWSrBVzILQlp2xLP4E4QXxRwadIh0Y9Vg6297BV2ljginDwoR5nneEIQP6fNCXYwll5hUYYlL0ZO7pD/8KXStAh8pnOAyFtEVrDqIRCWZbftzdsAi76qFMXd3Z2bRSjl5zrt8Qfua6Nu1b3MNNVlPQVMJsskEQFncnViNLDkRulgt5WezVF8N1m62nEqminLBF7m+36/pLi0t9JTfqQ0qNYahczAzyyCJhABkXRXXf9iF3YJ77gBD9mBFRVrePPNW0PnJyoQPvDikGzDTc4k5+NBBSEAJjBLlt94okHmh9eO2A9/xoUh7/ktI+Vjk2k+8PWDOAWIkVM4+7vPCrESYedVzTBd6BYIL7+oPmbAW5EJ1JC2twafmmVhL4lXwdz296aBtNDTIzV+of+Oc6JrEutzjVYm8qGdv4MO0DJ3eWG/r9G1QPaTR84CRXXxmoL/EAH9fLYGfQeJsGHmLKieX2b2IfHwTtTnFVloqwt0ywd47PnqLbZ+pETZgsUegZIUAPH6Hl3WMo2eXKbybyxuY70WV+OoIxKBGHQnPYndPA3aG7XeSiUXF/2vW/Qq+UVfxQc0O4X6A/qTYy5c1HlQVq7PloE2+jkGCtKpuvsuVnnRF7sxxG3Wke7Vlz6at/+CHdT0K91+a29U1E8JVwhjnXvT8E/FcvrwHaCMmK1eK8/sMFGQ=","signature":"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","header":{"ephemeralPublicKey":"MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEFnF0WIB3GTpyaP7rgW0kzUMgqfwsTecb7/JrSQXZSuILCBPBs2YQQXFfIHNYtFFMzMTY24/tgbolbKjkmIUwIw==","applicationData":"5cd2d027aa6372ea5420770272ef47a596e60f4299c16c6591c3e7e532208394","publicKeyHash":"sRANn6djBkx5m//vTDU6HFOX4j1Nn/X4bNlgxJYRZgo=","transactionId":"947a5fc21adcc692bd204fa4e1a7a4f83ab8383283f3fa46b204b514559adede"}}
like image 558
Vicson_Chen Avatar asked Jun 30 '26 06:06

Vicson_Chen


1 Answers

This Code(JAVA) will decrypt the ApplePay token. For this code to work convert the certificate file to JKS(retrieve merchant ID) and pk8(private key) formates.

import org.apache.commons.codec.binary.Base64;
import org.apache.commons.io.IOUtils;
import org.apache.commons.lang3.ArrayUtils;
import org.bouncycastle.asn1.ASN1UTCTime;
import org.bouncycastle.asn1.DERSequence;
import org.bouncycastle.asn1.DERSet;
import org.bouncycastle.asn1.cms.CMSAttributes;
import org.bouncycastle.cert.X509CertificateHolder;
import org.bouncycastle.cert.jcajce.JcaX509CertificateConverter;
import org.bouncycastle.cms.CMSProcessableByteArray;
import org.bouncycastle.cms.CMSSignedData;
import org.bouncycastle.cms.SignerInformation;
import org.bouncycastle.cms.SignerInformationStore;
import org.bouncycastle.cms.jcajce.JcaSimpleSignerInfoVerifierBuilder;
import org.bouncycastle.jce.provider.BouncyCastleProvider;
import org.bouncycastle.util.Store;
import org.bouncycastle.util.encoders.Hex;

import javax.crypto.Cipher;
import javax.crypto.KeyAgreement;
import javax.crypto.spec.IvParameterSpec;
import javax.crypto.spec.SecretKeySpec;

import static org.bouncycastle.jce.provider.BouncyCastleProvider.PROVIDER_NAME;

import java.io.*;
import java.nio.charset.Charset;
import java.security.*;
import java.security.cert.*;
import java.security.spec.PKCS8EncodedKeySpec;
import java.security.spec.X509EncodedKeySpec;
import java.util.*;

public class ApplePayDecrypt {
    public static final String MERCHANT_ID = "merchant.Id";

    private static KeyStore keyStore;
    private static PrivateKey merchantPrivateKey;

    static {
        if (Security.getProvider(BouncyCastleProvider.PROVIDER_NAME) == null) {
        Security.addProvider(new BouncyCastleProvider());
        }
    }

    public static AppleDecryptData decrypt(TokenData tokenData) {
        try {
        // Load merchant private key

        byte[] merchantbyte = IOUtils.toByteArray(Application.class.getResource("/apple_pay.pk8"));
        String key = new String(merchantbyte);
        key = key.replace("-----BEGIN PRIVATE KEY-----", "");
        key = key.replace("-----END PRIVATE KEY-----", "");
        key = key.replaceAll("\\s+", "");
        byte[] merchantPrivateKeyBytes = Base64.decodeBase64(key);
        PKCS8EncodedKeySpec pkcs8EncodedKeySpec = new PKCS8EncodedKeySpec(merchantPrivateKeyBytes);
        KeyFactory keyFactory = KeyFactory.getInstance("EC", PROVIDER_NAME);
        merchantPrivateKey = keyFactory.generatePrivate(pkcs8EncodedKeySpec);

        // Load Apple root certificate
        keyStore = KeyStore.getInstance("BKS");
        keyStore.load(GoSecureApplication.class.getResourceAsStream("/appleCA-G3"), "apple123".toCharArray());

        return unwrap(tokenData);
    } catch (Exception e) {
        e.printStackTrace();
        return null;
    }

}

@SuppressWarnings({ "unused", "unchecked" })
public static AppleDecryptData unwrap(TokenData tokenData) throws Exception {
    // Merchants should use the 'version' field to determine how to verify and
    // decrypt the message payload.
    // At this time the only published version is 'EC_v1' which is demonstrated
    // here.
    String version = tokenData.version;

    byte[] signatureBytes = Base64.decodeBase64(tokenData.signature);
    byte[] dataBytes = Base64.decodeBase64(tokenData.data);
    // JsonObject headerJsonObject =
    // jsonObject.get(PAYMENT_HEADER).getAsJsonObject();
    byte[] transactionIdBytes = Hex.decode(tokenData.header.transactionId);
    byte[] ephemeralPublicKeyBytes = Base64.decodeBase64(tokenData.header.ephemeralPublicKey);

    // Merchants that have more than one certificate may use the 'publicKeyHash'
    // field to determine which
    // certificate was used to encrypt this payload.
    byte[] publicKeyHash = Base64.decodeBase64(tokenData.header.publicKeyHash);

    // Application data is a conditional field, present when the merchant has
    // supplied it to the iOS SDK.
    byte[] applicationDataBytes = null;
    byte[] signedBytes = ArrayUtils.addAll(ephemeralPublicKeyBytes, dataBytes);
    signedBytes = ArrayUtils.addAll(signedBytes, transactionIdBytes);
    signedBytes = ArrayUtils.addAll(signedBytes, applicationDataBytes);

    CMSSignedData signedData = new CMSSignedData(new CMSProcessableByteArray(signedBytes), signatureBytes);

    // Check certificate path
    Store<?> certificateStore = signedData.getCertificates();
    List<X509Certificate> certificates = new ArrayList<X509Certificate>();
    JcaX509CertificateConverter certificateConverter = new JcaX509CertificateConverter();
    certificateConverter.setProvider(PROVIDER_NAME);
    for (Object o : certificateStore.getMatches(null)) {
        X509CertificateHolder certificateHolder = (X509CertificateHolder) o;
        certificates.add(certificateConverter.getCertificate(certificateHolder));
    }
    CertificateFactory certificateFactory = CertificateFactory.getInstance("X.509", PROVIDER_NAME);
    CertPath certificatePath = certificateFactory.generateCertPath(certificates);

    PKIXParameters params = new PKIXParameters(keyStore);
    params.setRevocationEnabled(false);

    // TODO: Test certificate has no CRLs. Merchants must perform revocation checks
    // in production.
    // TODO: Verify certificate attributes per instructions at
    // https://developer.apple.com/library/ios/documentation/PassKit/Reference/PaymentTokenJSON/PaymentTokenJSON.html#//apple_ref/doc/uid/TP40014929

    CertPathValidator validator = CertPathValidator.getInstance("PKIX", PROVIDER_NAME);
    PKIXCertPathValidatorResult result = (PKIXCertPathValidatorResult) validator.validate(certificatePath, params);
    System.out.println(result);

    // Verify signature
    SignerInformationStore signerInformationStore = signedData.getSignerInfos();
    boolean verified = false;
    for (Object o : signerInformationStore.getSigners()) {
        SignerInformation signer = (SignerInformation) o;
        Collection<?> matches = certificateStore.getMatches(signer.getSID());
        if (!matches.isEmpty()) {
            X509CertificateHolder certificateHolder = (X509CertificateHolder) matches.iterator().next();
            if (signer.verify(
                    new JcaSimpleSignerInfoVerifierBuilder().setProvider(PROVIDER_NAME).build(certificateHolder))) {
                DERSequence sequence = (DERSequence) signer.getSignedAttributes().get(CMSAttributes.signingTime)
                        .toASN1Primitive();
                DERSet set = (DERSet) sequence.getObjectAt(1);
                ASN1UTCTime signingTime = (ASN1UTCTime) set.getObjectAt(0).toASN1Primitive();
                // Merchants can check the signing time of this payment to determine its
                // freshness.
                System.out.println("Signature verified.  Signing time is " + signingTime.getDate());
                verified = true;
            }
        }
    }

    if (verified) {
        // Ephemeral public key
        KeyFactory keyFactory = KeyFactory.getInstance("EC", PROVIDER_NAME);
        PublicKey ephemeralPublicKey = keyFactory.generatePublic(new X509EncodedKeySpec(ephemeralPublicKeyBytes));

        // Key agreement
        String asymmetricKeyInfo = "ECDH";
        KeyAgreement agreement = KeyAgreement.getInstance(asymmetricKeyInfo, PROVIDER_NAME);
        agreement.init(merchantPrivateKey);
        agreement.doPhase(ephemeralPublicKey, true);
        byte[] sharedSecret = agreement.generateSecret();

        byte[] derivedSecret = performKDF(sharedSecret, extractMerchantIdFromCertificateOid());

        // Decrypt the payment data
        String symmetricKeyInfo = "AES/GCM/NoPadding";
        Cipher cipher = Cipher.getInstance(symmetricKeyInfo, PROVIDER_NAME);

        SecretKeySpec key = new SecretKeySpec(derivedSecret, cipher.getAlgorithm());
        IvParameterSpec ivspec = new IvParameterSpec(new byte[16]);
        cipher.init(Cipher.DECRYPT_MODE, key, ivspec);
        byte[] decryptedPaymentData = cipher.doFinal(dataBytes);

        // JSON payload
        String data = new String(decryptedPaymentData, "UTF-8");
        // System.out.println(data);
        AppleDecryptData decryptDat = ObjMapper.getInstance().readValue(data, AppleDecryptData.class);
        return decryptDat;
    } else {
        return null;
    }
}

private static final byte[] APPLE_OEM = "Apple".getBytes(Charset.forName("US-ASCII"));
private static final byte[] COUNTER = { 0x00, 0x00, 0x00, 0x01 };
private static final byte[] ALG_IDENTIFIER_BYTES = "id-aes256-GCM".getBytes(Charset.forName("US-ASCII"));

/**
 * 00000001_16 || sharedSecret || length("AES/GCM/NoPadding") ||
 * "AES/GCM/NoPadding" || "Apple" || merchantID
 */
private static byte[] performKDF(byte[] sharedSecret, byte[] merchantId) throws Exception {
    ByteArrayOutputStream baos = new ByteArrayOutputStream();
    baos.write(COUNTER);
    baos.write(sharedSecret);
    baos.write(ALG_IDENTIFIER_BYTES.length);
    baos.write(ALG_IDENTIFIER_BYTES);
    baos.write(APPLE_OEM);
    baos.write(merchantId);
    MessageDigest messageDigest = MessageDigest.getInstance("SHA256", PROVIDER_NAME);
    return messageDigest.digest(baos.toByteArray());
}

@SuppressWarnings("unused")
private static byte[] performKDF(byte[] sharedSecret, String merchantId) throws Exception {
    MessageDigest messageDigest = MessageDigest.getInstance("SHA256", PROVIDER_NAME);
    return performKDF(sharedSecret, messageDigest.digest(merchantId.getBytes("UTF-8")));
}

protected static byte[] extractMerchantIdFromCertificateOid() throws Exception {
    KeyStore vkeyStore = KeyStore.getInstance("JKS");
vkeyStore.load(GoSecureApplication.class.getResourceAsStream("/kapple_pay.jks"), "".toCharArray());
    Enumeration<String> aliases = vkeyStore.aliases();
    String alias = null;
    while (aliases.hasMoreElements()) {
        alias = aliases.nextElement();
    }
    X509Certificate cert = (X509Certificate) vkeyStore.getCertificate(alias);
    byte[] merchantIdentifierTlv = cert.getExtensionValue("1.2.840.113635.100.6.32");
    byte[] merchantIdentifier = new byte[64];
    System.arraycopy(merchantIdentifierTlv, 4, merchantIdentifier, 0, 64);

    return Hex.decode(merchantIdentifier);
}

}
like image 147
CHARAN DOMMARA Avatar answered Jul 02 '26 20:07

CHARAN DOMMARA



Donate For Us

If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!