diff --git a/.travis.yml b/.travis.yml index 3f4905b9e..4a9845db9 100644 --- a/.travis.yml +++ b/.travis.yml @@ -7,6 +7,10 @@ jdk: - openjdk7 - oraclejdk8 - oraclejdk9 + - oraclejdk10 + - openjdk10 + - oraclejdk-ea + - openjdk11 before_install: - export BUILD_COVERAGE="$([ $TRAVIS_JDK_VERSION == 'oraclejdk8' ] && echo 'true')" diff --git a/CHANGELOG.md b/CHANGELOG.md index 5d41cb4d6..7c957ec04 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -136,7 +136,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: @@ -155,7 +155,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) diff --git a/NOTICE b/NOTICE new file mode 100644 index 000000000..02cf1046f --- /dev/null +++ b/NOTICE @@ -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 ***** \ No newline at end of file diff --git a/pom.xml b/pom.xml index e6f00267c..8c890a93b 100644 --- a/pom.xml +++ b/pom.xml @@ -91,13 +91,13 @@ 1.56 - 2.4.11 + 2.4.15 1.2.3 3.5 4.12 2.0.0-beta.5 - 2.20.1 - 2.20.1 + 2.22.0 + 2.22.0 4.2.0 @@ -233,35 +233,24 @@ - org.codehaus.gmaven - gmaven-plugin - 1.5 - - 2.0 - - + org.codehaus.gmavenplus + gmavenplus-plugin + 1.6.1 + addSources + addTestSources generateStubs compile generateTestStubs - testCompile + compileTests + removeStubs + removeTestStubs - - org.codehaus.gmaven.runtime - gmaven-runtime-2.0 - 1.5 - - - org.codehaus.groovy - groovy-all - - - org.codehaus.groovy groovy-all @@ -406,12 +395,12 @@ - jdk8 + nonJDK7 - 1.8 + [1.8,) - + -Xdoclint:none diff --git a/src/main/java/io/jsonwebtoken/CompressionCodec.java b/src/main/java/io/jsonwebtoken/CompressionCodec.java index b1b3dd6cc..b17153d3c 100644 --- a/src/main/java/io/jsonwebtoken/CompressionCodec.java +++ b/src/main/java/io/jsonwebtoken/CompressionCodec.java @@ -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(); diff --git a/src/main/java/io/jsonwebtoken/CompressionCodecResolver.java b/src/main/java/io/jsonwebtoken/CompressionCodecResolver.java index afb2e82a8..65dc7980c 100644 --- a/src/main/java/io/jsonwebtoken/CompressionCodecResolver.java +++ b/src/main/java/io/jsonwebtoken/CompressionCodecResolver.java @@ -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. * *

JJWT's default {@link JwtParser} implementation supports both the @@ -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; diff --git a/src/main/java/io/jsonwebtoken/Header.java b/src/main/java/io/jsonwebtoken/Header.java index 1e7687bca..589af0d55 100644 --- a/src/main/java/io/jsonwebtoken/Header.java +++ b/src/main/java/io/jsonwebtoken/Header.java @@ -109,24 +109,24 @@ public interface Header> extends Map { T setContentType(String cty); /** - * Returns the JWT calg (Compression Algorithm) header value or {@code null} if not present. + * Returns the JWT zip (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 calg (Compression Algorithm) header parameter value. A {@code null} value will remove + * Sets the JWT zip (Compression Algorithm) header parameter value. A {@code null} value will remove * the property from the JSON map. *

*

The compression algorithm is NOT part of the JWT specification * 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.

* - * @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); } diff --git a/src/main/java/io/jsonwebtoken/JwtBuilder.java b/src/main/java/io/jsonwebtoken/JwtBuilder.java index 5626218cc..bff3f1521 100644 --- a/src/main/java/io/jsonwebtoken/JwtBuilder.java +++ b/src/main/java/io/jsonwebtoken/JwtBuilder.java @@ -15,6 +15,8 @@ */ package io.jsonwebtoken; +import io.jsonwebtoken.codec.Encoder; + import java.security.Key; import java.util.Date; import java.util.Map; @@ -345,11 +347,36 @@ public interface JwtBuilder extends ClaimsMutator { *

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[])}.

* + *

Deprecation Notice: Deprecated as of 0.10.0, will be removed in 1.0.0

+ * + *

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.

+ * + *

This method always expected a String argument that was effectively the same as the result of the following + * (pseudocode):

+ * + *

{@code String base64EncodedSecretKey = base64Encode(secretKeyBytes);}

+ * + *

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.

+ * + *

See this + * + * StackOverflow answer explaining why raw (non-base64-encoded) strings are almost always incorrect for + * signature operations.

+ * + *

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.

+ * * @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); /** @@ -387,6 +414,18 @@ public interface JwtBuilder extends ClaimsMutator { */ JwtBuilder compressWith(CompressionCodec codec); + /** + * Perform Base64Url encoding with the specified Encoder. + * + *

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.

+ * + * @param base64UrlEncoder the encoder to use when Base64Url-encoding + * @return the builder for method chaining. + * @since 0.10.0 + */ + JwtBuilder base64UrlEncodeWith(Encoder base64UrlEncoder); + /** * Actually builds the JWT and serializes it to a compact, URL-safe string according to the * JWT Compact Serialization diff --git a/src/main/java/io/jsonwebtoken/JwtParser.java b/src/main/java/io/jsonwebtoken/JwtParser.java index 1dcdcf58f..b5283bb0e 100644 --- a/src/main/java/io/jsonwebtoken/JwtParser.java +++ b/src/main/java/io/jsonwebtoken/JwtParser.java @@ -15,6 +15,7 @@ */ package io.jsonwebtoken; +import io.jsonwebtoken.codec.Decoder; import io.jsonwebtoken.impl.DefaultClock; import java.security.Key; @@ -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. - *

+ * *

Note that this key MUST be a valid key for the signature algorithm found in the JWT header * (as the {@code alg} header parameter).

- *

+ * *

This method overwrites any previously set key.

- *

+ * *

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[])}.

* - * @param base64EncodedKeyBytes the BASE64-encoded algorithm-specific signature verification key to use to validate + *

Deprecation Notice: Deprecated as of 0.10.0, will be removed in 1.0.0

+ * + *

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.

+ * + *

This method always expected a String argument that was effectively the same as the result of the following + * (pseudocode):

+ * + *

{@code String base64EncodedSecretKey = base64Encode(secretKeyBytes);}

+ * + *

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.

+ * + *

See this + * + * StackOverflow answer explaining why raw (non-base64-encoded) strings are almost always incorrect for + * signature operations.

+ * + *

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.

