diff --git a/CHANGELOG.md b/CHANGELOG.md index e1de24fb..7d7a7ae1 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -67,6 +67,9 @@ This patch release: algorithm name instead of the Java Security Standard Algorithm Name of [`RSASSA-PSS`](https://docs.oracle.com/en/java/javase/11/docs/specs/security/standard-names.html#signature-algorithms). This release ensures the standard name is used moving forward. + +* Fixes a backwards-compatibility [bug](https://github.com/jwtk/jjwt/issues/536) when parsing compressed JWTs + created from 0.10.6 or earlier using the `DEFLATE` compression algorithm. ### 0.10.7 diff --git a/impl/src/main/java/io/jsonwebtoken/impl/compression/DeflateCompressionCodec.java b/impl/src/main/java/io/jsonwebtoken/impl/compression/DeflateCompressionCodec.java index f31e6c0d..978ced22 100644 --- a/impl/src/main/java/io/jsonwebtoken/impl/compression/DeflateCompressionCodec.java +++ b/impl/src/main/java/io/jsonwebtoken/impl/compression/DeflateCompressionCodec.java @@ -15,11 +15,16 @@ */ package io.jsonwebtoken.impl.compression; +import io.jsonwebtoken.lang.Objects; + import java.io.ByteArrayInputStream; +import java.io.ByteArrayOutputStream; import java.io.IOException; +import java.io.InputStream; import java.io.OutputStream; import java.util.zip.DeflaterOutputStream; import java.util.zip.InflaterInputStream; +import java.util.zip.InflaterOutputStream; /** * Codec implementing the deflate compression algorithm. @@ -48,7 +53,40 @@ public class DeflateCompressionCodec extends AbstractCompressionCodec { } @Override - protected byte[] doDecompress(byte[] compressed) throws IOException { - return readAndClose(new InflaterInputStream(new ByteArrayInputStream(compressed))); + protected byte[] doDecompress(final byte[] compressed) throws IOException { + try { + return readAndClose(new InflaterInputStream(new ByteArrayInputStream(compressed))); + } catch (IOException e1) { + try { + return doDecompressBackCompat(compressed); + } catch (IOException e2) { + throw e1; //retain/report original exception + } + } + } + + /** + * This implementation was in 0.10.6 and earlier - it will be used as a fallback for backwards compatibility if + * {@link #readAndClose(InputStream)} fails per Issue 536. + * + * @param compressed the compressed byte array + * @return decompressed bytes + * @throws IOException if unable to decompress using the 0.10.6 and earlier logic + * @since 0.10.8 + */ + // package protected on purpose + byte[] doDecompressBackCompat(byte[] compressed) throws IOException { + InflaterOutputStream inflaterOutputStream = null; + ByteArrayOutputStream decompressedOutputStream = null; + + try { + decompressedOutputStream = new ByteArrayOutputStream(); + inflaterOutputStream = new InflaterOutputStream(decompressedOutputStream); + inflaterOutputStream.write(compressed); + inflaterOutputStream.flush(); + return decompressedOutputStream.toByteArray(); + } finally { + Objects.nullSafeClose(decompressedOutputStream, inflaterOutputStream); + } } } diff --git a/impl/src/test/groovy/io/jsonwebtoken/impl/compression/DeflateCompressionCodecTest.groovy b/impl/src/test/groovy/io/jsonwebtoken/impl/compression/DeflateCompressionCodecTest.groovy new file mode 100644 index 00000000..52f2b4e1 --- /dev/null +++ b/impl/src/test/groovy/io/jsonwebtoken/impl/compression/DeflateCompressionCodecTest.groovy @@ -0,0 +1,49 @@ +package io.jsonwebtoken.impl.compression + +import io.jsonwebtoken.CompressionException +import io.jsonwebtoken.Jwts +import io.jsonwebtoken.io.Decoders +import org.junit.Test + +import static org.junit.Assert.assertNotSame + +/** + * @since 0.10.8 + */ +class DeflateCompressionCodecTest { + + /** + * Test case for Issue 536. + */ + @Test + void testBackwardsCompatibility_0_10_6() { + final String jwtFrom0106 = 'eyJhbGciOiJub25lIiwiemlwIjoiREVGIn0.eNqqVsosLlayUspNVdJRKi5NAjJLi1OLgJzMxBIlK0sTMzMLEwsDAx2l1IoCJSsTQwMjExOQQC0AAAD__w.' + Jwts.parserBuilder().build().parseClaimsJwt(jwtFrom0106) // no exception should be thrown + } + + /** + * Test to ensure that, even if the backwards-compatibility fallback method throws an exception, that the first + * one is retained/re-thrown to reflect the correct/expected implementation. + */ + @Test + void testBackwardsCompatibilityRetainsFirstIOException() { + + final String compressedFrom0_10_6 = 'eNqqVsosLlayUspNVdJRKi5NAjJLi1OLgJzMxBIlK0sTMzMLEwsDAx2l1IoCJSsTQwMjExOQQC0AAAD__w' + byte[] invalid = Decoders.BASE64URL.decode(compressedFrom0_10_6) + + IOException unexpected = new IOException("foo") + + def codec = new DeflateCompressionCodec() { + @Override + byte[] doDecompressBackCompat(byte[] compressed) throws IOException { + throw unexpected + } + } + + try { + codec.decompress(invalid) + } catch (CompressionException ce) { + assertNotSame(unexpected, ce.getCause()) + } + } +}