OVERVIEW
We had requirement to sign and encrypt all API request to our client’s partner bank and then decrypt the response. The bank called this encryption scheme as PKI encryption which upon research with the sample payload lead to the W3C Recommendation.
Here I will discussed how the encryption and decryption process can be achieved. Below is a sample of what an encrypted payload can be:
<xenc:EncryptedData xmlns:xenc="http://www.w3.org/2001/04/xmlenc#" Type="http://www.w3.org/2001/04/xmlenc#Element">
<xenc:EncryptionMethod Algorithm="http://www.w3.org/2001/04/xmlenc#tripledes-cbc"/>
<ds:KeyInfo xmlns:ds="http://www.w3.org/2000/09/xmldsig#">
<xenc:EncryptedKey>
<xenc:EncryptionMethod Algorithm="http://www.w3.org/2001/04/xmlenc#rsa-1_5"/>
<xenc:CipherData>
<xenc:CipherValue>b44GKIzOikbz.....</xenc:CipherValue>
</xenc:CipherData>
</xenc:EncryptedKey>
</ds:KeyInfo>
<xenc:CipherData>
<xenc:CipherValue>CO+A40a3vGzo47.....</xenc:CipherValue>
</xenc:CipherData>
</xenc:EncryptedData>
For our scenario, the keys are encrypted using RSA v1.5 algorithm with the actual request encrypted with TripleDES. You would need the Apache Santuario library to this process.
PAYLOAD SIGNATURE
As mentioned, our requirement includes signing the payloads. Thankfully, I was able to skipped on this with Carlos blog SAP PI/PO XML X509 signature by certificate.
ENCRYPTING THE REQUEST
For this process, involves the following steps:
import java.io.IOException;
import java.io.OutputStream;
import java.security.KeyStore;
import java.security.KeyStoreException;
import java.security.PublicKey;
import java.security.cert.X509Certificate;
import javax.crypto.KeyGenerator;
import javax.crypto.SecretKey;
import javax.xml.parsers.DocumentBuilderFactory;
import javax.xml.parsers.ParserConfigurationException;
import javax.xml.transform.OutputKeys;
import javax.xml.transform.Transformer;
import javax.xml.transform.TransformerConfigurationException;
import javax.xml.transform.TransformerException;
import javax.xml.transform.TransformerFactory;
import javax.xml.transform.dom.DOMSource;
import javax.xml.transform.stream.StreamResult;
import org.apache.xml.security.encryption.EncryptedData;
import org.apache.xml.security.encryption.EncryptedKey;
import org.apache.xml.security.encryption.XMLCipher;
import org.apache.xml.security.encryption.XMLEncryptionException;
import org.apache.xml.security.keys.KeyInfo;
import org.w3c.dom.Document;
import org.xml.sax.SAXException;
import com.sap.aii.mapping.api.AbstractTrace;
import com.sap.aii.mapping.api.AbstractTransformation;
import com.sap.aii.mapping.api.StreamTransformationException;
import com.sap.aii.mapping.api.TransformationInput;
import com.sap.aii.mapping.api.TransformationOutput;
import com.sap.aii.security.lib.KeyStoreManager;
import com.sap.security.api.ssf.ISsfProfile;
public class JM_XMLEncryption extends AbstractTransformation {
static AbstractTrace log = null;
@Override
public void transform(TransformationInput arg0, TransformationOutput arg1) throws StreamTransformationException {
log = this.getTrace();
String encKeyView = arg0.getInputParameters().getString("encKeyView");
String encKeyEntry = arg0.getInputParameters().getString("encKeyEntry");
try {
org.apache.xml.security.Init.init();
//load input payload as Document
DocumentBuilderFactory dbf = DocumentBuilderFactory.newInstance();
Document document = dbf.newDocumentBuilder().parse(arg0.getInputPayload().getInputStream());
//load the certificate for Key store
ISsfProfile encryptProfile = getSsfProfileKeyStore(encKeyView,encKeyEntry);
X509Certificate certificate = encryptProfile.getCertificate();
//generate secret key
KeyGenerator keyGenerator = KeyGenerator.getInstance("DESede");
keyGenerator.init(168);
SecretKey skey = keyGenerator.generateKey();
//encrypt the secret key
PublicKey pubkey = certificate.getPublicKey();
XMLCipher keyCipher = XMLCipher.getInstance(XMLCipher.RSA_v1dot5);
keyCipher.init(XMLCipher.WRAP_MODE, pubkey);
EncryptedKey encKey = keyCipher.encryptKey(document, skey);
//encrypt the contents of document
XMLCipher xmlCipher = XMLCipher.getInstance(XMLCipher.TRIPLEDES);
xmlCipher.init(XMLCipher.ENCRYPT_MODE, skey);
//add key info to encrypted Data
EncryptedData encData = xmlCipher.getEncryptedData();
KeyInfo keyInfo = new KeyInfo(document);
keyInfo.add(encKey);
encData.setKeyInfo(keyInfo);
//encrypt the document
xmlCipher.doFinal(document, document.getDocumentElement(), false);
//Output the resulting document.
OutputStream os = arg1.getOutputPayload().getOutputStream();
TransformerFactory tf = TransformerFactory.newInstance();
Transformer trans = tf.newTransformer();
trans.setOutputProperty(OutputKeys.OMIT_XML_DECLARATION, "yes");
trans.transform(new DOMSource(document), new StreamResult(os));
} catch (SAXException | IOException | ParserConfigurationException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
catch (XMLEncryptionException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
catch (TransformerConfigurationException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
catch (TransformerException e) {
// TODO Auto-generated catch block
e.printStackTrace();
} catch (Exception e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
}
private static ISsfProfile getSsfProfileKeyStore(String keyStoreAlias, String keyStoreEntry) throws StreamTransformationException {
KeyStoreManager managerPriviliged = null;
try {
managerPriviliged = com.sap.aii.af.service.resource.SAPSecurityResources.getInstance().getKeyStoreManager(
com.sap.aii.security.lib.PermissionMode.SYSTEM_LEVEL);
} catch (KeyStoreException e) {
throw new StreamTransformationException("SAPSecurityResources", e);
}
KeyStore keyStore;
try {
keyStore = managerPriviliged.getKeyStore(keyStoreAlias);
} catch (KeyStoreException e) {
throw new StreamTransformationException("managerPriviliged.getKeyStore " + keyStoreAlias, e);
}
ISsfProfile profile = null;
try {
profile = managerPriviliged.getISsfProfile(keyStore, keyStoreEntry, null);
} catch (KeyStoreException e) {
throw new StreamTransformationException("Failed to load SsfProfileKeyStore " + keyStoreAlias + " " + keyStoreEntry, e);
}
return profile;
}
}
DECRYPTING THE RESPONSE
For this process, involves the following steps:
import java.io.IOException;
import java.io.OutputStream;
import java.io.StringReader;
import java.security.InvalidAlgorithmParameterException;
import java.security.InvalidKeyException;
import java.security.KeyStore;
import java.security.KeyStoreException;
import java.security.NoSuchAlgorithmException;
import java.security.PrivateKey;
import java.security.spec.InvalidKeySpecException;
import java.util.Base64;
import javax.crypto.BadPaddingException;
import javax.crypto.Cipher;
import javax.crypto.IllegalBlockSizeException;
import javax.crypto.NoSuchPaddingException;
import javax.crypto.SecretKey;
import javax.crypto.SecretKeyFactory;
import javax.crypto.spec.DESedeKeySpec;
import javax.crypto.spec.IvParameterSpec;
import javax.xml.parsers.DocumentBuilder;
import javax.xml.parsers.DocumentBuilderFactory;
import javax.xml.parsers.ParserConfigurationException;
import javax.xml.transform.OutputKeys;
import javax.xml.transform.Transformer;
import javax.xml.transform.TransformerConfigurationException;
import javax.xml.transform.TransformerException;
import javax.xml.transform.TransformerFactory;
import javax.xml.transform.dom.DOMSource;
import javax.xml.transform.stream.StreamResult;
import org.w3c.dom.Document;
import org.w3c.dom.Element;
import org.w3c.dom.NodeList;
import org.xml.sax.InputSource;
import org.xml.sax.SAXException;
import com.sap.aii.af.service.resource.SAPSecurityResources;
import com.sap.aii.mapping.api.AbstractTrace;
import com.sap.aii.mapping.api.AbstractTransformation;
import com.sap.aii.mapping.api.StreamTransformationException;
import com.sap.aii.mapping.api.TransformationInput;
import com.sap.aii.mapping.api.TransformationOutput;
import com.sap.aii.security.lib.KeyStoreManager;
import com.sap.security.api.ssf.ISsfProfile;
public class JM_XMLDecryption extends AbstractTransformation {
static AbstractTrace log = null;
@Override
public void transform(TransformationInput arg0, TransformationOutput arg1) throws StreamTransformationException {
log = this.getTrace();
String keyView = arg0.getInputParameters().getString("keyView");
String keyEntry = arg0.getInputParameters().getString("keyEntry");
String password = arg0.getInputParameters().getString("password");
try {
org.apache.xml.security.Init.init();
//build document from input payload
DocumentBuilderFactory dbf = DocumentBuilderFactory.newInstance();
Document document = dbf.newDocumentBuilder().parse(arg0.getInputPayload().getInputStream());
//get the encrypted key and data in the CipherValue elements
NodeList cipherValueNodes = document.getElementsByTagName("xenc:CipherValue");
Element encKey = null;
Element encData = null;
for(int i = 0; i < cipherValueNodes.getLength(); i++) {
if (i == 0) {
encKey = (Element)cipherValueNodes.item(i);
}
else {
encData = (Element)cipherValueNodes.item(i);
}
}
//decrypt the encrypted key
PrivateKey privKey = getCertProfile(keyView,keyEntry,password).getPrivateKey();
Cipher cipher = Cipher.getInstance("RSA/ECB/PKCS1Padding");
cipher.init(Cipher.DECRYPT_MODE, privKey);
byte[] encryptedKeyBytes = Base64.getDecoder().decode(encKey.getTextContent());
byte[] decryptedKey = cipher.doFinal(encryptedKeyBytes);
//generate key from decrypted key
DESedeKeySpec desEdeKeySpec = new DESedeKeySpec(decryptedKey);
SecretKeyFactory keyFactory = SecretKeyFactory.getInstance("DESede");
SecretKey key = keyFactory.generateSecret(desEdeKeySpec);
//decrypt encrypted data
int ivLen = 8;
byte[] ivBytes = new byte[ivLen];
byte[] encryptedDataBytes = Base64.getDecoder().decode(encData.getTextContent());
System.arraycopy(encryptedDataBytes, 0, ivBytes, 0, ivLen);
IvParameterSpec ivParameterSpec = new IvParameterSpec(ivBytes);
Cipher cipherData = Cipher.getInstance("DESede/CBC/NoPadding");
cipherData.init(Cipher.DECRYPT_MODE, key, ivParameterSpec);
String decryptedData = new String(cipherData.doFinal(encryptedDataBytes, ivLen, encryptedDataBytes.length - ivLen));
DocumentBuilderFactory docFactory = DocumentBuilderFactory.newInstance();
DocumentBuilder builder = docFactory.newDocumentBuilder();
InputSource is = new InputSource(new StringReader(decryptedData.substring(0, decryptedData.length()-3)));
Document doc = builder.parse(is);
OutputStream os = arg1.getOutputPayload().getOutputStream();
TransformerFactory tf = TransformerFactory.newInstance();
Transformer trans = tf.newTransformer();
trans.setOutputProperty(OutputKeys.OMIT_XML_DECLARATION, "yes");
trans.transform(new DOMSource(doc), new StreamResult(os));
}
catch (ParserConfigurationException e) {
// TODO Auto-generated catch block
e.printStackTrace();
} catch (SAXException e) {
// TODO Auto-generated catch block
e.printStackTrace();
} catch (IOException e) {
// TODO Auto-generated catch block
e.printStackTrace();
} catch (NoSuchAlgorithmException e) {
// TODO Auto-generated catch block
e.printStackTrace();
} catch (NoSuchPaddingException e) {
// TODO Auto-generated catch block
e.printStackTrace();
} catch (InvalidKeyException e) {
// TODO Auto-generated catch block
e.printStackTrace();
} catch (IllegalBlockSizeException e) {
// TODO Auto-generated catch block
e.printStackTrace();
} catch (BadPaddingException e) {
// TODO Auto-generated catch block
e.printStackTrace();
} catch (InvalidAlgorithmParameterException e) {
// TODO Auto-generated catch block
e.printStackTrace();
} catch (TransformerConfigurationException e) {
// TODO Auto-generated catch block
e.printStackTrace();
} catch (TransformerException e) {
// TODO Auto-generated catch block
e.printStackTrace();
} catch (InvalidKeySpecException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
}
private static ISsfProfile getCertProfile(String alias, String entry,String pwd) throws StreamTransformationException {
KeyStoreManager managerPriviliged;
try {
managerPriviliged = SAPSecurityResources.getInstance().getKeyStoreManager(com.sap.aii.security.lib.PermissionMode.SYSTEM_LEVEL);
} catch (KeyStoreException e) {
throw new StreamTransformationException("SAPSecurityResources", e);
}
KeyStore keyStore;
try {
keyStore = managerPriviliged.getKeyStore(alias);
} catch (KeyStoreException e) {
throw new StreamTransformationException("managerPriviliged.getKeyStore " + alias, e);
}
ISsfProfile profile = null;
try {
profile = managerPriviliged.getISsfProfile(keyStore, entry, null);
} catch (KeyStoreException e) {
throw new StreamTransformationException(
"Failed to load SsfProfileKeyStore " + alias + " " + entry, e);
}
return profile;
}
}
I would like to highlight specific section of the code as it might not be applicable to your scenario.
Depending on the algorithm, you may have to search for the equivalent Spec class of the algorithm in your scenario. In my case, it is TripleDES hence the DESedeKeySpec.
DESedeKeySpec desEdeKeySpec = new DESedeKeySpec(decryptedKey);
SecretKeyFactory keyFactory = SecretKeyFactory.getInstance("DESede");
For whatever reason, the response from the server has extract three characters at the end of the entire payload hence the substring being applied.
InputSource is = new InputSource(new StringReader(decryptedData.substring(0, decryptedData.length()-3)));
REFERENCES
https://blogs.sap.com/2020/01/09/sap-pi-po-xml-x509-signature-by-certificate/
https://answers.sap.com/questions/183771/keystore-access-from-java-mapping.html
https://stackoverflow.com/questions/57261251/examples-or-tutorials-about-santuario-java