blob: d74608e01429f7d35e8d0e843cfdf92316e94a67 [file] [log] [blame]
/**
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
package org.apache.xml.security.stax.impl.processor.input;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.io.OutputStreamWriter;
import java.io.PipedInputStream;
import java.io.PipedOutputStream;
import java.io.UnsupportedEncodingException;
import java.nio.charset.Charset;
import java.nio.charset.StandardCharsets;
import java.security.Key;
import java.security.NoSuchAlgorithmException;
import java.security.NoSuchProviderException;
import java.util.ArrayDeque;
import java.util.ArrayList;
import java.util.Deque;
import java.util.HashMap;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import javax.crypto.BadPaddingException;
import javax.crypto.Cipher;
import javax.crypto.CipherOutputStream;
import javax.crypto.IllegalBlockSizeException;
import javax.crypto.NoSuchPaddingException;
import javax.security.auth.DestroyFailedException;
import javax.security.auth.Destroyable;
import javax.xml.bind.JAXBElement;
import javax.xml.bind.JAXBException;
import javax.xml.bind.Unmarshaller;
import javax.xml.namespace.QName;
import javax.xml.stream.XMLInputFactory;
import javax.xml.stream.XMLStreamConstants;
import javax.xml.stream.XMLStreamException;
import javax.xml.stream.XMLStreamReader;
import javax.xml.stream.events.Attribute;
import org.apache.commons.codec.binary.Base64OutputStream;
import org.apache.xml.security.binding.xmldsig.KeyInfoType;
import org.apache.xml.security.binding.xmlenc.EncryptedDataType;
import org.apache.xml.security.binding.xmlenc.EncryptedKeyType;
import org.apache.xml.security.binding.xmlenc.ReferenceList;
import org.apache.xml.security.binding.xmlenc.ReferenceType;
import org.apache.xml.security.binding.xop.Include;
import org.apache.xml.security.exceptions.XMLSecurityException;
import org.apache.xml.security.stax.config.ConfigurationProperties;
import org.apache.xml.security.stax.config.JCEAlgorithmMapper;
import org.apache.xml.security.stax.ext.AbstractInputProcessor;
import org.apache.xml.security.stax.ext.InboundSecurityContext;
import org.apache.xml.security.stax.ext.InputProcessorChain;
import org.apache.xml.security.stax.ext.SecurePart;
import org.apache.xml.security.stax.ext.UncheckedXMLSecurityException;
import org.apache.xml.security.stax.ext.XMLSecurityConstants;
import org.apache.xml.security.stax.ext.XMLSecurityProperties;
import org.apache.xml.security.stax.ext.XMLSecurityUtils;
import org.apache.xml.security.stax.ext.stax.XMLSecEvent;
import org.apache.xml.security.stax.ext.stax.XMLSecEventFactory;
import org.apache.xml.security.stax.ext.stax.XMLSecNamespace;
import org.apache.xml.security.stax.ext.stax.XMLSecStartElement;
import org.apache.xml.security.stax.impl.XMLSecurityEventReader;
import org.apache.xml.security.stax.impl.util.FullyBufferedOutputStream;
import org.apache.xml.security.stax.impl.util.IDGenerator;
import org.apache.xml.security.stax.impl.util.IVSplittingOutputStream;
import org.apache.xml.security.stax.impl.util.MultiInputStream;
import org.apache.xml.security.stax.impl.util.ReplaceableOuputStream;
import org.apache.xml.security.stax.securityToken.InboundSecurityToken;
import org.apache.xml.security.stax.securityToken.SecurityTokenConstants;
import org.apache.xml.security.stax.securityToken.SecurityTokenFactory;
import org.apache.xml.security.stax.securityToken.SecurityTokenProvider;
import org.apache.xml.security.utils.UnsyncByteArrayInputStream;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* Processor for decryption of EncryptedData XML structures
*
*/
public abstract class AbstractDecryptInputProcessor extends AbstractInputProcessor {
private static final transient Logger LOG = LoggerFactory.getLogger(AbstractDecryptInputProcessor.class);
protected static final Integer maximumAllowedXMLStructureDepth =
Integer.valueOf(ConfigurationProperties.getProperty("MaximumAllowedXMLStructureDepth"));
protected static final Integer maximumAllowedEncryptedDataEvents =
Integer.valueOf(ConfigurationProperties.getProperty("MaximumAllowedEncryptedDataEvents"));
private final KeyInfoType keyInfoType;
private final Map<String, ReferenceType> references;
private final List<ReferenceType> processedReferences;
private final String uuid = IDGenerator.generateID(null);
private final QName wrapperElementName = new QName("http://dummy", "dummy", uuid);
private final ArrayDeque<XMLSecEvent> tmpXmlEventList = new ArrayDeque<>();
public AbstractDecryptInputProcessor(XMLSecurityProperties securityProperties) throws XMLSecurityException {
super(securityProperties);
keyInfoType = null;
references = null;
processedReferences = null;
}
public AbstractDecryptInputProcessor(KeyInfoType keyInfoType, ReferenceList referenceList,
XMLSecurityProperties securityProperties) throws XMLSecurityException {
super(securityProperties);
this.keyInfoType = keyInfoType;
final List<JAXBElement<ReferenceType>> dataReferenceOrKeyReference = referenceList.getDataReferenceOrKeyReference();
references = new HashMap<>(dataReferenceOrKeyReference.size() + 1); //+1 because the HashMap will resize otherwise
processedReferences = new ArrayList<>(dataReferenceOrKeyReference.size());
Iterator<JAXBElement<ReferenceType>> referenceTypeIterator = dataReferenceOrKeyReference.iterator();
while (referenceTypeIterator.hasNext()) {
ReferenceType referenceType = referenceTypeIterator.next().getValue();
if (referenceType.getURI() == null) {
throw new XMLSecurityException("stax.emptyReferenceURI");
}
references.put(XMLSecurityUtils.dropReferenceMarker(referenceType.getURI()), referenceType);
}
}
public Map<String, ReferenceType> getReferences() {
return references;
}
public List<ReferenceType> getProcessedReferences() {
return processedReferences;
}
/*
<xenc:EncryptedData xmlns:xenc="http://www.w3.org/2001/04/xmlenc#" Id="EncDataId-1612925417" Type="http://www.w3.org/2001/04/xmlenc#Content">
<xenc:EncryptionMethod xmlns:xenc="http://www.w3.org/2001/04/xmlenc#" Algorithm="http://www.w3.org/2001/04/xmlenc#aes256-cbc" />
<ds:KeyInfo xmlns:ds="http://www.w3.org/2000/09/xmldsig#">
<wsse:SecurityTokenReference xmlns:wsse="http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-wssecurity-secext-1.0.xsd">
<wsse:Reference xmlns:wsse="http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-wssecurity-secext-1.0.xsd" URI="#EncKeyId-1483925398" />
</wsse:SecurityTokenReference>
</ds:KeyInfo>
<xenc:CipherData xmlns:xenc="http://www.w3.org/2001/04/xmlenc#">
<xenc:CipherValue xmlns:xenc="http://www.w3.org/2001/04/xmlenc#">
...
</xenc:CipherValue>
</xenc:CipherData>
</xenc:EncryptedData>
*/
@Override
public XMLSecEvent processNextHeaderEvent(InputProcessorChain inputProcessorChain)
throws XMLStreamException, XMLSecurityException {
return processEvent(inputProcessorChain, true);
}
@Override
public XMLSecEvent processNextEvent(InputProcessorChain inputProcessorChain)
throws XMLStreamException, XMLSecurityException {
return processEvent(inputProcessorChain, false);
}
private XMLSecEvent processEvent(InputProcessorChain inputProcessorChain, boolean isSecurityHeaderEvent)
throws XMLStreamException, XMLSecurityException {
if (!tmpXmlEventList.isEmpty()) {
return tmpXmlEventList.pollLast();
}
XMLSecEvent xmlSecEvent = isSecurityHeaderEvent
? inputProcessorChain.processHeaderEvent()
: inputProcessorChain.processEvent();
boolean encryptedHeader = false;
if (xmlSecEvent.getEventType() == XMLStreamConstants.START_ELEMENT) {
XMLSecStartElement xmlSecStartElement = xmlSecEvent.asStartElement();
//buffer the events until the EncryptedData Element appears and discard it if we found the reference inside it
//otherwise replay it
if (xmlSecStartElement.getName().equals(XMLSecurityConstants.TAG_wsse11_EncryptedHeader)) {
xmlSecEvent = readAndBufferEncryptedHeader(inputProcessorChain, isSecurityHeaderEvent, xmlSecEvent);
xmlSecStartElement = xmlSecEvent.asStartElement();
encryptedHeader = true;
}
//check if the current start-element has the name EncryptedData and an Id attribute
if (xmlSecStartElement.getName().equals(XMLSecurityConstants.TAG_xenc_EncryptedData)) {
ReferenceType referenceType = null;
if (references != null) {
referenceType = matchesReferenceId(xmlSecStartElement);
if (referenceType == null) {
//if the events were not for us (no matching reference-id the we have to replay the EncryptedHeader elements)
if (!tmpXmlEventList.isEmpty()) {
return tmpXmlEventList.pollLast();
}
return xmlSecEvent;
}
//duplicate id's are forbidden
if (processedReferences.contains(referenceType)) {
throw new XMLSecurityException("signature.Verification.MultipleIDs");
}
processedReferences.add(referenceType);
}
tmpXmlEventList.clear();
//the following LOGic reads the encryptedData structure and doesn't pass them further
//through the chain
InputProcessorChain subInputProcessorChain = inputProcessorChain.createSubChain(this);
EncryptedDataType encryptedDataType =
parseEncryptedDataStructure(isSecurityHeaderEvent, xmlSecEvent, subInputProcessorChain);
if (encryptedDataType.getId() == null) {
encryptedDataType.setId(IDGenerator.generateID(null));
}
InboundSecurityToken inboundSecurityToken =
getSecurityToken(inputProcessorChain, xmlSecStartElement, encryptedDataType);
handleSecurityToken(inboundSecurityToken, inputProcessorChain.getSecurityContext(), encryptedDataType);
final String algorithmURI = encryptedDataType.getEncryptionMethod().getAlgorithm();
final int ivLength = JCEAlgorithmMapper.getIVLengthFromURI(algorithmURI) / 8;
Cipher symCipher = getCipher(algorithmURI);
if (encryptedDataType.getCipherData().getCipherReference() != null) {
handleCipherReference(inputProcessorChain, encryptedDataType, symCipher, inboundSecurityToken);
subInputProcessorChain.reset();
return isSecurityHeaderEvent
? subInputProcessorChain.processHeaderEvent()
: subInputProcessorChain.processEvent();
}
XMLSecStartElement parentXMLSecStartElement = xmlSecStartElement.getParentXMLSecStartElement();
if (encryptedHeader) {
parentXMLSecStartElement = parentXMLSecStartElement.getParentXMLSecStartElement();
}
AbstractDecryptedEventReaderInputProcessor decryptedEventReaderInputProcessor =
newDecryptedEventReaderInputProcessor(
encryptedHeader, parentXMLSecStartElement, encryptedDataType, inboundSecurityToken,
inputProcessorChain.getSecurityContext()
);
//add the new created EventReader processor to the chain.
inputProcessorChain.addProcessor(decryptedEventReaderInputProcessor);
inputProcessorChain.getDocumentContext().setIsInEncryptedContent(
inputProcessorChain.getProcessors().indexOf(decryptedEventReaderInputProcessor),
decryptedEventReaderInputProcessor);
//fire here only ContentEncryptedElementEvents
//the other ones will be fired later, because we don't know the encrypted element name yet
//important: this must occur after setIsInEncryptedContent!
if (SecurePart.Modifier.Content.getModifier().equals(encryptedDataType.getType())) {
handleEncryptedContent(inputProcessorChain, xmlSecStartElement.getParentXMLSecStartElement(),
inboundSecurityToken, encryptedDataType);
}
// Process the next event - we need to do this in case it's an xop:Include tag as we handle this
// differently compared to the usual inlined bytes. Note this only works if we have a single xop:Include
// chile element
XMLSecEvent nextEvent = null;
subInputProcessorChain.reset();
if (isSecurityHeaderEvent) {
nextEvent = subInputProcessorChain.processHeaderEvent();
} else {
nextEvent = subInputProcessorChain.processEvent();
}
InputStream decryptInputStream = null;
if (nextEvent.isStartElement() && nextEvent.asStartElement().getName().equals(XMLSecurityConstants.TAG_XOP_INCLUDE)) {
try {
// Unmarshal the XOP Include Element
Deque<XMLSecEvent> xmlSecEvents = new ArrayDeque<>();
xmlSecEvents.push(nextEvent);
xmlSecEvents.push(XMLSecEventFactory.createXmlSecEndElement(XMLSecurityConstants.TAG_XOP_INCLUDE));
Unmarshaller unmarshaller =
XMLSecurityConstants.getJaxbUnmarshaller(getSecurityProperties().isDisableSchemaValidation());
@SuppressWarnings("unchecked")
JAXBElement<Include> includeJAXBElement =
(JAXBElement<Include>) unmarshaller.unmarshal(new XMLSecurityEventReader(xmlSecEvents, 0));
Include include = includeJAXBElement.getValue();
String href = include.getHref();
decryptInputStream =
handleXOPInclude(inputProcessorChain, encryptedDataType, href, symCipher, inboundSecurityToken);
} catch (JAXBException e) {
throw new XMLSecurityException(e);
}
} else {
//create a new Thread for streaming decryption
DecryptionThread decryptionThread = new DecryptionThread(subInputProcessorChain, isSecurityHeaderEvent, nextEvent);
Key decryptionKey =
inboundSecurityToken.getSecretKey(algorithmURI, XMLSecurityConstants.Enc, encryptedDataType.getId());
decryptionKey = XMLSecurityUtils.prepareSecretKey(algorithmURI, decryptionKey.getEncoded());
decryptionThread.setSecretKey(decryptionKey);
decryptionThread.setSymmetricCipher(symCipher);
decryptionThread.setIvLength(ivLength);
Thread thread = new Thread(decryptionThread);
thread.setPriority(Thread.NORM_PRIORITY + 1);
thread.setName("decryption thread");
//when an exception in the decryption thread occurs, we want to forward them:
thread.setUncaughtExceptionHandler(decryptedEventReaderInputProcessor);
decryptedEventReaderInputProcessor.setDecryptionThread(thread);
//we have to start the thread before we call decryptionThread.getPipedInputStream().
//Otherwise we will end in a deadlock, because the StAX reader expects already data.
//@See some lines below:
LOG.debug("Starting decryption thread");
thread.start();
decryptInputStream = decryptionThread.getPipedInputStream();
}
InputStream prologInputStream;
InputStream epilogInputStream;
try {
prologInputStream = writeWrapperStartElement(xmlSecStartElement);
epilogInputStream = writeWrapperEndElement();
} catch (UnsupportedEncodingException e) {
throw new XMLSecurityException(e);
} catch (IOException e) {
throw new XMLSecurityException(e);
}
decryptInputStream = applyTransforms(referenceType, decryptInputStream);
//spec says (4.2): "The cleartext octet sequence obtained in step 3 is
//interpreted as UTF-8 encoded character data."
XMLStreamReader xmlStreamReader =
inputProcessorChain.getSecurityContext().<XMLInputFactory>get(
XMLSecurityConstants.XMLINPUTFACTORY).createXMLStreamReader(
new MultiInputStream(prologInputStream, decryptInputStream, epilogInputStream), StandardCharsets.UTF_8.name());
//forward to wrapper element
forwardToWrapperElement(xmlStreamReader);
decryptedEventReaderInputProcessor.setXmlStreamReader(xmlStreamReader);
if (isSecurityHeaderEvent) {
return decryptedEventReaderInputProcessor.processNextHeaderEvent(inputProcessorChain);
} else {
return decryptedEventReaderInputProcessor.processNextEvent(inputProcessorChain);
}
}
}
return xmlSecEvent;
}
protected InputStream applyTransforms(ReferenceType referenceType, InputStream inputStream) throws XMLSecurityException {
return inputStream;
}
private InputStream writeWrapperStartElement(XMLSecStartElement xmlSecStartElement) throws IOException {
//temporary writer to write the dummy wrapper element with all namespaces in the current scope
//spec says (4.2): "The cleartext octet sequence obtained in step 3 is interpreted as UTF-8 encoded character data."
StringBuilder stringBuilder = new StringBuilder();
stringBuilder.append('<');
stringBuilder.append(wrapperElementName.getPrefix());
stringBuilder.append(':');
stringBuilder.append(wrapperElementName.getLocalPart());
stringBuilder.append(" xmlns:");
stringBuilder.append(wrapperElementName.getPrefix());
stringBuilder.append("=\"");
stringBuilder.append(wrapperElementName.getNamespaceURI());
stringBuilder.append('\"');
//apply all namespaces from current scope to get a valid documentfragment:
List<XMLSecNamespace> comparableNamespacesToApply = new ArrayList<>();
List<XMLSecNamespace> comparableNamespaceList = new ArrayList<>();
xmlSecStartElement.getNamespacesFromCurrentScope(comparableNamespaceList);
//reverse iteration -> From current element namespaces to parent namespaces
for (int i = comparableNamespaceList.size() - 1; i >= 0; i--) {
XMLSecNamespace comparableNamespace = comparableNamespaceList.get(i);
if (!comparableNamespacesToApply.contains(comparableNamespace)) {
comparableNamespacesToApply.add(comparableNamespace);
stringBuilder.append(' ');
String prefix = comparableNamespace.getPrefix();
String uri = comparableNamespace.getNamespaceURI();
if (prefix == null || prefix.isEmpty()) {
stringBuilder.append("xmlns=\"");
stringBuilder.append(uri);
stringBuilder.append('\"');
} else {
stringBuilder.append("xmlns:");
stringBuilder.append(prefix);
stringBuilder.append("=\"");
stringBuilder.append(uri);
stringBuilder.append('\"');
}
}
}
stringBuilder.append('>');
return new UnsyncByteArrayInputStream(stringBuilder.toString().getBytes(StandardCharsets.UTF_8));
}
private InputStream writeWrapperEndElement() throws IOException {
StringBuilder stringBuilder = new StringBuilder();
//close the dummy wrapper element:
stringBuilder.append("</");
stringBuilder.append(wrapperElementName.getPrefix());
stringBuilder.append(':');
stringBuilder.append(wrapperElementName.getLocalPart());
stringBuilder.append('>');
return new UnsyncByteArrayInputStream(stringBuilder.toString().getBytes(StandardCharsets.UTF_8));
}
private void forwardToWrapperElement(XMLStreamReader xmlStreamReader) throws XMLStreamException {
do {
if (xmlStreamReader.getEventType() == XMLStreamConstants.START_ELEMENT
&& xmlStreamReader.getName().equals(wrapperElementName)) {
xmlStreamReader.next();
break;
}
xmlStreamReader.next();
} while (xmlStreamReader.hasNext());
}
private Cipher getCipher(String algorithmURI) throws XMLSecurityException {
Cipher symCipher;
try {
String jceName = JCEAlgorithmMapper.translateURItoJCEID(algorithmURI);
String jceProvider = JCEAlgorithmMapper.getJCEProviderFromURI(algorithmURI);
if (jceName == null) {
throw new XMLSecurityException("algorithms.NoSuchMap",
new Object[] {algorithmURI});
}
if (jceProvider != null) {
symCipher = Cipher.getInstance(jceName, jceProvider);
} else {
symCipher = Cipher.getInstance(jceName);
}
//we have to defer the initialization of the cipher until we can extract the IV...
} catch (NoSuchAlgorithmException e) {
throw new XMLSecurityException(e);
} catch (NoSuchPaddingException e) {
throw new XMLSecurityException(e);
} catch (NoSuchProviderException e) {
throw new XMLSecurityException(e);
}
return symCipher;
}
private InboundSecurityToken getSecurityToken(InputProcessorChain inputProcessorChain,
XMLSecStartElement xmlSecStartElement,
EncryptedDataType encryptedDataType) throws XMLSecurityException {
KeyInfoType keyInfoType = this.keyInfoType;
if (keyInfoType == null) {
keyInfoType = encryptedDataType.getKeyInfo();
}
if (keyInfoType != null) {
final EncryptedKeyType encryptedKeyType =
XMLSecurityUtils.getQNameType(keyInfoType.getContent(), XMLSecurityConstants.TAG_xenc_EncryptedKey);
if (encryptedKeyType != null) {
XMLEncryptedKeyInputHandler handler = new XMLEncryptedKeyInputHandler();
handler.handle(inputProcessorChain, encryptedKeyType, xmlSecStartElement, getSecurityProperties());
SecurityTokenProvider<? extends InboundSecurityToken> securityTokenProvider =
inputProcessorChain.getSecurityContext().getSecurityTokenProvider(encryptedKeyType.getId());
return securityTokenProvider.getSecurityToken();
}
}
//retrieve the securityToken which must be used for decryption
return SecurityTokenFactory.getInstance().getSecurityToken(
keyInfoType, SecurityTokenConstants.KeyUsage_Decryption,
getSecurityProperties(),
inputProcessorChain.getSecurityContext());
}
private EncryptedDataType parseEncryptedDataStructure(
boolean isSecurityHeaderEvent, XMLSecEvent xmlSecEvent, InputProcessorChain subInputProcessorChain)
throws XMLStreamException, XMLSecurityException {
Deque<XMLSecEvent> xmlSecEvents = new ArrayDeque<>();
xmlSecEvents.push(xmlSecEvent);
XMLSecEvent encryptedDataXMLSecEvent;
int count = 0;
int keyInfoCount = 0;
do {
subInputProcessorChain.reset();
if (isSecurityHeaderEvent) {
encryptedDataXMLSecEvent = subInputProcessorChain.processHeaderEvent();
} else {
encryptedDataXMLSecEvent = subInputProcessorChain.processEvent();
}
xmlSecEvents.push(encryptedDataXMLSecEvent);
if (++count >= maximumAllowedEncryptedDataEvents) {
throw new XMLSecurityException("stax.xmlStructureSizeExceeded",
new Object[] {maximumAllowedEncryptedDataEvents});
}
//the keyInfoCount is necessary to prevent early while-loop abort when the KeyInfo also contains a CipherValue.
if (encryptedDataXMLSecEvent.getEventType() == XMLStreamConstants.START_ELEMENT
&& encryptedDataXMLSecEvent.asStartElement().getName().equals(
XMLSecurityConstants.TAG_dsig_KeyInfo)) {
keyInfoCount++;
} else if (encryptedDataXMLSecEvent.getEventType() == XMLStreamConstants.END_ELEMENT
&& encryptedDataXMLSecEvent.asEndElement().getName().equals(
XMLSecurityConstants.TAG_dsig_KeyInfo)) {
keyInfoCount--;
}
}
while (!((encryptedDataXMLSecEvent.getEventType() == XMLStreamConstants.START_ELEMENT
&& encryptedDataXMLSecEvent.asStartElement().getName().equals(XMLSecurityConstants.TAG_xenc_CipherValue)
|| encryptedDataXMLSecEvent.getEventType() == XMLStreamConstants.END_ELEMENT
&& encryptedDataXMLSecEvent.asEndElement().getName().equals(XMLSecurityConstants.TAG_xenc_EncryptedData))
&& keyInfoCount == 0));
xmlSecEvents.push(XMLSecEventFactory.createXmlSecEndElement(XMLSecurityConstants.TAG_xenc_CipherValue));
xmlSecEvents.push(XMLSecEventFactory.createXmlSecEndElement(XMLSecurityConstants.TAG_xenc_CipherData));
xmlSecEvents.push(XMLSecEventFactory.createXmlSecEndElement(XMLSecurityConstants.TAG_xenc_EncryptedData));
EncryptedDataType encryptedDataType;
try {
Unmarshaller unmarshaller =
XMLSecurityConstants.getJaxbUnmarshaller(getSecurityProperties().isDisableSchemaValidation());
@SuppressWarnings("unchecked")
JAXBElement<EncryptedDataType> encryptedDataTypeJAXBElement =
(JAXBElement<EncryptedDataType>) unmarshaller.unmarshal(new XMLSecurityEventReader(xmlSecEvents, 0));
encryptedDataType = encryptedDataTypeJAXBElement.getValue();
} catch (JAXBException e) {
throw new XMLSecurityException(e);
}
return encryptedDataType;
}
private XMLSecEvent readAndBufferEncryptedHeader(InputProcessorChain inputProcessorChain, boolean isSecurityHeaderEvent,
XMLSecEvent xmlSecEvent) throws XMLStreamException, XMLSecurityException {
InputProcessorChain subInputProcessorChain = inputProcessorChain.createSubChain(this);
do {
tmpXmlEventList.push(xmlSecEvent);
subInputProcessorChain.reset();
if (isSecurityHeaderEvent) {
xmlSecEvent = subInputProcessorChain.processHeaderEvent();
} else {
xmlSecEvent = subInputProcessorChain.processEvent();
}
}
while (!(xmlSecEvent.getEventType() == XMLStreamConstants.START_ELEMENT
&& xmlSecEvent.asStartElement().getName().equals(XMLSecurityConstants.TAG_xenc_EncryptedData)));
tmpXmlEventList.push(xmlSecEvent);
return xmlSecEvent;
}
protected abstract AbstractDecryptedEventReaderInputProcessor newDecryptedEventReaderInputProcessor(
boolean encryptedHeader, XMLSecStartElement xmlSecStartElement, EncryptedDataType currentEncryptedDataType,
InboundSecurityToken inboundSecurityToken, InboundSecurityContext inboundSecurityContext) throws XMLSecurityException;
protected abstract void handleSecurityToken(InboundSecurityToken inboundSecurityToken, InboundSecurityContext inboundSecurityContext,
EncryptedDataType encryptedDataType) throws XMLSecurityException;
protected abstract void handleEncryptedContent(InputProcessorChain inputProcessorChain,
XMLSecStartElement parentXMLSecStartElement,
InboundSecurityToken inboundSecurityToken,
EncryptedDataType encryptedDataType) throws XMLSecurityException;
protected abstract void handleCipherReference(InputProcessorChain inputProcessorChain,
EncryptedDataType encryptedDataType, Cipher cipher,
InboundSecurityToken inboundSecurityToken) throws XMLSecurityException;
protected abstract InputStream handleXOPInclude(InputProcessorChain inputProcessorChain,
EncryptedDataType encryptedDataType, String href, Cipher cipher,
InboundSecurityToken inboundSecurityToken) throws XMLSecurityException;
protected ReferenceType matchesReferenceId(XMLSecStartElement xmlSecStartElement) {
Attribute refId = getReferenceIDAttribute(xmlSecStartElement);
if (refId != null) {
//does the id exist in the referenceList?
return this.references.get(refId.getValue());
}
return null;
}
@Override
public void doFinal(InputProcessorChain inputProcessorChain) throws XMLStreamException, XMLSecurityException {
inputProcessorChain.doFinal();
//here we check if all references where processed.
if (references != null) {
Iterator<Map.Entry<String, ReferenceType>> refEntryIterator = this.references.entrySet().iterator();
while (refEntryIterator.hasNext()) {
Map.Entry<String, ReferenceType> referenceTypeEntry = refEntryIterator.next();
if (!processedReferences.contains(referenceTypeEntry.getValue())) {
throw new XMLSecurityException("stax.encryption.unprocessedReferences");
}
}
}
}
/**
* The DecryptedEventReaderInputProcessor reads the decrypted stream with a StAX reader and
* forwards the generated XMLEvents
*/
public abstract class AbstractDecryptedEventReaderInputProcessor
extends AbstractInputProcessor implements Thread.UncaughtExceptionHandler {
private int currentXMLStructureDepth;
private XMLStreamReader xmlStreamReader;
private XMLSecStartElement parentXmlSecStartElement;
private boolean encryptedHeader = false;
private final InboundSecurityToken inboundSecurityToken;
private boolean rootElementProcessed;
private EncryptedDataType encryptedDataType;
private Thread decryptionThread;
public AbstractDecryptedEventReaderInputProcessor(
XMLSecurityProperties securityProperties, SecurePart.Modifier encryptionModifier,
boolean encryptedHeader, XMLSecStartElement xmlSecStartElement,
EncryptedDataType encryptedDataType,
AbstractDecryptInputProcessor abstractDecryptInputProcessor,
InboundSecurityToken inboundSecurityToken
) {
super(securityProperties);
addAfterProcessor(abstractDecryptInputProcessor);
this.rootElementProcessed = encryptionModifier == SecurePart.Modifier.Content;
this.encryptedHeader = encryptedHeader;
this.inboundSecurityToken = inboundSecurityToken;
this.parentXmlSecStartElement = xmlSecStartElement;
this.encryptedDataType = encryptedDataType;
//xmlSecStartElement can be null when the root element is the EncryptedData element:
if (xmlSecStartElement != null) {
this.currentXMLStructureDepth = xmlSecStartElement.getDocumentLevel();
}
}
public void setDecryptionThread(Thread decryptionThread) {
this.decryptionThread = decryptionThread;
}
public void setXmlStreamReader(XMLStreamReader xmlStreamReader) {
this.xmlStreamReader = xmlStreamReader;
}
@Override
public XMLSecEvent processNextHeaderEvent(InputProcessorChain inputProcessorChain)
throws XMLStreamException, XMLSecurityException {
return processEvent(inputProcessorChain, true);
}
@Override
public XMLSecEvent processNextEvent(InputProcessorChain inputProcessorChain)
throws XMLStreamException, XMLSecurityException {
return processEvent(inputProcessorChain, false);
}
private XMLSecEvent processEvent(InputProcessorChain inputProcessorChain, boolean headerEvent)
throws XMLStreamException, XMLSecurityException {
//did a execption occur during decryption in the decryption thread?
testAndThrowUncaughtException();
XMLSecEvent xmlSecEvent = XMLSecEventFactory.allocate(xmlStreamReader, parentXmlSecStartElement);
//here we request the next XMLEvent from the decryption thread
//instead from the processor-chain as we normally would do
if (XMLStreamConstants.START_ELEMENT == xmlSecEvent.getEventType()) {
currentXMLStructureDepth++;
if (currentXMLStructureDepth > maximumAllowedXMLStructureDepth) {
throw new XMLSecurityException(
"secureProcessing.MaximumAllowedXMLStructureDepth",
new Object[] {maximumAllowedXMLStructureDepth}
);
}
parentXmlSecStartElement = xmlSecEvent.asStartElement();
if (!rootElementProcessed) {
handleEncryptedElement(inputProcessorChain, parentXmlSecStartElement, this.inboundSecurityToken, encryptedDataType);
rootElementProcessed = true;
}
} else if (XMLStreamConstants.END_ELEMENT == xmlSecEvent.getEventType()) {
currentXMLStructureDepth--;
if (parentXmlSecStartElement != null) {
parentXmlSecStartElement = parentXmlSecStartElement.getParentXMLSecStartElement();
}
if (xmlSecEvent.asEndElement().getName().equals(wrapperElementName)) {
InputProcessorChain subInputProcessorChain = inputProcessorChain.createSubChain(this);
//skip EncryptedHeader Element when we processed it.
QName endElement;
if (encryptedHeader) {
endElement = XMLSecurityConstants.TAG_wsse11_EncryptedHeader;
} else {
endElement = XMLSecurityConstants.TAG_xenc_EncryptedData;
}
//read and discard XMLEvents until the EncryptedData structure
XMLSecEvent endEvent;
do {
subInputProcessorChain.reset();
if (headerEvent) {
endEvent = subInputProcessorChain.processHeaderEvent();
} else {
endEvent = subInputProcessorChain.processEvent();
}
}
while (!(endEvent.getEventType() == XMLStreamConstants.END_ELEMENT
&& endEvent.asEndElement().getName().equals(endElement)));
inputProcessorChain.getDocumentContext().unsetIsInEncryptedContent(this);
//...fetch the next (unencrypted) event
if (headerEvent) {
xmlSecEvent = inputProcessorChain.processHeaderEvent();
} else {
xmlSecEvent = inputProcessorChain.processEvent();
}
if (decryptionThread != null) {
//wait until the decryption thread dies...
try {
decryptionThread.join();
} catch (InterruptedException e) {
throw new XMLStreamException(e);
}
//...and test again for an exception in the decryption thread.
testAndThrowUncaughtException();
}
inputProcessorChain.removeProcessor(this);
}
}
xmlStreamReader.next();
return xmlSecEvent;
}
protected abstract void handleEncryptedElement(
InputProcessorChain inputProcessorChain, XMLSecStartElement xmlSecStartElement,
InboundSecurityToken inboundSecurityToken, EncryptedDataType encryptedDataType) throws XMLSecurityException;
private volatile Throwable thrownException;
@Override
public void uncaughtException(Thread t, Throwable e) {
this.thrownException = e;
}
private void testAndThrowUncaughtException() throws XMLStreamException {
if (this.thrownException != null) {
if (this.thrownException instanceof UncheckedXMLSecurityException) {
UncheckedXMLSecurityException uxse = (UncheckedXMLSecurityException) this.thrownException;
throw new XMLStreamException(uxse.getCause());
} else {
throw new XMLStreamException(this.thrownException.getCause());
}
}
}
}
/**
* The DecryptionThread handles encrypted XML-Parts
*/
static class DecryptionThread implements Runnable {
private final InputProcessorChain inputProcessorChain;
private final boolean header;
private final PipedOutputStream pipedOutputStream;
private final PipedInputStream pipedInputStream;
private Cipher symmetricCipher;
private int ivLength;
private Key secretKey;
private final XMLSecEvent firstEvent;
protected DecryptionThread(InputProcessorChain inputProcessorChain,
boolean header,
XMLSecEvent firstEvent) throws XMLStreamException, XMLSecurityException {
this.inputProcessorChain = inputProcessorChain;
this.header = header;
this.firstEvent = firstEvent;
//prepare the piped streams and connect them:
this.pipedInputStream = new PipedInputStream(8192 * 5);
try {
this.pipedOutputStream = new PipedOutputStream(pipedInputStream);
} catch (IOException e) {
throw new XMLStreamException(e);
}
}
public PipedInputStream getPipedInputStream() {
return pipedInputStream;
}
private XMLSecEvent processNextEvent() throws XMLSecurityException, XMLStreamException {
inputProcessorChain.reset();
if (header) {
return inputProcessorChain.processHeaderEvent();
} else {
return inputProcessorChain.processEvent();
}
}
@Override
public void run() {
try {
final OutputStream outputStream;
final Cipher cipher = getSymmetricCipher();
if (cipher.getAlgorithm().toUpperCase().contains("GCM")) {
//we have to buffer the whole data until they are authenticated.
//In GCM mode the authentication tag is appended after the last cipher block...
outputStream = new FullyBufferedOutputStream(pipedOutputStream);
} else {
outputStream = pipedOutputStream;
}
final CipherOutputStream cipherOutputStream = new CipherOutputStream(outputStream, cipher) {
//override close() to workaround a bug in oracle-jdk:
//authentication failures when using AEAD ciphers are silently ignored...
@Override
public void close() throws IOException {
super.flush();
try {
byte[] bytes = cipher.doFinal();
outputStream.write(bytes);
outputStream.close();
} catch (IllegalBlockSizeException e) {
throw new IOException(e);
} catch (BadPaddingException e) {
throw new IOException(e);
}
}
};
IVSplittingOutputStream ivSplittingOutputStream = new IVSplittingOutputStream(
cipherOutputStream,
cipher, getSecretKey(), getIvLength());
//buffering seems not to help
//bufferedOutputStream = new BufferedOutputStream(new Base64OutputStream(ivSplittingOutputStream, false), 8192 * 5);
ReplaceableOuputStream replaceableOuputStream = new ReplaceableOuputStream(ivSplittingOutputStream);
OutputStream base64OutputStream = new Base64OutputStream(replaceableOuputStream, false);
ivSplittingOutputStream.setParentOutputStream(replaceableOuputStream);
OutputStreamWriter outputStreamWriter =
new OutputStreamWriter(base64OutputStream,
Charset.forName(inputProcessorChain.getDocumentContext().getEncoding()));
//read the encrypted data from the stream until an end-element occurs and write then
//to the decrypter-stream
XMLSecEvent xmlSecEvent = firstEvent;
exitLoop:
do {
switch (xmlSecEvent.getEventType()) {
case XMLStreamConstants.END_ELEMENT:
//this must be the CipherValue EndElement.
break exitLoop;
case XMLStreamConstants.CHARACTERS:
final char[] data = xmlSecEvent.asCharacters().getText();
outputStreamWriter.write(data);
break;
default:
throw new XMLSecurityException(
"stax.unexpectedXMLEvent",
new Object[] {XMLSecurityUtils.getXMLEventAsString(xmlSecEvent)}
);
}
xmlSecEvent = processNextEvent();
} while (true);
//close to get Cipher.doFinal() called
outputStreamWriter.close();
// Clean the secret key from memory now that we're done with it
if (secretKey instanceof Destroyable) {
try {
((Destroyable)secretKey).destroy();
} catch (DestroyFailedException e) {
LOG.debug("Error destroying key: {}", e.getMessage());
}
}
LOG.debug("Decryption thread finished");
} catch (Exception e) {
try {
//we have to close the pipe when an exception occurs. Otherwise we can run into a deadlock when an exception occurs
//before we have written any byte to the pipe.
this.pipedOutputStream.close();
} catch (IOException e1) { //NOPMD
//ignore since we will throw the original exception below
}
throw new UncheckedXMLSecurityException(e);
}
}
protected Cipher getSymmetricCipher() {
return symmetricCipher;
}
protected void setSymmetricCipher(Cipher symmetricCipher) {
this.symmetricCipher = symmetricCipher;
}
int getIvLength() {
return ivLength;
}
void setIvLength(int ivLength) {
this.ivLength = ivLength;
}
protected Key getSecretKey() {
return secretKey;
}
protected void setSecretKey(Key secretKey) {
this.secretKey = secretKey;
}
}
}