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  package org.apache.wss4j.common.util;
20  
21  import org.apache.wss4j.common.WSS4JConstants;
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.xml.security.algorithms.JCEMapper;
27  import org.apache.xml.security.encryption.XMLCipherUtil;
28  import org.apache.xml.security.stax.impl.util.MultiInputStream;
29  import org.apache.xml.security.utils.JavaUtils;
30  import org.w3c.dom.Document;
31  import org.w3c.dom.Element;
32  
33  import javax.crypto.Cipher;
34  import javax.crypto.CipherInputStream;
35  import jakarta.mail.internet.MimeUtility;
36  import javax.security.auth.callback.Callback;
37  import javax.security.auth.callback.CallbackHandler;
38  import javax.security.auth.callback.UnsupportedCallbackException;
39  
40  import java.io.*;
41  import java.net.URLDecoder;
42  import java.net.URLEncoder;
43  import java.nio.charset.StandardCharsets;
44  import java.security.InvalidAlgorithmParameterException;
45  import java.security.InvalidKeyException;
46  import java.security.Key;
47  import java.security.spec.AlgorithmParameterSpec;
48  import java.util.*;
49  
50  public final class AttachmentUtils {
51  
52      public static final String MIME_HEADER_CONTENT_DESCRIPTION = "Content-Description";
53      public static final String MIME_HEADER_CONTENT_DISPOSITION = "Content-Disposition";
54      public static final String MIME_HEADER_CONTENT_ID = "Content-ID";
55      public static final String MIME_HEADER_CONTENT_LOCATION = "Content-Location";
56      public static final String MIME_HEADER_CONTENT_TYPE = "Content-Type";
57  
58      public static final char DOUBLE_QUOTE = '"';
59      public static final char SINGLE_QUOTE = '\'';
60      public static final char LEFT_PARENTHESIS = '(';
61      public static final char RIGHT_PARENTHESIS = ')';
62      public static final char CARRIAGE_RETURN = '\r';
63      public static final char LINEFEED = '\n';
64      public static final char SPACE = ' ';
65      public static final char HTAB = '\t';
66      public static final char EQUAL = '=';
67      public static final char ASTERISK = '*';
68      public static final char SEMICOLON = ';';
69      public static final char BACKSLASH = '\\';
70  
71      public static final String PARAM_CHARSET = "charset";
72      public static final String PARAM_CREATION_DATE = "creation-date";
73      public static final String PARAM_FILENAME = "filename";
74      public static final String PARAM_MODIFICATION_DATE = "modification-date";
75      public static final String PARAM_PADDING = "padding";
76      public static final String PARAM_READ_DATE = "read-date";
77      public static final String PARAM_SIZE = "size";
78      public static final String PARAM_TYPE = "type";
79  
80      public static final Set<String> ALL_PARAMS = new HashSet<>();
81  
82      static {
83          ALL_PARAMS.add(PARAM_CHARSET);
84          ALL_PARAMS.add(PARAM_CREATION_DATE);
85          ALL_PARAMS.add(PARAM_FILENAME);
86          ALL_PARAMS.add(PARAM_MODIFICATION_DATE);
87          ALL_PARAMS.add(PARAM_PADDING);
88          ALL_PARAMS.add(PARAM_READ_DATE);
89          ALL_PARAMS.add(PARAM_SIZE);
90          ALL_PARAMS.add(PARAM_TYPE);
91      }
92  
93      private AttachmentUtils() {
94          // complete
95      }
96  
97      public static void canonizeMimeHeaders(OutputStream os, Map<String, String> headers) throws IOException {
98          //5.4.1 MIME header canonicalization:
99  
100         //3. sorting
101         Map<String, String> sortedHeaders = new TreeMap<>();
102         Iterator<Map.Entry<String, String>> iterator = headers.entrySet().iterator();
103         while (iterator.hasNext()) {
104             Map.Entry<String, String> next = iterator.next();
105             String name = next.getKey();
106             String value = next.getValue();
107 
108             //2. only listed headers; 4. case
109             if (MIME_HEADER_CONTENT_DESCRIPTION.equalsIgnoreCase(name)) {
110                 sortedHeaders.put(MIME_HEADER_CONTENT_DESCRIPTION,
111                         //9. uncomment
112                         uncomment(
113                                 //6. decode
114                                 MimeUtility.decodeText(
115                                         //5. unfold
116                                         MimeUtility.unfold(value)
117                                 )
118                         )
119                 );
120             } else if (MIME_HEADER_CONTENT_DISPOSITION.equalsIgnoreCase(name)) {
121                 sortedHeaders.put(MIME_HEADER_CONTENT_DISPOSITION,
122                         decodeRfc2184(
123                                 //9. uncomment
124                                 uncomment(
125                                         //8. unfold ws
126                                         unfoldWhitespace(
127                                                 //5. unfold
128                                                 MimeUtility.unfold(value)
129                                         )
130                                 )
131                         )
132                 );
133             } else if (MIME_HEADER_CONTENT_ID.equalsIgnoreCase(name)) {
134                 sortedHeaders.put(MIME_HEADER_CONTENT_ID,
135                         //9. uncomment
136                         uncomment(
137                                 //8. unfold ws
138                                 unfoldWhitespace(
139                                         //5. unfold
140                                         MimeUtility.unfold(value)
141                                 )
142                         )
143                 );
144             } else if (MIME_HEADER_CONTENT_LOCATION.equalsIgnoreCase(name)) {
145                 sortedHeaders.put(MIME_HEADER_CONTENT_LOCATION,
146                         //9. uncomment
147                         uncomment(
148                                 //8. unfold ws
149                                 unfoldWhitespace(
150                                         //5. unfold
151                                         MimeUtility.unfold(value)
152                                 )
153                         )
154                 );
155             } else if (MIME_HEADER_CONTENT_TYPE.equalsIgnoreCase(name)) {
156                 sortedHeaders.put(MIME_HEADER_CONTENT_TYPE,
157                         decodeRfc2184(
158                                 //9. uncomment
159                                 uncomment(
160                                         //8. unfold ws
161                                         unfoldWhitespace(
162                                                 //5. unfold
163                                                 MimeUtility.unfold(value)
164                                         )
165                                 )
166                         )
167                 );
168             }
169         }
170         //2. default content-type
171         if (!sortedHeaders.containsKey(MIME_HEADER_CONTENT_TYPE)) {
172             sortedHeaders.put(MIME_HEADER_CONTENT_TYPE, "text/plain;charset=\"us-ascii\"");
173         }
174 
175         OutputStreamWriter outputStreamWriter = new OutputStreamWriter(os, StandardCharsets.UTF_8);
176 
177         Iterator<Map.Entry<String, String>> entryIterator = sortedHeaders.entrySet().iterator();
178         while (entryIterator.hasNext()) {
179             Map.Entry<String, String> next = entryIterator.next();
180             String name = next.getKey();
181             String value = next.getValue();
182 
183             //12.
184             outputStreamWriter.write(name);
185             outputStreamWriter.write(':');
186             outputStreamWriter.write(value);
187             //18. CRLF pair
188             if (!value.endsWith("\r\n")) {
189                 outputStreamWriter.write("\r\n");
190             }
191         }
192         outputStreamWriter.flush();
193     }
194 
195     public static String unfoldWhitespace(String text) {
196         int count = 0;
197         char[] chars = text.toCharArray();
198         for (char character : chars) {
199             if (SPACE != character && HTAB != character) {
200                 break;
201             }
202             count++;
203         }
204         return text.substring(count, chars.length);
205     }
206 
207     //removes any CRLF followed by a whitespace
208     public static String unfold(final String text) {
209 
210         int length = text.length();
211         if (length < 3) {
212             return text;
213         }
214 
215         StringBuilder stringBuilder = new StringBuilder();
216 
217         for (int i = 0; i < length - 2; i++) {
218             char ch1 = text.charAt(i);
219             final char ch2 = text.charAt(i + 1);
220             final char ch3 = text.charAt(i + 2);
221 
222             if (CARRIAGE_RETURN == ch1 && LINEFEED == ch2 && (SPACE == ch3 || HTAB == ch3)) {
223 
224                 i += 2;
225                 if (i >= length - 3) {
226                     for (i++; i < length; i++) { //NOPMD
227                         stringBuilder.append(text.charAt(i));
228                     }
229                 }
230                 continue;
231             }
232             stringBuilder.append(ch1);
233             if (i == length - 3) {
234                 stringBuilder.append(ch2);
235                 stringBuilder.append(ch3);
236             }
237         }
238         return stringBuilder.toString();
239     }
240 
241     public static String decodeRfc2184(String text) throws UnsupportedEncodingException {
242         if (!text.contains(";")) {
243             return text;
244         }
245 
246         String[] params = text.split(";");
247         //first part is the Mime-Header-Value
248         StringBuilder stringBuilder = new StringBuilder();
249         //10. lower case
250         stringBuilder.append(params[0].toLowerCase());
251 
252         TreeMap<String, String> paramMap = new TreeMap<>();
253 
254         String parameterName = null;
255         String parameterValue = null;
256         String charset = "us-ascii";
257         for (int i = 1; i < params.length; i++) {
258             String param = params[i];
259 
260             int index = param.indexOf(EQUAL);
261             String pName = param.substring(0, index).trim().toLowerCase();
262             String pValue = param.substring(index + 1).trim();
263 
264             int idx = pName.lastIndexOf(ASTERISK);
265             if (idx == pName.length() - 1) {
266                 //language encoded
267                 pName = pName.substring(0, pName.length() - 1);
268 
269                 int charsetIdx = pValue.indexOf(SINGLE_QUOTE);
270                 if (charsetIdx >= 0) {
271                     charset = pValue.substring(0, charsetIdx);
272                 }
273                 pValue = pValue.substring(pValue.lastIndexOf(SINGLE_QUOTE) + 1);
274                 pValue = URLDecoder.decode(pValue, MimeUtility.javaCharset(charset));
275             }
276             idx = pName.lastIndexOf(ASTERISK);
277             if (idx >= 0) {
278                 //continuation
279                 //int curr = Integer.parseInt(pName.substring(idx+1).trim());
280                 String pn = pName.substring(0, idx).trim();
281                 if (pn.equals(parameterName)) {
282                     parameterValue = concatParamValues(parameterValue, pValue);
283                 } else if (parameterName == null) {
284                     parameterName = pn;
285                     parameterValue = pValue;
286                 } else {
287                     if (ALL_PARAMS.contains(parameterName)) {
288                         parameterValue = parameterValue.toLowerCase();
289                     }
290                     paramMap.put(parameterName,
291                             unquoteInnerText(
292                                     quote(parameterValue)
293                             )
294                     );
295                 }
296             } else {
297                 if (parameterName != null) {
298                     if (ALL_PARAMS.contains(parameterName)) {
299                         parameterValue = parameterValue.toLowerCase();
300                     }
301                     paramMap.put(parameterName,
302                             unquoteInnerText(
303                                     quote(parameterValue)
304                             )
305                     );
306                     parameterName = null;
307                     parameterValue = null;
308                 }
309 
310                 if (ALL_PARAMS.contains(pName)) {
311                     pValue = pValue.toLowerCase();
312                 }
313                 paramMap.put(pName,
314                         unquoteInnerText(
315                                 quote(pValue)
316                         )
317                 );
318             }
319         }
320         if (parameterName != null) {
321             if (ALL_PARAMS.contains(parameterName)) {
322                 parameterValue = parameterValue.toLowerCase();
323             }
324             paramMap.put(parameterName,
325                     unquoteInnerText(
326                             quote(parameterValue)
327                     )
328             );
329         }
330 
331         Iterator<Map.Entry<String, String>> iterator = paramMap.entrySet().iterator();
332         while (iterator.hasNext()) {
333             Map.Entry<String, String> next = iterator.next();
334             stringBuilder.append(SEMICOLON);
335             stringBuilder.append(next.getKey());
336             stringBuilder.append(EQUAL);
337             stringBuilder.append(next.getValue());
338         }
339         return stringBuilder.toString();
340     }
341 
342     public static String concatParamValues(String a, String b) {
343         if (DOUBLE_QUOTE == a.charAt(a.length() - 1)) {
344             a = a.substring(0, a.length() - 1);
345         }
346         if (DOUBLE_QUOTE == b.charAt(0)) {
347             b = b.substring(1);
348         }
349         return a + b;
350     }
351 
352     public static String quote(String text) {
353         char startChar = text.charAt(0);
354         char endChar = text.charAt(text.length() - 1);
355         if (DOUBLE_QUOTE == startChar && DOUBLE_QUOTE == endChar) {
356             return text;
357         } else if (DOUBLE_QUOTE != startChar && DOUBLE_QUOTE != endChar) {
358             return DOUBLE_QUOTE + text + DOUBLE_QUOTE;
359         } else if (DOUBLE_QUOTE != startChar) {
360             return DOUBLE_QUOTE + text;
361         } else {
362             return text + DOUBLE_QUOTE;
363         }
364     }
365 
366     public static String unquoteInnerText(final String text) {
367         StringBuilder stringBuilder = new StringBuilder();
368         int length = text.length();
369         for (int i = 0; i < length - 1; i++) {
370             char c = text.charAt(i);
371             char c1 = text.charAt(i + 1);
372             if (i == 0 && DOUBLE_QUOTE == c) {
373                 stringBuilder.append(c);
374                 continue;
375             }
376             if (BACKSLASH == c && (DOUBLE_QUOTE == c1 || BACKSLASH == c1)) {
377                 if (i != 0 && i != length - 2) {
378                     stringBuilder.append(c);
379                 }
380                 stringBuilder.append(c1);
381                 i++;
382             } else if (DOUBLE_QUOTE == c) {
383                 stringBuilder.append(BACKSLASH);
384                 stringBuilder.append(c);
385             } else if (BACKSLASH == c) {
386                 stringBuilder.append(c1);
387                 i++;
388             } else {
389                 stringBuilder.append(c);
390                 if (i == length - 2 && DOUBLE_QUOTE == c1) {
391                     stringBuilder.append(c1);
392                 }
393             }
394         }
395         return stringBuilder.toString();
396     }
397 
398     /*
399      * Removes any comment outside quoted text. Comments are enclosed between ()
400      */
401     public static String uncomment(final String text) {
402         StringBuilder stringBuilder = new StringBuilder();
403 
404         int inComment = 0;
405         int length = text.length();
406         outer:
407         for (int i = 0; i < length; i++) {
408             char ch = text.charAt(i);
409 
410             if (DOUBLE_QUOTE == ch) {
411                 stringBuilder.append(ch);
412                 for (i++; i < length; i++) { //NOPMD
413                     ch = text.charAt(i);
414                     stringBuilder.append(ch);
415                     if (DOUBLE_QUOTE == ch) {
416                         continue outer;
417                     }
418                 }
419             }
420             if (LEFT_PARENTHESIS == ch) {
421                 inComment++;
422                 for (i++; i < length; i++) { //NOPMD
423                     ch = text.charAt(i);
424                     if (LEFT_PARENTHESIS == ch) {
425                         inComment++;
426                     }
427                     if (RIGHT_PARENTHESIS == ch) {
428                         inComment--;
429                         if (inComment == 0) {
430                             continue outer;
431                         }
432                     }
433                 }
434             }
435             stringBuilder.append(ch);
436         }
437         return stringBuilder.toString();
438     }
439 
440     public static void readAndReplaceEncryptedAttachmentHeaders(
441             Map<String, String> headers, InputStream attachmentInputStream) throws IOException, WSSecurityException {
442 
443         //read and replace headers
444         List<String> headerLines = new ArrayList<>();
445         StringBuilder stringBuilder = new StringBuilder();
446         boolean cr = false;
447         int ch;
448         int lineLength = 0;
449         while ((ch = attachmentInputStream.read()) != -1) {
450             if (ch == '\r') {
451                 cr = true;
452             } else if (ch == '\n' && cr) {
453                 cr = false;
454                 if (lineLength == 1 && stringBuilder.charAt(0) == '\r') {
455                     break;
456                 }
457                 if (headerLines.size() > 100) {
458                     //so much headers? go away....
459                     throw new WSSecurityException(
460                             WSSecurityException.ErrorCode.FAILED_CHECK);
461                 }
462                 headerLines.add(stringBuilder.substring(0, stringBuilder.length() - 1));
463                 lineLength = 0;
464                 stringBuilder.delete(0, stringBuilder.length());
465                 continue;
466             }
467             lineLength++;
468             //Lines in a message MUST be a maximum of 998 characters excluding the CRLF
469             if (lineLength >= 1000) {
470                 throw new WSSecurityException(
471                         WSSecurityException.ErrorCode.FAILED_CHECK);
472             }
473             stringBuilder.append((char) ch);
474         }
475 
476         for (String s : headerLines) {
477             int idx = s.indexOf(':');
478             if (idx == -1) {
479                 throw new WSSecurityException(
480                         WSSecurityException.ErrorCode.FAILED_CHECK);
481             }
482             headers.put(s.substring(0, idx), s.substring(idx + 1));
483         }
484     }
485 
486     public static InputStream setupAttachmentDecryptionStream(
487             final String encAlgo, final Cipher cipher, final Key key, InputStream inputStream)
488             throws WSSecurityException {
489 
490         CipherInputStream cipherInputStream = new CipherInputStream(inputStream, cipher) {
491 
492             private boolean firstRead = true;
493 
494             private void initCipher() throws IOException {
495                 int ivLen = JCEMapper.getIVLengthFromURI(encAlgo) / 8;
496                 byte[] ivBytes = new byte[ivLen];
497 
498                 int read = super.in.read(ivBytes, 0, ivLen);
499                 while (read != ivLen) {
500                     read += super.in.read(ivBytes, read, ivLen - read);
501                 }
502 
503                 AlgorithmParameterSpec paramSpec =
504                     XMLCipherUtil.constructBlockCipherParameters(encAlgo, ivBytes);
505 
506                 try {
507                     cipher.init(Cipher.DECRYPT_MODE, key, paramSpec);
508                 } catch (InvalidKeyException | InvalidAlgorithmParameterException e) {
509                     throw new IOException(e);
510                 }
511             }
512 
513             @Override
514             public int read() throws IOException {
515                 if (firstRead) {
516                     initCipher();
517                     firstRead = false;
518                 }
519                 return super.read();
520             }
521 
522             @Override
523             public int read(byte[] bytes) throws IOException {
524                 if (firstRead) {
525                     initCipher();
526                     firstRead = false;
527                 }
528                 return super.read(bytes);
529             }
530 
531             @Override
532             public int read(byte[] bytes, int i, int i2) throws IOException {
533                 if (firstRead) {
534                     initCipher();
535                     firstRead = false;
536                 }
537                 return super.read(bytes, i, i2);
538             }
539 
540             @Override
541             public long skip(long l) throws IOException {
542                 if (firstRead) {
543                     initCipher();
544                     firstRead = false;
545                 }
546                 return super.skip(l);
547             }
548 
549             @Override
550             public int available() throws IOException {
551                 if (firstRead) {
552                     initCipher();
553                     firstRead = false;
554                 }
555                 return super.available();
556             }
557         };
558 
559         return cipherInputStream;
560     }
561 
562     public static InputStream setupAttachmentEncryptionStream(
563             Cipher cipher, boolean complete, Attachment attachment,
564             Map<String, String> headers) throws WSSecurityException {
565 
566         final InputStream attachmentInputStream;    //NOPMD
567 
568         if (complete) {
569             try (ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream();
570                 OutputStreamWriter outputStreamWriter = new OutputStreamWriter(byteArrayOutputStream, StandardCharsets.US_ASCII)) {
571 
572                 Iterator<Map.Entry<String, String>> iterator = headers.entrySet().iterator();
573                 while (iterator.hasNext()) {
574                     Map.Entry<String, String> next = iterator.next();
575                     String key = next.getKey();
576                     String value = next.getValue();
577                     //5.5.2 Encryption Processing Rules
578                     //When encryption includes MIME headers, only the headers listed in this specification
579                     //for the Attachment-Complete-Signature-Transform (Section 5.3.2) are to be included in
580                     //the encryption. If a header listed in the profile is present it MUST be included in
581                     //the encryption. If a header is not listed in this profile, then it MUST NOT be
582                     //included in the encryption.
583                     if (AttachmentUtils.MIME_HEADER_CONTENT_DESCRIPTION.equals(key)
584                         || AttachmentUtils.MIME_HEADER_CONTENT_DISPOSITION.equals(key)
585                         || AttachmentUtils.MIME_HEADER_CONTENT_ID.equals(key)
586                         || AttachmentUtils.MIME_HEADER_CONTENT_LOCATION.equals(key)
587                         || AttachmentUtils.MIME_HEADER_CONTENT_TYPE.equals(key)) {
588                         iterator.remove();
589                         outputStreamWriter.write(key);
590                         outputStreamWriter.write(':');
591                         outputStreamWriter.write(value);
592                         outputStreamWriter.write("\r\n");
593                     }
594                 }
595                 outputStreamWriter.write("\r\n");
596                 outputStreamWriter.close();
597                 attachmentInputStream = new MultiInputStream(
598                         new ByteArrayInputStream(byteArrayOutputStream.toByteArray()),
599                         attachment.getSourceStream()
600                 );
601             } catch (IOException e) {
602                 throw new WSSecurityException(WSSecurityException.ErrorCode.FAILED_ENCRYPTION, e);
603             }
604         } else {
605             attachmentInputStream = attachment.getSourceStream();
606         }
607 
608         final ByteArrayInputStream ivInputStream = new ByteArrayInputStream(cipher.getIV());
609         final CipherInputStream cipherInputStream = new CipherInputStream(attachmentInputStream, cipher);   //NOPMD
610 
611         return new MultiInputStream(ivInputStream, cipherInputStream);
612     }
613 
614     public static byte[] getBytesFromAttachment(
615         String xopUri, CallbackHandler attachmentCallbackHandler, boolean removeAttachments
616     ) throws WSSecurityException {
617         if (attachmentCallbackHandler == null) {
618             throw new WSSecurityException(WSSecurityException.ErrorCode.FAILED_CHECK);
619         }
620 
621         String attachmentId = getAttachmentId(xopUri);
622 
623         AttachmentRequestCallback attachmentRequestCallback = new AttachmentRequestCallback();
624         attachmentRequestCallback.setAttachmentId(attachmentId);
625         attachmentRequestCallback.setRemoveAttachments(removeAttachments);
626 
627         try {
628             attachmentCallbackHandler.handle(new Callback[]{attachmentRequestCallback});
629 
630             List<Attachment> attachments = attachmentRequestCallback.getAttachments();
631             if (attachments == null || attachments.isEmpty()
632                 || !attachmentId.equals(attachments.get(0).getId())) {
633                 throw new WSSecurityException(
634                     WSSecurityException.ErrorCode.INVALID_SECURITY,
635                     "empty", new Object[] {"Attachment not found: " + xopUri}
636                 );
637             }
638             Attachment attachment = attachments.get(0);
639             try (InputStream inputStream = attachment.getSourceStream()) {
640                 return JavaUtils.getBytesFromStream(inputStream);
641             }
642         } catch (UnsupportedCallbackException | IOException e) {
643             throw new WSSecurityException(WSSecurityException.ErrorCode.FAILED_CHECK, e);
644         }
645     }
646 
647     public static String getAttachmentId(String xopUri) throws WSSecurityException {
648         try {
649             return URLDecoder.decode(xopUri.substring("cid:".length()), StandardCharsets.UTF_8.name());
650         } catch (UnsupportedEncodingException e) {
651             throw new WSSecurityException(
652                 WSSecurityException.ErrorCode.INVALID_SECURITY,
653                 "empty", new Object[] {"Attachment ID cannot be decoded: " + xopUri}
654             );
655         }
656     }
657 
658     public static void storeBytesInAttachment(
659         Element parentElement,
660         Document doc,
661         String attachmentId,
662         byte[] bytes,
663         CallbackHandler attachmentCallbackHandler
664     ) throws WSSecurityException {
665         parentElement.setAttributeNS(XMLUtils.XMLNS_NS, "xmlns:xop", WSS4JConstants.XOP_NS);
666         Element xopInclude =
667             doc.createElementNS(WSS4JConstants.XOP_NS, "xop:Include");
668         try {
669             xopInclude.setAttributeNS(null, "href", "cid:" + URLEncoder.encode(attachmentId, StandardCharsets.UTF_8.name()));
670         } catch (UnsupportedEncodingException e) {
671             throw new WSSecurityException(WSSecurityException.ErrorCode.FAILURE, e);
672         }
673         parentElement.appendChild(xopInclude);
674 
675         Attachment resultAttachment = new Attachment();
676         resultAttachment.setId(attachmentId);
677         resultAttachment.setMimeType("application/ciphervalue");
678         resultAttachment.setSourceStream(new ByteArrayInputStream(bytes));
679 
680         AttachmentResultCallback attachmentResultCallback = new AttachmentResultCallback();
681         attachmentResultCallback.setAttachmentId(attachmentId);
682         attachmentResultCallback.setAttachment(resultAttachment);
683         try {
684             attachmentCallbackHandler.handle(new Callback[]{attachmentResultCallback});
685         } catch (Exception e) {
686             throw new WSSecurityException(WSSecurityException.ErrorCode.FAILURE, e);
687         }
688 
689     }
690 }