Skip to content

Commit

Permalink
Merge pull request #341 from jwtk/issue-333-base64
Browse files Browse the repository at this point in the history
Deterministic Base64 Behavior
  • Loading branch information
lhazlewood authored Jul 9, 2018
2 parents 130a841 + 6e1415c commit bae78f0
Show file tree
Hide file tree
Showing 53 changed files with 1,765 additions and 271 deletions.
4 changes: 4 additions & 0 deletions .travis.yml
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,10 @@ jdk:
- openjdk7
- oraclejdk8
- oraclejdk9
- oraclejdk10
- openjdk10
- oraclejdk-ea
- openjdk11

before_install:
- export BUILD_COVERAGE="$([ $TRAVIS_JDK_VERSION == 'oraclejdk8' ] && echo 'true')"
Expand Down
39 changes: 37 additions & 2 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,40 @@
## Release Notes

### 0.10.0

This is a minor feature enhancement that ensures Base64 encoding is identical and deterministic on all JDK and
Android platforms. JJWT previously relied on the underlying platform encoder (JAXB xml DataBinding or Android's native
Base64), however in some cases this produced unexpected results across platforms, including when compared against the
JDK's >= 8 Base64 implementation.

JJWT now embeds a super lightweight (1 class) and *extremely* fast RFC-compliant Base64 implementation to guarantee
portability across all supported platforms. It is now enabled automatically and there is nothing you need to
do to enable it.

However, if the default implementation isn't sufficient for your purposes, you may now specify your own
encoder during JWT building or decoder during JWT parsing as you see fit.

For example, an encoder during building:

```java
Encoder<byte[], String> encoder = new MyBase64UrlEncoder(); //implement me

Jwts.builder()
.base64UrlEncodeWith(encoder)
// ... etc ...
.compact();
```

Or a decoder during parsing:
```java
Decoder<String, byte[]> decoder = new MyBase64UrlDecoder(); //implement me

Jwts.parser()
.base64UrlDecodeWith(decoder)
// ... etc ...
.parseClaimsJws(jws);
```

### 0.9.1

This is a minor patch release that updates the Jackson dependency to 2.9.6 to address Jackson CVE-2017-17485.
Expand Down Expand Up @@ -136,7 +171,7 @@ Jwts.builder().claim("foo", "someReallyLongDataString...")
.compact();
```

This will set a new `calg` header with the name of the compression algorithm used so that parsers can see that value and decompress accordingly.
This will set a new `zip` header with the name of the compression algorithm used so that parsers can see that value and decompress accordingly.

The default parser implementation will automatically decompress DEFLATE or GZIP compressed bodies, so you don't need to set anything on the parser - it looks like normal:

Expand All @@ -155,7 +190,7 @@ Jwts.builder().claim("foo", "someReallyLongDataString...")
.compact();
```

You will then need to specify a `CompressionCodecResolver` on the parser, so you can inspect the `calg` header and return your custom codec when discovered:
You will then need to specify a `CompressionCodecResolver` on the parser, so you can inspect the `zip` header and return your custom codec when discovered:

