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":"MIAGCSqGSIb3DQEHAqCAMIACAQExDzANBglghkgBZQMEAgEFADCABgkqhkiG9w0BBwEAAKCAMIID7jCCA5SgAwIBAgIIOSxBHvsgmD0wCgYIKoZIzj0EAwIwejEuMCwGA1UEAwwlQXBwbGUgQXBwbGljYXRpb24gSW50ZWdyYXRpb24gQ0EgLSBHMzEmMCQGA1UECwwdQXBwbGUgQ2VydGlmaWNhdGlvbiBBdXRob3JpdHkxEzARBgNVBAoMCkFwcGxlIEluYy4xCzAJBgNVBAYTAlVTMB4XDTE2MDExMTIxMDc0NloXDTIxMDEwOTIxMDc0NlowazExMC8GA1UEAwwoZWNjLXNtcC1icm9rZXItc2lnbl9VQzQtUFJPRF9LcnlwdG9uX0VDQzEUMBIGA1UECwwLaU9TIFN5c3RlbXMxEzARBgNVBAoMCkFwcGxlIEluYy4xCzAJBgNVBAYTAlVTMFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEZuDqDnh9yz9mvFMxidor2gjtlXTkIRF6oa8swxD2qLGco+d+0A+oTo3yrIaI5SmGbnbrrYntpbfDNuDw2KfQXaOCAhEwggINMEUGCCsGAQUFBwEBBDkwNzA1BggrBgEFBQcwAYYpaHR0cDovL29jc3AuYXBwbGUuY29tL29jc3AwNC1hcHBsZWFpY2EzMDIwHQYDVR0OBBYEFFfHNZQqvZ6i/szTy+ft4KN8jMX6MAwGA1UdEwEB/wQCMAAwHwYDVR0jBBgwFoAUI/JJxE+T5O8n5sT2KGw/orv9LkswggEdBgNVHSAEggEUMIIBEDCCAQwGCSqGSIb3Y2QFATCB/jCBwwYIKwYBBQUHAgIwgbYMgbNSZWxpYW5jZSBvbiB0aGlzIGNlcnRpZmljYXRlIGJ5IGFueSBwYXJ0eSBhc3N1bWVzIGFjY2VwdGFuY2Ugb2YgdGhlIHRoZW4gYXBwbGljYWJsZSBzdGFuZGFyZCB0ZXJtcyBhbmQgY29uZGl0aW9ucyBvZiB1c2UsIGNlcnRpZmljYXRlIHBvbGljeSBhbmQgY2VydGlmaWNhdGlvbiBwcmFjdGljZSBzdGF0ZW1lbnRzLjA2BggrBgEFBQcCARYqaHR0cDovL3d3dy5hcHBsZS5jb20vY2VydGlmaWNhdGVhdXRob3JpdHkvMDQGA1UdHwQtMCswKaAnoCWGI2h0dHA6Ly9jcmwuYXBwbGUuY29tL2FwcGxlYWljYTMuY3JsMA4GA1UdDwEB/wQEAwIHgDAPBgkqhkiG92NkBh0EAgUAMAoGCCqGSM49BAMCA0gAMEUCIESIU8bEgwEjtEq2dDbRO+C10CsxjVVVISgpzdjEylGWAiEAkOZ+sj5vSzNlDlOy5vyJ5ZO3b5G5PpnvwJx1gc4A9eYwggLuMIICdaADAgECAghJbS+/OpjalzAKBggqhkjOPQQDAjBnMRswGQYDVQQDDBJBcHBsZSBSb290IENBIC0gRzMxJjAkBgNVBAsMHUFwcGxlIENlcnRpZmljYXRpb24gQXV0aG9yaXR5MRMwEQYDVQQKDApBcHBsZSBJbmMuMQswCQYDVQQGEwJVUzAeFw0xNDA1MDYyMzQ2MzBaFw0yOTA1MDYyMzQ2MzBaMHoxLjAsBgNVBAMMJUFwcGxlIEFwcGxpY2F0aW9uIEludGVncmF0aW9uIENBIC0gRzMxJjAkBgNVBAsMHUFwcGxlIENlcnRpZmljYXRpb24gQXV0aG9yaXR5MRMwEQYDVQQKDApBcHBsZSBJbmMuMQswCQYDVQQGEwJVUzBZMBMGByqGSM49AgEGCCqGSM49AwEHA0IABPAXEYQZ12SF1RpeJYEHduiAou/ee65N4I38S5PhM1bVZls1riLQl3YNIk57ugj9dhfOiMt2u2ZwvsjoKYT/VEWjgfcwgfQwRgYIKwYBBQUHAQEEOjA4MDYGCCsGAQUFBzABhipodHRwOi8vb2NzcC5hcHBsZS5jb20vb2NzcDA0LWFwcGxlcm9vdGNhZzMwHQYDVR0OBBYEFCPyScRPk+TvJ+bE9ihsP6K7/S5LMA8GA1UdEwEB/wQFMAMBAf8wHwYDVR0jBBgwFoAUu7DeoVgziJqkipnevr3rr9rLJKswNwYDVR0fBDAwLjAsoCqgKIYmaHR0cDovL2NybC5hcHBsZS5jb20vYXBwbGVyb290Y2FnMy5jcmwwDgYDVR0PAQH/BAQDAgEGMBAGCiqGSIb3Y2QGAg4EAgUAMAoGCCqGSM49BAMCA2cAMGQCMDrPcoNRFpmxhvs1w1bKYr/0F+3ZD3VNoo6+8ZyBXkK3ifiY95tZn5jVQQ2PnenC/gIwMi3VRCGwowV3bF3zODuQZ/0XfCwhbZZPxnJpghJvVPh6fRuZy5sJiSFhBpkPCZIdAAAxggGMMIIBiAIBATCBhjB6MS4wLAYDVQQDDCVBcHBsZSBBcHBsaWNhdGlvbiBJbnRlZ3JhdGlvbiBDQSAtIEczMSYwJAYDVQQLDB1BcHBsZSBDZXJ0aWZpY2F0aW9uIEF1dGhvcml0eTETMBEGA1UECgwKQXBwbGUgSW5jLjELMAkGA1UEBhMCVVMCCDksQR77IJg9MA0GCWCGSAFlAwQCAQUAoIGVMBgGCSqGSIb3DQEJAzELBgkqhkiG9w0BBwEwHAYJKoZIhvcNAQkFMQ8XDTE5MDkxNzA2MzEyOVowKgYJKoZIhvcNAQk0MR0wGzANBglghkgBZQMEAgEFAKEKBggqhkjOPQQDAjAvBgkqhkiG9w0BCQQxIgQgi0pw8YTdD5wAw9Wct6Io9DQGiB1iXyGcK9XCWnSu/08wCgYIKoZIzj0EAwIERzBFAiEA+H89sz2Jo8GPM86s7sZ7nQ1RKu/R9I0fkkRBclcppFICIGJbrR764YuHK7ptg9Ch50muHKEuYUa0BjsVhtgCgJvyAAAAAAAA","header":{"ephemeralPublicKey":"MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEFnF0WIB3GTpyaP7rgW0kzUMgqfwsTecb7/JrSQXZSuILCBPBs2YQQXFfIHNYtFFMzMTY24/tgbolbKjkmIUwIw==","applicationData":"5cd2d027aa6372ea5420770272ef47a596e60f4299c16c6591c3e7e532208394","publicKeyHash":"sRANn6djBkx5m//vTDU6HFOX4j1Nn/X4bNlgxJYRZgo=","transactionId":"947a5fc21adcc692bd204fa4e1a7a4f83ab8383283f3fa46b204b514559adede"}}
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);
}
}
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With