package de.kp.wsclient.security;
import java.security.InvalidAlgorithmParameterException;
import java.security.InvalidKeyException;
import java.security.NoSuchAlgorithmException;
import java.security.spec.MGF1ParameterSpec;
import java.util.ArrayList;
import java.util.List;
import javax.crypto.Cipher;
import javax.crypto.IllegalBlockSizeException;
import javax.crypto.KeyGenerator;
import javax.crypto.NoSuchPaddingException;
import javax.crypto.SecretKey;
import javax.crypto.spec.OAEPParameterSpec;
import javax.crypto.spec.PSource;
import org.apache.xml.security.algorithms.JCEMapper;
import org.apache.xml.security.encryption.EncryptedData;
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.w3c.dom.Element;
import org.w3c.dom.Text;
import de.kp.wsclient.util.UUIDGenerator;
/*
* This class MUST always be used in conjunction with SecSignature, as
* e.g. no BinarySecurityToken element is created (done through signing)
*
*
* This class implements OASIS Web Services Security X.509 Certificate
* Token Profile 1.1 and is restricted to referencing a Binaray Security
* Token, i.e. no subject key identifier or a reference to an issuer and
* serial number is supported.
*
*/
public class SecEncryptor extends SecBase {
static {
// initialize apache santuario framework
org.apache.xml.security.Init.init();
}
private SecCrypto crypto;
// Symmetric key used in the EncryptedKey.
private SecretKey symmetricKey = null;
// Encrypted bytes of the symmetric key
private byte[] encryptedSymmetricKey;
// DEFAULT Algorithm used to encrypt the symmetric key;
// as an alternative, also KEYTRANSPORT_RSAOEP is supported
private String keyEncAlgo = SecConstants.KEYTRANSPORT_RSA15;
// DEFAULT Algorithm to be used with the ephemeral key.
// This parameter determines the key generator.
private String symEncAlgo = SecConstants.AES_128;
// xenc:EncryptedKey element
private Element encryptedKeyElement = null;
private String encKeyId = null;
private KeyInfo keyInfo;
private Document xmlDoc;
/**
* Constructor SecEncryptor
* @param crypto
*/
public SecEncryptor(SecCrypto crypto) {
this.crypto = crypto;
}
/**
* This method builds the SOAP envelope with encrypted Body and adds
* encrypted key; this method is an adapted version of the WSS4j build
* method
*
* @param xmlDoc (SOAP envelope)
* @return
* @throws Exception
*/
/**
* @param xmlDoc
* @return
* @throws Exception
*/
public Document encrypt(Document xmlDoc) throws Exception {
// set reference to xml document as this is used
// with other encryption methods
this.xmlDoc = xmlDoc;
Element soapHeader = getSOAPHeader(xmlDoc);
if (soapHeader == null) throw new Exception("SOAP Header not found.");
buildEncKeyElement();
Element envelope = xmlDoc.getDocumentElement();
List<SecEncPart> parts = new ArrayList<SecEncPart>();
String soapNamespace = SecUtil.getSOAPNamespace(envelope);
SecEncPart encP = new SecEncPart(SecConstants.ELEM_BODY, soapNamespace, "Content");
parts.add(encP);
Element refs = encryptForRef(parts);
Element secHeader = getSecHeader(this.xmlDoc);
if (this.encryptedKeyElement != null) {
/*
* Adds the internal Reference element to this Encrypt data.
* The reference element must be created by the encryptForInternalRef
* method. The reference element is added to the EncryptedKey element
* of this encrypt block.
*/
this.encryptedKeyElement.appendChild(refs);
/*
* Prepend the EncryptedKey element to the elements already in the
* Security header.
*
* The method allows to insert the EncryptedKey element at any position
* in the Security header.
*/
SecUtil.prependChildElement(secHeader, encryptedKeyElement);
} else {
/*
* Adds (prepends) the external Reference element to the Security header.
*
* The reference element must be created by the encryptForExternalRef
* method. The method prepends the reference element in the SecurityHeader.
*/
SecUtil.prependChildElement(secHeader, refs);
}
soapHeader.appendChild(secHeader);
return xmlDoc;
}
/**
* This method generates the symmetric key and also its
* encrypted version; to this end, the public key of the
* receiver of the SOAP message is used.
*
* In addition the <xenc:EncryptedKey> element is built
* and added to the <wsse:Security> header
*
* @throws Exception
*/
private void buildEncKeyElement() throws Exception {
// the subsequent part of code is adapted from the 'prepare'
// method of WSS4J (1.6.4) WSSecEncrypt
KeyGenerator keyGen = getKeyGenerator();
this.symmetricKey = keyGen.generateKey();
/*
* Encrypt the symmetric key data and prepare the EncryptedKey element
* This method does the most work for to prepare the EncryptedKey element.
*/
Cipher cipher = getCipherInstance(this.keyEncAlgo);
try {
OAEPParameterSpec oaepParameterSpec = null;
// the default encoding algorithm is SecConstants.KEYTRANSPORT_RSA15
if (SecConstants.KEYTRANSPORT_RSAOEP.equals(this.keyEncAlgo)) {
oaepParameterSpec = new OAEPParameterSpec("SHA-1", "MGF1", new MGF1ParameterSpec("SHA-1"), PSource.PSpecified.DEFAULT);
}
if (oaepParameterSpec == null) {
// this is the default way to initialize the Cipher instance
cipher.init(Cipher.WRAP_MODE, this.crypto.getPublicKey());
} else {
cipher.init(Cipher.WRAP_MODE, this.crypto.getPublicKey(), oaepParameterSpec);
}
this.encryptedSymmetricKey = cipher.wrap(this.symmetricKey);
} catch (InvalidKeyException e) {
throw new Exception("Encryption failed: " + e.getMessage());
} catch (InvalidAlgorithmParameterException e) {
throw new Exception("Encryption failed: " + e.getMessage());
} catch (IllegalStateException e) {
throw new Exception("Encryption failed: " + e.getMessage());
} catch (IllegalBlockSizeException e) {
throw new Exception("Encryption failed: " + e.getMessage());
}
//
// Now we need to setup the EncryptedKey header block 1) create a
// EncryptedKey element and set a wsu:Id for it 2) Generate ds:KeyInfo
// element, this wraps the wsse:SecurityTokenReference 3) Create and set
// up the SecurityTokenReference according to the keyIdentifier parameter
// 4) Create the CipherValue element structure and insert the encrypted
// session key
//
/*
* EXAMPLE STRUCTURE
*
* <wsse:Security>
* <xenc:EncryptedKey>
* <xenc:EncryptionMethod Algorithm="�"/>
* <ds:KeyInfo>
* <wsse:SecurityTokenReference>
*
*/
this.encryptedKeyElement = createEncKeyElement(this.keyEncAlgo);
if (this.encKeyId == null || "".equals(this.encKeyId)) {
this.encKeyId = "EK-" + UUIDGenerator.getUUID();
}
this.encryptedKeyElement.setAttributeNS(null, "Id", this.encKeyId);
/*
* __DESIGN__
*
* The actual version of the encryption mechanism exclusively
* supports supports a security token reference, that holds
* a reference to a binary security token as part of the
* message; the same mechanism is also used with signature
*/
/*
* <ds:KeyInfo>
* <wsse:SecurityTokenReference>
* <wsse:Reference URI="#urn:oasis:names:tc:ebxmlregrep:rs:security:SenderCert" ValueType="http://docs.oasisopen.org/wss/2004/01/oasis-200401-wss-x509-token-profile-1.0#X509v3"/>
* </wsse:SecurityTokenReference>
*/
this.keyInfo = createKeyInfo();
/*
* <xenc:CipherData>
* <xenc:CipherValue>�</xenc:CipherValue>
* </xenc:CipherData>
*/
Text keyText = SecUtil.createBase64EncodedTextNode(this.xmlDoc, this.encryptedSymmetricKey);
Element xencCipherValue = createCipherValue(this.encryptedKeyElement);
xencCipherValue.appendChild(keyText);
}
/*
* Encrypt one or more parts or elements of the message.
*
* This method takes a vector of WSEncryptionPart object that
* contain information about the elements to encrypt. The method
* call the encryption method, takes the reference information
* generated during encryption and add this to the xenc:Reference
* element.
*/
public Element encryptForRef(List<SecEncPart> references) throws Exception {
List<String> encDataRefs = doEncryption(symmetricKey, symEncAlgo, references);
/*
* <xenc:EncryptedKey>
* <xenc:ReferenceList>
* <xenc:DataReference URI="#encrypted"/>
* </xenc:ReferenceList>
*/
Element referenceList = this.xmlDoc.createElementNS(SecConstants.ENC_NS, SecConstants.ENC_PRE + ":ReferenceList");
return createDataRefList(referenceList, encDataRefs);
}
/**
* This method encrypts a list of body elements (references)
* of the SOAP message
*
* @param secretKey
* @param encryptionAlgorithm
* @param references
* @return
* @throws Exception
*/
public List<String> doEncryption(SecretKey secretKey, String encryptionAlgorithm, List<SecEncPart> references) throws Exception {
XMLCipher xmlCipher = null;
try {
xmlCipher = XMLCipher.getInstance(encryptionAlgorithm);
} catch (XMLEncryptionException ex) {
throw new Exception("Unsupported encryption algorithm.");
}
List<String> encDataRef = new ArrayList<String>();
for (int part = 0; part < references.size(); part++) {
SecEncPart encPart = references.get(part);
// Get the data to encrypt.
DOMCallbackLookup callbackLookup = new DOMCallbackLookup(this.xmlDoc);
List<Element> elementsToEncrypt = SecUtil.findElements(encPart, callbackLookup, this.xmlDoc);
if (elementsToEncrypt == null || elementsToEncrypt.size() == 0) {
throw new Exception("Encryption failed.");
}
String modifier = encPart.getEncModifier();
for (Element elementToEncrypt:elementsToEncrypt) {
String id = encryptElement(elementToEncrypt, modifier, xmlCipher, secretKey);
encPart.setEncId(id);
encDataRef.add("#" + id);
}
}
return encDataRef;
}
/**
* This method encrypts a single DOM element.
*
* @param elementToEncrypt
* @param modifier
* @param xmlCipher
* @param secretKey
* @return
* @throws Exception
*/
private String encryptElement(Element elementToEncrypt, String modifier, XMLCipher xmlCipher, SecretKey secretKey) throws Exception {
boolean content = "Content".equals(modifier) ? true : false;
if (content == false) {
throw new Exception("[SecEncryptor] Encryption is actually restricted to content.");
}
// Encrypt data, and set necessary attributes in xenc:EncryptedData
String xencEncryptedDataId = SecUtil.getIdAllocator().createId("ED-", elementToEncrypt);
try {
// this is the DEFAULT way to encrypt the content,
// i.e. the body of a SOAP message
xmlCipher.init(XMLCipher.ENCRYPT_MODE, secretKey);
EncryptedData encData = xmlCipher.getEncryptedData();
encData.setId(xencEncryptedDataId);
encData.setKeyInfo(this.keyInfo);
xmlCipher.doFinal(this.xmlDoc, elementToEncrypt, content);
return xencEncryptedDataId;
} catch (Exception ex) {
throw new Exception("Encryprtion failed.");
}
}
// Create DOM subtree for <xenc:EncryptedKey>
public Element createDataRefList(Element referenceList, List<String> encDataRefs) {
for (String dataReferenceUri:encDataRefs) {
Element dataReference = this.xmlDoc.createElementNS(SecConstants.ENC_NS, SecConstants.ENC_PRE + ":DataReference");
dataReference.setAttributeNS(null, "URI", dataReferenceUri);
referenceList.appendChild(dataReference);
}
return referenceList;
}
private KeyInfo createKeyInfo() throws Exception {
KeyInfo keyInfo = new KeyInfo(this.xmlDoc);
Element keyInfoElement = keyInfo.getElement();
keyInfoElement.setAttributeNS(SecConstants.XMLNS_NS, "xmlns:" + SecConstants.SIG_PRE, SecConstants.SIG_NS);
/*
* __DESIGN__
*
* The actual version of the encryption mechanism exclusively
* supports supports a security token reference, that holds
* a reference to a binary security token as part of the
* message; the same mechanism is also used with signature
*/
/*
* <wsse:SecurityTokenReference>
* <wsse:Reference URI="#urn:oasis:names:tc:ebxmlregrep:rs:security:SenderCert" ValueType="http://docs.oasisopen.org/wss/2004/01/oasis-200401-wss-x509-token-profile-1.0#X509v3"/>
* </wsse:SecurityTokenReference>
*/
Element secToken = createSTR(this.xmlDoc);
// IMPORTANT: The wsse:Namespace MUST be set explicity
// to ensure proper validation of signature
SecUtil.setNamespace(secToken, SecConstants.WSSE_NS, SecConstants.WSSE_PRE);
keyInfoElement.appendChild(secToken);
this.encryptedKeyElement.appendChild(keyInfoElement);
return keyInfo;
}
private KeyGenerator getKeyGenerator() throws Exception {
try {
// Algorithm to be used with the ephemeral key::SecConstants.AES_128 (DEFAULT)
String keyAlgorithm = JCEMapper.getJCEKeyAlgorithmFromURI(this.symEncAlgo);
if (keyAlgorithm == null || "".equals(keyAlgorithm)) {
keyAlgorithm = JCEMapper.translateURItoJCEID(this.symEncAlgo);
}
KeyGenerator keyGen = KeyGenerator.getInstance(keyAlgorithm);
if (this.symEncAlgo.equalsIgnoreCase(SecConstants.AES_128) || this.symEncAlgo.equalsIgnoreCase(SecConstants.AES_128_GCM)) {
// this is the default way to initialize the key generator
keyGen.init(128);
} else if (this.symEncAlgo.equalsIgnoreCase(SecConstants.AES_192) || this.symEncAlgo.equalsIgnoreCase(SecConstants.AES_192_GCM)) {
keyGen.init(192);
} else if (this.symEncAlgo.equalsIgnoreCase(SecConstants.AES_256) || this.symEncAlgo.equalsIgnoreCase(SecConstants.AES_256_GCM)) {
keyGen.init(256);
}
return keyGen;
} catch (NoSuchAlgorithmException e) {
throw new Exception("[KEY GENERATOR] Unsupported algorithm.");
}
}
/*
* Create DOM subtree for <code>xenc:EncryptedKey</code>
*/
protected Element createEncKeyElement(String keyTransportAlgo) {
Element encryptedKey = this.xmlDoc.createElementNS(SecConstants.ENC_NS, SecConstants.ENC_PRE + ":EncryptedKey");
SecUtil.setNamespace(encryptedKey, SecConstants.ENC_NS, SecConstants.ENC_PRE);
Element encryptionMethod = this.xmlDoc.createElementNS(SecConstants.ENC_NS, SecConstants.ENC_PRE + ":EncryptionMethod");
encryptionMethod.setAttributeNS(null, "Algorithm", keyTransportAlgo);
encryptedKey.appendChild(encryptionMethod);
return encryptedKey;
}
private Element createCipherValue(Element encryptedKey) {
Element cipherData = this.xmlDoc.createElementNS(SecConstants.ENC_NS, SecConstants.ENC_PRE + ":CipherData");
Element cipherValue = this.xmlDoc.createElementNS(SecConstants.ENC_NS, SecConstants.ENC_PRE + ":CipherValue");
cipherData.appendChild(cipherValue);
encryptedKey.appendChild(cipherData);
return cipherValue;
}
/**
* Translate the "cipherAlgo" URI to a JCE ID, and
* return a javax.crypto.Cipher instance of this type.
*
* @param cipherAlgo
* @return
* @throws Exception
*/
public Cipher getCipherInstance(String cipherAlgo) throws Exception {
try {
String keyAlgorithm = JCEMapper.translateURItoJCEID(cipherAlgo);
return Cipher.getInstance(keyAlgorithm);
} catch (NoSuchPaddingException ex) {
throw new Exception("Unsupported algorithm: " + cipherAlgo);
} catch (NoSuchAlgorithmException ex) {
// Check to see if an RSA OAEP MGF-1 with SHA-1 algorithm was
// requested. Some JDKs don't support RSA/ECB/OAEPPadding
if (SecConstants.KEYTRANSPORT_RSAOEP.equals(cipherAlgo)) {
try {
return Cipher.getInstance("RSA/ECB/OAEPWithSHA1AndMGF1Padding");
} catch (Exception e) {
throw new Exception("Unsupported algorithm: " + cipherAlgo);
}
} else {
throw new Exception("Unsupported algorithm: " + cipherAlgo);
}
}
}
}