diff --git a/src/main/java/org/apache/xml/security/signature/XMLSignature.java b/src/main/java/org/apache/xml/security/signature/XMLSignature.java index 2387f8b24e..77f50dc726 100644 --- a/src/main/java/org/apache/xml/security/signature/XMLSignature.java +++ b/src/main/java/org/apache/xml/security/signature/XMLSignature.java @@ -661,11 +661,6 @@ private void setSignatureValueElement(byte[] bytes) { } String base64codedValue = XMLUtils.encodeToString(bytes); - - if (base64codedValue.length() > 76 && !XMLUtils.ignoreLineBreaks()) { - base64codedValue = "\n" + base64codedValue + "\n"; - } - Text t = createText(base64codedValue); signatureValueElement.appendChild(t); } diff --git a/src/main/java/org/apache/xml/security/stax/ext/AbstractOutputProcessor.java b/src/main/java/org/apache/xml/security/stax/ext/AbstractOutputProcessor.java index 4603d03a0c..9f9469faaa 100644 --- a/src/main/java/org/apache/xml/security/stax/ext/AbstractOutputProcessor.java +++ b/src/main/java/org/apache/xml/security/stax/ext/AbstractOutputProcessor.java @@ -37,6 +37,7 @@ import org.apache.xml.security.stax.ext.stax.XMLSecEventFactory; import org.apache.xml.security.stax.ext.stax.XMLSecNamespace; import org.apache.xml.security.stax.ext.stax.XMLSecStartElement; +import org.apache.xml.security.utils.XMLUtils; import org.w3c.dom.Attr; import org.w3c.dom.Element; import org.w3c.dom.NamedNodeMap; @@ -72,6 +73,7 @@ public void setAction(XMLSecurityConstants.Action action) { @Override public void init(OutputProcessorChain outputProcessorChain) throws XMLSecurityException { + XMLUtils.setThreadLocalBase64Parameters(securityProperties.getBase64LineLength(), securityProperties.getBase64LineSeparator()); outputProcessorChain.addProcessor(this); } diff --git a/src/main/java/org/apache/xml/security/stax/ext/XMLSecurityProperties.java b/src/main/java/org/apache/xml/security/stax/ext/XMLSecurityProperties.java index 8ee89f448d..e7f9c25f07 100644 --- a/src/main/java/org/apache/xml/security/stax/ext/XMLSecurityProperties.java +++ b/src/main/java/org/apache/xml/security/stax/ext/XMLSecurityProperties.java @@ -18,8 +18,6 @@ */ package org.apache.xml.security.stax.ext; -import org.apache.xml.security.stax.securityToken.SecurityTokenConstants; - import java.security.Key; import java.security.cert.X509Certificate; import java.security.spec.AlgorithmParameterSpec; @@ -27,6 +25,9 @@ import javax.xml.namespace.QName; +import org.apache.xml.security.stax.securityToken.SecurityTokenConstants; +import org.apache.xml.security.utils.XMLUtils; + /** * Main configuration class to supply keys etc. @@ -80,6 +81,8 @@ public class XMLSecurityProperties { private QName signaturePositionQName; private boolean signaturePositionStart = false; private AlgorithmParameterSpec algorithmParameterSpec; + private int base64LineLength = XMLUtils.DEFAULT_BASE64_LINE_LENGTH; + private byte[] base64LineSeparator = XMLUtils.DEFAULT_BASE64_LINE_SEPARATOR; public XMLSecurityProperties() { } @@ -120,6 +123,8 @@ protected XMLSecurityProperties(XMLSecurityProperties xmlSecurityProperties) { this.signaturePositionQName = xmlSecurityProperties.signaturePositionQName; this.signaturePositionStart = xmlSecurityProperties.signaturePositionStart; this.algorithmParameterSpec = xmlSecurityProperties.algorithmParameterSpec; + this.base64LineSeparator = xmlSecurityProperties.base64LineSeparator; + this.base64LineLength = xmlSecurityProperties.base64LineLength; } public boolean isSignaturePositionStart() { @@ -539,4 +544,44 @@ public AlgorithmParameterSpec getAlgorithmParameterSpec() { public void setAlgorithmParameterSpec(AlgorithmParameterSpec algorithmParameterSpec) { this.algorithmParameterSpec = algorithmParameterSpec; } + + /** + * @return the Base64 line separator, or {@code null} for no line separator. + */ + public byte[] getBase64LineSeparator() { + return base64LineSeparator; + } + + /** + * Sets the Base64 line separator to the given US-ASCII bytes. + * The default is CRLF. + * For no line separator, use the combination of {@code base64LineLength = 4} and + * {@code base64LineSeparator = null}. + * + * @param base64LineSeparator a Base64 line separator, or {@code null} for no line separator. + * @see #setBase64LineLength(int) + */ + public void setBase64LineSeparator(byte[] base64LineSeparator) { + this.base64LineSeparator = base64LineSeparator; + } + + /** + * @return the Base64 line length, possibly ≤ {@code 0} for no line separator. + */ + public int getBase64LineLength() { + return base64LineLength; + } + + /** + * Sets the Base64 line length, which must be a multiple of 4. + * The default is 76. + * For no line separator, use the combination of {@code base64LineLength = 4} and + * {@code base64LineSeparator = null}. + * + * @param base64LineLength a Base64 line length, or {@code 0} for no line separator. + * @see #setBase64LineSeparator(byte[]) + */ + public void setBase64LineLength(int base64LineLength) { + this.base64LineLength = base64LineLength; + } } diff --git a/src/main/java/org/apache/xml/security/stax/impl/processor/output/AbstractEncryptOutputProcessor.java b/src/main/java/org/apache/xml/security/stax/impl/processor/output/AbstractEncryptOutputProcessor.java index ad822a60c2..f06e5a40ce 100644 --- a/src/main/java/org/apache/xml/security/stax/impl/processor/output/AbstractEncryptOutputProcessor.java +++ b/src/main/java/org/apache/xml/security/stax/impl/processor/output/AbstractEncryptOutputProcessor.java @@ -174,12 +174,7 @@ public void init(OutputProcessorChain outputProcessorChain) throws XMLSecurityEx symmetricCipher.init(Cipher.ENCRYPT_MODE, encryptionPartDef.getSymmetricKey(), parameterSpec); characterEventGeneratorOutputStream = new CharacterEventGeneratorOutputStream(); - Base64OutputStream base64EncoderStream = null; - if (XMLUtils.isIgnoreLineBreaks()) { - base64EncoderStream = new Base64OutputStream(characterEventGeneratorOutputStream, true, 0, null); - } else { - base64EncoderStream = new Base64OutputStream(characterEventGeneratorOutputStream, true); - } + Base64OutputStream base64EncoderStream = XMLUtils.createBase64OutputStream(characterEventGeneratorOutputStream, true); base64EncoderStream.write(iv); OutputStream outputStream = new CipherOutputStream(base64EncoderStream, symmetricCipher); diff --git a/src/main/java/org/apache/xml/security/utils/ElementProxy.java b/src/main/java/org/apache/xml/security/utils/ElementProxy.java index 5b02f05bad..9112a4fff8 100644 --- a/src/main/java/org/apache/xml/security/utils/ElementProxy.java +++ b/src/main/java/org/apache/xml/security/utils/ElementProxy.java @@ -292,9 +292,6 @@ public void addBase64Element(byte[] bytes, String localname) { el.appendChild(text); appendSelf(el); - if (!XMLUtils.ignoreLineBreaks()) { - appendSelf(createText("\n")); - } } } @@ -320,9 +317,7 @@ public void addTextElement(String text, String localname) { */ public void addBase64Text(byte[] bytes) { if (bytes != null) { - Text t = XMLUtils.ignoreLineBreaks() - ? createText(XMLUtils.encodeToString(bytes)) - : createText("\n" + XMLUtils.encodeToString(bytes) + "\n"); + Text t = createText(XMLUtils.encodeToString(bytes)); appendSelf(t); } } diff --git a/src/main/java/org/apache/xml/security/utils/XMLUtils.java b/src/main/java/org/apache/xml/security/utils/XMLUtils.java index b6b4661fcb..9ec1393486 100644 --- a/src/main/java/org/apache/xml/security/utils/XMLUtils.java +++ b/src/main/java/org/apache/xml/security/utils/XMLUtils.java @@ -31,12 +31,14 @@ import java.util.List; import java.util.Set; +import org.apache.commons.codec.binary.Base64OutputStream; import org.apache.xml.security.c14n.CanonicalizationException; import org.apache.xml.security.c14n.Canonicalizer; import org.apache.xml.security.c14n.InvalidCanonicalizerException; import org.apache.xml.security.parser.XMLParser; import org.apache.xml.security.parser.XMLParserException; import org.apache.xml.security.parser.XMLParserImpl; +import org.apache.xml.security.stax.impl.util.KeyValue; import org.w3c.dom.Attr; import org.w3c.dom.Document; import org.w3c.dom.Element; @@ -77,6 +79,12 @@ public final class XMLUtils { private static volatile String xencPrefix = "xenc"; private static volatile String xenc11Prefix = "xenc11"; + public static final int DEFAULT_BASE64_LINE_LENGTH = 76; + public static final byte[] DEFAULT_BASE64_LINE_SEPARATOR = {13, 10}; + private static final KeyValue DEFAULT_BASE64_PARAMETERS = new KeyValue<>(DEFAULT_BASE64_LINE_LENGTH, DEFAULT_BASE64_LINE_SEPARATOR); + private static final KeyValue IGNORE_LINE_BREAKS_BASE64_PARAMETERS = new KeyValue<>(0, null); + private static final ThreadLocal> BASE64_PARAMETERS = ThreadLocal.withInitial(() -> DEFAULT_BASE64_PARAMETERS); + /** * Constructor XMLUtils * @@ -525,10 +533,20 @@ public static void addReturnBeforeChild(Element e, Node child) { } public static String encodeToString(byte[] bytes) { - if (ignoreLineBreaks) { + KeyValue base64Parameters = getEffectiveBase64Parameters(); + int lineLength = base64Parameters.getKey(); + byte[] lineSeparator = base64Parameters.getValue(); + if (lineSeparator == null) { return Base64.getEncoder().encodeToString(bytes); } - return Base64.getMimeEncoder().encodeToString(bytes); + return Base64.getMimeEncoder(lineLength, lineSeparator).encodeToString(bytes); + } + + private static KeyValue getEffectiveBase64Parameters() { + if (ignoreLineBreaks) { + return IGNORE_LINE_BREAKS_BASE64_PARAMETERS; + } + return BASE64_PARAMETERS.get(); } public static byte[] decode(String encodedString) { @@ -539,10 +557,24 @@ public static byte[] decode(byte[] encodedBytes) { return Base64.getMimeDecoder().decode(encodedBytes); } + @Deprecated public static boolean isIgnoreLineBreaks() { return ignoreLineBreaks; } + /** + * Creates a new Base64 output stream around a given output stream, using the thread-local Base64 parameters + * which were configured on this thread using {@link #setThreadLocalBase64Parameters(int, byte[])}. + * + * @param out The delegate output stream, which must not be {@code null}. + * @param doEncode {@code true} to encode, {@code false} to decode Base64. + * @return A new Base64 output stream, never {@code null}. + */ + public static Base64OutputStream createBase64OutputStream(OutputStream out, boolean doEncode) { + KeyValue base64Parameters = getEffectiveBase64Parameters(); + return new Base64OutputStream(out, true, base64Parameters.getKey(), base64Parameters.getValue()); + } + /** * Method convertNodelistToSet * @@ -854,6 +886,7 @@ public static boolean isDescendantOrSelf(Node ctx, Node descendantOrSelf) { } } + @Deprecated public static boolean ignoreLineBreaks() { return ignoreLineBreaks; } @@ -1050,6 +1083,16 @@ public static byte[] getBytes(BigInteger big, int bitlen) { return resizedBytes; } - - + /** + * Sets the thread-local Base64 parameters that will be used by this thread in further calls to + * {@link #encodeToString(byte[])} and {@link #createBase64OutputStream(OutputStream, boolean)}. + * Any thread-local parameters set here will be overruled (have no effect) when the system property + * {@code "org.apache.xml.security.ignoreLineBreaks"} is set. + * + * @param lineLength a line length, 76 by default. Use 0 for no line separator. + * @param lineSeparator a line separator, CRLF {@code {13, 10}} by default. Use {@code null} for no line separator. + */ + public static void setThreadLocalBase64Parameters(int lineLength, byte[] lineSeparator) { + BASE64_PARAMETERS.set(new KeyValue<>(lineLength, lineSeparator)); + } } diff --git a/src/test/java/org/apache/xml/security/test/stax/encryption/EncryptionCreationTest.java b/src/test/java/org/apache/xml/security/test/stax/encryption/EncryptionCreationTest.java index 995bce64f6..acf4429c21 100644 --- a/src/test/java/org/apache/xml/security/test/stax/encryption/EncryptionCreationTest.java +++ b/src/test/java/org/apache/xml/security/test/stax/encryption/EncryptionCreationTest.java @@ -30,8 +30,17 @@ import java.security.PublicKey; import java.security.cert.X509Certificate; import java.util.ArrayList; +import java.util.Arrays; import java.util.Collections; import java.util.List; +import java.util.Random; +import java.util.concurrent.CyclicBarrier; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; +import java.util.concurrent.Future; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.atomic.AtomicReference; +import java.util.regex.Pattern; import javax.crypto.KeyGenerator; import javax.crypto.SecretKey; @@ -44,6 +53,7 @@ import javax.xml.stream.XMLStreamReader; import javax.xml.stream.XMLStreamWriter; +import org.apache.commons.codec.binary.StringUtils; import org.apache.xml.security.encryption.XMLCipher; import org.apache.xml.security.exceptions.XMLSecurityException; import org.apache.xml.security.stax.ext.OutboundXMLSec; @@ -55,13 +65,20 @@ import org.apache.xml.security.test.stax.utils.XMLSecEventAllocator; import org.apache.xml.security.test.stax.utils.XmlReaderToWriter; import org.apache.xml.security.utils.XMLUtils; +import org.hamcrest.Description; +import org.hamcrest.Matcher; +import org.hamcrest.TypeSafeDiagnosingMatcher; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.Timeout; import org.w3c.dom.Attr; import org.w3c.dom.Document; import org.w3c.dom.Element; import org.w3c.dom.NodeList; +import static org.hamcrest.CoreMatchers.not; +import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.Matchers.containsString; import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertNull; import static org.junit.jupiter.api.Assertions.assertTrue; @@ -75,6 +92,10 @@ */ public class EncryptionCreationTest { + private static final byte[] LF = {10}; + private static final byte[] CRLF = {13, 10}; + private static final List LINE_SEPARATORS = Arrays.asList(CRLF, LF); + private XMLInputFactory xmlInputFactory; private boolean isIBMJdK = System.getProperty("java.vendor").contains("IBM"); @@ -1687,6 +1708,262 @@ public void testTransportKey() throws Exception { assertEquals(nodeList.getLength(), 1); } + @Test + public void testDefaultBase64LineSeparatorX509KeyIdentifier() throws Exception { + XMLSecurityProperties properties = new XMLSecurityProperties(); + properties.setEncryptionKeyIdentifier(SecurityTokenConstants.KeyIdentifier_X509KeyIdentifier); + String encrypted = encryptBase64LineSeparator(properties); + assertThat(encrypted, containsString("&#")); + } + + @Test + public void testBase64LineSeparatorX509KeyIdentifier() throws Exception { + XMLSecurityProperties properties = new XMLSecurityProperties(); + properties.setEncryptionKeyIdentifier(SecurityTokenConstants.KeyIdentifier_X509KeyIdentifier); + properties.setBase64LineSeparator(LF); + String encrypted = encryptBase64LineSeparator(properties); + assertThat(encrypted, not(containsString("&#"))); + } + + @Test + public void testBase64LineSeparatorSkiKeyIdentifier() throws Exception { + XMLSecurityProperties properties = new XMLSecurityProperties(); + properties.setEncryptionKeyIdentifier(SecurityTokenConstants.KeyIdentifier_SkiKeyIdentifier); + properties.setBase64LineSeparator(LF); + String encrypted = encryptBase64LineSeparator(properties); + assertThat(encrypted, not(containsString("&#"))); + } + + @Test + public void testBase64LineSeparatorKeyValue() throws Exception { + XMLSecurityProperties properties = new XMLSecurityProperties(); + properties.setEncryptionKeyIdentifier(SecurityTokenConstants.KeyIdentifier_KeyValue); + properties.setBase64LineSeparator(LF); + String encrypted = encryptBase64LineSeparator(properties); + assertThat(encrypted, not(containsString("&#"))); + } + + @Test + public void testBase64LineSeparatorNoKeyInfo() throws Exception { + XMLSecurityProperties properties = new XMLSecurityProperties(); + properties.setEncryptionKeyIdentifier(SecurityTokenConstants.KeyIdentifier_NoKeyInfo); + properties.setBase64LineSeparator(LF); + String encrypted = encryptBase64LineSeparator(properties); + assertThat(encrypted, not(containsString("&#"))); + } + + @Test + public void testBase64LineSeparatorKeyName() throws Exception { + XMLSecurityProperties properties = new XMLSecurityProperties(); + properties.setEncryptionKeyIdentifier(SecurityTokenConstants.KeyIdentifier_KeyName); + properties.setBase64LineSeparator(LF); + String encrypted = encryptBase64LineSeparator(properties); + assertThat(encrypted, not(containsString("&#"))); + } + + @Test + public void testBase64LineSeparatorIssuerSerial() throws Exception { + XMLSecurityProperties properties = new XMLSecurityProperties(); + properties.setEncryptionKeyIdentifier(SecurityTokenConstants.KeyIdentifier_IssuerSerial); + properties.setBase64LineSeparator(LF); + String encrypted = encryptBase64LineSeparator(properties); + assertThat(encrypted, not(containsString("&#"))); + } + + @Test + public void testBase64LineSeparatorX509SubjectName() throws Exception { + XMLSecurityProperties properties = new XMLSecurityProperties(); + properties.setEncryptionKeyIdentifier(SecurityTokenConstants.KeyIdentifier_X509SubjectName); + properties.setBase64LineSeparator(LF); + String encrypted = encryptBase64LineSeparator(properties); + assertThat(encrypted, not(containsString("&#"))); + } + + @Test + public void testBase64LineSeparatorEncryptedKey() throws Exception { + XMLSecurityProperties properties = new XMLSecurityProperties(); + properties.setEncryptionKeyIdentifier(SecurityTokenConstants.KeyIdentifier_EncryptedKey); + properties.setBase64LineSeparator(LF); + String encrypted = encryptBase64LineSeparator(properties); + assertThat(encrypted, not(containsString("&#"))); + } + + private String encryptBase64LineSeparator(XMLSecurityProperties properties) throws Exception { + // Set the configuration up. + properties.addAction(XMLSecurityConstants.ENCRYPT); + + // Set the key up. + KeyStore keyStore = KeyStore.getInstance("JCEKS"); + keyStore.load( + this.getClass().getClassLoader().getResource("test.jceks").openStream(), + "secret".toCharArray() + ); + PrivateKey privateKey = (PrivateKey)keyStore.getKey("rsakey", "secret".toCharArray()); + properties.setEncryptionKeyTransportAlgorithm("http://www.w3.org/2001/04/xmlenc#rsa-1_5"); + properties.setEncryptionTransportKey(privateKey); + properties.setEncryptionSymAlgorithm("http://www.w3.org/2001/04/xmlenc#aes256-cbc"); + + SecurePart securePart = + new SecurePart(new QName("urn:example:po", "PaymentInfo"), SecurePart.Modifier.Content); + properties.addEncryptionPart(securePart); + + OutboundXMLSec outboundXMLSec = XMLSec.getOutboundXMLSec(properties); + ByteArrayOutputStream baos = new ByteArrayOutputStream(); + XMLStreamWriter xmlStreamWriter = outboundXMLSec.processOutMessage(baos, StandardCharsets.UTF_8.name()); + + InputStream sourceDocument = + this.getClass().getClassLoader().getResourceAsStream( + "ie/baltimore/merlin-examples/merlin-xmlenc-five/plaintext.xml"); + XMLStreamReader xmlStreamReader = xmlInputFactory.createXMLStreamReader(sourceDocument); + + XmlReaderToWriter.writeAll(xmlStreamReader, xmlStreamWriter); + xmlStreamWriter.close(); + + // System.out.println("Got:\n" + new String(baos.toByteArray(), StandardCharsets.UTF_8.name())); + return new String(baos.toByteArray(), StandardCharsets.UTF_8); + } + + @Test + @Timeout(60L) + public void testThreadLocalityOfBase64Parameters() throws Exception { + // We want to create a sweet spot of concurrent _encrypting_ as much as possible. + // We take some measures to improve the size of the sweet spot: + // Measure 1: prepare common configuration for all threads, such that they spend as little time as possible + // doing irrelevant work such as generating symmetric keys. + KeyStore keyStore = KeyStore.getInstance("JCEKS"); + keyStore.load( + this.getClass().getClassLoader().getResource("test.jceks").openStream(), + "secret".toCharArray() + ); + PrivateKey privateKey = (PrivateKey) keyStore.getKey("rsakey", "secret".toCharArray()); + StringBuilder contentBuilder = new StringBuilder(1000); + contentBuilder.append("\n"); + int elementCount = 100; + for (int i = 0, n = elementCount; i != n; i++) { + contentBuilder.append("\t\n" + + "\t\tLorem ipsum dolor sit amet, \n" + + "\t\tconsectetur adipiscing elit, \n" + + "\t\tsed do eiusmod tempor incididunt ut labore et dolore magna aliqua. \n" + + "\t\tUt enim ad minim veniam, \n" + + "\t\tquis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. \n" + + "\t\tDuis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. \n" + + "\t\tExcepteur sint occaecat cupidatat non proident, \n" + + "\t\tsunt in culpa qui officia deserunt mollit anim id est laborum.\n" + + "\t\n"); + } + contentBuilder.append("\n"); + byte[] content = StringUtils.getBytesUtf8(contentBuilder.toString()); +// System.out.println(contentBuilder); + SecurePart securePart = + new SecurePart(new QName("ElementToEncrypt"), SecurePart.Modifier.Content); + KeyGenerator keyGenerator = KeyGenerator.getInstance("AES"); + keyGenerator.init(256); + SecretKey secretKey = keyGenerator.generateKey(); + + // Actual test with multiple concurrent encryption threads. + int threadCount = 10; + ExecutorService executor = Executors.newFixedThreadPool(threadCount); + AtomicReference firstExceptionReference = new AtomicReference<>(); + Random random = new Random(); + try { + List> futures = new ArrayList<>(threadCount); + // threadCount + 1 because this thread, which is not one of the executor threads, needs to reach the barrier too. + CyclicBarrier barrier = new CyclicBarrier(threadCount + 1); + // We want the test to fail if any exceptions happen on one of the threads. + // Capture the first of the off-thread exceptions and throw it at the end to make the test fail. + for (int i = 0, n = threadCount; i++ != n;) { + Runnable runnable = () -> { + try { + // Perform an actual test. + XMLSecurityProperties properties = new XMLSecurityProperties(); + properties.addAction(XMLSecurityConstants.ENCRYPT); + properties.setEncryptionKeyIdentifier(SecurityTokenConstants.KeyIdentifier_NoKeyInfo); + properties.setEncryptionKeyTransportAlgorithm("http://www.w3.org/2001/04/xmlenc#rsa-1_5"); + properties.setEncryptionTransportKey(privateKey); + properties.setEncryptionSymAlgorithm("http://www.w3.org/2001/04/xmlenc#aes256-cbc"); + // Use an explicit (common) key so that not all threads lose time generating one of their own. + properties.setEncryptionKey(secretKey); + properties.addEncryptionPart(securePart); + // Line length must be a multiple of 4, and must be > 0. + int lineLength = 4 * (random.nextInt(76 / 4 - 1) + 1); + byte[] lineSeparator = LINE_SEPARATORS.get(random.nextInt(LINE_SEPARATORS.size())); + properties.setBase64LineLength(lineLength); + properties.setBase64LineSeparator(lineSeparator); + OutboundXMLSec outboundXMLSec = XMLSec.getOutboundXMLSec(properties); + ByteArrayOutputStream out = new ByteArrayOutputStream(); + XMLStreamWriter xmlStreamWriter = outboundXMLSec.processOutMessage(out, StandardCharsets.UTF_8.name()); + InputStream sourceDocument = new ByteArrayInputStream(content); + XMLStreamReader xmlStreamReader = xmlInputFactory.createXMLStreamReader(sourceDocument); + Pattern pattern = Arrays.equals(lineSeparator, CRLF) + ? Pattern.compile("[a-zA-Z0-9+/]{" + lineLength + "} \\n", Pattern.DOTALL) + : Pattern.compile("[a-zA-Z0-9+/]{" + lineLength + "}\\n", Pattern.DOTALL); + // Use a barrier to align the start times of the threads as much as possible. + barrier.await(); + XmlReaderToWriter.writeAll(xmlStreamReader, xmlStreamWriter); + xmlStreamWriter.close(); + String encryptedContent = new String(out.toByteArray(), StandardCharsets.UTF_8); +// System.out.println("====="); +// System.out.println(encryptedContent); + assertThat(encryptedContent, not(containsString("Lorem ipsum dolor sit amet"))); + assertThat(encryptedContent, findRegex(pattern, true, elementCount)); + } catch (Exception e) { + firstExceptionReference.compareAndSet(null, e); + } + }; + Future future = executor.submit(runnable, null); + futures.add(future); + } + barrier.await(); + for (Future future : futures) { + future.get(); + } + } finally { + executor.shutdownNow(); + executor.awaitTermination(120L, TimeUnit.SECONDS); + } + Exception firstException = firstExceptionReference.get(); + if (firstException != null) { + throw firstException; + } + } + + private static boolean contains(byte[] array, byte b) { + for (byte a : array) { + if (a == b) { + return true; + } + } + return false; + } + + private static Matcher findRegex(Pattern pattern, boolean atLeast, int times) { + return new TypeSafeDiagnosingMatcher() { + + public void describeTo(Description description) { + description.appendText("a string matching the pattern ").appendValue(pattern) + .appendText(atLeast ? " at least " : " ").appendValue(times).appendText(" times"); + } + + protected boolean matchesSafely(String actual, Description mismatchDescription) { + java.util.regex.Matcher matcher = pattern.matcher(actual); + boolean matches = true; + for (int i = 0; i++ != times;) { + if (!matcher.find()) { + matches = false; + break; + } + } + if (matches && !atLeast) { + matches = !matcher.find(); + } + if (!matches) { + mismatchDescription.appendText("the string was ").appendValue(actual); + } + return matches; + } + }; + } + /** * Generate a secret key */ diff --git a/src/test/java/org/apache/xml/security/test/stax/signature/SignatureCreationTest.java b/src/test/java/org/apache/xml/security/test/stax/signature/SignatureCreationTest.java index 9f840b908b..5b2151242b 100644 --- a/src/test/java/org/apache/xml/security/test/stax/signature/SignatureCreationTest.java +++ b/src/test/java/org/apache/xml/security/test/stax/signature/SignatureCreationTest.java @@ -63,6 +63,9 @@ import static org.apache.xml.security.stax.ext.XMLSecurityConstants.NS_C14N_EXCL; import static org.apache.xml.security.stax.ext.XMLSecurityConstants.NS_XMLDSIG_ENVELOPED_SIGNATURE; +import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.Matchers.containsString; +import static org.hamcrest.Matchers.not; import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertNotNull; import static org.junit.jupiter.api.Assertions.assertTrue; @@ -1637,4 +1640,45 @@ public void testSignatureCreationWithoutOmittedDefaultTransform() throws Excepti // Verify using DOM verifyUsingDOMWithoutIdAndDefaultTransform(document, cert.getPublicKey(), properties.getSignatureSecureParts()); } + + @Test + public void testBas64LineSeparator() throws Exception { + byte[] lineSeparator = {10}; + // Set up the Configuration + XMLSecurityProperties properties = new XMLSecurityProperties(); + properties.setBase64LineSeparator(lineSeparator); + List actions = new ArrayList<>(); + actions.add(XMLSecurityConstants.SIGNATURE); + properties.setActions(actions); + + // Set the key up + KeyStore keyStore = KeyStore.getInstance("jks"); + keyStore.load( + this.getClass().getClassLoader().getResource("transmitter.jks").openStream(), + "default".toCharArray() + ); + Key key = keyStore.getKey("transmitter", "default".toCharArray()); + properties.setSignatureKey(key); + X509Certificate cert = (X509Certificate)keyStore.getCertificate("transmitter"); + properties.setSignatureCerts(new X509Certificate[]{cert}); + + SecurePart securePart = + new SecurePart(new QName("urn:example:po", "PaymentInfo"), SecurePart.Modifier.Content); + properties.addSignaturePart(securePart); + + OutboundXMLSec outboundXMLSec = XMLSec.getOutboundXMLSec(properties); + ByteArrayOutputStream baos = new ByteArrayOutputStream(); + XMLStreamWriter xmlStreamWriter = outboundXMLSec.processOutMessage(baos, StandardCharsets.UTF_8.name()); + + InputStream sourceDocument = + this.getClass().getClassLoader().getResourceAsStream( + "ie/baltimore/merlin-examples/merlin-xmlenc-five/plaintext.xml"); + XMLStreamReader xmlStreamReader = xmlInputFactory.createXMLStreamReader(sourceDocument); + + XmlReaderToWriter.writeAll(xmlStreamReader, xmlStreamWriter); + xmlStreamWriter.close(); + + String signed = new String(baos.toByteArray(), StandardCharsets.UTF_8); + assertThat(signed, not(containsString("&#"))); + } }