Replace String/byte[] with (N)IO streams (#838)

Closes #837 

Replaced raw `String` and `byte[]` usages with `CharSequence`, `InputStream`/`OutputStream` and `CharBuffer`/`ByteBuffer` concepts where possible to eliminate unnecessary creation of intermediate byte arrays and/or temporary Strings.

-----
- Changed TokenizedJwt and TokenizedJwe interfaces and implementations to return CharSequences instead of Strings to avoid creating new Strings on the heap
- Changed internal Base64 implementation to work with a CharSequence instead of a raw char[] to reduce need to create new arrays on the heap
- Changed Base64Decoder generics signature from Decoder<String,byte[]> to Decoder<CharSequence,byte[]>
- Decoders.BASE64 and Decoders.BASE64URL now reflect Decoder<CharSequence,byte[]>
- Changed Strings#utf8 implementation to accept a CharSequence instead of a String
- Added new Strings#wrap to wrap a CharSequence into a CharBuffer if necessary
- Renamed not-yet-released JwtBuilder#serializer method with JwtBuilder#json
- Renamed not-yet-released JwtParserBuilder#deserializer method with JwtParserBuilder#json

-----
- Moved JwtDeserializer from io.jsonwebtoken.impl to io.jsonwebtoken.impl.io package, created two new subclass implementations for use with Jwks and JwkSets
- Renamed JwtDeserializer to JsonObjectDeserializer that defaults to throwing MalformedJwtException.  Added two subclasses, JwkDeserializer and JwkSetDeserializer that throws JWK and JWK Set-specific exceptions.

-----
Changed ParserBuilder#deserializer method name to ParserBuilder#jsonReader

-----
Removed all usages of Serializer#serialize and Deserializer#deserialize except for deprecated implementations.  All other usages now use InputStream/OutputStream concepts

-----
Added Jwks#json and Jwks#UNSAFE_JSON for assistance in serializing JWKs to JSON (test cases, README examples, etc)

-----
- Ensured Encoder and CompressionAlgorithm supported streams instead of just byte arrays
- Copied over necessary (Apache-licensed) code from Apache commons-codec to obtain Base64OutputStream and Base64InputStream capability for efficient encoding during compact JWT creation.  Hopefully this is temporary and we can strip out most if not all of this and modify our existing Base64.java class for simpler support since we have many less use cases than what commons-codec supports.  All implementations are now in the `impl` module only.

-----
Converted all DigestAlgorithms to utilize an InputStream for data instead of byte[]

-----
- Added JwtBuilder InputStream payload support: added JwtBuilder#content(InputStream), JwtBuilder#content(InputStream, String contentType), JwtBuilder#content(String, String contentType)
- Added CountingInputStream as a way to check and assert that b64/unencoded payload InputStreams cannot be empty.

------
Renamed Encoder/Decoder and CompressionAlgorithm 'wrap' methods to encode/decode/compress/decompress for better readability and to make clearer the intent of the method. Also to avoid name/text/search collisions with 'wrap' references.

-----
Renamed new JwtBuilder#encoder and JwtParserBuilder#decoder methods to JwtBuilder#b64Url and JwtParserBuilder#b64Url for shorter method chains

-----
- Updated AeadAlgorithm and its AeadRequest/AeadResult concepts to utilize Input/Output Streams
- Renamed InitializationVectorSupplier to IvSupplier (was verbose, and it's a new interface, and it's not commonly referenced in the API, so the extra verbosity isn't needed)
This commit is contained in:
lhazlewood 2023-09-27 16:31:11 -07:00 committed by GitHub
parent 7fcd652aea
commit b687ca5c72
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
179 changed files with 7283 additions and 1971 deletions

View File

@ -33,3 +33,39 @@ WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWIS
ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY
OF SUCH DAMAGE.
```
Additionally, the following classes were copied from the Apache Commons-Codec project, with further JJWT-specific
modifications:
* io.jsonwebtoken.impl.io.Base64Codec
* io.jsonwebtoken.impl.io.Base64InputStream
* io.jsonwebtoken.impl.io.Base64OutputStream
* io.jsonwebtoken.impl.io.BaseNCodec
* io.jsonwebtoken.impl.io.BaseNCodecInputStream
* io.jsonwebtoken.impl.io.BaseNCodecOutputStream
* io.jsonwebtoken.impl.io.CodecPolicy
Its attribution:
```
Apache Commons Codec
Copyright 2002-2023 The Apache Software Foundation
This product includes software developed at
The Apache Software Foundation (https://www.apache.org/).
```
Also, the following classes were copied from the Apache Commons-IO project, with further JJWT-specific modifications:
* io.jsonwebtoken.impl.io.FilteredInputStream
* io.jsonwebtoken.impl.io.FilteredOutputStream
* io.jsonwebtoken.impl.io.ClosedInputStream
* io.jsonwebtoken.impl.io.UncloseableInputStream
It's attribution:
```
Apache Commons IO
Copyright 2002-2023 The Apache Software Foundation
This product includes software developed at
The Apache Software Foundation (https://www.apache.org/).
```

View File

@ -164,7 +164,7 @@ JJWT is open source under the terms of the [Apache 2.0 License](http://www.apach
* Convenient and readable [fluent](http://en.wikipedia.org/wiki/Fluent_interface) interfaces, great for IDE
auto-completion to write code quickly
* Fully RFC specification compliant on all implemented functionality, tested against RFC-specified test vectors
* Stable implementation with over 1,500+ tests and enforced 100% test code coverage. Every single method, statement
* Stable implementation with almost 1,700 tests and enforced 100% test code coverage. Every single method, statement
and conditional branch variant in the entire codebase is tested and required to pass on every build.
* Creating, parsing and verifying digitally signed compact JWTs (aka JWSs) with all standard JWS algorithms:

View File

@ -144,14 +144,14 @@ public interface Claims extends Map<String, Object>, Identifiable {
String getId();
/**
* Returns the JWTs claim ({@code claimName}) value as a type {@code requiredType}, or {@code null} if not present.
* Returns the JWTs claim ({@code claimName}) value as a {@code requiredType} instance, or {@code null} if not
* present.
*
* <p>JJWT only converts simple String, Date, Long, Integer, Short and Byte types automatically. Anything more
* complex is expected to be already converted to your desired type by the JSON
* {@link io.jsonwebtoken.io.Deserializer Deserializer} implementation. You may specify a custom Deserializer for a
* JwtParser with the desired conversion configuration via the
* {@link JwtParserBuilder#deserializer deserializer} method.
* See <a href="https://github.com/jwtk/jjwt#custom-json-processor">custom JSON processor</a> for more
* complex is expected to be already converted to your desired type by the JSON parser. You may specify a custom
* JSON processor using the {@code JwtParserBuilder}'s
* {@link JwtParserBuilder#json(io.jsonwebtoken.io.Deserializer) json(Deserializer)} method. See the JJWT
* documentation on <a href="https://github.com/jwtk/jjwt#custom-json-processor">custom JSON processor</a>s for more
* information. If using Jackson, you can specify custom claim POJO types as described in
* <a href="https://github.com/jwtk/jjwt#json-jackson-custom-types">custom claim types</a>.
*
@ -160,6 +160,7 @@ public interface Claims extends Map<String, Object>, Identifiable {
* @param <T> the type of the value expected to be returned
* @return the JWT {@code claimName} value or {@code null} if not present.
* @throws RequiredTypeException throw if the claim value is not null and not of type {@code requiredType}
* @see <a href="https://github.com/jwtk/jjwt#json-support">JJWT JSON Support</a>
*/
<T> T get(String claimName, Class<T> requiredType);
}

View File

@ -30,7 +30,7 @@ public interface ClaimsMutator<T extends ClaimsMutator<T>> {
/**
* Sets the JWT <a href="https://www.rfc-editor.org/rfc/rfc7519.html#section-4.1.1">
* <code>iss</code></a> (issuer) value. A {@code null} value will remove the property from the JSON map.
* <code>iss</code></a> (issuer) claim. A {@code null} value will remove the property from the JSON Claims map.
*
* @param iss the JWT {@code iss} value or {@code null} to remove the property from the JSON map.
* @return the {@code Claims} instance for method chaining.
@ -42,7 +42,7 @@ public interface ClaimsMutator<T extends ClaimsMutator<T>> {
/**
* Sets the JWT <a href="https://www.rfc-editor.org/rfc/rfc7519.html#section-4.1.1">
* <code>iss</code></a> (issuer) value. A {@code null} value will remove the property from the JSON map.
* <code>iss</code></a> (issuer) claim. A {@code null} value will remove the property from the JSON Claims map.
*
* @param iss the JWT {@code iss} value or {@code null} to remove the property from the JSON map.
* @return the {@code Claims} instance for method chaining.
@ -52,7 +52,7 @@ public interface ClaimsMutator<T extends ClaimsMutator<T>> {
/**
* Sets the JWT <a href="https://www.rfc-editor.org/rfc/rfc7519.html#section-4.1.2">
* <code>sub</code></a> (subject) value. A {@code null} value will remove the property from the JSON map.
* <code>sub</code></a> (subject) claim. A {@code null} value will remove the property from the JSON Claims map.
*
* @param sub the JWT {@code sub} value or {@code null} to remove the property from the JSON map.
* @return the {@code Claims} instance for method chaining.
@ -64,7 +64,7 @@ public interface ClaimsMutator<T extends ClaimsMutator<T>> {
/**
* Sets the JWT <a href="https://www.rfc-editor.org/rfc/rfc7519.html#section-4.1.2">
* <code>sub</code></a> (subject) value. A {@code null} value will remove the property from the JSON map.
* <code>sub</code></a> (subject) claim. A {@code null} value will remove the property from the JSON Claims map.
*
* @param sub the JWT {@code sub} value or {@code null} to remove the property from the JSON map.
* @return the {@code Claims} instance for method chaining.
@ -74,7 +74,7 @@ public interface ClaimsMutator<T extends ClaimsMutator<T>> {
/**
* Sets the JWT <a href="https://www.rfc-editor.org/rfc/rfc7519.html#section-4.1.3"><code>aud</code> (audience)
* Claim</a> as <em>a single String, <b>NOT</b> a String array</em>. This method exists only for producing
* claim</a> as <em>a single String, <b>NOT</b> a String array</em>. This method exists only for producing
* JWTs sent to legacy recipients that are unable to interpret the {@code aud} value as a JSON String Array; it is
* strongly recommended to avoid calling this method whenever possible and favor the
* {@link #audience(String)} or {@link #audience(Collection)} methods instead, as they ensure a single deterministic
@ -135,7 +135,8 @@ public interface ClaimsMutator<T extends ClaimsMutator<T>> {
/**
* Sets the JWT <a href="https://www.rfc-editor.org/rfc/rfc7519.html#section-4.1.4">
* <code>exp</code></a> (expiration) timestamp. A {@code null} value will remove the property from the JSON map.
* <code>exp</code></a> (expiration) timestamp claim. A {@code null} value will remove the property from the
* JSON Claims map.
*
* <p>A JWT obtained after this timestamp should not be used.</p>
*
@ -149,7 +150,8 @@ public interface ClaimsMutator<T extends ClaimsMutator<T>> {
/**
* Sets the JWT <a href="https://www.rfc-editor.org/rfc/rfc7519.html#section-4.1.4">
* <code>exp</code></a> (expiration) timestamp. A {@code null} value will remove the property from the JSON map.
* <code>exp</code></a> (expiration) timestamp claim. A {@code null} value will remove the property from the
* JSON Claims map.
*
* <p>A JWT obtained after this timestamp should not be used.</p>
*
@ -161,7 +163,8 @@ public interface ClaimsMutator<T extends ClaimsMutator<T>> {
/**
* Sets the JWT <a href="https://www.rfc-editor.org/rfc/rfc7519.html#section-4.1.5">
* <code>nbf</code></a> (not before) timestamp. A {@code null} value will remove the property from the JSON map.
* <code>nbf</code></a> (not before) timestamp claim. A {@code null} value will remove the property from the
* JSON Claims map.
*
* <p>A JWT obtained before this timestamp should not be used.</p>
*
@ -175,7 +178,8 @@ public interface ClaimsMutator<T extends ClaimsMutator<T>> {
/**
* Sets the JWT <a href="https://www.rfc-editor.org/rfc/rfc7519.html#section-4.1.5">
* <code>nbf</code></a> (not before) timestamp. A {@code null} value will remove the property from the JSON map.
* <code>nbf</code></a> (not before) timestamp claim. A {@code null} value will remove the property from the
* JSON Claims map.
*
* <p>A JWT obtained before this timestamp should not be used.</p>
*
@ -187,7 +191,8 @@ public interface ClaimsMutator<T extends ClaimsMutator<T>> {
/**
* Sets the JWT <a href="https://www.rfc-editor.org/rfc/rfc7519.html#section-4.1.6">
* <code>iat</code></a> (issued at) timestamp. A {@code null} value will remove the property from the JSON map.
* <code>iat</code></a> (issued at) timestamp claim. A {@code null} value will remove the property from the
* JSON Claims map.
*
* <p>The value is the timestamp when the JWT was created.</p>
*
@ -201,7 +206,8 @@ public interface ClaimsMutator<T extends ClaimsMutator<T>> {
/**
* Sets the JWT <a href="https://www.rfc-editor.org/rfc/rfc7519.html#section-4.1.6">
* <code>iat</code></a> (issued at) timestamp. A {@code null} value will remove the property from the JSON map.
* <code>iat</code></a> (issued at) timestamp claim. A {@code null} value will remove the property from the
* JSON Claims map.
*
* <p>The value is the timestamp when the JWT was created.</p>
*
@ -213,7 +219,7 @@ public interface ClaimsMutator<T extends ClaimsMutator<T>> {
/**
* Sets the JWT <a href="https://www.rfc-editor.org/rfc/rfc7519.html#section-4.1.7">
* <code>jti</code></a> (JWT ID) value. A {@code null} value will remove the property from the JSON map.
* <code>jti</code></a> (JWT ID) claim. A {@code null} value will remove the property from the JSON Claims map.
*
* <p>This value is a CaSe-SenSiTiVe unique identifier for the JWT. If specified, this value MUST be assigned in a
* manner that ensures that there is a negligible probability that the same value will be accidentally
@ -229,7 +235,7 @@ public interface ClaimsMutator<T extends ClaimsMutator<T>> {
/**
* Sets the JWT <a href="https://www.rfc-editor.org/rfc/rfc7519.html#section-4.1.7">
* <code>jti</code></a> (JWT ID) value. A {@code null} value will remove the property from the JSON map.
* <code>jti</code></a> (JWT ID) claim. A {@code null} value will remove the property from the JSON Claims map.
*
* <p>This value is a CaSe-SenSiTiVe unique identifier for the JWT. If specified, this value MUST be assigned in a
* manner that ensures that there is a negligible probability that the same value will be accidentally

View File

@ -46,4 +46,25 @@ public interface CompressionCodec extends CompressionAlgorithm {
@SuppressWarnings("DeprecatedIsStillUsed")
@Deprecated
String getAlgorithmName();
}
/**
* Compresses the specified byte array, returning the compressed byte array result.
*
* @param content bytes to compress
* @return compressed bytes
* @throws CompressionException if the specified byte array cannot be compressed.
*/
@Deprecated
byte[] compress(byte[] content) throws CompressionException;
/**
* Decompresses the specified compressed byte array, returning the decompressed byte array result. The
* specified byte array must already be in compressed form.
*
* @param compressed compressed bytes
* @return decompressed bytes
* @throws CompressionException if the specified byte array cannot be decompressed.
*/
@Deprecated
byte[] decompress(byte[] compressed) throws CompressionException;
}

View File

@ -31,6 +31,8 @@ import io.jsonwebtoken.security.WeakKeyException;
import io.jsonwebtoken.security.X509Builder;
import javax.crypto.SecretKey;
import java.io.InputStream;
import java.io.OutputStream;
import java.security.Key;
import java.security.PrivateKey;
import java.security.Provider;
@ -149,25 +151,13 @@ public interface JwtBuilder extends ClaimsMutator<JwtBuilder> {
JwtBuilder setHeaderParam(String name, Object value);
/**
* Sets the JWT payload to the string's UTF-8-encoded bytes. It is strongly recommended to also set the
* {@link BuilderHeader#contentType(String) contentType} header value so the JWT recipient may inspect that value to
* determine how to convert the byte array to the final data type as desired. In this case, consider using
* {@link #content(byte[], String)} instead.
*
* <p>This is a wrapper method for:</p>
* <blockquote><pre>s
* {@link #content(byte[]) setPayload}(payload.getBytes(StandardCharsets.UTF_8));</pre></blockquote>
*
* <p>If you want the JWT payload to be JSON, use the {@link #claims()} method instead.</p>
*
* <p>This method is mutually exclusive of the {@link #claims()} and {@link #claim(String, Object)}
* methods. Either {@code claims} or {@code content}/{@code payload} method variants may be used, but not both.</p>
* Since JJWT JJWT_RELEASE_VERSION, this is an alias for {@link #content(String)}. This method will be removed
* before the 1.0 release.
*
* @param payload the string used to set UTF-8-encoded bytes as the JWT payload.
* @return the builder for method chaining.
* @see #content(byte[])
* @see #content(byte[], String)
* @deprecated since JJWT_RELEASE VERSION in favor of {@link #content(byte[])} or {@link #content(byte[], String)}
* @see #content(String)
* @deprecated since JJWT_RELEASE VERSION in favor of {@link #content(String)}
* because both Claims and Content are technically 'payloads', so this method name is misleading. This method will
* be removed before the 1.0 release.
*/
@ -176,14 +166,18 @@ public interface JwtBuilder extends ClaimsMutator<JwtBuilder> {
JwtBuilder setPayload(String payload);
/**
* Sets the JWT payload to be the specified string's UTF-8 bytes.
* Sets the JWT payload to be the specified string's UTF-8 bytes. This is a convenience method semantically
* equivalent to calling:
*
* <blockquote><pre>
* {@link #content(byte[]) content}(payload.getBytes(StandardCharsets.UTF_8))</pre></blockquote>
*
* <p><b>Content Type Recommendation</b></p>
*
* <p>Unless you are confident that the JWT recipient will <em>always</em> know how to use the string
* without additional metadata, it is strongly recommended to use the {@link #content(byte[], String)} method
* instead of this one. That method ensures that a JWT recipient can inspect the {@code cty} header to know
* how to handle the content without ambiguity.</p>
* <p>Unless you are confident that the JWT recipient will <em>always</em> know to convert the payload bytes
* to a UTF-8 string without additional metadata, it is strongly recommended to use the
* {@link #content(String, String)} method instead of this one. That method ensures that a JWT recipient can
* inspect the {@code cty} header to know how to handle the payload bytes without ambiguity.</p>
*
* <p><b>Mutually Exclusive Claims and Content</b></p>
*
@ -193,20 +187,30 @@ public interface JwtBuilder extends ClaimsMutator<JwtBuilder> {
*
* @param content the content string to use for the JWT payload
* @return the builder for method chaining.
* @see #content(String, String)
* @see #content(byte[], String)
* @see #content(InputStream, String)
* @since JJWT_RELEASE_VERSION
*/
JwtBuilder content(String content);
/**
* Sets the JWT payload to be the specified content byte array.
* Sets the JWT payload to be the specified content byte array. This is a convenience method semantically
* equivalent to calling:
* <blockquote><pre>
* {@link #content(InputStream) content}(new ByteArrayInputStream(content))</pre></blockquote>
*
* <p><b>Content Type Recommendation</b></p>
*
* <p>Unless you are confident that the JWT recipient will <em>always</em> know how to use
* the given byte array without additional metadata, it is strongly recommended to use the
* {@link #content(byte[], String)} method instead of this one. That method ensures that a JWT recipient
* can inspect the {@code cty} header to know how to handle the byte array without ambiguity.</p>
* <p>Unless you are confident that the JWT recipient will <em>always</em> know how to use the payload bytes
* without additional metadata, it is strongly recommended to also set the
* {@link Header#getContentType() contentType} header. For example:</p>
*
* <blockquote><pre>
* content(bytes).{@link #header() header()}.{@link HeaderMutator#contentType(String) contentType(cty)}.{@link BuilderHeader#and() and()}</pre></blockquote>
*
* <p>This ensures a JWT recipient can inspect the {@code cty} header to know how to handle the payload bytes
* without ambiguity.</p>
*
* <p><b>Mutually Exclusive Claims and Content</b></p>
*
@ -222,15 +226,89 @@ public interface JwtBuilder extends ClaimsMutator<JwtBuilder> {
JwtBuilder content(byte[] content);
/**
* Sets the JWT payload to be the specified content byte array and also sets the
* {@link BuilderHeader#contentType(String) contentType} header value to a compact {@code cty} IANA Media Type
* identifier to indicate the data format of the byte array. The JWT recipient can inspect the
* {@code cty} value to determine how to convert the byte array to the final content type as desired.
* Sets the JWT payload to be the bytes in the specified content stream.
*
* <p><b>Content Type Recommendation</b></p>
*
* <p>Unless you are confident that the JWT recipient will <em>always</em> know how to use the payload bytes
* without additional metadata, it is strongly recommended to also set the
* {@link HeaderMutator#contentType(String) contentType} header. For example:</p>
*
* <p>This is a convenience method semantically equivalent to:</p>
* <blockquote><pre>
* {@link #header()}.{@link HeaderMutator#contentType(String) contentType(cty)}.{@link BuilderHeader#and() and()}
* {@link #content(byte[]) content(content)}</pre></blockquote>
* content(in).{@link #header() header()}.{@link HeaderMutator#contentType(String) contentType(cty)}.{@link BuilderHeader#and() and()}</pre></blockquote>
*
* <p>This ensures a JWT recipient can inspect the {@code cty} header to know how to handle the payload bytes
* without ambiguity.</p>
*
* <p><b>Mutually Exclusive Claims and Content</b></p>
*
* <p>This method is mutually exclusive of the {@link #claim(String, Object)} and {@link #claims()}
* methods. Either {@code claims} or {@code content} method variants may be used, but not both. If you want the
* JWT payload to be JSON claims, use the {@link #claim(String, Object)} or {@link #claims()} methods instead.</p>
*
* @param in the input stream containing the bytes to use as the JWT payload
* @return the builder for method chaining.
* @see #content(byte[], String)
* @since JJWT_RELEASE_VERSION
*/
JwtBuilder content(InputStream in);
/**
* Sets the JWT payload to be the specified String's UTF-8 bytes, and also sets the
* {@link HeaderMutator#contentType(String) contentType} header value to a compact {@code cty} IANA Media Type
* identifier to indicate the data format of the resulting byte array. The JWT recipient can inspect the
* {@code cty} value to determine how to convert the byte array to the final content type as desired. This is a
* convenience method semantically equivalent to:
*
* <blockquote><pre>
* {@link #content(String) content(content)}.{@link #header()}.{@link HeaderMutator#contentType(String) contentType(cty)}.{@link BuilderHeader#and() and()}</pre></blockquote>
*
* <p><b>Compact Media Type Identifier</b></p>
*
* <p>This method will automatically remove any <code><b>application/</b></code> prefix from the
* {@code cty} string if possible according to the rules defined in the last paragraph of
* <a href="https://www.rfc-editor.org/rfc/rfc7515.html#section-4.1.10">RFC 7517, Section 4.1.10</a>:</p>
*
* <blockquote><pre>
* To keep messages compact in common situations, it is RECOMMENDED that
* producers omit an "application/" prefix of a media type value in a
* "cty" Header Parameter when no other '/' appears in the media type
* value. A recipient using the media type value MUST treat it as if
* "application/" were prepended to any "cty" value not containing a
* '/'. For instance, a "cty" value of "example" SHOULD be used to
* represent the "application/example" media type, whereas the media
* type "application/example;part="1/2"" cannot be shortened to
* "example;part="1/2"".</pre></blockquote>
*
* <p>JJWT performs the reverse during JWT parsing: {@link Header#getContentType()} will automatically prepend the
* {@code application/} prefix if the parsed {@code cty} value does not contain a '<code>/</code>' character (as
* mandated by the RFC language above). This ensures application developers can use and read standard IANA Media
* Type identifiers without needing JWT-specific prefix conditional logic in application code.
* </p>
*
* <p><b>Mutually Exclusive Claims and Content</b></p>
*
* <p>This method is mutually exclusive of the {@link #claim(String, Object)} and {@link #claims()}
* methods. Either {@code claims} or {@code content} method variants may be used, but not both. If you want the
* JWT payload to be JSON claims, use the {@link #claim(String, Object)} or {@link #claims()} methods instead.</p>
*
* @param content the content byte array that will be the JWT payload. Cannot be null or empty.
* @param cty the content type (media type) identifier attributed to the byte array. Cannot be null or empty.
* @return the builder for method chaining.
* @throws IllegalArgumentException if either {@code content} or {@code cty} are null or empty.
* @since JJWT_RELEASE_VERSION
*/
JwtBuilder content(String content, String cty) throws IllegalArgumentException;
/**
* Sets the JWT payload to be the specified byte array, and also sets the
* {@link HeaderMutator#contentType(String) contentType} header value to a compact {@code cty} IANA Media Type
* identifier to indicate the data format of the byte array. The JWT recipient can inspect the
* {@code cty} value to determine how to convert the byte array to the final content type as desired. This is a
* convenience method semantically equivalent to:
*
* <blockquote><pre>
* {@link #content(byte[]) content(content)}.{@link #header()}.{@link HeaderMutator#contentType(String) contentType(cty)}.{@link BuilderHeader#and() and()}</pre></blockquote>
*
* <p><b>Compact Media Type Identifier</b></p>
*
@ -268,6 +346,53 @@ public interface JwtBuilder extends ClaimsMutator<JwtBuilder> {
*/
JwtBuilder content(byte[] content, String cty) throws IllegalArgumentException;
/**
* Sets the JWT payload to be the specified content byte stream and also sets the
* {@link BuilderHeader#contentType(String) contentType} header value to a compact {@code cty} IANA Media Type
* identifier to indicate the data format of the byte array. The JWT recipient can inspect the
* {@code cty} value to determine how to convert the byte array to the final content type as desired. This is a
* convenience method semantically equivalent to:
*
* <blockquote><pre>
* {@link #content(InputStream) content(content)}.{@link #header()}.{@link HeaderMutator#contentType(String) contentType(cty)}.{@link BuilderHeader#and() and()}</pre></blockquote>
*
* <p><b>Compact Media Type Identifier</b></p>
*
* <p>This method will automatically remove any <code><b>application/</b></code> prefix from the
* {@code cty} string if possible according to the rules defined in the last paragraph of
* <a href="https://www.rfc-editor.org/rfc/rfc7515.html#section-4.1.10">RFC 7517, Section 4.1.10</a>:</p>
*
* <blockquote><pre>
* To keep messages compact in common situations, it is RECOMMENDED that
* producers omit an "application/" prefix of a media type value in a
* "cty" Header Parameter when no other '/' appears in the media type
* value. A recipient using the media type value MUST treat it as if
* "application/" were prepended to any "cty" value not containing a
* '/'. For instance, a "cty" value of "example" SHOULD be used to
* represent the "application/example" media type, whereas the media
* type "application/example;part="1/2"" cannot be shortened to
* "example;part="1/2"".</pre></blockquote>
*
* <p>JJWT performs the reverse during JWT parsing: {@link Header#getContentType()} will automatically prepend the
* {@code application/} prefix if the parsed {@code cty} value does not contain a '<code>/</code>' character (as
* mandated by the RFC language above). This ensures application developers can use and read standard IANA Media
* Type identifiers without needing JWT-specific prefix conditional logic in application code.
* </p>
*
* <p><b>Mutually Exclusive Claims and Content</b></p>
*
* <p>This method is mutually exclusive of the {@link #claim(String, Object)} and {@link #claims()}
* methods. Either {@code claims} or {@code content} method variants may be used, but not both. If you want the
* JWT payload to be JSON claims, use the {@link #claim(String, Object)} or {@link #claims()} methods instead.</p>
*
* @param content the content byte array that will be the JWT payload. Cannot be null.
* @param cty the content type (media type) identifier attributed to the byte array. Cannot be null or empty.
* @return the builder for method chaining.
* @throws IllegalArgumentException if either {@code content} or {@code cty} are null or empty.
* @since JJWT_RELEASE_VERSION
*/
JwtBuilder content(InputStream content, String cty) throws IllegalArgumentException;
/**
* Returns the JWT {@code Claims} payload to modify as desired. When finished, callers may
* return to {@code JwtBuilder} configuration via the {@link BuilderClaims#and() and()} method.
@ -295,24 +420,27 @@ public interface JwtBuilder extends ClaimsMutator<JwtBuilder> {
BuilderClaims claims();
/**
* Sets (and replaces) the JWT Claims payload with the specified name/value pairs. If you do not want the JWT
* payload to be JSON claims and instead want it to be a byte array for any content, use the
* {@link #content(byte[])} or {@link #content(byte[], String)} methods instead.
* Replaces the JWT Claims payload with the specified name/value pairs. This is an alias for:
* <blockquote><pre>
* {@link #claims()}.{@link MapMutator#empty() empty()}.{@link MapMutator#add(Map) add(claims)}.{@link BuilderClaims#and() and()}</pre></blockquote>
*
* <p>The content and claims properties are mutually exclusive - only one of the two may be used.</p>
* <p>The {@code content} and {@code claims} properties are mutually exclusive - only one of the two variants
* may be used.</p>
*
* @param claims the JWT Claims to be set as the JWT payload.
* @return the builder for method chaining.
* @deprecated since JJWT_RELEASE_VERSION in favor of the more modern builder-style {@link #claims()} method.
* @see #claims()
* @see #content(String)
* @see #content(byte[])
* @see #content(InputStream)
* @deprecated since JJWT_RELEASE_VERSION in favor of using the {@link #claims()} builder.
*/
@SuppressWarnings("DeprecatedIsStillUsed")
@Deprecated
JwtBuilder setClaims(Map<String, ?> claims);
/**
* Adds/appends all given name/value pairs to the JSON Claims in the payload.
* <p>
* This is a convenience wrapper for:
* Adds/appends all given name/value pairs to the JSON Claims in the payload. This is an alias for:
*
* <blockquote><pre>
* {@link #claims()}.{@link MapMutator#add(Map) add(claims)}.{@link BuilderClaims#and() and()}</pre></blockquote>
@ -332,7 +460,7 @@ public interface JwtBuilder extends ClaimsMutator<JwtBuilder> {
/**
* Sets a JWT claim, overwriting any existing claim with the same name. A {@code null} or empty
* value will remove the claim entirely. This is a convenience wrapper for:
* value will remove the claim entirely. This is a convenience alias for:
* <blockquote><pre>
* {@link #claims()}.{@link MapMutator#add(Object, Object) add(name, value)}.{@link BuilderClaims#and() and()}</pre></blockquote>
*
@ -346,7 +474,7 @@ public interface JwtBuilder extends ClaimsMutator<JwtBuilder> {
/**
* Adds all given name/value pairs to the JSON Claims in the payload, overwriting any existing claims
* with the same names. If any name has a {@code null} or empty value, that claim will be removed from the
* Claims. This is a convenience wrapper for:
* Claims. This is a convenience alias for:
* <blockquote><pre>
* {@link #claims()}.{@link MapMutator#add(Map) add(claims)}.{@link BuilderClaims#and() and()}</pre></blockquote>
*
@ -360,7 +488,7 @@ public interface JwtBuilder extends ClaimsMutator<JwtBuilder> {
/**
* Sets the JWT Claims <a href="https://www.rfc-editor.org/rfc/rfc7519.html#section-4.1.1">
* <code>iss</code></a> (issuer) value. A {@code null} value will remove the property from the Claims.
* <code>iss</code></a> (issuer) claim. A {@code null} value will remove the property from the Claims.
* This is a convenience wrapper for:
* <blockquote><pre>
* {@link #claims()}.{@link ClaimsMutator#issuer(String) issuer(iss)}.{@link BuilderClaims#and() and()}</pre></blockquote>
@ -374,7 +502,7 @@ public interface JwtBuilder extends ClaimsMutator<JwtBuilder> {
/**
* Sets the JWT Claims <a href="https://www.rfc-editor.org/rfc/rfc7519.html#section-4.1.2">
* <code>sub</code></a> (subject) value. A {@code null} value will remove the property from the Claims.
* <code>sub</code></a> (subject) claim. A {@code null} value will remove the property from the Claims.
* This is a convenience wrapper for:
* <blockquote><pre>
* {@link #claims()}.{@link ClaimsMutator#subject(String) subject(sub)}.{@link BuilderClaims#and() and()}</pre></blockquote>
@ -388,7 +516,7 @@ public interface JwtBuilder extends ClaimsMutator<JwtBuilder> {
/**
* Sets the JWT Claims <a href="https://www.rfc-editor.org/rfc/rfc7519.html#section-4.1.3">
* <code>aud</code></a> (audience) value. A {@code null} value will remove the property from the Claims.
* <code>aud</code></a> (audience) claim. A {@code null} value will remove the property from the Claims.
* This is a convenience wrapper for:
* <blockquote><pre>
* {@link #claims()}.{@link ClaimsMutator#audience(String) audience(aud)}.{@link BuilderClaims#and() and()}</pre></blockquote>
@ -402,7 +530,7 @@ public interface JwtBuilder extends ClaimsMutator<JwtBuilder> {
/**
* Sets the JWT Claims <a href="https://www.rfc-editor.org/rfc/rfc7519.html#section-4.1.4">
* <code>exp</code></a> (expiration) value. A {@code null} value will remove the property from the Claims.
* <code>exp</code></a> (expiration) claim. A {@code null} value will remove the property from the Claims.
*
* <p>A JWT obtained after this timestamp should not be used.</p>
*
@ -419,7 +547,7 @@ public interface JwtBuilder extends ClaimsMutator<JwtBuilder> {
/**
* Sets the JWT Claims <a href="https://www.rfc-editor.org/rfc/rfc7519.html#section-4.1.5">
* <code>nbf</code></a> (not before) value. A {@code null} value will remove the property from the Claims.
* <code>nbf</code></a> (not before) claim. A {@code null} value will remove the property from the Claims.
*
* <p>A JWT obtained before this timestamp should not be used.</p>
*
@ -432,11 +560,11 @@ public interface JwtBuilder extends ClaimsMutator<JwtBuilder> {
*/
@Override
// for better/targeted JavaDoc
JwtBuilder setNotBefore(Date nbf);
JwtBuilder notBefore(Date nbf);
/**
* Sets the JWT Claims <a href="https://www.rfc-editor.org/rfc/rfc7519.html#section-4.1.6">
* <code>iat</code></a> (issued at) value. A {@code null} value will remove the property from the Claims.
* <code>iat</code></a> (issued at) claim. A {@code null} value will remove the property from the Claims.
*
* <p>The value is the timestamp when the JWT was created.</p>
*
@ -453,7 +581,7 @@ public interface JwtBuilder extends ClaimsMutator<JwtBuilder> {
/**
* Sets the JWT Claims <a href="https://www.rfc-editor.org/rfc/rfc7519.html#section-4.1.7">
* <code>jti</code></a> (JWT ID) value. A {@code null} value will remove the property from the Claims.
* <code>jti</code></a> (JWT ID) claim. A {@code null} value will remove the property from the Claims.
*
* <p>The value is a CaSe-SenSiTiVe unique identifier for the JWT. If specified, this value MUST be assigned in a
* manner that ensures that there is a negligible probability that the same value will be accidentally
@ -840,25 +968,28 @@ public interface JwtBuilder extends ClaimsMutator<JwtBuilder> {
*
* @param base64UrlEncoder the encoder to use when Base64Url-encoding
* @return the builder for method chaining.
* @see #encoder(Encoder)
* @see #b64Url(Encoder)
* @since 0.10.0
* @deprecated since JJWT_RELEASE_VERSION in favor of the more modern builder-style
* {@link #encoder(Encoder)} method.
* @deprecated since JJWT_RELEASE_VERSION in favor of {@link #b64Url(Encoder)}.
*/
@SuppressWarnings("DeprecatedIsStillUsed")
@Deprecated
JwtBuilder base64UrlEncodeWith(Encoder<byte[], String> base64UrlEncoder);
/**
* Perform Base64Url encoding during {@link #compact() compaction} with the specified Encoder.
* Perform Base64Url encoding during {@link #compact() compaction} with the specified {@code OutputStream} Encoder.
* The Encoder's {@link Encoder#encode(Object) encode} method will be given a target {@code OutputStream} to
* wrap, and the resulting (wrapping) {@code OutputStream} will be used for writing, ensuring automatic
* Base64URL-encoding during write operations.
*
* <p>JJWT uses a spec-compliant encoder that works on all supported JDK versions, but you may call this method
* to specify a different encoder if necessar.</p>
* to specify a different stream encoder if desired.</p>
*
* @param encoder the encoder to use when Base64Url-encoding
* @return the builder for method chaining.
* @since JJWT_RELEASE_VERSION
*/
JwtBuilder encoder(Encoder<byte[], String> encoder);
JwtBuilder b64Url(Encoder<OutputStream, OutputStream> encoder);
/**
* Enables <a href="https://datatracker.ietf.org/doc/html/rfc7797">RFC 7797: JSON Web Signature (JWS)
@ -885,9 +1016,9 @@ public interface JwtBuilder extends ClaimsMutator<JwtBuilder> {
* @param serializer the serializer to use when converting Map objects to JSON strings.
* @return the builder for method chaining.
* @since 0.10.0
* @deprecated since JJWT_RELEASE_VERSION in favor of the more modern builder-style
* {@link #serializer(Serializer)} method.
* @deprecated since JJWT_RELEASE_VERSION in favor of {@link #json(Serializer)}
*/
@SuppressWarnings("DeprecatedIsStillUsed")
@Deprecated
JwtBuilder serializeToJsonWith(Serializer<Map<String, ?>> serializer);
@ -895,15 +1026,15 @@ public interface JwtBuilder extends ClaimsMutator<JwtBuilder> {
* Perform Map-to-JSON serialization with the specified Serializer. This is used by the builder to convert
* JWT/JWS/JWE headers and Claims Maps to JSON strings as required by the JWT specification.
*
* <p>If this method is not called, JJWT will use whatever serializer it can find at runtime, checking for the
* <p>If this method is not called, JJWT will use whatever Serializer it can find at runtime, checking for the
* presence of well-known implementations such Jackson, Gson, and org.json. If one of these is not found
* in the runtime classpath, an exception will be thrown when the {@link #compact()} method is invoked.</p>
*
* @param serializer the serializer to use when converting Map objects to JSON strings.
* @param serializer the Serializer to use when converting Map objects to JSON strings.
* @return the builder for method chaining.
* @since JJWT_RELEASE_VERSION
*/
JwtBuilder serializer(Serializer<Map<String, ?>> serializer);
JwtBuilder json(Serializer<Map<String, ?>> serializer);
/**
* Actually builds the JWT and serializes it to a compact, URL-safe string according to the

View File

@ -18,6 +18,8 @@ package io.jsonwebtoken;
import io.jsonwebtoken.security.SecurityException;
import io.jsonwebtoken.security.SignatureException;
import java.io.InputStream;
/**
* A parser for reading JWT strings, used to convert them into a {@link Jwt} object representing the expanded JWT.
*
@ -217,10 +219,86 @@ public interface JwtParser {
Jws<byte[]> parseContentJws(String jws) throws UnsupportedJwtException, MalformedJwtException, SignatureException,
SecurityException, IllegalArgumentException;
/**
* Parses a JWS known to use the
* <a href="https://datatracker.ietf.org/doc/html/rfc7797">RFC 7797: JSON Web Signature (JWS) Unencoded Payload
* Option</a>, using the specified {@code unencodedPayload} for signature verification.
*
* <p><b>Unencoded Non-Detached Payload</b></p>
* <p>Note that if the JWS contains a valid unencoded Payload string (what RFC 7797 calls an
* &quot;<a href="https://datatracker.ietf.org/doc/html/rfc7797#section-5.2">unencoded non-detached
* payload</a>&quot;, the {@code unencodedPayload} method argument will be ignored, as the JWS already includes
* the payload content necessary for signature verification.</p>
*
* @param jws the Unencoded Payload JWS to parse.
* @param unencodedPayload the JWS's associated required unencoded payload used for signature verification.
* @return the parsed Unencoded Payload.
*/
Jws<byte[]> parseContentJws(String jws, byte[] unencodedPayload);
/**
* Parses a JWS known to use the
* <a href="https://datatracker.ietf.org/doc/html/rfc7797">RFC 7797: JSON Web Signature (JWS) Unencoded Payload
* Option</a>, using the specified {@code unencodedPayload} for signature verification.
*
* <p><b>Unencoded Non-Detached Payload</b></p>
* <p>Note that if the JWS contains a valid unencoded payload String (what RFC 7797 calls an
* &quot;<a href="https://datatracker.ietf.org/doc/html/rfc7797#section-5.2">unencoded non-detached
* payload</a>&quot;, the {@code unencodedPayload} method argument will be ignored, as the JWS already includes
* the payload content necessary for signature verification and claims creation.</p>
*
* @param jws the Unencoded Payload JWS to parse.
* @param unencodedPayload the JWS's associated required unencoded payload used for signature verification.
* @return the parsed Unencoded Payload.
*/
Jws<Claims> parseClaimsJws(String jws, byte[] unencodedPayload);
/**
* Parses a JWS known to use the
* <a href="https://datatracker.ietf.org/doc/html/rfc7797">RFC 7797: JSON Web Signature (JWS) Unencoded Payload
* Option</a>, using the bytes from the specified {@code unencodedPayload} stream for signature verification.
*
* <p>Because it is not possible to know how large the {@code unencodedPayload} stream will be, the stream bytes
* will not be buffered in memory, ensuring the resulting {@link Jws} return value's {@link Jws#getPayload()}
* is always empty. This is generally not a concern since the caller already has access to the stream bytes and
* may obtain them independently before or after calling this method if they are needed otherwise.</p>
*
* <p><b>Unencoded Non-Detached Payload</b></p>
* <p>Note that if the JWS contains a valid unencoded payload String (what RFC 7797 calls an
* &quot;<a href="https://datatracker.ietf.org/doc/html/rfc7797#section-5.2">unencoded non-detached
* payload</a>&quot;, the {@code unencodedPayload} method argument will be ignored, as the JWS already includes
* the payload content necessary for signature verification. In this case the resulting {@link Jws} return
* value's {@link Jws#getPayload()} will contain the embedded payload String's UTF-8 bytes.</p>
*
* @param jws the Unencoded Payload JWS to parse.
* @param unencodedPayload the JWS's associated required unencoded payload used for signature verification.
* @return the parsed Unencoded Payload.
*/
Jws<byte[]> parseContentJws(String jws, InputStream unencodedPayload);
/**
* Parses a JWS known to use the
* <a href="https://datatracker.ietf.org/doc/html/rfc7797">RFC 7797: JSON Web Signature (JWS) Unencoded Payload
* Option</a>, using the bytes from the specified {@code unencodedPayload} stream for signature verification and
* {@link Claims} creation.
*
* <p><b>NOTE:</b> however, because calling this method indicates a completed
* {@link Claims} instance is desired, the specified {@code unencodedPayload} JSON stream will be fully
* read into a Claims instance. If this will be problematic for your application (perhaps if you expect extremely
* large Claims), it is recommended to use the {@link #parseContentJws(String, InputStream)} method instead.</p>
*
* <p><b>Unencoded Non-Detached Payload</b></p>
* <p>Note that if the JWS contains a valid unencoded Payload string (what RFC 7797 calls an
* &quot;<a href="https://datatracker.ietf.org/doc/html/rfc7797#section-5.2">unencoded non-detached
* payload</a>&quot;, the {@code unencodedPayload} method argument will be ignored, as the JWS already includes
* the payload content necessary for signature verification and Claims creation.</p>
*
* @param jws the Unencoded Payload JWS to parse.
* @param unencodedPayload the JWS's associated required unencoded payload used for signature verification.
* @return the parsed Unencoded Payload.
*/
Jws<Claims> parseClaimsJws(String jws, InputStream unencodedPayload);
/**
* Parses the specified compact serialized JWS string based on the builder's current configuration state and
* returns the resulting Claims JWS instance.

View File

@ -24,6 +24,7 @@ import io.jsonwebtoken.security.KeyAlgorithm;
import io.jsonwebtoken.security.SecureDigestAlgorithm;
import javax.crypto.SecretKey;
import java.io.InputStream;
import java.security.Key;
import java.security.PrivateKey;
import java.security.Provider;
@ -695,22 +696,25 @@ public interface JwtParserBuilder extends Builder<JwtParser> {
*
* @param base64UrlDecoder the decoder to use when Base64Url-decoding
* @return the parser builder for method chaining.
* @deprecated since JJWT_RELEASE_VERSION in favor of the shorter and more modern builder-style named
* {@link #decoder(Decoder)}. This method will be removed before the JJWT 1.0 release.
* @deprecated since JJWT_RELEASE_VERSION in favor of {@link #b64Url(Decoder)}. This method will be removed
* before the JJWT 1.0 release.
*/
@Deprecated
JwtParserBuilder base64UrlDecodeWith(Decoder<String, byte[]> base64UrlDecoder);
JwtParserBuilder base64UrlDecodeWith(Decoder<CharSequence, byte[]> base64UrlDecoder);
/**
* Perform Base64Url decoding with the specified Decoder
* Perform Base64Url decoding during parsing with the specified {@code InputStream} Decoder.
* The Decoder's {@link Decoder#decode(Object) decode} method will be given a source {@code InputStream} to
* wrap, and the resulting (wrapping) {@code InputStream} will be used for reading , ensuring automatic
* Base64URL-decoding during read operations.
*
* <p>JJWT uses a spec-compliant decoder that works on all supported JDK versions, but you may call this method
* to specify a different decoder if you desire.</p>
* to specify a different stream decoder if desired.</p>
*
* @param base64UrlDecoder the decoder to use when Base64Url-decoding
* @param base64UrlDecoder the stream decoder to use when Base64Url-decoding
* @return the parser builder for method chaining.
*/
JwtParserBuilder decoder(Decoder<String, byte[]> base64UrlDecoder);
JwtParserBuilder b64Url(Decoder<InputStream, InputStream> base64UrlDecoder);
/**
* Uses the specified deserializer to convert JSON Strings (UTF-8 byte arrays) into Java Map objects. This is
@ -724,26 +728,27 @@ public interface JwtParserBuilder extends Builder<JwtParser> {
*
* @param deserializer the deserializer to use when converting JSON Strings (UTF-8 byte arrays) into Map objects.
* @return the builder for method chaining.
* @deprecated since JJWT_RELEASE_VERSION in favor of the shorter and more modern builder-style named
* {@link #deserializer(Deserializer)}. This method will be removed before the JJWT 1.0 release.
* @deprecated since JJWT_RELEASE_VERSION in favor of {@link #json(Deserializer)}.
* This method will be removed before the JJWT 1.0 release.
*/
@Deprecated
JwtParserBuilder deserializeJsonWith(Deserializer<Map<String, ?>> deserializer);
/**
* Uses the specified deserializer to convert JSON Strings (UTF-8 byte arrays) into Java Map objects. This is
* used by the parser after Base64Url-decoding to convert JWT/JWS/JWT JSON headers and claims into Java Map
* objects.
* Uses the specified JSON {@link Deserializer} to deserialize JSON (UTF-8 byte streams) into Java Map objects.
* This is used by the parser after Base64Url-decoding to convert JWT/JWS/JWT headers and Claims into Java Map
* instances.
*
* <p>If this method is not called, JJWT will use whatever deserializer it can find at runtime, checking for the
* <p>If this method is not called, JJWT will use whatever Deserializer it can find at runtime, checking for the
* presence of well-known implementations such Jackson, Gson, and org.json. If one of these is not found
* in the runtime classpath, an exception will be thrown when one of the various {@code parse}* methods is
* invoked.</p>
*
* @param deserializer the deserializer to use when converting JSON Strings (UTF-8 byte arrays) into Map objects.
* @param deserializer the deserializer to use to deserialize JSON (UTF-8 byte streams) into Map instances.
* @return the builder for method chaining.
* @since JJWT_RELEASE_VERSION
*/
JwtParserBuilder deserializer(Deserializer<Map<String, ?>> deserializer);
JwtParserBuilder json(Deserializer<Map<String, ?>> deserializer);
/**
* Returns an immutable/thread-safe {@link JwtParser} created from the configuration from this JwtParserBuilder.

View File

@ -0,0 +1,73 @@
/*
* Copyright © 2023 jsonwebtoken.io
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package io.jsonwebtoken.io;
import io.jsonwebtoken.lang.Assert;
import java.io.ByteArrayInputStream;
import java.io.InputStream;
/**
* Convenient base class to use to implement {@link Deserializer}s, with subclasses only needing to implement
* {@link #doDeserialize(InputStream)}.
*
* @param <T> the type of object returned after deserialization
* @since JJWT_RELEASE_VERSION
*/
public abstract class AbstractDeserializer<T> implements Deserializer<T> {
/**
* EOF (End of File) marker, equal to {@code -1}.
*/
protected static final int EOF = -1;
private static final byte[] EMPTY_BYTES = new byte[0];
/**
* {@inheritDoc}
*/
@Override
public final T deserialize(byte[] bytes) throws DeserializationException {
bytes = bytes == null ? EMPTY_BYTES : bytes; // null safe
return deserialize(new ByteArrayInputStream(bytes));
}
/**
* {@inheritDoc}
*/
@Override
public final T deserialize(InputStream in) throws DeserializationException {
Assert.notNull(in, "InputStream argument cannot be null.");
try {
return doDeserialize(in);
} catch (Throwable t) {
if (t instanceof DeserializationException) {
throw (DeserializationException) t;
}
String msg = "Unable to deserialize: " + t.getMessage();
throw new DeserializationException(msg, t);
}
}
/**
* Reads the specified {@code InputStream} and returns the corresponding Java object.
*
* @param in the input stream to read
* @return the deserialized Java object
* @throws DeserializationException if there is a problem reading the stream or creating the expected Java object
*/
protected abstract T doDeserialize(InputStream in) throws Exception;
}

View File

@ -0,0 +1,69 @@
/*
* Copyright © 2023 jsonwebtoken.io
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package io.jsonwebtoken.io;
import io.jsonwebtoken.lang.Objects;
import java.io.ByteArrayOutputStream;
import java.io.OutputStream;
/**
* Convenient base class to use to implement {@link Serializer}s, with subclasses only needing to implement
* * {@link #doSerialize(Object, OutputStream)}.
*
* @param <T> the type of object to serialize
* @since JJWT_RELEASE_VERSION
*/
public abstract class AbstractSerializer<T> implements Serializer<T> {
/**
* {@inheritDoc}
*/
@Override
public final byte[] serialize(T t) throws SerializationException {
ByteArrayOutputStream out = new ByteArrayOutputStream();
serialize(t, out);
return out.toByteArray();
}
/**
* {@inheritDoc}
*/
@Override
public final void serialize(T t, OutputStream out) throws SerializationException {
try {
doSerialize(t, out);
} catch (Throwable e) {
if (e instanceof SerializationException) {
throw (SerializationException) e;
}
String msg = "Unable to serialize object of type " + Objects.nullSafeClassName(t) + ": " + e.getMessage();
throw new SerializationException(msg, e);
}
}
/**
* Converts the specified Java object into a formatted data byte stream, writing the bytes to the specified
* {@code out}put stream.
*
* @param t the object to convert to a byte stream
* @param out the stream to write to
* @throws SerializationException if there is a problem converting the object to a byte stream or writing the
* bytes to the {@code out}put stream.
* @since JJWT_RELEASE_VERSION
*/
protected abstract void doSerialize(T t, OutputStream out) throws Exception;
}

View File

@ -224,20 +224,21 @@ final class Base64 { //final and package-protected on purpose
}
/**
* Decodes a BASE64 encoded char array that is known to be reasonably well formatted. The preconditions are:<br>
* + The array must have a line length of 76 chars OR no line separators at all (one line).<br>
* Decodes a BASE64-encoded {@code CharSequence} that is known to be reasonably well formatted. The preconditions
* are:<br>
* + The sequence must have a line length of 76 chars OR no line separators at all (one line).<br>
* + Line separator must be "\r\n", as specified in RFC 2045
* + The array must not contain illegal characters within the encoded string<br>
* + The array CAN have illegal characters at the beginning and end, those will be dealt with appropriately.<br>
* + The sequence must not contain illegal characters within the encoded string<br>
* + The sequence CAN have illegal characters at the beginning and end, those will be dealt with appropriately.<br>
*
* @param sArr The source array. Length 0 will return an empty array. <code>null</code> will throw an exception.
* @param seq The source sequence. Length 0 will return an empty array. <code>null</code> will throw an exception.
* @return The decoded array of bytes. May be of length 0.
* @throws DecodingException on illegal input
*/
final byte[] decodeFast(char[] sArr) throws DecodingException {
byte[] decodeFast(CharSequence seq) throws DecodingException {
// Check special case
int sLen = sArr != null ? sArr.length : 0;
int sLen = seq != null ? seq.length() : 0;
if (sLen == 0) {
return new byte[0];
}
@ -245,19 +246,19 @@ final class Base64 { //final and package-protected on purpose
int sIx = 0, eIx = sLen - 1; // Start and end index after trimming.
// Trim illegal chars from start
while (sIx < eIx && IALPHABET[sArr[sIx]] < 0) {
while (sIx < eIx && IALPHABET[seq.charAt(sIx)] < 0) {
sIx++;
}
// Trim illegal chars from end
while (eIx > 0 && IALPHABET[sArr[eIx]] < 0) {
while (eIx > 0 && IALPHABET[seq.charAt(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 pad = seq.charAt(eIx) == '=' ? (seq.charAt(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 sepCnt = sLen > 76 ? (seq.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
@ -267,7 +268,7 @@ final class Base64 { //final and package-protected on purpose
for (int cc = 0, eLen = (len / 3) * 3; d < eLen; ) {
// Assemble three bytes into an int from four "valid" characters.
int i = ctoi(sArr[sIx++]) << 18 | ctoi(sArr[sIx++]) << 12 | ctoi(sArr[sIx++]) << 6 | ctoi(sArr[sIx++]);
int i = ctoi(seq.charAt(sIx++)) << 18 | ctoi(seq.charAt(sIx++)) << 12 | ctoi(seq.charAt(sIx++)) << 6 | ctoi(seq.charAt(sIx++));
// Add the bytes
dArr[d++] = (byte) (i >> 16);
@ -285,7 +286,7 @@ final class Base64 { //final and package-protected on purpose
// Decode last 1-3 bytes (incl '=') into 1-3 bytes
int i = 0;
for (int j = 0; sIx <= eIx - pad; j++) {
i |= ctoi(sArr[sIx++]) << (18 - j * 6);
i |= ctoi(seq.charAt(sIx++)) << (18 - j * 6);
}
for (int r = 16; d < len; r -= 8) {
@ -404,7 +405,7 @@ final class Base64 { //final and package-protected on purpose
}
}
// Check so that legal chars (including '=') are evenly divideable by 4 as specified in RFC 2045.
// Check so that legal chars (including '=') are evenly divisible by 4 as specified in RFC 2045.
if ((sLen - sepCnt) % 4 != 0) {
return null;
}
@ -447,7 +448,7 @@ final class Base64 { //final and package-protected on purpose
/*
* Decodes a BASE64 encoded byte array that is known to be resonably well formatted. The method is about twice as
* Decodes a BASE64 encoded byte array that is known to be reasonably well formatted. The method is about twice as
* fast as {@link #decode(byte[])}. The preconditions are:<br>
* + The array must have a line length of 76 chars OR no line separators at all (one line).<br>
* + Line separator must be "\r\n", as specified in RFC 2045
@ -533,7 +534,7 @@ final class Base64 { //final and package-protected on purpose
* little faster.
* @return A BASE64 encoded array. Never <code>null</code>.
*/
final String encodeToString(byte[] sArr, boolean lineSep) {
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));
}

View File

@ -23,7 +23,7 @@ import io.jsonwebtoken.lang.Assert;
*
* @since 0.10.0
*/
class Base64Decoder extends Base64Support implements Decoder<String, byte[]> {
class Base64Decoder extends Base64Support implements Decoder<CharSequence, byte[]> {
Base64Decoder() {
super(Base64.DEFAULT);
@ -34,8 +34,8 @@ class Base64Decoder extends Base64Support implements Decoder<String, byte[]> {
}
@Override
public byte[] decode(String s) throws DecodingException {
public byte[] decode(CharSequence s) throws DecodingException {
Assert.notNull(s, "String argument cannot be null");
return this.base64.decodeFast(s.toCharArray());
return this.base64.decodeFast(s);
}
}

View File

@ -26,7 +26,7 @@ import io.jsonwebtoken.lang.Assert;
class Base64Encoder extends Base64Support implements Encoder<byte[], String> {
Base64Encoder() {
super(Base64.DEFAULT);
this(Base64.DEFAULT);
}
Base64Encoder(Base64 base64) {

View File

@ -15,14 +15,15 @@
*/
package io.jsonwebtoken.io;
import io.jsonwebtoken.CompressionException;
import io.jsonwebtoken.Identifiable;
import io.jsonwebtoken.Jwts;
import java.io.InputStream;
import java.io.OutputStream;
import java.util.Collection;
/**
* Compresses and decompresses byte arrays.
* Compresses and decompresses byte streams.
*
* <p><b>&quot;zip&quot; identifier</b></p>
*
@ -44,21 +45,18 @@ import java.util.Collection;
public interface CompressionAlgorithm extends Identifiable {
/**
* Compresses the specified byte array, returning the compressed byte array result.
* Wraps the specified {@code OutputStream} to ensure any stream bytes are compressed as they are written.
*
* @param content bytes to compress
* @return compressed bytes
* @throws CompressionException if the specified byte array cannot be compressed.
* @param out the stream to wrap for compression
* @return the stream to use for writing
*/
byte[] compress(byte[] content) throws CompressionException;
OutputStream compress(OutputStream out);
/**
* Decompresses the specified compressed byte array, returning the decompressed byte array result. The
* specified byte array must already be in compressed form.
* Wraps the specified {@code InputStream} to ensure any stream bytes are decompressed as they are read.
*
* @param compressed compressed bytes
* @return decompressed bytes
* @throws CompressionException if the specified byte array cannot be decompressed.
* @param in the stream to wrap for decompression
* @return the stream to use for reading
*/
byte[] decompress(byte[] compressed) throws CompressionException;
InputStream decompress(InputStream in);
}

View File

@ -28,13 +28,13 @@ public final class Decoders {
* Very fast <a href="https://datatracker.ietf.org/doc/html/rfc4648#section-4">Base64</a> decoder guaranteed to
* work in all &gt;= Java 7 JDK and Android environments.
*/
public static final Decoder<String, byte[]> BASE64 = new ExceptionPropagatingDecoder<>(new Base64Decoder());
public static final Decoder<CharSequence, byte[]> BASE64 = new ExceptionPropagatingDecoder<>(new Base64Decoder());
/**
* Very fast <a href="https://datatracker.ietf.org/doc/html/rfc4648#section-5">Base64Url</a> decoder guaranteed to
* work in all &gt;= Java 7 JDK and Android environments.
*/
public static final Decoder<String, byte[]> BASE64URL = new ExceptionPropagatingDecoder<>(new Base64UrlDecoder());
public static final Decoder<CharSequence, byte[]> BASE64URL = new ExceptionPropagatingDecoder<>(new Base64UrlDecoder());
private Decoders() { //prevent instantiation
}

View File

@ -15,8 +15,10 @@
*/
package io.jsonwebtoken.io;
import java.io.InputStream;
/**
* A {@code Deserializer} is able to convert serialized data byte arrays into Java objects.
* A {@code Deserializer} is able to convert serialized byte streams into Java objects.
*
* @param <T> the type of object to be returned as a result of deserialization.
* @since 0.10.0
@ -28,7 +30,19 @@ public interface Deserializer<T> {
*
* @param bytes the formatted data byte array to convert
* @return the reconstituted Java object
* @throws DeserializationException if there is a problem converting the byte array to to an object.
* @throws DeserializationException if there is a problem converting the byte array to an object.
* @deprecated since JJWT_RELEASE_VERSION in favor of {@link #deserialize(InputStream)}
*/
@Deprecated
T deserialize(byte[] bytes) throws DeserializationException;
/**
* Reads the specified {@code InputStream} and returns the corresponding Java object.
*
* @param in the input stream to read
* @return the deserialized Java object
* @throws DeserializationException if there is a problem reading the stream or creating the expected Java object
* @since JJWT_RELEASE_VERSION
*/
T deserialize(InputStream in) throws DeserializationException;
}

View File

@ -40,15 +40,15 @@ public interface ParserBuilder<T, B extends ParserBuilder<T, B>> extends Builder
B provider(Provider provider);
/**
* Uses the specified deserializer to convert JSON Strings (UTF-8 byte arrays) into Java Map objects. The
* Uses the specified {@code Deserializer} to convert JSON Strings (UTF-8 byte streams) into Java Map objects. The
* resulting Maps are then used to construct respective JWT objects (JWTs, JWKs, etc).
*
* <p>If this method is not called, JJWT will use whatever deserializer it can find at runtime, checking for the
* <p>If this method is not called, JJWT will use whatever Deserializer it can find at runtime, checking for the
* presence of well-known implementations such as Jackson, Gson, and org.json. If one of these is not found
* in the runtime classpath, an exception will be thrown when the {@link #build()} method is called.
*
* @param deserializer the deserializer to use when converting JSON Strings (UTF-8 byte arrays) into Map objects.
* @param deserializer the Deserializer to use when converting JSON Strings (UTF-8 byte streams) into Map objects.
* @return the builder for method chaining.
*/
B deserializer(Deserializer<Map<String, ?>> deserializer);
B json(Deserializer<Map<String, ?>> deserializer);
}

View File

@ -15,8 +15,10 @@
*/
package io.jsonwebtoken.io;
import java.io.OutputStream;
/**
* A {@code Serializer} is able to convert a Java object into a formatted data byte array. It is expected this data
* A {@code Serializer} is able to convert a Java object into a formatted byte stream. It is expected this byte stream
* can be reconstituted back into a Java object with a matching {@link Deserializer}.
*
* @param <T> The type of object to serialize.
@ -25,12 +27,25 @@ package io.jsonwebtoken.io;
public interface Serializer<T> {
/**
* Convert the specified Java object into a formatted data byte array.
* Converts the specified Java object into a formatted data byte array.
*
* @param t the object to serialize
* @return the serialized byte array representing the specified object.
* @throws SerializationException if there is a problem converting the object to a byte array.
* @deprecated since JJWT_RELEASE_VERSION in favor of {@link #serialize(Object, OutputStream)}
*/
@Deprecated
byte[] serialize(T t) throws SerializationException;
/**
* Converts the specified Java object into a formatted data byte stream, writing the bytes to the specified
* {@code out}put stream.
*
* @param t the object to convert to a byte stream
* @param out the stream to write to
* @throws SerializationException if there is a problem converting the object to a byte stream or writing the
* bytes to the {@code out}put stream.
* @since JJWT_RELEASE_VERSION
*/
void serialize(T t, OutputStream out) throws SerializationException;
}

View File

@ -140,12 +140,13 @@ public final class Assert {
* be <code>null</code> and must contain at least one non-whitespace character.
* <pre class="code">Assert.hasText(name, "'name' must not be empty");</pre>
*
* @param text the String to check
* @param <T> the type of CharSequence
* @param text the CharSequence to check
* @param message the exception message to use if the assertion fails
* @return the string if it has text
* @return the CharSequence if it has text
* @see Strings#hasText
*/
public static String hasText(String text, String message) {
public static <T extends CharSequence> T hasText(T text, String message) {
if (!Strings.hasText(text)) {
throw new IllegalArgumentException(message);
}

View File

@ -17,6 +17,7 @@ package io.jsonwebtoken.lang;
import java.io.InputStream;
import java.lang.reflect.Constructor;
import java.lang.reflect.Field;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.net.URL;
@ -329,6 +330,20 @@ public final class Classes {
}
}
public static <T> T getFieldValue(Object instance, String fieldName, Class<T> fieldType) {
if (instance == null) return null;
try {
Field field = instance.getClass().getDeclaredField(fieldName);
field.setAccessible(true);
Object o = field.get(instance);
return fieldType.cast(o);
} catch (Throwable t) {
String msg = "Unable to read field " + instance.getClass().getName() +
"#" + fieldName + ": " + t.getMessage();
throw new IllegalStateException(msg, t);
}
}
/**
* @since 1.0
*/

View File

@ -16,6 +16,7 @@
package io.jsonwebtoken.lang;
import java.io.Closeable;
import java.io.Flushable;
import java.io.IOException;
import java.lang.reflect.Array;
import java.util.Arrays;
@ -1008,4 +1009,23 @@ public final class Objects {
}
}
}
/**
* Iterate over the specified {@link Flushable} instances, invoking
* {@link Flushable#flush()} on each one, ignoring any potential {@link IOException}s.
*
* @param flushables the flushables to flush.
* @since JJWT_RELEASE_VERSION
*/
public static void nullSafeFlush(Flushable... flushables) {
if (flushables == null) return;
for (Flushable flushable : flushables) {
if (flushable != null) {
try {
flushable.flush();
} catch (IOException ignored) {
}
}
}
}
}

View File

@ -15,6 +15,8 @@
*/
package io.jsonwebtoken.lang;
import java.nio.ByteBuffer;
import java.nio.CharBuffer;
import java.nio.charset.Charset;
import java.nio.charset.StandardCharsets;
import java.util.ArrayList;
@ -41,6 +43,8 @@ public final class Strings {
*/
public static final String EMPTY = "";
private static final CharBuffer EMPTY_BUF = CharBuffer.wrap(EMPTY);
private static final String FOLDER_SEPARATOR = "/";
private static final String WINDOWS_FOLDER_SEPARATOR = "\\";
@ -238,11 +242,14 @@ public final class Strings {
* @return the specified string's UTF-8 bytes, or {@code null} if the string is {@code null}.
* @since JJWT_RELEASE_VERSION
*/
public static byte[] utf8(String s) {
byte[] bytes = null;
if (s != null) {
bytes = s.getBytes(UTF_8);
}
public static byte[] utf8(CharSequence s) {
if (s == null) return null;
CharBuffer cb = s instanceof CharBuffer ? (CharBuffer) s : CharBuffer.wrap(s);
cb.mark();
ByteBuffer buf = UTF_8.encode(cb);
byte[] bytes = new byte[buf.remaining()];
buf.get(bytes);
cb.reset();
return bytes;
}
@ -257,6 +264,41 @@ public final class Strings {
return new String(utf8Bytes, UTF_8);
}
/**
* Returns {@code new String(asciiBytes, StandardCharsets.US_ASCII)}.
*
* @param asciiBytes US_ASCII bytes to use with the {@code String} constructor.
* @return {@code new String(asciiBytes, StandardCharsets.US_ASCII)}.
* @since JJWT_RELEASE_VERSION
*/
public static String ascii(byte[] asciiBytes) {
return new String(asciiBytes, StandardCharsets.US_ASCII);
}
public static byte[] ascii(CharSequence s) {
byte[] bytes = null;
if (s != null) {
CharBuffer cb = s instanceof CharBuffer ? (CharBuffer) s : CharBuffer.wrap(s);
ByteBuffer buf = StandardCharsets.US_ASCII.encode(cb);
bytes = new byte[buf.remaining()];
buf.get(bytes);
}
return bytes;
}
/**
* Returns a {@code CharBuffer} that wraps {@code seq}, or an empty buffer if {@code seq} is null. If
* {@code seq} is already a {@code CharBuffer}, it is returned unmodified.
*
* @param seq the {@code CharSequence} to wrap.
* @return a {@code CharBuffer} that wraps {@code seq}, or an empty buffer if {@code seq} is null.
*/
public static CharBuffer wrap(CharSequence seq) {
if (!hasLength(seq)) return EMPTY_BUF;
if (seq instanceof CharBuffer) return (CharBuffer) seq;
return CharBuffer.wrap(seq);
}
/**
* Returns a String representation (1s and 0s) of the specified byte.
*
@ -823,8 +865,7 @@ public final class Strings {
for (int i = 0; i < localePart.length(); i++) {
char ch = localePart.charAt(i);
if (ch != '_' && ch != ' ' && !Character.isLetterOrDigit(ch)) {
throw new IllegalArgumentException(
"Locale part \"" + localePart + "\" contains invalid characters");
throw new IllegalArgumentException("Locale part \"" + localePart + "\" contains invalid characters");
}
}
}
@ -1049,8 +1090,7 @@ public final class Strings {
* @return a <code>Properties</code> instance representing the array contents,
* or <code>null</code> if the array to process was <code>null</code> or empty
*/
public static Properties splitArrayElementsIntoProperties(
String[] array, String delimiter, String charsToDelete) {
public static Properties splitArrayElementsIntoProperties(String[] array, String delimiter, String charsToDelete) {
if (Objects.isEmpty(array)) {
return null;
@ -1109,8 +1149,7 @@ public final class Strings {
* @see java.lang.String#trim()
* @see #delimitedListToStringArray
*/
public static String[] tokenizeToStringArray(
String str, String delimiters, boolean trimTokens, boolean ignoreEmptyTokens) {
public static String[] tokenizeToStringArray(String str, String delimiters, boolean trimTokens, boolean ignoreEmptyTokens) {
if (str == null) {
return null;

View File

@ -19,6 +19,7 @@ import io.jsonwebtoken.Identifiable;
import io.jsonwebtoken.Jwts;
import javax.crypto.SecretKey;
import java.io.OutputStream;
/**
* A cryptographic algorithm that performs
@ -67,25 +68,24 @@ import javax.crypto.SecretKey;
public interface AeadAlgorithm extends Identifiable, KeyLengthSupplier, KeyBuilderSupplier<SecretKey, SecretKeyBuilder> {
/**
* Perform AEAD encryption with the plaintext represented by the specified {@code request}, returning the
* integrity-protected encrypted ciphertext result.
* Encrypts plaintext and signs any {@link AeadRequest#getAssociatedData() associated data}, placing the resulting
* ciphertext, initialization vector and authentication tag in the provided {@code result}.
*
* @param request the encryption request representing the plaintext to be encrypted, any additional
* integrity-protected data and the encryption key.
* @return the encryption result containing the ciphertext, and associated initialization vector and resulting
* authentication tag.
* @throws SecurityException if there is an encryption problem or authenticity cannot be guaranteed.
* @param req the encryption request representing the plaintext to be encrypted, any additional
* integrity-protected data and the encryption key.
* @param res the result to write ciphertext, initialization vector and AAD authentication tag (aka digest)
* @throws SecurityException if there is an encryption problem or AAD authenticity cannot be guaranteed.
*/
AeadResult encrypt(AeadRequest request) throws SecurityException;
void encrypt(AeadRequest req, AeadResult res) throws SecurityException;
/**
* Perform AEAD decryption with the ciphertext represented by the specific {@code request}, also verifying the
* integrity and authenticity of any associated data, returning the decrypted plaintext result.
* Decrypts ciphertext and authenticates any {@link DecryptAeadRequest#getAssociatedData() associated data},
* writing the decrypted plaintext to the provided {@code out}put stream.
*
* @param request the decryption request representing the ciphertext to be decrypted, any additional
* integrity-protected data, authentication tag, initialization vector, and the decryption key.
* @return the decryption result containing the plaintext
* integrity-protected data, authentication tag, initialization vector, and decryption key
* @param out the OutputStream for writing decrypted plaintext
* @throws SecurityException if there is a decryption problem or authenticity assertions fail.
*/
Message<byte[]> decrypt(DecryptAeadRequest request) throws SecurityException;
void decrypt(DecryptAeadRequest request, OutputStream out) throws SecurityException;
}

View File

@ -16,6 +16,7 @@
package io.jsonwebtoken.security;
import javax.crypto.SecretKey;
import java.io.InputStream;
/**
* A request to an {@link AeadAlgorithm} to perform authenticated encryption with a supplied symmetric
@ -25,5 +26,5 @@ import javax.crypto.SecretKey;
* @see AssociatedDataSupplier
* @since JJWT_RELEASE_VERSION
*/
public interface AeadRequest extends SecureRequest<byte[], SecretKey>, AssociatedDataSupplier {
public interface AeadRequest extends SecureRequest<InputStream, SecretKey>, AssociatedDataSupplier {
}

View File

@ -15,23 +15,39 @@
*/
package io.jsonwebtoken.security;
import java.io.OutputStream;
/**
* The result of authenticated encryption, providing access to the resulting {@link #getPayload() ciphertext},
* {@link #getDigest() AAD tag}, and {@link #getInitializationVector() initialization vector}. The AAD tag and
* initialization vector must be supplied with the ciphertext to decrypt.
*
* <p><b>AAD Tag</b></p>
*
* {@code AeadResult} inherits {@link DigestSupplier} which is a generic concept for supplying any digest. The digest
* in the case of AEAD is called an AAD tag, and it must in turn be supplied for verification during decryption.
*
* <p><b>Initialization Vector</b></p>
*
* All JWE-standard AEAD algorithms use a secure-random Initialization Vector for safe ciphertext creation, so
* {@code AeadResult} inherits {@link InitializationVectorSupplier} to make the generated IV available after
* encryption. This IV must in turn be supplied during decryption.
* The result of authenticated encryption, providing access to the ciphertext {@link #getOutputStream() output stream}
* and resulting {@link #setTag(byte[]) AAD tag} and {@link #setIv(byte[]) initialization vector}.
* The AAD tag and initialization vector must be supplied with the ciphertext to decrypt.
*
* @since JJWT_RELEASE_VERSION
*/
public interface AeadResult extends Message<byte[]>, DigestSupplier, InitializationVectorSupplier {
public interface AeadResult {
/**
* Returns the {@code OutputStream} the AeadAlgorithm will use to write the resulting ciphertext during
* encryption or plaintext during decryption.
*
* @return the {@code OutputStream} the AeadAlgorithm will use to write the resulting ciphertext during
* encryption or plaintext during decryption.
*/
OutputStream getOutputStream();
/**
* Sets the AEAD authentication tag.
*
* @param tag the AEAD authentication tag.
* @return the AeadResult for method chaining.
*/
AeadResult setTag(byte[] tag);
/**
* Sets the initialization vector used during encryption.
*
* @param iv the initialization vector used during encryption.
* @return the AeadResult for method chaining.
*/
AeadResult setIv(byte[] iv);
}

View File

@ -15,6 +15,8 @@
*/
package io.jsonwebtoken.security;
import java.io.InputStream;
/**
* Provides any &quot;associated data&quot; that must be integrity protected (but not encrypted) when performing
* <a href="https://en.wikipedia.org/wiki/Authenticated_encryption">AEAD encryption or decryption</a>.
@ -33,5 +35,5 @@ public interface AssociatedDataSupplier {
* <a href="https://en.wikipedia.org/wiki/Authenticated_encryption">AEAD encryption or decryption</a>, or
* {@code null} if no additional data must be integrity protected.
*/
byte[] getAssociatedData();
InputStream getAssociatedData();
}

View File

@ -19,10 +19,10 @@ import javax.crypto.SecretKey;
/**
* A request to an {@link AeadAlgorithm} to decrypt ciphertext and perform integrity-protection with a supplied
* decryption {@link SecretKey}. Extends both {@link InitializationVectorSupplier} and {@link DigestSupplier} to
* decryption {@link SecretKey}. Extends both {@link IvSupplier} and {@link DigestSupplier} to
* ensure the respective required IV and AAD tag returned from an {@link AeadResult} are available for decryption.
*
* @since JJWT_RELEASE_VERSION
*/
public interface DecryptAeadRequest extends AeadRequest, InitializationVectorSupplier, DigestSupplier {
public interface DecryptAeadRequest extends AeadRequest, IvSupplier, DigestSupplier {
}

View File

@ -19,6 +19,7 @@ import io.jsonwebtoken.Identifiable;
import io.jsonwebtoken.lang.Registry;
import javax.crypto.SecretKey;
import java.io.InputStream;
import java.security.PrivateKey;
import java.security.PublicKey;
@ -75,7 +76,7 @@ import java.security.PublicKey;
* @see io.jsonwebtoken.Jwts.SIG Jwts.SIG
* @since JJWT_RELEASE_VERSION
*/
public interface DigestAlgorithm<R extends Request<byte[]>, V extends VerifyDigestRequest> extends Identifiable {
public interface DigestAlgorithm<R extends Request<InputStream>, V extends VerifyDigestRequest> extends Identifiable {
/**
* Returns a cryptographic digest of the request {@link Request#getPayload() payload}.

View File

@ -17,6 +17,8 @@ package io.jsonwebtoken.security;
import io.jsonwebtoken.Identifiable;
import java.io.InputStream;
/**
* A {@link DigestAlgorithm} that computes and verifies digests without the use of a cryptographic key, such as for
* thumbprints and <a href="https://en.wikipedia.org/wiki/Fingerprint_(computing)">digital fingerprint</a>s.
@ -39,5 +41,5 @@ import io.jsonwebtoken.Identifiable;
* @see Jwks.HASH
* @since JJWT_RELEASE_VERSION
*/
public interface HashAlgorithm extends DigestAlgorithm<Request<byte[]>, VerifyDigestRequest> {
public interface HashAlgorithm extends DigestAlgorithm<Request<InputStream>, VerifyDigestRequest> {
}

View File

@ -16,14 +16,14 @@
package io.jsonwebtoken.security;
/**
* An {@code InitializationVectorSupplier} provides access to the secure-random Initialization Vector used during
* An {@code IvSupplier} provides access to the secure-random Initialization Vector used during
* encryption, which must in turn be presented for use during decryption. To maintain the security integrity of cryptographic
* algorithms, a <em>new</em> secure-random Initialization Vector <em>MUST</em> be generated for every individual
* encryption attempt.
*
* @since JJWT_RELEASE_VERSION
*/
public interface InitializationVectorSupplier {
public interface IvSupplier {
/**
* Returns the secure-random Initialization Vector used during encryption, which must in turn be presented for
@ -32,5 +32,5 @@ public interface InitializationVectorSupplier {
* @return the secure-random Initialization Vector used during encryption, which must in turn be presented for
* use during decryption.
*/
byte[] getInitializationVector();
byte[] getIv();
}

View File

@ -23,9 +23,10 @@ import io.jsonwebtoken.io.ParserBuilder;
* Example usage:
* <blockquote><pre>
* JwkSet jwkSet = Jwks.setParser()
* .provider(aJcaProvider) // optional
* .deserializer(deserializer) // optional
* .operationPolicy(policy) // optional
* .provider(aJcaProvider) // optional
* .json(deserializer) // optional
* .operationPolicy(policy) // optional
* .ignoreUnsupported(aBoolean) // optional
* .build()
* .parse(jwkSetString);</pre></blockquote>
*

View File

@ -41,6 +41,7 @@ public final class Jwks {
private Jwks() {
} //prevent instantiation
private static final String JWKS_BRIDGE_FQCN = "io.jsonwebtoken.impl.security.JwksBridge";
private static final String BUILDER_FQCN = "io.jsonwebtoken.impl.security.DefaultDynamicJwkBuilder";
private static final String PARSER_BUILDER_FQCN = "io.jsonwebtoken.impl.security.DefaultJwkParserBuilder";
private static final String SET_BUILDER_FQCN = "io.jsonwebtoken.impl.security.DefaultJwkSetBuilder";
@ -105,6 +106,30 @@ public final class Jwks {
return Classes.newInstance(SET_PARSER_BUILDER_FQCN);
}
/**
* Converts the specified {@link PublicJwk} into JSON. Because {@link PublicJwk}s do not contain secret or private
* key material, they are safe to be printed to application logs or {@code System.out}.
*
* @param publicJwk the {@code PublicJwk} to convert to JSON
* @return the JWK's canonical JSON value
*/
public static String json(PublicJwk<?> publicJwk) {
return UNSAFE_JSON(publicJwk); // safe by nature of it being a Public JWK
}
/**
* <b>WARNING - UNSAFE OPERATION - RETURN VALUES CONTAIN RAW KEY MATERIAL, DO NOT LOG OR PRINT TO SYSTEM.OUT.</b>
* Converts the specified JWK into JSON, including raw key material. If the specified JWK
* is a {@link SecretJwk} or a {@link PrivateJwk}, be very careful with the return value, ensuring it is not
* printed to application logs or system.out.
*
* @param jwk the JWK to convert to JSON
* @return the JWK's canonical JSON value
*/
public static String UNSAFE_JSON(Jwk<?> jwk) {
return Classes.invokeStatic(JWKS_BRIDGE_FQCN, "UNSAFE_JSON", new Class[]{Jwk.class}, jwk);
}
/**
* Constants for all standard JWK
* <a href="https://www.rfc-editor.org/rfc/rfc7518.html#section-6.2.1.1">crv (Curve)</a> parameter values

View File

@ -17,6 +17,7 @@ package io.jsonwebtoken.security;
import io.jsonwebtoken.Identifiable;
import java.io.InputStream;
import java.security.Key;
/**
@ -50,5 +51,5 @@ import java.security.Key;
* @since JJWT_RELEASE_VERSION
*/
public interface SecureDigestAlgorithm<S extends Key, V extends Key>
extends DigestAlgorithm<SecureRequest<byte[], S>, VerifySecureDigestRequest<V>> {
extends DigestAlgorithm<SecureRequest<InputStream, S>, VerifySecureDigestRequest<V>> {
}

View File

@ -15,6 +15,8 @@
*/
package io.jsonwebtoken.security;
import java.io.InputStream;
/**
* A request to verify a previously-computed cryptographic digest (available via {@link #getDigest()}) against the
* digest to be computed for the specified {@link #getPayload() payload}.
@ -27,5 +29,5 @@ package io.jsonwebtoken.security;
* @see VerifySecureDigestRequest
* @since JJWT_RELEASE_VERSION
*/
public interface VerifyDigestRequest extends Request<byte[]>, DigestSupplier {
public interface VerifyDigestRequest extends Request<InputStream>, DigestSupplier {
}

View File

@ -15,6 +15,7 @@
*/
package io.jsonwebtoken.security;
import java.io.InputStream;
import java.security.Key;
/**
@ -29,5 +30,5 @@ import java.security.Key;
* @param <K> the type of {@link Key} used to verify a digital signature or message authentication code
* @since JJWT_RELEASE_VERSION
*/
public interface VerifySecureDigestRequest<K extends Key> extends SecureRequest<byte[], K>, VerifyDigestRequest {
public interface VerifySecureDigestRequest<K extends Key> extends SecureRequest<InputStream, K>, VerifyDigestRequest {
}

View File

@ -0,0 +1,86 @@
/*
* Copyright © 2023 jsonwebtoken.io
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package io.jsonwebtoken.io
import org.junit.Test
import static org.junit.Assert.*
class AbstractDeserializerTest {
@Test
void deserializeNullByteArray() {
boolean invoked = false
def deser = new AbstractDeserializer() {
@Override
protected Object doDeserialize(InputStream inputStream) throws Exception {
assertEquals EOF, inputStream.read()
invoked = true
}
}
deser.deserialize((byte[]) null)
assertTrue invoked
}
@Test
void deserializeEmptyByteArray() {
boolean invoked = false
def deser = new AbstractDeserializer() {
@Override
protected Object doDeserialize(InputStream inputStream) throws Exception {
assertEquals EOF, inputStream.read()
invoked = true
}
}
deser.deserialize(new byte[0])
assertTrue invoked
}
@Test
void deserializeByteArray() {
byte b = 0x01
def bytes = new byte[1]
bytes[0] = b
def des = new AbstractDeserializer() {
@Override
protected Object doDeserialize(InputStream inputStream) throws Exception {
assertEquals b, inputStream.read()
return 42
}
}
assertEquals 42, des.deserialize(bytes)
}
@Test
void deserializeException() {
def ex = new RuntimeException('foo')
def des = new AbstractDeserializer() {
@Override
protected Object doDeserialize(InputStream inputStream) throws Exception {
throw ex
}
}
try {
des.deserialize(new byte[0])
} catch (DeserializationException expected) {
String msg = 'Unable to deserialize: foo'
assertEquals msg, expected.message
assertSame ex, expected.cause
}
}
}

View File

@ -0,0 +1,59 @@
/*
* Copyright © 2023 jsonwebtoken.io
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package io.jsonwebtoken.io
import org.junit.Test
import static org.junit.Assert.assertEquals
import static org.junit.Assert.assertSame
class AbstractSerializerTest {
@Test
void serializeByteArray() {
def value = 42
def ser = new AbstractSerializer() {
@Override
protected void doSerialize(Object o, OutputStream out) throws Exception {
assertEquals value, o
out.write(0x01)
}
}
def out = ser.serialize(value)
assertEquals 0x01, out[0]
}
@Test
void serializeException() {
def ex = new RuntimeException('foo')
def ser = new AbstractSerializer() {
@Override
protected void doSerialize(Object o, OutputStream out) throws Exception {
throw ex
}
}
try {
ser.serialize(42, new ByteArrayOutputStream())
} catch (SerializationException expected) {
String msg = 'Unable to serialize object of type java.lang.Integer: foo'
assertEquals msg, expected.message
assertSame ex, expected.cause
}
}
}

View File

@ -28,7 +28,7 @@ class Base64DecoderTest {
}
@Test
void testDecode() {
void decode() {
String encoded = 'SGVsbG8g5LiW55WM' // Hello
byte[] bytes = new Base64Decoder().decode(encoded)
String result = new String(bytes, Strings.UTF_8)

View File

@ -28,7 +28,7 @@ class Base64EncoderTest {
}
@Test
void testDecode() {
void encode() {
String input = 'Hello 世界'
byte[] bytes = input.getBytes(Strings.UTF_8)
String encoded = new Base64Encoder().encode(bytes)

View File

@ -67,8 +67,7 @@ class Base64Test {
String encoded = Base64.DEFAULT.encodeToString(bytes, true)
def r = new StringReader(encoded)
String line = ''
String line
while ((line = r.readLine()) != null) {
assertTrue line.length() <= 76
}
@ -82,7 +81,7 @@ class Base64Test {
@Test
void testDecodeFastWithEmptyCharArray() {
byte[] bytes = Base64.DEFAULT.decodeFast(new char[0])
byte[] bytes = Base64.DEFAULT.decodeFast(Strings.EMPTY)
assertEquals 0, bytes.length
}
@ -90,7 +89,7 @@ class Base64Test {
void testDecodeFastWithSurroundingIllegalCharacters() {
String expected = 'Hello 世界'
def encoded = '***SGVsbG8g5LiW55WM!!!'
byte[] bytes = Base64.DEFAULT.decodeFast(encoded.toCharArray())
byte[] bytes = Base64.DEFAULT.decodeFast(encoded)
String result = new String(bytes, Strings.UTF_8)
assertEquals expected, result
}
@ -99,7 +98,7 @@ class Base64Test {
void testDecodeFastWithIntermediateIllegalInboundCharacters() {
def encoded = 'SGVsbG8g*5LiW55WM'
try {
Base64.DEFAULT.decodeFast(encoded.toCharArray())
Base64.DEFAULT.decodeFast(encoded)
fail()
} catch (DecodingException de) {
assertEquals 'Illegal base64 character: \'*\'', de.getMessage()
@ -110,7 +109,7 @@ class Base64Test {
void testDecodeFastWithIntermediateIllegalOutOfBoundCharacters() {
def encoded = 'SGVsbG8g世5LiW55WM'
try {
Base64.DEFAULT.decodeFast(encoded.toCharArray())
Base64.DEFAULT.decodeFast(encoded)
fail()
} catch (DecodingException de) {
assertEquals 'Illegal base64 character: \'世\'', de.getMessage()
@ -121,7 +120,7 @@ class Base64Test {
void testDecodeFastWithIntermediateIllegalSpaceCharacters() {
def encoded = 'SGVsbG8g 5LiW55WM'
try {
Base64.DEFAULT.decodeFast(encoded.toCharArray())
Base64.DEFAULT.decodeFast(encoded)
fail()
} catch (DecodingException de) {
assertEquals 'Illegal base64 character: \' \'', de.getMessage()
@ -134,23 +133,24 @@ class Base64Test {
byte[] bytes = PLAINTEXT.getBytes(Strings.UTF_8)
String encoded = Base64.DEFAULT.encodeToString(bytes, true)
byte[] resultBytes = Base64.DEFAULT.decodeFast(encoded.toCharArray())
byte[] resultBytes = Base64.DEFAULT.decodeFast(encoded)
assertTrue Arrays.equals(bytes, resultBytes)
assertEquals PLAINTEXT, new String(resultBytes, Strings.UTF_8)
}
private static String encode(String s) {
byte[] bytes = s.getBytes(Strings.UTF_8);
byte[] bytes = s.getBytes(Strings.UTF_8)
return Base64.DEFAULT.encodeToString(bytes, false)
}
private static String decode(String s) {
byte[] bytes = Base64.DEFAULT.decodeFast(s.toCharArray())
byte[] bytes = Base64.DEFAULT.decodeFast(s)
return new String(bytes, Strings.UTF_8)
}
@Test // https://tools.ietf.org/html/rfc4648#page-12
@Test
// https://tools.ietf.org/html/rfc4648#page-12
void testRfc4648Base64TestVectors() {
assertEquals "", encode("")
@ -181,16 +181,17 @@ class Base64Test {
}
private static String urlEncode(String s) {
byte[] bytes = s.getBytes(Strings.UTF_8);
byte[] bytes = s.getBytes(Strings.UTF_8)
return Base64.URL_SAFE.encodeToString(bytes, false)
}
private static String urlDecode(String s) {
byte[] bytes = Base64.URL_SAFE.decodeFast(s.toCharArray())
byte[] bytes = Base64.URL_SAFE.decodeFast(s)
return new String(bytes, Strings.UTF_8)
}
@Test //same test vectors above, but with padding removed & some specials swapped: https://brockallen.com/2014/10/17/base64url-encoding/
@Test
//same test vectors above, but with padding removed & some specials swapped: https://brockallen.com/2014/10/17/base64url-encoding/
void testRfc4648Base64UrlTestVectors() {
assertEquals "", urlEncode("")
@ -216,9 +217,9 @@ class Base64Test {
def input = 'special: [\r\n \t], ascii[32..126]: [ !"#$%&\'()*+,-./0123456789:;<=>?@ABCDEFGHIJKLMNOPQRSTUVWXYZ[\\]^_`abcdefghijklmnopqrstuvwxyz{|}~]\n'
def expected = "c3BlY2lhbDogWw0KIAldLCBhc2NpaVszMi4uMTI2XTogWyAhIiMkJSYnKCkqKywtLi8wMTIzNDU2Nzg5Ojs8PT4/QEFCQ0RFRkdISUpLTE1OT1BRUlNUVVZXWFlaW1xdXl9gYWJjZGVmZ2hpamtsbW5vcHFyc3R1dnd4eXp7fH1+XQo="
.replace("=", "")
.replace("+", "-")
.replace("/", "_")
.replace("=", "")
.replace("+", "-")
.replace("/", "_")
assertEquals expected, urlEncode(input)
assertEquals input, urlDecode(expected)
}

View File

@ -16,24 +16,24 @@
package io.jsonwebtoken.gson.io;
import com.google.gson.Gson;
import io.jsonwebtoken.io.DeserializationException;
import io.jsonwebtoken.io.Deserializer;
import io.jsonwebtoken.io.AbstractDeserializer;
import io.jsonwebtoken.lang.Assert;
import io.jsonwebtoken.lang.Strings;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.io.Reader;
import java.nio.charset.StandardCharsets;
public class GsonDeserializer<T> implements Deserializer<T> {
public class GsonDeserializer<T> extends AbstractDeserializer<T> {
private final Class<T> returnType;
private final Gson gson;
protected final Gson gson;
@SuppressWarnings("unused") //used via reflection by RuntimeClasspathDeserializerLocator
public GsonDeserializer() {
this(GsonSerializer.DEFAULT_GSON);
}
@SuppressWarnings({"unchecked", "WeakerAccess", "unused"}) // for end-users providing a custom gson
@SuppressWarnings("unchecked")
public GsonDeserializer(Gson gson) {
this(gson, (Class<T>) Object.class);
}
@ -46,16 +46,8 @@ public class GsonDeserializer<T> implements Deserializer<T> {
}
@Override
public T deserialize(byte[] bytes) throws DeserializationException {
try {
return readValue(bytes);
} catch (IOException e) {
String msg = "Unable to deserialize bytes into a " + returnType.getName() + " instance: " + e.getMessage();
throw new DeserializationException(msg, e);
}
}
protected T readValue(byte[] bytes) throws IOException {
return gson.fromJson(new String(bytes, Strings.UTF_8), returnType);
protected T doDeserialize(InputStream in) throws Exception {
Reader reader = new InputStreamReader(in, StandardCharsets.UTF_8);
return gson.fromJson(reader, returnType);
}
}

View File

@ -17,26 +17,29 @@ package io.jsonwebtoken.gson.io;
import com.google.gson.Gson;
import com.google.gson.GsonBuilder;
import io.jsonwebtoken.io.AbstractSerializer;
import io.jsonwebtoken.io.Encoders;
import io.jsonwebtoken.io.SerializationException;
import io.jsonwebtoken.io.Serializer;
import io.jsonwebtoken.lang.Assert;
import io.jsonwebtoken.lang.Strings;
import io.jsonwebtoken.lang.Objects;
import io.jsonwebtoken.lang.Supplier;
public class GsonSerializer<T> implements Serializer<T> {
import java.io.OutputStream;
import java.io.OutputStreamWriter;
import java.io.Writer;
import java.nio.charset.StandardCharsets;
public class GsonSerializer<T> extends AbstractSerializer<T> {
static final Gson DEFAULT_GSON = new GsonBuilder()
.registerTypeHierarchyAdapter(Supplier.class, GsonSupplierSerializer.INSTANCE)
.disableHtmlEscaping().create();
private final Gson gson;
@SuppressWarnings("unused") //used via reflection by RuntimeClasspathDeserializerLocator
protected final Gson gson;
public GsonSerializer() {
this(DEFAULT_GSON);
}
@SuppressWarnings("WeakerAccess") //intended for end-users to use when providing a custom gson
public GsonSerializer(Gson gson) {
Assert.notNull(gson, "gson cannot be null.");
this.gson = gson;
@ -54,27 +57,23 @@ public class GsonSerializer<T> implements Serializer<T> {
}
@Override
public byte[] serialize(T t) throws SerializationException {
Assert.notNull(t, "Object to serialize cannot be null.");
protected void doSerialize(T t, OutputStream out) {
Writer writer = new OutputStreamWriter(out, StandardCharsets.UTF_8);
try {
return writeValueAsBytes(t);
} catch (Exception e) {
String msg = "Unable to serialize object: " + e.getMessage();
throw new SerializationException(msg, e);
Object o = t;
if (o instanceof byte[]) {
o = Encoders.BASE64.encode((byte[]) o);
} else if (o instanceof char[]) {
o = new String((char[]) o);
}
writeValue(o, writer);
} finally {
Objects.nullSafeClose(writer);
}
}
@SuppressWarnings("WeakerAccess") //for testing
protected byte[] writeValueAsBytes(T t) {
Object o;
if (t instanceof byte[]) {
o = Encoders.BASE64.encode((byte[]) t);
} else if (t instanceof char[]) {
o = new String((char[]) t);
} else {
o = t;
}
return this.gson.toJson(o).getBytes(Strings.UTF_8);
protected void writeValue(Object o, java.io.Writer writer) {
this.gson.toJson(o, writer);
}
private static class TestSupplier<T> implements Supplier<T> {

View File

@ -13,79 +13,80 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
//file:noinspection GrDeprecatedAPIUsage
package io.jsonwebtoken.gson.io
import com.google.gson.Gson
import io.jsonwebtoken.io.DeserializationException
import io.jsonwebtoken.io.Deserializer
import io.jsonwebtoken.lang.Strings
import org.junit.Before
import org.junit.Test
import java.text.DecimalFormat
import java.text.NumberFormat
import static org.easymock.EasyMock.*
import static org.junit.Assert.*
import static org.hamcrest.CoreMatchers.instanceOf
class GsonDeserializerTest {
private GsonDeserializer deserializer
private def deser(byte[] data) {
deserializer.deserialize(new ByteArrayInputStream(data))
}
private def deser(String s) {
return deser(Strings.utf8(s))
}
@Before
void setUp() {
deserializer = new GsonDeserializer()
}
@Test
void loadService() {
def deserializer = ServiceLoader.load(Deserializer).iterator().next()
assertThat(deserializer, instanceOf(GsonDeserializer))
assertTrue deserializer instanceof GsonDeserializer
}
@Test
void testDefaultConstructor() {
def deserializer = new GsonDeserializer()
assertNotNull deserializer.gson
}
@Test
void testObjectMapperConstructor() {
void testGsonConstructor() {
def customGSON = new Gson()
def deserializer = new GsonDeserializer(customGSON)
deserializer = new GsonDeserializer(customGSON)
assertSame customGSON, deserializer.gson
}
@Test(expected = IllegalArgumentException)
void testObjectMapperConstructorWithNullArgument() {
new GsonDeserializer<>(null)
void testGsonConstructorNullArgument() {
new GsonDeserializer(null)
}
@Test
void testDeserialize() {
byte[] serialized = '{"hello":"世界"}'.getBytes(Strings.UTF_8)
def expected = [hello: '世界']
def result = new GsonDeserializer().deserialize(serialized)
assertEquals expected, result
assertEquals expected, deser('{"hello":"世界"}')
}
@Test
void testDeserializeFailsWithJsonProcessingException() {
def ex = createMock(java.io.IOException)
expect(ex.getMessage()).andReturn('foo')
def deserializer = new GsonDeserializer() {
void testDeserializeThrows() {
def ex = new IOException('foo')
deserializer = new GsonDeserializer() {
@Override
protected Object readValue(byte[] bytes) throws java.io.IOException {
protected Object doDeserialize(InputStream inputStream) throws Exception {
throw ex
}
}
replay ex
try {
deserializer.deserialize('{"hello":"世界"}'.getBytes(Strings.UTF_8))
deser('{"hello":"世界"}')
fail()
} catch (DeserializationException se) {
assertEquals 'Unable to deserialize bytes into a java.lang.Object instance: foo', se.getMessage()
assertSame ex, se.getCause()
} catch (DeserializationException expected) {
String msg = 'Unable to deserialize: foo'
assertEquals msg, expected.message
assertSame ex, expected.cause
}
verify ex
}
}

View File

@ -13,6 +13,7 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
//file:noinspection GrDeprecatedAPIUsage
package io.jsonwebtoken.gson.io
import com.google.gson.Gson
@ -21,23 +22,33 @@ import io.jsonwebtoken.io.SerializationException
import io.jsonwebtoken.io.Serializer
import io.jsonwebtoken.lang.Strings
import io.jsonwebtoken.lang.Supplier
import org.junit.Before
import org.junit.Test
import static org.easymock.EasyMock.*
import static org.junit.Assert.*
class GsonSerializerTest {
private GsonSerializer s
@Before
void setUp() {
s = new GsonSerializer()
}
private String ser(Object o) {
return Strings.utf8(s.serialize(o))
}
@Test
void loadService() {
def serializer = ServiceLoader.load(Serializer).iterator().next()
assertTrue serializer instanceof GsonSerializer
assert serializer instanceof GsonSerializer
}
@Test
void testDefaultConstructor() {
def serializer = new GsonSerializer()
assertNotNull serializer.gson
assertNotNull s.gson
}
@Test
@ -45,8 +56,23 @@ class GsonSerializerTest {
def customGSON = new GsonBuilder()
.registerTypeHierarchyAdapter(Supplier.class, GsonSupplierSerializer.INSTANCE)
.disableHtmlEscaping().create()
def serializer = new GsonSerializer<>(customGSON)
assertSame customGSON, serializer.gson
s = new GsonSerializer(customGSON)
assertSame customGSON, s.gson
}
@Test
void testSerialize() {
assertEquals '"hello"', ser('hello')
}
private byte[] bytes(def o) {
ByteArrayOutputStream out = new ByteArrayOutputStream()
s.serialize(o, out)
return out.toByteArray()
}
private String json(def o) {
return Strings.utf8(bytes(o))
}
@Test
@ -72,71 +98,74 @@ class GsonSerializerTest {
@Test
void testByte() {
byte[] expected = "120".getBytes(Strings.UTF_8) //ascii("x") = 120
byte[] bytes = "x".getBytes(Strings.UTF_8)
byte[] result = new GsonSerializer().serialize(bytes[0]) //single byte
assertTrue Arrays.equals(expected, result)
byte[] expected = Strings.utf8("120") //ascii("x") = 120
byte[] result = bytes(Strings.utf8("x")[0]) //single byte
assertArrayEquals expected, result
}
@Test
void testByteArray() { //expect Base64 string by default:
byte[] bytes = "hi".getBytes(Strings.UTF_8)
String expected = '"aGk="' as String //base64(hi) --> aGk=
byte[] result = new GsonSerializer().serialize(bytes)
assertEquals expected, new String(result, Strings.UTF_8)
assertEquals expected, json(Strings.utf8('hi'))
}
@Test
void testEmptyByteArray() { //expect Base64 string by default:
byte[] bytes = new byte[0]
byte[] result = new GsonSerializer().serialize(bytes)
assertEquals '""', new String(result, Strings.UTF_8)
byte[] result = bytes(new byte[0])
assertEquals '""', Strings.utf8(result)
}
@Test
void testChar() { //expect Base64 string by default:
byte[] result = new GsonSerializer().serialize('h' as char)
assertEquals "\"h\"", new String(result, Strings.UTF_8)
assertEquals '"h"', json('h' as char)
}
@Test
void testCharArray() { //expect Base64 string by default:
byte[] result = new GsonSerializer().serialize("hi".toCharArray())
assertEquals "\"hi\"", new String(result, Strings.UTF_8)
void testCharArray() { //expect string by default:
assertEquals '"hi"', json('hi'.toCharArray())
}
@Test
void testSerialize() {
byte[] expected = '{"hello":"世界"}'.getBytes(Strings.UTF_8)
byte[] result = new GsonSerializer().serialize([hello: '世界'])
assertTrue Arrays.equals(expected, result)
void testWrite() {
assertEquals '{"hello":"世界"}', json([hello: '世界'])
}
@Test
void testSerializeFailsWithJsonProcessingException() {
def ex = createMock(SerializationException)
expect(ex.getMessage()).andReturn('foo')
def serializer = new GsonSerializer() {
void testWriteFailure() {
def ex = new IOException('foo')
s = new GsonSerializer() {
@Override
protected byte[] writeValueAsBytes(Object o) throws SerializationException {
protected void doSerialize(Object o, OutputStream out) {
throw ex
}
}
replay ex
try {
serializer.serialize([hello: 'world'])
ser([hello: 'world'])
fail()
} catch (SerializationException se) {
assertEquals 'Unable to serialize object: foo', se.getMessage()
assertSame ex, se.getCause()
} catch (SerializationException expected) {
String msg = 'Unable to serialize object of type java.util.LinkedHashMap: foo'
assertEquals msg, expected.message
assertSame ex, expected.cause
}
}
verify ex
@Test
void testIOExceptionConvertedToSerializationException() {
def ex = new IOException('foo')
s = new GsonSerializer() {
@Override
protected void doSerialize(Object o, OutputStream out) {
throw ex
}
}
try {
ser(new Object())
fail()
} catch (SerializationException expected) {
String causeMsg = 'foo'
String msg = "Unable to serialize object of type java.lang.Object: $causeMsg"
assertEquals causeMsg, expected.cause.message
assertEquals msg, expected.message
}
}
}

View File

@ -20,11 +20,11 @@ import com.fasterxml.jackson.databind.DeserializationContext;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.deser.std.UntypedObjectDeserializer;
import com.fasterxml.jackson.databind.module.SimpleModule;
import io.jsonwebtoken.io.DeserializationException;
import io.jsonwebtoken.io.Deserializer;
import io.jsonwebtoken.io.AbstractDeserializer;
import io.jsonwebtoken.lang.Assert;
import java.io.IOException;
import java.io.InputStream;
import java.util.Collections;
import java.util.Map;
@ -33,9 +33,10 @@ import java.util.Map;
*
* @since 0.10.0
*/
public class JacksonDeserializer<T> implements Deserializer<T> {
public class JacksonDeserializer<T> extends AbstractDeserializer<T> {
private final Class<T> returnType;
private final ObjectMapper objectMapper;
/**
@ -86,7 +87,7 @@ public class JacksonDeserializer<T> implements Deserializer<T> {
*
* @param objectMapper the ObjectMapper to use for deserialization.
*/
@SuppressWarnings({"unchecked", "WeakerAccess", "unused"}) // for end-users providing a custom ObjectMapper
@SuppressWarnings("unchecked")
public JacksonDeserializer(ObjectMapper objectMapper) {
this(objectMapper, (Class<T>) Object.class);
}
@ -99,24 +100,8 @@ public class JacksonDeserializer<T> implements Deserializer<T> {
}
@Override
public T deserialize(byte[] bytes) throws DeserializationException {
try {
return readValue(bytes);
} catch (IOException e) {
String msg = "Unable to deserialize bytes into a " + returnType.getName() + " instance: " + e.getMessage();
throw new DeserializationException(msg, e);
}
}
/**
* Converts the specified byte array value to the desired typed instance using the Jackson {@link ObjectMapper}.
*
* @param bytes the byte array value to convert
* @return the desired typed instance
* @throws IOException if there is a problem during reading or instance creation
*/
protected T readValue(byte[] bytes) throws IOException {
return objectMapper.readValue(bytes, returnType);
protected T doDeserialize(InputStream in) throws Exception {
return objectMapper.readValue(in, returnType);
}
/**
@ -138,6 +123,7 @@ public class JacksonDeserializer<T> implements Deserializer<T> {
String name = parser.currentName();
if (claimTypeMap != null && name != null && claimTypeMap.containsKey(name)) {
Class<?> type = claimTypeMap.get(name);
//noinspection resource
return parser.readValueAsTree().traverse(parser.getCodec()).readValueAs(type);
}
// otherwise default to super

View File

@ -15,20 +15,22 @@
*/
package io.jsonwebtoken.jackson.io;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.core.JsonGenerator;
import com.fasterxml.jackson.databind.Module;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.ObjectWriter;
import com.fasterxml.jackson.databind.module.SimpleModule;
import io.jsonwebtoken.io.SerializationException;
import io.jsonwebtoken.io.Serializer;
import io.jsonwebtoken.io.AbstractSerializer;
import io.jsonwebtoken.lang.Assert;
import java.io.OutputStream;
/**
* Serializer using a Jackson {@link ObjectMapper}.
*
* @since 0.10.0
*/
public class JacksonSerializer<T> implements Serializer<T> {
public class JacksonSerializer<T> extends AbstractSerializer<T> {
static final String MODULE_ID = "jjwt-jackson";
static final Module MODULE;
@ -41,12 +43,11 @@ public class JacksonSerializer<T> implements Serializer<T> {
static final ObjectMapper DEFAULT_OBJECT_MAPPER = new ObjectMapper().registerModule(MODULE);
private final ObjectMapper objectMapper;
protected final ObjectMapper objectMapper;
/**
* Constructor using JJWT's default {@link ObjectMapper} singleton for serialization.
*/
@SuppressWarnings("unused") //used via reflection by RuntimeClasspathDeserializerLocator
public JacksonSerializer() {
this(DEFAULT_OBJECT_MAPPER);
}
@ -56,32 +57,15 @@ public class JacksonSerializer<T> implements Serializer<T> {
*
* @param objectMapper the ObjectMapper to use for serialization.
*/
@SuppressWarnings("WeakerAccess") //intended for end-users to use when providing a custom ObjectMapper
public JacksonSerializer(ObjectMapper objectMapper) {
Assert.notNull(objectMapper, "ObjectMapper cannot be null.");
this.objectMapper = objectMapper.registerModule(MODULE);
}
@Override
public byte[] serialize(T t) throws SerializationException {
Assert.notNull(t, "Object to serialize cannot be null.");
try {
return writeValueAsBytes(t);
} catch (JsonProcessingException e) {
String msg = "Unable to serialize object: " + e.getMessage();
throw new SerializationException(msg, e);
}
}
/**
* Serializes the specified instance value to a byte array using the underlying Jackson {@link ObjectMapper}.
*
* @param t the instance to serialize to a byte array
* @return the byte array serialization of the specified instance
* @throws JsonProcessingException if there is a problem during serialization
*/
@SuppressWarnings("WeakerAccess") //for testing
protected byte[] writeValueAsBytes(T t) throws JsonProcessingException {
return this.objectMapper.writeValueAsBytes(t);
protected void doSerialize(T t, OutputStream out) throws Exception {
Assert.notNull(out, "OutputStream cannot be null.");
ObjectWriter writer = this.objectMapper.writer().without(JsonGenerator.Feature.AUTO_CLOSE_TARGET);
writer.writeValue(out, t);
}
}

View File

@ -13,6 +13,7 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
//file:noinspection GrDeprecatedAPIUsage
package io.jsonwebtoken.jackson.io
import com.fasterxml.jackson.databind.ObjectMapper
@ -22,29 +23,35 @@ import io.jsonwebtoken.io.Encoders
import io.jsonwebtoken.jackson.io.stubs.CustomBean
import io.jsonwebtoken.lang.Maps
import io.jsonwebtoken.lang.Strings
import org.junit.Before
import org.junit.Test
import static org.easymock.EasyMock.*
import static org.junit.Assert.*
import static org.hamcrest.CoreMatchers.instanceOf
class JacksonDeserializerTest {
private JacksonDeserializer deserializer
@Before
void setUp() {
deserializer = new JacksonDeserializer()
}
@Test
void loadService() {
def deserializer = ServiceLoader.load(Deserializer).iterator().next()
assertThat(deserializer, instanceOf(JacksonDeserializer))
assertTrue deserializer instanceof JacksonDeserializer
}
@Test
void testDefaultConstructor() {
def deserializer = new JacksonDeserializer()
assertNotNull deserializer.objectMapper
assertSame JacksonSerializer.DEFAULT_OBJECT_MAPPER, deserializer.objectMapper
}
@Test
void testObjectMapperConstructor() {
def customOM = new ObjectMapper()
def deserializer = new JacksonDeserializer(customOM)
deserializer = new JacksonDeserializer<>(customOM)
assertSame customOM, deserializer.objectMapper
}
@ -55,9 +62,9 @@ class JacksonDeserializerTest {
@Test
void testDeserialize() {
byte[] serialized = '{"hello":"世界"}'.getBytes(Strings.UTF_8)
byte[] data = Strings.utf8('{"hello":"世界"}')
def expected = [hello: '世界']
def result = new JacksonDeserializer().deserialize(serialized)
def result = deserializer.deserialize(new ByteArrayInputStream(data))
assertEquals expected, result
}
@ -66,7 +73,8 @@ class JacksonDeserializerTest {
long currentTime = System.currentTimeMillis()
byte[] serialized = """{
String json = """
{
"oneKey":"oneValue",
"custom": {
"stringValue": "s-value",
@ -87,28 +95,31 @@ class JacksonDeserializerTest {
}
}
}
""".getBytes(Strings.UTF_8)
"""
byte[] serialized = Strings.utf8(json)
CustomBean expectedCustomBean = new CustomBean()
.setByteArrayValue("bytes".getBytes("UTF-8"))
.setByteValue(0xF as byte)
.setDateValue(new Date(currentTime))
.setIntValue(11)
.setShortValue(22 as short)
.setLongValue(33L)
.setStringValue("s-value")
.setNestedValue(new CustomBean()
.setByteArrayValue("bytes2".getBytes("UTF-8"))
.setByteValue(0xA as byte)
.setDateValue(new Date(currentTime+1))
.setIntValue(111)
.setShortValue(222 as short)
.setLongValue(333L)
.setStringValue("nested-value")
)
.setByteArrayValue("bytes".getBytes("UTF-8"))
.setByteValue(0xF as byte)
.setDateValue(new Date(currentTime))
.setIntValue(11)
.setShortValue(22 as short)
.setLongValue(33L)
.setStringValue("s-value")
.setNestedValue(new CustomBean()
.setByteArrayValue("bytes2".getBytes("UTF-8"))
.setByteValue(0xA as byte)
.setDateValue(new Date(currentTime + 1))
.setIntValue(111)
.setShortValue(222 as short)
.setLongValue(333L)
.setStringValue("nested-value")
)
def expected = [oneKey: "oneValue", custom: expectedCustomBean]
def result = new JacksonDeserializer(Maps.of("custom", CustomBean).build()).deserialize(serialized)
def result = new JacksonDeserializer(Maps.of("custom", CustomBean).build())
.deserialize(new ByteArrayInputStream(serialized))
assertEquals expected, result
}
@ -143,7 +154,8 @@ class JacksonDeserializerTest {
typeMap.put("custom", CustomBean)
def deserializer = new JacksonDeserializer(typeMap)
def result = deserializer.deserialize('{"alg":"HS256"}'.getBytes("UTF-8"))
def ins = new ByteArrayInputStream(Strings.utf8('{"alg":"HS256"}'))
def result = deserializer.deserialize(ins)
assertEquals(["alg": "HS256"], result)
}
@ -153,33 +165,27 @@ class JacksonDeserializerTest {
}
@Test
void testDeserializeFailsWithJsonProcessingException() {
void testDeserializeFailsWithException() {
def ex = createMock(java.io.IOException)
def ex = new IOException('foo')
expect(ex.getMessage()).andReturn('foo')
def deserializer = new JacksonDeserializer() {
deserializer = new JacksonDeserializer() {
@Override
protected Object readValue(byte[] bytes) throws java.io.IOException {
protected Object doDeserialize(InputStream inputStream) throws Exception {
throw ex
}
}
replay ex
try {
deserializer.deserialize('{"hello":"世界"}'.getBytes(Strings.UTF_8))
deserializer.deserialize(new ByteArrayInputStream(Strings.utf8('{"hello":"世界"}')))
fail()
} catch (DeserializationException se) {
assertEquals 'Unable to deserialize bytes into a java.lang.Object instance: foo', se.getMessage()
String msg = 'Unable to deserialize: foo'
assertEquals msg, se.getMessage()
assertSame ex, se.getCause()
}
verify ex
}
private String base64(String input) {
private static String base64(String input) {
return Encoders.BASE64.encode(input.getBytes('UTF-8'))
}
}

View File

@ -15,36 +15,46 @@
*/
package io.jsonwebtoken.jackson.io
import com.fasterxml.jackson.core.JsonProcessingException
import com.fasterxml.jackson.databind.ObjectMapper
import io.jsonwebtoken.io.SerializationException
import io.jsonwebtoken.io.Serializer
import io.jsonwebtoken.lang.Strings
import org.junit.Before
import org.junit.Test
import static org.easymock.EasyMock.*
import static org.hamcrest.CoreMatchers.instanceOf
import static org.junit.Assert.*
class JacksonSerializerTest {
private JacksonSerializer ser
@Before
void setUp() {
ser = new JacksonSerializer()
}
byte[] serialize(def value) {
def os = new ByteArrayOutputStream()
ser.serialize(value, os)
return os.toByteArray()
}
@Test
void loadService() {
def serializer = ServiceLoader.load(Serializer).iterator().next()
assertThat(serializer, instanceOf(JacksonSerializer))
assertTrue serializer instanceof JacksonSerializer
}
@Test
void testDefaultConstructor() {
def serializer = new JacksonSerializer()
assertNotNull serializer.objectMapper
assertSame JacksonSerializer.DEFAULT_OBJECT_MAPPER, ser.objectMapper
}
@Test
void testObjectMapperConstructor() {
def customOM = new ObjectMapper()
def serializer = new JacksonSerializer<>(customOM)
assertSame customOM, serializer.objectMapper
ObjectMapper customOM = new ObjectMapper()
ser = new JacksonSerializer(customOM)
assertSame customOM, ser.objectMapper
}
@Test(expected = IllegalArgumentException)
@ -54,79 +64,58 @@ class JacksonSerializerTest {
@Test
void testObjectMapperConstructorAutoRegistersModule() {
def om = createMock(ObjectMapper)
ObjectMapper om = createMock(ObjectMapper)
expect(om.registerModule(same(JacksonSerializer.MODULE))).andReturn(om)
replay om
def serializer = new JacksonSerializer<>(om)
//noinspection GroovyResultOfObjectAllocationIgnored
new JacksonSerializer<>(om)
verify om
}
@Test
void testByte() {
byte[] expected = "120".getBytes(Strings.UTF_8) //ascii("x") = 120
byte[] bytes = "x".getBytes(Strings.UTF_8)
byte[] result = new JacksonSerializer().serialize(bytes[0]) //single byte
assertTrue Arrays.equals(expected, result)
}
@Test
void testByteArray() { //expect Base64 string by default:
byte[] bytes = "hi".getBytes(Strings.UTF_8)
String expected = '"aGk="' as String //base64(hi) --> aGk=
byte[] result = new JacksonSerializer().serialize(bytes)
assertEquals expected, new String(result, Strings.UTF_8)
}
@Test
void testEmptyByteArray() { //expect Base64 string by default:
byte[] bytes = new byte[0]
byte[] result = new JacksonSerializer().serialize(bytes)
assertEquals '""', new String(result, Strings.UTF_8)
}
@Test
void testChar() { //expect Base64 string by default:
byte[] result = new JacksonSerializer().serialize('h' as char)
assertEquals "\"h\"", new String(result, Strings.UTF_8)
}
@Test
void testCharArray() { //expect Base64 string by default:
byte[] result = new JacksonSerializer().serialize("hi".toCharArray())
assertEquals "\"hi\"", new String(result, Strings.UTF_8)
}
@Test
void testSerialize() {
byte[] expected = '{"hello":"世界"}'.getBytes(Strings.UTF_8)
byte[] result = new JacksonSerializer().serialize([hello: '世界'])
byte[] result = ser.serialize([hello: '世界'])
assertTrue Arrays.equals(expected, result)
}
@Test
void testSerializeFailsWithJsonProcessingException() {
void testByte() {
byte[] expected = Strings.utf8("120") //ascii("x") = 120
byte[] bytes = Strings.utf8("x")
assertArrayEquals expected, serialize(bytes[0]) // single byte
}
def ex = createMock(JsonProcessingException)
@Test
void testByteArray() { //expect Base64 string by default:
byte[] bytes = Strings.utf8("hi")
String expected = '"aGk="' as String //base64(hi) --> aGk=
assertEquals expected, Strings.utf8(serialize(bytes))
}
expect(ex.getMessage()).andReturn('foo')
@Test
void testEmptyByteArray() { //expect Base64 string by default:
byte[] bytes = new byte[0]
byte[] result = serialize(bytes)
assertEquals '""', Strings.utf8(result)
}
def serializer = new JacksonSerializer() {
@Override
protected byte[] writeValueAsBytes(Object o) throws JsonProcessingException {
throw ex
}
}
@Test
void testChar() { //expect Base64 string by default:
byte[] result = serialize('h' as char)
assertEquals "\"h\"", Strings.utf8(result)
}
replay ex
@Test
void testCharArray() { //expect Base64 string by default:
byte[] result = serialize('hi'.toCharArray())
assertEquals "\"hi\"", Strings.utf8(result)
}
try {
serializer.serialize([hello: 'world'])
fail()
} catch (SerializationException se) {
assertEquals 'Unable to serialize object: foo', se.getMessage()
assertSame ex, se.getCause()
}
verify ex
@Test
void testWriteObject() {
byte[] expected = Strings.utf8('{"hello":"世界"}' as String)
byte[] result = serialize([hello: '世界'])
assertArrayEquals expected, result
}
}

View File

@ -15,11 +15,10 @@
*/
package io.jsonwebtoken.jackson.io
import io.jsonwebtoken.lang.Strings
import io.jsonwebtoken.lang.Supplier
import org.junit.Test
import java.nio.charset.StandardCharsets
import static org.junit.Assert.assertEquals
class JacksonSupplierSerializerTest {
@ -33,7 +32,22 @@ class JacksonSupplierSerializerTest {
return null
}
}
byte[] bytes = serializer.serialize(supplier)
assertEquals 'null', new String(bytes, StandardCharsets.UTF_8)
ByteArrayOutputStream out = new ByteArrayOutputStream()
serializer.serialize(supplier, out)
assertEquals 'null', Strings.utf8(out.toByteArray())
}
@Test
void testSupplierStringValue() {
def serializer = new JacksonSerializer()
def supplier = new Supplier() {
@Override
Object get() {
return 'hello'
}
}
ByteArrayOutputStream out = new ByteArrayOutputStream()
serializer.serialize(supplier, out)
assertEquals '"hello"', Strings.utf8(out.toByteArray())
}
}

View File

@ -0,0 +1,33 @@
/*
* Copyright © 2023 jsonwebtoken.io
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package io.jsonwebtoken.jackson.io
import io.jsonwebtoken.lang.Supplier
class TestSupplier<T> implements Supplier<T> {
private static final TestSupplier<String> INSTANCE = new TestSupplier<>("test")
private final T value;
private TestSupplier(T value) {
this.value = value;
}
@Override
T get() {
return value;
}
}

View File

@ -131,16 +131,16 @@ class CustomBean {
@Override
public String toString() {
String toString() {
return "CustomBean{" +
"stringValue='" + stringValue + '\'' +
", intValue=" + intValue +
", dateValue=" + dateValue?.time+
", dateValue=" + dateValue?.time +
", shortValue=" + shortValue +
", longValue=" + longValue +
", byteValue=" + byteValue +
// ", byteArrayValue=" + Arrays.toString(byteArrayValue) +
", nestedValue=" + nestedValue +
'}';
'}'
}
}

View File

@ -15,15 +15,16 @@
*/
package io.jsonwebtoken.orgjson.io;
import io.jsonwebtoken.io.DeserializationException;
import io.jsonwebtoken.io.Deserializer;
import io.jsonwebtoken.lang.Assert;
import io.jsonwebtoken.lang.Strings;
import io.jsonwebtoken.io.AbstractDeserializer;
import org.json.JSONArray;
import org.json.JSONException;
import org.json.JSONObject;
import org.json.JSONTokener;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.io.Reader;
import java.nio.charset.StandardCharsets;
import java.util.ArrayList;
import java.util.Iterator;
import java.util.LinkedHashMap;
@ -33,29 +34,17 @@ import java.util.Map;
/**
* @since 0.10.0
*/
public class OrgJsonDeserializer implements Deserializer<Object> {
public class OrgJsonDeserializer extends AbstractDeserializer<Object> {
@Override
public Object deserialize(byte[] bytes) throws DeserializationException {
Assert.notNull(bytes, "JSON byte array cannot be null");
if (bytes.length == 0) {
throw new DeserializationException("Invalid JSON: zero length byte array.");
}
try {
String s = new String(bytes, Strings.UTF_8);
return parse(s);
} catch (Exception e) {
String msg = "Invalid JSON: " + e.getMessage();
throw new DeserializationException(msg, e);
}
protected Object doDeserialize(InputStream in) {
Reader reader = new InputStreamReader(in, StandardCharsets.UTF_8);
return parse(reader);
}
private Object parse(String json) throws JSONException {
private Object parse(java.io.Reader reader) throws JSONException {
JSONTokener tokener = new JSONTokener(json);
JSONTokener tokener = new JSONTokener(reader);
char c = tokener.nextClean(); //peak ahead
tokener.back(); //revert

View File

@ -15,9 +15,8 @@
*/
package io.jsonwebtoken.orgjson.io;
import io.jsonwebtoken.io.AbstractSerializer;
import io.jsonwebtoken.io.Encoders;
import io.jsonwebtoken.io.SerializationException;
import io.jsonwebtoken.io.Serializer;
import io.jsonwebtoken.lang.Classes;
import io.jsonwebtoken.lang.Collections;
import io.jsonwebtoken.lang.DateFormats;
@ -27,6 +26,8 @@ import io.jsonwebtoken.lang.Supplier;
import org.json.JSONArray;
import org.json.JSONObject;
import java.io.IOException;
import java.io.OutputStream;
import java.math.BigDecimal;
import java.math.BigInteger;
import java.util.Calendar;
@ -37,7 +38,7 @@ import java.util.Map;
/**
* @since 0.10.0
*/
public class OrgJsonSerializer<T> implements Serializer<T> {
public class OrgJsonSerializer<T> extends AbstractSerializer<T> {
// we need reflection for these because of Android - see https://github.com/jwtk/jjwt/issues/388
private static final String JSON_WRITER_CLASS_NAME = "org.json.JSONWriter";
@ -54,17 +55,11 @@ public class OrgJsonSerializer<T> implements Serializer<T> {
}
@Override
public byte[] serialize(T t) throws SerializationException {
try {
Object o = toJSONInstance(t);
return toBytes(o);
} catch (SerializationException se) {
//propagate
throw se;
} catch (Exception e) {
String msg = "Unable to serialize object of type " + t.getClass().getName() + " to JSON: " + e.getMessage();
throw new SerializationException(msg, e);
}
protected void doSerialize(T t, OutputStream out) throws Exception {
Object o = toJSONInstance(t);
String s = toString(o);
byte[] bytes = Strings.utf8(s);
out.write(bytes);
}
/**
@ -77,7 +72,7 @@ public class OrgJsonSerializer<T> implements Serializer<T> {
return false;
}
private Object toJSONInstance(Object object) {
private Object toJSONInstance(Object object) throws IOException {
if (object == null) {
return JSONObject.NULL;
@ -132,10 +127,10 @@ public class OrgJsonSerializer<T> implements Serializer<T> {
//not an immediately JSON-compatible object and probably a JavaBean (or similar). We can't convert that
//directly without using a marshaller of some sort:
String msg = "Unable to serialize object of type " + object.getClass().getName() + " to JSON using known heuristics.";
throw new SerializationException(msg);
throw new IOException(msg);
}
private JSONObject toJSONObject(Map<?, ?> m) {
private JSONObject toJSONObject(Map<?, ?> m) throws IOException {
JSONObject obj = new JSONObject();
@ -151,7 +146,7 @@ public class OrgJsonSerializer<T> implements Serializer<T> {
return obj;
}
private JSONArray toJSONArray(Collection<?> c) {
private JSONArray toJSONArray(Collection<?> c) throws IOException {
JSONArray array = new JSONArray();
@ -164,14 +159,12 @@ public class OrgJsonSerializer<T> implements Serializer<T> {
}
/**
* Serializes the specified org.json instance a byte array.
* Serializes the specified org.json instance a JSON String.
*
* @param o the org.json instance to serialize
* @return the JSON byte array
* @param o the org.json instance to convert to a String
* @return the JSON String
*/
@SuppressWarnings("WeakerAccess") //for testing
protected byte[] toBytes(Object o) {
String s;
protected String toString(Object o) {
// https://github.com/jwtk/jjwt/issues/380 for Android compatibility (Android doesn't have org.json.JSONWriter):
// This instanceof check is a sneaky (hacky?) heuristic: A JwtBuilder only ever provides Map<String,Object>
// instances to its serializer instances, so by the time this method is invoked, 'o' will always be a
@ -181,12 +174,23 @@ public class OrgJsonSerializer<T> implements Serializer<T> {
// JJWT's internal Serializer implementation for general JSON serialization. That is, its intended use
// is within the context of JwtBuilder execution and not for application use beyond that.
if (o instanceof JSONObject) {
s = o.toString();
} else {
// we still call JSONWriter for all other values 'just in case', and this works for all valid JSON values
// This would fail on Android unless they include the newer org.json dependency and ignore Android's.
s = Classes.invokeStatic(JSON_WRITER_CLASS_NAME, "valueToString", VALUE_TO_STRING_ARG_TYPES, o);
return o.toString();
}
return s.getBytes(Strings.UTF_8);
// we still call JSONWriter for all other values 'just in case', and this works for all valid JSON values
// This would fail on Android unless they include the newer org.json dependency and ignore Android's.
return Classes.invokeStatic(JSON_WRITER_CLASS_NAME, "valueToString", VALUE_TO_STRING_ARG_TYPES, o);
}
/**
* Serializes the specified org.json instance a byte array.
*
* @param o the org.json instance to serialize
* @return the JSON byte array
* @deprecated not called by JJWT
*/
@Deprecated
protected byte[] toBytes(Object o) {
String s = toString(o);
return Strings.utf8(s);
}
}

View File

@ -13,98 +13,79 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
//file:noinspection GrDeprecatedAPIUsage
package io.jsonwebtoken.orgjson.io
import io.jsonwebtoken.io.DeserializationException
import io.jsonwebtoken.io.Deserializer
import io.jsonwebtoken.lang.Strings
import org.junit.Before
import org.junit.Test
import static org.hamcrest.CoreMatchers.instanceOf
import static org.junit.Assert.*
class OrgJsonDeserializerTest {
@Test
void loadService() {
def deserializer = ServiceLoader.load(Deserializer).iterator().next()
assertThat(deserializer, instanceOf(OrgJsonDeserializer))
private OrgJsonDeserializer des
private Object fromBytes(byte[] data) {
return des.deserialize(new ByteArrayInputStream(data))
}
@Test(expected=IllegalArgumentException)
private Object read(String s) {
return fromBytes(Strings.utf8(s))
}
@Test(expected = IllegalArgumentException)
void testNullArgument() {
def d = new OrgJsonDeserializer()
d.deserialize(null)
des.deserialize((InputStream) null)
}
@Test(expected = DeserializationException)
void testEmptyByteArray() {
def d = new OrgJsonDeserializer()
d.deserialize(new byte[0])
fromBytes(new byte[0])
}
@Test(expected = DeserializationException)
void testInvalidJson() {
def d = new OrgJsonDeserializer()
d.deserialize('"'.getBytes(Strings.UTF_8))
read('"')
}
@Test
void testLiteralNull() {
def d = new OrgJsonDeserializer();
def b = 'null'.getBytes(Strings.UTF_8)
def value = d.deserialize(b)
assertNull value
assertNull read('null')
}
@Test
void testLiteralTrue() {
def d = new OrgJsonDeserializer();
def b = 'true'.getBytes(Strings.UTF_8)
def value = d.deserialize(b)
assertEquals Boolean.TRUE, value
assertTrue read('true') as boolean
}
@Test
void testLiteralFalse() {
def d = new OrgJsonDeserializer();
def b = 'false'.getBytes(Strings.UTF_8)
def value = d.deserialize(b)
assertEquals Boolean.FALSE, value
assertFalse read('false') as boolean
}
@Test
void testLiteralInteger() {
def d = new OrgJsonDeserializer();
def b = '1'.getBytes(Strings.UTF_8)
def value = d.deserialize(b)
assert value instanceof Integer
assertEquals 1 as Integer, value
assertEquals 1 as Integer, read('1')
}
@Test
void testLiteralDecimal() {
def d = new OrgJsonDeserializer();
def b = '3.14159'.getBytes(Strings.UTF_8)
def value = d.deserialize(b)
assert value instanceof BigDecimal
assertEquals 3.14159 as Double, value, 0d
assertEquals 3.14159 as Double, read('3.14159') as BigDecimal, 0d
}
@Test
void testEmptyArray() {
def d = new OrgJsonDeserializer();
def b = '[]'.getBytes(Strings.UTF_8)
def value = d.deserialize(b)
def value = read('[]')
assert value instanceof List
assertEquals 0, value.size()
}
@Test
void testSimpleArray() {
def d = new OrgJsonDeserializer();
def b = '[1, 2]'.getBytes(Strings.UTF_8)
def value = d.deserialize(b)
def value = read('[1, 2]')
assert value instanceof List
def expected = [1, 2]
assertEquals expected, value
@ -112,9 +93,7 @@ class OrgJsonDeserializerTest {
@Test
void testArrayWithNullElements() {
def d = new OrgJsonDeserializer();
def b = '[1, null, 3]'.getBytes(Strings.UTF_8)
def value = d.deserialize(b)
def value = read('[1, null, 3]')
assert value instanceof List
def expected = [1, null, 3]
assertEquals expected, value
@ -122,18 +101,14 @@ class OrgJsonDeserializerTest {
@Test
void testEmptyObject() {
def d = new OrgJsonDeserializer();
def b = '{}'.getBytes(Strings.UTF_8)
def value = d.deserialize(b)
def value = read('{}')
assert value instanceof Map
assertEquals 0, value.size()
}
@Test
void testSimpleObject() {
def d = new OrgJsonDeserializer();
def b = '{"hello": "世界"}'.getBytes(Strings.UTF_8)
def value = d.deserialize(b)
def value = read('{"hello": "世界"}')
assert value instanceof Map
def expected = [hello: '世界']
assertEquals expected, value
@ -141,9 +116,7 @@ class OrgJsonDeserializerTest {
@Test
void testObjectWithKeyHavingNullValue() {
def d = new OrgJsonDeserializer();
def b = '{"hello": "世界", "test": null}'.getBytes(Strings.UTF_8)
def value = d.deserialize(b)
def value = read('{"hello": "世界", "test": null}')
assert value instanceof Map
def expected = [hello: '世界', test: null]
assertEquals expected, value
@ -151,9 +124,7 @@ class OrgJsonDeserializerTest {
@Test
void testObjectWithKeyHavingArrayValue() {
def d = new OrgJsonDeserializer();
def b = '{"hello": "世界", "test": [1, 2]}'.getBytes(Strings.UTF_8)
def value = d.deserialize(b)
def value = read('{"hello": "世界", "test": [1, 2]}')
assert value instanceof Map
def expected = [hello: '世界', test: [1, 2]]
assertEquals expected, value
@ -161,11 +132,58 @@ class OrgJsonDeserializerTest {
@Test
void testObjectWithKeyHavingObjectValue() {
def d = new OrgJsonDeserializer();
def b = '{"hello": "世界", "test": {"foo": "bar"}}'.getBytes(Strings.UTF_8)
def value = d.deserialize(b)
def value = read('{"hello": "世界", "test": {"foo": "bar"}}')
assert value instanceof Map
def expected = [hello: '世界', test: [foo: 'bar']]
assertEquals expected, value
}
@Before
void setUp() {
des = new OrgJsonDeserializer()
}
@Test
void loadService() {
def deserializer = ServiceLoader.load(Deserializer).iterator().next()
assert deserializer instanceof OrgJsonDeserializer
}
@Test
void deserialize() {
def m = [hello: 42]
assertEquals m, des.deserialize(Strings.utf8('{"hello":42}'))
}
@Test(expected = IllegalArgumentException)
void deserializeNull() {
des.deserialize((InputStream) null)
}
@Test(expected = DeserializationException)
void deserializeEmpty() {
read('')
}
@Test
void throwableConvertsToDeserializationException() {
def t = new Throwable("foo")
des = new OrgJsonDeserializer() {
@Override
protected Object doDeserialize(InputStream inputStream) {
throw t
}
}
try {
des.deserialize(Strings.utf8('whatever'))
fail()
} catch (DeserializationException expected) {
String msg = 'Unable to deserialize: foo'
assertEquals msg, expected.message
}
}
}

View File

@ -13,6 +13,7 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
//file:noinspection GrDeprecatedAPIUsage
package io.jsonwebtoken.orgjson.io
import io.jsonwebtoken.SignatureAlgorithm
@ -20,13 +21,13 @@ import io.jsonwebtoken.io.SerializationException
import io.jsonwebtoken.io.Serializer
import io.jsonwebtoken.lang.DateFormats
import io.jsonwebtoken.lang.Strings
import io.jsonwebtoken.lang.Supplier
import org.json.JSONObject
import org.json.JSONString
import org.junit.Before
import org.junit.Test
import static org.junit.Assert.*
import static org.hamcrest.CoreMatchers.instanceOf
class OrgJsonSerializerTest {
@ -38,38 +39,24 @@ class OrgJsonSerializerTest {
}
private String ser(Object o) {
byte[] bytes = s.serialize(o)
return new String(bytes, Strings.UTF_8)
return Strings.utf8(s.serialize(o))
}
@Test
void loadService() {
def serializer = ServiceLoader.load(Serializer).iterator().next()
assertThat(serializer, instanceOf(OrgJsonSerializer))
}
@Test(expected = SerializationException)
void testInvalidArgument() {
s.serialize(new Object())
assertTrue serializer instanceof OrgJsonSerializer
}
@Test
void testToBytesFailure() {
final IllegalArgumentException iae = new IllegalArgumentException("foo")
s = new OrgJsonSerializer() {
@Override
protected byte[] toBytes(Object o) {
throw iae
}
}
void testInvalidArgument() {
try {
s.serialize("hello")
ser(new Object())
fail()
} catch (SerializationException se) {
assertTrue se.getMessage().endsWith(iae.getMessage())
assertSame iae, se.getCause()
} catch (SerializationException expected) {
String causeMsg = 'Unable to serialize object of type java.lang.Object to JSON using known heuristics.'
String msg = "Unable to serialize object of type java.lang.Object: $causeMsg"
assertEquals msg, expected.message
}
}
@ -176,6 +163,17 @@ class OrgJsonSerializerTest {
assertEquals '"HS256"', ser(SignatureAlgorithm.HS256)
}
@Test
void testSupplier() {
def supplier = new Supplier() {
@Override
Object get() {
return 'test'
}
}
assertEquals '"test"', ser(supplier)
}
@Test
void testEmptyString() {
assertEquals '""', ser('')
@ -276,4 +274,28 @@ class OrgJsonSerializerTest {
void testListWithNestedObject() {
assertEquals '[1,null,{"hello":"世界"}]', ser([1, null, [hello: '世界']])
}
@Test
void testSerialize() {
assertEquals '"hello"', ser('hello')
}
@Test
void testIOExceptionConvertedToSerializationException() {
try {
ser(new Object())
fail()
} catch (SerializationException expected) {
String causeMsg = 'Unable to serialize object of type java.lang.Object to JSON using known heuristics.'
String msg = "Unable to serialize object of type java.lang.Object: $causeMsg"
assertEquals causeMsg, expected.cause.message
assertEquals msg, expected.message
}
}
@Test
void testToBytes() {
assertEquals 'null', Strings.utf8(s.toBytes(null))
assertEquals '"hello"', Strings.utf8(s.toBytes('hello'))
}
}

View File

@ -62,7 +62,7 @@ public class DefaultClaims extends ParameterMap implements Claims {
@Override
public String getName() {
return "JWT Claim";
return "JWT Claims";
}
@Override

View File

@ -21,12 +21,20 @@ import io.jsonwebtoken.JweHeader;
import io.jsonwebtoken.JwsHeader;
import io.jsonwebtoken.JwtBuilder;
import io.jsonwebtoken.Jwts;
import io.jsonwebtoken.impl.io.Base64UrlStreamEncoder;
import io.jsonwebtoken.impl.io.ByteBase64UrlStreamEncoder;
import io.jsonwebtoken.impl.io.CountingInputStream;
import io.jsonwebtoken.impl.io.EncodingOutputStream;
import io.jsonwebtoken.impl.io.NamedSerializer;
import io.jsonwebtoken.impl.io.Streams;
import io.jsonwebtoken.impl.io.UncloseableInputStream;
import io.jsonwebtoken.impl.lang.Bytes;
import io.jsonwebtoken.impl.lang.Function;
import io.jsonwebtoken.impl.lang.Functions;
import io.jsonwebtoken.impl.lang.Parameter;
import io.jsonwebtoken.impl.lang.Services;
import io.jsonwebtoken.impl.security.DefaultAeadRequest;
import io.jsonwebtoken.impl.security.DefaultAeadResult;
import io.jsonwebtoken.impl.security.DefaultKeyRequest;
import io.jsonwebtoken.impl.security.DefaultSecureRequest;
import io.jsonwebtoken.impl.security.Pbes2HsAkwAlgorithm;
@ -35,11 +43,10 @@ import io.jsonwebtoken.impl.security.StandardSecureDigestAlgorithms;
import io.jsonwebtoken.io.CompressionAlgorithm;
import io.jsonwebtoken.io.Decoders;
import io.jsonwebtoken.io.Encoder;
import io.jsonwebtoken.io.Encoders;
import io.jsonwebtoken.io.SerializationException;
import io.jsonwebtoken.io.Serializer;
import io.jsonwebtoken.lang.Assert;
import io.jsonwebtoken.lang.Collections;
import io.jsonwebtoken.lang.Objects;
import io.jsonwebtoken.lang.Strings;
import io.jsonwebtoken.security.AeadAlgorithm;
import io.jsonwebtoken.security.AeadRequest;
@ -57,7 +64,11 @@ import io.jsonwebtoken.security.UnsupportedKeyException;
import javax.crypto.SecretKey;
import javax.crypto.spec.SecretKeySpec;
import java.nio.charset.StandardCharsets;
import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.InputStream;
import java.io.OutputStream;
import java.io.SequenceInputStream;
import java.security.Key;
import java.security.PrivateKey;
import java.security.Provider;
@ -71,10 +82,11 @@ import java.util.Set;
public class DefaultJwtBuilder implements JwtBuilder {
private static final String PUB_KEY_SIGN_MSG = "PublicKeys may not be used to create digital signatures. " + "PrivateKeys are used to sign, and PublicKeys are used to verify.";
private static final String PRIV_KEY_ENC_MSG = "PrivateKeys may not be used to encrypt data. PublicKeys are " + "used to encrypt, and PrivateKeys are used to decrypt.";
private static final String PUB_KEY_SIGN_MSG = "PublicKeys may not be used to create digital signatures. " +
"PrivateKeys are used to sign, and PublicKeys are used to verify.";
private static final String PRIV_KEY_ENC_MSG = "PrivateKeys may not be used to encrypt data. PublicKeys are " +
"used to encrypt, and PrivateKeys are used to decrypt.";
protected Provider provider;
protected SecureRandom secureRandom;
@ -85,21 +97,18 @@ public class DefaultJwtBuilder implements JwtBuilder {
private Payload payload = Payload.EMPTY;
private SecureDigestAlgorithm<Key, ?> sigAlg = Jwts.SIG.NONE;
private Function<SecureRequest<byte[], Key>, byte[]> signFunction;
private Function<SecureRequest<InputStream, Key>, byte[]> signFunction;
private AeadAlgorithm enc; // MUST be Symmetric AEAD per https://tools.ietf.org/html/rfc7516#section-4.1.2
private Function<AeadRequest, AeadResult> encFunction;
private KeyAlgorithm<Key, ?> keyAlg;
private Function<KeyRequest<Key>, KeyResult> keyAlgFunction;
private Key key;
protected Serializer<Map<String, ?>> serializer;
protected Function<Map<String, ?>, byte[]> headerSerializer;
protected Function<Map<String, ?>, byte[]> claimsSerializer;
private Serializer<Map<String, ?>> serializer;
protected Encoder<byte[], String> encoder = Encoders.BASE64URL;
protected Encoder<OutputStream, OutputStream> encoder = Base64UrlStreamEncoder.INSTANCE;
private boolean encodePayload = true;
protected CompressionAlgorithm compressionAlgorithm;
@ -130,46 +139,24 @@ public class DefaultJwtBuilder implements JwtBuilder {
return this;
}
protected <I, O> Function<I, O> wrap(Function<I, O> fn, String fmt, Object... args) {
return Functions.wrap(fn, SecurityException.class, fmt, args);
}
protected Function<Map<String, ?>, byte[]> wrap(final Serializer<Map<String, ?>> serializer, final String which) {
return new Function<Map<String, ?>, byte[]>() {
@Override
public byte[] apply(Map<String, ?> stringMap) {
try {
return serializer.serialize(stringMap);
} catch (Exception e) {
String fmt = String.format("Unable to serialize %s to JSON.", which);
String msg = fmt + " Cause: " + e.getMessage();
throw new SerializationException(msg);
}
}
};
}
@Override
public JwtBuilder serializeToJsonWith(final Serializer<Map<String, ?>> serializer) {
return serializer(serializer);
return json(serializer);
}
@Override
public JwtBuilder serializer(Serializer<Map<String, ?>> serializer) {
Assert.notNull(serializer, "Serializer cannot be null.");
this.serializer = serializer;
this.headerSerializer = wrap(serializer, "header");
this.claimsSerializer = wrap(serializer, "claims");
public JwtBuilder json(Serializer<Map<String, ?>> serializer) {
this.serializer = Assert.notNull(serializer, "JSON Serializer cannot be null.");
return this;
}
@Override
public JwtBuilder base64UrlEncodeWith(Encoder<byte[], String> encoder) {
return encoder(encoder);
return b64Url(new ByteBase64UrlStreamEncoder(encoder));
}
@Override
public JwtBuilder encoder(Encoder<byte[], String> encoder) {
public JwtBuilder b64Url(Encoder<OutputStream, OutputStream> encoder) {
Assert.notNull(encoder, "encoder cannot be null.");
this.encoder = encoder;
return this;
@ -189,24 +176,28 @@ public class DefaultJwtBuilder implements JwtBuilder {
@Override
public JwtBuilder setHeader(Map<String, ?> map) {
return this.headerBuilder.empty().add(map).and();
return header().empty().add(map).and();
}
@Override
public JwtBuilder setHeaderParams(Map<String, ?> params) {
return this.headerBuilder.add(params).and();
return header().add(params).and();
}
@Override
public JwtBuilder setHeaderParam(String name, Object value) {
return this.headerBuilder.add(name, value).and();
return header().add(name, value).and();
}
protected static <K extends Key> SecureDigestAlgorithm<K, ?> forSigningKey(K key) {
Assert.notNull(key, "Key cannot be null.");
SecureDigestAlgorithm<K, ?> alg = StandardSecureDigestAlgorithms.findBySigningKey(key);
if (alg == null) {
String msg = "Unable to determine a suitable MAC or Signature algorithm for the specified key using " + "available heuristics: either the key size is too weak be used with available algorithms, or the " + "key size is unavailable (e.g. if using a PKCS11 or HSM (Hardware Security Module) key store). " + "If you are using a PKCS11 or HSM keystore, consider using the " + "JwtBuilder.signWith(Key, SecureDigestAlgorithm) method instead.";
String msg = "Unable to determine a suitable MAC or Signature algorithm for the specified key using " +
"available heuristics: either the key size is too weak be used with available algorithms, or the " +
"key size is unavailable (e.g. if using a PKCS11 or HSM (Hardware Security Module) key store). " +
"If you are using a PKCS11 or HSM keystore, consider using the " +
"JwtBuilder.signWith(Key, SecureDigestAlgorithm) method instead.";
throw new UnsupportedKeyException(msg);
}
return alg;
@ -220,7 +211,9 @@ public class DefaultJwtBuilder implements JwtBuilder {
}
@Override
public <K extends Key> JwtBuilder signWith(K key, final SecureDigestAlgorithm<? super K, ?> alg) throws InvalidKeyException {
public <K extends Key> JwtBuilder signWith(K key, final SecureDigestAlgorithm<? super K, ?> alg)
throws InvalidKeyException {
Assert.notNull(key, "Key argument cannot be null.");
if (key instanceof PublicKey) { // it's always wrong/insecure to try to create signatures with PublicKeys:
throw new IllegalArgumentException(PUB_KEY_SIGN_MSG);
@ -248,9 +241,9 @@ public class DefaultJwtBuilder implements JwtBuilder {
this.key = key;
//noinspection unchecked
this.sigAlg = (SecureDigestAlgorithm<Key, ?>) alg;
this.signFunction = Functions.wrap(new Function<SecureRequest<byte[], Key>, byte[]>() {
this.signFunction = Functions.wrap(new Function<SecureRequest<InputStream, Key>, byte[]>() {
@Override
public byte[] apply(SecureRequest<byte[], Key> request) {
public byte[] apply(SecureRequest<InputStream, Key> request) {
return sigAlg.digest(request);
}
}, SignatureException.class, "Unable to compute %s signature.", id);
@ -270,7 +263,8 @@ public class DefaultJwtBuilder implements JwtBuilder {
public JwtBuilder signWith(io.jsonwebtoken.SignatureAlgorithm alg, byte[] secretKeyBytes) throws InvalidKeyException {
Assert.notNull(alg, "SignatureAlgorithm cannot be null.");
Assert.notEmpty(secretKeyBytes, "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.");
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.");
SecretKey key = new SecretKeySpec(secretKeyBytes, alg.getJcaName());
return signWith(key, alg);
}
@ -279,7 +273,8 @@ public class DefaultJwtBuilder implements JwtBuilder {
@Override
public JwtBuilder signWith(io.jsonwebtoken.SignatureAlgorithm alg, String base64EncodedSecretKey) throws InvalidKeyException {
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.");
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 = Decoders.BASE64.decode(base64EncodedSecretKey);
return signWith(alg, bytes);
}
@ -301,13 +296,7 @@ public class DefaultJwtBuilder implements JwtBuilder {
@Override
public <K extends Key> JwtBuilder encryptWith(final K key, final KeyAlgorithm<? super K, ?> keyAlg, final AeadAlgorithm enc) {
this.enc = Assert.notNull(enc, "Encryption algorithm cannot be null.");
final String encId = Assert.hasText(enc.getId(), "Encryption algorithm id cannot be null or empty.");
this.encFunction = wrap(new Function<AeadRequest, AeadResult>() {
@Override
public AeadResult apply(AeadRequest request) {
return enc.encrypt(request);
}
}, "%s encryption failed.", encId);
Assert.hasText(enc.getId(), "Encryption algorithm id cannot be null or empty.");
Assert.notNull(key, "Encryption key cannot be null.");
if (key instanceof PrivateKey) {
@ -350,7 +339,7 @@ public class DefaultJwtBuilder implements JwtBuilder {
@Override
public JwtBuilder content(String content) {
if (Strings.hasText(content)) {
this.payload = new Payload(content, Bytes.EMPTY, null);
this.payload = new Payload(content, null);
}
return this;
}
@ -358,7 +347,15 @@ public class DefaultJwtBuilder implements JwtBuilder {
@Override
public JwtBuilder content(byte[] content) {
if (!Bytes.isEmpty(content)) {
this.payload = new Payload(null, content, null);
this.payload = new Payload(content, null);
}
return this;
}
@Override
public JwtBuilder content(InputStream in) {
if (in != null) {
this.payload = new Payload(in, null);
}
return this;
}
@ -367,7 +364,25 @@ public class DefaultJwtBuilder implements JwtBuilder {
public JwtBuilder content(byte[] content, String cty) {
Assert.notEmpty(content, "content byte array cannot be null or empty.");
Assert.hasText(cty, "Content Type String cannot be null or empty.");
this.payload = new Payload(null, content, cty);
this.payload = new Payload(content, cty);
// clear out any previous value - it will be set appropriately during compact()
return header().delete(DefaultHeader.CONTENT_TYPE.getId()).and();
}
@Override
public JwtBuilder content(String content, String cty) throws IllegalArgumentException {
Assert.hasText(content, "Content string cannot be null or empty.");
Assert.hasText(cty, "ContentType string cannot be null or empty.");
this.payload = new Payload(content, cty);
// clear out any previous value - it will be set appropriately during compact()
return header().delete(DefaultHeader.CONTENT_TYPE.getId()).and();
}
@Override
public JwtBuilder content(InputStream in, String cty) throws IllegalArgumentException {
Assert.notNull(in, "Payload InputStream cannot be null.");
Assert.hasText(cty, "ContentType string cannot be null or empty.");
this.payload = new Payload(in, cty);
// clear out any previous value - it will be set appropriately during compact()
return header().delete(DefaultHeader.CONTENT_TYPE.getId()).and();
}
@ -410,32 +425,29 @@ public class DefaultJwtBuilder implements JwtBuilder {
@Override
public JwtBuilder subject(String sub) {
this.claimsBuilder.subject(sub);
return this;
return claims().subject(sub).and();
}
@Override
public JwtBuilder setAudience(String aud) {
this.claimsBuilder.setAudience(aud);
return this;
//noinspection deprecation
return claims().setAudience(aud).and();
}
@Override
public JwtBuilder audienceSingle(String aud) {
this.claimsBuilder.audienceSingle(aud);
return this;
//noinspection deprecation
return claims().audienceSingle(aud).and();
}
@Override
public JwtBuilder audience(String aud) {
this.claimsBuilder.audience(aud);
return this;
return claims().audience(aud).and();
}
@Override
public JwtBuilder audience(Collection<String> aud) {
this.claimsBuilder.audience(aud);
return this;
return claims().audience(aud).and();
}
@Override
@ -445,8 +457,7 @@ public class DefaultJwtBuilder implements JwtBuilder {
@Override
public JwtBuilder expiration(Date exp) {
this.claimsBuilder.expiration(exp);
return this;
return claims().expiration(exp).and();
}
@Override
@ -456,8 +467,7 @@ public class DefaultJwtBuilder implements JwtBuilder {
@Override
public JwtBuilder notBefore(Date nbf) {
this.claimsBuilder.notBefore(nbf);
return this;
return claims().notBefore(nbf).and();
}
@Override
@ -467,8 +477,7 @@ public class DefaultJwtBuilder implements JwtBuilder {
@Override
public JwtBuilder issuedAt(Date iat) {
this.claimsBuilder.issuedAt(iat);
return this;
return claims().issuedAt(iat).and();
}
@Override
@ -478,8 +487,7 @@ public class DefaultJwtBuilder implements JwtBuilder {
@Override
public JwtBuilder id(String jti) {
this.claimsBuilder.id(jti);
return this;
return claims().id(jti).and();
}
private void assertPayloadEncoding(String type) {
@ -492,110 +500,165 @@ public class DefaultJwtBuilder implements JwtBuilder {
@Override
public String compact() {
final boolean jwe = encFunction != null;
final boolean jwe = this.enc != null;
if (jwe && signFunction != null) {
String msg = "Both 'signWith' and 'encryptWith' cannot be specified. Choose either one.";
throw new IllegalStateException(msg);
}
Payload content = Assert.stateNotNull(this.payload, "content instance null, internal error");
Payload payload = Assert.stateNotNull(this.payload, "Payload instance null, internal error");
final Claims claims = this.claimsBuilder.build();
if (content.isEmpty() && Collections.isEmpty(claims)) {
if (jwe) { // JWE payload can never be empty:
String msg = "Encrypted JWTs must have either 'claims' or non-empty 'content'.";
throw new IllegalStateException(msg);
} else { //JWS or Unprotected JWT payloads can be empty
content = new Payload(null, Bytes.EMPTY, content.getContentType());
}
}
if (!content.isEmpty() && !Collections.isEmpty(claims)) {
if (jwe && payload.isEmpty() && Collections.isEmpty(claims)) { // JWE payload can never be empty:
String msg = "Encrypted JWTs must have either 'claims' or non-empty 'content'.";
throw new IllegalStateException(msg);
} // otherwise JWS and Unprotected JWT payloads can be empty
if (!payload.isEmpty() && !Collections.isEmpty(claims)) {
throw new IllegalStateException("Both 'content' and 'claims' cannot be specified. Choose either one.");
}
if (this.serializer == null) { // try to find one based on the services available
//noinspection unchecked
serializer(Services.loadFirst(Serializer.class));
json(Services.loadFirst(Serializer.class));
}
if (!Collections.isEmpty(claims)) { // normalize so we have one object to deal with:
byte[] serialized = claimsSerializer.apply(claims);
content = new Payload(null, serialized, null);
payload = new Payload(claims);
}
if (compressionAlgorithm != null && !content.isEmpty()) {
byte[] data = content.toByteArray();
data = compressionAlgorithm.compress(data);
content = new Payload(null, data, content.getContentType());
if (compressionAlgorithm != null && !payload.isEmpty()) {
payload.setZip(compressionAlgorithm);
this.headerBuilder.put(DefaultHeader.COMPRESSION_ALGORITHM.getId(), compressionAlgorithm.getId());
}
if (Strings.hasText(content.getContentType())) {
if (Strings.hasText(payload.getContentType())) {
// We retain the value from the content* calls to prevent accidental removal from
// header().empty() or header().delete calls
this.headerBuilder.contentType(content.getContentType());
this.headerBuilder.contentType(payload.getContentType());
}
Provider keyProvider = ProviderKey.getProvider(this.key, this.provider);
Key key = ProviderKey.getKey(this.key);
if (jwe) {
return encrypt(content, key, keyProvider);
return encrypt(payload, key, keyProvider);
} else if (key != null) {
return sign(content, key, keyProvider);
return sign(payload, key, keyProvider);
} else {
return unprotected(content);
return unprotected(payload);
}
}
private String sign(final Payload content, final Key key, final Provider provider) {
// automatically closes the OutputStream
private void writeAndClose(String name, Map<String, ?> map, OutputStream out) {
try {
Serializer<Map<String, ?>> named = new NamedSerializer(name, this.serializer);
named.serialize(map, out);
} finally {
Objects.nullSafeClose(out);
}
}
private void writeAndClose(String name, final Payload payload, OutputStream out) {
out = payload.compress(out); // compression if necessary
if (payload.isClaims()) {
writeAndClose(name, payload.getRequiredClaims(), out);
} else {
try {
InputStream in = payload.toInputStream();
Streams.copy(in, out, new byte[4096], "Unable to copy payload.");
} finally {
Objects.nullSafeClose(out);
}
}
}
private String sign(final Payload payload, final Key key, final Provider provider) {
Assert.stateNotNull(key, "Key is required."); // set by signWithWith*
Assert.stateNotNull(sigAlg, "SignatureAlgorithm is required."); // invariant
Assert.stateNotNull(signFunction, "Signature Algorithm function cannot be null.");
Assert.stateNotNull(content, "Payload argument cannot be null.");
Assert.stateNotNull(payload, "Payload argument cannot be null.");
final ByteArrayOutputStream jws = new ByteArrayOutputStream(4096);
// ----- header -----
this.headerBuilder.add(DefaultHeader.ALGORITHM.getId(), sigAlg.getId());
if (!this.encodePayload) { // b64 extension:
if (content.isEmpty()) {
String msg = "'b64' Unencoded payload option has been specified, but payload is empty.";
throw new IllegalStateException(msg);
}
String id = DefaultJwsHeader.B64.getId();
this.headerBuilder.critical(id).add(id, false);
}
final JwsHeader header = Assert.isInstanceOf(JwsHeader.class, this.headerBuilder.build());
encodeAndWrite("JWS Protected Header", header, jws);
final JwsHeader header = Assert.isInstanceOf(JwsHeader.class, this.headerBuilder.build(), "Invalid header created: ");
byte[] headerBytes = headerSerializer.apply(header);
String b64UrlHeader = encoder.encode(headerBytes);
String jwt = b64UrlHeader + DefaultJwtParser.SEPARATOR_CHAR;
String payloadString = Strings.EMPTY; // can be empty for JWS
// ----- separator -----
jws.write(DefaultJwtParser.SEPARATOR_CHAR);
// ----- payload -----
// Logic defined by table in https://datatracker.ietf.org/doc/html/rfc7797#section-3 :
byte[] signingInput;
InputStream signingInput;
InputStream payloadStream = null; // not needed unless b64 is enabled
if (this.encodePayload) {
if (!content.isEmpty()) {
payloadString = encoder.encode(content.toByteArray());
encodeAndWrite("JWS Payload", payload, jws);
signingInput = new ByteArrayInputStream(jws.toByteArray());
} else { // b64
// First, ensure we have the base64url header bytes + the SEPARATOR_CHAR byte:
ByteArrayInputStream prefixStream = new ByteArrayInputStream(jws.toByteArray());
// Next, b64 extension requires the raw (non-encoded) payload to be included directly in the signing input,
// so we ensure we have an input stream for that:
if (payload.isClaims() || payload.isCompressed()) {
ByteArrayOutputStream claimsOut = new ByteArrayOutputStream(8192);
writeAndClose("JWS Unencoded Payload", payload, claimsOut);
payloadStream = new ByteArrayInputStream(claimsOut.toByteArray());
} else {
// No claims and not compressed, so just get the direct InputStream:
payloadStream = Assert.stateNotNull(payload.toInputStream(), "Payload InputStream cannot be null.");
}
jwt += payloadString;
signingInput = jwt.getBytes(StandardCharsets.US_ASCII);
} else {
// b64 extension payload included directly in signing input:
signingInput = Bytes.concat(jwt.getBytes(StandardCharsets.US_ASCII), content.toByteArray());
if (Strings.hasText(content.getString())) {
// 'unencoded non-detached' per https://datatracker.ietf.org/doc/html/rfc7797#section-5.2
jwt += content.getString();
if (!payload.isClaims()) {
payloadStream = new CountingInputStream(payloadStream); // we'll need to assert if it's empty later
}
if (payloadStream.markSupported()) {
payloadStream.mark(0); // to rewind
}
// (base64url header bytes + separator char) + raw payload bytes:
// and don't let the SequenceInputStream close the payloadStream in case reset is needed:
signingInput = new SequenceInputStream(prefixStream, new UncloseableInputStream(payloadStream));
}
SecureRequest<byte[], Key> request = new DefaultSecureRequest<>(signingInput, provider, secureRandom, key);
byte[] signature = signFunction.apply(request);
String base64UrlSignature = encoder.encode(signature);
jwt += DefaultJwtParser.SEPARATOR_CHAR + base64UrlSignature;
byte[] signature;
try {
SecureRequest<InputStream, Key> request = new DefaultSecureRequest<>(signingInput, provider, secureRandom, key);
signature = signFunction.apply(request);
return jwt;
// now that we've calculated the signature, if using the b64 extension, and the payload is
// attached ('non-detached'), we need to include it in the jws before the signature token.
// (Note that if encodePayload is true, the payload has already been written to jws at this point, so
// we only need to write if encodePayload is false and the payload is attached):
if (!this.encodePayload) {
if (!payload.isCompressed() // don't print raw compressed bytes
&& (payload.isClaims() || payload.isString())) {
// now add the payload to the jws output:
Streams.copy(payloadStream, jws, new byte[8192], "Unable to copy attached Payload InputStream.");
}
if (payloadStream instanceof CountingInputStream && ((CountingInputStream) payloadStream).getCount() <= 0) {
String msg = "'b64' Unencoded payload option has been specified, but payload is empty.";
throw new IllegalStateException(msg);
}
}
} finally {
Streams.reset(payloadStream);
}
// ----- separator -----
jws.write(DefaultJwtParser.SEPARATOR_CHAR);
// ----- signature -----
encodeAndWrite("JWS Signature", signature, jws);
return Strings.utf8(jws.toByteArray());
}
private String unprotected(final Payload content) {
@ -604,18 +667,34 @@ public class DefaultJwtBuilder implements JwtBuilder {
assertPayloadEncoding("unprotected JWT");
this.headerBuilder.add(DefaultHeader.ALGORITHM.getId(), Jwts.SIG.NONE.getId());
final ByteArrayOutputStream jwt = new ByteArrayOutputStream(512);
// ----- header -----
final Header header = this.headerBuilder.build();
byte[] headerBytes = headerSerializer.apply(header);
encodeAndWrite("JWT Header", header, jwt);
String b64UrlHeader = encoder.encode(headerBytes);
String b64UrlPayload = Strings.EMPTY;
if (!content.isEmpty()) {
b64UrlPayload = encoder.encode(content.toByteArray());
}
// ----- separator -----
jwt.write(DefaultJwtParser.SEPARATOR_CHAR);
return b64UrlHeader + DefaultJwtParser.SEPARATOR_CHAR +
// Must terminate with a period per https://www.rfc-editor.org/rfc/rfc7519#section-6.1 :
b64UrlPayload + DefaultJwtParser.SEPARATOR_CHAR;
// ----- payload -----
encodeAndWrite("JWT Payload", content, jwt);
// ----- period terminator -----
jwt.write(DefaultJwtParser.SEPARATOR_CHAR); // https://www.rfc-editor.org/rfc/rfc7519#section-6.1
return Strings.ascii(jwt.toByteArray());
}
private void encrypt(final AeadRequest req, final AeadResult res) throws SecurityException {
Function<Object, Object> fn = Functions.wrap(new Function<Object, Object>() {
@Override
public Object apply(Object o) {
enc.encrypt(req, res);
return null;
}
}, SecurityException.class, "%s encryption failed.", enc.getId());
fn.apply(null);
}
private String encrypt(final Payload content, final Key key, final Provider keyProvider) {
@ -623,11 +702,18 @@ public class DefaultJwtBuilder implements JwtBuilder {
Assert.stateNotNull(content, "Payload argument cannot be null.");
Assert.stateNotNull(key, "Key is required."); // set by encryptWith*
Assert.stateNotNull(enc, "Encryption algorithm is required."); // set by encryptWith*
Assert.stateNotNull(encFunction, "Encryption function cannot be null.");
Assert.stateNotNull(keyAlg, "KeyAlgorithm is required."); //set by encryptWith*
Assert.stateNotNull(keyAlgFunction, "KeyAlgorithm function cannot be null.");
assertPayloadEncoding("JWE");
final byte[] payload = Assert.notEmpty(content.toByteArray(), "JWE payload bytes cannot be empty."); // JWE invariant (JWS can be empty however)
InputStream plaintext;
if (content.isClaims()) {
ByteArrayOutputStream out = new ByteArrayOutputStream(4096);
writeAndClose("JWE Claims", content, out);
plaintext = new ByteArrayInputStream(out.toByteArray());
} else {
plaintext = content.toInputStream();
}
//only expose (mutable) JweHeader functionality to KeyAlgorithm instances, not the full headerBuilder
// (which exposes this JwtBuilder and shouldn't be referenced by KeyAlgorithms):
@ -637,34 +723,51 @@ public class DefaultJwtBuilder implements JwtBuilder {
Assert.stateNotNull(keyResult, "KeyAlgorithm must return a KeyResult.");
SecretKey cek = Assert.notNull(keyResult.getKey(), "KeyResult must return a content encryption key.");
byte[] encryptedCek = Assert.notNull(keyResult.getPayload(), "KeyResult must return an encrypted key byte array, even if empty.");
byte[] encryptedCek = Assert.notNull(keyResult.getPayload(),
"KeyResult must return an encrypted key byte array, even if empty.");
this.headerBuilder.add(DefaultHeader.ALGORITHM.getId(), keyAlg.getId());
this.headerBuilder.put(DefaultJweHeader.ENCRYPTION_ALGORITHM.getId(), enc.getId());
final JweHeader header = Assert.isInstanceOf(JweHeader.class, this.headerBuilder.build(), "Invalid header created: ");
final JweHeader header = Assert.isInstanceOf(JweHeader.class, this.headerBuilder.build(),
"Invalid header created: ");
byte[] headerBytes = this.headerSerializer.apply(header);
final String base64UrlEncodedHeader = encoder.encode(headerBytes);
byte[] aad = base64UrlEncodedHeader.getBytes(StandardCharsets.US_ASCII);
// ----- header -----
ByteArrayOutputStream jwe = new ByteArrayOutputStream(8192);
encodeAndWrite("JWE Protected Header", header, jwe);
// JWE RFC requires AAD to be the ASCII bytes of the Base64URL-encoded header. Since the header bytes are
// already Base64URL-encoded at this point (via the encoder.wrap call just above), and Base64Url-encoding uses
// only ASCII characters, we don't need to use StandardCharsets.US_ASCII to explicitly convert here - just
// use the already-encoded (ascii) bytes:
InputStream aad = Streams.of(jwe.toByteArray());
// During encryption, the configured Provider applies to the KeyAlgorithm, not the AeadAlgorithm, mostly
// because all JVMs support the standard AeadAlgorithms (especially with BouncyCastle in the classpath).
// As such, the provider here is intentionally omitted (null):
// TODO: add encProvider(Provider) builder method that applies to this request only?
AeadRequest encRequest = new DefaultAeadRequest(payload, null, secureRandom, cek, aad);
AeadResult encResult = encFunction.apply(encRequest);
ByteArrayOutputStream ciphertextOut = new ByteArrayOutputStream(8192);
AeadRequest req = new DefaultAeadRequest(plaintext, null, secureRandom, cek, aad);
DefaultAeadResult res = new DefaultAeadResult(ciphertextOut);
encrypt(req, res);
byte[] iv = Assert.notEmpty(encResult.getInitializationVector(), "Encryption result must have a non-empty initialization vector.");
byte[] ciphertext = Assert.notEmpty(encResult.getPayload(), "Encryption result must have non-empty ciphertext (result.getData()).");
byte[] tag = Assert.notEmpty(encResult.getDigest(), "Encryption result must have a non-empty authentication tag.");
byte[] iv = Assert.notEmpty(res.getIv(), "Encryption result must have a non-empty initialization vector.");
byte[] tag = Assert.notEmpty(res.getDigest(), "Encryption result must have a non-empty authentication tag.");
byte[] ciphertext = Assert.notEmpty(ciphertextOut.toByteArray(), "Encryption result must have non-empty ciphertext.");
String base64UrlEncodedEncryptedCek = encoder.encode(encryptedCek);
String base64UrlEncodedIv = encoder.encode(iv);
String base64UrlEncodedCiphertext = encoder.encode(ciphertext);
String base64UrlEncodedTag = encoder.encode(tag);
jwe.write(DefaultJwtParser.SEPARATOR_CHAR);
encodeAndWrite("JWE Encrypted CEK", encryptedCek, jwe);
return base64UrlEncodedHeader + DefaultJwtParser.SEPARATOR_CHAR + base64UrlEncodedEncryptedCek + DefaultJwtParser.SEPARATOR_CHAR + base64UrlEncodedIv + DefaultJwtParser.SEPARATOR_CHAR + base64UrlEncodedCiphertext + DefaultJwtParser.SEPARATOR_CHAR + base64UrlEncodedTag;
jwe.write(DefaultJwtParser.SEPARATOR_CHAR);
encodeAndWrite("JWE Initialization Vector", iv, jwe);
jwe.write(DefaultJwtParser.SEPARATOR_CHAR);
encodeAndWrite("JWE Ciphertext", ciphertext, jwe);
jwe.write(DefaultJwtParser.SEPARATOR_CHAR);
encodeAndWrite("JWE AAD Tag", tag, jwe);
return Strings.utf8(jwe.toByteArray());
}
private static class DefaultBuilderClaims extends DelegatingClaimsMutator<BuilderClaims> implements BuilderClaims {
@ -710,4 +813,24 @@ public class DefaultJwtBuilder implements JwtBuilder {
}
}
private OutputStream encode(OutputStream out, String name) {
out = this.encoder.encode(out);
return new EncodingOutputStream(out, "base64url", name);
}
private void encodeAndWrite(String name, Map<String, ?> map, OutputStream out) {
out = encode(out, name);
writeAndClose(name, map, out);
}
private void encodeAndWrite(String name, Payload payload, OutputStream out) {
out = encode(out, name);
writeAndClose(name, payload, out);
}
private void encodeAndWrite(String name, byte[] data, OutputStream out) {
out = encode(out, name);
Streams.writeAndClose(out, data, "Unable to write bytes");
}
}

View File

@ -39,35 +39,45 @@ import io.jsonwebtoken.PrematureJwtException;
import io.jsonwebtoken.ProtectedHeader;
import io.jsonwebtoken.SigningKeyResolver;
import io.jsonwebtoken.UnsupportedJwtException;
import io.jsonwebtoken.impl.io.JsonObjectDeserializer;
import io.jsonwebtoken.impl.io.Streams;
import io.jsonwebtoken.impl.io.UncloseableInputStream;
import io.jsonwebtoken.impl.lang.Bytes;
import io.jsonwebtoken.impl.lang.Function;
import io.jsonwebtoken.impl.security.DefaultAeadResult;
import io.jsonwebtoken.impl.lang.RedactedSupplier;
import io.jsonwebtoken.impl.security.DefaultDecryptAeadRequest;
import io.jsonwebtoken.impl.security.DefaultDecryptionKeyRequest;
import io.jsonwebtoken.impl.security.DefaultVerifySecureDigestRequest;
import io.jsonwebtoken.impl.security.LocatingKeyResolver;
import io.jsonwebtoken.impl.security.ProviderKey;
import io.jsonwebtoken.io.CompressionAlgorithm;
import io.jsonwebtoken.io.Decoder;
import io.jsonwebtoken.io.DecodingException;
import io.jsonwebtoken.io.DeserializationException;
import io.jsonwebtoken.io.Deserializer;
import io.jsonwebtoken.lang.Arrays;
import io.jsonwebtoken.lang.Assert;
import io.jsonwebtoken.lang.Classes;
import io.jsonwebtoken.lang.Collections;
import io.jsonwebtoken.lang.DateFormats;
import io.jsonwebtoken.lang.Objects;
import io.jsonwebtoken.lang.Strings;
import io.jsonwebtoken.security.AeadAlgorithm;
import io.jsonwebtoken.security.DecryptAeadRequest;
import io.jsonwebtoken.security.DecryptionKeyRequest;
import io.jsonwebtoken.security.InvalidKeyException;
import io.jsonwebtoken.security.KeyAlgorithm;
import io.jsonwebtoken.security.Message;
import io.jsonwebtoken.security.SecureDigestAlgorithm;
import io.jsonwebtoken.security.SignatureException;
import io.jsonwebtoken.security.VerifySecureDigestRequest;
import io.jsonwebtoken.security.WeakKeyException;
import javax.crypto.SecretKey;
import java.io.BufferedInputStream;
import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.InputStream;
import java.io.SequenceInputStream;
import java.nio.ByteBuffer;
import java.nio.CharBuffer;
import java.nio.charset.StandardCharsets;
import java.security.Key;
import java.security.PrivateKey;
@ -75,6 +85,7 @@ import java.security.Provider;
import java.security.PublicKey;
import java.util.Collection;
import java.util.Date;
import java.util.LinkedHashSet;
import java.util.Map;
import java.util.Set;
@ -147,8 +158,8 @@ public class DefaultJwtParser implements JwtParser {
private static final String B64_MISSING_PAYLOAD = "Unable to verify JWS signature: the parser has encountered an " +
"Unencoded Payload JWS with detached payload, but the detached payload value required for signature " +
"verification has not been provided. If you expect to receive and parse Unencoded Payload JWSs in your " +
"application, the JwtParser.parseContentJws(String, byte[]) or JwtParser.parseClaimsJws(String, byte[]) " +
"methods must be used for these kinds of JWSs. Header: %s";
"application, the overloaded JwtParser.parseContentJws or JwtParser.parseClaimsJws methods that " +
"accept a byte[] or InputStream must be used for these kinds of JWSs. Header: %s";
private static final String B64_DECOMPRESSION_MSG = "The JWT header references compression algorithm " +
"'%s', but payload decompression for Unencoded JWSs (those with an " + DefaultJwsHeader.B64 +
@ -187,7 +198,7 @@ public class DefaultJwtParser implements JwtParser {
private final Locator<? extends Key> keyLocator;
private final Decoder<String, byte[]> decoder;
private final Decoder<InputStream, InputStream> decoder;
private final Deserializer<Map<String, ?>> deserializer;
@ -210,7 +221,7 @@ public class DefaultJwtParser implements JwtParser {
Set<String> critical,
long allowedClockSkewMillis,
DefaultClaims expectedClaims,
Decoder<String, byte[]> base64UrlDecoder,
Decoder<InputStream, InputStream> base64UrlDecoder,
Deserializer<Map<String, ?>> deserializer,
CompressionCodecResolver compressionCodecResolver,
Collection<CompressionAlgorithm> extraZipAlgs,
@ -223,11 +234,11 @@ public class DefaultJwtParser implements JwtParser {
this.signingKeyResolver = signingKeyResolver;
this.keyLocator = Assert.notNull(keyLocator, "Key Locator cannot be null.");
this.clock = Assert.notNull(clock, "Clock cannot be null.");
this.critical = Assert.notNull(critical, "Critical set cannot be null (but it can be empty).");
this.critical = Collections.nullSafe(critical);
this.allowedClockSkewMillis = allowedClockSkewMillis;
this.expectedClaims = Jwts.claims().add(expectedClaims);
this.decoder = Assert.notNull(base64UrlDecoder, "base64UrlDecoder cannot be null.");
this.deserializer = Assert.notNull(deserializer, "Deserializer cannot be null.");
this.deserializer = Assert.notNull(deserializer, "JSON Deserializer cannot be null.");
this.sigAlgFn = new IdLocator<>(DefaultHeader.ALGORITHM, Jwts.SIG.get(), extraSigAlgs, MISSING_JWS_ALG_MSG);
this.keyAlgFn = new IdLocator<>(DefaultHeader.ALGORITHM, Jwts.KEY.get(), extraKeyAlgs, MISSING_JWE_ALG_MSG);
@ -253,79 +264,8 @@ public class DefaultJwtParser implements JwtParser {
return header != null && Strings.hasText(header.getContentType());
}
/**
* Returns {@code true} IFF the specified payload starts with a <code>&#123;</code> character and ends with a
* <code>&#125;</code> character, ignoring any leading or trailing whitespace as defined by
* {@link Character#isWhitespace(char)}. This does not guarantee JSON, just that it is likely JSON and
* should be passed to a JSON Deserializer to see if it is actually JSON. If this {@code returns false}, it
* should be considered a byte[] payload and <em>not</em> delegated to a JSON Deserializer.
*
* @param payload the byte array that could be JSON
* @return {@code true} IFF the specified payload starts with a <code>&#123;</code> character and ends with a
* <code>&#125;</code> character, ignoring any leading or trailing whitespace as defined by
* {@link Character#isWhitespace(char)}
* @since JJWT_RELEASE_VERSION
*/
private static boolean isLikelyJson(byte[] payload) {
int len = Bytes.length(payload);
if (len == 0) {
return false;
}
int maxIndex = len - 1;
int jsonStartIndex = -1; // out of bounds means didn't find any
int jsonEndIndex = len; // out of bounds means didn't find any
for (int i = 0; i < len; i++) {
int c = payload[i];
if (c == '{') {
jsonStartIndex = i;
break;
}
}
if (jsonStartIndex == -1) { //exhausted entire payload, didn't find starting '{', can't be a JSON object
return false;
}
if (jsonStartIndex > 0) {
// we found content at the start of the payload, but before the first '{' character, so we need to check
// to see if any of it (when UTF-8 decoded) is not whitespace. If so, it can't be a valid JSON object.
byte[] leading = new byte[jsonStartIndex];
System.arraycopy(payload, 0, leading, 0, jsonStartIndex);
String s = new String(leading, StandardCharsets.UTF_8);
if (Strings.hasText(s)) { // found something before '{' that isn't whitespace; can't be a valid JSON object
return false;
}
}
for (int i = maxIndex; i > jsonStartIndex; i--) {
int c = payload[i];
if (c == '}') {
jsonEndIndex = i;
break;
}
}
if (jsonEndIndex > maxIndex) { // found start '{' char, but no closing '} char. Can't be a JSON object
return false;
}
if (jsonEndIndex < maxIndex) {
// We found content at the end of the payload, after the last '}' character. We need to check to see if
// any of it (when UTF-8 decoded) is not whitespace. If so, it's not a valid JSON object payload.
int size = maxIndex - jsonEndIndex;
byte[] trailing = new byte[size];
System.arraycopy(payload, jsonEndIndex + 1, trailing, 0, size);
String s = new String(trailing, StandardCharsets.UTF_8);
return !Strings.hasText(s); // if just whitespace after last '}', we can assume JSON and try and parse it
}
return true;
}
private void verifySignature(final TokenizedJwt tokenized, final JwsHeader jwsHeader, final String alg,
@SuppressWarnings("deprecation") SigningKeyResolver resolver, Claims claims, byte[] payload) {
@SuppressWarnings("deprecation") SigningKeyResolver resolver, Claims claims, Payload payload) {
Assert.notNull(resolver, "SigningKeyResolver instance cannot be null.");
@ -344,7 +284,7 @@ public class DefaultJwtParser implements JwtParser {
if (claims != null) {
key = resolver.resolveSigningKey(jwsHeader, claims);
} else {
key = resolver.resolveSigningKey(jwsHeader, payload);
key = resolver.resolveSigningKey(jwsHeader, payload.getBytes());
}
if (key == null) {
String msg = "Cannot verify JWS signature: unable to locate signature verification key for JWS with header: " + jwsHeader;
@ -360,14 +300,34 @@ public class DefaultJwtParser implements JwtParser {
final byte[] signature = decode(tokenized.getDigest(), "JWS signature");
//re-create the jwt part without the signature. This is what is needed for signature verification:
byte[] verificationInput;
String jwtPrefix = tokenized.getProtected() + SEPARATOR_CHAR;
InputStream payloadStream = null;
InputStream verificationInput;
if (jwsHeader.isPayloadEncoded()) {
jwtPrefix += tokenized.getPayload();
verificationInput = jwtPrefix.getBytes(StandardCharsets.US_ASCII);
} else {
byte[] prefixBytes = jwtPrefix.getBytes(StandardCharsets.US_ASCII);
verificationInput = Bytes.concat(prefixBytes, payload);
int len = tokenized.getProtected().length() + 1 + tokenized.getPayload().length();
CharBuffer cb = CharBuffer.allocate(len);
cb.put(Strings.wrap(tokenized.getProtected()));
cb.put(SEPARATOR_CHAR);
cb.put(Strings.wrap(tokenized.getPayload()));
cb.rewind();
ByteBuffer bb = StandardCharsets.US_ASCII.encode(cb);
bb.rewind();
byte[] data = new byte[bb.remaining()];
bb.get(data);
verificationInput = new ByteArrayInputStream(data);
} else { // b64 extension
ByteBuffer headerBuf = StandardCharsets.US_ASCII.encode(Strings.wrap(tokenized.getProtected()));
headerBuf.rewind();
ByteBuffer buf = ByteBuffer.allocate(headerBuf.remaining() + 1);
buf.put(headerBuf);
buf.put((byte) SEPARATOR_CHAR);
buf.rewind();
byte[] data = new byte[buf.remaining()];
buf.get(data);
InputStream prefixStream = new ByteArrayInputStream(data);
payloadStream = payload.toInputStream();
// We wrap the payloadStream here in an UncloseableInputStream to prevent the SequenceInputStream from
// closing it since we'll need to rewind/reset it if decompression is enabled
verificationInput = new SequenceInputStream(prefixStream, new UncloseableInputStream(payloadStream));
}
try {
@ -390,28 +350,34 @@ public class DefaultJwtParser implements JwtParser {
"trusted. Another possibility is that the parser was provided the incorrect " +
"signature verification key, but this cannot be assumed for security reasons.";
throw new UnsupportedJwtException(msg, e);
} finally {
Streams.reset(payloadStream);
}
}
@Override
public Jwt<?, ?> parse(String compact) {
return parse(compact, Bytes.EMPTY);
CharBuffer buffer = Strings.wrap(compact); // so compact.subsequence calls don't add new Strings on the heap
return parse(buffer, Payload.EMPTY);
}
private Jwt<?, ?> parse(String compact, final byte[] unencodedPayload) throws ExpiredJwtException, MalformedJwtException, SignatureException {
private Jwt<?, ?> parse(CharSequence compact, Payload unencodedPayload)
throws ExpiredJwtException, MalformedJwtException, SignatureException {
Assert.hasText(compact, "JWT String cannot be null or empty.");
Assert.stateNotNull(unencodedPayload, "internal error: unencodedPayload is null.");
final TokenizedJwt tokenized = jwtTokenizer.tokenize(compact);
final String base64UrlHeader = tokenized.getProtected();
final CharSequence base64UrlHeader = tokenized.getProtected();
if (!Strings.hasText(base64UrlHeader)) {
String msg = "Compact JWT strings MUST always have a Base64Url protected header per https://tools.ietf.org/html/rfc7519#section-7.2 (steps 2-4).";
String msg = "Compact JWT strings MUST always have a Base64Url protected header per " +
"https://tools.ietf.org/html/rfc7519#section-7.2 (steps 2-4).";
throw new MalformedJwtException(msg);
}
// =============== Header =================
final byte[] headerBytes = decode(base64UrlHeader, "protected header");
Map<String, ?> m = deserialize(headerBytes, "protected header");
Map<String, ?> m = deserialize(new ByteArrayInputStream(headerBytes), "protected header");
Header header;
try {
header = tokenized.createHeader(m);
@ -433,7 +399,7 @@ public class DefaultJwtParser implements JwtParser {
}
final boolean unsecured = Jwts.SIG.NONE.getId().equalsIgnoreCase(alg);
final String base64UrlDigest = tokenized.getDigest();
final CharSequence base64UrlDigest = tokenized.getDigest();
final boolean hasDigest = Strings.hasText(base64UrlDigest);
if (unsecured) {
if (tokenized instanceof TokenizedJwe) {
@ -459,13 +425,23 @@ public class DefaultJwtParser implements JwtParser {
// ----- crit assertions -----
if (header instanceof ProtectedHeader) {
Set<String> crit = Collections.nullSafe(((ProtectedHeader) header).getCritical());
Set<String> supportedCrit = this.critical;
String b64Id = DefaultJwsHeader.B64.getId();
if (!unencodedPayload.isEmpty() && !this.critical.contains(b64Id)) {
// The application developer explicitly indicates they're using a B64 payload, so
// ensure that the B64 crit header is supported, even if they forgot to configure it on the
// parser builder:
supportedCrit = new LinkedHashSet<>(Collections.size(this.critical) + 1);
supportedCrit.add(DefaultJwsHeader.B64.getId());
supportedCrit.addAll(this.critical);
}
// assert any values per https://www.rfc-editor.org/rfc/rfc7515.html#section-4.1.11:
for (String name : crit) {
if (!header.containsKey(name)) {
String msg = String.format(CRIT_MISSING_MSG, name, name, header);
throw new MalformedJwtException(msg);
}
if (!this.critical.contains(name)) {
if (!supportedCrit.contains(name)) {
String msg = String.format(CRIT_UNSUPPORTED_MSG, name, name, header);
throw new UnsupportedJwtException(msg);
}
@ -473,27 +449,27 @@ public class DefaultJwtParser implements JwtParser {
}
// =============== Payload =================
final String payloadToken = tokenized.getPayload();
byte[] payload;
final CharSequence payloadToken = tokenized.getPayload();
Payload payload;
boolean integrityVerified = false; // only true after successful signature verification or AEAD decryption
// check if b64 extension enabled:
final boolean payloadBase64UrlEncoded = !(header instanceof JwsHeader) || ((JwsHeader) header).isPayloadEncoded();
if (payloadBase64UrlEncoded) {
// standard encoding, so decode it:
payload = decode(tokenized.getPayload(), "payload");
byte[] data = decode(tokenized.getPayload(), "payload");
payload = new Payload(data, header.getContentType());
} else {
// The JWT uses the b64 extension, and we already know the parser supports that extension at this point
// in the code execution path because of the ----- crit ----- assertions section above as well as the
// (JwsHeader).isPayloadEncoded() check
if (Strings.hasText(payloadToken)) {
// we need to verify what was in the token, otherwise it'd be a security issue if we ignored it
// and assumed the (likely safe) unencodedPayload value instead:
payload = Strings.utf8(payloadToken);
payload = new Payload(payloadToken, header.getContentType());
} else {
//no payload token (a detached payload), so we need to ensure that they've specified the payload value:
if (Bytes.isEmpty(unencodedPayload)) {
if (unencodedPayload.isEmpty()) {
String msg = String.format(B64_MISSING_PAYLOAD, header);
throw new SignatureException(msg);
}
@ -502,7 +478,7 @@ public class DefaultJwtParser implements JwtParser {
}
}
if (tokenized instanceof TokenizedJwe && Bytes.isEmpty(payload)) {
if (tokenized instanceof TokenizedJwe && payload.isEmpty()) {
// Only JWS payload can be empty per https://github.com/jwtk/jjwt/pull/540
String msg = "Compact JWE strings MUST always contain a payload (ciphertext).";
throw new MalformedJwtException(msg);
@ -516,10 +492,10 @@ public class DefaultJwtParser implements JwtParser {
JweHeader jweHeader = Assert.stateIsInstance(JweHeader.class, header, "Not a JweHeader. ");
byte[] cekBytes = Bytes.EMPTY; //ignored unless using an encrypted key algorithm
String base64Url = tokenizedJwe.getEncryptedKey();
CharSequence base64Url = tokenizedJwe.getEncryptedKey();
if (Strings.hasText(base64Url)) {
cekBytes = decode(base64Url, "JWE encrypted key");
if (Arrays.length(cekBytes) == 0) {
if (Bytes.isEmpty(cekBytes)) {
String msg = "Compact JWE string represents an encrypted key, but the key is empty.";
throw new MalformedJwtException(msg);
}
@ -529,7 +505,7 @@ public class DefaultJwtParser implements JwtParser {
if (Strings.hasText(base64Url)) {
iv = decode(base64Url, "JWE Initialization Vector");
}
if (Arrays.length(iv) == 0) {
if (Bytes.isEmpty(iv)) {
String msg = "Compact JWE strings must always contain an Initialization Vector.";
throw new MalformedJwtException(msg);
}
@ -537,13 +513,16 @@ public class DefaultJwtParser implements JwtParser {
// The AAD (Additional Authenticated Data) scheme for compact JWEs is to use the ASCII bytes of the
// raw base64url text as the AAD, and NOT the base64url-decoded bytes per
// https://www.rfc-editor.org/rfc/rfc7516.html#section-5.1, Step 14.
final byte[] aad = base64UrlHeader.getBytes(StandardCharsets.US_ASCII);
ByteBuffer buf = StandardCharsets.US_ASCII.encode(Strings.wrap(base64UrlHeader));
final byte[] aadBytes = new byte[buf.remaining()];
buf.get(aadBytes);
InputStream aad = new ByteArrayInputStream(aadBytes);
base64Url = base64UrlDigest;
//guaranteed to be non-empty via the `alg` + digest check above:
Assert.hasText(base64Url, "JWE AAD Authentication Tag cannot be null or empty.");
tag = decode(base64Url, "JWE AAD Authentication Tag");
if (Arrays.length(tag) == 0) {
if (Bytes.isEmpty(tag)) {
String msg = "Compact JWE strings must always contain an AAD Authentication Tag.";
throw new MalformedJwtException(msg);
}
@ -583,10 +562,11 @@ public class DefaultJwtParser implements JwtParser {
// because all JVMs support the standard AeadAlgorithms (especially with BouncyCastle in the classpath).
// As such, the provider here is intentionally omitted (null):
// TODO: add encProvider(Provider) builder method that applies to this request only?
DecryptAeadRequest decryptRequest =
new DefaultAeadResult(null, null, payload, cek, aad, tag, iv);
Message<byte[]> result = encAlg.decrypt(decryptRequest);
payload = result.getPayload();
InputStream ciphertext = payload.toInputStream();
ByteArrayOutputStream plaintext = new ByteArrayOutputStream(8192);
DecryptAeadRequest dreq = new DefaultDecryptAeadRequest(ciphertext, cek, aad, iv, tag);
encAlg.decrypt(dreq, plaintext);
payload = new Payload(plaintext.toByteArray(), header.getContentType());
integrityVerified = true; // AEAD performs integrity verification, so no exception = verified
@ -595,7 +575,7 @@ public class DefaultJwtParser implements JwtParser {
// always safer:
JwsHeader jwsHeader = Assert.stateIsInstance(JwsHeader.class, header, "Not a JwsHeader. ");
verifySignature(tokenized, jwsHeader, alg, new LocatingKeyResolver(this.keyLocator), null, payload);
integrityVerified = true; // no exception = signature verified
integrityVerified = true; // no exception means signature verified
}
final CompressionAlgorithm compressionAlgorithm = zipAlgFn.apply(header);
@ -609,34 +589,59 @@ public class DefaultJwtParser implements JwtParser {
throw new UnsupportedJwtException(msg);
}
}
payload = compressionAlgorithm.decompress(payload);
payload = payload.decompress(compressionAlgorithm);
}
Claims claims = null;
if (!hasContentType(header) && // If there is a content type set, then the application using JJWT is expected
byte[] payloadBytes = payload.getBytes();
if (payload.isConsumable()) {
InputStream in = payload.toInputStream();
if (!hasContentType(header)) { // If there is a content type set, then the application using JJWT is expected
// to convert the byte payload themselves based on this content type
// https://www.rfc-editor.org/rfc/rfc7515.html#section-4.1.10 :
//
// "This parameter is ignored by JWS implementations; any processing of this
// parameter is performed by the JWS application."
//
isLikelyJson(payload)) { // likely to be json, try to deserialize it:
Map<String, ?> claimsMap = deserialize(payload, "claims");
try {
claims = new DefaultClaims(claimsMap);
} catch (Exception e) {
String msg = "Invalid claims: " + e.getMessage();
throw new MalformedJwtException(msg, e);
Map<String, ?> claimsMap = null;
try {
// if deserialization fails, we'll need to rewind to convert to a byte array. So if
// mark/reset isn't possible, we'll need to buffer:
if (!in.markSupported()) {
in = new BufferedInputStream(in);
in.mark(0);
}
claimsMap = deserialize(new UncloseableInputStream(in) /* Don't close in case we need to rewind */, "claims");
} catch (DeserializationException | MalformedJwtException ignored) { // not JSON, treat it as a byte[]
// String msg = "Invalid claims: " + e.getMessage();
// throw new MalformedJwtException(msg, e);
} finally {
Streams.reset(in);
}
if (claimsMap != null) {
try {
claims = new DefaultClaims(claimsMap);
} catch (Throwable t) {
String msg = "Invalid claims: " + t.getMessage();
throw new MalformedJwtException(msg);
}
}
}
if (claims == null) {
// consumable, but not claims, so convert to byte array:
payloadBytes = Streams.bytes(in, "Unable to convert payload to byte array.");
}
}
Jwt<?, ?> jwt;
Object body = claims != null ? claims : payload;
Object body = claims != null ? claims : payloadBytes;
if (header instanceof JweHeader) {
jwt = new DefaultJwe<>((JweHeader) header, body, iv, tag);
} else if (hasDigest) {
JwsHeader jwsHeader = Assert.isInstanceOf(JwsHeader.class, header, "JwsHeader required.");
jwt = new DefaultJws<>(jwsHeader, body, base64UrlDigest);
jwt = new DefaultJws<>(jwsHeader, body, base64UrlDigest.toString());
} else {
//noinspection rawtypes
jwt = new DefaultJwt(header, body);
@ -763,10 +768,10 @@ public class DefaultJwtParser implements JwtParser {
@Override
public <T> T parse(String compact, JwtHandler<T> handler) {
return parse(compact, Bytes.EMPTY, handler);
return parse(compact, Payload.EMPTY, handler);
}
private <T> T parse(String compact, byte[] unencodedPayload, JwtHandler<T> handler)
private <T> T parse(String compact, Payload unencodedPayload, JwtHandler<T> handler)
throws ExpiredJwtException, MalformedJwtException, SignatureException {
Assert.notNull(handler, "JwtHandler argument cannot be null.");
Assert.hasText(compact, "JWT String argument cannot be null or empty.");
@ -839,9 +844,7 @@ public class DefaultJwtParser implements JwtParser {
});
}
@Override
public Jws<byte[]> parseContentJws(String jws, byte[] unencodedPayload) {
Assert.notEmpty(unencodedPayload, "unencodedPayload argument cannot be null or empty.");
private Jws<byte[]> parseContentJws(String jws, Payload unencodedPayload) {
return parse(jws, unencodedPayload, new JwtHandlerAdapter<Jws<byte[]>>() {
@Override
public Jws<byte[]> onContentJws(Jws<byte[]> jws) {
@ -850,9 +853,8 @@ public class DefaultJwtParser implements JwtParser {
});
}
@Override
public Jws<Claims> parseClaimsJws(String jws, byte[] unencodedPayload) {
Assert.notEmpty(unencodedPayload, "unencodedPayload argument cannot be null or empty.");
private Jws<Claims> parseClaimsJws(String jws, Payload unencodedPayload) {
unencodedPayload.setClaimsExpected(true);
return parse(jws, unencodedPayload, new JwtHandlerAdapter<Jws<Claims>>() {
@Override
public Jws<Claims> onClaimsJws(Jws<Claims> jws) {
@ -861,6 +863,41 @@ public class DefaultJwtParser implements JwtParser {
});
}
@Override
public Jws<byte[]> parseContentJws(String jws, byte[] unencodedPayload) {
Assert.notEmpty(unencodedPayload, "unencodedPayload argument cannot be null or empty.");
return parseContentJws(jws, new Payload(unencodedPayload, null));
}
@Override
public Jws<Claims> parseClaimsJws(String jws, byte[] unencodedPayload) {
Assert.notEmpty(unencodedPayload, "unencodedPayload argument cannot be null or empty.");
return parseClaimsJws(jws, new Payload(unencodedPayload, null));
}
private static Payload payloadFor(InputStream in) {
if (in instanceof ByteArrayInputStream) {
byte[] data = Classes.getFieldValue(in, "buf", byte[].class);
return new Payload(data, null);
}
//if (in.markSupported()) in.mark(0);
return new Payload(in, null);
}
@Override
public Jws<byte[]> parseContentJws(String jws, InputStream unencodedPayload) {
Assert.notNull(unencodedPayload, "unencodedPayload InputStream cannot be null.");
return parseContentJws(jws, payloadFor(unencodedPayload));
}
@Override
public Jws<Claims> parseClaimsJws(String jws, InputStream unencodedPayload) {
Assert.notNull(unencodedPayload, "unencodedPayload InputStream cannot be null.");
byte[] bytes = Streams.bytes(unencodedPayload,
"Unable to obtain Claims bytes from unencodedPayload InputStream");
return parseClaimsJws(jws, new Payload(bytes, null));
}
@Override
public Jwe<byte[]> parseContentJwe(String compact) throws JwtException {
return parse(compact, new JwtHandlerAdapter<Jwe<byte[]>>() {
@ -881,21 +918,24 @@ public class DefaultJwtParser implements JwtParser {
});
}
protected byte[] decode(String base64UrlEncoded, String name) {
protected byte[] decode(CharSequence base64UrlEncoded, String name) {
try {
return decoder.decode(base64UrlEncoded);
} catch (DecodingException e) {
String msg = "Invalid Base64Url " + name + ": " + base64UrlEncoded;
throw new MalformedJwtException(msg, e);
InputStream decoding = this.decoder.decode(new ByteArrayInputStream(Strings.utf8(base64UrlEncoded)));
return Streams.bytes(decoding, "Unable to Base64Url-decode input.");
} catch (Throwable t) {
// Don't disclose potentially-sensitive information per https://github.com/jwtk/jjwt/issues/824:
String value = "payload".equals(name) ? RedactedSupplier.REDACTED_VALUE : base64UrlEncoded.toString();
String msg = "Invalid Base64Url " + name + ": " + value;
throw new MalformedJwtException(msg, t);
}
}
protected Map<String, ?> deserialize(byte[] bytes, final String name) {
protected Map<String, ?> deserialize(InputStream in, final String name) {
try {
return deserializer.deserialize(bytes);
} catch (MalformedJwtException | DeserializationException e) {
String s = new String(bytes, StandardCharsets.UTF_8);
throw new MalformedJwtException("Unable to read " + name + " JSON: " + s, e);
JsonObjectDeserializer deserializer = new JsonObjectDeserializer(this.deserializer, name);
return deserializer.apply(in);
} finally {
Objects.nullSafeClose(in);
}
}
}

View File

@ -23,6 +23,7 @@ import io.jsonwebtoken.JwtParserBuilder;
import io.jsonwebtoken.Jwts;
import io.jsonwebtoken.Locator;
import io.jsonwebtoken.SigningKeyResolver;
import io.jsonwebtoken.impl.io.DelegateStringDecoder;
import io.jsonwebtoken.impl.lang.Services;
import io.jsonwebtoken.impl.security.ConstantKeyLocator;
import io.jsonwebtoken.io.CompressionAlgorithm;
@ -39,6 +40,7 @@ import io.jsonwebtoken.security.Keys;
import io.jsonwebtoken.security.SecureDigestAlgorithm;
import javax.crypto.SecretKey;
import java.io.InputStream;
import java.security.Key;
import java.security.PrivateKey;
import java.security.Provider;
@ -88,7 +90,7 @@ public class DefaultJwtParserBuilder implements JwtParserBuilder {
@SuppressWarnings("deprecation")
private CompressionCodecResolver compressionCodecResolver;
private Decoder<String, byte[]> decoder = Decoders.BASE64URL;
private Decoder<InputStream, InputStream> decoder = new DelegateStringDecoder(Decoders.BASE64URL);
private Deserializer<Map<String, ?>> deserializer;
@ -123,23 +125,24 @@ public class DefaultJwtParserBuilder implements JwtParserBuilder {
@Override
public JwtParserBuilder deserializeJsonWith(Deserializer<Map<String, ?>> deserializer) {
return deserializer(deserializer);
return json(deserializer);
}
@Override
public JwtParserBuilder deserializer(Deserializer<Map<String, ?>> deserializer) {
Assert.notNull(deserializer, "deserializer cannot be null.");
this.deserializer = deserializer;
public JwtParserBuilder json(Deserializer<Map<String, ?>> reader) {
this.deserializer = Assert.notNull(reader, "JSON Deserializer cannot be null.");
return this;
}
@SuppressWarnings("deprecation")
@Override
public JwtParserBuilder base64UrlDecodeWith(Decoder<String, byte[]> decoder) {
return decoder(decoder);
public JwtParserBuilder base64UrlDecodeWith(final Decoder<CharSequence, byte[]> decoder) {
Assert.notNull(decoder, "decoder cannot be null.");
return b64Url(new DelegateStringDecoder(decoder));
}
@Override
public JwtParserBuilder decoder(Decoder<String, byte[]> decoder) {
public JwtParserBuilder b64Url(Decoder<InputStream, InputStream> decoder) {
Assert.notNull(decoder, "decoder cannot be null.");
this.decoder = decoder;
return this;
@ -349,14 +352,10 @@ public class DefaultJwtParserBuilder implements JwtParserBuilder {
@Override
public JwtParser build() {
// Only lookup the deserializer IF it is null. It is possible a Deserializer implementation was set
// that is NOT exposed as a service and no other implementations are available for lookup.
if (this.deserializer == null) {
// try to find one based on the services available:
//noinspection unchecked
this.deserializer = Services.loadFirst(Deserializer.class);
json(Services.loadFirst(Deserializer.class));
}
if (this.signingKeyResolver != null && this.signatureVerificationKey != null) {
String msg = "Both a 'signingKeyResolver and a 'verifyWith' key cannot be configured. " +
"Choose either, or prefer `keyLocator` when possible.";
@ -408,7 +407,7 @@ public class DefaultJwtParserBuilder implements JwtParserBuilder {
allowedClockSkewMillis,
expClaims,
decoder,
new JwtDeserializer<>(deserializer),
deserializer,
compressionCodecResolver,
extraZipAlgs,
extraSigAlgs,

View File

@ -21,22 +21,23 @@ import java.util.Map;
class DefaultTokenizedJwe extends DefaultTokenizedJwt implements TokenizedJwe {
private final String encryptedKey;
private final String iv;
private final CharSequence encryptedKey;
private final CharSequence iv;
DefaultTokenizedJwe(String protectedHeader, String body, String digest, String encryptedKey, String iv) {
DefaultTokenizedJwe(CharSequence protectedHeader, CharSequence body, CharSequence digest,
CharSequence encryptedKey, CharSequence iv) {
super(protectedHeader, body, digest);
this.encryptedKey = encryptedKey;
this.iv = iv;
}
@Override
public String getEncryptedKey() {
public CharSequence getEncryptedKey() {
return this.encryptedKey;
}
@Override
public String getIv() {
public CharSequence getIv() {
return this.iv;
}

View File

@ -23,28 +23,28 @@ import java.util.Map;
class DefaultTokenizedJwt implements TokenizedJwt {
private final String protectedHeader;
private final String payload;
private final String digest;
private final CharSequence protectedHeader;
private final CharSequence payload;
private final CharSequence digest;
DefaultTokenizedJwt(String protectedHeader, String payload, String digest) {
DefaultTokenizedJwt(CharSequence protectedHeader, CharSequence payload, CharSequence digest) {
this.protectedHeader = protectedHeader;
this.payload = payload;
this.digest = digest;
}
@Override
public String getProtected() {
public CharSequence getProtected() {
return this.protectedHeader;
}
@Override
public String getPayload() {
public CharSequence getPayload() {
return this.payload;
}
@Override
public String getDigest() {
public CharSequence getDigest() {
return this.digest;
}

View File

@ -1,54 +0,0 @@
/*
* Copyright (C) 2021 jsonwebtoken.io
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package io.jsonwebtoken.impl;
import io.jsonwebtoken.MalformedJwtException;
import io.jsonwebtoken.io.DeserializationException;
import io.jsonwebtoken.io.Deserializer;
import io.jsonwebtoken.io.IOException;
import io.jsonwebtoken.lang.Assert;
import java.nio.charset.StandardCharsets;
/**
* A {@link Deserializer} implementation that wraps another Deserializer implementation to add common JWT related
* error handling.
* @param <T> type of object to deserialize.
* @since 0.11.3
*/
class JwtDeserializer<T> implements Deserializer<T> {
static final String MALFORMED_ERROR = "Malformed JWT JSON: ";
static final String MALFORMED_COMPLEX_ERROR = "Malformed or excessively complex JWT JSON. This could reflect a potential malicious JWT, please investigate the JWT source further. JSON: ";
private final Deserializer<T> deserializer;
JwtDeserializer(Deserializer<T> deserializer) {
Assert.notNull(deserializer, "deserializer cannot be null.");
this.deserializer = deserializer;
}
@Override
public T deserialize(byte[] bytes) throws DeserializationException {
try {
return deserializer.deserialize(bytes);
} catch (DeserializationException e) {
throw new MalformedJwtException(MALFORMED_ERROR + new String(bytes, StandardCharsets.UTF_8), e);
} catch (StackOverflowError e) {
throw new IOException(MALFORMED_COMPLEX_ERROR + new String(bytes, StandardCharsets.UTF_8), e);
}
}
}

View File

@ -17,28 +17,28 @@ package io.jsonwebtoken.impl;
import io.jsonwebtoken.MalformedJwtException;
import io.jsonwebtoken.lang.Assert;
import io.jsonwebtoken.lang.Strings;
public class JwtTokenizer {
static final char DELIMITER = '.';
private static final String DELIM_ERR_MSG_PREFIX = "Invalid compact JWT string: Compact JWSs must contain " +
"exactly 2 period characters, and compact JWEs must contain exactly 4. Found: ";
"exactly 2 period characters, and compact JWEs must contain exactly 4. Found: ";
@SuppressWarnings("unchecked")
public <T extends TokenizedJwt> T tokenize(String jwt) {
public <T extends TokenizedJwt> T tokenize(CharSequence jwt) {
Assert.hasText(jwt, "Argument cannot be null or empty.");
String protectedHeader = ""; //Both JWS and JWE
String body = ""; //JWS payload or JWE Ciphertext
String encryptedKey = ""; //JWE only
String iv = ""; //JWE only
String digest; //JWS Signature or JWE AAD Tag
CharSequence protectedHeader = Strings.EMPTY; //Both JWS and JWE
CharSequence body = Strings.EMPTY; //JWS payload or JWE Ciphertext
CharSequence encryptedKey = Strings.EMPTY; //JWE only
CharSequence iv = Strings.EMPTY; //JWE only
CharSequence digest; //JWS Signature or JWE AAD Tag
int delimiterCount = 0;
StringBuilder sb = new StringBuilder(128);
int start = 0;
for (int i = 0; i < jwt.length(); i++) {
@ -51,7 +51,8 @@ public class JwtTokenizer {
if (c == DELIMITER) {
String token = sb.toString();
CharSequence token = jwt.subSequence(start, i);
start = i + 1;
switch (delimiterCount) {
case 0:
@ -62,7 +63,7 @@ public class JwtTokenizer {
encryptedKey = token; //for JWE
break;
case 2:
body = ""; //clear out value set for JWS
body = Strings.EMPTY; //clear out value set for JWS
iv = token;
break;
case 3:
@ -70,10 +71,7 @@ public class JwtTokenizer {
break;
}
sb.setLength(0);
delimiterCount++;
} else {
sb.append(c);
}
}
@ -82,7 +80,7 @@ public class JwtTokenizer {
throw new MalformedJwtException(msg);
}
digest = sb.toString();
digest = jwt.subSequence(start, jwt.length());
if (delimiterCount == 2) {
return (T) new DefaultTokenizedJwt(protectedHeader, body, digest);

View File

@ -15,42 +15,138 @@
*/
package io.jsonwebtoken.impl;
import io.jsonwebtoken.Claims;
import io.jsonwebtoken.CompressionCodec;
import io.jsonwebtoken.Jwts;
import io.jsonwebtoken.impl.lang.Bytes;
import io.jsonwebtoken.io.CompressionAlgorithm;
import io.jsonwebtoken.lang.Assert;
import io.jsonwebtoken.lang.Collections;
import io.jsonwebtoken.lang.Strings;
import java.io.ByteArrayInputStream;
import java.io.InputStream;
import java.io.OutputStream;
class Payload {
static final Payload EMPTY = new Payload(null, null, null);
static final Payload EMPTY = new Payload(Bytes.EMPTY, null);
private String string;
private byte[] bytes;
private String contentType;
private final CharSequence string;
private final byte[] bytes;
private final Claims claims;
private final InputStream inputStream;
private final boolean inputStreamEmpty;
private final String contentType;
private CompressionAlgorithm zip;
private boolean claimsExpected;
Payload(String string, byte[] bytes, String contentType) {
this.string = Strings.clean(string);
this.bytes = Bytes.nullSafe(bytes);
this.contentType = Strings.clean(contentType);
Payload(Claims claims) {
this(claims, null, null, null, null);
}
String getString() {
return this.string;
Payload(CharSequence content, String contentType) {
this(null, content, null, null, contentType);
}
Payload(byte[] content, String contentType) {
this(null, null, content, null, contentType);
}
Payload(InputStream inputStream, String contentType) {
this(null, null, null, inputStream, contentType);
}
private Payload(Claims claims, CharSequence string, byte[] bytes, InputStream inputStream, String contentType) {
this.claims = claims;
this.string = Strings.clean(string);
this.contentType = Strings.clean(contentType);
InputStream in = inputStream;
byte[] data = Bytes.nullSafe(bytes);
if (Strings.hasText(this.string)) {
data = Strings.utf8(this.string);
}
this.bytes = data;
if (in == null && !Bytes.isEmpty(this.bytes)) {
in = new ByteArrayInputStream(data);
}
this.inputStreamEmpty = in == null;
this.inputStream = this.inputStreamEmpty ? new ByteArrayInputStream(Bytes.EMPTY) : in;
}
boolean isClaims() {
return !Collections.isEmpty(this.claims);
}
Claims getRequiredClaims() {
return Assert.notEmpty(this.claims, "Claims cannot be null or empty when calling this method.");
}
boolean isString() {
return Strings.hasText(this.string);
}
String getContentType() {
return this.contentType;
}
boolean isEmpty() {
return !Strings.hasText(this.string) && Bytes.isEmpty(this.bytes);
public void setZip(CompressionAlgorithm zip) {
this.zip = zip;
}
byte[] toByteArray() {
if (Bytes.isEmpty(this.bytes)) {
if (!Strings.hasText(this.string)) {
throw new IllegalStateException("Content is empty.");
boolean isCompressed() {
return this.zip != null;
}
public void setClaimsExpected(boolean claimsExpected) {
this.claimsExpected = claimsExpected;
}
/**
* Returns {@code true} if the payload may be fully consumed and retained in memory, {@code false} if empty,
* already extracted, or a potentially too-large InputStream.
*
* @return {@code true} if the payload may be fully consumed and retained in memory, {@code false} if empty,
* already extracted, or a potentially too-large InputStream.
*/
boolean isConsumable() {
return !isClaims() && (isString() || !Bytes.isEmpty(this.bytes) || (inputStream != null && claimsExpected));
}
boolean isEmpty() {
return !isClaims() && !isString() && Bytes.isEmpty(this.bytes) && this.inputStreamEmpty;
}
public OutputStream compress(OutputStream out) {
return this.zip != null ? zip.compress(out) : out;
}
public Payload decompress(CompressionAlgorithm alg) {
Assert.notNull(alg, "CompressionAlgorithm cannot be null.");
Payload payload = this;
if (!isString() && isConsumable()) {
if (alg.equals(Jwts.ZIP.DEF) && !Bytes.isEmpty(this.bytes)) { // backwards compatibility
byte[] data = ((CompressionCodec) alg).decompress(this.bytes);
payload = new Payload(claims, string, data, null, getContentType());
} else {
InputStream in = toInputStream();
in = alg.decompress(in);
payload = new Payload(claims, string, bytes, in, getContentType());
}
this.bytes = Strings.utf8(this.string);
payload.setClaimsExpected(claimsExpected);
}
// otherwise it's a String or b64/detached payload, in either case, we don't decompress since the caller is
// providing the bytes necessary for signature verification as-is, and there's no conversion we need to perform
return payload;
}
public byte[] getBytes() {
return this.bytes;
}
InputStream toInputStream() {
// should only ever call this when claims don't exist:
Assert.state(!isClaims(), "Claims exist, cannot convert to InputStream directly.");
return this.inputStream;
}
}

View File

@ -17,7 +17,7 @@ package io.jsonwebtoken.impl;
public interface TokenizedJwe extends TokenizedJwt {
String getEncryptedKey();
CharSequence getEncryptedKey();
String getIv();
CharSequence getIv();
}

View File

@ -26,21 +26,21 @@ public interface TokenizedJwt {
*
* @return protected header.
*/
String getProtected();
CharSequence getProtected();
/**
* Returns the Payload for a JWS or Ciphertext for a JWE.
*
* @return the Payload for a JWS or Ciphertext for a JWE.
*/
String getPayload();
CharSequence getPayload();
/**
* Returns the Signature for JWS or AAD Tag for JWE.
*
* @return the Signature for JWS or AAD Tag for JWE.
*/
String getDigest();
CharSequence getDigest();
/**
* Returns a new {@link Header} instance with the specified map state.

View File

@ -17,11 +17,17 @@ package io.jsonwebtoken.impl.compression;
import io.jsonwebtoken.CompressionCodec;
import io.jsonwebtoken.CompressionException;
import io.jsonwebtoken.impl.io.Streams;
import io.jsonwebtoken.impl.lang.Bytes;
import io.jsonwebtoken.impl.lang.CheckedFunction;
import io.jsonwebtoken.impl.lang.Function;
import io.jsonwebtoken.impl.lang.PropagatingExceptionFunction;
import io.jsonwebtoken.io.CompressionAlgorithm;
import io.jsonwebtoken.lang.Assert;
import io.jsonwebtoken.lang.Objects;
import io.jsonwebtoken.lang.Strings;
import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.InputStream;
@ -32,12 +38,54 @@ import java.io.OutputStream;
*
* @since 0.6.0
*/
@SuppressWarnings("deprecation")
public abstract class AbstractCompressionAlgorithm implements CompressionAlgorithm, CompressionCodec {
private static <T, R> Function<T, R> propagate(CheckedFunction<T, R> fn, String msg) {
return new PropagatingExceptionFunction<>(fn, CompressionException.class, msg);
}
private static <T, R> Function<T, R> forCompression(CheckedFunction<T, R> fn) {
return propagate(fn, "Compression failed.");
}
private static <T, R> Function<T, R> forDecompression(CheckedFunction<T, R> fn) {
return propagate(fn, "Decompression failed.");
}
private final String id;
private final Function<OutputStream, OutputStream> OS_WRAP_FN;
private final Function<InputStream, InputStream> IS_WRAP_FN;
private final Function<byte[], byte[]> COMPRESS_FN;
private final Function<byte[], byte[]> DECOMPRESS_FN;
protected AbstractCompressionAlgorithm(String id) {
this.id = Assert.hasText(Strings.clean(id), "id argument cannot be null or empty.");
this.OS_WRAP_FN = forCompression(new CheckedFunction<OutputStream, OutputStream>() {
@Override
public OutputStream apply(OutputStream out) throws Exception {
return doCompress(out);
}
});
this.COMPRESS_FN = forCompression(new CheckedFunction<byte[], byte[]>() {
@Override
public byte[] apply(byte[] data) throws Exception {
return doCompress(data);
}
});
this.IS_WRAP_FN = forDecompression(new CheckedFunction<InputStream, InputStream>() {
@Override
public InputStream apply(InputStream is) throws Exception {
return doDecompress(is);
}
});
this.DECOMPRESS_FN = forDecompression(new CheckedFunction<byte[], byte[]>() {
@Override
public byte[] apply(byte[] data) throws Exception {
return doDecompress(data);
}
});
}
@Override
@ -50,70 +98,38 @@ public abstract class AbstractCompressionAlgorithm implements CompressionAlgorit
return getId();
}
//package-protected for a point release. This can be made protected on a minor release (0.11.0, 0.12.0, 1.0, etc).
//TODO: make protected on a minor release
interface StreamWrapper {
OutputStream wrap(OutputStream out) throws IOException;
@Override
public final OutputStream compress(final OutputStream out) throws CompressionException {
return OS_WRAP_FN.apply(out);
}
//package-protected for a point release. This can be made protected on a minor release (0.11.0, 0.12.0, 1.0, etc).
//TODO: make protected on a minor release
byte[] readAndClose(InputStream input) throws IOException {
byte[] buffer = new byte[512];
ByteArrayOutputStream out = new ByteArrayOutputStream(buffer.length);
int read;
protected abstract OutputStream doCompress(OutputStream out) throws IOException;
@Override
public final InputStream decompress(InputStream is) throws CompressionException {
return IS_WRAP_FN.apply(is);
}
protected abstract InputStream doDecompress(InputStream is) throws IOException;
@Override
public final byte[] compress(byte[] content) {
if (Bytes.isEmpty(content)) return Bytes.EMPTY;
return this.COMPRESS_FN.apply(content);
}
private byte[] doCompress(byte[] data) throws IOException {
ByteArrayOutputStream out = new ByteArrayOutputStream(512);
OutputStream compression = compress(out);
try {
read = input.read(buffer); //assignment separate from loop invariant check for code coverage checks
while (read != -1) {
out.write(buffer, 0, read);
read = input.read(buffer);
}
compression.write(data);
compression.flush();
} finally {
Objects.nullSafeClose(input);
Objects.nullSafeClose(compression);
}
return out.toByteArray();
}
//package-protected for a point release. This can be made protected on a minor release (0.11.0, 0.12.0, 1.0, etc).
//TODO: make protected on a minor release
byte[] writeAndClose(byte[] content, StreamWrapper wrapper) throws IOException {
ByteArrayOutputStream outputStream = new ByteArrayOutputStream(512);
OutputStream compressionStream = wrapper.wrap(outputStream);
try {
compressionStream.write(content);
compressionStream.flush();
} finally {
Objects.nullSafeClose(compressionStream);
}
return outputStream.toByteArray();
}
/**
* Implement this method to do the actual work of compressing the content
*
* @param content the bytes to compress
* @return the compressed bytes
* @throws IOException if the compression causes an IOException
*/
protected abstract byte[] doCompress(byte[] content) throws IOException;
/**
* Asserts that content is not null and calls {@link #doCompress(byte[]) doCompress}
*
* @param content bytes to compress
* @return compressed bytes
* @throws CompressionException if {@link #doCompress(byte[]) doCompress} throws an IOException
*/
@Override
public final byte[] compress(byte[] content) {
Assert.notNull(content, "content cannot be null.");
try {
return doCompress(content);
} catch (IOException e) {
throw new CompressionException("Unable to compress content.", e);
}
}
/**
* Asserts the compressed bytes is not null and calls {@link #doDecompress(byte[]) doDecompress}
@ -124,13 +140,8 @@ public abstract class AbstractCompressionAlgorithm implements CompressionAlgorit
*/
@Override
public final byte[] decompress(byte[] compressed) {
Assert.notNull(compressed, "compressed bytes cannot be null.");
try {
return doDecompress(compressed);
} catch (IOException e) {
throw new CompressionException("Unable to decompress bytes.", e);
}
if (Bytes.isEmpty(compressed)) return Bytes.EMPTY;
return this.DECOMPRESS_FN.apply(compressed);
}
/**
@ -140,5 +151,20 @@ public abstract class AbstractCompressionAlgorithm implements CompressionAlgorit
* @return decompressed bytes
* @throws IOException if the decompression runs into an IO problem
*/
protected abstract byte[] doDecompress(byte[] compressed) throws IOException;
protected byte[] doDecompress(byte[] compressed) throws IOException {
InputStream is = new ByteArrayInputStream(compressed);
InputStream decompress = decompress(is);
byte[] buffer = new byte[512];
ByteArrayOutputStream out = new ByteArrayOutputStream(buffer.length);
int read = 0;
try {
while (read != Streams.EOF) {
read = decompress.read(buffer); //assignment separate from loop invariant check for code coverage checks
if (read > 0) out.write(buffer, 0, read);
}
} finally {
Objects.nullSafeClose(decompress);
}
return out.toByteArray();
}
}

View File

@ -17,7 +17,6 @@ package io.jsonwebtoken.impl.compression;
import io.jsonwebtoken.lang.Objects;
import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.InputStream;
@ -35,26 +34,24 @@ public class DeflateCompressionAlgorithm extends AbstractCompressionAlgorithm {
private static final String ID = "DEF";
private static final StreamWrapper WRAPPER = new StreamWrapper() {
@Override
public OutputStream wrap(OutputStream out) {
return new DeflaterOutputStream(out);
}
};
public DeflateCompressionAlgorithm() {
super(ID);
}
@Override
protected byte[] doCompress(byte[] content) throws IOException {
return writeAndClose(content, WRAPPER);
protected OutputStream doCompress(OutputStream out) {
return new DeflaterOutputStream(out);
}
@Override
protected InputStream doDecompress(InputStream is) {
return new InflaterInputStream(is);
}
@Override
protected byte[] doDecompress(final byte[] compressed) throws IOException {
try {
return readAndClose(new InflaterInputStream(new ByteArrayInputStream(compressed)));
return super.doDecompress(compressed);
} catch (IOException e1) {
try {
return doDecompressBackCompat(compressed);
@ -66,7 +63,7 @@ public class DeflateCompressionAlgorithm extends AbstractCompressionAlgorithm {
/**
* This implementation was in 0.10.6 and earlier - it will be used as a fallback for backwards compatibility if
* {@link #readAndClose(InputStream)} fails per <a href="https://github.com/jwtk/jjwt/issues/536">Issue 536</a>.
* {@link #doDecompress(byte[])} fails per <a href="https://github.com/jwtk/jjwt/issues/536">Issue 536</a>.
*
* @param compressed the compressed byte array
* @return decompressed bytes

View File

@ -15,10 +15,8 @@
*/
package io.jsonwebtoken.impl.compression;
import io.jsonwebtoken.CompressionCodec;
import java.io.ByteArrayInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.util.zip.GZIPInputStream;
import java.util.zip.GZIPOutputStream;
@ -28,28 +26,21 @@ import java.util.zip.GZIPOutputStream;
*
* @since 0.6.0
*/
public class GzipCompressionAlgorithm extends AbstractCompressionAlgorithm implements CompressionCodec {
public class GzipCompressionAlgorithm extends AbstractCompressionAlgorithm {
private static final String ID = "GZIP";
private static final StreamWrapper WRAPPER = new StreamWrapper() {
@Override
public OutputStream wrap(OutputStream out) throws IOException {
return new GZIPOutputStream(out);
}
};
public GzipCompressionAlgorithm() {
super(ID);
}
@Override
protected byte[] doCompress(byte[] content) throws IOException {
return writeAndClose(content, WRAPPER);
protected OutputStream doCompress(OutputStream out) throws IOException {
return new GZIPOutputStream(out);
}
@Override
protected byte[] doDecompress(byte[] compressed) throws IOException {
return readAndClose(new GZIPInputStream(new ByteArrayInputStream(compressed)));
protected InputStream doDecompress(InputStream is) throws IOException {
return new GZIPInputStream(is);
}
}

View File

@ -41,15 +41,14 @@ public abstract class AbstractParserBuilder<T, B extends ParserBuilder<T, B>> im
}
@Override
public B deserializer(Deserializer<Map<String, ?>> deserializer) {
this.deserializer = deserializer;
public B json(Deserializer<Map<String, ?>> reader) {
this.deserializer = reader;
return self();
}
@Override
public final Parser<T> build() {
if (this.deserializer == null) {
// try to find one based on the services available:
//noinspection unchecked
this.deserializer = Services.loadFirst(Deserializer.class);
}

View File

@ -0,0 +1,796 @@
/*
* Copyright © 2023 jsonwebtoken.io
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package io.jsonwebtoken.impl.io;
import io.jsonwebtoken.lang.Strings;
/**
* Provides Base64 encoding and decoding as defined by <a href="http://www.ietf.org/rfc/rfc2045.txt">RFC 2045</a>.
*
* <p>
* This class implements section <cite>6.8. Base64 Content-Transfer-Encoding</cite> from RFC 2045 <cite>Multipurpose
* Internet Mail Extensions (MIME) Part One: Format of Internet Message Bodies</cite> by Freed and Borenstein.
* </p>
* <p>
* The class can be parameterized in the following manner with various constructors:
* </p>
* <ul>
* <li>URL-safe mode: Default off.</li>
* <li>Line length: Default 76. Line length that aren't multiples of 4 will still essentially end up being multiples of
* 4 in the encoded data.
* <li>Line separator: Default is CRLF ("\r\n")</li>
* </ul>
* <p>
* The URL-safe parameter is only applied to encode operations. Decoding seamlessly handles both modes.
* </p>
* <p>
* Since this class operates directly on byte streams, and not character streams, it is hard-coded to only
* encode/decode character encodings which are compatible with the lower 127 ASCII chart (ISO-8859-1, Windows-1252,
* UTF-8, etc).
* </p>
* <p>
* This class is thread-safe.
* </p>
*
* @see <a href="http://www.ietf.org/rfc/rfc2045.txt">RFC 2045</a>
* @since JJWT_RELEASE_VERSION, copied from
* <a href="https://github.com/apache/commons-codec/tree/585497f09b026f6602daf986723a554e051bdfe6">commons-codec
* 585497f09b026f6602daf986723a554e051bdfe6</a>
*/
class Base64Codec extends BaseNCodec {
/**
* BASE64 characters are 6 bits in length.
* They are formed by taking a block of 3 octets to form a 24-bit string,
* which is converted into 4 BASE64 characters.
*/
private static final int BITS_PER_ENCODED_BYTE = 6;
private static final int BYTES_PER_UNENCODED_BLOCK = 3;
private static final int BYTES_PER_ENCODED_BLOCK = 4;
/**
* This array is a lookup table that translates 6-bit positive integer index values into their "Base64 Alphabet"
* equivalents as specified in Table 1 of RFC 2045.
* <p>
* Thanks to "commons" project in ws.apache.org for this code.
* http://svn.apache.org/repos/asf/webservices/commons/trunk/modules/util/
* </p>
*/
private static final byte[] STANDARD_ENCODE_TABLE = {
'A', 'B', 'C', 'D', 'E', 'F', 'G', 'H', 'I', 'J', 'K', 'L', 'M',
'N', 'O', 'P', 'Q', 'R', 'S', 'T', 'U', 'V', 'W', 'X', 'Y', 'Z',
'a', 'b', 'c', 'd', 'e', 'f', 'g', 'h', 'i', 'j', 'k', 'l', 'm',
'n', 'o', 'p', 'q', 'r', 's', 't', 'u', 'v', 'w', 'x', 'y', 'z',
'0', '1', '2', '3', '4', '5', '6', '7', '8', '9', '+', '/'
};
/**
* This is a copy of the STANDARD_ENCODE_TABLE above, but with + and /
* changed to - and _ to make the encoded Base64 results more URL-SAFE.
* This table is only used when the Base64's mode is set to URL-SAFE.
*/
private static final byte[] URL_SAFE_ENCODE_TABLE = {
'A', 'B', 'C', 'D', 'E', 'F', 'G', 'H', 'I', 'J', 'K', 'L', 'M',
'N', 'O', 'P', 'Q', 'R', 'S', 'T', 'U', 'V', 'W', 'X', 'Y', 'Z',
'a', 'b', 'c', 'd', 'e', 'f', 'g', 'h', 'i', 'j', 'k', 'l', 'm',
'n', 'o', 'p', 'q', 'r', 's', 't', 'u', 'v', 'w', 'x', 'y', 'z',
'0', '1', '2', '3', '4', '5', '6', '7', '8', '9', '-', '_'
};
/**
* This array is a lookup table that translates Unicode characters drawn from the "Base64 Alphabet" (as specified
* in Table 1 of RFC 2045) into their 6-bit positive integer equivalents. Characters that are not in the Base64
* alphabet but fall within the bounds of the array are translated to -1.
* <p>
* Note: '+' and '-' both decode to 62. '/' and '_' both decode to 63. This means decoder seamlessly handles both
* URL_SAFE and STANDARD base64. (The encoder, on the other hand, needs to know ahead of time what to emit).
* </p>
* <p>
* Thanks to "commons" project in ws.apache.org for this code.
* http://svn.apache.org/repos/asf/webservices/commons/trunk/modules/util/
* </p>
*/
// @formatter:off
private static final byte[] DECODE_TABLE = {
// 0 1 2 3 4 5 6 7 8 9 A B C D E F
-1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, // 00-0f
-1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, // 10-1f
-1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, 62, -1, 62, -1, 63, // 20-2f + - /
52, 53, 54, 55, 56, 57, 58, 59, 60, 61, -1, -1, -1, -1, -1, -1, // 30-3f 0-9
-1, 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, // 40-4f A-O
15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, -1, -1, -1, -1, 63, // 50-5f P-Z _
-1, 26, 27, 28, 29, 30, 31, 32, 33, 34, 35, 36, 37, 38, 39, 40, // 60-6f a-o
41, 42, 43, 44, 45, 46, 47, 48, 49, 50, 51 // 70-7a p-z
};
// @formatter:on
// The static final fields above are used for the original static byte[] methods on Base64.
// The private member fields below are used with the new streaming approach, which requires
// some state be preserved between calls of encode() and decode().
/* Base64 uses 6-bit fields. */
/**
* Mask used to extract 6 bits, used when encoding
*/
private static final int MASK_6BITS = 0x3f;
/**
* Mask used to extract 4 bits, used when decoding final trailing character.
*/
private static final int MASK_4BITS = 0xf;
/**
* Mask used to extract 2 bits, used when decoding final trailing character.
*/
private static final int MASK_2BITS = 0x3;
/**
* Encode table to use: either STANDARD or URL_SAFE. Note: the DECODE_TABLE above remains static because it is able
* to decode both STANDARD and URL_SAFE streams, but the encodeTable must be a member variable so we can switch
* between the two modes.
*/
private final byte[] encodeTable;
/**
* Only one decode table currently; keep for consistency with Base32 code.
*/
private final byte[] decodeTable = DECODE_TABLE;
/**
* Line separator for encoding. Not used when decoding. Only used if lineLength &gt; 0.
*/
private final byte[] lineSeparator;
/**
* Convenience variable to help us determine when our buffer is going to run out of room and needs resizing.
* {@code decodeSize = 3 + lineSeparator.length;}
*/
private final int decodeSize;
/**
* Convenience variable to help us determine when our buffer is going to run out of room and needs resizing.
* {@code encodeSize = 4 + lineSeparator.length;}
*/
private final int encodeSize;
// /**
// * Decodes Base64 data into octets.
// * <p>
// * <b>Note:</b> this method seamlessly handles data encoded in URL-safe or normal mode.
// * </p>
// *
// * @param base64Data Byte array containing Base64 data
// * @return Array containing decoded data.
// */
// public static byte[] decodeBase64(final byte[] base64Data) {
// return new Base64().decode(base64Data);
// }
//
// /**
// * Decodes a Base64 String into octets.
// * <p>
// * <b>Note:</b> this method seamlessly handles data encoded in URL-safe or normal mode.
// * </p>
// *
// * @param base64String String containing Base64 data
// * @return Array containing decoded data.
// * @since 1.4
// */
// public static byte[] decodeBase64(final String base64String) {
// return new Base64().decode(base64String);
// }
//
// // Implementation of integer encoding used for crypto
//
// /**
// * Decodes a byte64-encoded integer according to crypto standards such as W3C's XML-Signature.
// *
// * @param pArray a byte array containing base64 character data
// * @return A BigInteger
// * @since 1.4
// */
// public static BigInteger decodeInteger(final byte[] pArray) {
// return new BigInteger(1, decodeBase64(pArray));
// }
//
// /**
// * Encodes binary data using the base64 algorithm but does not chunk the output.
// *
// * @param binaryData binary data to encode
// * @return byte[] containing Base64 characters in their UTF-8 representation.
// */
// public static byte[] encodeBase64(final byte[] binaryData) {
// return encodeBase64(binaryData, false);
// }
//
// /**
// * Encodes binary data using the base64 algorithm, optionally chunking the output into 76 character blocks.
// *
// * @param binaryData Array containing binary data to encode.
// * @param isChunked if {@code true} this encoder will chunk the base64 output into 76 character blocks
// * @return Base64-encoded data.
// * @throws IllegalArgumentException Thrown when the input array needs an output array bigger than {@link Integer#MAX_VALUE}
// */
// public static byte[] encodeBase64(final byte[] binaryData, final boolean isChunked) {
// return encodeBase64(binaryData, isChunked, false);
// }
//
// /**
// * Encodes binary data using the base64 algorithm, optionally chunking the output into 76 character blocks.
// *
// * @param binaryData Array containing binary data to encode.
// * @param isChunked if {@code true} this encoder will chunk the base64 output into 76 character blocks
// * @param urlSafe if {@code true} this encoder will emit - and _ instead of the usual + and / characters.
// * <b>Note: no padding is added when encoding using the URL-safe alphabet.</b>
// * @return Base64-encoded data.
// * @throws IllegalArgumentException Thrown when the input array needs an output array bigger than {@link Integer#MAX_VALUE}
// * @since 1.4
// */
// public static byte[] encodeBase64(final byte[] binaryData, final boolean isChunked, final boolean urlSafe) {
// return encodeBase64(binaryData, isChunked, urlSafe, Integer.MAX_VALUE);
// }
//
// /**
// * Encodes binary data using the base64 algorithm, optionally chunking the output into 76 character blocks.
// *
// * @param binaryData Array containing binary data to encode.
// * @param isChunked if {@code true} this encoder will chunk the base64 output into 76 character blocks
// * @param urlSafe if {@code true} this encoder will emit - and _ instead of the usual + and / characters.
// * <b>Note: no padding is added when encoding using the URL-safe alphabet.</b>
// * @param maxResultSize The maximum result size to accept.
// * @return Base64-encoded data.
// * @throws IllegalArgumentException Thrown when the input array needs an output array bigger than maxResultSize
// * @since 1.4
// */
// public static byte[] encodeBase64(final byte[] binaryData, final boolean isChunked,
// final boolean urlSafe, final int maxResultSize) {
// if (Bytes.isEmpty(binaryData)) {
// return binaryData;
// }
//
// // Create this so can use the super-class method
// // Also ensures that the same roundings are performed by the ctor and the code
// final Base64 b64 = isChunked ? new Base64(urlSafe) : new Base64(0, CHUNK_SEPARATOR, urlSafe);
// final long len = b64.getEncodedLength(binaryData);
// if (len > maxResultSize) {
// throw new IllegalArgumentException("Input array too big, the output array would be bigger (" +
// len +
// ") than the specified maximum size of " +
// maxResultSize);
// }
//
// return b64.encode(binaryData);
// }
//
// /**
// * Encodes binary data using the base64 algorithm and chunks the encoded output into 76 character blocks
// *
// * @param binaryData binary data to encode
// * @return Base64 characters chunked in 76 character blocks
// */
// public static byte[] encodeBase64Chunked(final byte[] binaryData) {
// return encodeBase64(binaryData, true);
// }
//
// /**
// * Encodes binary data using the base64 algorithm but does not chunk the output.
// * <p>
// * NOTE: We changed the behavior of this method from multi-line chunking (commons-codec-1.4) to
// * single-line non-chunking (commons-codec-1.5).
// *
// * @param binaryData binary data to encode
// * @return String containing Base64 characters.
// * @since 1.4 (NOTE: 1.4 chunked the output, whereas 1.5 does not).
// */
// public static String encodeBase64String(final byte[] binaryData) {
// byte[] encoded = encodeBase64(binaryData, false);
// return new String(encoded, StandardCharsets.US_ASCII);
// }
//
// /**
// * Encodes binary data using a URL-safe variation of the base64 algorithm but does not chunk the output. The
// * url-safe variation emits - and _ instead of + and / characters.
// * <b>Note: no padding is added.</b>
// *
// * @param binaryData binary data to encode
// * @return byte[] containing Base64 characters in their UTF-8 representation.
// * @since 1.4
// */
// public static byte[] encodeBase64URLSafe(final byte[] binaryData) {
// return encodeBase64(binaryData, false, true);
// }
//
// /**
// * Encodes binary data using a URL-safe variation of the base64 algorithm but does not chunk the output. The
// * url-safe variation emits - and _ instead of + and / characters.
// * <b>Note: no padding is added.</b>
// *
// * @param binaryData binary data to encode
// * @return String containing Base64 characters
// * @since 1.4
// */
// public static String encodeBase64URLSafeString(final byte[] binaryData) {
// byte[] encoded = encodeBase64(binaryData, false, true);
// return new String(encoded, StandardCharsets.US_ASCII);
// }
//
// /**
// * Encodes to a byte64-encoded integer according to crypto standards such as W3C's XML-Signature.
// *
// * @param bigInteger a BigInteger
// * @return A byte array containing base64 character data
// * @throws NullPointerException if null is passed in
// * @since 1.4
// */
// public static byte[] encodeInteger(final BigInteger bigInteger) {
// Objects.requireNonNull(bigInteger, "bigInteger");
// return encodeBase64(toIntegerBytes(bigInteger), false);
// }
//
// /**
// * Returns whether or not the {@code octet} is in the base 64 alphabet.
// *
// * @param octet The value to test
// * @return {@code true} if the value is defined in the base 64 alphabet, {@code false} otherwise.
// * @since 1.4
// */
// public static boolean isBase64(final byte octet) {
// return octet == PAD_DEFAULT || octet >= 0 && octet < DECODE_TABLE.length && DECODE_TABLE[octet] != -1;
// }
//
// /**
// * Tests a given byte array to see if it contains only valid characters within the Base64 alphabet. Currently the
// * method treats whitespace as valid.
// *
// * @param arrayOctet byte array to test
// * @return {@code true} if all bytes are valid characters in the Base64 alphabet or if the byte array is empty;
// * {@code false}, otherwise
// * @since 1.5
// */
// public static boolean isBase64(final byte[] arrayOctet) {
// for (final byte element : arrayOctet) {
// if (!isBase64(element) && !Character.isWhitespace(element)) {
// return false;
// }
// }
// return true;
// }
//
// /**
// * Tests a given String to see if it contains only valid characters within the Base64 alphabet. Currently the
// * method treats whitespace as valid.
// *
// * @param base64 String to test
// * @return {@code true} if all characters in the String are valid characters in the Base64 alphabet or if
// * the String is empty; {@code false}, otherwise
// * @since 1.5
// */
// public static boolean isBase64(final String base64) {
// return isBase64(Strings.utf8(base64));
// }
//
// /**
// * Returns a byte-array representation of a {@code BigInteger} without sign bit.
// *
// * @param bigInt {@code BigInteger} to be converted
// * @return a byte array representation of the BigInteger parameter
// */
// static byte[] toIntegerBytes(final BigInteger bigInt) {
// int bitlen = bigInt.bitLength();
// // round bitlen
// bitlen = bitlen + 7 >> 3 << 3;
// final byte[] bigBytes = bigInt.toByteArray();
//
// if (bigInt.bitLength() % 8 != 0 && bigInt.bitLength() / 8 + 1 == bitlen / 8) {
// return bigBytes;
// }
// // set up params for copying everything but sign bit
// int startSrc = 0;
// int len = bigBytes.length;
//
// // if bigInt is exactly byte-aligned, just skip signbit in copy
// if (bigInt.bitLength() % 8 == 0) {
// startSrc = 1;
// len--;
// }
// final int startDst = bitlen / 8 - len; // to pad w/ nulls as per spec
// final byte[] resizedBytes = new byte[bitlen / 8];
// System.arraycopy(bigBytes, startSrc, resizedBytes, startDst, len);
// return resizedBytes;
// }
/**
* Creates a Base64 codec used for decoding (all modes) and encoding in URL-unsafe mode.
* <p>
* When encoding the line length is 0 (no chunking), and the encoding table is STANDARD_ENCODE_TABLE.
* </p>
*
* <p>
* When decoding all variants are supported.
* </p>
*/
Base64Codec() {
this(0);
}
/**
* Creates a Base64 codec used for decoding (all modes) and encoding in the given URL-safe mode.
* <p>
* When encoding the line length is 76, the line separator is CRLF, and the encoding table is STANDARD_ENCODE_TABLE.
* </p>
*
* <p>
* When decoding all variants are supported.
* </p>
*
* @param urlSafe if {@code true}, URL-safe encoding is used. In most cases this should be set to
* {@code false}.
* @since 1.4
*/
Base64Codec(final boolean urlSafe) {
this(MIME_CHUNK_SIZE, CHUNK_SEPARATOR, urlSafe);
}
/**
* Creates a Base64 codec used for decoding (all modes) and encoding in URL-unsafe mode.
* <p>
* When encoding the line length is given in the constructor, the line separator is CRLF, and the encoding table is
* STANDARD_ENCODE_TABLE.
* </p>
* <p>
* Line lengths that aren't multiples of 4 will still essentially end up being multiples of 4 in the encoded data.
* </p>
* <p>
* When decoding all variants are supported.
* </p>
*
* @param lineLength Each line of encoded data will be at most of the given length (rounded down to the nearest multiple of
* 4). If lineLength &lt;= 0, then the output will not be divided into lines (chunks). Ignored when
* decoding.
* @since 1.4
*/
Base64Codec(final int lineLength) {
this(lineLength, CHUNK_SEPARATOR);
}
/**
* Creates a Base64 codec used for decoding (all modes) and encoding in URL-unsafe mode.
* <p>
* When encoding the line length and line separator are given in the constructor, and the encoding table is
* STANDARD_ENCODE_TABLE.
* </p>
* <p>
* Line lengths that aren't multiples of 4 will still essentially end up being multiples of 4 in the encoded data.
* </p>
* <p>
* When decoding all variants are supported.
* </p>
*
* @param lineLength Each line of encoded data will be at most of the given length (rounded down to the nearest multiple of
* 4). If lineLength &lt;= 0, then the output will not be divided into lines (chunks). Ignored when
* decoding.
* @param lineSeparator Each line of encoded data will end with this sequence of bytes.
* @throws IllegalArgumentException Thrown when the provided lineSeparator included some base64 characters.
* @since 1.4
*/
Base64Codec(final int lineLength, final byte[] lineSeparator) {
this(lineLength, lineSeparator, false);
}
/**
* Creates a Base64 codec used for decoding (all modes) and encoding in URL-unsafe mode.
* <p>
* When encoding the line length and line separator are given in the constructor, and the encoding table is
* STANDARD_ENCODE_TABLE.
* </p>
* <p>
* Line lengths that aren't multiples of 4 will still essentially end up being multiples of 4 in the encoded data.
* </p>
* <p>
* When decoding all variants are supported.
* </p>
*
* @param lineLength Each line of encoded data will be at most of the given length (rounded down to the nearest multiple of
* 4). If lineLength &lt;= 0, then the output will not be divided into lines (chunks). Ignored when
* decoding.
* @param lineSeparator Each line of encoded data will end with this sequence of bytes.
* @param urlSafe Instead of emitting '+' and '/' we emit '-' and '_' respectively. urlSafe is only applied to encode
* operations. Decoding seamlessly handles both modes.
* <b>Note: no padding is added when using the URL-safe alphabet.</b>
* @throws IllegalArgumentException Thrown when the {@code lineSeparator} contains Base64 characters.
* @since 1.4
*/
Base64Codec(final int lineLength, final byte[] lineSeparator, final boolean urlSafe) {
this(lineLength, lineSeparator, urlSafe, DECODING_POLICY_DEFAULT);
}
/**
* Creates a Base64 codec used for decoding (all modes) and encoding in URL-unsafe mode.
* <p>
* When encoding the line length and line separator are given in the constructor, and the encoding table is
* STANDARD_ENCODE_TABLE.
* </p>
* <p>
* Line lengths that aren't multiples of 4 will still essentially end up being multiples of 4 in the encoded data.
* </p>
* <p>
* When decoding all variants are supported.
* </p>
*
* @param lineLength Each line of encoded data will be at most of the given length (rounded down to the nearest multiple of
* 4). If lineLength &lt;= 0, then the output will not be divided into lines (chunks). Ignored when
* decoding.
* @param lineSeparator Each line of encoded data will end with this sequence of bytes.
* @param urlSafe Instead of emitting '+' and '/' we emit '-' and '_' respectively. urlSafe is only applied to encode
* operations. Decoding seamlessly handles both modes.
* <b>Note: no padding is added when using the URL-safe alphabet.</b>
* @param decodingPolicy The decoding policy.
* @throws IllegalArgumentException Thrown when the {@code lineSeparator} contains Base64 characters.
* @since 1.15
*/
Base64Codec(final int lineLength, final byte[] lineSeparator, final boolean urlSafe, final CodecPolicy decodingPolicy) {
super(BYTES_PER_UNENCODED_BLOCK, BYTES_PER_ENCODED_BLOCK, lineLength, BaseNCodec.length(lineSeparator),
PAD_DEFAULT, decodingPolicy);
// TODO could be simplified if there is no requirement to reject invalid line sep when length <=0
// @see test case Base64Test.testConstructors()
if (lineSeparator != null) {
if (containsAlphabetOrPad(lineSeparator)) {
final String sep = Strings.utf8(lineSeparator);
throw new IllegalArgumentException("lineSeparator must not contain base64 characters: [" + sep + "]");
}
if (lineLength > 0) { // null line-sep forces no chunking rather than throwing IAE
this.encodeSize = BYTES_PER_ENCODED_BLOCK + lineSeparator.length;
this.lineSeparator = lineSeparator.clone();
} else {
this.encodeSize = BYTES_PER_ENCODED_BLOCK;
this.lineSeparator = null;
}
} else {
this.encodeSize = BYTES_PER_ENCODED_BLOCK;
this.lineSeparator = null;
}
this.decodeSize = this.encodeSize - 1;
this.encodeTable = urlSafe ? URL_SAFE_ENCODE_TABLE : STANDARD_ENCODE_TABLE;
}
// Implementation of the Encoder Interface
/**
* <p>
* Decodes all of the provided data, starting at inPos, for inAvail bytes. Should be called at least twice: once
* with the data to decode, and once with inAvail set to "-1" to alert decoder that EOF has been reached. The "-1"
* call is not necessary when decoding, but it doesn't hurt, either.
* </p>
* <p>
* Ignores all non-base64 characters. This is how chunked (e.g. 76 character) data is handled, since CR and LF are
* silently ignored, but has implications for other bytes, too. This method subscribes to the garbage-in,
* garbage-out philosophy: it will not check the provided data for validity.
* </p>
* <p>
* Thanks to "commons" project in ws.apache.org for the bitwise operations, and general approach.
* http://svn.apache.org/repos/asf/webservices/commons/trunk/modules/util/
* </p>
*
* @param input byte[] array of ASCII data to base64 decode.
* @param inPos Position to start reading data from.
* @param inAvail Amount of bytes available from input for decoding.
* @param context the context to be used
*/
@Override
void decode(final byte[] input, int inPos, final int inAvail, final Context context) {
if (context.eof) {
return;
}
if (inAvail < 0) {
context.eof = true;
}
for (int i = 0; i < inAvail; i++) {
final byte[] buffer = ensureBufferSize(decodeSize, context);
final byte b = input[inPos++];
if (b == pad) {
// We're done.
context.eof = true;
break;
}
if (b >= 0 && b < DECODE_TABLE.length) {
final int result = DECODE_TABLE[b];
if (result >= 0) {
context.modulus = (context.modulus + 1) % BYTES_PER_ENCODED_BLOCK;
context.ibitWorkArea = (context.ibitWorkArea << BITS_PER_ENCODED_BYTE) + result;
if (context.modulus == 0) {
buffer[context.pos++] = (byte) (context.ibitWorkArea >> 16 & MASK_8BITS);
buffer[context.pos++] = (byte) (context.ibitWorkArea >> 8 & MASK_8BITS);
buffer[context.pos++] = (byte) (context.ibitWorkArea & MASK_8BITS);
}
}
}
}
// Two forms of EOF as far as base64 decoder is concerned: actual
// EOF (-1) and first time '=' character is encountered in stream.
// This approach makes the '=' padding characters completely optional.
if (context.eof && context.modulus != 0) {
final byte[] buffer = ensureBufferSize(decodeSize, context);
// We have some spare bits remaining
// Output all whole multiples of 8 bits and ignore the rest
switch (context.modulus) {
// case 0 : // impossible, as excluded above
case 1: // 6 bits - either ignore entirely, or raise an exception
validateTrailingCharacter();
break;
case 2: // 12 bits = 8 + 4
validateCharacter(MASK_4BITS, context);
context.ibitWorkArea = context.ibitWorkArea >> 4; // dump the extra 4 bits
buffer[context.pos++] = (byte) (context.ibitWorkArea & MASK_8BITS);
break;
case 3: // 18 bits = 8 + 8 + 2
validateCharacter(MASK_2BITS, context);
context.ibitWorkArea = context.ibitWorkArea >> 2; // dump 2 bits
buffer[context.pos++] = (byte) (context.ibitWorkArea >> 8 & MASK_8BITS);
buffer[context.pos++] = (byte) (context.ibitWorkArea & MASK_8BITS);
break;
default:
throw new IllegalStateException("Impossible modulus " + context.modulus);
}
}
}
/**
* <p>
* Encodes all of the provided data, starting at inPos, for inAvail bytes. Must be called at least twice: once with
* the data to encode, and once with inAvail set to "-1" to alert encoder that EOF has been reached, to flush last
* remaining bytes (if not multiple of 3).
* </p>
* <p><b>Note: no padding is added when encoding using the URL-safe alphabet.</b></p>
* <p>
* Thanks to "commons" project in ws.apache.org for the bitwise operations, and general approach.
* http://svn.apache.org/repos/asf/webservices/commons/trunk/modules/util/
* </p>
*
* @param in byte[] array of binary data to base64 encode.
* @param inPos Position to start reading data from.
* @param inAvail Amount of bytes available from input for encoding.
* @param context the context to be used
*/
@Override
void encode(final byte[] in, int inPos, final int inAvail, final Context context) {
if (context.eof) {
return;
}
// inAvail < 0 is how we're informed of EOF in the underlying data we're
// encoding.
if (inAvail < 0) {
context.eof = true;
if (0 == context.modulus && lineLength == 0) {
return; // no leftovers to process and not using chunking
}
final byte[] buffer = ensureBufferSize(encodeSize, context);
final int savedPos = context.pos;
switch (context.modulus) { // 0-2
case 0: // nothing to do here
break;
case 1: // 8 bits = 6 + 2
// top 6 bits:
buffer[context.pos++] = encodeTable[context.ibitWorkArea >> 2 & MASK_6BITS];
// remaining 2:
buffer[context.pos++] = encodeTable[context.ibitWorkArea << 4 & MASK_6BITS];
// URL-SAFE skips the padding to further reduce size.
if (encodeTable == STANDARD_ENCODE_TABLE) {
buffer[context.pos++] = pad;
buffer[context.pos++] = pad;
}
break;
case 2: // 16 bits = 6 + 6 + 4
buffer[context.pos++] = encodeTable[context.ibitWorkArea >> 10 & MASK_6BITS];
buffer[context.pos++] = encodeTable[context.ibitWorkArea >> 4 & MASK_6BITS];
buffer[context.pos++] = encodeTable[context.ibitWorkArea << 2 & MASK_6BITS];
// URL-SAFE skips the padding to further reduce size.
if (encodeTable == STANDARD_ENCODE_TABLE) {
buffer[context.pos++] = pad;
}
break;
default:
throw new IllegalStateException("Impossible modulus " + context.modulus);
}
context.currentLinePos += context.pos - savedPos; // keep track of current line position
// if currentPos == 0 we are at the start of a line, so don't add CRLF
if (lineLength > 0 && context.currentLinePos > 0) {
System.arraycopy(lineSeparator, 0, buffer, context.pos, lineSeparator.length);
context.pos += lineSeparator.length;
}
} else {
for (int i = 0; i < inAvail; i++) {
final byte[] buffer = ensureBufferSize(encodeSize, context);
context.modulus = (context.modulus + 1) % BYTES_PER_UNENCODED_BLOCK;
int b = in[inPos++];
if (b < 0) {
b += 256;
}
context.ibitWorkArea = (context.ibitWorkArea << 8) + b; // BITS_PER_BYTE
if (0 == context.modulus) { // 3 bytes = 24 bits = 4 * 6 bits to extract
buffer[context.pos++] = encodeTable[context.ibitWorkArea >> 18 & MASK_6BITS];
buffer[context.pos++] = encodeTable[context.ibitWorkArea >> 12 & MASK_6BITS];
buffer[context.pos++] = encodeTable[context.ibitWorkArea >> 6 & MASK_6BITS];
buffer[context.pos++] = encodeTable[context.ibitWorkArea & MASK_6BITS];
context.currentLinePos += BYTES_PER_ENCODED_BLOCK;
if (lineLength > 0 && lineLength <= context.currentLinePos) {
System.arraycopy(lineSeparator, 0, buffer, context.pos, lineSeparator.length);
context.pos += lineSeparator.length;
context.currentLinePos = 0;
}
}
}
}
}
/**
* Returns whether or not the {@code octet} is in the Base64 alphabet.
*
* @param octet The value to test
* @return {@code true} if the value is defined in the Base64 alphabet {@code false} otherwise.
*/
@Override
protected boolean isInAlphabet(final byte octet) {
return octet >= 0 && octet < decodeTable.length && decodeTable[octet] != -1;
}
/**
* Returns our current encode mode. True if we're URL-SAFE, false otherwise.
*
* @return true if we're in URL-SAFE mode, false otherwise.
* @since 1.4
*/
public boolean isUrlSafe() {
return this.encodeTable == URL_SAFE_ENCODE_TABLE;
}
/**
* Validates whether decoding the final trailing character is possible in the context
* of the set of possible base 64 values.
* <p>
* The character is valid if the lower bits within the provided mask are zero. This
* is used to test the final trailing base-64 digit is zero in the bits that will be discarded.
* </p>
*
* @param emptyBitsMask The mask of the lower bits that should be empty
* @param context the context to be used
* @throws IllegalArgumentException if the bits being checked contain any non-zero value
*/
private void validateCharacter(final int emptyBitsMask, final Context context) {
if (isStrictDecoding() && (context.ibitWorkArea & emptyBitsMask) != 0) {
throw new IllegalArgumentException(
"Strict decoding: Last encoded character (before the paddings if any) is a valid " +
"base 64 alphabet but not a possible encoding. " +
"Expected the discarded bits from the character to be zero.");
}
}
/**
* Validates whether decoding allows an entire final trailing character that cannot be
* used for a complete byte.
*
* @throws IllegalArgumentException if strict decoding is enabled
*/
private void validateTrailingCharacter() {
if (isStrictDecoding()) {
throw new IllegalArgumentException(
"Strict decoding: Last encoded character (before the paddings if any) is a valid " +
"base 64 alphabet but not a possible encoding. " +
"Decoding requires at least two trailing 6-bit characters to create bytes.");
}
}
}

View File

@ -0,0 +1,114 @@
/*
* Copyright © 2023 jsonwebtoken.io
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package io.jsonwebtoken.impl.io;
import java.io.InputStream;
/**
* Provides Base64 encoding and decoding in a streaming fashion (unlimited size). When encoding the default lineLength
* is 76 characters and the default lineEnding is CRLF, but these can be overridden by using the appropriate
* constructor.
* <p>
* The default behavior of the Base64InputStream is to DECODE, whereas the default behavior of the Base64OutputStream
* is to ENCODE, but this behavior can be overridden by using a different constructor.
* </p>
* <p>
* This class implements section <cite>6.8. Base64 Content-Transfer-Encoding</cite> from RFC 2045 <cite>Multipurpose
* Internet Mail Extensions (MIME) Part One: Format of Internet Message Bodies</cite> by Freed and Borenstein.
* </p>
* <p>
* Since this class operates directly on byte streams, and not character streams, it is hard-coded to only encode/decode
* character encodings which are compatible with the lower 127 ASCII chart (ISO-8859-1, Windows-1252, UTF-8, etc).
* </p>
* <p>
* You can set the decoding behavior when the input bytes contain leftover trailing bits that cannot be created by a
* valid encoding. These can be bits that are unused from the final character or entire characters. The default mode is
* lenient decoding.
* </p>
* <ul>
* <li>Lenient: Any trailing bits are composed into 8-bit bytes where possible. The remainder are discarded.
* <li>Strict: The decoding will raise an {@link IllegalArgumentException} if trailing bits are not part of a valid
* encoding. Any unused bits from the final character must be zero. Impossible counts of entire final characters are not
* allowed.
* </ul>
* <p>
* When strict decoding is enabled it is expected that the decoded bytes will be re-encoded to a byte array that matches
* the original, i.e. no changes occur on the final character. This requires that the input bytes use the same padding
* and alphabet as the encoder.
* </p>
*
* @see <a href="http://www.ietf.org/rfc/rfc2045.txt">RFC 2045</a>
* @since JJWT_RELEASE_VERSION, copied from
* <a href="https://github.com/apache/commons-codec/tree/585497f09b026f6602daf986723a554e051bdfe6">commons-codec
* 585497f09b026f6602daf986723a554e051bdfe6</a>
*/
public class Base64InputStream extends BaseNCodecInputStream {
/**
* Creates a Base64InputStream such that all data read is Base64-decoded from the original provided InputStream.
*
* @param inputStream InputStream to wrap.
*/
public Base64InputStream(final InputStream inputStream) {
this(inputStream, false);
}
/**
* Creates a Base64InputStream such that all data read is either Base64-encoded or Base64-decoded from the original
* provided InputStream.
*
* @param inputStream InputStream to wrap.
* @param doEncode true if we should encode all data read from us, false if we should decode.
*/
Base64InputStream(final InputStream inputStream, final boolean doEncode) {
super(inputStream, new Base64Codec(0, BaseNCodec.CHUNK_SEPARATOR, false, CodecPolicy.STRICT), doEncode);
}
// /**
// * Creates a Base64InputStream such that all data read is either Base64-encoded or Base64-decoded from the original
// * provided InputStream.
// *
// * @param inputStream InputStream to wrap.
// * @param doEncode true if we should encode all data read from us, false if we should decode.
// * @param lineLength If doEncode is true, each line of encoded data will contain lineLength characters (rounded down to
// * the nearest multiple of 4). If lineLength &lt;= 0, the encoded data is not divided into lines. If
// * doEncode is false, lineLength is ignored.
// * @param lineSeparator If doEncode is true, each line of encoded data will be terminated with this byte sequence (e.g. \r\n).
// * If lineLength &lt;= 0, the lineSeparator is not used. If doEncode is false lineSeparator is ignored.
// */
// Base64InputStream(final InputStream inputStream, final boolean doEncode, final int lineLength, final byte[] lineSeparator) {
// super(inputStream, new Base64Codec(lineLength, lineSeparator), doEncode);
// }
//
// /**
// * Creates a Base64InputStream such that all data read is either Base64-encoded or Base64-decoded from the original
// * provided InputStream.
// *
// * @param inputStream InputStream to wrap.
// * @param doEncode true if we should encode all data read from us, false if we should decode.
// * @param lineLength If doEncode is true, each line of encoded data will contain lineLength characters (rounded down to
// * the nearest multiple of 4). If lineLength &lt;= 0, the encoded data is not divided into lines. If
// * doEncode is false, lineLength is ignored.
// * @param lineSeparator If doEncode is true, each line of encoded data will be terminated with this byte sequence (e.g. \r\n).
// * If lineLength &lt;= 0, the lineSeparator is not used. If doEncode is false lineSeparator is ignored.
// * @param decodingPolicy The decoding policy.
// * @since 1.15
// */
// Base64InputStream(final InputStream inputStream, final boolean doEncode, final int lineLength, final byte[] lineSeparator,
// final CodecPolicy decodingPolicy) {
// super(inputStream, new Base64Codec(lineLength, lineSeparator, false, decodingPolicy), doEncode);
// }
}

View File

@ -0,0 +1,118 @@
/*
* Copyright © 2023 jsonwebtoken.io
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package io.jsonwebtoken.impl.io;
import java.io.OutputStream;
/**
* Provides Base64 encoding and decoding in a streaming fashion (unlimited size). When encoding the default lineLength
* is 76 characters and the default lineEnding is CRLF, but these can be overridden by using the appropriate
* constructor.
* <p>
* The default behavior of the Base64OutputStream is to ENCODE, whereas the default behavior of the Base64InputStream
* is to DECODE. But this behavior can be overridden by using a different constructor.
* </p>
* <p>
* This class implements section <cite>6.8. Base64 Content-Transfer-Encoding</cite> from RFC 2045 <cite>Multipurpose
* Internet Mail Extensions (MIME) Part One: Format of Internet Message Bodies</cite> by Freed and Borenstein.
* </p>
* <p>
* Since this class operates directly on byte streams, and not character streams, it is hard-coded to only encode/decode
* character encodings which are compatible with the lower 127 ASCII chart (ISO-8859-1, Windows-1252, UTF-8, etc).
* </p>
* <p>
* <b>Note:</b> It is mandatory to close the stream after the last byte has been written to it, otherwise the
* final padding will be omitted and the resulting data will be incomplete/inconsistent.
* </p>
* <p>
* You can set the decoding behavior when the input bytes contain leftover trailing bits that cannot be created by a
* valid encoding. These can be bits that are unused from the final character or entire characters. The default mode is
* lenient decoding.
* </p>
* <ul>
* <li>Lenient: Any trailing bits are composed into 8-bit bytes where possible. The remainder are discarded.
* <li>Strict: The decoding will raise an {@link IllegalArgumentException} if trailing bits are not part of a valid
* encoding. Any unused bits from the final character must be zero. Impossible counts of entire final characters are not
* allowed.
* </ul>
* <p>
* When strict decoding is enabled it is expected that the decoded bytes will be re-encoded to a byte array that matches
* the original, i.e. no changes occur on the final character. This requires that the input bytes use the same padding
* and alphabet as the encoder.
* </p>
*
* @see <a href="http://www.ietf.org/rfc/rfc2045.txt">RFC 2045</a>
* @since JJWT_RELEASE_VERSION, copied from
* <a href="https://github.com/apache/commons-codec/tree/585497f09b026f6602daf986723a554e051bdfe6">commons-codec
* 585497f09b026f6602daf986723a554e051bdfe6</a>
*/
class Base64OutputStream extends BaseNCodecOutputStream {
/**
* Creates a Base64OutputStream such that all data written is Base64-encoded to the original provided OutputStream.
*
* @param outputStream OutputStream to wrap.
*/
Base64OutputStream(final OutputStream outputStream) {
this(outputStream, true, true);
}
/**
* Creates a Base64OutputStream such that all data written is either Base64-encoded or Base64-decoded to the
* original provided OutputStream.
*
* @param outputStream OutputStream to wrap.
* @param doEncode true if we should encode all data written to us, false if we should decode.
*/
Base64OutputStream(final OutputStream outputStream, final boolean doEncode, boolean urlSafe) {
super(outputStream, new Base64Codec(0, BaseNCodec.CHUNK_SEPARATOR, urlSafe), doEncode);
}
/**
* Creates a Base64OutputStream such that all data written is either Base64-encoded or Base64-decoded to the
* original provided OutputStream.
*
* @param outputStream OutputStream to wrap.
* @param doEncode true if we should encode all data written to us, false if we should decode.
* @param lineLength If doEncode is true, each line of encoded data will contain lineLength characters (rounded down to
* the nearest multiple of 4). If lineLength &lt;= 0, the encoded data is not divided into lines. If
* doEncode is false, lineLength is ignored.
* @param lineSeparator If doEncode is true, each line of encoded data will be terminated with this byte sequence (e.g. \r\n).
* If lineLength &lt;= 0, the lineSeparator is not used. If doEncode is false lineSeparator is ignored.
*/
Base64OutputStream(final OutputStream outputStream, final boolean doEncode, final int lineLength, final byte[] lineSeparator) {
super(outputStream, new Base64Codec(lineLength, lineSeparator), doEncode);
}
/**
* Creates a Base64OutputStream such that all data written is either Base64-encoded or Base64-decoded to the
* original provided OutputStream.
*
* @param outputStream OutputStream to wrap.
* @param doEncode true if we should encode all data written to us, false if we should decode.
* @param lineLength If doEncode is true, each line of encoded data will contain lineLength characters (rounded down to
* the nearest multiple of 4). If lineLength &lt;= 0, the encoded data is not divided into lines. If
* doEncode is false, lineLength is ignored.
* @param lineSeparator If doEncode is true, each line of encoded data will be terminated with this byte sequence (e.g. \r\n).
* If lineLength &lt;= 0, the lineSeparator is not used. If doEncode is false lineSeparator is ignored.
* @param decodingPolicy The decoding policy.
* @since 1.15
*/
Base64OutputStream(final OutputStream outputStream, final boolean doEncode, final int lineLength,
final byte[] lineSeparator, final CodecPolicy decodingPolicy) {
super(outputStream, new Base64Codec(lineLength, lineSeparator, false, decodingPolicy), doEncode);
}
}

View File

@ -0,0 +1,34 @@
/*
* Copyright © 2023 jsonwebtoken.io
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package io.jsonwebtoken.impl.io;
import io.jsonwebtoken.io.Encoder;
import io.jsonwebtoken.io.EncodingException;
import java.io.OutputStream;
public final class Base64UrlStreamEncoder implements Encoder<OutputStream, OutputStream> {
public static final Base64UrlStreamEncoder INSTANCE = new Base64UrlStreamEncoder();
private Base64UrlStreamEncoder() {
}
@Override
public OutputStream encode(OutputStream outputStream) throws EncodingException {
return new Base64OutputStream(outputStream);
}
}

View File

@ -0,0 +1,660 @@
/*
* Copyright © 2023 jsonwebtoken.io
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package io.jsonwebtoken.impl.io;
import io.jsonwebtoken.lang.Strings;
import java.util.Arrays;
import java.util.Objects;
/**
* Abstract superclass for Base-N encoders and decoders.
*
* <p>
* This class is thread-safe.
* </p>
* <p>
* You can set the decoding behavior when the input bytes contain leftover trailing bits that cannot be created by a
* valid encoding. These can be bits that are unused from the final character or entire characters. The default mode is
* lenient decoding.
* </p>
* <ul>
* <li>Lenient: Any trailing bits are composed into 8-bit bytes where possible. The remainder are discarded.
* <li>Strict: The decoding will raise an {@link IllegalArgumentException} if trailing bits are not part of a valid
* encoding. Any unused bits from the final character must be zero. Impossible counts of entire final characters are not
* allowed.
* </ul>
* <p>
* When strict decoding is enabled it is expected that the decoded bytes will be re-encoded to a byte array that matches
* the original, i.e. no changes occur on the final character. This requires that the input bytes use the same padding
* and alphabet as the encoder.
* </p>
*
* @since JJWT_RELEASE_VERSION, copied from
* <a href="https://github.com/apache/commons-codec/tree/585497f09b026f6602daf986723a554e051bdfe6">commons-codec
* 585497f09b026f6602daf986723a554e051bdfe6</a>
*/
abstract class BaseNCodec {
/**
* EOF
*/
static final int EOF = -1;
/**
* MIME chunk size per RFC 2045 section 6.8.
*
* <p>
* The {@value} character limit does not count the trailing CRLF, but counts all other characters, including any
* equal signs.
* </p>
*
* @see <a href="http://www.ietf.org/rfc/rfc2045.txt">RFC 2045 section 6.8</a>
*/
public static final int MIME_CHUNK_SIZE = 76;
// /**
// * PEM chunk size per RFC 1421 section 4.3.2.4.
// *
// * <p>
// * The {@value} character limit does not count the trailing CRLF, but counts all other characters, including any
// * equal signs.
// * </p>
// *
// * @see <a href="http://tools.ietf.org/html/rfc1421">RFC 1421 section 4.3.2.4</a>
// */
// public static final int PEM_CHUNK_SIZE = 64;
private static final int DEFAULT_BUFFER_RESIZE_FACTOR = 2;
/**
* Defines the default buffer size - currently {@value}
* - must be large enough for at least one encoded block+separator
*/
private static final int DEFAULT_BUFFER_SIZE = 8192;
/**
* The maximum size buffer to allocate.
*
* <p>This is set to the same size used in the JDK {@code java.util.ArrayList}:</p>
* <blockquote>
* Some VMs reserve some header words in an array.
* Attempts to allocate larger arrays may result in
* OutOfMemoryError: Requested array size exceeds VM limit.
* </blockquote>
*/
private static final int MAX_BUFFER_SIZE = Integer.MAX_VALUE - 8;
/**
* Mask used to extract 8 bits, used in decoding bytes
*/
protected static final int MASK_8BITS = 0xff;
/**
* Byte used to pad output.
*/
protected static final byte PAD_DEFAULT = '='; // Allow static access to default
/**
* The default decoding policy.
*
* @since 1.15
*/
protected static final CodecPolicy DECODING_POLICY_DEFAULT = CodecPolicy.LENIENT;
/**
* Chunk separator per RFC 2045 section 2.1.
*
* @see <a href="http://www.ietf.org/rfc/rfc2045.txt">RFC 2045 section 2.1</a>
*/
static final byte[] CHUNK_SEPARATOR = {'\r', '\n'};
/**
* Pad byte. Instance variable just in case it needs to vary later.
*/
protected final byte pad;
/**
* Number of bytes in each full block of unencoded data, e.g. 4 for Base64 and 5 for Base32
*/
private final int unencodedBlockSize;
/**
* Number of bytes in each full block of encoded data, e.g. 3 for Base64 and 8 for Base32
*/
private final int encodedBlockSize;
/**
* Chunksize for encoding. Not used when decoding.
* A value of zero or less implies no chunking of the encoded data.
* Rounded down to the nearest multiple of encodedBlockSize.
*/
protected final int lineLength;
/**
* Size of chunk separator. Not used unless {@link #lineLength} &gt; 0.
*/
private final int chunkSeparatorLength;
/**
* Defines the decoding behavior when the input bytes contain leftover trailing bits that
* cannot be created by a valid encoding. These can be bits that are unused from the final
* character or entire characters. The default mode is lenient decoding. Set this to
* {@code true} to enable strict decoding.
* <ul>
* <li>Lenient: Any trailing bits are composed into 8-bit bytes where possible.
* The remainder are discarded.
* <li>Strict: The decoding will raise an {@link IllegalArgumentException} if trailing bits
* are not part of a valid encoding. Any unused bits from the final character must
* be zero. Impossible counts of entire final characters are not allowed.
* </ul>
* <p>
* When strict decoding is enabled it is expected that the decoded bytes will be re-encoded
* to a byte array that matches the original, i.e. no changes occur on the final
* character. This requires that the input bytes use the same padding and alphabet
* as the encoder.
* </p>
*/
private final CodecPolicy decodingPolicy;
/**
* Holds thread context so classes can be thread-safe.
* <p>
* This class is not itself thread-safe; each thread must allocate its own copy.
*
* @since 1.7
*/
static class Context {
/**
* Placeholder for the bytes we're dealing with for our based logic.
* Bitwise operations store and extract the encoding or decoding from this variable.
*/
int ibitWorkArea;
/**
* Placeholder for the bytes we're dealing with for our based logic.
* Bitwise operations store and extract the encoding or decoding from this variable.
*/
long lbitWorkArea;
/**
* Buffer for streaming.
*/
byte[] buffer;
/**
* Position where next character should be written in the buffer.
*/
int pos;
/**
* Position where next character should be read from the buffer.
*/
int readPos;
/**
* Boolean flag to indicate the EOF has been reached. Once EOF has been reached, this object becomes useless,
* and must be thrown away.
*/
boolean eof;
/**
* Variable tracks how many characters have been written to the current line. Only used when encoding. We use
* it to make sure each encoded line never goes beyond lineLength (if lineLength &gt; 0).
*/
int currentLinePos;
/**
* Writes to the buffer only occur after every 3/5 reads when encoding, and every 4/8 reads when decoding. This
* variable helps track that.
*/
int modulus;
/**
* Returns a String useful for debugging (especially within a debugger.)
*
* @return a String useful for debugging.
*/
@Override
public String toString() {
return String.format("%s[buffer=%s, currentLinePos=%s, eof=%s, ibitWorkArea=%s, lbitWorkArea=%s, " +
"modulus=%s, pos=%s, readPos=%s]", getClass().getSimpleName(), Arrays.toString(buffer),
currentLinePos, eof, ibitWorkArea, lbitWorkArea, modulus, pos, readPos);
}
}
/**
* Create a positive capacity at least as large the minimum required capacity.
* If the minimum capacity is negative then this throws an OutOfMemoryError as no array
* can be allocated.
*
* @param minCapacity the minimum capacity
* @return the capacity
* @throws OutOfMemoryError if the {@code minCapacity} is negative
*/
private static int createPositiveCapacity(final int minCapacity) {
if (minCapacity < 0) {
// overflow
throw new OutOfMemoryError("Unable to allocate array size: " + (minCapacity & 0xffffffffL));
}
// This is called when we require buffer expansion to a very big array.
// Use the conservative maximum buffer size if possible, otherwise the biggest required.
//
// Note: In this situation JDK 1.8 java.util.ArrayList returns Integer.MAX_VALUE.
// This excludes some VMs that can exceed MAX_BUFFER_SIZE but not allocate a full
// Integer.MAX_VALUE length array.
// The result is that we may have to allocate an array of this size more than once if
// the capacity must be expanded again.
return Math.max(minCapacity, MAX_BUFFER_SIZE);
}
// /**
// * Gets a copy of the chunk separator per RFC 2045 section 2.1.
// *
// * @return the chunk separator
// * @see <a href="http://www.ietf.org/rfc/rfc2045.txt">RFC 2045 section 2.1</a>
// * @since 1.15
// */
// public static byte[] getChunkSeparator() {
// return CHUNK_SEPARATOR.clone();
// }
/**
* Checks if a byte value is whitespace or not.
*
* @param byteToCheck the byte to check
* @return true if byte is whitespace, false otherwise
* @see Character#isWhitespace(int)
* @deprecated Use {@link Character#isWhitespace(int)}.
*/
@Deprecated
protected static boolean isWhiteSpace(final byte byteToCheck) {
return Character.isWhitespace(byteToCheck);
}
private static int compareUnsigned(int x, int y) {
return Integer.compare(x + Integer.MIN_VALUE, y + Integer.MIN_VALUE);
}
/**
* Increases our buffer by the {@link #DEFAULT_BUFFER_RESIZE_FACTOR}.
*
* @param context the context to be used
* @param minCapacity the minimum required capacity
* @return the resized byte[] buffer
* @throws OutOfMemoryError if the {@code minCapacity} is negative
*/
private static byte[] resizeBuffer(final Context context, final int minCapacity) {
// Overflow-conscious code treats the min and new capacity as unsigned.
final int oldCapacity = context.buffer.length;
int newCapacity = oldCapacity * DEFAULT_BUFFER_RESIZE_FACTOR;
if (compareUnsigned(newCapacity, minCapacity) < 0) {
newCapacity = minCapacity;
}
if (compareUnsigned(newCapacity, MAX_BUFFER_SIZE) > 0) {
newCapacity = createPositiveCapacity(minCapacity);
}
final byte[] b = Arrays.copyOf(context.buffer, newCapacity);
context.buffer = b;
return b;
}
/**
* Note {@code lineLength} is rounded down to the nearest multiple of the encoded block size.
* If {@code chunkSeparatorLength} is zero, then chunking is disabled.
*
* @param unencodedBlockSize the size of an unencoded block (e.g. Base64 = 3)
* @param encodedBlockSize the size of an encoded block (e.g. Base64 = 4)
* @param lineLength if &gt; 0, use chunking with a length {@code lineLength}
* @param chunkSeparatorLength the chunk separator length, if relevant
*/
protected BaseNCodec(final int unencodedBlockSize, final int encodedBlockSize,
final int lineLength, final int chunkSeparatorLength) {
this(unencodedBlockSize, encodedBlockSize, lineLength, chunkSeparatorLength, PAD_DEFAULT);
}
/**
* Note {@code lineLength} is rounded down to the nearest multiple of the encoded block size.
* If {@code chunkSeparatorLength} is zero, then chunking is disabled.
*
* @param unencodedBlockSize the size of an unencoded block (e.g. Base64 = 3)
* @param encodedBlockSize the size of an encoded block (e.g. Base64 = 4)
* @param lineLength if &gt; 0, use chunking with a length {@code lineLength}
* @param chunkSeparatorLength the chunk separator length, if relevant
* @param pad byte used as padding byte.
*/
protected BaseNCodec(final int unencodedBlockSize, final int encodedBlockSize,
final int lineLength, final int chunkSeparatorLength, final byte pad) {
this(unencodedBlockSize, encodedBlockSize, lineLength, chunkSeparatorLength, pad, DECODING_POLICY_DEFAULT);
}
/**
* Note {@code lineLength} is rounded down to the nearest multiple of the encoded block size.
* If {@code chunkSeparatorLength} is zero, then chunking is disabled.
*
* @param unencodedBlockSize the size of an unencoded block (e.g. Base64 = 3)
* @param encodedBlockSize the size of an encoded block (e.g. Base64 = 4)
* @param lineLength if &gt; 0, use chunking with a length {@code lineLength}
* @param chunkSeparatorLength the chunk separator length, if relevant
* @param pad byte used as padding byte.
* @param decodingPolicy Decoding policy.
* @since 1.15
*/
protected BaseNCodec(final int unencodedBlockSize, final int encodedBlockSize,
final int lineLength, final int chunkSeparatorLength, final byte pad,
final CodecPolicy decodingPolicy) {
this.unencodedBlockSize = unencodedBlockSize;
this.encodedBlockSize = encodedBlockSize;
final boolean useChunking = lineLength > 0 && chunkSeparatorLength > 0;
this.lineLength = useChunking ? lineLength / encodedBlockSize * encodedBlockSize : 0;
this.chunkSeparatorLength = chunkSeparatorLength;
this.pad = pad;
this.decodingPolicy = Objects.requireNonNull(decodingPolicy, "codecPolicy");
}
/**
* Returns the amount of buffered data available for reading.
*
* @param context the context to be used
* @return The amount of buffered data available for reading.
*/
int available(final Context context) { // package protected for access from I/O streams
return hasData(context) ? context.pos - context.readPos : 0;
}
/**
* Tests a given byte array to see if it contains any characters within the alphabet or PAD.
* <p>
* Intended for use in checking line-ending arrays
*
* @param arrayOctet byte array to test
* @return {@code true} if any byte is a valid character in the alphabet or PAD; {@code false} otherwise
*/
protected boolean containsAlphabetOrPad(final byte[] arrayOctet) {
if (arrayOctet == null) {
return false;
}
for (final byte element : arrayOctet) {
if (pad == element || isInAlphabet(element)) {
return true;
}
}
return false;
}
static int length(byte[] bytes) {
return bytes != null ? bytes.length : 0;
}
static boolean isEmpty(byte[] bytes) {
return length(bytes) == 0;
}
/**
* Decodes a byte[] containing characters in the Base-N alphabet.
*
* @param pArray A byte array containing Base-N character data
* @return a byte array containing binary data
*/
public byte[] decode(final byte[] pArray) {
if (isEmpty(pArray)) {
return pArray;
}
final Context context = new Context();
decode(pArray, 0, pArray.length, context);
decode(pArray, 0, EOF, context); // Notify decoder of EOF.
final byte[] result = new byte[context.pos];
readResults(result, 0, result.length, context);
return result;
}
// package protected for access from I/O streams
abstract void decode(byte[] pArray, int i, int length, Context context);
/**
* Decodes a String containing characters in the Base-N alphabet.
*
* @param pArray A String containing Base-N character data
* @return a byte array containing binary data
*/
public byte[] decode(final String pArray) {
return decode(Strings.utf8(pArray));
}
/**
* Encodes a byte[] containing binary data, into a byte[] containing characters in the alphabet.
*
* @param pArray a byte array containing binary data
* @return A byte array containing only the base N alphabetic character data
*/
public byte[] encode(final byte[] pArray) {
if (isEmpty(pArray)) {
return pArray;
}
return encode(pArray, 0, pArray.length);
}
/**
* Encodes a byte[] containing binary data, into a byte[] containing
* characters in the alphabet.
*
* @param pArray a byte array containing binary data
* @param offset initial offset of the subarray.
* @param length length of the subarray.
* @return A byte array containing only the base N alphabetic character data
*/
public byte[] encode(final byte[] pArray, final int offset, final int length) {
if (isEmpty(pArray)) {
return pArray;
}
final Context context = new Context();
encode(pArray, offset, length, context);
encode(pArray, offset, EOF, context); // Notify encoder of EOF.
final byte[] buf = new byte[context.pos - context.readPos];
readResults(buf, 0, buf.length, context);
return buf;
}
// package protected for access from I/O streams
abstract void encode(byte[] pArray, int i, int length, Context context);
/**
* Encodes a byte[] containing binary data, into a String containing characters in the appropriate alphabet.
* Uses UTF8 encoding.
*
* @param pArray a byte array containing binary data
* @return String containing only character data in the appropriate alphabet.
* @since 1.5
* This is a duplicate of {@link #encodeToString(byte[])}; it was merged during refactoring.
*/
public String encodeAsString(final byte[] pArray) {
return Strings.utf8(encode(pArray));
}
/**
* Encodes a byte[] containing binary data, into a String containing characters in the Base-N alphabet.
* Uses UTF8 encoding.
*
* @param pArray a byte array containing binary data
* @return A String containing only Base-N character data
*/
public String encodeToString(final byte[] pArray) {
return Strings.utf8(encode(pArray));
}
/**
* Ensure that the buffer has room for {@code size} bytes
*
* @param size minimum spare space required
* @param context the context to be used
* @return the buffer
*/
protected byte[] ensureBufferSize(final int size, final Context context) {
if (context.buffer == null) {
context.buffer = new byte[Math.max(size, getDefaultBufferSize())];
context.pos = 0;
context.readPos = 0;
// Overflow-conscious:
// x + y > z == x + y - z > 0
} else if (context.pos + size - context.buffer.length > 0) {
return resizeBuffer(context, context.pos + size);
}
return context.buffer;
}
// /**
// * Returns the decoding behavior policy.
// *
// * <p>
// * The default is lenient. If the decoding policy is strict, then decoding will raise an
// * {@link IllegalArgumentException} if trailing bits are not part of a valid encoding. Decoding will compose
// * trailing bits into 8-bit bytes and discard the remainder.
// * </p>
// *
// * @return true if using strict decoding
// * @since 1.15
// */
// public CodecPolicy getCodecPolicy() {
// return decodingPolicy;
// }
/**
* Get the default buffer size. Can be overridden.
*
* @return the default buffer size.
*/
protected int getDefaultBufferSize() {
return DEFAULT_BUFFER_SIZE;
}
/**
* Calculates the amount of space needed to encode the supplied array.
*
* @param pArray byte[] array which will later be encoded
* @return amount of space needed to encode the supplied array.
* Returns a long since a max-len array will require &gt; Integer.MAX_VALUE
*/
public long getEncodedLength(final byte[] pArray) {
// Calculate non-chunked size - rounded up to allow for padding
// cast to long is needed to avoid possibility of overflow
long len = (pArray.length + unencodedBlockSize - 1) / unencodedBlockSize * (long) encodedBlockSize;
if (lineLength > 0) { // We're using chunking
// Round up to nearest multiple
len += (len + lineLength - 1) / lineLength * chunkSeparatorLength;
}
return len;
}
/**
* Returns true if this object has buffered data for reading.
*
* @param context the context to be used
* @return true if there is data still available for reading.
*/
boolean hasData(final Context context) { // package protected for access from I/O streams
return context.pos > context.readPos;
}
/**
* Returns whether or not the {@code octet} is in the current alphabet.
* Does not allow whitespace or pad.
*
* @param value The value to test
* @return {@code true} if the value is defined in the current alphabet, {@code false} otherwise.
*/
protected abstract boolean isInAlphabet(byte value);
/**
* Tests a given byte array to see if it contains only valid characters within the alphabet.
* The method optionally treats whitespace and pad as valid.
*
* @param arrayOctet byte array to test
* @param allowWSPad if {@code true}, then whitespace and PAD are also allowed
* @return {@code true} if all bytes are valid characters in the alphabet or if the byte array is empty;
* {@code false}, otherwise
*/
public boolean isInAlphabet(final byte[] arrayOctet, final boolean allowWSPad) {
for (final byte octet : arrayOctet) {
if (!isInAlphabet(octet) &&
(!allowWSPad || octet != pad && !Character.isWhitespace(octet))) {
return false;
}
}
return true;
}
/**
* Tests a given String to see if it contains only valid characters within the alphabet.
* The method treats whitespace and PAD as valid.
*
* @param basen String to test
* @return {@code true} if all characters in the String are valid characters in the alphabet or if
* the String is empty; {@code false}, otherwise
* @see #isInAlphabet(byte[], boolean)
*/
public boolean isInAlphabet(final String basen) {
return isInAlphabet(Strings.utf8(basen), true);
}
/**
* Returns true if decoding behavior is strict. Decoding will raise an {@link IllegalArgumentException} if trailing
* bits are not part of a valid encoding.
*
* <p>
* The default is false for lenient decoding. Decoding will compose trailing bits into 8-bit bytes and discard the
* remainder.
* </p>
*
* @return true if using strict decoding
* @since 1.15
*/
public boolean isStrictDecoding() {
return decodingPolicy == CodecPolicy.STRICT;
}
/**
* Extracts buffered data into the provided byte[] array, starting at position bPos, up to a maximum of bAvail
* bytes. Returns how many bytes were actually extracted.
* <p>
* Package private for access from I/O streams.
* </p>
*
* @param b byte[] array to extract the buffered data into.
* @param bPos position in byte[] array to start extraction at.
* @param bAvail amount of bytes we're allowed to extract. We may extract fewer (if fewer are available).
* @param context the context to be used
* @return The number of bytes successfully extracted into the provided byte[] array.
*/
int readResults(final byte[] b, final int bPos, final int bAvail, final Context context) {
if (hasData(context)) {
final int len = Math.min(available(context), bAvail);
System.arraycopy(context.buffer, context.readPos, b, bPos, len);
context.readPos += len;
if (!hasData(context)) {
// All data read.
// Reset position markers but do not set buffer to null to allow its reuse.
// hasData(context) will still return false, and this method will return 0 until
// more data is available, or -1 if EOF.
context.pos = context.readPos = 0;
}
return len;
}
return context.eof ? EOF : 0;
}
}

View File

@ -0,0 +1,236 @@
/*
* Copyright © 2023 jsonwebtoken.io
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package io.jsonwebtoken.impl.io;
import java.io.FilterInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.util.Objects;
/**
* Abstract superclass for Base-N input streams.
*
* @since JJWT_RELEASE_VERSION, copied from
* <a href="https://github.com/apache/commons-codec/tree/585497f09b026f6602daf986723a554e051bdfe6">commons-codec
* 585497f09b026f6602daf986723a554e051bdfe6</a>
*/
class BaseNCodecInputStream extends FilterInputStream {
private final BaseNCodec baseNCodec;
private final boolean doEncode;
private final byte[] singleByte = new byte[1];
private final byte[] buf;
private final BaseNCodec.Context context = new BaseNCodec.Context();
/**
* Create an instance.
*
* @param inputStream the input stream
* @param baseNCodec the codec
* @param doEncode set to true to perform encoding, else decoding
*/
protected BaseNCodecInputStream(final InputStream inputStream, final BaseNCodec baseNCodec, final boolean doEncode) {
super(inputStream);
this.doEncode = doEncode;
this.baseNCodec = baseNCodec;
this.buf = new byte[doEncode ? 4096 : 8192];
}
/**
* {@inheritDoc}
*
* @return {@code 0} if the {@link InputStream} has reached {@code EOF},
* {@code 1} otherwise
* @since 1.7
*/
@Override
public int available() throws IOException {
// Note: the logic is similar to the InflaterInputStream:
// as long as we have not reached EOF, indicate that there is more
// data available. As we do not know for sure how much data is left,
// just return 1 as a safe guess.
return context.eof ? 0 : 1;
}
/**
* Returns true if decoding behavior is strict. Decoding will raise an
* {@link IllegalArgumentException} if trailing bits are not part of a valid encoding.
*
* <p>
* The default is false for lenient encoding. Decoding will compose trailing bits
* into 8-bit bytes and discard the remainder.
* </p>
*
* @return true if using strict decoding
* @since 1.15
*/
public boolean isStrictDecoding() {
return baseNCodec.isStrictDecoding();
}
/**
* Marks the current position in this input stream.
* <p>
* The {@link #mark} method of {@link BaseNCodecInputStream} does nothing.
* </p>
*
* @param readLimit the maximum limit of bytes that can be read before the mark position becomes invalid.
* @see #markSupported()
* @since 1.7
*/
@Override
public synchronized void mark(final int readLimit) {
// noop
}
/**
* {@inheritDoc}
*
* @return Always returns {@code false}
*/
@Override
public boolean markSupported() {
return false; // not an easy job to support marks
}
/**
* Reads one {@code byte} from this input stream.
*
* @return the byte as an integer in the range 0 to 255. Returns -1 if EOF has been reached.
* @throws IOException if an I/O error occurs.
*/
@Override
public int read() throws IOException {
int r = read(singleByte, 0, 1);
while (r == 0) {
r = read(singleByte, 0, 1);
}
if (r > 0) {
final byte b = singleByte[0];
return b < 0 ? 256 + b : b;
}
return BaseNCodec.EOF;
}
/**
* Attempts to read {@code len} bytes into the specified {@code b} array starting at {@code offset}
* from this InputStream.
*
* @param array destination byte array
* @param offset where to start writing the bytes
* @param len maximum number of bytes to read
* @return number of bytes read
* @throws IOException if an I/O error occurs.
* @throws NullPointerException if the byte array parameter is null
* @throws IndexOutOfBoundsException if offset, len or buffer size are invalid
*/
@Override
public int read(final byte[] array, final int offset, final int len) throws IOException {
Objects.requireNonNull(array, "array");
if (offset < 0 || len < 0) {
throw new IndexOutOfBoundsException();
}
if (offset > array.length || offset + len > array.length) {
throw new IndexOutOfBoundsException();
}
if (len == 0) {
return 0;
}
int readLen = 0;
/*
Rationale for while-loop on (readLen == 0):
-----
Base32.readResults() usually returns > 0 or EOF (-1). In the
rare case where it returns 0, we just keep trying.
This is essentially an undocumented contract for InputStream
implementors that want their code to work properly with
java.io.InputStreamReader, since the latter hates it when
InputStream.read(byte[]) returns a zero. Unfortunately our
readResults() call must return 0 if a large amount of the data
being decoded was non-base32, so this while-loop enables proper
interop with InputStreamReader for that scenario.
-----
This is a fix for CODEC-101
*/
// Attempt to read the request length
while (readLen < len) {
if (!baseNCodec.hasData(context)) {
// Obtain more data.
// buf is reused across calls to read to avoid repeated allocations
final int c = in.read(buf);
if (doEncode) {
baseNCodec.encode(buf, 0, c, context);
} else {
baseNCodec.decode(buf, 0, c, context);
}
}
final int read = baseNCodec.readResults(array, offset + readLen, len - readLen, context);
if (read < 0) {
// Return the amount read or EOF
return readLen != 0 ? readLen : -1;
}
readLen += read;
}
return readLen;
}
/**
* Repositions this stream to the position at the time the mark method was last called on this input stream.
* <p>
* The {@link #reset} method of {@link BaseNCodecInputStream} does nothing except throw an {@link IOException}.
*
* @throws IOException if this method is invoked
* @since 1.7
*/
@Override
public synchronized void reset() throws IOException {
throw new IOException("mark/reset not supported");
}
/**
* {@inheritDoc}
*
* @throws IllegalArgumentException if the provided skip length is negative
* @since 1.7
*/
@Override
public long skip(final long n) throws IOException {
if (n < 0) {
throw new IllegalArgumentException("Negative skip length: " + n);
}
// skip in chunks of 512 bytes
final byte[] b = new byte[512];
long todo = n;
while (todo > 0) {
int len = (int) Math.min(b.length, todo);
len = this.read(b, 0, len);
if (len == BaseNCodec.EOF) {
break;
}
todo -= len;
}
return n - todo;
}
}

View File

@ -0,0 +1,180 @@
/*
* Copyright © 2023 jsonwebtoken.io
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package io.jsonwebtoken.impl.io;
import java.io.FilterOutputStream;
import java.io.IOException;
import java.io.OutputStream;
import java.util.Objects;
/**
* Abstract superclass for Base-N output streams.
* <p>
* To write the EOF marker without closing the stream, call {@link #eof()} or use an <a
* href="https://commons.apache.org/proper/commons-io/">Apache Commons IO</a> <a href=
* "https://commons.apache.org/proper/commons-io/apidocs/org/apache/commons/io/output/CloseShieldOutputStream.html"
* >CloseShieldOutputStream</a>.
* </p>
*
* @since JJWT_RELEASE_VERSION, copied from
* <a href="https://github.com/apache/commons-codec/tree/585497f09b026f6602daf986723a554e051bdfe6">commons-codec
* 585497f09b026f6602daf986723a554e051bdfe6</a>
*/
class BaseNCodecOutputStream extends FilterOutputStream {
private final boolean doEncode;
private final BaseNCodec baseNCodec;
private final byte[] singleByte = new byte[1];
private final BaseNCodec.Context context = new BaseNCodec.Context();
/**
* TODO should this be protected?
*
* @param outputStream the underlying output or null.
* @param basedCodec a BaseNCodec.
* @param doEncode true to encode, false to decode, TODO should be an enum?
*/
BaseNCodecOutputStream(final OutputStream outputStream, final BaseNCodec basedCodec, final boolean doEncode) {
super(outputStream);
this.baseNCodec = basedCodec;
this.doEncode = doEncode;
}
/**
* Closes this output stream and releases any system resources associated with the stream.
* <p>
* To write the EOF marker without closing the stream, call {@link #eof()} or use an
* <a href="https://commons.apache.org/proper/commons-io/">Apache Commons IO</a> <a href=
* "https://commons.apache.org/proper/commons-io/apidocs/org/apache/commons/io/output/CloseShieldOutputStream.html"
* >CloseShieldOutputStream</a>.
* </p>
*
* @throws IOException if an I/O error occurs.
*/
@Override
public void close() throws IOException {
eof();
flush();
out.close();
}
/**
* Writes EOF.
*
* @since 1.11
*/
public void eof() {
// Notify encoder of EOF (-1).
if (doEncode) {
baseNCodec.encode(singleByte, 0, BaseNCodec.EOF, context);
} else {
baseNCodec.decode(singleByte, 0, BaseNCodec.EOF, context);
}
}
/**
* Flushes this output stream and forces any buffered output bytes to be written out to the stream.
*
* @throws IOException if an I/O error occurs.
*/
@Override
public void flush() throws IOException {
flush(true);
}
/**
* Flushes this output stream and forces any buffered output bytes to be written out to the stream. If propagate is
* true, the wrapped stream will also be flushed.
*
* @param propagate boolean flag to indicate whether the wrapped OutputStream should also be flushed.
* @throws IOException if an I/O error occurs.
*/
private void flush(final boolean propagate) throws IOException {
final int avail = baseNCodec.available(context);
if (avail > 0) {
final byte[] buf = new byte[avail];
final int c = baseNCodec.readResults(buf, 0, avail, context);
if (c > 0) {
out.write(buf, 0, c);
}
}
if (propagate) {
out.flush();
}
}
/**
* Returns true if decoding behavior is strict. Decoding will raise an
* {@link IllegalArgumentException} if trailing bits are not part of a valid encoding.
*
* <p>
* The default is false for lenient encoding. Decoding will compose trailing bits
* into 8-bit bytes and discard the remainder.
* </p>
*
* @return true if using strict decoding
* @since 1.15
*/
public boolean isStrictDecoding() {
return baseNCodec.isStrictDecoding();
}
/**
* Writes {@code len} bytes from the specified {@code b} array starting at {@code offset} to this
* output stream.
*
* @param array source byte array
* @param offset where to start reading the bytes
* @param len maximum number of bytes to write
* @throws IOException if an I/O error occurs.
* @throws NullPointerException if the byte array parameter is null
* @throws IndexOutOfBoundsException if offset, len or buffer size are invalid
*/
@Override
public void write(final byte[] array, final int offset, final int len) throws IOException {
Objects.requireNonNull(array, "array");
if (offset < 0 || len < 0) {
throw new IndexOutOfBoundsException();
}
if (offset > array.length || offset + len > array.length) {
throw new IndexOutOfBoundsException();
}
if (len > 0) {
if (doEncode) {
baseNCodec.encode(array, offset, len, context);
} else {
baseNCodec.decode(array, offset, len, context);
}
flush(false);
}
}
/**
* Writes the specified {@code byte} to this output stream.
*
* @param i source byte
* @throws IOException if an I/O error occurs.
*/
@Override
public void write(final int i) throws IOException {
singleByte[0] = (byte) i;
write(singleByte, 0, 1);
}
}

View File

@ -0,0 +1,61 @@
/*
* Copyright © 2023 jsonwebtoken.io
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package io.jsonwebtoken.impl.io;
import io.jsonwebtoken.io.Encoder;
import io.jsonwebtoken.io.EncodingException;
import io.jsonwebtoken.lang.Assert;
import io.jsonwebtoken.lang.Strings;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.OutputStream;
public class ByteBase64UrlStreamEncoder implements Encoder<OutputStream, OutputStream> {
private final Encoder<byte[], String> delegate;
public ByteBase64UrlStreamEncoder(Encoder<byte[], String> delegate) {
this.delegate = Assert.notNull(delegate, "delegate cannot be null.");
}
@Override
public OutputStream encode(OutputStream outputStream) throws EncodingException {
Assert.notNull(outputStream, "outputStream cannot be null.");
return new TranslatingOutputStream(outputStream, delegate);
}
private static class TranslatingOutputStream extends FilteredOutputStream {
private final OutputStream dst;
private final Encoder<byte[], String> delegate;
public TranslatingOutputStream(OutputStream dst, Encoder<byte[], String> delegate) {
super(new ByteArrayOutputStream());
this.dst = dst;
this.delegate = delegate;
}
@Override
public void close() throws IOException {
byte[] data = ((ByteArrayOutputStream) out).toByteArray();
String s = delegate.encode(data);
dst.write(Strings.utf8(s));
dst.flush();
dst.close();
}
}
}

View File

@ -0,0 +1,37 @@
/*
* Copyright © 2023 jsonwebtoken.io
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package io.jsonwebtoken.impl.io;
import java.io.IOException;
import java.io.InputStream;
/**
* @since JJWT_RELEASE_VERSION, copied from
* <a href="https://github.com/apache/commons-io/blob/3a17f5259b105e734c8adce1d06d68f29884d1bb/src/main/java/org/apache/commons/io/input/ClosedInputStream.java">
* commons-io 3a17f5259b105e734c8adce1d06d68f29884d1bb</a>
*/
public final class ClosedInputStream extends InputStream {
public static final ClosedInputStream INSTANCE = new ClosedInputStream();
private ClosedInputStream() {
}
@Override
public int read() throws IOException {
return Streams.EOF;
}
}

View File

@ -23,15 +23,15 @@ import io.jsonwebtoken.io.Encoder;
import io.jsonwebtoken.io.Encoders;
import io.jsonwebtoken.lang.Assert;
public class Codec implements Converter<byte[], String> {
public class Codec implements Converter<byte[], CharSequence> {
public static final Codec BASE64 = new Codec(Encoders.BASE64, Decoders.BASE64);
public static final Codec BASE64URL = new Codec(Encoders.BASE64URL, Decoders.BASE64URL);
private final Encoder<byte[], String> encoder;
private final Decoder<String, byte[]> decoder;
private final Decoder<CharSequence, byte[]> decoder;
public Codec(Encoder<byte[], String> encoder, Decoder<String, byte[]> decoder) {
public Codec(Encoder<byte[], String> encoder, Decoder<CharSequence, byte[]> decoder) {
this.encoder = Assert.notNull(encoder, "Encoder cannot be null.");
this.decoder = Assert.notNull(decoder, "Decoder cannot be null.");
}
@ -42,7 +42,7 @@ public class Codec implements Converter<byte[], String> {
}
@Override
public byte[] applyFrom(String b) {
public byte[] applyFrom(CharSequence b) {
try {
return this.decoder.decode(b);
} catch (DecodingException e) {

View File

@ -0,0 +1,38 @@
/*
* Licensed to the Apache Software Foundation (ASF) under one or more
* contributor license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright ownership.
* The ASF licenses this file to You under the Apache License, Version 2.0
* (the "License"); you may not use this file except in compliance with
* the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package io.jsonwebtoken.impl.io;
/**
* Defines encoding and decoding policies.
*
* @since JJWT_RELEASE_VERSION, copied from
* <a href="https://github.com/apache/commons-codec/tree/585497f09b026f6602daf986723a554e051bdfe6">commons-codec
* 585497f09b026f6602daf986723a554e051bdfe6</a>
*/
enum CodecPolicy {
/**
* The strict policy. Data that causes a codec to fail should throw an exception.
*/
STRICT,
/**
* The lenient policy. Data that causes a codec to fail should not throw an exception.
*/
LENIENT
}

View File

@ -17,65 +17,33 @@ package io.jsonwebtoken.impl.io;
import io.jsonwebtoken.impl.lang.Converter;
import io.jsonwebtoken.impl.lang.Function;
import io.jsonwebtoken.io.DeserializationException;
import io.jsonwebtoken.io.Deserializer;
import io.jsonwebtoken.io.Parser;
import io.jsonwebtoken.lang.Assert;
import io.jsonwebtoken.lang.Strings;
import java.nio.charset.StandardCharsets;
import java.io.ByteArrayInputStream;
import java.io.InputStream;
import java.util.Map;
public class ConvertingParser<T> implements Parser<T> {
private final Deserializer<?> deserializer;
private final Function<InputStream, Map<String, ?>> deserializer;
private final Converter<T, Object> converter;
private final Function<Throwable, RuntimeException> exceptionHandler;
public ConvertingParser(Deserializer<Map<String, ?>> deserializer, Converter<T, Object> converter,
Function<Throwable, RuntimeException> exceptionHandler) {
this.deserializer = Assert.notNull(deserializer, "Deserializer cannot be null.");
this.converter = Assert.notNull(converter, "Converter canot be null.");
this.exceptionHandler = Assert.notNull(exceptionHandler, "exceptionHandler function cannot be null.");
}
private RuntimeException doThrow(Throwable t) {
DeserializationException e = t instanceof DeserializationException ? (DeserializationException) t :
new DeserializationException("Unable to deserialize JSON: " + t.getMessage(), t);
throw Assert.notNull(this.exceptionHandler.apply(e), "Exception handler cannot return null.");
}
private Map<String, ?> deserialize(String json) {
Assert.hasText(json, "JSON string cannot be null or empty.");
byte[] data = json.getBytes(StandardCharsets.UTF_8);
try {
return deserialize(data);
} catch (Throwable t) {
throw doThrow(t);
}
}
@SuppressWarnings("unchecked")
private Map<String, ?> deserialize(byte[] data) {
Object val = this.deserializer.deserialize(data);
if (val == null) {
String msg = "Deserialized data resulted in a null value; cannot create Map<String,?>";
throw new DeserializationException(msg);
}
if (!(val instanceof Map)) {
String msg = "Deserialized data is not a JSON Object; cannot create Map<String,?>";
throw new DeserializationException(msg);
}
// JSON Specification requires all JSON Objects to have string-only keys. So instead of
// checking that the val.keySet() has all Strings, we blindly cast to a Map<String,?>
// since input would rarely, if ever have non-string keys. Even if it did, the resulting
// ClassCastException would be caught by the calling deserialize(String) method above.
return (Map<String, ?>) val;
public ConvertingParser(Function<InputStream, Map<String, ?>> deserializer, Converter<T, Object> converter) {
this.deserializer = Assert.notNull(deserializer, "Deserializer function cannot be null.");
this.converter = Assert.notNull(converter, "Converter cannot be null.");
}
@Override
public final T parse(String input) {
Map<String, ?> m = deserialize(input);
Assert.hasText(input, "Parse input String cannot be null or empty.");
InputStream in = new ByteArrayInputStream(Strings.utf8(input));
return parse(in);
}
public final T parse(InputStream in) {
Map<String, ?> m = this.deserializer.apply(in);
return this.converter.applyFrom(m);
}
}

View File

@ -0,0 +1,67 @@
/*
* Copyright © 2023 jsonwebtoken.io
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package io.jsonwebtoken.impl.io;
import java.io.FilterInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.util.concurrent.atomic.AtomicLong;
public class CountingInputStream extends FilterInputStream {
private final AtomicLong count = new AtomicLong(0);
public CountingInputStream(InputStream in) {
super(in);
}
public long getCount() {
return count.get();
}
private void add(long n) {
// n can be -1 for EOF, and 0 for no bytes read, so we only add if we actually read 1 or more bytes:
if (n > 0) count.addAndGet(n);
}
@Override
public int read() throws IOException {
int next = super.read();
add(next == Streams.EOF ? Streams.EOF : 1);
return next;
}
@Override
public int read(byte[] b) throws IOException {
int n = super.read(b);
add(n);
return n;
}
@Override
public int read(byte[] b, int off, int len) throws IOException {
int n = super.read(b, off, len);
add(n);
return n;
}
@Override
public long skip(long n) throws IOException {
final long skipped = super.skip(n);
add(skipped);
return skipped;
}
}

View File

@ -0,0 +1,39 @@
/*
* Copyright © 2023 jsonwebtoken.io
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package io.jsonwebtoken.impl.io;
import io.jsonwebtoken.io.DecodingException;
import io.jsonwebtoken.lang.Assert;
import java.io.InputStream;
public class DecodingInputStream extends FilteredInputStream {
private final String codecName;
private final String name;
public DecodingInputStream(InputStream in, String codecName, String name) {
super(in);
this.codecName = Assert.hasText(codecName, "codecName cannot be null or empty.");
this.name = Assert.hasText(name, "Name cannot be null or empty.");
}
@Override
protected void onThrowable(Throwable t) {
String msg = "Unable to " + this.codecName + "-decode " + this.name + ": " + t.getMessage();
throw new DecodingException(msg, t);
}
}

View File

@ -0,0 +1,47 @@
/*
* Copyright © 2023 jsonwebtoken.io
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package io.jsonwebtoken.impl.io;
import io.jsonwebtoken.io.Decoder;
import io.jsonwebtoken.io.DecodingException;
import io.jsonwebtoken.lang.Assert;
import io.jsonwebtoken.lang.Strings;
import java.io.ByteArrayInputStream;
import java.io.InputStream;
@SuppressWarnings("DeprecatedIsStillUsed")
@Deprecated //TODO: delete when deleting JwtParserBuilder#base64UrlDecodeWith
public class DelegateStringDecoder implements Decoder<InputStream, InputStream> {
private final Decoder<CharSequence, byte[]> delegate;
public DelegateStringDecoder(Decoder<CharSequence, byte[]> delegate) {
this.delegate = Assert.notNull(delegate, "delegate cannot be null.");
}
@Override
public InputStream decode(InputStream in) throws DecodingException {
try {
byte[] data = Streams.bytes(in, "Unable to Base64URL-decode input.");
data = delegate.decode(Strings.utf8(data));
return new ByteArrayInputStream(data);
} catch (Throwable t) {
String msg = "Unable to Base64Url-decode InputStream: " + t.getMessage();
throw new DecodingException(msg, t);
}
}
}

View File

@ -0,0 +1,39 @@
/*
* Copyright © 2023 jsonwebtoken.io
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package io.jsonwebtoken.impl.io;
import io.jsonwebtoken.io.EncodingException;
import io.jsonwebtoken.lang.Assert;
import java.io.OutputStream;
public class EncodingOutputStream extends FilteredOutputStream {
private final String codecName;
private final String name;
public EncodingOutputStream(OutputStream out, String codecName, String name) {
super(out);
this.codecName = Assert.hasText(codecName, "codecName cannot be null or empty.");
this.name = Assert.hasText(name, "name cannot be null or empty.");
}
@Override
protected void onThrowable(Throwable t) {
String msg = "Unable to " + this.codecName + "-encode " + this.name + ": " + t.getMessage();
throw new EncodingException(msg, t);
}
}

View File

@ -0,0 +1,251 @@
/*
* Copyright © 2023 jsonwebtoken.io
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package io.jsonwebtoken.impl.io;
import io.jsonwebtoken.impl.lang.Bytes;
import java.io.FilterInputStream;
import java.io.IOException;
import java.io.InputStream;
/**
* A filter stream that delegates its calls to an internal delegate stream without changing behavior, but also providing
* pre/post/error handling hooks. It is useful as a base for extending and adding custom functionality.
*
* <p>It is an alternative base class to FilterInputStream to increase re-usability, because FilterInputStream changes
* the methods being called, such as read(byte[]) to read(byte[], int, int).</p>
*
* @since JJWT_RELEASE_VERSION, copied from
* <a href="https://github.com/apache/commons-io/blob/3a17f5259b105e734c8adce1d06d68f29884d1bb/src/main/java/org/apache/commons/io/input/ProxyInputStream.java">
* commons-io 3a17f5259b105e734c8adce1d06d68f29884d1bb</a>
*/
public abstract class FilteredInputStream extends FilterInputStream {
/**
* Constructs a new FilteredInputStream that delegates to the specified {@link InputStream}.
*
* @param in the InputStream to delegate to
*/
public FilteredInputStream(final InputStream in) {
super(in); // the delegate is stored in a protected superclass variable named 'in'
}
/**
* Invoked by the read methods after the proxied call has returned
* successfully. The number of bytes returned to the caller (or -1 if
* the end of stream was reached) is given as an argument.
* <p>
* Subclasses can override this method to add common post-processing
* functionality without having to override all the read methods.
* The default implementation does nothing.
* </p>
* <p>
* Note this method is <em>not</em> called from {@link #skip(long)} or
* {@link #reset()}. You need to explicitly override those methods if
* you want to add post-processing steps also to them.
* </p>
*
* @param n number of bytes read, or -1 if the end of stream was reached
* @throws IOException if the post-processing fails
* @since 2.0
*/
@SuppressWarnings({"unused", "RedundantThrows"}) // Possibly thrown from subclasses.
protected void afterRead(final int n) throws IOException {
// no-op
}
/**
* Invokes the delegate's {@code available()} method.
*
* @return the number of available bytes
* @throws IOException if an I/O error occurs.
*/
@Override
public int available() throws IOException {
try {
return super.available();
} catch (final Throwable t) {
onThrowable(t);
return 0;
}
}
/**
* Invoked by the read methods before the call is proxied. The number
* of bytes that the caller wanted to read (1 for the {@link #read()}
* method, buffer length for {@link #read(byte[])}, etc.) is given as
* an argument.
* <p>
* Subclasses can override this method to add common pre-processing
* functionality without having to override all the read methods.
* The default implementation does nothing.
* </p>
* <p>
* Note this method is <em>not</em> called from {@link #skip(long)} or
* {@link #reset()}. You need to explicitly override those methods if
* you want to add pre-processing steps also to them.
* </p>
*
* @param n number of bytes that the caller asked to be read
* @throws IOException if the pre-processing fails
* @since 2.0
*/
@SuppressWarnings({"unused", "RedundantThrows"}) // Possibly thrown from subclasses.
protected void beforeRead(final int n) throws IOException {
// no-op
}
/**
* Invokes the delegate's {@code close()} method.
*
* @throws IOException if an I/O error occurs.
*/
@Override
public void close() throws IOException {
try {
super.close();
} catch (Throwable t) {
onThrowable(t);
}
}
/**
* Handle any Throwable thrown; by default, throws the given exception.
* <p>
* This method provides a point to implement custom exception
* handling. The default behavior is to re-throw the exception.
* </p>
*
* @param t The IOException thrown
* @throws IOException if an I/O error occurs.
*/
protected void onThrowable(final Throwable t) throws IOException {
if (t instanceof IOException) throw (IOException) t;
throw new IOException("IO Exception: " + t.getMessage(), t);
}
/**
* Invokes the delegate's {@code mark(int)} method.
*
* @param readlimit read ahead limit
*/
@Override
public synchronized void mark(final int readlimit) {
in.mark(readlimit);
}
/**
* Invokes the delegate's {@code markSupported()} method.
*
* @return true if mark is supported, otherwise false
*/
@Override
public boolean markSupported() {
return in.markSupported();
}
/**
* Invokes the delegate's {@code read()} method.
*
* @return the byte read or -1 if the end of stream
* @throws IOException if an I/O error occurs.
*/
@Override
public int read() throws IOException {
try {
beforeRead(1);
final int b = in.read();
afterRead(b != Streams.EOF ? 1 : Streams.EOF);
return b;
} catch (final Throwable t) {
onThrowable(t);
return Streams.EOF;
}
}
/**
* Invokes the delegate's {@code read(byte[])} method.
*
* @param bts the buffer to read the bytes into
* @return the number of bytes read or EOF if the end of stream
* @throws IOException if an I/O error occurs.
*/
@Override
public int read(final byte[] bts) throws IOException {
try {
beforeRead(Bytes.length(bts));
final int n = in.read(bts);
afterRead(n);
return n;
} catch (final Throwable t) {
onThrowable(t);
return Streams.EOF;
}
}
/**
* Invokes the delegate's {@code read(byte[], int, int)} method.
*
* @param bts the buffer to read the bytes into
* @param off The start offset
* @param len The number of bytes to read
* @return the number of bytes read or -1 if the end of stream
* @throws IOException if an I/O error occurs.
*/
@Override
public int read(final byte[] bts, final int off, final int len) throws IOException {
try {
beforeRead(len);
final int n = in.read(bts, off, len);
afterRead(n);
return n;
} catch (final Throwable t) {
onThrowable(t);
return Streams.EOF;
}
}
/**
* Invokes the delegate's {@code reset()} method.
*
* @throws IOException if an I/O error occurs.
*/
@Override
public synchronized void reset() throws IOException {
try {
in.reset();
} catch (final Throwable t) {
onThrowable(t);
}
}
/**
* Invokes the delegate's {@code skip(long)} method.
*
* @param ln the number of bytes to skip
* @return the actual number of bytes skipped
* @throws IOException if an I/O error occurs.
*/
@Override
public long skip(final long ln) throws IOException {
try {
return in.skip(ln);
} catch (final Throwable t) {
onThrowable(t);
return 0;
}
}
}

View File

@ -0,0 +1,181 @@
/*
* Copyright © 2023 jsonwebtoken.io
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package io.jsonwebtoken.impl.io;
import io.jsonwebtoken.impl.lang.Bytes;
import java.io.FilterOutputStream;
import java.io.IOException;
import java.io.OutputStream;
/**
* A Proxy stream which acts as expected, that is it passes the method
* calls on to the proxied stream and doesn't change which methods are
* being called. It is an alternative base class to FilterOutputStream
* to increase reusability.
* <p>
* See the protected methods for ways in which a subclass can easily decorate
* a stream with custom pre-, post- or error processing functionality.
* </p>
*
* @since JJWT_RELEASE_VERSION, copied from
* <a href="https://github.com/apache/commons-io/blob/3a17f5259b105e734c8adce1d06d68f29884d1bb/src/main/java/org/apache/commons/io/output/ProxyOutputStream.java">
* commons-io 3a17f5259b105e734c8adce1d06d68f29884d1bb</a>
*/
public class FilteredOutputStream extends FilterOutputStream {
/**
* Constructs a new ProxyOutputStream.
*
* @param out the OutputStream to delegate to
*/
public FilteredOutputStream(final OutputStream out) {
super(out); // the proxy is stored in a protected superclass variable named 'out'
}
/**
* Invoked by the write methods after the proxied call has returned
* successfully. The number of bytes written (1 for the
* {@link #write(int)} method, buffer length for {@link #write(byte[])},
* etc.) is given as an argument.
* <p>
* Subclasses can override this method to add common post-processing
* functionality without having to override all the write methods.
* The default implementation does nothing.
*
* @param n number of bytes written
* @throws IOException if the post-processing fails
* @since 2.0
*/
@SuppressWarnings({"unused", "RedundantThrows"}) // Possibly thrown from subclasses.
protected void afterWrite(final int n) throws IOException {
// noop
}
/**
* Invoked by the write methods before the call is proxied. The number
* of bytes to be written (1 for the {@link #write(int)} method, buffer
* length for {@link #write(byte[])}, etc.) is given as an argument.
* <p>
* Subclasses can override this method to add common pre-processing
* functionality without having to override all the write methods.
* The default implementation does nothing.
*
* @param n number of bytes to be written
* @throws IOException if the pre-processing fails
*/
@SuppressWarnings({"unused", "RedundantThrows"}) // Possibly thrown from subclasses.
protected void beforeWrite(final int n) throws IOException {
// noop
}
/**
* Invokes the delegate's {@code close()} method.
*
* @throws IOException if an I/O error occurs.
*/
@Override
public void close() throws IOException {
try {
super.close();
} catch (Throwable t) {
onThrowable(t);
}
}
/**
* Invokes the delegate's {@code flush()} method.
*
* @throws IOException if an I/O error occurs.
*/
@Override
public void flush() throws IOException {
try {
out.flush();
} catch (final Throwable t) {
onThrowable(t);
}
}
/**
* Handle any IOExceptions thrown.
* <p>
* This method provides a point to implement custom exception
* handling. The default behavior is to re-throw the exception.
*
* @param t The Throwable thrown
* @throws IOException if an I/O error occurs.
*/
protected void onThrowable(final Throwable t) throws IOException {
if (t instanceof IOException) throw (IOException) t;
throw new IOException("IO Exception " + t.getMessage(), t);
}
/**
* Invokes the delegate's {@code write(byte[])} method.
*
* @param bts the bytes to write
* @throws IOException if an I/O error occurs.
*/
@Override
public void write(final byte[] bts) throws IOException {
try {
final int len = Bytes.length(bts);
beforeWrite(len);
out.write(bts);
afterWrite(len);
} catch (final Throwable t) {
onThrowable(t);
}
}
/**
* Invokes the delegate's {@code write(byte[])} method.
*
* @param bts the bytes to write
* @param st The start offset
* @param end The number of bytes to write
* @throws IOException if an I/O error occurs.
*/
@Override
public void write(final byte[] bts, final int st, final int end) throws IOException {
try {
beforeWrite(end);
out.write(bts, st, end);
afterWrite(end);
} catch (final Throwable t) {
onThrowable(t);
}
}
/**
* Invokes the delegate's {@code write(int)} method.
*
* @param idx the byte to write
* @throws IOException if an I/O error occurs.
*/
@Override
public void write(final int idx) throws IOException {
try {
beforeWrite(1);
out.write(idx);
afterWrite(1);
} catch (final Throwable t) {
onThrowable(t);
}
}
}

View File

@ -0,0 +1,78 @@
/*
* Copyright (C) 2021 jsonwebtoken.io
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package io.jsonwebtoken.impl.io;
import io.jsonwebtoken.MalformedJwtException;
import io.jsonwebtoken.impl.lang.Function;
import io.jsonwebtoken.io.DeserializationException;
import io.jsonwebtoken.io.Deserializer;
import io.jsonwebtoken.lang.Assert;
import java.io.InputStream;
import java.util.Map;
/**
* Function that wraps a {@link Deserializer} to add JWT-related error handling.
*
* @since 0.11.3 (renamed from JwtDeserializer)
*/
public class JsonObjectDeserializer implements Function<InputStream, Map<String, ?>> {
private static final String MALFORMED_ERROR = "Malformed %s JSON: %s";
private static final String MALFORMED_COMPLEX_ERROR = "Malformed or excessively complex %s JSON. " +
"If experienced in a production environment, this could reflect a potential malicious %s, please " +
"investigate the source further. Cause: %s";
private final Deserializer<?> deserializer;
private final String name;
public JsonObjectDeserializer(Deserializer<?> deserializer, String name) {
this.deserializer = Assert.notNull(deserializer, "JSON Deserializer cannot be null.");
this.name = Assert.hasText(name, "name cannot be null or empty.");
}
@Override
public Map<String, ?> apply(InputStream in) {
Assert.notNull(in, "InputStream argument cannot be null.");
Object value;
try {
value = this.deserializer.deserialize(in);
if (value == null) {
String msg = "Deserialized data resulted in a null value; cannot create Map<String,?>";
throw new DeserializationException(msg);
}
if (!(value instanceof Map)) {
String msg = "Deserialized data is not a JSON Object; cannot create Map<String,?>";
throw new DeserializationException(msg);
}
// JSON Specification requires all JSON Objects to have string-only keys. So instead of
// checking that the val.keySet() has all Strings, we blindly cast to a Map<String,?>
// since input would rarely, if ever, have non-string keys.
//noinspection unchecked
return (Map<String, ?>) value;
} catch (StackOverflowError e) {
String msg = String.format(MALFORMED_COMPLEX_ERROR, this.name, this.name, e.getMessage());
throw new DeserializationException(msg, e);
} catch (Throwable t) {
throw malformed(t);
}
}
protected RuntimeException malformed(Throwable t) {
String msg = String.format(MALFORMED_ERROR, this.name, t.getMessage());
throw new MalformedJwtException(msg, t);
}
}

View File

@ -0,0 +1,45 @@
/*
* Copyright © 2023 jsonwebtoken.io
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package io.jsonwebtoken.impl.io;
import io.jsonwebtoken.io.AbstractSerializer;
import io.jsonwebtoken.io.SerializationException;
import io.jsonwebtoken.io.Serializer;
import io.jsonwebtoken.lang.Assert;
import java.io.OutputStream;
import java.util.Map;
public class NamedSerializer extends AbstractSerializer<Map<String, ?>> {
private final String name;
private final Serializer<Map<String, ?>> DELEGATE;
public NamedSerializer(String name, Serializer<Map<String, ?>> serializer) {
this.DELEGATE = Assert.notNull(serializer, "JSON Serializer cannot be null.");
this.name = Assert.hasText(name, "Name cannot be null or empty.");
}
@Override
protected void doSerialize(Map<String, ?> m, OutputStream out) throws SerializationException {
try {
this.DELEGATE.serialize(m, out);
} catch (Throwable t) {
String msg = String.format("Cannot serialize %s to JSON. Cause: %s", this.name, t.getMessage());
throw new SerializationException(msg, t);
}
}
}

View File

@ -0,0 +1,160 @@
/*
* Copyright © 2023 jsonwebtoken.io
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package io.jsonwebtoken.impl.io;
import io.jsonwebtoken.impl.lang.Bytes;
import io.jsonwebtoken.lang.Assert;
import io.jsonwebtoken.lang.Classes;
import io.jsonwebtoken.lang.Objects;
import io.jsonwebtoken.lang.Strings;
import java.io.BufferedInputStream;
import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.Flushable;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.util.concurrent.Callable;
/**
* @since JJWT_RELEASE_VERSION
*/
public class Streams {
/**
* Represents the end-of-file (or stream).
*/
public static final int EOF = -1;
public static byte[] bytes(final InputStream in, String exmsg) {
if (in instanceof ByteArrayInputStream) {
return Classes.getFieldValue(in, "buf", byte[].class);
}
ByteArrayOutputStream out = new ByteArrayOutputStream(8192);
copy(in, out, new byte[8192], exmsg);
return out.toByteArray();
}
public static ByteArrayInputStream of(byte[] bytes) {
return Bytes.isEmpty(bytes) ? new ByteArrayInputStream(Bytes.EMPTY) : new ByteArrayInputStream(bytes);
}
public static ByteArrayInputStream of(CharSequence seq) {
return of(Strings.utf8(seq));
}
public static void flush(Flushable... flushables) {
Objects.nullSafeFlush(flushables);
}
/**
* Copies bytes from a {@link InputStream} to an {@link OutputStream} using the specified {@code buffer}, avoiding
* the need for a {@link BufferedInputStream}.
*
* @param inputStream the {@link InputStream} to read.
* @param outputStream the {@link OutputStream} to write.
* @param buffer the buffer to use for the copy
* @return the number of bytes copied.
* @throws IllegalArgumentException if the InputStream is {@code null}.
* @throws IllegalArgumentException if the OutputStream is {@code null}.
* @throws IOException if an I/O error occurs.
*/
public static long copy(final InputStream inputStream, final OutputStream outputStream, final byte[] buffer)
throws IOException {
Assert.notNull(inputStream, "inputStream cannot be null.");
Assert.notNull(outputStream, "outputStream cannot be null.");
Assert.notEmpty(buffer, "buffer cannot be null or empty.");
long count = 0;
int n = 0;
while (n != EOF) {
n = inputStream.read(buffer);
if (n > 0) outputStream.write(buffer, 0, n);
count += n;
}
return count;
}
public static long copy(final InputStream in, final OutputStream out, final byte[] buffer, final String exmsg) {
return run(new Callable<Long>() {
@Override
public Long call() throws IOException {
try {
reset(in);
return copy(in, out, buffer);
} finally {
Objects.nullSafeFlush(out);
reset(in);
}
}
}, exmsg);
}
public static void reset(final InputStream in) {
if (in == null) return;
Callable<Object> callable = new Callable<Object>() {
@Override
public Object call() {
try {
in.reset();
} catch (Throwable ignored) {
}
return null;
}
};
try {
callable.call();
} catch (Throwable ignored) {
}
}
public static void write(final OutputStream out, final byte[] bytes, String exMsg) {
write(out, bytes, 0, Bytes.length(bytes), exMsg);
}
public static void write(final OutputStream out, final byte[] data, final int offset, final int len, String exMsg) {
if (out == null || Bytes.isEmpty(data) || len <= 0) return;
run(new Callable<Object>() {
@Override
public Object call() throws Exception {
out.write(data, offset, len);
return null;
}
}, exMsg);
}
public static void writeAndClose(final OutputStream out, final byte[] data, String exMsg) {
try {
write(out, data, exMsg);
} finally {
Objects.nullSafeClose(out);
}
}
public static <V> V run(Callable<V> c, String ioExMsg) {
Assert.hasText(ioExMsg, "IO Exception Message cannot be null or empty.");
try {
return c.call();
} catch (Throwable t) {
String msg = "IO failure: " + ioExMsg;
if (!msg.endsWith(".")) {
msg += ".";
}
msg += " Cause: " + t.getMessage();
throw new io.jsonwebtoken.io.IOException(msg, t);
}
}
}

View File

@ -0,0 +1,67 @@
/*
* Copyright © 2023 jsonwebtoken.io
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package io.jsonwebtoken.impl.io;
import io.jsonwebtoken.lang.Assert;
import java.io.IOException;
import java.io.OutputStream;
/**
* @since JJWT_RELEASE_VERSION
*/
public class TeeOutputStream extends FilteredOutputStream {
private final OutputStream other;
public TeeOutputStream(OutputStream one, OutputStream two) {
super(one);
this.other = Assert.notNull(two, "Second OutputStream cannot be null.");
}
@Override
public void close() throws IOException {
try {
super.close();
} finally {
this.other.close();
}
}
@Override
public void flush() throws IOException {
super.flush();
this.other.flush();
}
@Override
public void write(byte[] bts) throws IOException {
super.write(bts);
this.other.write(bts);
}
@Override
public void write(byte[] bts, int st, int end) throws IOException {
super.write(bts, st, end);
this.other.write(bts, st, end);
}
@Override
public void write(int idx) throws IOException {
super.write(idx);
this.other.write(idx);
}
}

View File

@ -0,0 +1,36 @@
/*
* Copyright © 2023 jsonwebtoken.io
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package io.jsonwebtoken.impl.io;
import java.io.FilterInputStream;
import java.io.InputStream;
/**
* @since JJWT_RELEASE_VERSION, copied from
* <a href="https://github.com/apache/commons-io/blob/3a17f5259b105e734c8adce1d06d68f29884d1bb/src/main/java/org/apache/commons/io/input/CloseShieldInputStream.java">
* commons-io 3a17f5259b105e734c8adce1d06d68f29884d1bb</a>
*/
public final class UncloseableInputStream extends FilterInputStream {
public UncloseableInputStream(InputStream in) {
super(in);
}
@Override
public void close() {
in = ClosedInputStream.INSTANCE;
}
}

View File

@ -0,0 +1,21 @@
/*
* Copyright © 2023 jsonwebtoken.io
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package io.jsonwebtoken.impl.lang;
public interface BiConsumer<T, U> {
void accept(T t, U u);
}

View File

@ -31,11 +31,11 @@ public final class Converters {
public static final Converter<byte[], Object> BASE64URL_BYTES = Converters.forEncoded(byte[].class, Codec.BASE64URL);
public static final Converter<X509Certificate, Object> X509_CERTIFICATE =
Converters.forEncoded(X509Certificate.class, JwtX509StringConverter.INSTANCE);
Converters.forEncoded(X509Certificate.class, JwtX509StringConverter.INSTANCE);
public static final Converter<BigInteger, byte[]> BIGINT_UBYTES = new BigIntegerUBytesConverter();
public static final Converter<BigInteger, Object> BIGINT = Converters.forEncoded(BigInteger.class,
compound(BIGINT_UBYTES, Codec.BASE64URL));
compound(BIGINT_UBYTES, Codec.BASE64URL));
//prevent instantiation
private Converters() {
@ -53,7 +53,7 @@ public final class Converters {
return CollectionConverter.forList(elementConverter);
}
public static <T> Converter<T, Object> forEncoded(Class<T> elementType, Converter<T, String> elementConverter) {
public static <T> Converter<T, Object> forEncoded(Class<T> elementType, Converter<T, CharSequence> elementConverter) {
return new EncodedObjectConverter<>(elementType, elementConverter);
}

View File

@ -20,9 +20,9 @@ import io.jsonwebtoken.lang.Assert;
public class EncodedObjectConverter<T> implements Converter<T, Object> {
private final Class<T> type;
private final Converter<T, String> converter;
private final Converter<T, ? super CharSequence> converter;
public EncodedObjectConverter(Class<T> type, Converter<T, String> converter) {
public EncodedObjectConverter(Class<T> type, Converter<T, ? super CharSequence> converter) {
this.type = Assert.notNull(type, "Value type cannot be null.");
this.converter = Assert.notNull(converter, "Value converter cannot be null.");
}
@ -38,11 +38,11 @@ public class EncodedObjectConverter<T> implements Converter<T, Object> {
Assert.notNull(value, "Value argument cannot be null.");
if (type.isInstance(value)) {
return type.cast(value);
} else if (value instanceof String) {
return converter.applyFrom((String) value);
} else if (value instanceof CharSequence) {
return converter.applyFrom((CharSequence) value);
} else {
String msg = "Values must be either String or " + type.getName() +
" instances. Value type found: " + value.getClass().getName() + ".";
" instances. Value type found: " + value.getClass().getName() + ".";
throw new IllegalArgumentException(msg);
}
}

View File

@ -32,6 +32,10 @@ public class PropagatingExceptionFunction<T, R, E extends RuntimeException> impl
this(new DelegatingCheckedFunction<>(f), exceptionClass, new ConstantFunction<T, String>(msg));
}
public PropagatingExceptionFunction(CheckedFunction<T, R> f, Class<E> exceptionClass, final String msg) {
this(f, exceptionClass, new ConstantFunction<T, String>(msg));
}
public PropagatingExceptionFunction(CheckedFunction<T, R> fn, Class<E> exceptionClass, final Supplier<String> msgSupplier) {
this(fn, exceptionClass, new Function<T, String>() {
@Override

View File

@ -19,7 +19,7 @@ import io.jsonwebtoken.lang.Assert;
import java.net.URI;
public class UriStringConverter implements Converter<URI, String> {
public class UriStringConverter implements Converter<URI, CharSequence> {
@Override
public String applyTo(URI uri) {
@ -28,10 +28,10 @@ public class UriStringConverter implements Converter<URI, String> {
}
@Override
public URI applyFrom(String s) {
public URI applyFrom(CharSequence s) {
Assert.hasText(s, "URI string cannot be null or empty.");
try {
return URI.create(s);
return URI.create(s.toString());
} catch (Exception e) {
String msg = "Unable to convert String value '" + s + "' to URI instance: " + e.getMessage();
throw new IllegalArgumentException(msg, e);

View File

@ -31,6 +31,8 @@ import io.jsonwebtoken.security.JwkThumbprint;
import io.jsonwebtoken.security.Jwks;
import io.jsonwebtoken.security.KeyOperation;
import java.io.ByteArrayInputStream;
import java.io.InputStream;
import java.nio.charset.StandardCharsets;
import java.security.Key;
import java.security.PrivateKey;
@ -150,7 +152,8 @@ public abstract class AbstractJwk<K extends Key> implements Jwk<K>, ParameterRea
String json = toThumbprintJson();
Assert.hasText(json, "Canonical JWK Thumbprint JSON cannot be null or empty.");
byte[] bytes = json.getBytes(StandardCharsets.UTF_8); // https://www.rfc-editor.org/rfc/rfc7638#section-3 #2
byte[] digest = alg.digest(new DefaultRequest<>(bytes, this.context.getProvider(), this.context.getRandom()));
InputStream in = new ByteArrayInputStream(bytes);
byte[] digest = alg.digest(new DefaultRequest<>(in, this.context.getProvider(), this.context.getRandom()));
return new DefaultJwkThumbprint(digest, alg);
}

View File

@ -23,6 +23,7 @@ import io.jsonwebtoken.security.SecurityException;
import io.jsonwebtoken.security.SignatureException;
import io.jsonwebtoken.security.VerifySecureDigestRequest;
import java.io.InputStream;
import java.security.Key;
abstract class AbstractSecureDigestAlgorithm<S extends Key, V extends Key> extends CryptoAlgorithm implements SecureDigestAlgorithm<S, V> {
@ -38,10 +39,10 @@ abstract class AbstractSecureDigestAlgorithm<S extends Key, V extends Key> exten
protected abstract void validateKey(Key key, boolean signing);
@Override
public final byte[] digest(SecureRequest<byte[], S> request) throws SecurityException {
public final byte[] digest(SecureRequest<InputStream, S> request) throws SecurityException {
Assert.notNull(request, "Request cannot be null.");
final S key = Assert.notNull(request.getKey(), "Signing key cannot be null.");
Assert.notEmpty(request.getPayload(), "Request content cannot be null or empty.");
Assert.notNull(request.getPayload(), "Request content cannot be null.");
try {
validateKey(key, true);
return doDigest(request);
@ -54,13 +55,13 @@ abstract class AbstractSecureDigestAlgorithm<S extends Key, V extends Key> exten
}
}
protected abstract byte[] doDigest(SecureRequest<byte[], S> request) throws Exception;
protected abstract byte[] doDigest(SecureRequest<InputStream, S> request) throws Exception;
@Override
public final boolean verify(VerifySecureDigestRequest<V> request) throws SecurityException {
Assert.notNull(request, "Request cannot be null.");
final V key = Assert.notNull(request.getKey(), "Verification key cannot be null.");
Assert.notEmpty(request.getPayload(), "Request content cannot be null or empty.");
Assert.notNull(request.getPayload(), "Request content cannot be null or empty.");
Assert.notEmpty(request.getDigest(), "Request signature byte array cannot be null or empty.");
try {
validateKey(key, false);

Some files were not shown because too many files have changed in this diff Show More