【问题标题】:Apple Pay Payment Token Decryption in JavaJava中的Apple Pay支付令牌解密
【发布时间】: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"}}

【问题讨论】:

    标签: java applepay


    【解决方案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
    猜你喜欢
    • 1970-01-01
    • 2016-09-27
    • 1970-01-01
    • 2020-08-18
    • 2020-11-12
    • 1970-01-01
    • 2019-10-16
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多