Skip to content

Commit a3f3bf3

Browse files
authored
Added properties for base64 formatting (#504)
* Added properties for base64 formatting * updated XMLUtils in response to review comments * review: fixed access modifiers, added license and more comments in the test * added framework for formatting tests, refactored XMLUtilsTest * added FormattingTest annotation (JUnit tagging) * added FormattingChecker interface, various implementations for different formatting configurations and a factory to get appropriate implementation * added formatting options properties sets and multiple executions for Surefire plugin * refactored XMLUtilsTest: made it a @FormattingTest, removed hacks with classloader * added high-level formatting tests for signature and encryption * fixed grammar and codestyle in XMLUtilsTest.java * changed test class/method modifiers to pkg-private (PMD rule)
1 parent 6b1f608 commit a3f3bf3

23 files changed

Lines changed: 1399 additions & 25 deletions

pom.xml

Lines changed: 74 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -551,6 +551,80 @@
551551
<LANGUAGE>en_US:us</LANGUAGE>
552552
</environmentVariables>
553553
</configuration>
554+
<executions>
555+
<execution>
556+
<id>formatting-ignore-line-breaks</id>
557+
<goals>
558+
<goal>test</goal>
559+
</goals>
560+
<configuration>
561+
<groups>formattingTest</groups>
562+
<systemPropertiesFile>
563+
${project.build.testOutputDirectory}/formatting/ignore-line-breaks.properties
564+
</systemPropertiesFile>
565+
</configuration>
566+
</execution>
567+
<execution>
568+
<id>formatting-ignore-line-breaks-override</id>
569+
<goals>
570+
<goal>test</goal>
571+
</goals>
572+
<configuration>
573+
<groups>formattingTest</groups>
574+
<systemPropertiesFile>
575+
${project.build.testOutputDirectory}/formatting/ignore-line-breaks-override.properties
576+
</systemPropertiesFile>
577+
</configuration>
578+
</execution>
579+
<execution>
580+
<id>formatting-ignore-base64-line-breaks</id>
581+
<goals>
582+
<goal>test</goal>
583+
</goals>
584+
<configuration>
585+
<groups>formattingTest</groups>
586+
<systemPropertiesFile>
587+
${project.build.testOutputDirectory}/formatting/ignore-base64-line-breaks.properties
588+
</systemPropertiesFile>
589+
</configuration>
590+
</execution>
591+
<execution>
592+
<id>formatting-ignore-base64-line-breaks-override</id>
593+
<goals>
594+
<goal>test</goal>
595+
</goals>
596+
<configuration>
597+
<groups>formattingTest</groups>
598+
<systemPropertiesFile>
599+
${project.build.testOutputDirectory}/formatting/ignore-base64-line-breaks-override.properties
600+
</systemPropertiesFile>
601+
</configuration>
602+
</execution>
603+
<execution>
604+
<id>formatting-base64-custom-formatting</id>
605+
<goals>
606+
<goal>test</goal>
607+
</goals>
608+
<configuration>
609+
<groups>formattingTest</groups>
610+
<systemPropertiesFile>
611+
${project.build.testOutputDirectory}/formatting/base64-custom-formatting.properties
612+
</systemPropertiesFile>
613+
</configuration>
614+
</execution>
615+
<execution>
616+
<id>formatting-illegal</id>
617+
<goals>
618+
<goal>test</goal>
619+
</goals>
620+
<configuration>
621+
<groups>formattingTest</groups>
622+
<systemPropertiesFile>
623+
${project.build.testOutputDirectory}/formatting/illegal.properties
624+
</systemPropertiesFile>
625+
</configuration>
626+
</execution>
627+
</executions>
554628
</plugin>
555629
<plugin>
556630
<artifactId>maven-failsafe-plugin</artifactId>

src/main/java/org/apache/xml/security/signature/XMLSignature.java

Lines changed: 1 addition & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -684,11 +684,7 @@ private void setSignatureValueElement(byte[] bytes) {
684684
signatureValueElement.removeChild(signatureValueElement.getFirstChild());
685685
}
686686

687-
String base64codedValue = XMLUtils.encodeToString(bytes);
688-
689-
if (base64codedValue.length() > 76 && !XMLUtils.ignoreLineBreaks()) {
690-
base64codedValue = "\n" + base64codedValue + "\n";
691-
}
687+
String base64codedValue = XMLUtils.encodeElementValue(bytes);
692688

693689
Text t = createText(base64codedValue);
694690
signatureValueElement.appendChild(t);

src/main/java/org/apache/xml/security/stax/impl/processor/output/AbstractEncryptOutputProcessor.java

Lines changed: 1 addition & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -40,8 +40,6 @@
4040
import javax.xml.stream.XMLStreamConstants;
4141
import javax.xml.stream.XMLStreamException;
4242

43-
import org.apache.commons.codec.binary.Base64;
44-
import org.apache.commons.codec.binary.Base64OutputStream;
4543
import org.apache.xml.security.algorithms.JCEMapper;
4644
import org.apache.xml.security.encryption.XMLCipherUtil;
4745
import org.apache.xml.security.exceptions.XMLSecurityException;
@@ -176,14 +174,7 @@ public void init(OutputProcessorChain outputProcessorChain) throws XMLSecurityEx
176174
symmetricCipher.init(Cipher.ENCRYPT_MODE, encryptionPartDef.getSymmetricKey(), parameterSpec);
177175

178176
characterEventGeneratorOutputStream = new CharacterEventGeneratorOutputStream();
179-
Base64OutputStream.Builder builder = Base64OutputStream.builder()
180-
.setOutputStream(characterEventGeneratorOutputStream)
181-
.setEncode(true);
182-
if (XMLUtils.isIgnoreLineBreaks()) {
183-
builder.setBaseNCodec(Base64.builder().setLineLength(0).setLineSeparator(null).get());
184-
}
185-
186-
Base64OutputStream base64EncoderStream = builder.get(); //NOPMD
177+
OutputStream base64EncoderStream = XMLUtils.encodeStream(characterEventGeneratorOutputStream); //NOPMD
187178
base64EncoderStream.write(iv);
188179

189180
OutputStream outputStream = new CipherOutputStream(base64EncoderStream, symmetricCipher); //NOPMD

src/main/java/org/apache/xml/security/utils/ElementProxy.java

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -313,9 +313,7 @@ public void addTextElement(String text, String localname) {
313313
*/
314314
public void addBase64Text(byte[] bytes) {
315315
if (bytes != null) {
316-
Text t = XMLUtils.ignoreLineBreaks()
317-
? createText(XMLUtils.encodeToString(bytes))
318-
: createText("\n" + XMLUtils.encodeToString(bytes) + "\n");
316+
Text t = createText(XMLUtils.encodeElementValue(bytes));
319317
appendSelf(t);
320318
}
321319
}

src/main/java/org/apache/xml/security/utils/XMLUtils.java

Lines changed: 147 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -56,14 +56,38 @@
5656
/**
5757
* DOM and XML accessibility and comfort functions.
5858
*
59+
* @implNote
60+
* The following system properties affect XML formatting:
61+
* <ul>
62+
* <li>{@systemProperty org.apache.xml.security.ignoreLineBreaks} - ignores all line breaks,
63+
* making a single-line document. Overrides all other formatting options. Default: false</li>
64+
* <li>{@systemProperty org.apache.xml.security.base64.ignoreLineBreaks} - ignores line breaks in base64Binary values.
65+
* Takes precedence over line length and separator options (see below). Default: false</li>
66+
* <li>{@systemProperty org.apache.xml.security.base64.lineSeparator} - Sets the line separator sequence in base64Binary values.
67+
* Possible values: crlf, lf. Default: crlf</li>
68+
* <li>{@systemProperty org.apache.xml.security.base64.lineLength} - Sets maximum line length in base64Binary values.
69+
* The value is rounded down to the nearest multiple of 4. Values less than 4 are ignored. Default: 76</li>
70+
* </ul>
5971
*/
6072
public final class XMLUtils {
6173

74+
private static final Logger LOG = System.getLogger(XMLUtils.class.getName());
75+
76+
private static final String IGNORE_LINE_BREAKS_PROP = "org.apache.xml.security.ignoreLineBreaks";
77+
6278
private static boolean ignoreLineBreaks =
6379
AccessController.doPrivileged(
64-
(PrivilegedAction<Boolean>) () -> Boolean.getBoolean("org.apache.xml.security.ignoreLineBreaks"));
80+
(PrivilegedAction<Boolean>) () -> Boolean.getBoolean(IGNORE_LINE_BREAKS_PROP));
6581

66-
private static final Logger LOG = System.getLogger(XMLUtils.class.getName());
82+
private static Base64FormattingOptions base64Formatting =
83+
AccessController.doPrivileged(
84+
(PrivilegedAction<Base64FormattingOptions>) () -> new Base64FormattingOptions());
85+
86+
private static Base64.Encoder base64Encoder = (ignoreLineBreaks || base64Formatting.isIgnoreLineBreaks()) ?
87+
Base64.getEncoder() :
88+
Base64.getMimeEncoder(base64Formatting.getLineLength(), base64Formatting.getLineSeparator().getBytes());
89+
90+
private static Base64.Decoder base64Decoder = Base64.getMimeDecoder();
6791

6892
private static XMLParser xmlParserImpl =
6993
AccessController.doPrivileged(
@@ -515,18 +539,48 @@ public static void addReturnBeforeChild(Element e, Node child) {
515539
}
516540

517541
public static String encodeToString(byte[] bytes) {
518-
if (ignoreLineBreaks) {
519-
return Base64.getEncoder().encodeToString(bytes);
542+
return base64Encoder.encodeToString(bytes);
543+
}
544+
545+
/**
546+
* Encodes bytes using Base64, with or without line breaks, depending on configuration (see {@link XMLUtils}).
547+
* @param bytes Bytes to encode
548+
* @return Base64 string
549+
*/
550+
public static String encodeElementValue(byte[] bytes) {
551+
String encoded = encodeToString(bytes);
552+
if (!ignoreLineBreaks && !base64Formatting.isIgnoreLineBreaks()
553+
&& encoded.length() > base64Formatting.getLineLength()) {
554+
encoded = "\n" + encoded + "\n";
520555
}
521-
return Base64.getMimeEncoder().encodeToString(bytes);
556+
return encoded;
557+
}
558+
559+
/**
560+
* Wraps output stream for Base64 encoding.
561+
* Output data may contain line breaks or not, depending on configuration (see {@link XMLUtils})
562+
* @param stream The underlying output stream to write Base64-encoded data
563+
* @return Stream which writes binary data using Base64 encoder
564+
*/
565+
public static OutputStream encodeStream(OutputStream stream) {
566+
return base64Encoder.wrap(stream);
522567
}
523568

524569
public static byte[] decode(String encodedString) {
525-
return Base64.getMimeDecoder().decode(encodedString);
570+
return base64Decoder.decode(encodedString);
526571
}
527572

528573
public static byte[] decode(byte[] encodedBytes) {
529-
return Base64.getMimeDecoder().decode(encodedBytes);
574+
return base64Decoder.decode(encodedBytes);
575+
}
576+
577+
/**
578+
* Wraps input stream for Base64 decoding.
579+
* @param stream Input stream with Base64-encoded data
580+
* @return Input stream with decoded binary data
581+
*/
582+
public static InputStream decodeStream(InputStream stream) {
583+
return base64Decoder.wrap(stream);
530584
}
531585

532586
public static boolean isIgnoreLineBreaks() {
@@ -1068,4 +1122,90 @@ public static byte[] getBytes(BigInteger big, int bitlen) {
10681122

10691123
return resizedBytes;
10701124
}
1125+
1126+
/**
1127+
* Aggregates formatting options for base64Binary values.
1128+
*/
1129+
static class Base64FormattingOptions {
1130+
private static final String BASE64_IGNORE_LINE_BREAKS_PROP = "org.apache.xml.security.base64.ignoreLineBreaks";
1131+
private static final String BASE64_LINE_SEPARATOR_PROP = "org.apache.xml.security.base64.lineSeparator";
1132+
private static final String BASE64_LINE_LENGTH_PROP = "org.apache.xml.security.base64.lineLength";
1133+
1134+
private boolean ignoreLineBreaks = false;
1135+
private Base64LineSeparator lineSeparator = Base64LineSeparator.CRLF;
1136+
private int lineLength = 76;
1137+
1138+
/**
1139+
* Creates new formatting options by reading system properties.
1140+
*/
1141+
Base64FormattingOptions() {
1142+
String ignoreLineBreaksProp = System.getProperty(BASE64_IGNORE_LINE_BREAKS_PROP);
1143+
ignoreLineBreaks = Boolean.parseBoolean(ignoreLineBreaksProp);
1144+
if (XMLUtils.ignoreLineBreaks && ignoreLineBreaksProp != null && !ignoreLineBreaks) {
1145+
LOG.log(Level.WARNING, "{0} property takes precedence over {1}, line breaks will be ignored",
1146+
IGNORE_LINE_BREAKS_PROP, BASE64_IGNORE_LINE_BREAKS_PROP);
1147+
}
1148+
1149+
String lineSeparatorProp = System.getProperty(BASE64_LINE_SEPARATOR_PROP);
1150+
if (lineSeparatorProp != null) {
1151+
try {
1152+
lineSeparator = Base64LineSeparator.valueOf(lineSeparatorProp.toUpperCase());
1153+
if (XMLUtils.ignoreLineBreaks || ignoreLineBreaks) {
1154+
LOG.log(Level.WARNING, "Property {0} has no effect since line breaks are ignored",
1155+
BASE64_LINE_SEPARATOR_PROP);
1156+
}
1157+
} catch (IllegalArgumentException e) {
1158+
LOG.log(Level.WARNING, "Illegal value of {0} property is ignored: {1}",
1159+
BASE64_LINE_SEPARATOR_PROP, lineSeparatorProp);
1160+
}
1161+
}
1162+
1163+
String lineLengthProp = System.getProperty(BASE64_LINE_LENGTH_PROP);
1164+
if (lineLengthProp != null) {
1165+
try {
1166+
int lineLength = Integer.parseInt(lineLengthProp);
1167+
if (lineLength >= 4) {
1168+
this.lineLength = lineLength;
1169+
if (XMLUtils.ignoreLineBreaks || ignoreLineBreaks) {
1170+
LOG.log(Level.WARNING, "Property {0} has no effect since line breaks are ignored",
1171+
BASE64_LINE_LENGTH_PROP);
1172+
}
1173+
} else {
1174+
LOG.log(Level.WARNING, "Illegal value of {0} property is ignored: {1}",
1175+
BASE64_LINE_LENGTH_PROP, lineLengthProp);
1176+
}
1177+
} catch (NumberFormatException e) {
1178+
LOG.log(Level.WARNING, "Illegal value of {0} property is ignored: {1}",
1179+
BASE64_LINE_LENGTH_PROP, lineLengthProp);
1180+
}
1181+
}
1182+
}
1183+
1184+
public boolean isIgnoreLineBreaks() {
1185+
return ignoreLineBreaks;
1186+
}
1187+
1188+
public Base64LineSeparator getLineSeparator() {
1189+
return lineSeparator;
1190+
}
1191+
1192+
public int getLineLength() {
1193+
return lineLength;
1194+
}
1195+
}
1196+
1197+
enum Base64LineSeparator {
1198+
CRLF(new byte[]{'\r', '\n'}),
1199+
LF(new byte[]{'\n'});
1200+
1201+
private byte[] bytes;
1202+
1203+
Base64LineSeparator(byte[] bytes) {
1204+
this.bytes = bytes;
1205+
}
1206+
1207+
byte[] getBytes() {
1208+
return bytes;
1209+
}
1210+
}
10711211
}
Lines changed: 74 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,74 @@
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.xml.security.formatting;
20+
21+
import static org.junit.jupiter.api.Assertions.assertEquals;
22+
import static org.hamcrest.MatcherAssert.assertThat;
23+
import static org.hamcrest.Matchers.*;
24+
25+
/**
26+
* Checks that XML document is 'pretty-printed', including Base64 values.
27+
*/
28+
public class CustomBase64FormattingChecker implements FormattingChecker {
29+
private int lineLength;
30+
private String lineSeparatorRegex;
31+
32+
/**
33+
* Creates new checker.
34+
* @param lineLength Expected base64 maximum line length
35+
* @param lineSeparatorRegex Regex matching line separator used in Base64 values
36+
*/
37+
public CustomBase64FormattingChecker(int lineLength, String lineSeparatorRegex) {
38+
this.lineLength = lineLength;
39+
this.lineSeparatorRegex = lineSeparatorRegex;
40+
}
41+
42+
@Override
43+
public void checkDocument(String document) {
44+
assertThat(document, containsString("\n"));
45+
}
46+
47+
@Override
48+
public void checkBase64Value(String value) {
49+
String[] lines = value.split(lineSeparatorRegex);
50+
if (lines.length == 0) return;
51+
52+
for (int i = 0; i < lines.length - 1; ++i) {
53+
assertThat(lines[i], matchesPattern(BASE64_PATTERN));
54+
assertEquals(lineLength, lines[i].length());
55+
}
56+
57+
assertThat(lines[lines.length - 1], matchesPattern(BASE64_PATTERN));
58+
assertThat(lines[lines.length - 1].length(), lessThanOrEqualTo(lineLength));
59+
}
60+
61+
@Override
62+
public void checkBase64ValueWithSpacing(String value) {
63+
/* spacing is added only if the value has multiple lines */
64+
if (value.length() <= lineLength) {
65+
assertThat(value, matchesRegex(BASE64_PATTERN));
66+
return;
67+
}
68+
69+
assertThat(value.length(), greaterThanOrEqualTo(2));
70+
assertThat(value, startsWith("\n"));
71+
assertThat(value, endsWith("\n"));
72+
checkBase64Value(value.substring(1, value.length() - 1));
73+
}
74+
}

0 commit comments

Comments
 (0)