View Javadoc
1   /**
2    * Licensed to the Apache Software Foundation (ASF) under one
3    * or more contributor license agreements. See the NOTICE file
4    * distributed with this work for additional information
5    * regarding copyright ownership. The ASF licenses this file
6    * to you under the Apache License, Version 2.0 (the
7    * "License"); you may not use this file except in compliance
8    * with the License. You may obtain a copy of the License at
9    *
10   * http://www.apache.org/licenses/LICENSE-2.0
11   *
12   * Unless required by applicable law or agreed to in writing,
13   * software distributed under the License is distributed on an
14   * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
15   * KIND, either express or implied. See the License for the
16   * specific language governing permissions and limitations
17   * under the License.
18   */
19  
20  package org.apache.wss4j.dom.util;
21  
22  import org.apache.wss4j.common.ext.Attachment;
23  import org.apache.wss4j.common.ext.AttachmentRequestCallback;
24  import org.apache.wss4j.common.ext.AttachmentResultCallback;
25  import org.apache.wss4j.common.ext.WSSecurityException;
26  import org.apache.wss4j.common.util.AttachmentUtils;
27  import org.apache.wss4j.common.util.XMLUtils;
28  import org.apache.wss4j.dom.WSConstants;
29  import org.apache.wss4j.dom.WSDataRef;
30  import org.apache.wss4j.dom.WSDocInfo;
31  import org.apache.wss4j.dom.callback.CallbackLookup;
32  import org.apache.xml.security.algorithms.JCEMapper;
33  import org.apache.xml.security.encryption.Serializer;
34  import org.apache.xml.security.encryption.XMLCipher;
35  import org.apache.xml.security.encryption.XMLEncryptionException;
36  import org.apache.xml.security.parser.XMLParserException;
37  import org.apache.xml.security.utils.JavaUtils;
38  import org.w3c.dom.Attr;
39  import org.w3c.dom.Document;
40  import org.w3c.dom.Element;
41  import org.w3c.dom.NamedNodeMap;
42  import org.w3c.dom.Node;
43  import org.xml.sax.SAXException;
44  
45  import javax.crypto.Cipher;
46  import javax.crypto.NoSuchPaddingException;
47  import javax.crypto.SecretKey;
48  import javax.security.auth.callback.Callback;
49  import javax.security.auth.callback.CallbackHandler;
50  import javax.security.auth.callback.UnsupportedCallbackException;
51  
52  import java.io.ByteArrayInputStream;
53  import java.io.IOException;
54  import java.io.InputStream;
55  import java.security.NoSuchAlgorithmException;
56  import java.util.List;
57  
58  public final class EncryptionUtils {
59  
60      private EncryptionUtils() {
61          // complete
62      }
63  
64      /**
65       * Look up the encrypted data. First try Id="someURI". If no such Id then try
66       * wsu:Id="someURI".
67       *
68       * @param wsDocInfo The WSDocInfo object to use
69       * @param dataRefURI The URI of EncryptedData
70       * @return The EncryptedData element
71       * @throws WSSecurityException if the EncryptedData element referenced by dataRefURI is
72       * not found
73       */
74      public static Element
75      findEncryptedDataElement(
76          WSDocInfo wsDocInfo,
77          String dataRefURI
78      ) throws WSSecurityException {
79          CallbackLookup callbackLookup = wsDocInfo.getCallbackLookup();
80          Element encryptedDataElement =
81              callbackLookup.getElement(dataRefURI, null, true);
82          if (encryptedDataElement == null) {
83              throw new WSSecurityException(
84                  WSSecurityException.ErrorCode.INVALID_SECURITY, "dataRef",
85                  new Object[] {dataRefURI});
86          }
87          if (encryptedDataElement.getLocalName().equals(WSConstants.ENCRYPTED_HEADER)
88              && encryptedDataElement.getNamespaceURI().equals(WSConstants.WSSE11_NS)) {
89              Node child = encryptedDataElement.getFirstChild();
90              while (child != null && child.getNodeType() != Node.ELEMENT_NODE) {
91                  child = child.getNextSibling();
92              }
93              return (Element)child;
94          }
95          return encryptedDataElement;
96      }
97  
98      /**
99       * Decrypt the EncryptedData argument using a SecretKey.
100      * @param doc The (document) owner of EncryptedData
101      * @param dataRefURI The URI of EncryptedData
102      * @param encData The EncryptedData element
103      * @param symmetricKey The SecretKey with which to decrypt EncryptedData
104      * @param symEncAlgo The symmetric encryption algorithm to use
105      * @param attachmentCallbackHandler The CallbackHandler from which to get attachments
106      * @throws WSSecurityException
107      */
108     public static WSDataRef
109     decryptEncryptedData(
110         Document doc,
111         String dataRefURI,
112         Element encData,
113         SecretKey symmetricKey,
114         String symEncAlgo,
115         CallbackHandler attachmentCallbackHandler
116     ) throws WSSecurityException {
117         return decryptEncryptedData(doc, dataRefURI, encData, symmetricKey,
118                                     symEncAlgo, attachmentCallbackHandler, null);
119 
120     }
121     /**
122      * Decrypt the EncryptedData argument using a SecretKey.
123      * @param doc The (document) owner of EncryptedData
124      * @param dataRefURI The URI of EncryptedData
125      * @param encData The EncryptedData element
126      * @param symmetricKey The SecretKey with which to decrypt EncryptedData
127      * @param symEncAlgo The symmetric encryption algorithm to use
128      * @param attachmentCallbackHandler The CallbackHandler from which to get attachments
129      * @throws WSSecurityException
130      */
131     public static WSDataRef
132     decryptEncryptedData(
133         Document doc,
134         String dataRefURI,
135         Element encData,
136         SecretKey symmetricKey,
137         String symEncAlgo,
138         CallbackHandler attachmentCallbackHandler,
139         Serializer encryptionSerializer
140     ) throws WSSecurityException {
141 
142         // See if it is an attachment, and handle that differently
143         String typeStr = encData.getAttributeNS(null, "Type");
144         String xopURI = getXOPURIFromEncryptedData(encData);
145         if (typeStr != null
146             && (WSConstants.SWA_ATTACHMENT_ENCRYPTED_DATA_TYPE_CONTENT_ONLY.equals(typeStr)
147                 || WSConstants.SWA_ATTACHMENT_ENCRYPTED_DATA_TYPE_COMPLETE.equals(typeStr))) {
148 
149             Element cipherData = XMLUtils.getDirectChildElement(encData, "CipherData", WSConstants.ENC_NS);
150             if (cipherData == null) {
151                 throw new WSSecurityException(WSSecurityException.ErrorCode.FAILED_CHECK);
152             }
153             Element cipherReference = XMLUtils.getDirectChildElement(cipherData, "CipherReference", WSConstants.ENC_NS);
154             if (cipherReference == null) {
155                 throw new WSSecurityException(WSSecurityException.ErrorCode.FAILED_CHECK);
156             }
157             String uri = cipherReference.getAttributeNS(null, "URI");
158 
159             return decryptAttachment(dataRefURI, uri, encData, symmetricKey, symEncAlgo, attachmentCallbackHandler);
160         }
161 
162         WSDataRef dataRef = new WSDataRef();
163         dataRef.setEncryptedElement(encData);
164         dataRef.setWsuId(dataRefURI);
165         dataRef.setAlgorithm(symEncAlgo);
166 
167         boolean content = X509Util.isContent(encData);
168         dataRef.setContent(content);
169 
170         Element encDataOrig = encData;
171         Node parent = encData.getParentNode();
172         Node previousSibling = encData.getPreviousSibling();
173         if (content) {
174             encData = (Element) encData.getParentNode();
175             parent = encData.getParentNode();
176         }
177 
178         XMLCipher xmlCipher = null;
179         try {
180             if (encryptionSerializer != null) {
181                 xmlCipher = XMLCipher.getInstance(encryptionSerializer, symEncAlgo);
182             } else {
183                 xmlCipher = XMLCipher.getInstance(symEncAlgo);
184             }
185             xmlCipher.setSecureValidation(true);
186             xmlCipher.init(XMLCipher.DECRYPT_MODE, symmetricKey);
187         } catch (XMLEncryptionException ex) {
188             throw new WSSecurityException(
189                     WSSecurityException.ErrorCode.UNSUPPORTED_ALGORITHM, ex
190             );
191         }
192 
193         Node decryptedNode = null;
194         try {
195             if (xopURI != null) {
196                 Element tempEncData;
197 
198                 //if content == true, use encDataOrig (i.e., actual EncryptedData element instead of parent)
199                 //We will replace the EncryptedData element itself with the decrypted data found in attachment
200                 if (content) {
201                     tempEncData = encDataOrig;
202                 } else {
203                     tempEncData = encData;
204                 }
205                 decryptedNode = decryptXopAttachment(symmetricKey, symEncAlgo, attachmentCallbackHandler,
206                                                      xopURI, tempEncData);
207             } else {
208                 //in this case, the XMLCipher knows how to handle encData when it's the parent node
209                 // (i.e., when content == true)
210                 xmlCipher.doFinal(doc, encData, content);
211             }
212         } catch (Exception ex) {
213             throw new WSSecurityException(WSSecurityException.ErrorCode.FAILED_CHECK, ex);
214         }
215 
216         if (parent.getLocalName().equals(WSConstants.ENCRYPTED_HEADER)
217             && parent.getNamespaceURI().equals(WSConstants.WSSE11_NS)
218             || parent.getLocalName().equals(WSConstants.ENCRYPED_ASSERTION_LN)
219             && parent.getNamespaceURI().equals(WSConstants.SAML2_NS)) {
220 
221             Node decryptedHeader = parent.getFirstChild();
222             Node soapHeader = parent.getParentNode();
223             soapHeader.replaceChild(decryptedHeader, parent);
224 
225             dataRef.setProtectedElement((Element)decryptedHeader);
226             dataRef.setXpath(getXPath(decryptedHeader));
227         } else if (content) {
228             dataRef.setProtectedElement(encData);
229             dataRef.setXpath(getXPath(encData));
230         } else {
231             if (decryptedNode == null) {
232                 if (previousSibling == null) {
233                     decryptedNode = parent.getFirstChild();
234                 } else {
235                     decryptedNode = previousSibling.getNextSibling();
236                 }
237             }
238             if (decryptedNode != null && Node.ELEMENT_NODE == decryptedNode.getNodeType()) {
239                 dataRef.setProtectedElement((Element)decryptedNode);
240             }
241             dataRef.setXpath(getXPath(decryptedNode));
242         }
243 
244         return dataRef;
245     }
246 
247     private static String getXOPURIFromEncryptedData(Element encData) {
248         Element cipherValue = getCipherValueFromEncryptedData(encData);
249         if (cipherValue != null) {
250             return getXOPURIFromCipherValue(cipherValue);
251         }
252 
253         return null;
254     }
255 
256     public static Element getCipherValueFromEncryptedData(Element encData) {
257         Element cipherData = XMLUtils.getDirectChildElement(encData, "CipherData", WSConstants.ENC_NS);
258         if (cipherData != null) {
259             return XMLUtils.getDirectChildElement(cipherData, "CipherValue", WSConstants.ENC_NS);
260         }
261 
262         return null;
263     }
264 
265     public static String getXOPURIFromCipherValue(Element cipherValue) {
266         if (cipherValue != null) {
267             Element cipherValueChild =
268                 XMLUtils.getDirectChildElement(cipherValue, "Include", WSConstants.XOP_NS);
269             if (cipherValueChild != null && cipherValueChild.hasAttributeNS(null, "href")) {
270                 return cipherValueChild.getAttributeNS(null, "href");
271             }
272         }
273 
274         return null;
275     }
276 
277 
278     private static WSDataRef
279     decryptAttachment(
280         String dataRefURI,
281         String uri,
282         Element encData,
283         SecretKey symmetricKey,
284         String symEncAlgo,
285         CallbackHandler attachmentCallbackHandler
286     ) throws WSSecurityException {
287         WSDataRef dataRef = new WSDataRef();
288         dataRef.setWsuId(dataRefURI);
289         dataRef.setAlgorithm(symEncAlgo);
290 
291         try {
292             if (uri == null || uri.length() < 5 || !uri.startsWith("cid:")) {
293                 throw new WSSecurityException(WSSecurityException.ErrorCode.FAILED_CHECK);
294             }
295             dataRef.setWsuId(uri);
296             dataRef.setAttachment(true);
297 
298             if (attachmentCallbackHandler == null) {
299                 throw new WSSecurityException(WSSecurityException.ErrorCode.FAILED_CHECK);
300             }
301 
302             final String attachmentId = AttachmentUtils.getAttachmentId(uri);
303 
304             AttachmentRequestCallback attachmentRequestCallback = new AttachmentRequestCallback();
305             attachmentRequestCallback.setAttachmentId(attachmentId);
306 
307             attachmentCallbackHandler.handle(new Callback[]{attachmentRequestCallback});
308             List<Attachment> attachments = attachmentRequestCallback.getAttachments();
309             if (attachments == null || attachments.isEmpty() || !attachmentId.equals(attachments.get(0).getId())) {
310                 throw new WSSecurityException(
311                         WSSecurityException.ErrorCode.INVALID_SECURITY,
312                         "empty", new Object[] {"Attachment not found"}
313                 );
314             }
315             Attachment attachment = attachments.get(0);
316 
317             final String encAlgo = X509Util.getEncAlgo(encData);
318             final String jceAlgorithm =
319                     JCEMapper.translateURItoJCEID(encAlgo);
320             final Cipher cipher = Cipher.getInstance(jceAlgorithm);
321 
322             InputStream attachmentInputStream = //NOPMD
323                     AttachmentUtils.setupAttachmentDecryptionStream(
324                             encAlgo, cipher, symmetricKey, attachment.getSourceStream());
325 
326             Attachment resultAttachment = new Attachment();
327             resultAttachment.setId(attachment.getId());
328             resultAttachment.setMimeType(encData.getAttributeNS(null, "MimeType"));
329             resultAttachment.setSourceStream(attachmentInputStream);
330             resultAttachment.addHeaders(attachment.getHeaders());
331 
332             String typeStr = encData.getAttributeNS(null, "Type");
333             if (WSConstants.SWA_ATTACHMENT_ENCRYPTED_DATA_TYPE_COMPLETE.equals(typeStr)) {
334                 AttachmentUtils.readAndReplaceEncryptedAttachmentHeaders(
335                         resultAttachment.getHeaders(), attachmentInputStream);
336             }
337 
338             AttachmentResultCallback attachmentResultCallback = new AttachmentResultCallback();
339             attachmentResultCallback.setAttachment(resultAttachment);
340             attachmentResultCallback.setAttachmentId(resultAttachment.getId());
341             attachmentCallbackHandler.handle(new Callback[]{attachmentResultCallback});
342 
343         } catch (UnsupportedCallbackException | IOException
344             | NoSuchAlgorithmException | NoSuchPaddingException e) {
345             throw new WSSecurityException(
346                     WSSecurityException.ErrorCode.FAILED_CHECK, e);
347         }
348 
349         dataRef.setContent(true);
350         // Remove this EncryptedData from the security header to avoid processing it again
351         encData.getParentNode().removeChild(encData);
352 
353         return dataRef;
354     }
355 
356 
357     private static Node decryptXopAttachment(
358        SecretKey symmetricKey, String symEncAlgo, CallbackHandler attachmentCallbackHandler,
359        String xopURI, Element encData
360    ) throws WSSecurityException, IOException, UnsupportedCallbackException, NoSuchAlgorithmException,
361         NoSuchPaddingException, XMLParserException {
362 
363         if (attachmentCallbackHandler == null) {
364             throw new WSSecurityException(WSSecurityException.ErrorCode.FAILED_CHECK);
365         }
366         final String attachmentId = AttachmentUtils.getAttachmentId(xopURI);
367 
368         AttachmentRequestCallback attachmentRequestCallback = new AttachmentRequestCallback();
369         attachmentRequestCallback.setAttachmentId(attachmentId);
370 
371         attachmentCallbackHandler.handle(new Callback[]{attachmentRequestCallback});
372         List<Attachment> attachments = attachmentRequestCallback.getAttachments();
373         if (attachments == null || attachments.isEmpty() || !attachmentId.equals(attachments.get(0).getId())) {
374             throw new WSSecurityException(
375                     WSSecurityException.ErrorCode.INVALID_SECURITY,
376                     "empty", new Object[] {"Attachment not found"}
377             );
378         }
379         Attachment attachment = attachments.get(0);
380 
381         final String jceAlgorithm =
382                 JCEMapper.translateURItoJCEID(symEncAlgo);
383         final Cipher cipher = Cipher.getInstance(jceAlgorithm);
384 
385         InputStream attachmentInputStream = //NOPMD
386                 AttachmentUtils.setupAttachmentDecryptionStream(
387                         symEncAlgo, cipher, symmetricKey, attachment.getSourceStream());
388 
389         // For the xop:Include case, we need to replace the xop:Include Element with the
390         // decrypted Element
391         byte[] bytes = JavaUtils.getBytesFromStream(attachmentInputStream);
392 
393         Document document = null;
394         try {
395             document = org.apache.xml.security.utils.XMLUtils.read(new ByteArrayInputStream(bytes), true);
396         } catch (XMLParserException ex) {
397             if (ex.getCause() instanceof SAXException) {
398                 // A prefix may not have been bound, try to fix the DOM Element in this case.
399                 String fixedElementStr = setParentPrefixes(encData, new String(bytes));
400                 document = org.apache.xml.security.utils.XMLUtils.read(
401                     new ByteArrayInputStream(fixedElementStr.getBytes()), true);
402             } else {
403                 throw ex;
404             }
405         }
406 
407         Node decryptedNode =
408             encData.getOwnerDocument().importNode(document.getDocumentElement(), true);
409         encData.getParentNode().appendChild(decryptedNode);
410         encData.getParentNode().removeChild(encData);
411         return decryptedNode;
412     }
413 
414     /**
415      * Set the parent prefix definitions on the "String" (representation of the Element to be parsed)
416      */
417     private static String setParentPrefixes(Element target, String str) {
418         Node parent = target;
419 
420         // Get the point at where to insert new prefix definitions
421         int insertionIndex = str.indexOf('>');
422         StringBuilder prefix = new StringBuilder(str.substring(0, insertionIndex));
423         StringBuilder suffix = new StringBuilder(str.substring(insertionIndex, str.length()));
424 
425         // Don't add more than 20 prefixes
426         int prefixAddedCount = 0;
427         while (parent.getParentNode() != null && prefixAddedCount < 20
428             && Node.DOCUMENT_NODE != parent.getParentNode().getNodeType()) {
429             parent = parent.getParentNode();
430             NamedNodeMap attributes = parent.getAttributes();
431             int length = attributes.getLength();
432             for (int i = 0; i < length; i++) {
433                 Node attribute = attributes.item(i);
434                 String attrDef = "xmlns:" + attribute.getLocalName();
435                 if (WSConstants.XMLNS_NS.equals(attribute.getNamespaceURI()) && !prefix.toString().contains(attrDef)) {
436                     attrDef += "=\"" + attribute.getNodeValue() + "\"";
437                     prefix.append(' ').append(attrDef);
438                     prefixAddedCount++;
439                 }
440                 if (prefixAddedCount >= 20) {
441                     break;
442                 }
443             }
444         }
445 
446         return prefix.toString() + suffix.toString();
447     }
448 
449     /**
450      * @param decryptedNode the decrypted node
451      * @return a fully built xpath
452      *        (eg. &quot;/soapenv:Envelope/soapenv:Body/ns:decryptedElement&quot;)
453      *        if the decryptedNode is an Element or an Attr node and is not detached
454      *        from the document. <code>null</code> otherwise
455      */
456     public static String getXPath(Node decryptedNode) {
457         if (decryptedNode == null) {
458             return null;
459         }
460 
461         String result = "";
462         if (Node.ELEMENT_NODE == decryptedNode.getNodeType()) {
463             result = decryptedNode.getNodeName();
464             result = prependFullPath(result, decryptedNode.getParentNode());
465         } else if (Node.ATTRIBUTE_NODE == decryptedNode.getNodeType()) {
466             result = "@" + decryptedNode.getNodeName();
467             result = prependFullPath(result, ((Attr)decryptedNode).getOwnerElement());
468         } else {
469             return null;
470         }
471 
472         return result;
473     }
474 
475 
476     /**
477      * Recursively build an absolute xpath (starting with the root &quot;/&quot;)
478      *
479      * @param xpath the xpath expression built so far
480      * @param node the current node whose name is to be prepended
481      * @return a fully built xpath
482      */
483     private static String prependFullPath(String xpath, Node node) {
484         if (node == null) {
485             // probably a detached node... not really useful
486             return null;
487         } else if (Node.ELEMENT_NODE == node.getNodeType()) {
488             xpath = node.getNodeName() + "/" + xpath;
489             return prependFullPath(xpath, node.getParentNode());
490         } else if (Node.DOCUMENT_NODE == node.getNodeType()) {
491             return "/" + xpath;
492         } else {
493             return prependFullPath(xpath, node.getParentNode());
494         }
495     }
496 
497     public static String getDigestAlgorithm(Node encBodyData) throws WSSecurityException {
498         Element tmpE =
499             XMLUtils.getDirectChildElement(
500                 encBodyData, "EncryptionMethod", WSConstants.ENC_NS
501             );
502         if (tmpE != null) {
503             Element digestElement =
504                 XMLUtils.getDirectChildElement(tmpE, "DigestMethod", WSConstants.SIG_NS);
505             if (digestElement != null) {
506                 return digestElement.getAttributeNS(null, "Algorithm");
507             }
508         }
509         return null;
510     }
511 
512     public static String getMGFAlgorithm(Node encBodyData) throws WSSecurityException {
513         Element tmpE =
514             XMLUtils.getDirectChildElement(
515                         encBodyData, "EncryptionMethod", WSConstants.ENC_NS
516                 );
517         if (tmpE != null) {
518             Element mgfElement =
519                 XMLUtils.getDirectChildElement(tmpE, "MGF", WSConstants.ENC11_NS);
520             if (mgfElement != null) {
521                 return mgfElement.getAttributeNS(null, "Algorithm");
522             }
523         }
524         return null;
525     }
526 
527     public static byte[] getPSource(Node encBodyData) throws WSSecurityException {
528         Element tmpE =
529             XMLUtils.getDirectChildElement(
530                         encBodyData, "EncryptionMethod", WSConstants.ENC_NS
531                 );
532         if (tmpE != null) {
533             Element pSourceElement =
534                 XMLUtils.getDirectChildElement(tmpE, "OAEPparams", WSConstants.ENC_NS);
535             if (pSourceElement != null) {
536                 return getDecodedBase64EncodedData(pSourceElement);
537             }
538         }
539         return new byte[0];
540     }
541 
542     /**
543      * Method getDecodedBase64EncodedData
544      *
545      * @param element
546      * @return a byte array containing the decoded data
547      * @throws WSSecurityException
548      */
549     public static byte[] getDecodedBase64EncodedData(Element element) throws WSSecurityException {
550         String text = XMLUtils.getElementText(element);
551         if (text == null) {
552             return new byte[0];
553         }
554         return org.apache.xml.security.utils.XMLUtils.decode(text);
555     }
556 }