此代码(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);
}
}