Java中的Apple Pay支付令牌解密

Posted

技术标签:

【中文标题】Java中的Apple Pay支付令牌解密【英文标题】:Apple Pay Payment Token Decryption in Java 【发布时间】:2019-09-20 01:26:43 【问题描述】:

我正在尝试在服务器端使用 Java 中的 ECC 算法解密 Apple Pay 支付令牌中的 data 字段。如何实现?

我在 Java 中寻找这样的实现已经有一段时间了,但我找不到。相反,我找到了一个使用 Bouncy Castle C# 库来解密令牌的实现: https://github.com/chengbo/ApplePayandroidPayDecryption 虽然Java中也有Bouncy Castle库,但是我发现C#和Java在实现上有些差异,导致我尝试按照上面的C#实现编码时解密失败。 我已经在 Apple Dev Center 中生成了我的证书,并且我很确定解密过程中所需的证书文件是正确的。 有没有人成功解密 Java 中的令牌?任何帮助表示赞赏,非常感谢!

这是我进行测试付款时Apple返回消息的关键部分:

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"

【问题讨论】:

【参考方案1】:

此代码(JAVA)将解密 ApplePay 令牌。要使此代码正常工作,请将证书文件转换为 JKS(检索商家 ID)和 pk8(私钥)格式。

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);



【讨论】:

添加 cmets 以准确解释代码的工作原理,以便所有人都能理解 在哪里可以得到merchantId? developer.apple.com/library/archive/documentation/PassKit/… 在此页面上,它说“明文商家 ID 包含在通用名称的 UID 字段中。”我不明白从哪里得到它,所以我一直在使用我在 Apple Developer 帐户中设置的商家 ID并得到这个错误:线程“主”javax.crypto.AEADBadTagException中的异常:GCM中的mac检查失败 你必须从苹果支付证书中获取商家ID

以上是关于Java中的Apple Pay支付令牌解密的主要内容,如果未能解决你的问题,请参考以下文章

Apple Pay 令牌的交易金额不正确

Google Pay 使用 .net 解密直接令牌

如何在 Google Pay 和 Apple Pay 中为 Authorize.net 生成支付令牌(在移动应用交易中)?

在 Apple Pay 流程之后,支付令牌变得未定义

Apple Pay 令牌 transactionId 是全球唯一的吗?

java使用AES-256-ECB(PKCS7Padding)解密——微信支付退款通知接口指定解密方式