From c83fbe9f41d51975842b364072374826bddbfa00 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mikko=20Nyl=C3=A9n?= Date: Mon, 22 Apr 2024 08:16:20 +0300 Subject: [PATCH 1/4] Fix compression being omitted for JWEs with arbitrary content The JWE content was compressed only when claims was used as payload, but when using arbitrary content, the compression was omitted, while keeping the "zip" header field, leading to decompression failing. --- .../jsonwebtoken/impl/DefaultJwtBuilder.java | 7 +- .../groovy/io/jsonwebtoken/JwtsTest.groovy | 92 +++++++++++++++++++ 2 files changed, 95 insertions(+), 4 deletions(-) diff --git a/impl/src/main/java/io/jsonwebtoken/impl/DefaultJwtBuilder.java b/impl/src/main/java/io/jsonwebtoken/impl/DefaultJwtBuilder.java index ef41c7aec..101ae856a 100644 --- a/impl/src/main/java/io/jsonwebtoken/impl/DefaultJwtBuilder.java +++ b/impl/src/main/java/io/jsonwebtoken/impl/DefaultJwtBuilder.java @@ -693,14 +693,13 @@ private String encrypt(final Payload content, final Key key, final Provider keyP Assert.stateNotNull(keyAlgFunction, "KeyAlgorithm function cannot be null."); assertPayloadEncoding("JWE"); - InputStream plaintext; + ByteArrayOutputStream out = new ByteArrayOutputStream(4096); if (content.isClaims()) { - ByteArrayOutputStream out = new ByteArrayOutputStream(4096); writeAndClose("JWE Claims", content, out); - plaintext = Streams.of(out.toByteArray()); } else { - plaintext = content.toInputStream(); + writeAndClose("JWE Content", content, out); } + InputStream plaintext = Streams.of(out.toByteArray()); //only expose (mutable) JweHeader functionality to KeyAlgorithm instances, not the full headerBuilder // (which exposes this JwtBuilder and shouldn't be referenced by KeyAlgorithms): diff --git a/impl/src/test/groovy/io/jsonwebtoken/JwtsTest.groovy b/impl/src/test/groovy/io/jsonwebtoken/JwtsTest.groovy index 2f1c432c8..5f6b82229 100644 --- a/impl/src/test/groovy/io/jsonwebtoken/JwtsTest.groovy +++ b/impl/src/test/groovy/io/jsonwebtoken/JwtsTest.groovy @@ -22,6 +22,7 @@ import io.jsonwebtoken.impl.io.Streams import io.jsonwebtoken.impl.lang.Bytes import io.jsonwebtoken.impl.lang.Services import io.jsonwebtoken.impl.security.* +import io.jsonwebtoken.io.CompressionAlgorithm import io.jsonwebtoken.io.Decoders import io.jsonwebtoken.io.Deserializer import io.jsonwebtoken.io.Encoders @@ -1398,6 +1399,97 @@ class JwtsTest { } } + @Test + void testJweCompressionWithArbitraryContentString() { + def codecs = [Jwts.ZIP.DEF, Jwts.ZIP.GZIP] + + for (CompressionAlgorithm zip : codecs) { + + for (AeadAlgorithm enc : Jwts.ENC.get().values()) { + + SecretKey key = enc.key().build() + + String payload = 'hello, world!' + + // encrypt and compress: + String jwe = Jwts.builder() + .content(payload) + .compressWith(zip) + .encryptWith(key, enc) + .compact() + + //decompress and decrypt: + def jwt = Jwts.parser() + .decryptWith(key) + .build() + .parseEncryptedContent(jwe) + assertEquals payload, new String(jwt.getPayload(), StandardCharsets.UTF_8) + } + } + } + + @Test + void testJweCompressionWithArbitraryContentByteArray() { + def codecs = [Jwts.ZIP.DEF, Jwts.ZIP.GZIP] + + for (CompressionAlgorithm zip : codecs) { + + for (AeadAlgorithm enc : Jwts.ENC.get().values()) { + + SecretKey key = enc.key().build() + + byte[] payload = new byte[14]; + Randoms.secureRandom().nextBytes(payload) + + // encrypt and compress: + String jwe = Jwts.builder() + .content(payload) + .compressWith(zip) + .encryptWith(key, enc) + .compact() + + //decompress and decrypt: + def jwt = Jwts.parser() + .decryptWith(key) + .build() + .parseEncryptedContent(jwe) + assertArrayEquals payload, jwt.getPayload() + } + } + } + + @Test + void testJweCompressionWithArbitraryContentInputStream() { + def codecs = [Jwts.ZIP.DEF, Jwts.ZIP.GZIP] + + for (CompressionAlgorithm zip : codecs) { + + for (AeadAlgorithm enc : Jwts.ENC.get().values()) { + + SecretKey key = enc.key().build() + + byte[] payloadBytes = new byte[14]; + Randoms.secureRandom().nextBytes(payloadBytes) + + ByteArrayInputStream payload = new ByteArrayInputStream(payloadBytes) + + // encrypt and compress: + String jwe = Jwts.builder() + .content(payload) + .compressWith(zip) + .encryptWith(key, enc) + .compact() + + //decompress and decrypt: + def jwt = Jwts.parser() + .decryptWith(key) + .build() + .parseEncryptedContent(jwe) + assertArrayEquals payloadBytes, jwt.getPayload() + } + } + } + @Test void testPasswordJwes() { From 22b946fafcc158ed67913a35a6e501d146ccdc5f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mikko=20Nyl=C3=A9n?= Date: Tue, 23 Apr 2024 05:04:19 +0300 Subject: [PATCH 2/4] Refactor duplicate payload -> input stream logic from sign()/encrypt() --- .../jsonwebtoken/impl/DefaultJwtBuilder.java | 29 +++++++++---------- 1 file changed, 14 insertions(+), 15 deletions(-) diff --git a/impl/src/main/java/io/jsonwebtoken/impl/DefaultJwtBuilder.java b/impl/src/main/java/io/jsonwebtoken/impl/DefaultJwtBuilder.java index 101ae856a..b1ca7be4a 100644 --- a/impl/src/main/java/io/jsonwebtoken/impl/DefaultJwtBuilder.java +++ b/impl/src/main/java/io/jsonwebtoken/impl/DefaultJwtBuilder.java @@ -595,14 +595,8 @@ private String sign(final Payload payload, final Key key, final Provider provide // Next, b64 extension requires the raw (non-encoded) payload to be included directly in the signing input, // so we ensure we have an input stream for that: - if (payload.isClaims() || payload.isCompressed()) { - ByteArrayOutputStream claimsOut = new ByteArrayOutputStream(8192); - writeAndClose("JWS Unencoded Payload", payload, claimsOut); - payloadStream = Streams.of(claimsOut.toByteArray()); - } else { - // No claims and not compressed, so just get the direct InputStream: - payloadStream = Assert.stateNotNull(payload.toInputStream(), "Payload InputStream cannot be null."); - } + payloadStream = convertPayloadToInputStream(payload); + if (!payload.isClaims()) { payloadStream = new CountingInputStream(payloadStream); // we'll need to assert if it's empty later } @@ -693,13 +687,7 @@ private String encrypt(final Payload content, final Key key, final Provider keyP Assert.stateNotNull(keyAlgFunction, "KeyAlgorithm function cannot be null."); assertPayloadEncoding("JWE"); - ByteArrayOutputStream out = new ByteArrayOutputStream(4096); - if (content.isClaims()) { - writeAndClose("JWE Claims", content, out); - } else { - writeAndClose("JWE Content", content, out); - } - InputStream plaintext = Streams.of(out.toByteArray()); + InputStream plaintext = convertPayloadToInputStream(content); //only expose (mutable) JweHeader functionality to KeyAlgorithm instances, not the full headerBuilder // (which exposes this JwtBuilder and shouldn't be referenced by KeyAlgorithms): @@ -819,4 +807,15 @@ private void encodeAndWrite(String name, byte[] data, OutputStream out) { Streams.writeAndClose(out, data, "Unable to write bytes"); } + private InputStream convertPayloadToInputStream(Payload payload) { + if (payload.isClaims() || payload.isCompressed()) { + ByteArrayOutputStream claimsOut = new ByteArrayOutputStream(8192); + writeAndClose("JWS Unencoded Payload", payload, claimsOut); + return Streams.of(claimsOut.toByteArray()); + } else { + // No claims and not compressed, so just get the direct InputStream: + return Assert.stateNotNull(payload.toInputStream(), "Payload InputStream cannot be null."); + } + } + } From 9e946653244912a2fd2d6ebd7bb91886889a0f83 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mikko=20Nyl=C3=A9n?= Date: Wed, 24 Apr 2024 09:24:35 +0300 Subject: [PATCH 3/4] Preserve the content name --- .../main/java/io/jsonwebtoken/impl/DefaultJwtBuilder.java | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/impl/src/main/java/io/jsonwebtoken/impl/DefaultJwtBuilder.java b/impl/src/main/java/io/jsonwebtoken/impl/DefaultJwtBuilder.java index b1ca7be4a..a3e6d81e8 100644 --- a/impl/src/main/java/io/jsonwebtoken/impl/DefaultJwtBuilder.java +++ b/impl/src/main/java/io/jsonwebtoken/impl/DefaultJwtBuilder.java @@ -595,7 +595,7 @@ private String sign(final Payload payload, final Key key, final Provider provide // Next, b64 extension requires the raw (non-encoded) payload to be included directly in the signing input, // so we ensure we have an input stream for that: - payloadStream = convertPayloadToInputStream(payload); + payloadStream = toInputStream("JWS Unencoded Payload", payload); if (!payload.isClaims()) { payloadStream = new CountingInputStream(payloadStream); // we'll need to assert if it's empty later @@ -687,7 +687,7 @@ private String encrypt(final Payload content, final Key key, final Provider keyP Assert.stateNotNull(keyAlgFunction, "KeyAlgorithm function cannot be null."); assertPayloadEncoding("JWE"); - InputStream plaintext = convertPayloadToInputStream(content); + InputStream plaintext = toInputStream("JWE Unencoded Payload", content); //only expose (mutable) JweHeader functionality to KeyAlgorithm instances, not the full headerBuilder // (which exposes this JwtBuilder and shouldn't be referenced by KeyAlgorithms): @@ -807,10 +807,10 @@ private void encodeAndWrite(String name, byte[] data, OutputStream out) { Streams.writeAndClose(out, data, "Unable to write bytes"); } - private InputStream convertPayloadToInputStream(Payload payload) { + private InputStream toInputStream(final String name, Payload payload) { if (payload.isClaims() || payload.isCompressed()) { ByteArrayOutputStream claimsOut = new ByteArrayOutputStream(8192); - writeAndClose("JWS Unencoded Payload", payload, claimsOut); + writeAndClose(name, payload, claimsOut); return Streams.of(claimsOut.toByteArray()); } else { // No claims and not compressed, so just get the direct InputStream: From 6d88e342b62fa2d3c6fde527b1ade1c762ad307b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mikko=20Nyl=C3=A9n?= Date: Thu, 25 Apr 2024 05:41:29 +0300 Subject: [PATCH 4/4] Fix name for JWE payload --- impl/src/main/java/io/jsonwebtoken/impl/DefaultJwtBuilder.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/impl/src/main/java/io/jsonwebtoken/impl/DefaultJwtBuilder.java b/impl/src/main/java/io/jsonwebtoken/impl/DefaultJwtBuilder.java index a3e6d81e8..afa6a4804 100644 --- a/impl/src/main/java/io/jsonwebtoken/impl/DefaultJwtBuilder.java +++ b/impl/src/main/java/io/jsonwebtoken/impl/DefaultJwtBuilder.java @@ -687,7 +687,7 @@ private String encrypt(final Payload content, final Key key, final Provider keyP Assert.stateNotNull(keyAlgFunction, "KeyAlgorithm function cannot be null."); assertPayloadEncoding("JWE"); - InputStream plaintext = toInputStream("JWE Unencoded Payload", content); + InputStream plaintext = toInputStream("JWE Payload", content); //only expose (mutable) JweHeader functionality to KeyAlgorithm instances, not the full headerBuilder // (which exposes this JwtBuilder and shouldn't be referenced by KeyAlgorithms):