+ * + * @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 @@ -246,6 +270,18 @@ public interface JwtParser { */ JwtParser setCompressionCodecResolver(CompressionCodecResolver compressionCodecResolver); + /** + * Perform Base64Url decoding with the specified Decoder + * + *

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.

+ * + * @param base64UrlDecoder the decoder to use when Base64Url-decoding + * @return the parser for method chaining. + * @since 0.10.0 + */ + JwtParser base64UrlDecodeWith(Decoder base64UrlDecoder); + /** * Returns {@code true} if the specified JWT compact string represents a signed JWT (aka a 'JWS'), {@code false} * otherwise. diff --git a/src/main/java/io/jsonwebtoken/codec/CodecException.java b/src/main/java/io/jsonwebtoken/codec/CodecException.java new file mode 100644 index 000000000..aa2743285 --- /dev/null +++ b/src/main/java/io/jsonwebtoken/codec/CodecException.java @@ -0,0 +1,13 @@ +package io.jsonwebtoken.codec; + +import io.jsonwebtoken.JwtException; + +/** + * @since 0.10.0 + */ +public class CodecException extends JwtException { + + public CodecException(String message, Throwable cause) { + super(message, cause); + } +} diff --git a/src/main/java/io/jsonwebtoken/codec/Decoder.java b/src/main/java/io/jsonwebtoken/codec/Decoder.java new file mode 100644 index 000000000..591ab5a18 --- /dev/null +++ b/src/main/java/io/jsonwebtoken/codec/Decoder.java @@ -0,0 +1,18 @@ +package io.jsonwebtoken.codec; + +import io.jsonwebtoken.codec.impl.Base64Decoder; +import io.jsonwebtoken.codec.impl.Base64UrlDecoder; +import io.jsonwebtoken.codec.impl.ExceptionPropagatingDecoder; + +/** + * @param + * @param + * @since 0.10.0 + */ +public interface Decoder { + + Decoder BASE64 = new ExceptionPropagatingDecoder<>(new Base64Decoder()); + Decoder BASE64URL = new ExceptionPropagatingDecoder<>(new Base64UrlDecoder()); + + R decode(T t) throws DecodingException; +} diff --git a/src/main/java/io/jsonwebtoken/codec/DecodingException.java b/src/main/java/io/jsonwebtoken/codec/DecodingException.java new file mode 100644 index 000000000..a3a8446e9 --- /dev/null +++ b/src/main/java/io/jsonwebtoken/codec/DecodingException.java @@ -0,0 +1,11 @@ +package io.jsonwebtoken.codec; + +/** + * @since 0.10.0 + */ +public class DecodingException extends CodecException { + + public DecodingException(String message, Throwable cause) { + super(message, cause); + } +} diff --git a/src/main/java/io/jsonwebtoken/codec/Encoder.java b/src/main/java/io/jsonwebtoken/codec/Encoder.java new file mode 100644 index 000000000..e6c070c4e --- /dev/null +++ b/src/main/java/io/jsonwebtoken/codec/Encoder.java @@ -0,0 +1,19 @@ +package io.jsonwebtoken.codec; + +import io.jsonwebtoken.codec.impl.Base64Encoder; +import io.jsonwebtoken.codec.impl.Base64UrlEncoder; +import io.jsonwebtoken.codec.impl.ExceptionPropagatingEncoder; + +/** + * @param + * @param + * @since 0.10.0 + */ +public interface Encoder { + + Encoder BASE64 = new ExceptionPropagatingEncoder<>(new Base64Encoder()); + Encoder BASE64URL = new ExceptionPropagatingEncoder<>(new Base64UrlEncoder()); + + R encode(T t) throws EncodingException; + +} diff --git a/src/main/java/io/jsonwebtoken/codec/EncodingException.java b/src/main/java/io/jsonwebtoken/codec/EncodingException.java new file mode 100644 index 000000000..d538d12b2 --- /dev/null +++ b/src/main/java/io/jsonwebtoken/codec/EncodingException.java @@ -0,0 +1,11 @@ +package io.jsonwebtoken.codec; + +/** + * @since 0.10.0 + */ +public class EncodingException extends CodecException { + + public EncodingException(String message, Throwable cause) { + super(message, cause); + } +} diff --git a/src/main/java/io/jsonwebtoken/codec/impl/Base64.java b/src/main/java/io/jsonwebtoken/codec/impl/Base64.java new file mode 100644 index 000000000..238b35f96 --- /dev/null +++ b/src/main/java/io/jsonwebtoken/codec/impl/Base64.java @@ -0,0 +1,649 @@ +package io.jsonwebtoken.codec.impl; + +import java.util.Arrays; + +/** + * A very fast and memory efficient class to encode and decode to and from BASE64 or BASE64URL in full accordance + * with RFC 4648. + * + *

Based initially on MigBase64 with continued modifications for Base64 URL support and JDK-standard code formatting.

+ * + *

This encode/decode algorithm doesn't create any temporary arrays as many other codecs do, it only + * allocates the resulting array. This produces less garbage and it is possible to handle arrays twice + * as large as algorithms that create a temporary array.

+ * + *

There is also a "fast" version of all decode methods that works the same way as the normal ones, but + * has a few demands on the decoded input. Normally though, these fast versions should be used if the source if + * the input is known and it hasn't bee tampered with.

+ * + * @author Mikael Grev + * @author Les Hazlewood + */ +@SuppressWarnings("Duplicates") +final class Base64 { //final and package-protected on purpose + + private static final char[] BASE64_ALPHABET = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/".toCharArray(); + private static final char[] BASE64URL_ALPHABET = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-_".toCharArray(); + private static final int[] BASE64_IALPHABET = new int[256]; + private static final int[] BASE64URL_IALPHABET = new int[256]; + + static { + Arrays.fill(BASE64_IALPHABET, -1); + System.arraycopy(BASE64_IALPHABET, 0, BASE64URL_IALPHABET, 0, BASE64_IALPHABET.length); + for (int i = 0, iS = BASE64_ALPHABET.length; i < iS; i++) { + BASE64_IALPHABET[BASE64_ALPHABET[i]] = i; + BASE64URL_IALPHABET[BASE64URL_ALPHABET[i]] = i; + } + BASE64_IALPHABET['='] = 0; + BASE64URL_IALPHABET['='] = 0; + } + + static final Base64 DEFAULT = new Base64(false); + static final Base64 URL_SAFE = new Base64(true); + + private final boolean urlsafe; + private final char[] ALPHABET; + private final int[] IALPHABET; + + private Base64(boolean urlsafe) { + this.urlsafe = urlsafe; + this.ALPHABET = urlsafe ? BASE64URL_ALPHABET : BASE64_ALPHABET; + this.IALPHABET = urlsafe ? BASE64URL_IALPHABET : BASE64_IALPHABET; + } + + // **************************************************************************************** + // * char[] version + // **************************************************************************************** + + /** + * Encodes a raw byte array into a BASE64 char[] representation in accordance with RFC 2045. + * + * @param sArr The bytes to convert. If null or length 0 an empty array will be returned. + * @param lineSep Optional "\r\n" after 76 characters, unless end of file.
+ * No line separator will be in breach of RFC 2045 which specifies max 76 per line but will be a + * little faster. + * @return A BASE64 encoded array. Never null. + */ + private char[] encodeToChar(byte[] sArr, boolean lineSep) { + + // Check special case + int sLen = sArr != null ? sArr.length : 0; + if (sLen == 0) { + return new char[0]; + } + + int eLen = (sLen / 3) * 3; // # of bytes that can encode evenly into 24-bit chunks + int left = sLen - eLen; // # of bytes that remain after 24-bit chunking. Always 0, 1 or 2 + + int cCnt = (((sLen - 1) / 3 + 1) << 2); // # of base64-encoded characters including padding + int dLen = cCnt + (lineSep ? (cCnt - 1) / 76 << 1 : 0); // Length of returned char array with padding and any line separators + + int padCount = 0; + if (left == 2) { + padCount = 1; + } else if (left == 1) { + padCount = 2; + } + + char[] dArr = new char[urlsafe ? (dLen - padCount) : dLen]; + + // Encode even 24-bits + for (int s = 0, d = 0, cc = 0; s < eLen; ) { + + // Copy next three bytes into lower 24 bits of int, paying attension to sign. + int i = (sArr[s++] & 0xff) << 16 | (sArr[s++] & 0xff) << 8 | (sArr[s++] & 0xff); + + // Encode the int into four chars + dArr[d++] = ALPHABET[(i >>> 18) & 0x3f]; + dArr[d++] = ALPHABET[(i >>> 12) & 0x3f]; + dArr[d++] = ALPHABET[(i >>> 6) & 0x3f]; + dArr[d++] = ALPHABET[i & 0x3f]; + + // Add optional line separator + if (lineSep && ++cc == 19 && d < dLen - 2) { + dArr[d++] = '\r'; + dArr[d++] = '\n'; + cc = 0; + } + } + + // Pad and encode last bits if source isn't even 24 bits. + if (left > 0) { + // Prepare the int + int i = ((sArr[eLen] & 0xff) << 10) | (left == 2 ? ((sArr[sLen - 1] & 0xff) << 2) : 0); + + // Set last four chars + dArr[dLen - 4] = ALPHABET[i >> 12]; + dArr[dLen - 3] = ALPHABET[(i >>> 6) & 0x3f]; + //dArr[dLen - 2] = left == 2 ? ALPHABET[i & 0x3f] : '='; + //dArr[dLen - 1] = '='; + if (left == 2) { + dArr[dLen - 2] = ALPHABET[i & 0x3f]; + } else if (!urlsafe) { // if not urlsafe, we need to include the padding characters + dArr[dLen - 2] = '='; + } + if (!urlsafe) { // include padding + dArr[dLen - 1] = '='; + } + } + return dArr; + } + + /* + * Decodes a BASE64 encoded char array. All illegal characters will be ignored and can handle both arrays with + * and without line separators. + * + * @param sArr The source array. null or length 0 will return an empty array. + * @return The decoded array of bytes. May be of length 0. Will be null if the legal characters + * (including '=') isn't divideable by 4. (I.e. definitely corrupted). + * + public final byte[] decode(char[] sArr) { + // Check special case + int sLen = sArr != null ? sArr.length : 0; + if (sLen == 0) { + return new byte[0]; + } + + // Count illegal characters (including '\r', '\n') to know what size the returned array will be, + // so we don't have to reallocate & copy it later. + int sepCnt = 0; // Number of separator characters. (Actually illegal characters, but that's a bonus...) + for (int i = 0; i < sLen; i++) { // If input is "pure" (I.e. no line separators or illegal chars) base64 this loop can be commented out. + if (IALPHABET[sArr[i]] < 0) { + sepCnt++; + } + } + + // Check so that legal chars (including '=') are evenly divideable by 4 as specified in RFC 2045. + if ((sLen - sepCnt) % 4 != 0) { + return null; + } + + int pad = 0; + for (int i = sLen; i > 1 && IALPHABET[sArr[--i]] <= 0; ) { + if (sArr[i] == '=') { + pad++; + } + } + + int len = ((sLen - sepCnt) * 6 >> 3) - pad; + + byte[] dArr = new byte[len]; // Preallocate byte[] of exact length + + for (int s = 0, d = 0; d < len; ) { + // Assemble three bytes into an int from four "valid" characters. + int i = 0; + for (int j = 0; j < 4; j++) { // j only increased if a valid char was found. + int c = IALPHABET[sArr[s++]]; + if (c >= 0) { + i |= c << (18 - j * 6); + } else { + j--; + } + } + // Add the bytes + dArr[d++] = (byte) (i >> 16); + if (d < len) { + dArr[d++] = (byte) (i >> 8); + if (d < len) { + dArr[d++] = (byte) i; + } + } + } + return dArr; + } + */ + + /** + * Decodes a BASE64 encoded char array that is known to be reasonably well formatted. The preconditions are:
+ * + The array must have a line length of 76 chars OR no line separators at all (one line).
+ * + Line separator must be "\r\n", as specified in RFC 2045 + * + The array must not contain illegal characters within the encoded string
+ * + The array CAN have illegal characters at the beginning and end, those will be dealt with appropriately.
+ * + * @param sArr The source array. Length 0 will return an empty array. null will throw an exception. + * @return The decoded array of bytes. May be of length 0. + */ + final byte[] decodeFast(char[] sArr) { + + // Check special case + int sLen = sArr != null ? sArr.length : 0; + if (sLen == 0) { + return new byte[0]; + } + + int sIx = 0, eIx = sLen - 1; // Start and end index after trimming. + + // Trim illegal chars from start + while (sIx < eIx && IALPHABET[sArr[sIx]] < 0) { + sIx++; + } + + // Trim illegal chars from end + while (eIx > 0 && IALPHABET[sArr[eIx]] < 0) { + eIx--; + } + + // get the padding count (=) (0, 1 or 2) + int pad = sArr[eIx] == '=' ? (sArr[eIx - 1] == '=' ? 2 : 1) : 0; // Count '=' at end. + int cCnt = eIx - sIx + 1; // Content count including possible separators + int sepCnt = sLen > 76 ? (sArr[76] == '\r' ? cCnt / 78 : 0) << 1 : 0; + + int len = ((cCnt - sepCnt) * 6 >> 3) - pad; // The number of decoded bytes + byte[] dArr = new byte[len]; // Preallocate byte[] of exact length + + // Decode all but the last 0 - 2 bytes. + int d = 0; + for (int cc = 0, eLen = (len / 3) * 3; d < eLen; ) { + + // Assemble three bytes into an int from four "valid" characters. + int i = IALPHABET[sArr[sIx++]] << 18 | IALPHABET[sArr[sIx++]] << 12 | IALPHABET[sArr[sIx++]] << 6 | IALPHABET[sArr[sIx++]]; + + // Add the bytes + dArr[d++] = (byte) (i >> 16); + dArr[d++] = (byte) (i >> 8); + dArr[d++] = (byte) i; + + // If line separator, jump over it. + if (sepCnt > 0 && ++cc == 19) { + sIx += 2; + cc = 0; + } + } + + if (d < len) { + // Decode last 1-3 bytes (incl '=') into 1-3 bytes + int i = 0; + for (int j = 0; sIx <= eIx - pad; j++) { + i |= IALPHABET[sArr[sIx++]] << (18 - j * 6); + } + + for (int r = 16; d < len; r -= 8) { + dArr[d++] = (byte) (i >> r); + } + } + + return dArr; + } + + // **************************************************************************************** + // * byte[] version + // **************************************************************************************** + + /* + * Encodes a raw byte array into a BASE64 byte[] representation i accordance with RFC 2045. + * + * @param sArr The bytes to convert. If null or length 0 an empty array will be returned. + * @param lineSep Optional "\r\n" after 76 characters, unless end of file.
+ * No line separator will be in breach of RFC 2045 which specifies max 76 per line but will be a + * little faster. + * @return A BASE64 encoded array. Never null. + * + public final byte[] encodeToByte(byte[] sArr, boolean lineSep) { + return encodeToByte(sArr, 0, sArr != null ? sArr.length : 0, lineSep); + } + + /** + * Encodes a raw byte array into a BASE64 byte[] representation i accordance with RFC 2045. + * + * @param sArr The bytes to convert. If null an empty array will be returned. + * @param sOff The starting position in the bytes to convert. + * @param sLen The number of bytes to convert. If 0 an empty array will be returned. + * @param lineSep Optional "\r\n" after 76 characters, unless end of file.
+ * No line separator will be in breach of RFC 2045 which specifies max 76 per line but will be a + * little faster. + * @return A BASE64 encoded array. Never null. + * + public final byte[] encodeToByte(byte[] sArr, int sOff, int sLen, boolean lineSep) { + + // Check special case + if (sArr == null || sLen == 0) { + return new byte[0]; + } + + int eLen = (sLen / 3) * 3; // Length of even 24-bits. + int cCnt = ((sLen - 1) / 3 + 1) << 2; // Returned character count + int dLen = cCnt + (lineSep ? (cCnt - 1) / 76 << 1 : 0); // Length of returned array + byte[] dArr = new byte[dLen]; + + // Encode even 24-bits + for (int s = sOff, d = 0, cc = 0; s < sOff + eLen; ) { + + // Copy next three bytes into lower 24 bits of int, paying attension to sign. + int i = (sArr[s++] & 0xff) << 16 | (sArr[s++] & 0xff) << 8 | (sArr[s++] & 0xff); + + // Encode the int into four chars + dArr[d++] = (byte) ALPHABET[(i >>> 18) & 0x3f]; + dArr[d++] = (byte) ALPHABET[(i >>> 12) & 0x3f]; + dArr[d++] = (byte) ALPHABET[(i >>> 6) & 0x3f]; + dArr[d++] = (byte) ALPHABET[i & 0x3f]; + + // Add optional line separator + if (lineSep && ++cc == 19 && d < dLen - 2) { + dArr[d++] = '\r'; + dArr[d++] = '\n'; + cc = 0; + } + } + + // Pad and encode last bits if source isn't an even 24 bits. + int left = sLen - eLen; // 0 - 2. + if (left > 0) { + // Prepare the int + int i = ((sArr[sOff + eLen] & 0xff) << 10) | (left == 2 ? ((sArr[sOff + sLen - 1] & 0xff) << 2) : 0); + + // Set last four chars + dArr[dLen - 4] = (byte) ALPHABET[i >> 12]; + dArr[dLen - 3] = (byte) ALPHABET[(i >>> 6) & 0x3f]; + dArr[dLen - 2] = left == 2 ? (byte) ALPHABET[i & 0x3f] : (byte) '='; + dArr[dLen - 1] = '='; + } + return dArr; + } + + /** + * Decodes a BASE64 encoded byte array. All illegal characters will be ignored and can handle both arrays with + * and without line separators. + * + * @param sArr The source array. Length 0 will return an empty array. null will throw an exception. + * @return The decoded array of bytes. May be of length 0. Will be null if the legal characters + * (including '=') isn't divideable by 4. (I.e. definitely corrupted). + * + public final byte[] decode(byte[] sArr) { + return decode(sArr, 0, sArr.length); + } + + /** + * Decodes a BASE64 encoded byte array. All illegal characters will be ignored and can handle both arrays with + * and without line separators. + * + * @param sArr The source array. null will throw an exception. + * @param sOff The starting position in the source array. + * @param sLen The number of bytes to decode from the source array. Length 0 will return an empty array. + * @return The decoded array of bytes. May be of length 0. Will be null if the legal characters + * (including '=') isn't divideable by 4. (I.e. definitely corrupted). + * + public final byte[] decode(byte[] sArr, int sOff, int sLen) { + + // Count illegal characters (including '\r', '\n') to know what size the returned array will be, + // so we don't have to reallocate & copy it later. + int sepCnt = 0; // Number of separator characters. (Actually illegal characters, but that's a bonus...) + for (int i = 0; i < sLen; i++) { // If input is "pure" (I.e. no line separators or illegal chars) base64 this loop can be commented out. + if (IALPHABET[sArr[sOff + i] & 0xff] < 0) { + sepCnt++; + } + } + + // Check so that legal chars (including '=') are evenly divideable by 4 as specified in RFC 2045. + if ((sLen - sepCnt) % 4 != 0) { + return null; + } + + int pad = 0; + for (int i = sLen; i > 1 && IALPHABET[sArr[sOff + --i] & 0xff] <= 0; ) { + if (sArr[sOff + i] == '=') { + pad++; + } + } + + int len = ((sLen - sepCnt) * 6 >> 3) - pad; + + byte[] dArr = new byte[len]; // Preallocate byte[] of exact length + + for (int s = 0, d = 0; d < len; ) { + // Assemble three bytes into an int from four "valid" characters. + int i = 0; + for (int j = 0; j < 4; j++) { // j only increased if a valid char was found. + int c = IALPHABET[sArr[sOff + s++] & 0xff]; + if (c >= 0) { + i |= c << (18 - j * 6); + } else { + j--; + } + } + + // Add the bytes + dArr[d++] = (byte) (i >> 16); + if (d < len) { + dArr[d++] = (byte) (i >> 8); + if (d < len) { + dArr[d++] = (byte) i; + } + } + } + + return dArr; + } + + + /* + * Decodes a BASE64 encoded byte array that is known to be resonably well formatted. The method is about twice as + * fast as {@link #decode(byte[])}. The preconditions are:
+ * + The array must have a line length of 76 chars OR no line separators at all (one line).
+ * + Line separator must be "\r\n", as specified in RFC 2045 + * + The array must not contain illegal characters within the encoded string
+ * + The array CAN have illegal characters at the beginning and end, those will be dealt with appropriately.
+ * + * @param sArr The source array. Length 0 will return an empty array. null will throw an exception. + * @return The decoded array of bytes. May be of length 0. + * + public final byte[] decodeFast(byte[] sArr) { + + // Check special case + int sLen = sArr.length; + if (sLen == 0) { + return new byte[0]; + } + + int sIx = 0, eIx = sLen - 1; // Start and end index after trimming. + + // Trim illegal chars from start + while (sIx < eIx && IALPHABET[sArr[sIx] & 0xff] < 0) { + sIx++; + } + + // Trim illegal chars from end + while (eIx > 0 && IALPHABET[sArr[eIx] & 0xff] < 0) { + eIx--; + } + + // get the padding count (=) (0, 1 or 2) + int pad = sArr[eIx] == '=' ? (sArr[eIx - 1] == '=' ? 2 : 1) : 0; // Count '=' at end. + int cCnt = eIx - sIx + 1; // Content count including possible separators + int sepCnt = sLen > 76 ? (sArr[76] == '\r' ? cCnt / 78 : 0) << 1 : 0; + + int len = ((cCnt - sepCnt) * 6 >> 3) - pad; // The number of decoded bytes + byte[] dArr = new byte[len]; // Preallocate byte[] of exact length + + // Decode all but the last 0 - 2 bytes. + int d = 0; + for (int cc = 0, eLen = (len / 3) * 3; d < eLen; ) { + + // Assemble three bytes into an int from four "valid" characters. + int i = IALPHABET[sArr[sIx++]] << 18 | IALPHABET[sArr[sIx++]] << 12 | IALPHABET[sArr[sIx++]] << 6 | IALPHABET[sArr[sIx++]]; + + // Add the bytes + dArr[d++] = (byte) (i >> 16); + dArr[d++] = (byte) (i >> 8); + dArr[d++] = (byte) i; + + // If line separator, jump over it. + if (sepCnt > 0 && ++cc == 19) { + sIx += 2; + cc = 0; + } + } + + if (d < len) { + // Decode last 1-3 bytes (incl '=') into 1-3 bytes + int i = 0; + for (int j = 0; sIx <= eIx - pad; j++) { + i |= IALPHABET[sArr[sIx++]] << (18 - j * 6); + } + + for (int r = 16; d < len; r -= 8) { + dArr[d++] = (byte) (i >> r); + } + } + + return dArr; + } + */ + + // **************************************************************************************** + // * String version + // **************************************************************************************** + + /** + * Encodes a raw byte array into a BASE64 String representation i accordance with RFC 2045. + * + * @param sArr The bytes to convert. If null or length 0 an empty array will be returned. + * @param lineSep Optional "\r\n" after 76 characters, unless end of file.
+ * No line separator will be in breach of RFC 2045 which specifies max 76 per line but will be a + * little faster. + * @return A BASE64 encoded array. Never null. + */ + final String encodeToString(byte[] sArr, boolean lineSep) { + // Reuse char[] since we can't create a String incrementally anyway and StringBuffer/Builder would be slower. + return new String(encodeToChar(sArr, lineSep)); + } + + /* + * Decodes a BASE64 encoded String. All illegal characters will be ignored and can handle both strings with + * and without line separators.
+ * Note! It can be up to about 2x the speed to call decode(str.toCharArray()) instead. That + * will create a temporary array though. This version will use str.charAt(i) to iterate the string. + * + * @param str The source string. null or length 0 will return an empty array. + * @return The decoded array of bytes. May be of length 0. Will be null if the legal characters + * (including '=') isn't divideable by 4. (I.e. definitely corrupted). + * + public final byte[] decode(String str) { + + // Check special case + int sLen = str != null ? str.length() : 0; + if (sLen == 0) { + return new byte[0]; + } + + // Count illegal characters (including '\r', '\n') to know what size the returned array will be, + // so we don't have to reallocate & copy it later. + int sepCnt = 0; // Number of separator characters. (Actually illegal characters, but that's a bonus...) + for (int i = 0; i < sLen; i++) { // If input is "pure" (I.e. no line separators or illegal chars) base64 this loop can be commented out. + if (IALPHABET[str.charAt(i)] < 0) { + sepCnt++; + } + } + + // Check so that legal chars (including '=') are evenly divideable by 4 as specified in RFC 2045. + if ((sLen - sepCnt) % 4 != 0) { + return null; + } + + // Count '=' at end + int pad = 0; + for (int i = sLen; i > 1 && IALPHABET[str.charAt(--i)] <= 0; ) { + if (str.charAt(i) == '=') { + pad++; + } + } + + int len = ((sLen - sepCnt) * 6 >> 3) - pad; + + byte[] dArr = new byte[len]; // Preallocate byte[] of exact length + + for (int s = 0, d = 0; d < len; ) { + // Assemble three bytes into an int from four "valid" characters. + int i = 0; + for (int j = 0; j < 4; j++) { // j only increased if a valid char was found. + int c = IALPHABET[str.charAt(s++)]; + if (c >= 0) { + i |= c << (18 - j * 6); + } else { + j--; + } + } + // Add the bytes + dArr[d++] = (byte) (i >> 16); + if (d < len) { + dArr[d++] = (byte) (i >> 8); + if (d < len) { + dArr[d++] = (byte) i; + } + } + } + return dArr; + } + + /** + * Decodes a BASE64 encoded string that is known to be resonably well formatted. The method is about twice as + * fast as {@link #decode(String)}. The preconditions are:
+ * + The array must have a line length of 76 chars OR no line separators at all (one line).
+ * + Line separator must be "\r\n", as specified in RFC 2045 + * + The array must not contain illegal characters within the encoded string
+ * + The array CAN have illegal characters at the beginning and end, those will be dealt with appropriately.
+ * + * @param s The source string. Length 0 will return an empty array. null will throw an exception. + * @return The decoded array of bytes. May be of length 0. + * + public final byte[] decodeFast(String s) { + + // Check special case + int sLen = s.length(); + if (sLen == 0) { + return new byte[0]; + } + + int sIx = 0, eIx = sLen - 1; // Start and end index after trimming. + + // Trim illegal chars from start + while (sIx < eIx && IALPHABET[s.charAt(sIx) & 0xff] < 0) { + sIx++; + } + + // Trim illegal chars from end + while (eIx > 0 && IALPHABET[s.charAt(eIx) & 0xff] < 0) { + eIx--; + } + + // get the padding count (=) (0, 1 or 2) + int pad = s.charAt(eIx) == '=' ? (s.charAt(eIx - 1) == '=' ? 2 : 1) : 0; // Count '=' at end. + int cCnt = eIx - sIx + 1; // Content count including possible separators + int sepCnt = sLen > 76 ? (s.charAt(76) == '\r' ? cCnt / 78 : 0) << 1 : 0; + + int len = ((cCnt - sepCnt) * 6 >> 3) - pad; // The number of decoded bytes + byte[] dArr = new byte[len]; // Preallocate byte[] of exact length + + // Decode all but the last 0 - 2 bytes. + int d = 0; + for (int cc = 0, eLen = (len / 3) * 3; d < eLen; ) { + // Assemble three bytes into an int from four "valid" characters. + int i = IALPHABET[s.charAt(sIx++)] << 18 | IALPHABET[s.charAt(sIx++)] << 12 | IALPHABET[s.charAt(sIx++)] << 6 | IALPHABET[s.charAt(sIx++)]; + + // Add the bytes + dArr[d++] = (byte) (i >> 16); + dArr[d++] = (byte) (i >> 8); + dArr[d++] = (byte) i; + + // If line separator, jump over it. + if (sepCnt > 0 && ++cc == 19) { + sIx += 2; + cc = 0; + } + } + + if (d < len) { + // Decode last 1-3 bytes (incl '=') into 1-3 bytes + int i = 0; + for (int j = 0; sIx <= eIx - pad; j++) { + i |= IALPHABET[s.charAt(sIx++)] << (18 - j * 6); + } + + for (int r = 16; d < len; r -= 8) { + dArr[d++] = (byte) (i >> r); + } + } + + return dArr; + } + */ +} diff --git a/src/main/java/io/jsonwebtoken/codec/impl/Base64Decoder.java b/src/main/java/io/jsonwebtoken/codec/impl/Base64Decoder.java new file mode 100644 index 000000000..66175d5c4 --- /dev/null +++ b/src/main/java/io/jsonwebtoken/codec/impl/Base64Decoder.java @@ -0,0 +1,22 @@ +package io.jsonwebtoken.codec.impl; + +import io.jsonwebtoken.codec.Decoder; +import io.jsonwebtoken.codec.DecodingException; +import io.jsonwebtoken.lang.Assert; + +public class Base64Decoder extends Base64Support implements Decoder { + + public Base64Decoder() { + super(Base64.DEFAULT); + } + + Base64Decoder(Base64 base64) { + super(base64); + } + + @Override + public byte[] decode(String s) throws DecodingException { + Assert.notNull(s, "String argument cannot be null"); + return this.base64.decodeFast(s.toCharArray()); + } +} \ No newline at end of file diff --git a/src/main/java/io/jsonwebtoken/codec/impl/Base64Encoder.java b/src/main/java/io/jsonwebtoken/codec/impl/Base64Encoder.java new file mode 100644 index 000000000..f6ff16767 --- /dev/null +++ b/src/main/java/io/jsonwebtoken/codec/impl/Base64Encoder.java @@ -0,0 +1,22 @@ +package io.jsonwebtoken.codec.impl; + +import io.jsonwebtoken.codec.Encoder; +import io.jsonwebtoken.codec.EncodingException; +import io.jsonwebtoken.lang.Assert; + +public class Base64Encoder extends Base64Support implements Encoder { + + public Base64Encoder() { + super(Base64.DEFAULT); + } + + Base64Encoder(Base64 base64) { + super(base64); + } + + @Override + public String encode(byte[] bytes) throws EncodingException { + Assert.notNull(bytes, "byte array argument cannot be null"); + return this.base64.encodeToString(bytes, false); + } +} diff --git a/src/main/java/io/jsonwebtoken/codec/impl/Base64Support.java b/src/main/java/io/jsonwebtoken/codec/impl/Base64Support.java new file mode 100644 index 000000000..77bdd044f --- /dev/null +++ b/src/main/java/io/jsonwebtoken/codec/impl/Base64Support.java @@ -0,0 +1,13 @@ +package io.jsonwebtoken.codec.impl; + +import io.jsonwebtoken.lang.Assert; + +public class Base64Support { + + protected final Base64 base64; + + Base64Support(Base64 base64) { + Assert.notNull(base64, "Base64 argument cannot be null"); + this.base64 = base64; + } +} diff --git a/src/main/java/io/jsonwebtoken/codec/impl/Base64UrlDecoder.java b/src/main/java/io/jsonwebtoken/codec/impl/Base64UrlDecoder.java new file mode 100644 index 000000000..1a3f70421 --- /dev/null +++ b/src/main/java/io/jsonwebtoken/codec/impl/Base64UrlDecoder.java @@ -0,0 +1,11 @@ +package io.jsonwebtoken.codec.impl; + +/** + * @since 0.10.0 + */ +public class Base64UrlDecoder extends Base64Decoder { + + public Base64UrlDecoder() { + super(Base64.URL_SAFE); + } +} diff --git a/src/main/java/io/jsonwebtoken/codec/impl/Base64UrlEncoder.java b/src/main/java/io/jsonwebtoken/codec/impl/Base64UrlEncoder.java new file mode 100644 index 000000000..1859cc7ed --- /dev/null +++ b/src/main/java/io/jsonwebtoken/codec/impl/Base64UrlEncoder.java @@ -0,0 +1,11 @@ +package io.jsonwebtoken.codec.impl; + +/** + * @since 0.10.0 + */ +public class Base64UrlEncoder extends Base64Encoder { + + public Base64UrlEncoder() { + super(Base64.URL_SAFE); + } +} diff --git a/src/main/java/io/jsonwebtoken/codec/impl/ExceptionPropagatingDecoder.java b/src/main/java/io/jsonwebtoken/codec/impl/ExceptionPropagatingDecoder.java new file mode 100644 index 000000000..1e186e00b --- /dev/null +++ b/src/main/java/io/jsonwebtoken/codec/impl/ExceptionPropagatingDecoder.java @@ -0,0 +1,28 @@ +package io.jsonwebtoken.codec.impl; + +import io.jsonwebtoken.codec.Decoder; +import io.jsonwebtoken.codec.DecodingException; +import io.jsonwebtoken.lang.Assert; + +public class ExceptionPropagatingDecoder implements Decoder { + + private final Decoder decoder; + + public ExceptionPropagatingDecoder(Decoder decoder) { + Assert.notNull(decoder, "Decoder cannot be null."); + this.decoder = decoder; + } + + @Override + public R decode(T t) throws DecodingException { + Assert.notNull(t, "Decode argument cannot be null."); + try { + return decoder.decode(t); + } catch (DecodingException e) { + throw e; //propagate + } catch (Exception e) { + String msg = "Unable to decode input: " + e.getMessage(); + throw new DecodingException(msg, e); + } + } +} diff --git a/src/main/java/io/jsonwebtoken/codec/impl/ExceptionPropagatingEncoder.java b/src/main/java/io/jsonwebtoken/codec/impl/ExceptionPropagatingEncoder.java new file mode 100644 index 000000000..ab21278ea --- /dev/null +++ b/src/main/java/io/jsonwebtoken/codec/impl/ExceptionPropagatingEncoder.java @@ -0,0 +1,28 @@ +package io.jsonwebtoken.codec.impl; + +import io.jsonwebtoken.codec.Encoder; +import io.jsonwebtoken.codec.EncodingException; +import io.jsonwebtoken.lang.Assert; + +public class ExceptionPropagatingEncoder implements Encoder { + + private final Encoder encoder; + + public ExceptionPropagatingEncoder(Encoder encoder) { + Assert.notNull(encoder, "Encoder cannot be null."); + this.encoder = encoder; + } + + @Override + public R encode(T t) throws EncodingException { + Assert.notNull(t, "Encode argument cannot be null."); + try { + return this.encoder.encode(t); + } catch (EncodingException e) { + throw e; //propagate + } catch (Exception e) { + String msg = "Unable to encode input: " + e.getMessage(); + throw new EncodingException(msg, e); + } + } +} diff --git a/src/main/java/io/jsonwebtoken/impl/AbstractTextCodec.java b/src/main/java/io/jsonwebtoken/impl/AbstractTextCodec.java index 3ba757a62..1d3ddcdaa 100644 --- a/src/main/java/io/jsonwebtoken/impl/AbstractTextCodec.java +++ b/src/main/java/io/jsonwebtoken/impl/AbstractTextCodec.java @@ -15,13 +15,19 @@ */ package io.jsonwebtoken.impl; +import io.jsonwebtoken.codec.Decoder; +import io.jsonwebtoken.codec.Encoder; import io.jsonwebtoken.lang.Assert; import java.nio.charset.Charset; +/** + * @deprecated since 0.10.0 - will be removed before 1.0.0. Use {@link Encoder} orr {@link Decoder} instead. + */ +@Deprecated public abstract class AbstractTextCodec implements TextCodec { - protected static final Charset UTF8 = Charset.forName("UTF-8"); + protected static final Charset UTF8 = Charset.forName("UTF-8"); protected static final Charset US_ASCII = Charset.forName("US-ASCII"); @Override diff --git a/src/main/java/io/jsonwebtoken/impl/AndroidBase64Codec.java b/src/main/java/io/jsonwebtoken/impl/AndroidBase64Codec.java index 8ad32c87f..96586111d 100644 --- a/src/main/java/io/jsonwebtoken/impl/AndroidBase64Codec.java +++ b/src/main/java/io/jsonwebtoken/impl/AndroidBase64Codec.java @@ -15,6 +15,14 @@ */ package io.jsonwebtoken.impl; +import io.jsonwebtoken.codec.Decoder; +import io.jsonwebtoken.codec.Encoder; + +/** + * @deprecated since 0.10.0 - will be removed before 1.0.0. Use {@link Encoder#BASE64 Encoder.BASE64} + * or {@link Decoder#BASE64 Decoder.BASE64} instead. + */ +@Deprecated public class AndroidBase64Codec extends AbstractTextCodec { @Override diff --git a/src/main/java/io/jsonwebtoken/impl/Base64Codec.java b/src/main/java/io/jsonwebtoken/impl/Base64Codec.java index d2faf5c07..5867d4725 100644 --- a/src/main/java/io/jsonwebtoken/impl/Base64Codec.java +++ b/src/main/java/io/jsonwebtoken/impl/Base64Codec.java @@ -15,14 +15,22 @@ */ package io.jsonwebtoken.impl; +import io.jsonwebtoken.codec.Decoder; +import io.jsonwebtoken.codec.Encoder; + +/** + * @deprecated since 0.10.0 - will be removed before 1.0.0. Use {@link Encoder#BASE64 Encoder.BASE64} + * or {@link Decoder#BASE64 Decoder.BASE64} instead. + */ +@Deprecated public class Base64Codec extends AbstractTextCodec { public String encode(byte[] data) { - return javax.xml.bind.DatatypeConverter.printBase64Binary(data); + return Encoder.BASE64.encode(data); } @Override public byte[] decode(String encoded) { - return javax.xml.bind.DatatypeConverter.parseBase64Binary(encoded); + return Decoder.BASE64.decode(encoded); } } diff --git a/src/main/java/io/jsonwebtoken/impl/Base64UrlCodec.java b/src/main/java/io/jsonwebtoken/impl/Base64UrlCodec.java index ea87f514b..68d584a65 100644 --- a/src/main/java/io/jsonwebtoken/impl/Base64UrlCodec.java +++ b/src/main/java/io/jsonwebtoken/impl/Base64UrlCodec.java @@ -15,90 +15,23 @@ */ package io.jsonwebtoken.impl; +import io.jsonwebtoken.codec.Decoder; +import io.jsonwebtoken.codec.Encoder; + +/** + * @deprecated since 0.10.0 - will be removed before 1.0.0. Use {@link Encoder#BASE64URL Encoder.BASE64URL} + * or {@link Decoder#BASE64URL Decoder.BASE64URL} instead. + */ +@Deprecated public class Base64UrlCodec extends AbstractTextCodec { @Override public String encode(byte[] data) { - String base64Text = TextCodec.BASE64.encode(data); - byte[] bytes = base64Text.getBytes(US_ASCII); - - //base64url encoding doesn't use padding chars: - bytes = removePadding(bytes); - - //replace URL-unfriendly Base64 chars to url-friendly ones: - for (int i = 0; i < bytes.length; i++) { - if (bytes[i] == '+') { - bytes[i] = '-'; - } else if (bytes[i] == '/') { - bytes[i] = '_'; - } - } - - return new String(bytes, US_ASCII); - } - - protected byte[] removePadding(byte[] bytes) { - - byte[] result = bytes; - - int paddingCount = 0; - for (int i = bytes.length - 1; i > 0; i--) { - if (bytes[i] == '=') { - paddingCount++; - } else { - break; - } - } - if (paddingCount > 0) { - result = new byte[bytes.length - paddingCount]; - System.arraycopy(bytes, 0, result, 0, bytes.length - paddingCount); - } - - return result; + return Encoder.BASE64URL.encode(data); } @Override public byte[] decode(String encoded) { - char[] chars = encoded.toCharArray(); //always ASCII - one char == 1 byte - - //Base64 requires padding to be in place before decoding, so add it if necessary: - chars = ensurePadding(chars); - - //Replace url-friendly chars back to normal Base64 chars: - for (int i = 0; i < chars.length; i++) { - if (chars[i] == '-') { - chars[i] = '+'; - } else if (chars[i] == '_') { - chars[i] = '/'; - } - } - - String base64Text = new String(chars); - - return TextCodec.BASE64.decode(base64Text); + return Decoder.BASE64URL.decode(encoded); } - - protected char[] ensurePadding(char[] chars) { - - char[] result = chars; //assume argument in case no padding is necessary - - int paddingCount = 0; - - //fix for https://github.com/jwtk/jjwt/issues/31 - int remainder = chars.length % 4; - if (remainder == 2 || remainder == 3) { - paddingCount = 4 - remainder; - } - - if (paddingCount > 0) { - result = new char[chars.length + paddingCount]; - System.arraycopy(chars, 0, result, 0, chars.length); - for (int i = 0; i < paddingCount; i++) { - result[chars.length + i] = '='; - } - } - - return result; - } - } diff --git a/src/main/java/io/jsonwebtoken/impl/DefaultJwtBuilder.java b/src/main/java/io/jsonwebtoken/impl/DefaultJwtBuilder.java index 25b48d62f..bb2865868 100644 --- a/src/main/java/io/jsonwebtoken/impl/DefaultJwtBuilder.java +++ b/src/main/java/io/jsonwebtoken/impl/DefaultJwtBuilder.java @@ -17,12 +17,20 @@ import com.fasterxml.jackson.core.JsonProcessingException; import com.fasterxml.jackson.databind.ObjectMapper; -import io.jsonwebtoken.*; +import io.jsonwebtoken.Claims; +import io.jsonwebtoken.CompressionCodec; +import io.jsonwebtoken.Header; +import io.jsonwebtoken.JwsHeader; +import io.jsonwebtoken.JwtBuilder; +import io.jsonwebtoken.JwtParser; +import io.jsonwebtoken.Jwts; +import io.jsonwebtoken.SignatureAlgorithm; +import io.jsonwebtoken.codec.Decoder; +import io.jsonwebtoken.codec.Encoder; import io.jsonwebtoken.impl.crypto.DefaultJwtSigner; import io.jsonwebtoken.impl.crypto.JwtSigner; import io.jsonwebtoken.lang.Assert; import io.jsonwebtoken.lang.Collections; -import io.jsonwebtoken.lang.Objects; import io.jsonwebtoken.lang.Strings; import javax.crypto.spec.SecretKeySpec; @@ -39,11 +47,19 @@ public class DefaultJwtBuilder implements JwtBuilder { private String payload; private SignatureAlgorithm algorithm; - private Key key; - private byte[] keyBytes; + private Key key; + + private Encoder base64UrlEncoder = Encoder.BASE64URL; private CompressionCodec compressionCodec; + @Override + public JwtBuilder base64UrlEncodeWith(Encoder base64UrlEncoder) { + Assert.notNull(base64UrlEncoder, "base64UrlEncoder cannot be null."); + this.base64UrlEncoder = base64UrlEncoder; + return this; + } + @Override public JwtBuilder setHeader(Header header) { this.header = header; @@ -88,7 +104,7 @@ public JwtBuilder signWith(SignatureAlgorithm alg, byte[] secretKey) { Assert.notEmpty(secretKey, "secret key byte array cannot be null or empty."); Assert.isTrue(alg.isHmac(), "Key bytes may only be specified for HMAC signatures. If using RSA or Elliptic Curve, use the signWith(SignatureAlgorithm, Key) method instead."); this.algorithm = alg; - this.keyBytes = secretKey; + this.key = new SecretKeySpec(secretKey, alg.getJcaName()); return this; } @@ -96,7 +112,7 @@ public JwtBuilder signWith(SignatureAlgorithm alg, byte[] secretKey) { public JwtBuilder signWith(SignatureAlgorithm alg, String base64EncodedSecretKey) { Assert.hasText(base64EncodedSecretKey, "base64-encoded secret key cannot be null or empty."); Assert.isTrue(alg.isHmac(), "Base64-encoded key bytes may only be specified for HMAC signatures. If using RSA or Elliptic Curve, use the signWith(SignatureAlgorithm, Key) method instead."); - byte[] bytes = TextCodec.BASE64.decode(base64EncodedSecretKey); + byte[] bytes = Decoder.BASE64.decode(base64EncodedSecretKey); return signWith(alg, bytes); } @@ -262,22 +278,13 @@ public String compact() { throw new IllegalStateException("Both 'payload' and 'claims' cannot both be specified. Choose either one."); } - if (key != null && keyBytes != null) { - throw new IllegalStateException("A key object and key bytes cannot both be specified. Choose either one."); - } - Header header = ensureHeader(); - Key key = this.key; - if (key == null && !Objects.isEmpty(keyBytes)) { - key = new SecretKeySpec(keyBytes, algorithm.getJcaName()); - } - JwsHeader jwsHeader; - if (header instanceof JwsHeader) { - jwsHeader = (JwsHeader)header; + jwsHeader = (JwsHeader) header; } else { + //noinspection unchecked jwsHeader = new DefaultJwsHeader(header); } @@ -294,25 +301,19 @@ public String compact() { String base64UrlEncodedHeader = base64UrlEncode(jwsHeader, "Unable to serialize header to json."); - String base64UrlEncodedBody; + byte[] bytes; + try { + bytes = this.payload != null ? payload.getBytes(Strings.UTF_8) : toJson(claims); + } catch (JsonProcessingException e) { + throw new IllegalArgumentException("Unable to serialize claims object to json."); + } if (compressionCodec != null) { - - byte[] bytes; - try { - bytes = this.payload != null ? payload.getBytes(Strings.UTF_8) : toJson(claims); - } catch (JsonProcessingException e) { - throw new IllegalArgumentException("Unable to serialize claims object to json."); - } - - base64UrlEncodedBody = TextCodec.BASE64URL.encode(compressionCodec.compress(bytes)); - - } else { - base64UrlEncodedBody = this.payload != null ? - TextCodec.BASE64URL.encode(this.payload) : - base64UrlEncode(claims, "Unable to serialize claims object to json."); + bytes = compressionCodec.compress(bytes); } + String base64UrlEncodedBody = base64UrlEncoder.encode(bytes); + String jwt = base64UrlEncodedHeader + JwtParser.SEPARATOR_CHAR + base64UrlEncodedBody; if (key != null) { //jwt must be signed: @@ -335,7 +336,7 @@ public String compact() { * @since 0.5 mostly to allow testing overrides */ protected JwtSigner createSigner(SignatureAlgorithm alg, Key key) { - return new DefaultJwtSigner(alg, key); + return new DefaultJwtSigner(alg, key, base64UrlEncoder); } protected String base64UrlEncode(Object o, String errMsg) { @@ -346,11 +347,10 @@ protected String base64UrlEncode(Object o, String errMsg) { throw new IllegalStateException(errMsg, e); } - return TextCodec.BASE64URL.encode(bytes); + return base64UrlEncoder.encode(bytes); } - - protected byte[] toJson(Object object) throws JsonProcessingException { + protected byte[] toJson(Object object) throws JsonProcessingException { return OBJECT_MAPPER.writeValueAsBytes(object); } } diff --git a/src/main/java/io/jsonwebtoken/impl/DefaultJwtParser.java b/src/main/java/io/jsonwebtoken/impl/DefaultJwtParser.java index 4e4b9c79c..46fd3fd68 100644 --- a/src/main/java/io/jsonwebtoken/impl/DefaultJwtParser.java +++ b/src/main/java/io/jsonwebtoken/impl/DefaultJwtParser.java @@ -38,6 +38,7 @@ import io.jsonwebtoken.SignatureException; import io.jsonwebtoken.SigningKeyResolver; import io.jsonwebtoken.UnsupportedJwtException; +import io.jsonwebtoken.codec.Decoder; import io.jsonwebtoken.impl.compression.DefaultCompressionCodecResolver; import io.jsonwebtoken.impl.crypto.DefaultJwtSignatureValidator; import io.jsonwebtoken.impl.crypto.JwtSignatureValidator; @@ -69,12 +70,21 @@ public class DefaultJwtParser implements JwtParser { private CompressionCodecResolver compressionCodecResolver = new DefaultCompressionCodecResolver(); - Claims expectedClaims = new DefaultClaims(); + private Decoder base64UrlDecoder = Decoder.BASE64URL; + + private Claims expectedClaims = new DefaultClaims(); private Clock clock = DefaultClock.INSTANCE; private long allowedClockSkewMillis = 0; + @Override + public JwtParser base64UrlDecodeWith(Decoder base64UrlDecoder) { + Assert.notNull(base64UrlDecoder, "base64UrlDecoder cannot be null."); + this.base64UrlDecoder = base64UrlDecoder; + return this; + } + @Override public JwtParser requireIssuedAt(Date issuedAt) { expectedClaims.setIssuedAt(issuedAt); @@ -146,9 +156,9 @@ public JwtParser setSigningKey(byte[] key) { } @Override - public JwtParser setSigningKey(String base64EncodedKeyBytes) { - Assert.hasText(base64EncodedKeyBytes, "signing key cannot be null or empty."); - this.keyBytes = TextCodec.BASE64.decode(base64EncodedKeyBytes); + public JwtParser setSigningKey(String base64EncodedSecretKey) { + Assert.hasText(base64EncodedSecretKey, "signing key cannot be null or empty."); + this.keyBytes = Decoder.BASE64.decode(base64EncodedSecretKey); return this; } @@ -215,7 +225,7 @@ public Jwt parse(String jwt) throws ExpiredJwtException, MalformedJwtException, if (c == SEPARATOR_CHAR) { CharSequence tokenSeq = Strings.clean(sb); - String token = tokenSeq!=null?tokenSeq.toString():null; + String token = tokenSeq != null ? tokenSeq.toString() : null; if (delimiterCount == 0) { base64UrlEncodedHeader = token; @@ -248,7 +258,8 @@ public Jwt parse(String jwt) throws ExpiredJwtException, MalformedJwtException, CompressionCodec compressionCodec = null; if (base64UrlEncodedHeader != null) { - String origValue = TextCodec.BASE64URL.decodeToString(base64UrlEncodedHeader); + byte[] bytes = base64UrlDecoder.decode(base64UrlEncodedHeader); + String origValue = new String(bytes, Strings.UTF_8); Map m = readValue(origValue); if (base64UrlEncodedDigest != null) { @@ -261,13 +272,11 @@ public Jwt parse(String jwt) throws ExpiredJwtException, MalformedJwtException, } // =============== Body ================= - String payload; + byte[] bytes = base64UrlDecoder.decode(base64UrlEncodedPayload); if (compressionCodec != null) { - byte[] decompressed = compressionCodec.decompress(TextCodec.BASE64URL.decode(base64UrlEncodedPayload)); - payload = new String(decompressed, Strings.UTF_8); - } else { - payload = TextCodec.BASE64URL.decodeToString(base64UrlEncodedPayload); + bytes = compressionCodec.decompress(bytes); } + String payload = new String(bytes, Strings.UTF_8); Claims claims = null; @@ -293,7 +302,7 @@ public Jwt parse(String jwt) throws ExpiredJwtException, MalformedJwtException, if (algorithm == null || algorithm == SignatureAlgorithm.NONE) { //it is plaintext, but it has a signature. This is invalid: String msg = "JWT string has a digest/signature, but the header does not reference a valid signature " + - "algorithm."; + "algorithm."; throw new MalformedJwtException(msg); } @@ -322,7 +331,7 @@ public Jwt parse(String jwt) throws ExpiredJwtException, MalformedJwtException, if (!Objects.isEmpty(keyBytes)) { Assert.isTrue(algorithm.isHmac(), - "Key bytes can only be specified for HMAC signatures. Please specify a PublicKey or PrivateKey instance."); + "Key bytes can only be specified for HMAC signatures. Please specify a PublicKey or PrivateKey instance."); key = new SecretKeySpec(keyBytes, algorithm.getJcaName()); } @@ -338,19 +347,19 @@ public Jwt parse(String jwt) throws ExpiredJwtException, MalformedJwtException, validator = createSignatureValidator(algorithm, key); } catch (IllegalArgumentException e) { String algName = algorithm.getValue(); - String msg = "The parsed JWT indicates it was signed with the " + algName + " signature " + - "algorithm, but the specified signing key of type " + key.getClass().getName() + - " may not be used to validate " + algName + " signatures. Because the specified " + - "signing key reflects a specific and expected algorithm, and the JWT does not reflect " + - "this algorithm, it is likely that the JWT was not expected and therefore should not be " + - "trusted. Another possibility is that the parser was configured with the incorrect " + - "signing key, but this cannot be assumed for security reasons."; + String msg = "The parsed JWT indicates it was signed with the " + algName + " signature " + + "algorithm, but the specified signing key of type " + key.getClass().getName() + + " may not be used to validate " + algName + " signatures. Because the specified " + + "signing key reflects a specific and expected algorithm, and the JWT does not reflect " + + "this algorithm, it is likely that the JWT was not expected and therefore should not be " + + "trusted. Another possibility is that the parser was configured with the incorrect " + + "signing key, but this cannot be assumed for security reasons."; throw new UnsupportedJwtException(msg, e); } if (!validator.isValid(jwtWithoutSignature, base64UrlEncodedDigest)) { String msg = "JWT signature does not match locally computed signature. JWT validity cannot be " + - "asserted and should not be trusted."; + "asserted and should not be trusted."; throw new SignatureException(msg); } } @@ -426,34 +435,31 @@ private void validateExpectedClaims(Header header, Claims claims) { Object expectedClaimValue = expectedClaims.get(expectedClaimName); Object actualClaimValue = claims.get(expectedClaimName); - if ( - Claims.ISSUED_AT.equals(expectedClaimName) || - Claims.EXPIRATION.equals(expectedClaimName) || - Claims.NOT_BEFORE.equals(expectedClaimName) - ) { + if (Claims.ISSUED_AT.equals(expectedClaimName) || Claims.EXPIRATION.equals(expectedClaimName) || + Claims.NOT_BEFORE.equals(expectedClaimName)) { + expectedClaimValue = expectedClaims.get(expectedClaimName, Date.class); actualClaimValue = claims.get(expectedClaimName, Date.class); - } else if ( - expectedClaimValue instanceof Date && - actualClaimValue != null && - actualClaimValue instanceof Long - ) { - actualClaimValue = new Date((Long)actualClaimValue); + + } else if (expectedClaimValue instanceof Date && actualClaimValue instanceof Long) { + + actualClaimValue = new Date((Long) actualClaimValue); } InvalidClaimException invalidClaimException = null; if (actualClaimValue == null) { - String msg = String.format( - ClaimJwtException.MISSING_EXPECTED_CLAIM_MESSAGE_TEMPLATE, - expectedClaimName, expectedClaimValue - ); + + String msg = String.format(ClaimJwtException.MISSING_EXPECTED_CLAIM_MESSAGE_TEMPLATE, + expectedClaimName, expectedClaimValue); + invalidClaimException = new MissingClaimException(header, claims, msg); + } else if (!expectedClaimValue.equals(actualClaimValue)) { - String msg = String.format( - ClaimJwtException.INCORRECT_EXPECTED_CLAIM_MESSAGE_TEMPLATE, - expectedClaimName, expectedClaimValue, actualClaimValue - ); + + String msg = String.format(ClaimJwtException.INCORRECT_EXPECTED_CLAIM_MESSAGE_TEMPLATE, + expectedClaimName, expectedClaimValue, actualClaimValue); + invalidClaimException = new IncorrectClaimException(header, claims, msg); } @@ -469,7 +475,7 @@ private void validateExpectedClaims(Header header, Claims claims) { * @since 0.5 mostly to allow testing overrides */ protected JwtSignatureValidator createSignatureValidator(SignatureAlgorithm alg, Key key) { - return new DefaultJwtSignatureValidator(alg, key); + return new DefaultJwtSignatureValidator(alg, key, base64UrlDecoder); } @Override diff --git a/src/main/java/io/jsonwebtoken/impl/DefaultTextCodecFactory.java b/src/main/java/io/jsonwebtoken/impl/DefaultTextCodecFactory.java index 9b3d9f6f2..c148c4f6b 100644 --- a/src/main/java/io/jsonwebtoken/impl/DefaultTextCodecFactory.java +++ b/src/main/java/io/jsonwebtoken/impl/DefaultTextCodecFactory.java @@ -15,6 +15,10 @@ */ package io.jsonwebtoken.impl; +/** + * @deprecated since 0.10.0 + */ +@Deprecated public class DefaultTextCodecFactory implements TextCodecFactory { protected String getSystemProperty(String key) { diff --git a/src/main/java/io/jsonwebtoken/impl/TextCodec.java b/src/main/java/io/jsonwebtoken/impl/TextCodec.java index 44e8b931a..2a51ed5cf 100644 --- a/src/main/java/io/jsonwebtoken/impl/TextCodec.java +++ b/src/main/java/io/jsonwebtoken/impl/TextCodec.java @@ -15,10 +15,26 @@ */ package io.jsonwebtoken.impl; +/** + * @deprecated since 0.10.0. Use an {@link io.jsonwebtoken.codec.Encoder} or {@link io.jsonwebtoken.codec.Decoder} + * as needed. This class will be removed before 1.0.0 + */ +@Deprecated public interface TextCodec { - public static final TextCodec BASE64 = new DefaultTextCodecFactory().getTextCodec(); - public static final TextCodec BASE64URL = new Base64UrlCodec(); + /** + * @deprecated since 0.10.0. Use {@link io.jsonwebtoken.codec.Encoder#BASE64 Encoder.BASE64} or + * {@link io.jsonwebtoken.codec.Decoder#BASE64 Decoder.BASE64} instead. This class will be removed before 1.0.0 + */ + @Deprecated + TextCodec BASE64 = new Base64Codec(); + + /** + * @deprecated since 0.10.0. Use {@link io.jsonwebtoken.codec.Encoder#BASE64URL Encoder.BASE64URL} or + * {@link io.jsonwebtoken.codec.Decoder#BASE64URL Decoder.BASE64URL} instead. This class will be removed before 1.0.0 + */ + @Deprecated + TextCodec BASE64URL = new Base64UrlCodec(); String encode(String data); diff --git a/src/main/java/io/jsonwebtoken/impl/TextCodecFactory.java b/src/main/java/io/jsonwebtoken/impl/TextCodecFactory.java index 43fc429c5..a318060f1 100644 --- a/src/main/java/io/jsonwebtoken/impl/TextCodecFactory.java +++ b/src/main/java/io/jsonwebtoken/impl/TextCodecFactory.java @@ -15,6 +15,10 @@ */ package io.jsonwebtoken.impl; +/** + * @deprecated since 0.10.0 + */ +@Deprecated public interface TextCodecFactory { TextCodec getTextCodec(); diff --git a/src/main/java/io/jsonwebtoken/impl/compression/DefaultCompressionCodecResolver.java b/src/main/java/io/jsonwebtoken/impl/compression/DefaultCompressionCodecResolver.java index fb32c1c62..64de55fd7 100644 --- a/src/main/java/io/jsonwebtoken/impl/compression/DefaultCompressionCodecResolver.java +++ b/src/main/java/io/jsonwebtoken/impl/compression/DefaultCompressionCodecResolver.java @@ -26,11 +26,11 @@ * Default implementation of {@link CompressionCodecResolver} that supports the following: *

*

    - *
  • If the specified JWT {@link Header} does not have a {@code calg} header, this implementation does + *
  • If the specified JWT {@link Header} does not have a {@code zip} header, this implementation does * nothing and returns {@code null} to the caller, indicating no compression was used.
  • - *
  • If the header has a {@code calg} value of {@code DEF}, a {@link DeflateCompressionCodec} will be returned.
  • - *
  • If the header has a {@code calg} value of {@code GZIP}, a {@link GzipCompressionCodec} will be returned.
  • - *
  • If the header has any other {@code calg} value, a {@link CompressionException} is thrown to reflect an + *
  • If the header has a {@code zip} value of {@code DEF}, a {@link DeflateCompressionCodec} will be returned.
  • + *
  • If the header has a {@code zip} value of {@code GZIP}, a {@link GzipCompressionCodec} will be returned.
  • + *
  • If the header has any other {@code zip} value, a {@link CompressionException} is thrown to reflect an * unrecognized algorithm.
  • *
* diff --git a/src/main/java/io/jsonwebtoken/impl/crypto/DefaultJwtSignatureValidator.java b/src/main/java/io/jsonwebtoken/impl/crypto/DefaultJwtSignatureValidator.java index 245c0d551..f9119878b 100644 --- a/src/main/java/io/jsonwebtoken/impl/crypto/DefaultJwtSignatureValidator.java +++ b/src/main/java/io/jsonwebtoken/impl/crypto/DefaultJwtSignatureValidator.java @@ -16,7 +16,7 @@ package io.jsonwebtoken.impl.crypto; import io.jsonwebtoken.SignatureAlgorithm; -import io.jsonwebtoken.impl.TextCodec; +import io.jsonwebtoken.codec.Decoder; import io.jsonwebtoken.lang.Assert; import java.nio.charset.Charset; @@ -27,14 +27,27 @@ public class DefaultJwtSignatureValidator implements JwtSignatureValidator { private static final Charset US_ASCII = Charset.forName("US-ASCII"); private final SignatureValidator signatureValidator; + private final Decoder base64UrlDecoder; + @Deprecated public DefaultJwtSignatureValidator(SignatureAlgorithm alg, Key key) { - this(DefaultSignatureValidatorFactory.INSTANCE, alg, key); + this(DefaultSignatureValidatorFactory.INSTANCE, alg, key, Decoder.BASE64URL); } + public DefaultJwtSignatureValidator(SignatureAlgorithm alg, Key key, Decoder base64UrlDecoder) { + this(DefaultSignatureValidatorFactory.INSTANCE, alg, key, base64UrlDecoder); + } + + @Deprecated public DefaultJwtSignatureValidator(SignatureValidatorFactory factory, SignatureAlgorithm alg, Key key) { + this(factory, alg, key, Decoder.BASE64URL); + } + + public DefaultJwtSignatureValidator(SignatureValidatorFactory factory, SignatureAlgorithm alg, Key key, Decoder base64UrlDecoder) { Assert.notNull(factory, "SignerFactory argument cannot be null."); + Assert.notNull(base64UrlDecoder, "Base64Url decoder argument cannot be null."); this.signatureValidator = factory.createSignatureValidator(alg, key); + this.base64UrlDecoder = base64UrlDecoder; } @Override @@ -42,7 +55,7 @@ public boolean isValid(String jwtWithoutSignature, String base64UrlEncodedSignat byte[] data = jwtWithoutSignature.getBytes(US_ASCII); - byte[] signature = TextCodec.BASE64URL.decode(base64UrlEncodedSignature); + byte[] signature = base64UrlDecoder.decode(base64UrlEncodedSignature); return this.signatureValidator.isValid(data, signature); } diff --git a/src/main/java/io/jsonwebtoken/impl/crypto/DefaultJwtSigner.java b/src/main/java/io/jsonwebtoken/impl/crypto/DefaultJwtSigner.java index e55d59332..de761a881 100644 --- a/src/main/java/io/jsonwebtoken/impl/crypto/DefaultJwtSigner.java +++ b/src/main/java/io/jsonwebtoken/impl/crypto/DefaultJwtSigner.java @@ -16,7 +16,8 @@ package io.jsonwebtoken.impl.crypto; import io.jsonwebtoken.SignatureAlgorithm; -import io.jsonwebtoken.impl.TextCodec; +import io.jsonwebtoken.codec.Encoder; +import io.jsonwebtoken.codec.impl.Base64UrlEncoder; import io.jsonwebtoken.lang.Assert; import java.nio.charset.Charset; @@ -27,13 +28,26 @@ public class DefaultJwtSigner implements JwtSigner { private static final Charset US_ASCII = Charset.forName("US-ASCII"); private final Signer signer; + private final Encoder base64UrlEncoder; + @Deprecated public DefaultJwtSigner(SignatureAlgorithm alg, Key key) { - this(DefaultSignerFactory.INSTANCE, alg, key); + this(DefaultSignerFactory.INSTANCE, alg, key, Encoder.BASE64URL); } + public DefaultJwtSigner(SignatureAlgorithm alg, Key key, Encoder base64UrlEncoder) { + this(DefaultSignerFactory.INSTANCE, alg, key, base64UrlEncoder); + } + + @Deprecated public DefaultJwtSigner(SignerFactory factory, SignatureAlgorithm alg, Key key) { + this(factory, alg, key, Encoder.BASE64URL); + } + + public DefaultJwtSigner(SignerFactory factory, SignatureAlgorithm alg, Key key, Encoder base64UrlEncoder) { Assert.notNull(factory, "SignerFactory argument cannot be null."); + Assert.notNull(base64UrlEncoder, "Base64Url Encoder cannot be null."); + this.base64UrlEncoder = base64UrlEncoder; this.signer = factory.createSigner(alg, key); } @@ -44,6 +58,6 @@ public String sign(String jwtWithoutSignature) { byte[] signature = signer.sign(bytesToSign); - return TextCodec.BASE64URL.encode(signature); + return base64UrlEncoder.encode(signature); } } diff --git a/src/test/groovy/io/jsonwebtoken/JwtParserTest.groovy b/src/test/groovy/io/jsonwebtoken/JwtParserTest.groovy index 00dc67f23..3140a0153 100644 --- a/src/test/groovy/io/jsonwebtoken/JwtParserTest.groovy +++ b/src/test/groovy/io/jsonwebtoken/JwtParserTest.groovy @@ -15,9 +15,10 @@ */ package io.jsonwebtoken +import io.jsonwebtoken.codec.Encoder import io.jsonwebtoken.impl.DefaultClock import io.jsonwebtoken.impl.FixedClock -import io.jsonwebtoken.impl.TextCodec +import io.jsonwebtoken.lang.Strings import org.junit.Test import javax.crypto.spec.SecretKeySpec @@ -38,6 +39,11 @@ class JwtParserTest { return key } + protected static String base64Url(String s) { + byte[] bytes = s.getBytes(Strings.UTF_8) + return Encoder.BASE64URL.encode(bytes) + } + @Test void testSetDuplicateSigningKeys() { @@ -70,8 +76,7 @@ class JwtParserTest { String junkPayload = '{;aklsjd;fkajsd;fkjasd;lfkj}' - String bad = TextCodec.BASE64.encode('{"alg":"none"}') + '.' + - TextCodec.BASE64.encode(junkPayload) + '.' + String bad = base64Url('{"alg":"none"}') + '.' + base64Url(junkPayload) + '.' try { Jwts.parser().parse(bad) @@ -92,9 +97,7 @@ class JwtParserTest { String badSig = ";aklsjdf;kajsd;fkjas;dklfj" - String bad = TextCodec.BASE64.encode(header) + '.' + - TextCodec.BASE64.encode(payload) + '.' + - TextCodec.BASE64.encode(badSig) + String bad = base64Url(header) + '.' + base64Url(payload) + '.' + base64Url(badSig) try { Jwts.parser().setSigningKey(randomKey()).parse(bad) @@ -113,9 +116,7 @@ class JwtParserTest { String badSig = ";aklsjdf;kajsd;fkjas;dklfj" - String bad = TextCodec.BASE64.encode(header) + '.' + - TextCodec.BASE64.encode(payload) + '.' + - TextCodec.BASE64.encode(badSig) + String bad = base64Url(header) + '.' + base64Url(payload) + '.' + base64Url(badSig) try { Jwts.parser().setSigningKey(randomKey()).parse(bad) @@ -129,15 +130,13 @@ class JwtParserTest { @Test void testParsePlaintextJwsWithIncorrectAlg() { - String header = '{"alg":"none"}' + def header = '{"alg":"none"}' - String payload = '{"subject":"Joe"}' + def payload = '{"subject":"Joe"}' - String badSig = ";aklsjdf;kajsd;fkjas;dklfj" + def badSig = ";aklsjdf;kajsd;fkjas;dklfj" - String bad = TextCodec.BASE64.encode(header) + '.' + - TextCodec.BASE64.encode(payload) + '.' + - TextCodec.BASE64.encode(badSig) + String bad = base64Url(header) + '.' + base64Url(payload) + '.' + base64Url(badSig) try { Jwts.parser().setSigningKey(randomKey()).parse(bad) @@ -150,8 +149,11 @@ class JwtParserTest { @Test void testParseWithBase64EncodedSigningKey() { + byte[] key = randomKey() - String base64Encodedkey = TextCodec.BASE64.encode(key) + + String base64Encodedkey = Encoder.BASE64.encode(key) + String payload = 'Hello world!' String compact = Jwts.builder().setPayload(payload).signWith(SignatureAlgorithm.HS256, base64Encodedkey).compact() @@ -1530,10 +1532,7 @@ class JwtParserTest { String bogus = 'bogus' - String bad = TextCodec.BASE64.encode(header) + '.' + - TextCodec.BASE64.encode(payload) + '.' + - TextCodec.BASE64.encode(badSig) + '.' + - TextCodec.BASE64.encode(bogus) + String bad = base64Url(header) + '.' + base64Url(payload) + '.' + base64Url(badSig) + '.' + base64Url(bogus) try { @@ -1549,7 +1548,7 @@ class JwtParserTest { void testNoHeaderNoSig() { String payload = '{"subject":"Joe"}' - String jwtStr = '.' + TextCodec.BASE64.encode(payload) + '.' + String jwtStr = '.' + base64Url(payload) + '.' Jwt jwt = Jwts.parser().parse(jwtStr) @@ -1559,14 +1558,15 @@ class JwtParserTest { @Test void testNoHeaderSig() { + String payload = '{"subject":"Joe"}' String sig = ";aklsjdf;kajsd;fkjas;dklfj" - String jwtStr = '.' + TextCodec.BASE64.encode(payload) + '.' + TextCodec.BASE64.encode(sig) + String jwtStr = '.' + base64Url(payload) + '.' + base64Url(sig) try { - Jwt jwt = Jwts.parser().parse(jwtStr) + Jwts.parser().parse(jwtStr) fail() } catch (MalformedJwtException se) { assertEquals 'JWT string has a digest/signature, but the header does not reference a valid signature algorithm.', se.message @@ -1581,7 +1581,7 @@ class JwtParserTest { String sig = ";aklsjdf;kajsd;fkjas;dklfj" - String jwtStr = TextCodec.BASE64.encode(payload) + '.' + TextCodec.BASE64.encode(payload) + '.' + TextCodec.BASE64.encode(sig) + String jwtStr = base64Url(payload) + '.' + base64Url(payload) + '.' + base64Url(sig) try { Jwt jwt = Jwts.parser().parse(jwtStr) diff --git a/src/test/groovy/io/jsonwebtoken/JwtsTest.groovy b/src/test/groovy/io/jsonwebtoken/JwtsTest.groovy index 9a344f0b9..42a597971 100644 --- a/src/test/groovy/io/jsonwebtoken/JwtsTest.groovy +++ b/src/test/groovy/io/jsonwebtoken/JwtsTest.groovy @@ -16,9 +16,9 @@ package io.jsonwebtoken import com.fasterxml.jackson.databind.ObjectMapper +import io.jsonwebtoken.codec.Encoder import io.jsonwebtoken.impl.DefaultHeader import io.jsonwebtoken.impl.DefaultJwsHeader -import io.jsonwebtoken.impl.TextCodec import io.jsonwebtoken.impl.compression.CompressionCodecs import io.jsonwebtoken.impl.compression.DefaultCompressionCodecResolver import io.jsonwebtoken.impl.compression.GzipCompressionCodec @@ -39,6 +39,11 @@ import static org.junit.Assert.* class JwtsTest { + protected static String base64Url(String s) { + byte[] bytes = s.getBytes(Strings.UTF_8) + return Encoder.BASE64URL.encode(bytes) + } + @Test void testSubclass() { new Jwts() @@ -613,8 +618,8 @@ class JwtsTest { PrivateKey privateKey = kp.getPrivate(); ObjectMapper om = new ObjectMapper() - String header = TextCodec.BASE64URL.encode(om.writeValueAsString(['alg': 'HS256'])) - String body = TextCodec.BASE64URL.encode(om.writeValueAsString('foo')) + String header = base64Url(om.writeValueAsString(['alg': 'HS256'])) + String body = base64Url(om.writeValueAsString('foo')) String compact = header + '.' + body + '.' // Now for the forgery: simulate an attacker using the RSA public key to sign a token, but @@ -622,7 +627,7 @@ class JwtsTest { Mac mac = Mac.getInstance('HmacSHA256'); mac.init(new SecretKeySpec(publicKey.getEncoded(), 'HmacSHA256')); byte[] signatureBytes = mac.doFinal(compact.getBytes(Charset.forName('US-ASCII'))) - String encodedSignature = TextCodec.BASE64URL.encode(signatureBytes); + String encodedSignature = Encoder.BASE64URL.encode(signatureBytes) //Finally, the forged token is the header + body + forged signature: String forged = compact + encodedSignature; @@ -646,8 +651,8 @@ class JwtsTest { //PrivateKey privateKey = kp.getPrivate(); ObjectMapper om = new ObjectMapper() - String header = TextCodec.BASE64URL.encode(om.writeValueAsString(['alg': 'HS256'])) - String body = TextCodec.BASE64URL.encode(om.writeValueAsString('foo')) + String header = base64Url(om.writeValueAsString(['alg': 'HS256'])) + String body = base64Url(om.writeValueAsString('foo')) String compact = header + '.' + body + '.' // Now for the forgery: simulate an attacker using the RSA public key to sign a token, but @@ -655,7 +660,7 @@ class JwtsTest { Mac mac = Mac.getInstance('HmacSHA256'); mac.init(new SecretKeySpec(publicKey.getEncoded(), 'HmacSHA256')); byte[] signatureBytes = mac.doFinal(compact.getBytes(Charset.forName('US-ASCII'))) - String encodedSignature = TextCodec.BASE64URL.encode(signatureBytes); + String encodedSignature = Encoder.BASE64URL.encode(signatureBytes); //Finally, the forged token is the header + body + forged signature: String forged = compact + encodedSignature; @@ -679,8 +684,8 @@ class JwtsTest { //PrivateKey privateKey = kp.getPrivate(); ObjectMapper om = new ObjectMapper() - String header = TextCodec.BASE64URL.encode(om.writeValueAsString(['alg': 'HS256'])) - String body = TextCodec.BASE64URL.encode(om.writeValueAsString('foo')) + String header = base64Url(om.writeValueAsString(['alg': 'HS256'])) + String body = base64Url(om.writeValueAsString('foo')) String compact = header + '.' + body + '.' // Now for the forgery: simulate an attacker using the Elliptic Curve public key to sign a token, but @@ -688,7 +693,7 @@ class JwtsTest { Mac mac = Mac.getInstance('HmacSHA256'); mac.init(new SecretKeySpec(publicKey.getEncoded(), 'HmacSHA256')); byte[] signatureBytes = mac.doFinal(compact.getBytes(Charset.forName('US-ASCII'))) - String encodedSignature = TextCodec.BASE64URL.encode(signatureBytes); + String encodedSignature = Encoder.BASE64URL.encode(signatureBytes); //Finally, the forged token is the header + body + forged signature: String forged = compact + encodedSignature; diff --git a/src/test/groovy/io/jsonwebtoken/codec/CodecExceptionTest.groovy b/src/test/groovy/io/jsonwebtoken/codec/CodecExceptionTest.groovy new file mode 100644 index 000000000..07151036e --- /dev/null +++ b/src/test/groovy/io/jsonwebtoken/codec/CodecExceptionTest.groovy @@ -0,0 +1,16 @@ +package io.jsonwebtoken.codec + +import org.junit.Test + +import static org.junit.Assert.assertEquals + +class CodecExceptionTest { + + @Test + void testConstructorWithCause() { + def ioException = new IOException("root error") + def exception = new CodecException("wrapping", ioException) + assertEquals "wrapping", exception.getMessage() + assertEquals ioException, exception.getCause() + } +} diff --git a/src/test/groovy/io/jsonwebtoken/codec/DecodingExceptionTest.groovy b/src/test/groovy/io/jsonwebtoken/codec/DecodingExceptionTest.groovy new file mode 100644 index 000000000..99c218623 --- /dev/null +++ b/src/test/groovy/io/jsonwebtoken/codec/DecodingExceptionTest.groovy @@ -0,0 +1,16 @@ +package io.jsonwebtoken.codec + +import org.junit.Test + +import static org.junit.Assert.assertEquals + +class DecodingExceptionTest { + + @Test + void testConstructorWithCause() { + def ioException = new IOException("root error") + def exception = new DecodingException("wrapping", ioException) + assertEquals "wrapping", exception.getMessage() + assertEquals ioException, exception.getCause() + } +} diff --git a/src/test/groovy/io/jsonwebtoken/codec/EncodingExceptionTest.groovy b/src/test/groovy/io/jsonwebtoken/codec/EncodingExceptionTest.groovy new file mode 100644 index 000000000..8d68928c2 --- /dev/null +++ b/src/test/groovy/io/jsonwebtoken/codec/EncodingExceptionTest.groovy @@ -0,0 +1,16 @@ +package io.jsonwebtoken.codec + +import org.junit.Test + +import static org.junit.Assert.assertEquals + +class EncodingExceptionTest { + + @Test + void testConstructorWithCause() { + def ioException = new IOException("root error") + def exception = new EncodingException("wrapping", ioException) + assertEquals "wrapping", exception.getMessage() + assertEquals ioException, exception.getCause() + } +} diff --git a/src/test/groovy/io/jsonwebtoken/codec/impl/Base64DecoderTest.groovy b/src/test/groovy/io/jsonwebtoken/codec/impl/Base64DecoderTest.groovy new file mode 100644 index 000000000..307a8e370 --- /dev/null +++ b/src/test/groovy/io/jsonwebtoken/codec/impl/Base64DecoderTest.groovy @@ -0,0 +1,22 @@ +package io.jsonwebtoken.codec.impl + +import io.jsonwebtoken.lang.Strings +import org.junit.Test + +import static org.junit.Assert.assertEquals + +class Base64DecoderTest { + + @Test(expected = IllegalArgumentException) + void testDecodeWithNullArgument() { + new Base64Decoder().decode(null) + } + + @Test + void testDecode() { + String encoded = 'SGVsbG8g5LiW55WM' // Hello 世界 + byte[] bytes = new Base64Decoder().decode(encoded) + String result = new String(bytes, Strings.UTF_8) + assertEquals 'Hello 世界', result + } +} diff --git a/src/test/groovy/io/jsonwebtoken/codec/impl/Base64EncoderTest.groovy b/src/test/groovy/io/jsonwebtoken/codec/impl/Base64EncoderTest.groovy new file mode 100644 index 000000000..b1b544859 --- /dev/null +++ b/src/test/groovy/io/jsonwebtoken/codec/impl/Base64EncoderTest.groovy @@ -0,0 +1,22 @@ +package io.jsonwebtoken.codec.impl + +import io.jsonwebtoken.lang.Strings +import org.junit.Test + +import static org.junit.Assert.assertEquals + +class Base64EncoderTest { + + @Test(expected = IllegalArgumentException) + void testEncodeWithNullArgument() { + new Base64Encoder().encode(null) + } + + @Test + void testDecode() { + String input = 'Hello 世界' + byte[] bytes = input.getBytes(Strings.UTF_8) + String encoded = new Base64Encoder().encode(bytes) + assertEquals 'SGVsbG8g5LiW55WM', encoded + } +} diff --git a/src/test/groovy/io/jsonwebtoken/codec/impl/Base64Test.groovy b/src/test/groovy/io/jsonwebtoken/codec/impl/Base64Test.groovy new file mode 100644 index 000000000..2c6b3bceb --- /dev/null +++ b/src/test/groovy/io/jsonwebtoken/codec/impl/Base64Test.groovy @@ -0,0 +1,131 @@ +package io.jsonwebtoken.codec.impl + +import io.jsonwebtoken.lang.Strings +import org.junit.Test + +import static org.junit.Assert.* + +class Base64Test { + + private static final String PLAINTEXT = + '''Bacon ipsum dolor amet venison beef pork chop, doner jowl pastrami ground round alcatra. + Beef leberkas filet mignon ball tip pork spare ribs kevin short loin ribeye ground round + biltong jerky short ribs corned beef. Strip steak turducken meatball porchetta beef ribs + shoulder pork belly doner salami corned beef kielbasa cow filet mignon drumstick. Bacon + tenderloin pancetta flank frankfurter ham kevin leberkas meatball turducken beef ribs. + Cupim short loin short ribs shankle tenderloin. Ham ribeye hamburger flank tenderloin + cupim t-bone, shank tri-tip venison salami sausage pancetta. Pork belly chuck salami + alcatra sirloin. + + 以ケ ホゥ婧詃 橎ちゅぬ蛣埣 禧ざしゃ蟨廩 椥䤥グ曣わ 基覧 滯っ䶧きょメ Ủ䧞以ケ妣 择禤槜谣お 姨のドゥ, + らボみょば䪩 苯礊觊ツュ婃 䩦ディふげセ げセりょ 禤槜 Ủ䧞以ケ妣 せがみゅちょ䰯 择禤槜谣お 難ゞ滧 蝥ちゃ, + 滯っ䶧きょメ らボみょば䪩 礯みゃ楦と饥 椥䤥グ ウァ槚 訤をりゃしゑ びゃ驨も氩簥 栨キョ奎婨榞 ヌに楃 以ケ, + 姚奊べ 椥䤥グ曣わ 栨キョ奎婨榞 ちょ䰯 Ủ䧞以ケ妣 誧姨のドゥろ よ苯礊 く涥, りゅぽ槞 馣ぢゃ尦䦎ぎ + 大た䏩䰥ぐ 郎きや楺橯 䧎キェ, 難ゞ滧 栧择 谯䧟簨訧ぎょ 椥䤥グ曣わ''' + + @Test + void testEncodeToStringWithNullArgument() { + String s = Base64.DEFAULT.encodeToString(null, false) + assertEquals 0, s.toCharArray().length + } + + @Test + void testEncodeToStringWithEmptyByteArray() { + byte[] bytes = new byte[0] + String s = Base64.DEFAULT.encodeToString(bytes, false) + assertEquals 0, s.toCharArray().length + } + + @Test + void testLineSeparators() { + byte[] bytes = PLAINTEXT.getBytes(Strings.UTF_8) + String encoded = Base64.DEFAULT.encodeToString(bytes, true) + + def r = new StringReader(encoded) + String line = '' + + while ((line = r.readLine()) != null) { + assertTrue line.length() <= 76 + } + } + + @Test + void testDecodeFastWithNullArgument() { + byte[] bytes = Base64.DEFAULT.decodeFast(null) + assertEquals 0, bytes.length + } + + @Test + void testDecodeFastWithEmptyCharArray() { + byte[] bytes = Base64.DEFAULT.decodeFast(new char[0]) + assertEquals 0, bytes.length + } + + @Test + void testDecodeFastWithSurroundingIllegalCharacters() { + String expected = 'Hello 世界' + def encoded = '***SGVsbG8g5LiW55WM!!!' + byte[] bytes = Base64.DEFAULT.decodeFast(encoded.toCharArray()) + String result = new String(bytes, Strings.UTF_8) + assertEquals expected, result + } + + @Test + void testDecodeFastWithLineSeparators() { + + byte[] bytes = PLAINTEXT.getBytes(Strings.UTF_8) + String encoded = Base64.DEFAULT.encodeToString(bytes, true) + + byte[] resultBytes = Base64.DEFAULT.decodeFast(encoded.toCharArray()) + + assertTrue Arrays.equals(bytes, resultBytes) + assertEquals PLAINTEXT, new String(resultBytes, Strings.UTF_8) + } + + private static String BASE64(String s) { + byte[] bytes = s.getBytes(Strings.UTF_8); + return Base64.DEFAULT.encodeToString(bytes, false) + } + + @Test // https://tools.ietf.org/html/rfc4648#page-12 + void testRfc4648Base64TestVectors() { + + assertEquals "", BASE64("") + + assertEquals "Zg==", BASE64("f") + + assertEquals "Zm8=", BASE64("fo") + + assertEquals "Zm9v", BASE64("foo") + + assertEquals "Zm9vYg==", BASE64("foob") + + assertEquals "Zm9vYmE=", BASE64("fooba") + + assertEquals "Zm9vYmFy", BASE64("foobar") + } + + private static String BASE64URL(String s) { + byte[] bytes = s.getBytes(Strings.UTF_8); + return Base64.URL_SAFE.encodeToString(bytes, false) + } + + @Test //same test vectors above, but with padding removed + void testRfc4648Base64UrlTestVectors() { + + assertEquals "", BASE64URL("") + + assertEquals "Zg", BASE64URL("f") //base64 = 2 padding chars, base64url = no padding needed + + assertEquals "Zm8", BASE64URL("fo") //base64 = 1 padding char, base64url = no padding needed + + assertEquals "Zm9v", BASE64URL("foo") + + assertEquals "Zm9vYg", BASE64URL("foob") //base64 = 2 padding chars, base64url = no padding needed + + assertEquals "Zm9vYmE", BASE64URL("fooba") //base64 = 1 padding char, base64url = no padding needed + + assertEquals "Zm9vYmFy", BASE64URL("foobar") + + } +} diff --git a/src/test/groovy/io/jsonwebtoken/codec/impl/ExceptionPropagatingDecoderTest.groovy b/src/test/groovy/io/jsonwebtoken/codec/impl/ExceptionPropagatingDecoderTest.groovy new file mode 100644 index 000000000..3d3a0da9d --- /dev/null +++ b/src/test/groovy/io/jsonwebtoken/codec/impl/ExceptionPropagatingDecoderTest.groovy @@ -0,0 +1,58 @@ +package io.jsonwebtoken.codec.impl + +import io.jsonwebtoken.codec.Decoder +import io.jsonwebtoken.codec.DecodingException +import io.jsonwebtoken.codec.EncodingException +import org.junit.Test + +import static org.junit.Assert.* + +class ExceptionPropagatingDecoderTest { + + @Test(expected = IllegalArgumentException) + void testWithNullConstructorArgument() { + new ExceptionPropagatingDecoder(null) + } + + @Test(expected = IllegalArgumentException) + void testEncodeWithNullArgument() { + def decoder = new ExceptionPropagatingDecoder<>(new Base64UrlDecoder()) + decoder.decode(null) + } + + @Test + void testEncodePropagatesDecodingException() { + def decoder = new ExceptionPropagatingDecoder(new Decoder() { + @Override + Object decode(Object o) throws DecodingException { + throw new DecodingException("problem", new IOException("dummy")) + } + }) + try { + decoder.decode("hello") + fail() + } catch (DecodingException ex) { + assertEquals "problem", ex.getMessage() + } + } + + @Test + void testEncodeWithNonEncodingExceptionIsWrappedAsEncodingException() { + + def causeEx = new RuntimeException("whatevs") + + def decoder = new ExceptionPropagatingDecoder(new Decoder() { + @Override + Object decode(Object o) throws EncodingException { + throw causeEx + } + }) + try { + decoder.decode("hello") + fail() + } catch (DecodingException ex) { + assertEquals "Unable to decode input: whatevs", ex.getMessage() + assertSame causeEx, ex.getCause() + } + } +} diff --git a/src/test/groovy/io/jsonwebtoken/codec/impl/ExceptionPropagatingEncoderTest.groovy b/src/test/groovy/io/jsonwebtoken/codec/impl/ExceptionPropagatingEncoderTest.groovy new file mode 100644 index 000000000..625a5e9ca --- /dev/null +++ b/src/test/groovy/io/jsonwebtoken/codec/impl/ExceptionPropagatingEncoderTest.groovy @@ -0,0 +1,58 @@ +package io.jsonwebtoken.codec.impl + +import io.jsonwebtoken.codec.Encoder +import io.jsonwebtoken.codec.EncodingException +import org.junit.Test + +import static org.junit.Assert.* + +class ExceptionPropagatingEncoderTest { + + + @Test(expected = IllegalArgumentException) + void testWithNullConstructorArgument() { + new ExceptionPropagatingEncoder(null) + } + + @Test(expected = IllegalArgumentException) + void testEncodeWithNullArgument() { + def encoder = new ExceptionPropagatingEncoder<>(new Base64UrlEncoder()) + encoder.encode(null) + } + + @Test + void testEncodePropagatesEncodingException() { + def encoder = new ExceptionPropagatingEncoder(new Encoder() { + @Override + Object encode(Object o) throws EncodingException { + throw new EncodingException("problem", new IOException("dummy")) + } + }) + try { + encoder.encode("hello") + fail() + } catch (EncodingException ex) { + assertEquals "problem", ex.getMessage() + } + } + + @Test + void testEncodeWithNonEncodingExceptionIsWrappedAsEncodingException() { + + def causeEx = new RuntimeException("whatevs") + + def encoder = new ExceptionPropagatingEncoder(new Encoder() { + @Override + Object encode(Object o) throws EncodingException { + throw causeEx; + } + }) + try { + encoder.encode("hello") + fail() + } catch (EncodingException ex) { + assertEquals "Unable to encode input: whatevs", ex.getMessage() + assertSame causeEx, ex.getCause() + } + } +} diff --git a/src/test/groovy/io/jsonwebtoken/impl/AndroidBase64CodecTest.groovy b/src/test/groovy/io/jsonwebtoken/impl/AndroidBase64CodecTest.groovy index 54d53a238..d097dc559 100644 --- a/src/test/groovy/io/jsonwebtoken/impl/AndroidBase64CodecTest.groovy +++ b/src/test/groovy/io/jsonwebtoken/impl/AndroidBase64CodecTest.groovy @@ -27,6 +27,7 @@ import static org.powermock.api.easymock.PowerMock.mockStatic import static org.powermock.api.easymock.PowerMock.replayAll import static org.powermock.api.easymock.PowerMock.verifyAll +@Deprecated //remove just before 1.0.0 release @RunWith(PowerMockRunner.class) @PrepareForTest([Base64.class]) class AndroidBase64CodecTest { diff --git a/src/test/groovy/io/jsonwebtoken/impl/Base64CodecTest.groovy b/src/test/groovy/io/jsonwebtoken/impl/Base64CodecTest.groovy new file mode 100644 index 000000000..d9f0deddb --- /dev/null +++ b/src/test/groovy/io/jsonwebtoken/impl/Base64CodecTest.groovy @@ -0,0 +1,22 @@ +package io.jsonwebtoken.impl + +import org.junit.Test + +import static org.junit.Assert.assertEquals + +@Deprecated //remove just before 1.0.0 release +class Base64CodecTest { + + @Test + void testEncodeDecode() { + + String s = "Hello 世界" + + def codec = new Base64Codec() + + String encoded = codec.encode(s) + + assertEquals s, codec.decodeToString(encoded) + } + +} diff --git a/src/test/groovy/io/jsonwebtoken/impl/Base64UrlCodecTest.groovy b/src/test/groovy/io/jsonwebtoken/impl/Base64UrlCodecTest.groovy index 45969d862..b1f4971b1 100644 --- a/src/test/groovy/io/jsonwebtoken/impl/Base64UrlCodecTest.groovy +++ b/src/test/groovy/io/jsonwebtoken/impl/Base64UrlCodecTest.groovy @@ -1,19 +1,25 @@ package io.jsonwebtoken.impl +import io.jsonwebtoken.lang.Strings import org.junit.Test import static org.junit.Assert.* +@Deprecated //remove just before 1.0.0 release class Base64UrlCodecTest { @Test - void testRemovePaddingWithEmptyByteArray() { + void testEncodeDecode() { + + String s = "Hello 世界" def codec = new Base64UrlCodec() - byte[] empty = new byte[0]; + String base64url = codec.encode(s.getBytes(Strings.UTF_8)) + + byte[] decoded = codec.decode(base64url) - def result = codec.removePadding(empty) + String result = new String(decoded, Strings.UTF_8) - assertSame empty, result + assertEquals s, result } } diff --git a/src/test/groovy/io/jsonwebtoken/impl/DefaultJwtBuilderTest.groovy b/src/test/groovy/io/jsonwebtoken/impl/DefaultJwtBuilderTest.groovy index f4a87fc37..737067f93 100644 --- a/src/test/groovy/io/jsonwebtoken/impl/DefaultJwtBuilderTest.groovy +++ b/src/test/groovy/io/jsonwebtoken/impl/DefaultJwtBuilderTest.groovy @@ -19,6 +19,8 @@ import com.fasterxml.jackson.core.JsonProcessingException import com.fasterxml.jackson.databind.JsonMappingException import io.jsonwebtoken.Jwts import io.jsonwebtoken.SignatureAlgorithm +import io.jsonwebtoken.codec.Encoder +import io.jsonwebtoken.codec.EncodingException import io.jsonwebtoken.impl.compression.CompressionCodecs import io.jsonwebtoken.impl.crypto.MacProvider import org.junit.Test @@ -164,21 +166,6 @@ class DefaultJwtBuilderTest { } } - @Test - void testCompactWithBothKeyAndKeyBytes() { - def b = new DefaultJwtBuilder() - b.setPayload('foo') - def key = MacProvider.generateKey() - b.signWith(SignatureAlgorithm.HS256, key) - b.signWith(SignatureAlgorithm.HS256, key.encoded) - try { - b.compact() - fail() - } catch (IllegalStateException ise) { - assertEquals ise.message, "A key object and key bytes cannot both be specified. Choose either one." - } - } - @Test void testCompactWithJwsHeader() { def b = new DefaultJwtBuilder() @@ -320,4 +307,21 @@ class DefaultJwtBuilderTest { assertNull b.claims } + @Test(expected = IllegalArgumentException) + void testBase64UrlEncodeWithNullArgument() { + new DefaultJwtBuilder().base64UrlEncodeWith(null) + } + + @Test + void testBase64UrlEncodeWithCustomEncoder() { + def encoder = new Encoder() { + @Override + Object encode(Object o) throws EncodingException { + return null + } + } + def b = new DefaultJwtBuilder().base64UrlEncodeWith(encoder) + assertSame encoder, b.base64UrlEncoder + } + } diff --git a/src/test/groovy/io/jsonwebtoken/impl/DefaultJwtParserTest.groovy b/src/test/groovy/io/jsonwebtoken/impl/DefaultJwtParserTest.groovy new file mode 100644 index 000000000..26c4e22a2 --- /dev/null +++ b/src/test/groovy/io/jsonwebtoken/impl/DefaultJwtParserTest.groovy @@ -0,0 +1,30 @@ +package io.jsonwebtoken.impl + +import io.jsonwebtoken.codec.Decoder +import io.jsonwebtoken.codec.DecodingException +import org.junit.Test +import static org.junit.Assert.* + +// NOTE to the casual reader: even though this test class appears mostly empty, the DefaultJwtParser +// implementation is tested to 100% coverage. The vast majority of its tests are in the JwtsTest class. This class +// just fills in any remaining test gaps. + +class DefaultJwtParserTest { + + @Test(expected = IllegalArgumentException) + void testBase64UrlDecodeWithNullArgument() { + new DefaultJwtBuilder().base64UrlEncodeWith(null) + } + + @Test + void testBase64UrlEncodeWithCustomEncoder() { + def decoder = new Decoder() { + @Override + Object decode(Object o) throws DecodingException { + return null + } + } + def b = new DefaultJwtParser().base64UrlDecodeWith(decoder) + assertSame decoder, b.base64UrlDecoder + } +} diff --git a/src/test/groovy/io/jsonwebtoken/impl/DefaultTextCodecFactoryTest.groovy b/src/test/groovy/io/jsonwebtoken/impl/DefaultTextCodecFactoryTest.groovy index cb4b746f4..95ff6211e 100644 --- a/src/test/groovy/io/jsonwebtoken/impl/DefaultTextCodecFactoryTest.groovy +++ b/src/test/groovy/io/jsonwebtoken/impl/DefaultTextCodecFactoryTest.groovy @@ -26,6 +26,12 @@ import static org.junit.Assert.* @PrepareForTest([System.class]) class DefaultTextCodecFactoryTest { + @Test + void testGetSystemProperty() { + def factory = new DefaultTextCodecFactory() + assertNotNull factory.getSystemProperty("java.version") + } + @Test void testIsAndroidByVmName() { diff --git a/src/test/groovy/io/jsonwebtoken/impl/crypto/DefaultJwtSignatureValidatorTest.groovy b/src/test/groovy/io/jsonwebtoken/impl/crypto/DefaultJwtSignatureValidatorTest.groovy new file mode 100644 index 000000000..a86178f72 --- /dev/null +++ b/src/test/groovy/io/jsonwebtoken/impl/crypto/DefaultJwtSignatureValidatorTest.groovy @@ -0,0 +1,33 @@ +package io.jsonwebtoken.impl.crypto + +import io.jsonwebtoken.SignatureAlgorithm +import io.jsonwebtoken.codec.Decoder +import org.junit.Test +import static org.junit.Assert.* + +class DefaultJwtSignatureValidatorTest { + + @Test //TODO: remove this before 1.0 since it tests a deprecated method + @Deprecated + void testDeprecatedTwoArgCtor() { + + def alg = SignatureAlgorithm.HS256 + def key = MacProvider.generateKey(alg) + def validator = new DefaultJwtSignatureValidator(alg, key) + + assertNotNull validator.signatureValidator + assertSame Decoder.BASE64URL, validator.base64UrlDecoder + } + + @Test //TODO: remove this before 1.0 since it tests a deprecated method + @Deprecated + void testDeprecatedThreeArgCtor() { + + def alg = SignatureAlgorithm.HS256 + def key = MacProvider.generateKey(alg) + def validator = new DefaultJwtSignatureValidator(DefaultSignatureValidatorFactory.INSTANCE, alg, key) + + assertNotNull validator.signatureValidator + assertSame Decoder.BASE64URL, validator.base64UrlDecoder + } +} diff --git a/src/test/groovy/io/jsonwebtoken/impl/crypto/DefaultJwtSignerTest.groovy b/src/test/groovy/io/jsonwebtoken/impl/crypto/DefaultJwtSignerTest.groovy new file mode 100644 index 000000000..7dd1e97db --- /dev/null +++ b/src/test/groovy/io/jsonwebtoken/impl/crypto/DefaultJwtSignerTest.groovy @@ -0,0 +1,34 @@ +package io.jsonwebtoken.impl.crypto + +import io.jsonwebtoken.SignatureAlgorithm +import io.jsonwebtoken.codec.Encoder +import org.junit.Test + +import static org.junit.Assert.* + +class DefaultJwtSignerTest { + + @Test //TODO: remove this before 1.0 since it tests a deprecated method + @Deprecated //remove just before 1.0.0 release + void testDeprecatedTwoArgCtor() { + + def alg = SignatureAlgorithm.HS256 + def key = MacProvider.generateKey(alg) + def signer = new DefaultJwtSigner(alg, key) + + assertNotNull signer.signer + assertSame Encoder.BASE64URL, signer.base64UrlEncoder + } + + @Test //TODO: remove this before 1.0 since it tests a deprecated method + @Deprecated //remove just before 1.0.0 release + void testDeprecatedThreeArgCtor() { + + def alg = SignatureAlgorithm.HS256 + def key = MacProvider.generateKey(alg) + def signer = new DefaultJwtSigner(DefaultSignerFactory.INSTANCE, alg, key) + + assertNotNull signer.signer + assertSame Encoder.BASE64URL, signer.base64UrlEncoder + } +} diff --git a/src/test/groovy/io/jsonwebtoken/impl/crypto/EllipticCurveSignatureValidatorTest.groovy b/src/test/groovy/io/jsonwebtoken/impl/crypto/EllipticCurveSignatureValidatorTest.groovy index f23c38529..f9398f374 100644 --- a/src/test/groovy/io/jsonwebtoken/impl/crypto/EllipticCurveSignatureValidatorTest.groovy +++ b/src/test/groovy/io/jsonwebtoken/impl/crypto/EllipticCurveSignatureValidatorTest.groovy @@ -18,6 +18,7 @@ package io.jsonwebtoken.impl.crypto import io.jsonwebtoken.JwtException import io.jsonwebtoken.SignatureAlgorithm import io.jsonwebtoken.SignatureException +import io.jsonwebtoken.codec.Decoder import io.jsonwebtoken.impl.TextCodec import org.bouncycastle.jce.provider.BouncyCastleProvider import org.junit.Test @@ -64,13 +65,13 @@ class EllipticCurveSignatureValidatorTest { void ecdsaSignatureComplianceTest() { def fact = KeyFactory.getInstance("ECDSA", "BC"); def publicKey = "MIGbMBAGByqGSM49AgEGBSuBBAAjA4GGAAQASisgweVL1tAtIvfmpoqvdXF8sPKTV9YTKNxBwkdkm+/auh4pR8TbaIfsEzcsGUVv61DFNFXb0ozJfurQ59G2XcgAn3vROlSSnpbIvuhKrzL5jwWDTaYa5tVF1Zjwia/5HUhKBkcPuWGXg05nMjWhZfCuEetzMLoGcHmtvabugFrqsAg=" - def pub = fact.generatePublic(new X509EncodedKeySpec(TextCodec.BASE64.decode(publicKey))) + def pub = fact.generatePublic(new X509EncodedKeySpec(Decoder.BASE64.decode(publicKey))) def v = new EllipticCurveSignatureValidator(SignatureAlgorithm.ES512, pub) def verifier = { token -> def signatureStart = token.lastIndexOf('.') def withoutSignature = token.substring(0, signatureStart) def signature = token.substring(signatureStart + 1) - assert v.isValid(withoutSignature.getBytes("US-ASCII"), TextCodec.BASE64URL.decode(signature)), "Signature do not match that of other implementations" + assert v.isValid(withoutSignature.getBytes("US-ASCII"), Decoder.BASE64URL.decode(signature)), "Signature do not match that of other implementations" } //Test verification for token created using https://github.com/auth0/node-jsonwebtoken/tree/v7.0.1 verifier("eyJhbGciOiJFUzUxMiIsInR5cCI6IkpXVCJ9.eyJ0ZXN0IjoidGVzdCIsImlhdCI6MTQ2NzA2NTgyN30.Aab4x7HNRzetjgZ88AMGdYV2Ml7kzFbl8Ql2zXvBores7iRqm2nK6810ANpVo5okhHa82MQf2Q_Zn4tFyLDR9z4GAcKFdcAtopxq1h8X58qBWgNOc0Bn40SsgUc8wOX4rFohUCzEtnUREePsvc9EfXjjAH78WD2nq4tn-N94vf14SncQ") @@ -146,7 +147,7 @@ class EllipticCurveSignatureValidatorTest { @Test void edgeCaseSignatureToConcatLengthTest() { try { - def signature = TextCodec.BASE64.decode("MIEAAGg3OVb/ZeX12cYrhK3c07TsMKo7Kc6SiqW++4CAZWCX72DkZPGTdCv2duqlupsnZL53hiG3rfdOLj8drndCU+KHGrn5EotCATdMSLCXJSMMJoHMM/ZPG+QOHHPlOWnAvpC1v4lJb32WxMFNz1VAIWrl9Aa6RPG1GcjCTScKjvEE") + def signature = Decoder.BASE64.decode("MIEAAGg3OVb/ZeX12cYrhK3c07TsMKo7Kc6SiqW++4CAZWCX72DkZPGTdCv2duqlupsnZL53hiG3rfdOLj8drndCU+KHGrn5EotCATdMSLCXJSMMJoHMM/ZPG+QOHHPlOWnAvpC1v4lJb32WxMFNz1VAIWrl9Aa6RPG1GcjCTScKjvEE") EllipticCurveProvider.transcodeSignatureToConcat(signature, 132) fail() } catch (JwtException e) { @@ -157,7 +158,7 @@ class EllipticCurveSignatureValidatorTest { @Test void edgeCaseSignatureToConcatInvalidSignatureTest() { try { - def signature = TextCodec.BASE64.decode("MIGBAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA") + def signature = Decoder.BASE64.decode("MIGBAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA") EllipticCurveProvider.transcodeSignatureToConcat(signature, 132) fail() } catch (JwtException e) { @@ -168,7 +169,7 @@ class EllipticCurveSignatureValidatorTest { @Test void edgeCaseSignatureToConcatInvalidSignatureBranchTest() { try { - def signature = TextCodec.BASE64.decode("MIGBAD4AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA/AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA") + def signature = Decoder.BASE64.decode("MIGBAD4AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA/AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA") EllipticCurveProvider.transcodeSignatureToConcat(signature, 132) fail() } catch (JwtException e) { @@ -179,7 +180,7 @@ class EllipticCurveSignatureValidatorTest { @Test void edgeCaseSignatureToConcatInvalidSignatureBranch2Test() { try { - def signature = TextCodec.BASE64.decode("MIGBAj4AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA/AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA") + def signature = Decoder.BASE64.decode("MIGBAj4AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA/AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA") EllipticCurveProvider.transcodeSignatureToConcat(signature, 132) fail() } catch (JwtException e) {