```java
Jwts.parser().setSigningKey(key)
Expand Down
32 changes: 32 additions & 0 deletions NOTICE
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
The io.jsonwebtoken.codec.impl.Base64 implementation is based on MigBase64 with modifications for Base64 URL support. This
class's copyright and license notice have been retained and are repeated here per that code's requirements:

**** BEGIN MIGBASE64 NOTICE *****
Licence (BSD):
==============

Copyright (c) 2004, Mikael Grev, MiG InfoCom AB. (base64 @ miginfocom . com)
All rights reserved.

Redistribution and use in source and binary forms, with or without modification,
are permitted provided that the following conditions are met:
Redistributions of source code must retain the above copyright notice, this list
of conditions and the following disclaimer.
Redistributions in binary form must reproduce the above copyright notice, this
list of conditions and the following disclaimer in the documentation and/or other
materials provided with the distribution.
Neither the name of the MiG InfoCom AB nor the names of its contributors may be
used to endorse or promote products derived from this software without specific
prior written permission.

THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED.
IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT,
INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING,
BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA,
OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY,
WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY
OF SUCH DAMAGE.
**** END MIGBASE64 NOTICE *****
39 changes: 14 additions & 25 deletions pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -91,13 +91,13 @@
<bouncycastle.version>1.56</bouncycastle.version>

<!-- Test Dependencies: Only required for testing when building. Not required by users at runtime: -->
<groovy.version>2.4.11</groovy.version>
<groovy.version>2.4.15</groovy.version>
<logback.version>1.2.3</logback.version>
<easymock.version>3.5</easymock.version>
<junit.version>4.12</junit.version>
<powermock.version>2.0.0-beta.5</powermock.version> <!-- necessary for Java 9 support -->
<failsafe.plugin.version>2.20.1</failsafe.plugin.version>
<surefire.plugin.version>2.20.1</surefire.plugin.version>
<failsafe.plugin.version>2.22.0</failsafe.plugin.version>
<surefire.plugin.version>2.22.0</surefire.plugin.version>
<clover.version>4.2.0</clover.version>

</properties>
Expand Down Expand Up @@ -233,35 +233,24 @@
</plugin>
<!-- Allow for writing tests in Groovy: -->
<plugin>
<groupId>org.codehaus.gmaven</groupId>
<artifactId>gmaven-plugin</artifactId>
<version>1.5</version>
<configuration>
<providerSelection>2.0</providerSelection>
<source />
</configuration>
<groupId>org.codehaus.gmavenplus</groupId>
<artifactId>gmavenplus-plugin</artifactId>
<version>1.6.1</version>
<executions>
<execution>
<goals>
<goal>addSources</goal>
<goal>addTestSources</goal>
<goal>generateStubs</goal>
<goal>compile</goal>
<goal>generateTestStubs</goal>
<goal>testCompile</goal>
<goal>compileTests</goal>
<goal>removeStubs</goal>
<goal>removeTestStubs</goal>
</goals>
</execution>
</executions>
<dependencies>
<dependency>
<groupId>org.codehaus.gmaven.runtime</groupId>
<artifactId>gmaven-runtime-2.0</artifactId>
<version>1.5</version>
<exclusions>
<exclusion>
<groupId>org.codehaus.groovy</groupId>
<artifactId>groovy-all</artifactId>
</exclusion>
</exclusions>
</dependency>
<dependency>
<groupId>org.codehaus.groovy</groupId>
<artifactId>groovy-all</artifactId>
Expand Down Expand Up @@ -406,12 +395,12 @@
</build>
<profiles>
<profile>
<id>jdk8</id>
<id>nonJDK7</id>
<activation>
<jdk>1.8</jdk>
<jdk>[1.8,)</jdk>
</activation>
<properties>
<!-- Turn off JDK 8's lint checks: -->
<!-- Turn off JDK >= 8's lint checks: -->
<additionalparam>-Xdoclint:none</additionalparam>
</properties>
</profile>
Expand Down
4 changes: 2 additions & 2 deletions src/main/java/io/jsonwebtoken/CompressionCodec.java
Original file line number Diff line number Diff line change
Expand Up @@ -25,9 +25,9 @@
public interface CompressionCodec {

/**
* The algorithm name to use as the JWT's {@code calg} header value.
* The algorithm name to use as the JWT's {@code zip} header value.
*
* @return the algorithm name to use as the JWT's {@code calg} header value.
* @return the algorithm name to use as the JWT's {@code zip} header value.
*/
String getAlgorithmName();

Expand Down
8 changes: 4 additions & 4 deletions src/main/java/io/jsonwebtoken/CompressionCodecResolver.java
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@
package io.jsonwebtoken;

/**
* Looks for a JWT {@code calg} header, and if found, returns the corresponding {@link CompressionCodec} the parser
* Looks for a JWT {@code zip} header, and if found, returns the corresponding {@link CompressionCodec} the parser
* can use to decompress the JWT body.
*
* <p>JJWT's default {@link JwtParser} implementation supports both the
Expand All @@ -34,12 +34,12 @@
public interface CompressionCodecResolver {

/**
* Looks for a JWT {@code calg} header, and if found, returns the corresponding {@link CompressionCodec} the parser
* Looks for a JWT {@code zip} header, and if found, returns the corresponding {@link CompressionCodec} the parser
* can use to decompress the JWT body.
*
* @param header of the JWT
* @return CompressionCodec matching the {@code calg} header, or null if there is no {@code calg} header.
* @throws CompressionException if a {@code calg} header value is found and not supported.
* @return CompressionCodec matching the {@code zip} header, or null if there is no {@code zip} header.
* @throws CompressionException if a {@code zip} header value is found and not supported.
*/
CompressionCodec resolveCompressionCodec(Header header) throws CompressionException;

Expand Down
10 changes: 5 additions & 5 deletions src/main/java/io/jsonwebtoken/Header.java
Original file line number Diff line number Diff line change
Expand Up @@ -109,24 +109,24 @@ public interface Header<T extends Header<T>> extends Map<String,Object> {
T setContentType(String cty);

/**
* Returns the JWT <code>calg</code> (Compression Algorithm) header value or {@code null} if not present.
* Returns the JWT <code>zip</code> (Compression Algorithm) header value or {@code null} if not present.
*
* @return the {@code calg} header parameter value or {@code null} if not present.
* @return the {@code zip} header parameter value or {@code null} if not present.
* @since 0.6.0
*/
String getCompressionAlgorithm();

/**
* Sets the JWT <code>calg</code> (Compression Algorithm) header parameter value. A {@code null} value will remove
* Sets the JWT <code>zip</code> (Compression Algorithm) header parameter value. A {@code null} value will remove
* the property from the JSON map.
* <p>
* <p>The compression algorithm is NOT part of the <a href="https://tools.ietf.org/html/draft-ietf-oauth-json-web-token-25">JWT specification</a>
* and must be used carefully since, is not expected that other libraries (including previous versions of this one)
* be able to deserialize a compressed JTW body correctly. </p>
*
* @param calg the JWT compression algorithm {@code calg} value or {@code null} to remove the property from the JSON map.
* @param zip the JWT compression algorithm {@code zip} value or {@code null} to remove the property from the JSON map.
* @since 0.6.0
*/
T setCompressionAlgorithm(String calg);
T setCompressionAlgorithm(String zip);

}
39 changes: 39 additions & 0 deletions src/main/java/io/jsonwebtoken/JwtBuilder.java
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,8 @@
*/
package io.jsonwebtoken;

import io.jsonwebtoken.codec.Encoder;

import java.security.Key;
import java.util.Date;
import java.util.Map;
Expand Down Expand Up @@ -345,11 +347,36 @@ public interface JwtBuilder extends ClaimsMutator<JwtBuilder> {
* <p>This is a convenience method: the string argument is first BASE64-decoded to a byte array and this resulting
* byte array is used to invoke {@link #signWith(SignatureAlgorithm, byte[])}.</p>
*
* <h4>Deprecation Notice: Deprecated as of 0.10.0, will be removed in 1.0.0</h4>
*
* <p>This method has been deprecated because the {@code key} argument for this method can be confusing: keys for
* cryptographic operations are always binary (byte arrays), and many people were confused as to how bytes were
* obtained from the String argument.</p>
*
* <p>This method always expected a String argument that was effectively the same as the result of the following
* (pseudocode):</p>
*
* <p>{@code String base64EncodedSecretKey = base64Encode(secretKeyBytes);}</p>
*
* <p>However, a non-trivial number of JJWT users were confused by the method signature and attempted to
* use raw password strings as the key argument - for example {@code signWith(HS256, myPassword)} - which is
* almost always incorrect for cryptographic hashes and can produce erroneous or insecure results.</p>
*
* <p>See this
* <a href="https://stackoverflow.com/questions/40252903/static-secret-as-byte-key-or-string/40274325#40274325">
* StackOverflow answer</a> explaining why raw (non-base64-encoded) strings are almost always incorrect for
* signature operations.</p>
*
* <p>Finally, please use the {@link #signWith(SignatureAlgorithm, Key)} method, as this method and the
* {@code byte[]} variant will be removed before the 1.0.0 release.</p>
*
* @param alg the JWS algorithm to use to digitally sign the JWT, thereby producing a JWS.
* @param base64EncodedSecretKey the BASE64-encoded algorithm-specific signing key to use to digitally sign the
* JWT.
* @return the builder for method chaining.
* @deprecated as of 0.10.0 - use {@link #signWith(SignatureAlgorithm, Key)} instead.
*/
@Deprecated
JwtBuilder signWith(SignatureAlgorithm alg, String base64EncodedSecretKey);

/**
Expand Down Expand Up @@ -387,6 +414,18 @@ public interface JwtBuilder extends ClaimsMutator<JwtBuilder> {
*/
JwtBuilder compressWith(CompressionCodec codec);

/**
* Perform Base64Url encoding with the specified Encoder.
*
* <p>JJWT uses a spec-compliant encoder that works on all supported JDK versions, but you may call this method
* to specify a different encoder if you desire.</p>
*
* @param base64UrlEncoder the encoder to use when Base64Url-encoding
* @return the builder for method chaining.
* @since 0.10.0
*/
JwtBuilder base64UrlEncodeWith(Encoder<byte[], String> base64UrlEncoder);

/**
* Actually builds the JWT and serializes it to a compact, URL-safe string according to the
* <a href="https://tools.ietf.org/html/draft-ietf-oauth-json-web-token-25#section-7">JWT Compact Serialization</a>
Expand Down
46 changes: 41 additions & 5 deletions src/main/java/io/jsonwebtoken/JwtParser.java
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@
*/
package io.jsonwebtoken;

import io.jsonwebtoken.codec.Decoder;
import io.jsonwebtoken.impl.DefaultClock;

import java.security.Key;
Expand Down Expand Up @@ -164,20 +165,43 @@ public interface JwtParser {
/**
* Sets the signing key used to verify any discovered JWS digital signature. If the specified JWT string is not
* a JWS (no signature), this key is not used.
* <p>
*
* <p>Note that this key <em>MUST</em> be a valid key for the signature algorithm found in the JWT header
* (as the {@code alg} header parameter).</p>
* <p>
*
* <p>This method overwrites any previously set key.</p>
* <p>
*
* <p>This is a convenience method: the string argument is first BASE64-decoded to a byte array and this resulting
* byte array is used to invoke {@link #setSigningKey(byte[])}.</p>
*
* @param base64EncodedKeyBytes the BASE64-encoded algorithm-specific signature verification key to use to validate
* <h4>Deprecation Notice: Deprecated as of 0.10.0, will be removed in 1.0.0</h4>
*
* <p>This method has been deprecated because the {@code key} argument for this method can be confusing: keys for
* cryptographic operations are always binary (byte arrays), and many people were confused as to how bytes were
* obtained from the String argument.</p>
*
* <p>This method always expected a String argument that was effectively the same as the result of the following
* (pseudocode):</p>
*
* <p>{@code String base64EncodedSecretKey = base64Encode(secretKeyBytes);}</p>
*
* <p>However, a non-trivial number of JJWT users were confused by the method signature and attempted to
* use raw password strings as the key argument - for example {@code setSigningKey(myPassword)} - which is
* almost always incorrect for cryptographic hashes and can produce erroneous or insecure results.</p>
*
* <p>See this
* <a href="https://stackoverflow.com/questions/40252903/static-secret-as-byte-key-or-string/40274325#40274325">
* StackOverflow answer</a> explaining why raw (non-base64-encoded) strings are almost always incorrect for
* signature operations.</p>
*
* <p>Finally, please use the {@link #setSigningKey(Key) setSigningKey(Key)} instead, as this method and the
* {@code byte[]} variant will be removed before the 1.0.0 release.</p>
*
* @param base64EncodedSecretKey the BASE64-encoded algorithm-specific signature verification key to use to validate
* any discovered JWS digital signature.
* @return the parser for method chaining.
*/
JwtParser setSigningKey(String base64EncodedKeyBytes);
JwtParser setSigningKey(String base64EncodedSecretKey);

/**
* Sets the signing key used to verify any discovered JWS digital signature. If the specified JWT string is not
Expand Down Expand Up @@ -246,6 +270,18 @@ public interface JwtParser {
*/
JwtParser setCompressionCodecResolver(CompressionCodecResolver compressionCodecResolver);

/**
* Perform Base64Url decoding with the specified Decoder
*
* <p>JJWT uses a spec-compliant decoder that works on all supported JDK versions, but you may call this method
* to specify a different decoder if you desire.</p>
*
* @param base64UrlDecoder the decoder to use when Base64Url-decoding
* @return the parser for method chaining.
* @since 0.10.0
*/
JwtParser base64UrlDecodeWith(Decoder<String, byte[]> base64UrlDecoder);

/**
* Returns {@code true} if the specified JWT compact string represents a signed JWT (aka a 'JWS'), {@code false}
* otherwise.
Expand Down
Loading

0 comments on commit bae78f0

Please sign in to comment.