Skip to content

Commit 03cd571

Browse files
authored
Merge pull request #203 from CyberSource/feature/add-batch-api-pgp-mtls
Feature/add batch api pgp mtls
2 parents d8944b5 + d542254 commit 03cd571

File tree

6 files changed

+697
-0
lines changed

6 files changed

+697
-0
lines changed

generator/cybersource_java_sdk_gen.bat

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@ powershell -Command " Set-Content ..\src\main\java\Model\PblPaymentLinksAllGet20
2222
echo "completed the task of replacing the links keyword in PblPaymentLinksAllGet200Response.java model"
2323

2424
git checkout ..\src\main\java\Api\OAuthApi.java
25+
git checkout ..\src\main\java\Api\BatchUploadwithMTLSApi.java
2526
git checkout ..\src\main\java\Model\AccessTokenResponse.java
2627
git checkout ..\src\main\java\Model\CreateAccessTokenRequest.java
2728

pom.xml

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -306,6 +306,12 @@
306306
<artifactId>AuthenticationSdk</artifactId>
307307
<version>0.0.35</version>
308308
</dependency>
309+
310+
<dependency>
311+
<groupId>org.bouncycastle</groupId>
312+
<artifactId>bcpg-jdk18on</artifactId>
313+
<version>1.81</version>
314+
</dependency>
309315
</dependencies>
310316

