1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
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
95 }
96
97 public static void canonizeMimeHeaders(OutputStream os, Map<String, String> headers) throws IOException {
98
99
100
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
109 if (MIME_HEADER_CONTENT_DESCRIPTION.equalsIgnoreCase(name)) {
110 sortedHeaders.put(MIME_HEADER_CONTENT_DESCRIPTION,
111
112 uncomment(
113
114 MimeUtility.decodeText(
115
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
124 uncomment(
125
126 unfoldWhitespace(
127
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
136 uncomment(
137
138 unfoldWhitespace(
139
140 MimeUtility.unfold(value)
141 )
142 )
143 );
144 } else if (MIME_HEADER_CONTENT_LOCATION.equalsIgnoreCase(name)) {
145 sortedHeaders.put(MIME_HEADER_CONTENT_LOCATION,
146
147 uncomment(
148
149 unfoldWhitespace(
150
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
159 uncomment(
160
161 unfoldWhitespace(
162
163 MimeUtility.unfold(value)
164 )
165 )
166 )
167 );
168 }
169 }
170
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
184 outputStreamWriter.write(name);
185 outputStreamWriter.write(':');
186 outputStreamWriter.write(value);
187
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
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++) {
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
248 StringBuilder stringBuilder = new StringBuilder();
249
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
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
279
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
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++) {
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++) {
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
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
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
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;
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
578
579
580
581
582
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);
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 }