Before forwarding the payment data to your payment gateway for processing, use the following steps to decrypt the JWE payload using your private key.
Convert your PEM-formatted private key to DER format so that it can be correctly loaded by the decryption logic.
The following code snippets are example decryption implementations in Java and C#, and an example of a decrypted 3DS.data structure:
-
Example decryption implementation in Java:
import java.nio.file.Files;
import java.nio.file.Paths;
import java.security.KeyFactory;
import java.security.interfaces.RSAPrivateKey;
import java.security.spec.PKCS8EncodedKeySpec;
import java.util.Base64;
import javax.crypto.Cipher;
import javax.crypto.spec.GCMParameterSpec;
import javax.crypto.spec.SecretKeySpec;
import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.ObjectMapper;
public class DeveloperPortalSample {
public static void main(String[] args) throws Exception {
// Example JWE string (replace with your actual JWE and private key path)
String encryptedText = {{encryptedPayload}};
String privateKeyPath = "./rsapriv.der";
String PRIVATE_KEY = Base64.getEncoder().encodeToString(Files.readAllBytes(Paths.get(privateKeyPath)));
String result = decryptJWE(encryptedText, PRIVATE_KEY);
System.out.println(result);
}
public static String decryptJWE(String encryptedText, String privateKeyText) throws Exception {
// Split JWE parts by '.'
String delims = "[.]";
String[] tokens = encryptedText.split(delims);
if (tokens.length < 5) {
throw new IllegalArgumentException("Invalid JWE format");
}
// Decode and parse JWE header
byte[] headerBytes = Base64.getUrlDecoder().decode(tokens[0]);
String headerJson = new String(headerBytes);
ObjectMapper mapper = new ObjectMapper();
JsonNode header = mapper.readTree(headerJson);
// Extract algorithm information from header
String alg = header.has("alg") ? header.get("alg").asText() : "RSA1_5";
String enc = header.has("enc") ? header.get("enc").asText() : "A128GCM";
// Convert private key
byte[] privateKeyBytes = Base64.getDecoder().decode(privateKeyText);
PKCS8EncodedKeySpec privateKeySpec = new PKCS8EncodedKeySpec(privateKeyBytes);
KeyFactory keyFactory = KeyFactory.getInstance("RSA");
RSAPrivateKey privateKey = (RSAPrivateKey) keyFactory.generatePrivate(privateKeySpec);
// Decode encrypted key, IV, ciphertext, and authentication tag
byte[] encKey = Base64.getUrlDecoder().decode(tokens[1]);
byte[] iv = Base64.getUrlDecoder().decode(tokens[2]);
byte[] cipherText = Base64.getUrlDecoder().decode(tokens[3]);
byte[] tag = Base64.getUrlDecoder().decode(tokens[4]);
// Create Cipher instance based on key management algorithm
String keyManagementAlgorithm;
boolean useAAD = false;
if ("RSA-OAEP".equals(alg)) {
keyManagementAlgorithm = "RSA/ECB/OAEPPadding";
// At Samsung, OAEP uses AAD (Additional Authenticated Data)
useAAD = true;
} else if ("RSA1_5".equals(alg)) {
keyManagementAlgorithm = "RSA/ECB/PKCS1Padding";
// While RSA1_5 does not use AAD
useAAD = false;
} else {
throw new IllegalArgumentException("Unsupported key management algorithm: " + alg);
}
// Decrypt the CEK (Content Encryption Key)
Cipher decryptCipher = Cipher.getInstance(keyManagementAlgorithm);
decryptCipher.init(Cipher.DECRYPT_MODE, privateKey);
byte[] plainEncKey = decryptCipher.doFinal(encKey);
// Create Cipher instance based on content encryption algorithm
String contentEncryptionAlgorithm;
int gcmTagLength;
if ("A128GCM".equals(enc) || "A256GCM".equals(enc)) {
contentEncryptionAlgorithm = "AES/GCM/NoPadding";
gcmTagLength = 128;
} else {
throw new IllegalArgumentException("Unsupported content encryption algorithm: " + enc);
}
// Decrypt the content
Cipher contentCipher = Cipher.getInstance(contentEncryptionAlgorithm);
GCMParameterSpec gcmParameterSpec = new GCMParameterSpec(gcmTagLength, iv);
SecretKeySpec keySpec = new SecretKeySpec(plainEncKey, "AES");
contentCipher.init(Cipher.DECRYPT_MODE, keySpec, gcmParameterSpec);
// AAD handling: Use Base64Url-encoded header bytes as AAD
if (useAAD) {
byte[] encodedHeader = Base64.getUrlEncoder().withoutPadding().encode(headerBytes);
contentCipher.updateAAD(encodedHeader);
}
// Concatenate cipherText and tag, then pass to doFinal
byte[] cipherData = new byte[cipherText.length + tag.length];
System.arraycopy(cipherText, 0, cipherData, 0, cipherText.length);
System.arraycopy(tag, 0, cipherData, cipherText.length, tag.length);
byte[] plainText = contentCipher.doFinal(cipherData);
return new String(plainText, java.nio.charset.StandardCharsets.UTF_8);
}
-
Example decryption implementation in C#:
using System;
using System.IO;
using System.Text;
using System.Text.Json.Nodes;
using System.Security.Cryptography;
public static void Main(string[] args)
{
// Example JWE string (replace with your actual JWE and private key path)
string encryptedText = {{encryptedPayload}};
string privateKeyPath = "./rsapriv.der";
// Read the private key file (DER format)
byte[] privateKeyBytes = File.ReadAllBytes(privateKeyPath);
// Decrypt the JWE
string result = decryptJWE(encryptedText, privateKeyBytes);
// Print the result
Console.WriteLine(result);
}
public static string decryptJWE(string encryptedText, byte[] privateKeyBytes)
{
// Split JWE parts by '.'
var parts = encryptedText.Split('.');
if (parts.Length < 5)
throw new ArgumentException("Invalid JWE format");
// Decode and parse JWE header
var headerBytes = Base64UrlDecode(parts[0]);
var headerJson = Encoding.UTF8.GetString(headerBytes);
var header = JsonNode.Parse(headerJson);
// Extract algorithm information from header
string alg = header?["alg"]?.ToString() ?? "RSA1_5";
string enc = header?["enc"]?.ToString() ?? "A128GCM";
// Convert private key (assume PKCS8 DER)
using var rsa = RSA.Create();
rsa.ImportPkcs8PrivateKey(privateKeyBytes, out _);
// Decode encrypted key, IV, ciphertext, and authentication tag
var encKey = Base64UrlDecode(parts[1]);
var iv = Base64UrlDecode(parts[2]);
var cipherText = Base64UrlDecode(parts[3]);
var tag = Base64UrlDecode(parts[4]);
// Create Cipher instance based on key management algorithm
bool useAAD = false;
if (alg == "RSA-OAEP")
{
// At Samsung, OAEP uses AAD (Additional Authenticated Data)
useAAD = true;
}
else if (alg == "RSA1_5")
{
// While RSA1_5 does not use AAD
useAAD = false;
}
else
{
throw new ArgumentException($"Unsupported key management algorithm: {alg}");
}
// Decrypt the CEK (Content Encryption Key)
byte[] plainEncKey = alg == "RSA-OAEP"
? rsa.Decrypt(encKey, RSAEncryptionPadding.OaepSHA1)
: rsa.Decrypt(encKey, RSAEncryptionPadding.Pkcs1);
// Decrypt the content
using var aes = new AesGcm(plainEncKey, 16);
var plainText = new byte[cipherText.Length];
if (useAAD)
{
// AAD handling: Use Base64Url-encoded header bytes as AAD
var encodedHeader = Encoding.ASCII.GetBytes(Base64UrlEncode(headerBytes));
aes.Decrypt(iv, cipherText, tag, plainText, encodedHeader);
}
else
{
aes.Decrypt(iv, cipherText, tag, plainText);
}
return Encoding.UTF8.GetString(plainText).TrimEnd('\0');
}
private static byte[] Base64UrlDecode(string input)
{
string s = input.Replace('-', '+').Replace('_', '/');
switch (s.Length % 4)
{
case 2: s += "=="; break;
case 3: s += "="; break;
}
return Convert.FromBase64String(s);
}
private static string Base64UrlEncode(byte[] input)
{
return Convert.ToBase64String(input)
.TrimEnd('=')
.Replace('+', '-')
.Replace('/', '_');
}
-
Example of decrypted 3DS.data output:
{
"amount": "100",
"currency_code": "USD",
"utc": "1719388643614",
"eci_indicator": "5",
"tokenPAN": "5185731679991253",
"tokenPanExpiration": "0127",
"cryptogram": "AKkeaVcvwHfmAMmud6r3AoACFA=="
}
NoteThe tokenPanExpiration field format is "MMYY", except for American Express (amex) payments, where the format is "YYMMDD".