java - Java 中的 Apple Pay 支付 token 解密

标签 java applepay

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

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

以下是我进行测试付款时苹果返回消息的关键部分:

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

最佳答案

此代码(JAVA)将解密ApplePay token 。为了使此代码正常工作,请将证书文件转换为 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);
}

}

关于java - Java 中的 Apple Pay 支付 token 解密,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/58020641/

相关文章:

java - Android - Mediaplayer 下一个按钮不起作用

ios - PKPaymentAuthorizationViewController 不是零但没有显示

ios - 苹果和 Stripe : Merchant id/apple pay certificate could not seen in xcode

ios - Apple Pay token 的交易金额不正确

ios - 无法与 lrettig 的 react-native-stripe 模块链接

java - 如何清除 ResourceBundle 缓存

java - 从 arraylist 中删除一个对象而不改变其他对象的索引

java - 网站截图调度——PHP vs JAVA

ios - 检测到 13.56Mhz 时禁用 Apple Pay UI

java - IntelliJ refactor rename太强大了,如何最小化它?