package com.payomo.services; import java.io.DataInputStream; import java.io.FileInputStream; import java.io.IOException; import java.security.InvalidAlgorithmParameterException; import java.security.InvalidKeyException; import java.security.Key; import java.security.KeyFactory; import java.security.NoSuchAlgorithmException; import java.security.spec.InvalidKeySpecException; import java.security.spec.PKCS8EncodedKeySpec; import javax.crypto.BadPaddingException; import javax.crypto.Cipher; import javax.crypto.IllegalBlockSizeException; import javax.crypto.NoSuchPaddingException; import javax.crypto.spec.GCMParameterSpec; import javax.crypto.spec.SecretKeySpec; import org.apache.commons.codec.binary.Base64; import org.bouncycastle.crypto.BufferedBlockCipher; import org.bouncycastle.crypto.CipherParameters; import org.bouncycastle.crypto.InvalidCipherTextException; import org.bouncycastle.jce.provider.BouncyCastleProvider; import com.fasterxml.jackson.databind.ObjectMapper; import com.google.common.io.BaseEncoding; import com.payomo.dto.PaymentData; public class ApplePayDecryptService { // Apple Pay uses an 0s for the IV(initialization vector) private static final byte[] IV = { 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00 }; /** * Restore the symmetric key For RSA (RSA_v1), the symmetric key is encrypted by * a merchant’s public key using the RSA/ECB/OAEPWithSHA256AndMGF1Padding * algorithm. Use your RSA private key to decrypt the wrapped key blob and * access the symmetric key. * * @param wrappedKeyBytes * @param privKey * @return * @throws IOException * @throws NoSuchAlgorithmException * @throws InvalidKeySpecException * @throws NoSuchPaddingException * @throws InvalidKeyException * @throws InvalidAlgorithmParameterException * @throws IllegalBlockSizeException * @throws BadPaddingException */ public static byte[] restoreSymmetricKey(byte[] wrappedKeyBytes, byte[] privKey) throws IOException, NoSuchAlgorithmException, InvalidKeySpecException, NoSuchPaddingException, InvalidKeyException, InvalidAlgorithmParameterException, IllegalBlockSizeException, BadPaddingException { PKCS8EncodedKeySpec pkcs8KeySpec = new PKCS8EncodedKeySpec(privKey); KeyFactory keyFactory = KeyFactory.getInstance("RSA"); Key privateKey = keyFactory.generatePrivate(pkcs8KeySpec); Cipher cipher = Cipher.getInstance("RSA/ECB/OAEPWithSHA-256AndMGF1Padding", new BouncyCastleProvider()); cipher.init(Cipher.DECRYPT_MODE, privateKey); return cipher.doFinal(wrappedKeyBytes); } public byte[] process(byte[] input, BufferedBlockCipher bufferedBlockCipher, CipherParameters cipherParameters, boolean forEncryption) throws InvalidCipherTextException { bufferedBlockCipher.init(forEncryption, cipherParameters); byte[] rv = new byte[bufferedBlockCipher.getOutputSize(input.length)]; int tam = bufferedBlockCipher.processBytes(input, 0, input.length, rv, 0); try { bufferedBlockCipher.doFinal(rv, tam); return rv; } catch (Exception e) { e.printStackTrace(); } return rv; } /** * Use the symmetric key to decrypt the value of the data key. For RSA (RSA_v1), * Decrypt the data key using AES–128 (id-aes128-GCM 2.16.840.1.101.3.4.1.6), * with an initialization vector of 16 null bytes and no associated * authentication data. * * @param symmetricKeyBytes * @param dataBytes * @return * @throws NoSuchAlgorithmException * @throws NoSuchPaddingException * @throws InvalidKeyException * @throws IllegalBlockSizeException * @throws BadPaddingException * @throws InvalidAlgorithmParameterException */ public static byte[] decryptData(byte[] symmetricKeyBytes, byte[] dataBytes) throws NoSuchAlgorithmException, NoSuchPaddingException, InvalidKeyException, IllegalBlockSizeException, BadPaddingException, InvalidAlgorithmParameterException { SecretKeySpec secretKeySpec = new SecretKeySpec(symmetricKeyBytes, "AES"); // Cipher cipher = Cipher.getInstance("AES/GCM/NoPadding"); // this line works fine in JDK8 Cipher cipher = Cipher.getInstance("AES/GCM/NoPadding", new BouncyCastleProvider()); cipher.init(Cipher.DECRYPT_MODE, secretKeySpec, new GCMParameterSpec(128, IV)); return cipher.doFinal(dataBytes); } public static void main(String[] args) throws Exception { ApplePayDecryptService service = new ApplePayDecryptService(); service.testVerify(); } private ObjectMapper jsonMapper = new ObjectMapper(); // @Test public void testVerify() throws Exception { String tokenStr = "{\n" + " \"data\": \"W7ak9BW5yI1z/G/BJAQXNNBEa2pnMZ/KEueOS6brAjAqmgT91LRep7AXd5huEADg5jgrcWCedDCgXy8pTug0D\",\n" + " \"signature\": \"MIAGCSqGSIb3DQEHAqCAMIACAQExDzANBglghkgBZQMEAgEFADCABgkqhkiG9w0BBwEA\",\n" + " \"header\": {\n" + " \"publicKeyHash\": \"d51c+I13cskmxZ32YpsSXT6ZvOkjzGQ3liIGwHI+TZc=\",\n" + " \"transactionId\": \"0a36576e4dcd4bd9ac155c79ce7fd5cebc7597b503969f3e1fa1ec927885fcbc\",\n" + " \"wrappedKey\": \"xf5VxMQvTtns7DOPjhmRaXoXMdU6W7GQRmRjHkcCvqqA4OI/\"\n" + " },\n" + " \"version\": \"RSA_v1\"\n" + " }"; PaymentData paymentData = jsonMapper.readValue(tokenStr, PaymentData.class); byte[] symmetricKeyBytes = restoreSymmetricKey(Base64.decodeBase64(paymentData.getHeader().getWrappedKey()), getPrivateKey("merchant_identity.key")); System.out.println(BaseEncoding.base16().encode(symmetricKeyBytes)); byte[] decryptedDataBytes = decryptData(symmetricKeyBytes, Base64.decodeBase64(paymentData.getData())); System.out.println(new String(decryptedDataBytes, "UTF-8")); } private byte[] getPrivateKey(String privateKeyFile) throws IOException { System.out.println(privateKeyFile); DataInputStream dis = new DataInputStream( new FileInputStream(this.getClass().getClassLoader().getResource(privateKeyFile).getFile())); byte[] priKeyBytes = new byte[dis.available()]; dis.readFully(priKeyBytes); dis.close(); String temp = new String(priKeyBytes); temp = temp.replace("-----BEGIN PRIVATE KEY-----", "").replace("-----END PRIVATE KEY-----", "");// .replaceAll("\r", // "").replaceAll("\n", // ""); byte[] decoded = Base64.decodeBase64(temp); return decoded; } }