311317
<properties>
Lines changed: 103 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,103 @@
1+
package Api;
2+
3+
import java.io.File;
4+
import java.security.PrivateKey;
5+
import java.security.cert.X509Certificate;
6+
7+
import org.apache.logging.log4j.LogManager;
8+
import org.apache.logging.log4j.Logger;
9+
import org.bouncycastle.openpgp.PGPPublicKey;
10+
11+
import Invokers.ApiException;
12+
import Invokers.ApiResponse;
13+
import utilities.pgpBatchUpload.BatchUploadUtility;
14+
import utilities.pgpBatchUpload.MutualAuthUploadUtility;
15+
import utilities.pgpBatchUpload.PgpEncryptionUtility;
16+
17+
/**
18+
* API class for uploading batch files to CyberSource using mutual TLS authentication.
19+
* <p>
20+
* Supports multiple authentication mechanisms: JKS keystore/truststore, PKCS#12 client certificates,
21+
* and direct private key/certificate objects. Handles PGP encryption of files before upload.
22+
* </p>
23+
*/
24+
public class BatchUploadwithMTLSApi {
25+
private static Logger logger = LogManager.getLogger(BatchUploadwithMTLSApi.class);
26+
27+
/**
28+
* Uploads a batch file using mutual TLS authentication with JKS keystore and truststore.
29+
*
30+
* @param inputFile The file to be uploaded.
31+
* @param environmentHostname The environment hostname (e.g., secure-batch-test.cybersource.com).
32+
* @param pgpEncryptionCertPath Path to the PGP encryption certificate.
33+
* @param keystorePath Path to the JKS keystore file.
34+
* @param keystorePassword Password for the keystore.
35+
* @param truststorePath Path to the truststore file.
36+
* @param truststorePassword Password for the truststore.
37+
* @return ApiResponse containing the server response as a String.
38+
* @throws ApiException If an API error occurs.
39+
* @throws Exception If a general error occurs.
40+
*/
41+
public ApiResponse<String> uploadBatchAPI(File inputFile, String environmentHostname, String pgpEncryptionCertPath, String keystorePath, char[] keystorePassword, String truststorePath, char[] truststorePassword) throws ApiException, Exception {
42+
logger.info("Starting batch upload with JKS for given file");
43+
BatchUploadUtility.validateBatchApiJKSInputs(inputFile, environmentHostname, pgpEncryptionCertPath, keystorePath, truststorePath);
44+
String endpoint = "/pts/v1/transaction-batch-upload";
45+
String endpointUrl = BatchUploadUtility.getEndpointUrl(environmentHostname, endpoint);
46+
byte[] encryptedPgpBytes = PgpEncryptionUtility.handlePGPEncrypt(inputFile, pgpEncryptionCertPath);
47+
return MutualAuthUploadUtility.handleUploadOperationWithJKS(
48+
encryptedPgpBytes, endpointUrl, inputFile.getName(),
49+
keystorePath, keystorePassword, truststorePath, truststorePassword
50+
);
51+
}
52+
53+
/**
54+
* Uploads a batch file using mutual TLS authentication with a PKCS#12 (.p12/.pfx) client certificate file.
55+
*
56+
* @param inputFile The file to be uploaded.
57+
* @param environmentHostname The environment hostname (e.g., api.cybersource.com).
58+
* @param pgpEncryptionCertPath Path to the PGP encryption certificate.
59+
* @param clientCertP12FilePath Path to the PKCS#12 client certificate file.
60+
* @param clientCertP12Password Password for the PKCS#12 client certificate.
61+
* @param serverTrustCertPath Path to the server trust certificate.
62+
* @return ApiResponse containing the server response as a String.
63+
* @throws ApiException If an API error occurs.
64+
* @throws Exception If a general error occurs.
65+
*/
66+
public ApiResponse<String> uploadBatchAPI(File inputFile, String environmentHostname, String pgpEncryptionCertPath, String clientCertP12FilePath , char[] clientCertP12Password, String serverTrustCertPath) throws ApiException, Exception{
67+
logger.info("Starting batch upload with p12/pfx for given file");
68+
BatchUploadUtility.validateBatchApiP12Inputs(inputFile, environmentHostname, pgpEncryptionCertPath, clientCertP12FilePath, serverTrustCertPath);
69+
String endpoint = "/pts/v1/transaction-batch-upload";
70+
String endpointUrl = BatchUploadUtility.getEndpointUrl(environmentHostname, endpoint);
71+
byte[] encryptedPgpBytes = PgpEncryptionUtility.handlePGPEncrypt(inputFile, pgpEncryptionCertPath);
72+
return MutualAuthUploadUtility.handleUploadOperationUsingP12orPfx(
73+
encryptedPgpBytes, endpointUrl, inputFile.getName(),
74+
clientCertP12FilePath, clientCertP12Password, serverTrustCertPath
75+
);
76+
}
77+
78+
/**
79+
* Uploads a batch file using mutual TLS authentication with client private key and certificates as objects.
80+
*
81+
* @param inputFile The file to be uploaded.
82+
* @param environmentHostname The environment hostname (e.g., api.cybersource.com).
83+
* @param pgpPublicKey The PGP public key for encryption.
84+
* @param clientPrivateKey The client's private key.
85+
* @param clientCert The client's X509 certificate.
86+
* @param serverTrustCert The server's trust X509 certificate.
87+
* @return ApiResponse containing the server response as a String.
88+
* @throws ApiException If an API error occurs.
89+
* @throws Exception If a general error occurs.
90+
*/
91+
public ApiResponse<String> uploadBatchAPI(File inputFile, String environmentHostname, PGPPublicKey pgpPublicKey, PrivateKey clientPrivateKey, X509Certificate clientCert , X509Certificate serverTrustCert) throws ApiException, Exception {
92+
logger.info("Starting batch upload with client private key and certs for given file");
93+
BatchUploadUtility.validateBatchApiKeysInputs(inputFile, environmentHostname, pgpPublicKey, clientPrivateKey, clientCert, serverTrustCert);
94+
String endpoint = "/pts/v1/transaction-batch-upload";
95+
String endpointUrl = BatchUploadUtility.getEndpointUrl(environmentHostname, endpoint);
96+
byte[] encryptedPgpBytes = PgpEncryptionUtility.handlePGPEncrypt(inputFile, pgpPublicKey);
97+
return MutualAuthUploadUtility.handleUploadOperationUsingPrivateKeyAndCerts(
98+
encryptedPgpBytes, endpointUrl, inputFile.getName(),
99+
clientPrivateKey, clientCert, serverTrustCert
100+
);
101+
}
102+
103+
}
Lines changed: 223 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,223 @@
1+
package utilities.pgpBatchUpload;
2+
3+
import java.io.BufferedInputStream;
4+
import java.io.File;
5+
import java.io.FileInputStream;
6+
import java.io.IOException;
7+
import java.io.InputStream;
8+
import java.nio.file.Files;
9+
import java.nio.file.Path;
10+
import java.nio.file.Paths;
11+
import java.security.PrivateKey;
12+
import java.security.cert.CertificateException;
13+
import java.security.cert.CertificateFactory;
14+
import java.security.cert.X509Certificate;
15+
import java.util.Iterator;
16+
17+
import org.apache.commons.lang3.StringUtils;
18+
import org.apache.logging.log4j.LogManager;
19+
import org.apache.logging.log4j.Logger;
20+
import org.bouncycastle.openpgp.PGPException;
21+
import org.bouncycastle.openpgp.PGPPublicKey;
22+
import org.bouncycastle.openpgp.PGPPublicKeyRing;
23+
import org.bouncycastle.openpgp.PGPPublicKeyRingCollection;
24+
import org.bouncycastle.openpgp.PGPUtil;
25+
26+
import com.cybersource.authsdk.util.GlobalLabelParameters;
27+
28+
/**
29+
* Utility class for batch upload operations, including certificate and PGP key loading,
30+
* endpoint URL construction, and input validation for batch API operations.
31+
*/
32+
public class BatchUploadUtility {
33+
34+
private static final Logger logger = LogManager.getLogger(BatchUploadUtility.class);
35+
private static final long MAX_FILE_SIZE_BYTES = 75 * 1024 * 1024;
36+
37+
/**
38+
* Loads an X509 certificate from a PEM file.
39+
*
40+
* @param certFilePath The file path to the PEM certificate file.
41+
* @return The loaded X509Certificate object.
42+
* @throws CertificateException If the certificate cannot be parsed or is invalid.
43+
* @throws IOException If the file cannot be read or does not exist.
44+
*/
45+
public static X509Certificate loadCertificateFromPemFile(String certFilePath) throws CertificateException, IOException {
46+
try (FileInputStream inStream = new FileInputStream(certFilePath)) {
47+
CertificateFactory cf = CertificateFactory.getInstance("X.509");
48+
return (X509Certificate) cf.generateCertificate(inStream);
49+
}
50+
}
51+
52+
/**
53+
* Reads a PGP public key from the specified file.
54+
*
55+
* @param filePath The file path to the PGP public key.
56+
* @return The first encryption-capable PGPPublicKey found in the file.
57+
* @throws IOException If an I/O error occurs.
58+
* @throws PGPException If a PGP error occurs or no encryption key is found.
59+
* @throws IllegalArgumentException If the file path is null or empty.
60+
*/
61+
public static PGPPublicKey readPGPPublicKey(String filePath) throws IOException, PGPException {
62+
validatePathAndFile(filePath, "pgp public key path");
63+
logger.debug("Reading pgp public key from file: {}", filePath);
64+
try (InputStream keyIn = new BufferedInputStream(new FileInputStream(filePath))) {
65+
PGPPublicKeyRingCollection pgpPub = new PGPPublicKeyRingCollection(PGPUtil.getDecoderStream(keyIn),
66+
new org.bouncycastle.openpgp.operator.jcajce.JcaKeyFingerprintCalculator());
67+
68+
Iterator<PGPPublicKeyRing> keyRingIter = pgpPub.getKeyRings();
69+
while (keyRingIter.hasNext()) {
70+
PGPPublicKeyRing keyRing = keyRingIter.next();
71+
Iterator<PGPPublicKey> keyIter = keyRing.getPublicKeys();
72+
while (keyIter.hasNext()) {
73+
PGPPublicKey key = keyIter.next();
74+
if (key.isEncryptionKey()) {
75+
return key;
76+
}
77+
}
78+
}
79+
}
80+
throw new PGPException("No encryption key found in the provided key ring: " + filePath);
81+
}
82+
83+
/**
84+
* Constructs the full endpoint URL for the given environment hostname and endpoint path.
85+
*
86+
* @param environmentHostname The environment hostname (with or without protocol prefix).
87+
* @param endpoint The endpoint path to append.
88+
* @return The full endpoint URL.
89+
*/
90+
public static String getEndpointUrl(String environmentHostname , String endpoint) {
91+
String baseUrl;
92+
if(environmentHostname.startsWith(GlobalLabelParameters.URL_PREFIX))
93+
baseUrl=environmentHostname.trim();
94+
else
95+
baseUrl= GlobalLabelParameters.URL_PREFIX + environmentHostname.trim();
96+
return baseUrl + endpoint;
97+
}
98+
99+
/**
100+
* Validates the input parameters for batch API using JKS keystore.
101+
*
102+
* @param inputFile The input CSV file for batch upload.
103+
* @param environmentHostname The environment hostname.
104+
* @param pgpEncryptionCertPath The path to the PGP encryption certificate.
105+
* @param keystorePath The path to the keystore file.
106+
* @param truststorePath The path to the truststore file.
107+
* @throws Exception If any validation fails.
108+
*/
109+
public static void validateBatchApiJKSInputs(File inputFile, String environmentHostname, String pgpEncryptionCertPath, String keystorePath, String truststorePath) throws Exception{
110+
validateInputFile(inputFile);
111+
if(StringUtils.isEmpty(environmentHostname)) {
112+
logger.error("Environment Host Name for Batch Upload API cannot be null or empty.");
113+
throw new IllegalArgumentException("Environment Host Name for Batch Upload API cannot be null or empty.");
114+
}
115+
validatePathAndFile(pgpEncryptionCertPath, "PGP Encryption Cert Path");
116+
validatePathAndFile(keystorePath, "Keystore Path");
117+
validatePathAndFile(truststorePath, "Truststore Path");
118+
}
119+
120+
/**
121+
* Validates the input parameters for batch API using P12 client certificate.
122+
*
123+
* @param inputFile The input CSV file for batch upload.
124+
* @param environmentHostname The environment hostname.
125+
* @param pgpEncryptionCertPath The path to the PGP encryption certificate.
126+
* @param clientCertP12FilePath The path to the client certificate P12 file.
127+
* @param serverTrustCertPath The path to the server trust certificate.
128+
* @throws Exception If any validation fails.
129+
*/
130+
public static void validateBatchApiP12Inputs(File inputFile, String environmentHostname, String pgpEncryptionCertPath, String clientCertP12FilePath, String serverTrustCertPath) throws Exception{
131+
validateInputFile(inputFile);
132+
if(StringUtils.isEmpty(environmentHostname)) {
133+
logger.error("Environment Host Name for Batch Upload API cannot be null or empty.");
134+
throw new IllegalArgumentException("Environment Host Name for Batch Upload API cannot be null or empty.");
135+
}
136+
validatePathAndFile(pgpEncryptionCertPath, "PGP Encryption Cert Path");
137+
validatePathAndFile(clientCertP12FilePath, "Client Cert P12 File Path");
138+
validatePathAndFile(serverTrustCertPath, "Server Trust Cert Path");
139+
}
140+
141+
/**
142+
* Validates the input parameters for batch API using direct key and certificate objects.
143+
*
144+
* @param inputFile The input CSV file for batch upload.
145+
* @param environmentHostname The environment hostname.
146+
* @param pgpPublicKey The PGP public key object.
147+
* @param clientPrivateKey The client private key.
148+
* @param clientCert The client X509 certificate.
149+
* @param serverTrustCert The server trust X509 certificate.
150+
* @throws Exception If any validation fails.
151+
*/
152+
public static void validateBatchApiKeysInputs(File inputFile, String environmentHostname, PGPPublicKey pgpPublicKey, PrivateKey clientPrivateKey, X509Certificate clientCert , X509Certificate serverTrustCert) throws Exception{
153+
validateInputFile(inputFile);
154+
if(StringUtils.isEmpty(environmentHostname)) {
155+
logger.error("Environment Host Name for Batch Upload API cannot be null or empty.");
156+
throw new IllegalArgumentException("Environment Host Name for Batch Upload API cannot be null or empty.");
157+
}
158+
if (pgpPublicKey == null) throw new IllegalArgumentException("PGP Public Key is null");
159+
if (clientPrivateKey == null) throw new IllegalArgumentException("Client Private Key is null");
160+
if (clientCert == null) throw new IllegalArgumentException("Client Certificate is null");
161+
if (serverTrustCert == null) throw new IllegalArgumentException("Server Trust Certificate is null");
162+
}
163+
164+
/**
165+
* Validates the input file for batch upload.
166+
* Checks for existence, file type (CSV), and maximum file size (75MB).
167+
*
168+
* @param inputFile The input file to validate.
169+
* @throws IllegalArgumentException If the file is invalid, not a CSV, or exceeds size limit.
170+
*/
171+
private static void validateInputFile(File inputFile) {
172+
if (inputFile == null || !inputFile.exists() || !inputFile.isFile()) {
173+
logger.error("Input file is invalid or does not exist: " + (inputFile != null ? inputFile : "null"));
174+
throw new IllegalArgumentException("Input file is invalid or does not exist: " + inputFile);
175+
}
176+
//only csv files are allowed for batch api
177+
if (!inputFile.getName().toLowerCase().endsWith(".csv")) {
178+
logger.error("Only CSV file type is allowed: " + inputFile.getName());
179+
throw new IllegalArgumentException("Only CSV file type is allowed: " + inputFile.getName());
180+
}
181+
//maximum file size allowed is 75MB
182+
if (inputFile.length() > MAX_FILE_SIZE_BYTES) {
183+
logger.error("Input file size exceeds the maximum allowed size of 75MB: " + inputFile.length() + " fileName=" + inputFile.getName());
184+
throw new IllegalArgumentException("Input file size exceeds the maximum allowed size of 75MB: " + inputFile.length());
185+
}
186+
}
187+
188+
/**
189+
* Validates that the given file path exists and is not empty.
190+
*
191+
* @param path The file path to validate.
192+
* @param pathType A description of the path type (e.g., "Input file").
193+
* @throws IOException If the file does not exist.
194+
* @throws IllegalArgumentException If the path is null or empty.
195+
*/
196+
private static void validatePathAndFile(String filePath, String pathType) throws IOException {
197+
if (filePath == null || filePath.trim().isEmpty()) {
198+
logger.error(pathType + " path cannot be null or empty");
199+
throw new IllegalArgumentException(pathType + " path cannot be null or empty");
200+
}
201+
202+
// Normalize Windows-style paths that start with a slash before the drive letter
203+
String normalizedPath = filePath;
204+
if (File.separatorChar == '\\' && normalizedPath.matches("^/[A-Za-z]:.*")) {
205+
normalizedPath = normalizedPath.substring(1);
206+
}
207+
208+
Path path = Paths.get(normalizedPath);
209+
if (!Files.exists(path)) {
210+
logger.error(pathType + " does not exist: " + path);
211+
throw new IOException(pathType + " does not exist: " + path);
212+
}
213+
if (!Files.isRegularFile(path)) {
214+
logger.error(pathType + " does not have valid file: " + path);
215+
throw new IOException(pathType + " does not have valid file: " + path);
216+
}
217+
if (!Files.isReadable(path)) {
218+
logger.error(pathType + " is not readable: " + path);
219+
throw new IOException(pathType + " is not readable: " + path);
220+
}
221+
}
222+
223+
}

0 commit comments

Comments
 (0)