mirror of https://github.com/jwtk/jjwt.git
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:
parent
7fcd652aea
commit
b687ca5c72
36
NOTICE.md
36
NOTICE.md
|
@ -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
|
ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY
|
||||||
OF SUCH DAMAGE.
|
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/).
|
||||||
|
```
|
|
@ -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
|
* Convenient and readable [fluent](http://en.wikipedia.org/wiki/Fluent_interface) interfaces, great for IDE
|
||||||
auto-completion to write code quickly
|
auto-completion to write code quickly
|
||||||
* Fully RFC specification compliant on all implemented functionality, tested against RFC-specified test vectors
|
* 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.
|
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:
|
* Creating, parsing and verifying digitally signed compact JWTs (aka JWSs) with all standard JWS algorithms:
|
||||||
|
|
||||||
|
|
|
@ -144,14 +144,14 @@ public interface Claims extends Map<String, Object>, Identifiable {
|
||||||
String getId();
|
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
|
* <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
|
* complex is expected to be already converted to your desired type by the JSON parser. You may specify a custom
|
||||||
* {@link io.jsonwebtoken.io.Deserializer Deserializer} implementation. You may specify a custom Deserializer for a
|
* JSON processor using the {@code JwtParserBuilder}'s
|
||||||
* JwtParser with the desired conversion configuration via the
|
* {@link JwtParserBuilder#json(io.jsonwebtoken.io.Deserializer) json(Deserializer)} method. See the JJWT
|
||||||
* {@link JwtParserBuilder#deserializer deserializer} method.
|
* documentation on <a href="https://github.com/jwtk/jjwt#custom-json-processor">custom JSON processor</a>s for more
|
||||||
* See <a href="https://github.com/jwtk/jjwt#custom-json-processor">custom JSON processor</a> for more
|
|
||||||
* information. If using Jackson, you can specify custom claim POJO types as described in
|
* 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>.
|
* <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
|
* @param <T> the type of the value expected to be returned
|
||||||
* @return the JWT {@code claimName} value or {@code null} if not present.
|
* @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}
|
* @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);
|
<T> T get(String claimName, Class<T> requiredType);
|
||||||
}
|
}
|
||||||
|
|
|
@ -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">
|
* 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.
|
* @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.
|
* @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">
|
* 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.
|
* @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.
|
* @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">
|
* 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.
|
* @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.
|
* @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">
|
* 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.
|
* @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.
|
* @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)
|
* 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
|
* 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
|
* 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
|
* {@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">
|
* 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>
|
* <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">
|
* 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>
|
* <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">
|
* 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>
|
* <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">
|
* 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>
|
* <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">
|
* 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>
|
* <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">
|
* 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>
|
* <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">
|
* 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
|
* <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
|
* 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">
|
* 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
|
* <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
|
* manner that ensures that there is a negligible probability that the same value will be accidentally
|
||||||
|
|
|
@ -46,4 +46,25 @@ public interface CompressionCodec extends CompressionAlgorithm {
|
||||||
@SuppressWarnings("DeprecatedIsStillUsed")
|
@SuppressWarnings("DeprecatedIsStillUsed")
|
||||||
@Deprecated
|
@Deprecated
|
||||||
String getAlgorithmName();
|
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;
|
||||||
}
|
}
|
|
@ -31,6 +31,8 @@ import io.jsonwebtoken.security.WeakKeyException;
|
||||||
import io.jsonwebtoken.security.X509Builder;
|
import io.jsonwebtoken.security.X509Builder;
|
||||||
|
|
||||||
import javax.crypto.SecretKey;
|
import javax.crypto.SecretKey;
|
||||||
|
import java.io.InputStream;
|
||||||
|
import java.io.OutputStream;
|
||||||
import java.security.Key;
|
import java.security.Key;
|
||||||
import java.security.PrivateKey;
|
import java.security.PrivateKey;
|
||||||
import java.security.Provider;
|
import java.security.Provider;
|
||||||
|
@ -149,25 +151,13 @@ public interface JwtBuilder extends ClaimsMutator<JwtBuilder> {
|
||||||
JwtBuilder setHeaderParam(String name, Object value);
|
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
|
* Since JJWT JJWT_RELEASE_VERSION, this is an alias for {@link #content(String)}. This method will be removed
|
||||||
* {@link BuilderHeader#contentType(String) contentType} header value so the JWT recipient may inspect that value to
|
* before the 1.0 release.
|
||||||
* 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>
|
|
||||||
*
|
*
|
||||||
* @param payload the string used to set UTF-8-encoded bytes as the JWT payload.
|
* @param payload the string used to set UTF-8-encoded bytes as the JWT payload.
|
||||||
* @return the builder for method chaining.
|
* @return the builder for method chaining.
|
||||||
* @see #content(byte[])
|
* @see #content(String)
|
||||||
* @see #content(byte[], String)
|
* @deprecated since JJWT_RELEASE VERSION in favor of {@link #content(String)}
|
||||||
* @deprecated since JJWT_RELEASE VERSION in favor of {@link #content(byte[])} or {@link #content(byte[], String)}
|
|
||||||
* because both Claims and Content are technically 'payloads', so this method name is misleading. This method will
|
* because both Claims and Content are technically 'payloads', so this method name is misleading. This method will
|
||||||
* be removed before the 1.0 release.
|
* be removed before the 1.0 release.
|
||||||
*/
|
*/
|
||||||
|
@ -176,14 +166,18 @@ public interface JwtBuilder extends ClaimsMutator<JwtBuilder> {
|
||||||
JwtBuilder setPayload(String payload);
|
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><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
|
* <p>Unless you are confident that the JWT recipient will <em>always</em> know to convert the payload bytes
|
||||||
* without additional metadata, it is strongly recommended to use the {@link #content(byte[], String)} method
|
* to a UTF-8 string without additional metadata, it is strongly recommended to use the
|
||||||
* instead of this one. That method ensures that a JWT recipient can inspect the {@code cty} header to know
|
* {@link #content(String, String)} method instead of this one. That method ensures that a JWT recipient can
|
||||||
* how to handle the content without ambiguity.</p>
|
* 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><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
|
* @param content the content string to use for the JWT payload
|
||||||
* @return the builder for method chaining.
|
* @return the builder for method chaining.
|
||||||
|
* @see #content(String, String)
|
||||||
* @see #content(byte[], String)
|
* @see #content(byte[], String)
|
||||||
|
* @see #content(InputStream, String)
|
||||||
* @since JJWT_RELEASE_VERSION
|
* @since JJWT_RELEASE_VERSION
|
||||||
*/
|
*/
|
||||||
JwtBuilder content(String content);
|
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><b>Content Type Recommendation</b></p>
|
||||||
*
|
*
|
||||||
* <p>Unless you are confident that the JWT recipient will <em>always</em> know how to use
|
* <p>Unless you are confident that the JWT recipient will <em>always</em> know how to use the payload bytes
|
||||||
* the given byte array without additional metadata, it is strongly recommended to use the
|
* without additional metadata, it is strongly recommended to also set the
|
||||||
* {@link #content(byte[], String)} method instead of this one. That method ensures that a JWT recipient
|
* {@link Header#getContentType() contentType} header. For example:</p>
|
||||||
* can inspect the {@code cty} header to know how to handle the byte array without ambiguity.</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>
|
* <p><b>Mutually Exclusive Claims and Content</b></p>
|
||||||
*
|
*
|
||||||
|
@ -222,15 +226,89 @@ public interface JwtBuilder extends ClaimsMutator<JwtBuilder> {
|
||||||
JwtBuilder content(byte[] content);
|
JwtBuilder content(byte[] content);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Sets the JWT payload to be the specified content byte array and also sets the
|
* Sets the JWT payload to be the bytes in the specified content stream.
|
||||||
* {@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
|
* <p><b>Content Type Recommendation</b></p>
|
||||||
* {@code cty} value to determine how to convert the byte array to the final content type as desired.
|
*
|
||||||
|
* <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>
|
* <blockquote><pre>
|
||||||
* {@link #header()}.{@link HeaderMutator#contentType(String) contentType(cty)}.{@link BuilderHeader#and() and()}
|
* content(in).{@link #header() header()}.{@link HeaderMutator#contentType(String) contentType(cty)}.{@link BuilderHeader#and() and()}</pre></blockquote>
|
||||||
* {@link #content(byte[]) content(content)}</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>
|
* <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;
|
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
|
* 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.
|
* return to {@code JwtBuilder} configuration via the {@link BuilderClaims#and() and()} method.
|
||||||
|
@ -295,24 +420,27 @@ public interface JwtBuilder extends ClaimsMutator<JwtBuilder> {
|
||||||
BuilderClaims claims();
|
BuilderClaims claims();
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Sets (and replaces) the JWT Claims payload with the specified name/value pairs. If you do not want the JWT
|
* Replaces the JWT Claims payload with the specified name/value pairs. This is an alias for:
|
||||||
* payload to be JSON claims and instead want it to be a byte array for any content, use the
|
* <blockquote><pre>
|
||||||
* {@link #content(byte[])} or {@link #content(byte[], String)} methods instead.
|
* {@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.
|
* @param claims the JWT Claims to be set as the JWT payload.
|
||||||
* @return the builder for method chaining.
|
* @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")
|
@SuppressWarnings("DeprecatedIsStillUsed")
|
||||||
@Deprecated
|
@Deprecated
|
||||||
JwtBuilder setClaims(Map<String, ?> claims);
|
JwtBuilder setClaims(Map<String, ?> claims);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Adds/appends all given name/value pairs to the JSON Claims in the payload.
|
* Adds/appends all given name/value pairs to the JSON Claims in the payload. This is an alias for:
|
||||||
* <p>
|
|
||||||
* This is a convenience wrapper for:
|
|
||||||
*
|
*
|
||||||
* <blockquote><pre>
|
* <blockquote><pre>
|
||||||
* {@link #claims()}.{@link MapMutator#add(Map) add(claims)}.{@link BuilderClaims#and() and()}</pre></blockquote>
|
* {@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
|
* 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>
|
* <blockquote><pre>
|
||||||
* {@link #claims()}.{@link MapMutator#add(Object, Object) add(name, value)}.{@link BuilderClaims#and() and()}</pre></blockquote>
|
* {@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
|
* 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
|
* 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>
|
* <blockquote><pre>
|
||||||
* {@link #claims()}.{@link MapMutator#add(Map) add(claims)}.{@link BuilderClaims#and() and()}</pre></blockquote>
|
* {@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">
|
* 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:
|
* This is a convenience wrapper for:
|
||||||
* <blockquote><pre>
|
* <blockquote><pre>
|
||||||
* {@link #claims()}.{@link ClaimsMutator#issuer(String) issuer(iss)}.{@link BuilderClaims#and() and()}</pre></blockquote>
|
* {@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">
|
* 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:
|
* This is a convenience wrapper for:
|
||||||
* <blockquote><pre>
|
* <blockquote><pre>
|
||||||
* {@link #claims()}.{@link ClaimsMutator#subject(String) subject(sub)}.{@link BuilderClaims#and() and()}</pre></blockquote>
|
* {@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">
|
* 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:
|
* This is a convenience wrapper for:
|
||||||
* <blockquote><pre>
|
* <blockquote><pre>
|
||||||
* {@link #claims()}.{@link ClaimsMutator#audience(String) audience(aud)}.{@link BuilderClaims#and() and()}</pre></blockquote>
|
* {@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">
|
* 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>
|
* <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">
|
* 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>
|
* <p>A JWT obtained before this timestamp should not be used.</p>
|
||||||
*
|
*
|
||||||
|
@ -432,11 +560,11 @@ public interface JwtBuilder extends ClaimsMutator<JwtBuilder> {
|
||||||
*/
|
*/
|
||||||
@Override
|
@Override
|
||||||
// for better/targeted JavaDoc
|
// 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">
|
* 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>
|
* <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">
|
* 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
|
* <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
|
* 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
|
* @param base64UrlEncoder the encoder to use when Base64Url-encoding
|
||||||
* @return the builder for method chaining.
|
* @return the builder for method chaining.
|
||||||
* @see #encoder(Encoder)
|
* @see #b64Url(Encoder)
|
||||||
* @since 0.10.0
|
* @since 0.10.0
|
||||||
* @deprecated since JJWT_RELEASE_VERSION in favor of the more modern builder-style
|
* @deprecated since JJWT_RELEASE_VERSION in favor of {@link #b64Url(Encoder)}.
|
||||||
* {@link #encoder(Encoder)} method.
|
|
||||||
*/
|
*/
|
||||||
|
@SuppressWarnings("DeprecatedIsStillUsed")
|
||||||
@Deprecated
|
@Deprecated
|
||||||
JwtBuilder base64UrlEncodeWith(Encoder<byte[], String> base64UrlEncoder);
|
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
|
* <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
|
* @param encoder the encoder to use when Base64Url-encoding
|
||||||
* @return the builder for method chaining.
|
* @return the builder for method chaining.
|
||||||
* @since JJWT_RELEASE_VERSION
|
* @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)
|
* 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.
|
* @param serializer the serializer to use when converting Map objects to JSON strings.
|
||||||
* @return the builder for method chaining.
|
* @return the builder for method chaining.
|
||||||
* @since 0.10.0
|
* @since 0.10.0
|
||||||
* @deprecated since JJWT_RELEASE_VERSION in favor of the more modern builder-style
|
* @deprecated since JJWT_RELEASE_VERSION in favor of {@link #json(Serializer)}
|
||||||
* {@link #serializer(Serializer)} method.
|
|
||||||
*/
|
*/
|
||||||
|
@SuppressWarnings("DeprecatedIsStillUsed")
|
||||||
@Deprecated
|
@Deprecated
|
||||||
JwtBuilder serializeToJsonWith(Serializer<Map<String, ?>> serializer);
|
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
|
* 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.
|
* 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
|
* 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>
|
* 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.
|
* @return the builder for method chaining.
|
||||||
* @since JJWT_RELEASE_VERSION
|
* @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
|
* Actually builds the JWT and serializes it to a compact, URL-safe string according to the
|
||||||
|
|
|
@ -18,6 +18,8 @@ package io.jsonwebtoken;
|
||||||
import io.jsonwebtoken.security.SecurityException;
|
import io.jsonwebtoken.security.SecurityException;
|
||||||
import io.jsonwebtoken.security.SignatureException;
|
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.
|
* 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,
|
Jws<byte[]> parseContentJws(String jws) throws UnsupportedJwtException, MalformedJwtException, SignatureException,
|
||||||
SecurityException, IllegalArgumentException;
|
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
|
||||||
|
* "<a href="https://datatracker.ietf.org/doc/html/rfc7797#section-5.2">unencoded non-detached
|
||||||
|
* payload</a>", 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);
|
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
|
||||||
|
* "<a href="https://datatracker.ietf.org/doc/html/rfc7797#section-5.2">unencoded non-detached
|
||||||
|
* payload</a>", 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);
|
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
|
||||||
|
* "<a href="https://datatracker.ietf.org/doc/html/rfc7797#section-5.2">unencoded non-detached
|
||||||
|
* payload</a>", 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
|
||||||
|
* "<a href="https://datatracker.ietf.org/doc/html/rfc7797#section-5.2">unencoded non-detached
|
||||||
|
* payload</a>", 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
|
* Parses the specified compact serialized JWS string based on the builder's current configuration state and
|
||||||
* returns the resulting Claims JWS instance.
|
* returns the resulting Claims JWS instance.
|
||||||
|
|
|
@ -24,6 +24,7 @@ import io.jsonwebtoken.security.KeyAlgorithm;
|
||||||
import io.jsonwebtoken.security.SecureDigestAlgorithm;
|
import io.jsonwebtoken.security.SecureDigestAlgorithm;
|
||||||
|
|
||||||
import javax.crypto.SecretKey;
|
import javax.crypto.SecretKey;
|
||||||
|
import java.io.InputStream;
|
||||||
import java.security.Key;
|
import java.security.Key;
|
||||||
import java.security.PrivateKey;
|
import java.security.PrivateKey;
|
||||||
import java.security.Provider;
|
import java.security.Provider;
|
||||||
|
@ -695,22 +696,25 @@ public interface JwtParserBuilder extends Builder<JwtParser> {
|
||||||
*
|
*
|
||||||
* @param base64UrlDecoder the decoder to use when Base64Url-decoding
|
* @param base64UrlDecoder the decoder to use when Base64Url-decoding
|
||||||
* @return the parser builder for method chaining.
|
* @return the parser builder for method chaining.
|
||||||
* @deprecated since JJWT_RELEASE_VERSION in favor of the shorter and more modern builder-style named
|
* @deprecated since JJWT_RELEASE_VERSION in favor of {@link #b64Url(Decoder)}. This method will be removed
|
||||||
* {@link #decoder(Decoder)}. This method will be removed before the JJWT 1.0 release.
|
* before the JJWT 1.0 release.
|
||||||
*/
|
*/
|
||||||
@Deprecated
|
@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
|
* <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.
|
* @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
|
* 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.
|
* @param deserializer the deserializer to use when converting JSON Strings (UTF-8 byte arrays) into Map objects.
|
||||||
* @return the builder for method chaining.
|
* @return the builder for method chaining.
|
||||||
* @deprecated since JJWT_RELEASE_VERSION in favor of the shorter and more modern builder-style named
|
* @deprecated since JJWT_RELEASE_VERSION in favor of {@link #json(Deserializer)}.
|
||||||
* {@link #deserializer(Deserializer)}. This method will be removed before the JJWT 1.0 release.
|
* This method will be removed before the JJWT 1.0 release.
|
||||||
*/
|
*/
|
||||||
@Deprecated
|
@Deprecated
|
||||||
JwtParserBuilder deserializeJsonWith(Deserializer<Map<String, ?>> deserializer);
|
JwtParserBuilder deserializeJsonWith(Deserializer<Map<String, ?>> deserializer);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Uses the specified deserializer to convert JSON Strings (UTF-8 byte arrays) into Java Map objects. This is
|
* Uses the specified JSON {@link Deserializer} to deserialize JSON (UTF-8 byte streams) into Java Map objects.
|
||||||
* used by the parser after Base64Url-decoding to convert JWT/JWS/JWT JSON headers and claims into Java Map
|
* This is used by the parser after Base64Url-decoding to convert JWT/JWS/JWT headers and Claims into Java Map
|
||||||
* objects.
|
* 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
|
* 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
|
* in the runtime classpath, an exception will be thrown when one of the various {@code parse}* methods is
|
||||||
* invoked.</p>
|
* 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.
|
* @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.
|
* Returns an immutable/thread-safe {@link JwtParser} created from the configuration from this JwtParserBuilder.
|
||||||
|
|
|
@ -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;
|
||||||
|
}
|
|
@ -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;
|
||||||
|
}
|
|
@ -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>
|
* Decodes a BASE64-encoded {@code CharSequence} that is known to be reasonably well formatted. The preconditions
|
||||||
* + The array must have a line length of 76 chars OR no line separators at all (one line).<br>
|
* 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
|
* + Line separator must be "\r\n", as specified in RFC 2045
|
||||||
* + The array must not contain illegal characters within the encoded string<br>
|
* + The sequence 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 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.
|
* @return The decoded array of bytes. May be of length 0.
|
||||||
* @throws DecodingException on illegal input
|
* @throws DecodingException on illegal input
|
||||||
*/
|
*/
|
||||||
final byte[] decodeFast(char[] sArr) throws DecodingException {
|
byte[] decodeFast(CharSequence seq) throws DecodingException {
|
||||||
|
|
||||||
// Check special case
|
// Check special case
|
||||||
int sLen = sArr != null ? sArr.length : 0;
|
int sLen = seq != null ? seq.length() : 0;
|
||||||
if (sLen == 0) {
|
if (sLen == 0) {
|
||||||
return new byte[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.
|
int sIx = 0, eIx = sLen - 1; // Start and end index after trimming.
|
||||||
|
|
||||||
// Trim illegal chars from start
|
// Trim illegal chars from start
|
||||||
while (sIx < eIx && IALPHABET[sArr[sIx]] < 0) {
|
while (sIx < eIx && IALPHABET[seq.charAt(sIx)] < 0) {
|
||||||
sIx++;
|
sIx++;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Trim illegal chars from end
|
// Trim illegal chars from end
|
||||||
while (eIx > 0 && IALPHABET[sArr[eIx]] < 0) {
|
while (eIx > 0 && IALPHABET[seq.charAt(eIx)] < 0) {
|
||||||
eIx--;
|
eIx--;
|
||||||
}
|
}
|
||||||
|
|
||||||
// get the padding count (=) (0, 1 or 2)
|
// 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 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
|
int len = ((cCnt - sepCnt) * 6 >> 3) - pad; // The number of decoded bytes
|
||||||
byte[] dArr = new byte[len]; // Preallocate byte[] of exact length
|
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; ) {
|
for (int cc = 0, eLen = (len / 3) * 3; d < eLen; ) {
|
||||||
|
|
||||||
// Assemble three bytes into an int from four "valid" characters.
|
// 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
|
// Add the bytes
|
||||||
dArr[d++] = (byte) (i >> 16);
|
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
|
// Decode last 1-3 bytes (incl '=') into 1-3 bytes
|
||||||
int i = 0;
|
int i = 0;
|
||||||
for (int j = 0; sIx <= eIx - pad; j++) {
|
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) {
|
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) {
|
if ((sLen - sepCnt) % 4 != 0) {
|
||||||
return null;
|
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>
|
* 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>
|
* + 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
|
* + 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.
|
* little faster.
|
||||||
* @return A BASE64 encoded array. Never <code>null</code>.
|
* @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.
|
// Reuse char[] since we can't create a String incrementally anyway and StringBuffer/Builder would be slower.
|
||||||
return new String(encodeToChar(sArr, lineSep));
|
return new String(encodeToChar(sArr, lineSep));
|
||||||
}
|
}
|
||||||
|
|
|
@ -23,7 +23,7 @@ import io.jsonwebtoken.lang.Assert;
|
||||||
*
|
*
|
||||||
* @since 0.10.0
|
* @since 0.10.0
|
||||||
*/
|
*/
|
||||||
class Base64Decoder extends Base64Support implements Decoder<String, byte[]> {
|
class Base64Decoder extends Base64Support implements Decoder<CharSequence, byte[]> {
|
||||||
|
|
||||||
Base64Decoder() {
|
Base64Decoder() {
|
||||||
super(Base64.DEFAULT);
|
super(Base64.DEFAULT);
|
||||||
|
@ -34,8 +34,8 @@ class Base64Decoder extends Base64Support implements Decoder<String, byte[]> {
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public byte[] decode(String s) throws DecodingException {
|
public byte[] decode(CharSequence s) throws DecodingException {
|
||||||
Assert.notNull(s, "String argument cannot be null");
|
Assert.notNull(s, "String argument cannot be null");
|
||||||
return this.base64.decodeFast(s.toCharArray());
|
return this.base64.decodeFast(s);
|
||||||
}
|
}
|
||||||
}
|
}
|
|
@ -26,7 +26,7 @@ import io.jsonwebtoken.lang.Assert;
|
||||||
class Base64Encoder extends Base64Support implements Encoder<byte[], String> {
|
class Base64Encoder extends Base64Support implements Encoder<byte[], String> {
|
||||||
|
|
||||||
Base64Encoder() {
|
Base64Encoder() {
|
||||||
super(Base64.DEFAULT);
|
this(Base64.DEFAULT);
|
||||||
}
|
}
|
||||||
|
|
||||||
Base64Encoder(Base64 base64) {
|
Base64Encoder(Base64 base64) {
|
||||||
|
|
|
@ -15,14 +15,15 @@
|
||||||
*/
|
*/
|
||||||
package io.jsonwebtoken.io;
|
package io.jsonwebtoken.io;
|
||||||
|
|
||||||
import io.jsonwebtoken.CompressionException;
|
|
||||||
import io.jsonwebtoken.Identifiable;
|
import io.jsonwebtoken.Identifiable;
|
||||||
import io.jsonwebtoken.Jwts;
|
import io.jsonwebtoken.Jwts;
|
||||||
|
|
||||||
|
import java.io.InputStream;
|
||||||
|
import java.io.OutputStream;
|
||||||
import java.util.Collection;
|
import java.util.Collection;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Compresses and decompresses byte arrays.
|
* Compresses and decompresses byte streams.
|
||||||
*
|
*
|
||||||
* <p><b>"zip" identifier</b></p>
|
* <p><b>"zip" identifier</b></p>
|
||||||
*
|
*
|
||||||
|
@ -44,21 +45,18 @@ import java.util.Collection;
|
||||||
public interface CompressionAlgorithm extends Identifiable {
|
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
|
* @param out the stream to wrap for compression
|
||||||
* @return compressed bytes
|
* @return the stream to use for writing
|
||||||
* @throws CompressionException if the specified byte array cannot be compressed.
|
|
||||||
*/
|
*/
|
||||||
byte[] compress(byte[] content) throws CompressionException;
|
OutputStream compress(OutputStream out);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Decompresses the specified compressed byte array, returning the decompressed byte array result. The
|
* Wraps the specified {@code InputStream} to ensure any stream bytes are decompressed as they are read.
|
||||||
* specified byte array must already be in compressed form.
|
|
||||||
*
|
*
|
||||||
* @param compressed compressed bytes
|
* @param in the stream to wrap for decompression
|
||||||
* @return decompressed bytes
|
* @return the stream to use for reading
|
||||||
* @throws CompressionException if the specified byte array cannot be decompressed.
|
|
||||||
*/
|
*/
|
||||||
byte[] decompress(byte[] compressed) throws CompressionException;
|
InputStream decompress(InputStream in);
|
||||||
}
|
}
|
||||||
|
|
|
@ -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
|
* Very fast <a href="https://datatracker.ietf.org/doc/html/rfc4648#section-4">Base64</a> decoder guaranteed to
|
||||||
* work in all >= Java 7 JDK and Android environments.
|
* work in all >= 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
|
* Very fast <a href="https://datatracker.ietf.org/doc/html/rfc4648#section-5">Base64Url</a> decoder guaranteed to
|
||||||
* work in all >= Java 7 JDK and Android environments.
|
* work in all >= 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
|
private Decoders() { //prevent instantiation
|
||||||
}
|
}
|
||||||
|
|
|
@ -15,8 +15,10 @@
|
||||||
*/
|
*/
|
||||||
package io.jsonwebtoken.io;
|
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.
|
* @param <T> the type of object to be returned as a result of deserialization.
|
||||||
* @since 0.10.0
|
* @since 0.10.0
|
||||||
|
@ -28,7 +30,19 @@ public interface Deserializer<T> {
|
||||||
*
|
*
|
||||||
* @param bytes the formatted data byte array to convert
|
* @param bytes the formatted data byte array to convert
|
||||||
* @return the reconstituted Java object
|
* @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;
|
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;
|
||||||
}
|
}
|
||||||
|
|
|
@ -40,15 +40,15 @@ public interface ParserBuilder<T, B extends ParserBuilder<T, B>> extends Builder
|
||||||
B provider(Provider provider);
|
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).
|
* 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
|
* 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.
|
* 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.
|
* @return the builder for method chaining.
|
||||||
*/
|
*/
|
||||||
B deserializer(Deserializer<Map<String, ?>> deserializer);
|
B json(Deserializer<Map<String, ?>> deserializer);
|
||||||
}
|
}
|
||||||
|
|
|
@ -15,8 +15,10 @@
|
||||||
*/
|
*/
|
||||||
package io.jsonwebtoken.io;
|
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}.
|
* can be reconstituted back into a Java object with a matching {@link Deserializer}.
|
||||||
*
|
*
|
||||||
* @param <T> The type of object to serialize.
|
* @param <T> The type of object to serialize.
|
||||||
|
@ -25,12 +27,25 @@ package io.jsonwebtoken.io;
|
||||||
public interface Serializer<T> {
|
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
|
* @param t the object to serialize
|
||||||
* @return the serialized byte array representing the specified object.
|
* @return the serialized byte array representing the specified object.
|
||||||
* @throws SerializationException if there is a problem converting the object to a byte array.
|
* @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;
|
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;
|
||||||
}
|
}
|
||||||
|
|
|
@ -140,12 +140,13 @@ public final class Assert {
|
||||||
* be <code>null</code> and must contain at least one non-whitespace character.
|
* 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>
|
* <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
|
* @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
|
* @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)) {
|
if (!Strings.hasText(text)) {
|
||||||
throw new IllegalArgumentException(message);
|
throw new IllegalArgumentException(message);
|
||||||
}
|
}
|
||||||
|
|
|
@ -17,6 +17,7 @@ package io.jsonwebtoken.lang;
|
||||||
|
|
||||||
import java.io.InputStream;
|
import java.io.InputStream;
|
||||||
import java.lang.reflect.Constructor;
|
import java.lang.reflect.Constructor;
|
||||||
|
import java.lang.reflect.Field;
|
||||||
import java.lang.reflect.InvocationTargetException;
|
import java.lang.reflect.InvocationTargetException;
|
||||||
import java.lang.reflect.Method;
|
import java.lang.reflect.Method;
|
||||||
import java.net.URL;
|
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
|
* @since 1.0
|
||||||
*/
|
*/
|
||||||
|
|
|
@ -16,6 +16,7 @@
|
||||||
package io.jsonwebtoken.lang;
|
package io.jsonwebtoken.lang;
|
||||||
|
|
||||||
import java.io.Closeable;
|
import java.io.Closeable;
|
||||||
|
import java.io.Flushable;
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
import java.lang.reflect.Array;
|
import java.lang.reflect.Array;
|
||||||
import java.util.Arrays;
|
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) {
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -15,6 +15,8 @@
|
||||||
*/
|
*/
|
||||||
package io.jsonwebtoken.lang;
|
package io.jsonwebtoken.lang;
|
||||||
|
|
||||||
|
import java.nio.ByteBuffer;
|
||||||
|
import java.nio.CharBuffer;
|
||||||
import java.nio.charset.Charset;
|
import java.nio.charset.Charset;
|
||||||
import java.nio.charset.StandardCharsets;
|
import java.nio.charset.StandardCharsets;
|
||||||
import java.util.ArrayList;
|
import java.util.ArrayList;
|
||||||
|
@ -41,6 +43,8 @@ public final class Strings {
|
||||||
*/
|
*/
|
||||||
public static final String EMPTY = "";
|
public static final String EMPTY = "";
|
||||||
|
|
||||||
|
private static final CharBuffer EMPTY_BUF = CharBuffer.wrap(EMPTY);
|
||||||
|
|
||||||
private static final String FOLDER_SEPARATOR = "/";
|
private static final String FOLDER_SEPARATOR = "/";
|
||||||
|
|
||||||
private static final String WINDOWS_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}.
|
* @return the specified string's UTF-8 bytes, or {@code null} if the string is {@code null}.
|
||||||
* @since JJWT_RELEASE_VERSION
|
* @since JJWT_RELEASE_VERSION
|
||||||
*/
|
*/
|
||||||
public static byte[] utf8(String s) {
|
public static byte[] utf8(CharSequence s) {
|
||||||
byte[] bytes = null;
|
if (s == null) return null;
|
||||||
if (s != null) {
|
CharBuffer cb = s instanceof CharBuffer ? (CharBuffer) s : CharBuffer.wrap(s);
|
||||||
bytes = s.getBytes(UTF_8);
|
cb.mark();
|
||||||
}
|
ByteBuffer buf = UTF_8.encode(cb);
|
||||||
|
byte[] bytes = new byte[buf.remaining()];
|
||||||
|
buf.get(bytes);
|
||||||
|
cb.reset();
|
||||||
return bytes;
|
return bytes;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -257,6 +264,41 @@ public final class Strings {
|
||||||
return new String(utf8Bytes, UTF_8);
|
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.
|
* 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++) {
|
for (int i = 0; i < localePart.length(); i++) {
|
||||||
char ch = localePart.charAt(i);
|
char ch = localePart.charAt(i);
|
||||||
if (ch != '_' && ch != ' ' && !Character.isLetterOrDigit(ch)) {
|
if (ch != '_' && ch != ' ' && !Character.isLetterOrDigit(ch)) {
|
||||||
throw new IllegalArgumentException(
|
throw new IllegalArgumentException("Locale part \"" + localePart + "\" contains invalid characters");
|
||||||
"Locale part \"" + localePart + "\" contains invalid characters");
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1049,8 +1090,7 @@ public final class Strings {
|
||||||
* @return a <code>Properties</code> instance representing the array contents,
|
* @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
|
* or <code>null</code> if the array to process was <code>null</code> or empty
|
||||||
*/
|
*/
|
||||||
public static Properties splitArrayElementsIntoProperties(
|
public static Properties splitArrayElementsIntoProperties(String[] array, String delimiter, String charsToDelete) {
|
||||||
String[] array, String delimiter, String charsToDelete) {
|
|
||||||
|
|
||||||
if (Objects.isEmpty(array)) {
|
if (Objects.isEmpty(array)) {
|
||||||
return null;
|
return null;
|
||||||
|
@ -1109,8 +1149,7 @@ public final class Strings {
|
||||||
* @see java.lang.String#trim()
|
* @see java.lang.String#trim()
|
||||||
* @see #delimitedListToStringArray
|
* @see #delimitedListToStringArray
|
||||||
*/
|
*/
|
||||||
public static String[] tokenizeToStringArray(
|
public static String[] tokenizeToStringArray(String str, String delimiters, boolean trimTokens, boolean ignoreEmptyTokens) {
|
||||||
String str, String delimiters, boolean trimTokens, boolean ignoreEmptyTokens) {
|
|
||||||
|
|
||||||
if (str == null) {
|
if (str == null) {
|
||||||
return null;
|
return null;
|
||||||
|
|
|
@ -19,6 +19,7 @@ import io.jsonwebtoken.Identifiable;
|
||||||
import io.jsonwebtoken.Jwts;
|
import io.jsonwebtoken.Jwts;
|
||||||
|
|
||||||
import javax.crypto.SecretKey;
|
import javax.crypto.SecretKey;
|
||||||
|
import java.io.OutputStream;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* A cryptographic algorithm that performs
|
* A cryptographic algorithm that performs
|
||||||
|
@ -67,25 +68,24 @@ import javax.crypto.SecretKey;
|
||||||
public interface AeadAlgorithm extends Identifiable, KeyLengthSupplier, KeyBuilderSupplier<SecretKey, SecretKeyBuilder> {
|
public interface AeadAlgorithm extends Identifiable, KeyLengthSupplier, KeyBuilderSupplier<SecretKey, SecretKeyBuilder> {
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Perform AEAD encryption with the plaintext represented by the specified {@code request}, returning the
|
* Encrypts plaintext and signs any {@link AeadRequest#getAssociatedData() associated data}, placing the resulting
|
||||||
* integrity-protected encrypted ciphertext result.
|
* ciphertext, initialization vector and authentication tag in the provided {@code result}.
|
||||||
*
|
*
|
||||||
* @param request the encryption request representing the plaintext to be encrypted, any additional
|
* @param req the encryption request representing the plaintext to be encrypted, any additional
|
||||||
* integrity-protected data and the encryption key.
|
* integrity-protected data and the encryption key.
|
||||||
* @return the encryption result containing the ciphertext, and associated initialization vector and resulting
|
* @param res the result to write ciphertext, initialization vector and AAD authentication tag (aka digest)
|
||||||
* authentication tag.
|
* @throws SecurityException if there is an encryption problem or AAD authenticity cannot be guaranteed.
|
||||||
* @throws SecurityException if there is an encryption problem or 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
|
* Decrypts ciphertext and authenticates any {@link DecryptAeadRequest#getAssociatedData() associated data},
|
||||||
* integrity and authenticity of any associated data, returning the decrypted plaintext result.
|
* writing the decrypted plaintext to the provided {@code out}put stream.
|
||||||
*
|
*
|
||||||
* @param request the decryption request representing the ciphertext to be decrypted, any additional
|
* @param request the decryption request representing the ciphertext to be decrypted, any additional
|
||||||
* integrity-protected data, authentication tag, initialization vector, and the decryption key.
|
* integrity-protected data, authentication tag, initialization vector, and decryption key
|
||||||
* @return the decryption result containing the plaintext
|
* @param out the OutputStream for writing decrypted plaintext
|
||||||
* @throws SecurityException if there is a decryption problem or authenticity assertions fail.
|
* @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;
|
||||||
}
|
}
|
||||||
|
|
|
@ -16,6 +16,7 @@
|
||||||
package io.jsonwebtoken.security;
|
package io.jsonwebtoken.security;
|
||||||
|
|
||||||
import javax.crypto.SecretKey;
|
import javax.crypto.SecretKey;
|
||||||
|
import java.io.InputStream;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* A request to an {@link AeadAlgorithm} to perform authenticated encryption with a supplied symmetric
|
* A request to an {@link AeadAlgorithm} to perform authenticated encryption with a supplied symmetric
|
||||||
|
@ -25,5 +26,5 @@ import javax.crypto.SecretKey;
|
||||||
* @see AssociatedDataSupplier
|
* @see AssociatedDataSupplier
|
||||||
* @since JJWT_RELEASE_VERSION
|
* @since JJWT_RELEASE_VERSION
|
||||||
*/
|
*/
|
||||||
public interface AeadRequest extends SecureRequest<byte[], SecretKey>, AssociatedDataSupplier {
|
public interface AeadRequest extends SecureRequest<InputStream, SecretKey>, AssociatedDataSupplier {
|
||||||
}
|
}
|
||||||
|
|
|
@ -15,23 +15,39 @@
|
||||||
*/
|
*/
|
||||||
package io.jsonwebtoken.security;
|
package io.jsonwebtoken.security;
|
||||||
|
|
||||||
|
import java.io.OutputStream;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The result of authenticated encryption, providing access to the resulting {@link #getPayload() ciphertext},
|
* The result of authenticated encryption, providing access to the ciphertext {@link #getOutputStream() output stream}
|
||||||
* {@link #getDigest() AAD tag}, and {@link #getInitializationVector() initialization vector}. The AAD tag and
|
* and resulting {@link #setTag(byte[]) AAD tag} and {@link #setIv(byte[]) initialization vector}.
|
||||||
* initialization vector must be supplied with the ciphertext to decrypt.
|
* 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.
|
|
||||||
*
|
*
|
||||||
* @since JJWT_RELEASE_VERSION
|
* @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);
|
||||||
}
|
}
|
||||||
|
|
|
@ -15,6 +15,8 @@
|
||||||
*/
|
*/
|
||||||
package io.jsonwebtoken.security;
|
package io.jsonwebtoken.security;
|
||||||
|
|
||||||
|
import java.io.InputStream;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Provides any "associated data" that must be integrity protected (but not encrypted) when performing
|
* Provides any "associated data" that must be integrity protected (but not encrypted) when performing
|
||||||
* <a href="https://en.wikipedia.org/wiki/Authenticated_encryption">AEAD encryption or decryption</a>.
|
* <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
|
* <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.
|
* {@code null} if no additional data must be integrity protected.
|
||||||
*/
|
*/
|
||||||
byte[] getAssociatedData();
|
InputStream getAssociatedData();
|
||||||
}
|
}
|
||||||
|
|
|
@ -19,10 +19,10 @@ import javax.crypto.SecretKey;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* A request to an {@link AeadAlgorithm} to decrypt ciphertext and perform integrity-protection with a supplied
|
* 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.
|
* ensure the respective required IV and AAD tag returned from an {@link AeadResult} are available for decryption.
|
||||||
*
|
*
|
||||||
* @since JJWT_RELEASE_VERSION
|
* @since JJWT_RELEASE_VERSION
|
||||||
*/
|
*/
|
||||||
public interface DecryptAeadRequest extends AeadRequest, InitializationVectorSupplier, DigestSupplier {
|
public interface DecryptAeadRequest extends AeadRequest, IvSupplier, DigestSupplier {
|
||||||
}
|
}
|
||||||
|
|
|
@ -19,6 +19,7 @@ import io.jsonwebtoken.Identifiable;
|
||||||
import io.jsonwebtoken.lang.Registry;
|
import io.jsonwebtoken.lang.Registry;
|
||||||
|
|
||||||
import javax.crypto.SecretKey;
|
import javax.crypto.SecretKey;
|
||||||
|
import java.io.InputStream;
|
||||||
import java.security.PrivateKey;
|
import java.security.PrivateKey;
|
||||||
import java.security.PublicKey;
|
import java.security.PublicKey;
|
||||||
|
|
||||||
|
@ -75,7 +76,7 @@ import java.security.PublicKey;
|
||||||
* @see io.jsonwebtoken.Jwts.SIG Jwts.SIG
|
* @see io.jsonwebtoken.Jwts.SIG Jwts.SIG
|
||||||
* @since JJWT_RELEASE_VERSION
|
* @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}.
|
* Returns a cryptographic digest of the request {@link Request#getPayload() payload}.
|
||||||
|
|
|
@ -17,6 +17,8 @@ package io.jsonwebtoken.security;
|
||||||
|
|
||||||
import io.jsonwebtoken.Identifiable;
|
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
|
* 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.
|
* 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
|
* @see Jwks.HASH
|
||||||
* @since JJWT_RELEASE_VERSION
|
* @since JJWT_RELEASE_VERSION
|
||||||
*/
|
*/
|
||||||
public interface HashAlgorithm extends DigestAlgorithm<Request<byte[]>, VerifyDigestRequest> {
|
public interface HashAlgorithm extends DigestAlgorithm<Request<InputStream>, VerifyDigestRequest> {
|
||||||
}
|
}
|
||||||
|
|
|
@ -16,14 +16,14 @@
|
||||||
package io.jsonwebtoken.security;
|
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
|
* 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
|
* algorithms, a <em>new</em> secure-random Initialization Vector <em>MUST</em> be generated for every individual
|
||||||
* encryption attempt.
|
* encryption attempt.
|
||||||
*
|
*
|
||||||
* @since JJWT_RELEASE_VERSION
|
* @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
|
* 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
|
* @return the secure-random Initialization Vector used during encryption, which must in turn be presented for
|
||||||
* use during decryption.
|
* use during decryption.
|
||||||
*/
|
*/
|
||||||
byte[] getInitializationVector();
|
byte[] getIv();
|
||||||
}
|
}
|
|
@ -23,9 +23,10 @@ import io.jsonwebtoken.io.ParserBuilder;
|
||||||
* Example usage:
|
* Example usage:
|
||||||
* <blockquote><pre>
|
* <blockquote><pre>
|
||||||
* JwkSet jwkSet = Jwks.setParser()
|
* JwkSet jwkSet = Jwks.setParser()
|
||||||
* .provider(aJcaProvider) // optional
|
* .provider(aJcaProvider) // optional
|
||||||
* .deserializer(deserializer) // optional
|
* .json(deserializer) // optional
|
||||||
* .operationPolicy(policy) // optional
|
* .operationPolicy(policy) // optional
|
||||||
|
* .ignoreUnsupported(aBoolean) // optional
|
||||||
* .build()
|
* .build()
|
||||||
* .parse(jwkSetString);</pre></blockquote>
|
* .parse(jwkSetString);</pre></blockquote>
|
||||||
*
|
*
|
||||||
|
|
|
@ -41,6 +41,7 @@ public final class Jwks {
|
||||||
private Jwks() {
|
private Jwks() {
|
||||||
} //prevent instantiation
|
} //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 BUILDER_FQCN = "io.jsonwebtoken.impl.security.DefaultDynamicJwkBuilder";
|
||||||
private static final String PARSER_BUILDER_FQCN = "io.jsonwebtoken.impl.security.DefaultJwkParserBuilder";
|
private static final String PARSER_BUILDER_FQCN = "io.jsonwebtoken.impl.security.DefaultJwkParserBuilder";
|
||||||
private static final String SET_BUILDER_FQCN = "io.jsonwebtoken.impl.security.DefaultJwkSetBuilder";
|
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);
|
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
|
* 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
|
* <a href="https://www.rfc-editor.org/rfc/rfc7518.html#section-6.2.1.1">crv (Curve)</a> parameter values
|
||||||
|
|
|
@ -17,6 +17,7 @@ package io.jsonwebtoken.security;
|
||||||
|
|
||||||
import io.jsonwebtoken.Identifiable;
|
import io.jsonwebtoken.Identifiable;
|
||||||
|
|
||||||
|
import java.io.InputStream;
|
||||||
import java.security.Key;
|
import java.security.Key;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -50,5 +51,5 @@ import java.security.Key;
|
||||||
* @since JJWT_RELEASE_VERSION
|
* @since JJWT_RELEASE_VERSION
|
||||||
*/
|
*/
|
||||||
public interface SecureDigestAlgorithm<S extends Key, V extends Key>
|
public interface SecureDigestAlgorithm<S extends Key, V extends Key>
|
||||||
extends DigestAlgorithm<SecureRequest<byte[], S>, VerifySecureDigestRequest<V>> {
|
extends DigestAlgorithm<SecureRequest<InputStream, S>, VerifySecureDigestRequest<V>> {
|
||||||
}
|
}
|
||||||
|
|
|
@ -15,6 +15,8 @@
|
||||||
*/
|
*/
|
||||||
package io.jsonwebtoken.security;
|
package io.jsonwebtoken.security;
|
||||||
|
|
||||||
|
import java.io.InputStream;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* A request to verify a previously-computed cryptographic digest (available via {@link #getDigest()}) against the
|
* 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}.
|
* digest to be computed for the specified {@link #getPayload() payload}.
|
||||||
|
@ -27,5 +29,5 @@ package io.jsonwebtoken.security;
|
||||||
* @see VerifySecureDigestRequest
|
* @see VerifySecureDigestRequest
|
||||||
* @since JJWT_RELEASE_VERSION
|
* @since JJWT_RELEASE_VERSION
|
||||||
*/
|
*/
|
||||||
public interface VerifyDigestRequest extends Request<byte[]>, DigestSupplier {
|
public interface VerifyDigestRequest extends Request<InputStream>, DigestSupplier {
|
||||||
}
|
}
|
||||||
|
|
|
@ -15,6 +15,7 @@
|
||||||
*/
|
*/
|
||||||
package io.jsonwebtoken.security;
|
package io.jsonwebtoken.security;
|
||||||
|
|
||||||
|
import java.io.InputStream;
|
||||||
import java.security.Key;
|
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
|
* @param <K> the type of {@link Key} used to verify a digital signature or message authentication code
|
||||||
* @since JJWT_RELEASE_VERSION
|
* @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 {
|
||||||
}
|
}
|
||||||
|
|
|
@ -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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -28,7 +28,7 @@ class Base64DecoderTest {
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
void testDecode() {
|
void decode() {
|
||||||
String encoded = 'SGVsbG8g5LiW55WM' // Hello 世界
|
String encoded = 'SGVsbG8g5LiW55WM' // Hello 世界
|
||||||
byte[] bytes = new Base64Decoder().decode(encoded)
|
byte[] bytes = new Base64Decoder().decode(encoded)
|
||||||
String result = new String(bytes, Strings.UTF_8)
|
String result = new String(bytes, Strings.UTF_8)
|
||||||
|
|
|
@ -28,7 +28,7 @@ class Base64EncoderTest {
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
void testDecode() {
|
void encode() {
|
||||||
String input = 'Hello 世界'
|
String input = 'Hello 世界'
|
||||||
byte[] bytes = input.getBytes(Strings.UTF_8)
|
byte[] bytes = input.getBytes(Strings.UTF_8)
|
||||||
String encoded = new Base64Encoder().encode(bytes)
|
String encoded = new Base64Encoder().encode(bytes)
|
||||||
|
|
|
@ -67,8 +67,7 @@ class Base64Test {
|
||||||
String encoded = Base64.DEFAULT.encodeToString(bytes, true)
|
String encoded = Base64.DEFAULT.encodeToString(bytes, true)
|
||||||
|
|
||||||
def r = new StringReader(encoded)
|
def r = new StringReader(encoded)
|
||||||
String line = ''
|
String line
|
||||||
|
|
||||||
while ((line = r.readLine()) != null) {
|
while ((line = r.readLine()) != null) {
|
||||||
assertTrue line.length() <= 76
|
assertTrue line.length() <= 76
|
||||||
}
|
}
|
||||||
|
@ -82,7 +81,7 @@ class Base64Test {
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
void testDecodeFastWithEmptyCharArray() {
|
void testDecodeFastWithEmptyCharArray() {
|
||||||
byte[] bytes = Base64.DEFAULT.decodeFast(new char[0])
|
byte[] bytes = Base64.DEFAULT.decodeFast(Strings.EMPTY)
|
||||||
assertEquals 0, bytes.length
|
assertEquals 0, bytes.length
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -90,7 +89,7 @@ class Base64Test {
|
||||||
void testDecodeFastWithSurroundingIllegalCharacters() {
|
void testDecodeFastWithSurroundingIllegalCharacters() {
|
||||||
String expected = 'Hello 世界'
|
String expected = 'Hello 世界'
|
||||||
def encoded = '***SGVsbG8g5LiW55WM!!!'
|
def encoded = '***SGVsbG8g5LiW55WM!!!'
|
||||||
byte[] bytes = Base64.DEFAULT.decodeFast(encoded.toCharArray())
|
byte[] bytes = Base64.DEFAULT.decodeFast(encoded)
|
||||||
String result = new String(bytes, Strings.UTF_8)
|
String result = new String(bytes, Strings.UTF_8)
|
||||||
assertEquals expected, result
|
assertEquals expected, result
|
||||||
}
|
}
|
||||||
|
@ -99,7 +98,7 @@ class Base64Test {
|
||||||
void testDecodeFastWithIntermediateIllegalInboundCharacters() {
|
void testDecodeFastWithIntermediateIllegalInboundCharacters() {
|
||||||
def encoded = 'SGVsbG8g*5LiW55WM'
|
def encoded = 'SGVsbG8g*5LiW55WM'
|
||||||
try {
|
try {
|
||||||
Base64.DEFAULT.decodeFast(encoded.toCharArray())
|
Base64.DEFAULT.decodeFast(encoded)
|
||||||
fail()
|
fail()
|
||||||
} catch (DecodingException de) {
|
} catch (DecodingException de) {
|
||||||
assertEquals 'Illegal base64 character: \'*\'', de.getMessage()
|
assertEquals 'Illegal base64 character: \'*\'', de.getMessage()
|
||||||
|
@ -110,7 +109,7 @@ class Base64Test {
|
||||||
void testDecodeFastWithIntermediateIllegalOutOfBoundCharacters() {
|
void testDecodeFastWithIntermediateIllegalOutOfBoundCharacters() {
|
||||||
def encoded = 'SGVsbG8g世5LiW55WM'
|
def encoded = 'SGVsbG8g世5LiW55WM'
|
||||||
try {
|
try {
|
||||||
Base64.DEFAULT.decodeFast(encoded.toCharArray())
|
Base64.DEFAULT.decodeFast(encoded)
|
||||||
fail()
|
fail()
|
||||||
} catch (DecodingException de) {
|
} catch (DecodingException de) {
|
||||||
assertEquals 'Illegal base64 character: \'世\'', de.getMessage()
|
assertEquals 'Illegal base64 character: \'世\'', de.getMessage()
|
||||||
|
@ -121,7 +120,7 @@ class Base64Test {
|
||||||
void testDecodeFastWithIntermediateIllegalSpaceCharacters() {
|
void testDecodeFastWithIntermediateIllegalSpaceCharacters() {
|
||||||
def encoded = 'SGVsbG8g 5LiW55WM'
|
def encoded = 'SGVsbG8g 5LiW55WM'
|
||||||
try {
|
try {
|
||||||
Base64.DEFAULT.decodeFast(encoded.toCharArray())
|
Base64.DEFAULT.decodeFast(encoded)
|
||||||
fail()
|
fail()
|
||||||
} catch (DecodingException de) {
|
} catch (DecodingException de) {
|
||||||
assertEquals 'Illegal base64 character: \' \'', de.getMessage()
|
assertEquals 'Illegal base64 character: \' \'', de.getMessage()
|
||||||
|
@ -134,23 +133,24 @@ class Base64Test {
|
||||||
byte[] bytes = PLAINTEXT.getBytes(Strings.UTF_8)
|
byte[] bytes = PLAINTEXT.getBytes(Strings.UTF_8)
|
||||||
String encoded = Base64.DEFAULT.encodeToString(bytes, true)
|
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)
|
assertTrue Arrays.equals(bytes, resultBytes)
|
||||||
assertEquals PLAINTEXT, new String(resultBytes, Strings.UTF_8)
|
assertEquals PLAINTEXT, new String(resultBytes, Strings.UTF_8)
|
||||||
}
|
}
|
||||||
|
|
||||||
private static String encode(String s) {
|
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)
|
return Base64.DEFAULT.encodeToString(bytes, false)
|
||||||
}
|
}
|
||||||
|
|
||||||
private static String decode(String s) {
|
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)
|
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() {
|
void testRfc4648Base64TestVectors() {
|
||||||
|
|
||||||
assertEquals "", encode("")
|
assertEquals "", encode("")
|
||||||
|
@ -181,16 +181,17 @@ class Base64Test {
|
||||||
}
|
}
|
||||||
|
|
||||||
private static String urlEncode(String s) {
|
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)
|
return Base64.URL_SAFE.encodeToString(bytes, false)
|
||||||
}
|
}
|
||||||
|
|
||||||
private static String urlDecode(String s) {
|
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)
|
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() {
|
void testRfc4648Base64UrlTestVectors() {
|
||||||
|
|
||||||
assertEquals "", urlEncode("")
|
assertEquals "", urlEncode("")
|
||||||
|
@ -216,9 +217,9 @@ class Base64Test {
|
||||||
|
|
||||||
def input = 'special: [\r\n \t], ascii[32..126]: [ !"#$%&\'()*+,-./0123456789:;<=>?@ABCDEFGHIJKLMNOPQRSTUVWXYZ[\\]^_`abcdefghijklmnopqrstuvwxyz{|}~]\n'
|
def input = 'special: [\r\n \t], ascii[32..126]: [ !"#$%&\'()*+,-./0123456789:;<=>?@ABCDEFGHIJKLMNOPQRSTUVWXYZ[\\]^_`abcdefghijklmnopqrstuvwxyz{|}~]\n'
|
||||||
def expected = "c3BlY2lhbDogWw0KIAldLCBhc2NpaVszMi4uMTI2XTogWyAhIiMkJSYnKCkqKywtLi8wMTIzNDU2Nzg5Ojs8PT4/QEFCQ0RFRkdISUpLTE1OT1BRUlNUVVZXWFlaW1xdXl9gYWJjZGVmZ2hpamtsbW5vcHFyc3R1dnd4eXp7fH1+XQo="
|
def expected = "c3BlY2lhbDogWw0KIAldLCBhc2NpaVszMi4uMTI2XTogWyAhIiMkJSYnKCkqKywtLi8wMTIzNDU2Nzg5Ojs8PT4/QEFCQ0RFRkdISUpLTE1OT1BRUlNUVVZXWFlaW1xdXl9gYWJjZGVmZ2hpamtsbW5vcHFyc3R1dnd4eXp7fH1+XQo="
|
||||||
.replace("=", "")
|
.replace("=", "")
|
||||||
.replace("+", "-")
|
.replace("+", "-")
|
||||||
.replace("/", "_")
|
.replace("/", "_")
|
||||||
assertEquals expected, urlEncode(input)
|
assertEquals expected, urlEncode(input)
|
||||||
assertEquals input, urlDecode(expected)
|
assertEquals input, urlDecode(expected)
|
||||||
}
|
}
|
||||||
|
|
|
@ -16,24 +16,24 @@
|
||||||
package io.jsonwebtoken.gson.io;
|
package io.jsonwebtoken.gson.io;
|
||||||
|
|
||||||
import com.google.gson.Gson;
|
import com.google.gson.Gson;
|
||||||
import io.jsonwebtoken.io.DeserializationException;
|
import io.jsonwebtoken.io.AbstractDeserializer;
|
||||||
import io.jsonwebtoken.io.Deserializer;
|
|
||||||
import io.jsonwebtoken.lang.Assert;
|
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 Class<T> returnType;
|
||||||
private final Gson gson;
|
protected final Gson gson;
|
||||||
|
|
||||||
@SuppressWarnings("unused") //used via reflection by RuntimeClasspathDeserializerLocator
|
|
||||||
public GsonDeserializer() {
|
public GsonDeserializer() {
|
||||||
this(GsonSerializer.DEFAULT_GSON);
|
this(GsonSerializer.DEFAULT_GSON);
|
||||||
}
|
}
|
||||||
|
|
||||||
@SuppressWarnings({"unchecked", "WeakerAccess", "unused"}) // for end-users providing a custom gson
|
@SuppressWarnings("unchecked")
|
||||||
public GsonDeserializer(Gson gson) {
|
public GsonDeserializer(Gson gson) {
|
||||||
this(gson, (Class<T>) Object.class);
|
this(gson, (Class<T>) Object.class);
|
||||||
}
|
}
|
||||||
|
@ -46,16 +46,8 @@ public class GsonDeserializer<T> implements Deserializer<T> {
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public T deserialize(byte[] bytes) throws DeserializationException {
|
protected T doDeserialize(InputStream in) throws Exception {
|
||||||
try {
|
Reader reader = new InputStreamReader(in, StandardCharsets.UTF_8);
|
||||||
return readValue(bytes);
|
return gson.fromJson(reader, returnType);
|
||||||
} 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);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -17,26 +17,29 @@ package io.jsonwebtoken.gson.io;
|
||||||
|
|
||||||
import com.google.gson.Gson;
|
import com.google.gson.Gson;
|
||||||
import com.google.gson.GsonBuilder;
|
import com.google.gson.GsonBuilder;
|
||||||
|
import io.jsonwebtoken.io.AbstractSerializer;
|
||||||
import io.jsonwebtoken.io.Encoders;
|
import io.jsonwebtoken.io.Encoders;
|
||||||
import io.jsonwebtoken.io.SerializationException;
|
|
||||||
import io.jsonwebtoken.io.Serializer;
|
|
||||||
import io.jsonwebtoken.lang.Assert;
|
import io.jsonwebtoken.lang.Assert;
|
||||||
import io.jsonwebtoken.lang.Strings;
|
import io.jsonwebtoken.lang.Objects;
|
||||||
import io.jsonwebtoken.lang.Supplier;
|
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()
|
static final Gson DEFAULT_GSON = new GsonBuilder()
|
||||||
.registerTypeHierarchyAdapter(Supplier.class, GsonSupplierSerializer.INSTANCE)
|
.registerTypeHierarchyAdapter(Supplier.class, GsonSupplierSerializer.INSTANCE)
|
||||||
.disableHtmlEscaping().create();
|
.disableHtmlEscaping().create();
|
||||||
private final Gson gson;
|
|
||||||
|
|
||||||
@SuppressWarnings("unused") //used via reflection by RuntimeClasspathDeserializerLocator
|
protected final Gson gson;
|
||||||
|
|
||||||
public GsonSerializer() {
|
public GsonSerializer() {
|
||||||
this(DEFAULT_GSON);
|
this(DEFAULT_GSON);
|
||||||
}
|
}
|
||||||
|
|
||||||
@SuppressWarnings("WeakerAccess") //intended for end-users to use when providing a custom gson
|
|
||||||
public GsonSerializer(Gson gson) {
|
public GsonSerializer(Gson gson) {
|
||||||
Assert.notNull(gson, "gson cannot be null.");
|
Assert.notNull(gson, "gson cannot be null.");
|
||||||
this.gson = gson;
|
this.gson = gson;
|
||||||
|
@ -54,27 +57,23 @@ public class GsonSerializer<T> implements Serializer<T> {
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public byte[] serialize(T t) throws SerializationException {
|
protected void doSerialize(T t, OutputStream out) {
|
||||||
Assert.notNull(t, "Object to serialize cannot be null.");
|
Writer writer = new OutputStreamWriter(out, StandardCharsets.UTF_8);
|
||||||
try {
|
try {
|
||||||
return writeValueAsBytes(t);
|
Object o = t;
|
||||||
} catch (Exception e) {
|
if (o instanceof byte[]) {
|
||||||
String msg = "Unable to serialize object: " + e.getMessage();
|
o = Encoders.BASE64.encode((byte[]) o);
|
||||||
throw new SerializationException(msg, e);
|
} else if (o instanceof char[]) {
|
||||||
|
o = new String((char[]) o);
|
||||||
|
}
|
||||||
|
writeValue(o, writer);
|
||||||
|
} finally {
|
||||||
|
Objects.nullSafeClose(writer);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@SuppressWarnings("WeakerAccess") //for testing
|
protected void writeValue(Object o, java.io.Writer writer) {
|
||||||
protected byte[] writeValueAsBytes(T t) {
|
this.gson.toJson(o, writer);
|
||||||
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);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private static class TestSupplier<T> implements Supplier<T> {
|
private static class TestSupplier<T> implements Supplier<T> {
|
||||||
|
|
|
@ -13,79 +13,80 @@
|
||||||
* See the License for the specific language governing permissions and
|
* See the License for the specific language governing permissions and
|
||||||
* limitations under the License.
|
* limitations under the License.
|
||||||
*/
|
*/
|
||||||
|
//file:noinspection GrDeprecatedAPIUsage
|
||||||
package io.jsonwebtoken.gson.io
|
package io.jsonwebtoken.gson.io
|
||||||
|
|
||||||
import com.google.gson.Gson
|
import com.google.gson.Gson
|
||||||
import io.jsonwebtoken.io.DeserializationException
|
import io.jsonwebtoken.io.DeserializationException
|
||||||
import io.jsonwebtoken.io.Deserializer
|
import io.jsonwebtoken.io.Deserializer
|
||||||
import io.jsonwebtoken.lang.Strings
|
import io.jsonwebtoken.lang.Strings
|
||||||
|
import org.junit.Before
|
||||||
import org.junit.Test
|
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.junit.Assert.*
|
||||||
import static org.hamcrest.CoreMatchers.instanceOf
|
|
||||||
|
|
||||||
class GsonDeserializerTest {
|
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
|
@Test
|
||||||
void loadService() {
|
void loadService() {
|
||||||
def deserializer = ServiceLoader.load(Deserializer).iterator().next()
|
def deserializer = ServiceLoader.load(Deserializer).iterator().next()
|
||||||
assertThat(deserializer, instanceOf(GsonDeserializer))
|
assertTrue deserializer instanceof GsonDeserializer
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
void testDefaultConstructor() {
|
void testDefaultConstructor() {
|
||||||
def deserializer = new GsonDeserializer()
|
|
||||||
assertNotNull deserializer.gson
|
assertNotNull deserializer.gson
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
void testObjectMapperConstructor() {
|
void testGsonConstructor() {
|
||||||
def customGSON = new Gson()
|
def customGSON = new Gson()
|
||||||
def deserializer = new GsonDeserializer(customGSON)
|
deserializer = new GsonDeserializer(customGSON)
|
||||||
assertSame customGSON, deserializer.gson
|
assertSame customGSON, deserializer.gson
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test(expected = IllegalArgumentException)
|
@Test(expected = IllegalArgumentException)
|
||||||
void testObjectMapperConstructorWithNullArgument() {
|
void testGsonConstructorNullArgument() {
|
||||||
new GsonDeserializer<>(null)
|
new GsonDeserializer(null)
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
void testDeserialize() {
|
void testDeserialize() {
|
||||||
byte[] serialized = '{"hello":"世界"}'.getBytes(Strings.UTF_8)
|
|
||||||
def expected = [hello: '世界']
|
def expected = [hello: '世界']
|
||||||
def result = new GsonDeserializer().deserialize(serialized)
|
assertEquals expected, deser('{"hello":"世界"}')
|
||||||
assertEquals expected, result
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
void testDeserializeFailsWithJsonProcessingException() {
|
void testDeserializeThrows() {
|
||||||
|
def ex = new IOException('foo')
|
||||||
def ex = createMock(java.io.IOException)
|
deserializer = new GsonDeserializer() {
|
||||||
|
|
||||||
expect(ex.getMessage()).andReturn('foo')
|
|
||||||
|
|
||||||
def deserializer = new GsonDeserializer() {
|
|
||||||
@Override
|
@Override
|
||||||
protected Object readValue(byte[] bytes) throws java.io.IOException {
|
protected Object doDeserialize(InputStream inputStream) throws Exception {
|
||||||
throw ex
|
throw ex
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
replay ex
|
|
||||||
|
|
||||||
try {
|
try {
|
||||||
deserializer.deserialize('{"hello":"世界"}'.getBytes(Strings.UTF_8))
|
deser('{"hello":"世界"}')
|
||||||
fail()
|
fail()
|
||||||
} catch (DeserializationException se) {
|
} catch (DeserializationException expected) {
|
||||||
assertEquals 'Unable to deserialize bytes into a java.lang.Object instance: foo', se.getMessage()
|
String msg = 'Unable to deserialize: foo'
|
||||||
assertSame ex, se.getCause()
|
assertEquals msg, expected.message
|
||||||
|
assertSame ex, expected.cause
|
||||||
}
|
}
|
||||||
|
|
||||||
verify ex
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -13,6 +13,7 @@
|
||||||
* See the License for the specific language governing permissions and
|
* See the License for the specific language governing permissions and
|
||||||
* limitations under the License.
|
* limitations under the License.
|
||||||
*/
|
*/
|
||||||
|
//file:noinspection GrDeprecatedAPIUsage
|
||||||
package io.jsonwebtoken.gson.io
|
package io.jsonwebtoken.gson.io
|
||||||
|
|
||||||
import com.google.gson.Gson
|
import com.google.gson.Gson
|
||||||
|
@ -21,23 +22,33 @@ import io.jsonwebtoken.io.SerializationException
|
||||||
import io.jsonwebtoken.io.Serializer
|
import io.jsonwebtoken.io.Serializer
|
||||||
import io.jsonwebtoken.lang.Strings
|
import io.jsonwebtoken.lang.Strings
|
||||||
import io.jsonwebtoken.lang.Supplier
|
import io.jsonwebtoken.lang.Supplier
|
||||||
|
import org.junit.Before
|
||||||
import org.junit.Test
|
import org.junit.Test
|
||||||
|
|
||||||
import static org.easymock.EasyMock.*
|
|
||||||
import static org.junit.Assert.*
|
import static org.junit.Assert.*
|
||||||
|
|
||||||
class GsonSerializerTest {
|
class GsonSerializerTest {
|
||||||
|
|
||||||
|
private GsonSerializer s
|
||||||
|
|
||||||
|
@Before
|
||||||
|
void setUp() {
|
||||||
|
s = new GsonSerializer()
|
||||||
|
}
|
||||||
|
|
||||||
|
private String ser(Object o) {
|
||||||
|
return Strings.utf8(s.serialize(o))
|
||||||
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
void loadService() {
|
void loadService() {
|
||||||
def serializer = ServiceLoader.load(Serializer).iterator().next()
|
def serializer = ServiceLoader.load(Serializer).iterator().next()
|
||||||
assertTrue serializer instanceof GsonSerializer
|
assert serializer instanceof GsonSerializer
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
void testDefaultConstructor() {
|
void testDefaultConstructor() {
|
||||||
def serializer = new GsonSerializer()
|
assertNotNull s.gson
|
||||||
assertNotNull serializer.gson
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
|
@ -45,8 +56,23 @@ class GsonSerializerTest {
|
||||||
def customGSON = new GsonBuilder()
|
def customGSON = new GsonBuilder()
|
||||||
.registerTypeHierarchyAdapter(Supplier.class, GsonSupplierSerializer.INSTANCE)
|
.registerTypeHierarchyAdapter(Supplier.class, GsonSupplierSerializer.INSTANCE)
|
||||||
.disableHtmlEscaping().create()
|
.disableHtmlEscaping().create()
|
||||||
def serializer = new GsonSerializer<>(customGSON)
|
s = new GsonSerializer(customGSON)
|
||||||
assertSame customGSON, serializer.gson
|
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
|
@Test
|
||||||
|
@ -72,71 +98,74 @@ class GsonSerializerTest {
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
void testByte() {
|
void testByte() {
|
||||||
byte[] expected = "120".getBytes(Strings.UTF_8) //ascii("x") = 120
|
byte[] expected = Strings.utf8("120") //ascii("x") = 120
|
||||||
byte[] bytes = "x".getBytes(Strings.UTF_8)
|
byte[] result = bytes(Strings.utf8("x")[0]) //single byte
|
||||||
byte[] result = new GsonSerializer().serialize(bytes[0]) //single byte
|
assertArrayEquals expected, result
|
||||||
assertTrue Arrays.equals(expected, result)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
void testByteArray() { //expect Base64 string by default:
|
void testByteArray() { //expect Base64 string by default:
|
||||||
byte[] bytes = "hi".getBytes(Strings.UTF_8)
|
|
||||||
String expected = '"aGk="' as String //base64(hi) --> aGk=
|
String expected = '"aGk="' as String //base64(hi) --> aGk=
|
||||||
byte[] result = new GsonSerializer().serialize(bytes)
|
assertEquals expected, json(Strings.utf8('hi'))
|
||||||
assertEquals expected, new String(result, Strings.UTF_8)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
void testEmptyByteArray() { //expect Base64 string by default:
|
void testEmptyByteArray() { //expect Base64 string by default:
|
||||||
byte[] bytes = new byte[0]
|
byte[] result = bytes(new byte[0])
|
||||||
byte[] result = new GsonSerializer().serialize(bytes)
|
assertEquals '""', Strings.utf8(result)
|
||||||
assertEquals '""', new String(result, Strings.UTF_8)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
void testChar() { //expect Base64 string by default:
|
void testChar() { //expect Base64 string by default:
|
||||||
byte[] result = new GsonSerializer().serialize('h' as char)
|
assertEquals '"h"', json('h' as char)
|
||||||
assertEquals "\"h\"", new String(result, Strings.UTF_8)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
void testCharArray() { //expect Base64 string by default:
|
void testCharArray() { //expect string by default:
|
||||||
byte[] result = new GsonSerializer().serialize("hi".toCharArray())
|
assertEquals '"hi"', json('hi'.toCharArray())
|
||||||
assertEquals "\"hi\"", new String(result, Strings.UTF_8)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
void testSerialize() {
|
void testWrite() {
|
||||||
byte[] expected = '{"hello":"世界"}'.getBytes(Strings.UTF_8)
|
assertEquals '{"hello":"世界"}', json([hello: '世界'])
|
||||||
byte[] result = new GsonSerializer().serialize([hello: '世界'])
|
|
||||||
assertTrue Arrays.equals(expected, result)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
void testSerializeFailsWithJsonProcessingException() {
|
void testWriteFailure() {
|
||||||
|
def ex = new IOException('foo')
|
||||||
def ex = createMock(SerializationException)
|
s = new GsonSerializer() {
|
||||||
|
|
||||||
expect(ex.getMessage()).andReturn('foo')
|
|
||||||
|
|
||||||
def serializer = new GsonSerializer() {
|
|
||||||
@Override
|
@Override
|
||||||
protected byte[] writeValueAsBytes(Object o) throws SerializationException {
|
protected void doSerialize(Object o, OutputStream out) {
|
||||||
throw ex
|
throw ex
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
replay ex
|
|
||||||
|
|
||||||
try {
|
try {
|
||||||
serializer.serialize([hello: 'world'])
|
ser([hello: 'world'])
|
||||||
fail()
|
fail()
|
||||||
} catch (SerializationException se) {
|
} catch (SerializationException expected) {
|
||||||
assertEquals 'Unable to serialize object: foo', se.getMessage()
|
String msg = 'Unable to serialize object of type java.util.LinkedHashMap: foo'
|
||||||
assertSame ex, se.getCause()
|
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
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -20,11 +20,11 @@ import com.fasterxml.jackson.databind.DeserializationContext;
|
||||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||||
import com.fasterxml.jackson.databind.deser.std.UntypedObjectDeserializer;
|
import com.fasterxml.jackson.databind.deser.std.UntypedObjectDeserializer;
|
||||||
import com.fasterxml.jackson.databind.module.SimpleModule;
|
import com.fasterxml.jackson.databind.module.SimpleModule;
|
||||||
import io.jsonwebtoken.io.DeserializationException;
|
import io.jsonwebtoken.io.AbstractDeserializer;
|
||||||
import io.jsonwebtoken.io.Deserializer;
|
|
||||||
import io.jsonwebtoken.lang.Assert;
|
import io.jsonwebtoken.lang.Assert;
|
||||||
|
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
|
import java.io.InputStream;
|
||||||
import java.util.Collections;
|
import java.util.Collections;
|
||||||
import java.util.Map;
|
import java.util.Map;
|
||||||
|
|
||||||
|
@ -33,9 +33,10 @@ import java.util.Map;
|
||||||
*
|
*
|
||||||
* @since 0.10.0
|
* @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 Class<T> returnType;
|
||||||
|
|
||||||
private final ObjectMapper objectMapper;
|
private final ObjectMapper objectMapper;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -86,7 +87,7 @@ public class JacksonDeserializer<T> implements Deserializer<T> {
|
||||||
*
|
*
|
||||||
* @param objectMapper the ObjectMapper to use for deserialization.
|
* @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) {
|
public JacksonDeserializer(ObjectMapper objectMapper) {
|
||||||
this(objectMapper, (Class<T>) Object.class);
|
this(objectMapper, (Class<T>) Object.class);
|
||||||
}
|
}
|
||||||
|
@ -99,24 +100,8 @@ public class JacksonDeserializer<T> implements Deserializer<T> {
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public T deserialize(byte[] bytes) throws DeserializationException {
|
protected T doDeserialize(InputStream in) throws Exception {
|
||||||
try {
|
return objectMapper.readValue(in, returnType);
|
||||||
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);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -138,6 +123,7 @@ public class JacksonDeserializer<T> implements Deserializer<T> {
|
||||||
String name = parser.currentName();
|
String name = parser.currentName();
|
||||||
if (claimTypeMap != null && name != null && claimTypeMap.containsKey(name)) {
|
if (claimTypeMap != null && name != null && claimTypeMap.containsKey(name)) {
|
||||||
Class<?> type = claimTypeMap.get(name);
|
Class<?> type = claimTypeMap.get(name);
|
||||||
|
//noinspection resource
|
||||||
return parser.readValueAsTree().traverse(parser.getCodec()).readValueAs(type);
|
return parser.readValueAsTree().traverse(parser.getCodec()).readValueAs(type);
|
||||||
}
|
}
|
||||||
// otherwise default to super
|
// otherwise default to super
|
||||||
|
|
|
@ -15,20 +15,22 @@
|
||||||
*/
|
*/
|
||||||
package io.jsonwebtoken.jackson.io;
|
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.Module;
|
||||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||||
|
import com.fasterxml.jackson.databind.ObjectWriter;
|
||||||
import com.fasterxml.jackson.databind.module.SimpleModule;
|
import com.fasterxml.jackson.databind.module.SimpleModule;
|
||||||
import io.jsonwebtoken.io.SerializationException;
|
import io.jsonwebtoken.io.AbstractSerializer;
|
||||||
import io.jsonwebtoken.io.Serializer;
|
|
||||||
import io.jsonwebtoken.lang.Assert;
|
import io.jsonwebtoken.lang.Assert;
|
||||||
|
|
||||||
|
import java.io.OutputStream;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Serializer using a Jackson {@link ObjectMapper}.
|
* Serializer using a Jackson {@link ObjectMapper}.
|
||||||
*
|
*
|
||||||
* @since 0.10.0
|
* @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 String MODULE_ID = "jjwt-jackson";
|
||||||
static final Module MODULE;
|
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);
|
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.
|
* Constructor using JJWT's default {@link ObjectMapper} singleton for serialization.
|
||||||
*/
|
*/
|
||||||
@SuppressWarnings("unused") //used via reflection by RuntimeClasspathDeserializerLocator
|
|
||||||
public JacksonSerializer() {
|
public JacksonSerializer() {
|
||||||
this(DEFAULT_OBJECT_MAPPER);
|
this(DEFAULT_OBJECT_MAPPER);
|
||||||
}
|
}
|
||||||
|
@ -56,32 +57,15 @@ public class JacksonSerializer<T> implements Serializer<T> {
|
||||||
*
|
*
|
||||||
* @param objectMapper the ObjectMapper to use for serialization.
|
* @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) {
|
public JacksonSerializer(ObjectMapper objectMapper) {
|
||||||
Assert.notNull(objectMapper, "ObjectMapper cannot be null.");
|
Assert.notNull(objectMapper, "ObjectMapper cannot be null.");
|
||||||
this.objectMapper = objectMapper.registerModule(MODULE);
|
this.objectMapper = objectMapper.registerModule(MODULE);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public byte[] serialize(T t) throws SerializationException {
|
protected void doSerialize(T t, OutputStream out) throws Exception {
|
||||||
Assert.notNull(t, "Object to serialize cannot be null.");
|
Assert.notNull(out, "OutputStream cannot be null.");
|
||||||
try {
|
ObjectWriter writer = this.objectMapper.writer().without(JsonGenerator.Feature.AUTO_CLOSE_TARGET);
|
||||||
return writeValueAsBytes(t);
|
writer.writeValue(out, 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);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -13,6 +13,7 @@
|
||||||
* See the License for the specific language governing permissions and
|
* See the License for the specific language governing permissions and
|
||||||
* limitations under the License.
|
* limitations under the License.
|
||||||
*/
|
*/
|
||||||
|
//file:noinspection GrDeprecatedAPIUsage
|
||||||
package io.jsonwebtoken.jackson.io
|
package io.jsonwebtoken.jackson.io
|
||||||
|
|
||||||
import com.fasterxml.jackson.databind.ObjectMapper
|
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.jackson.io.stubs.CustomBean
|
||||||
import io.jsonwebtoken.lang.Maps
|
import io.jsonwebtoken.lang.Maps
|
||||||
import io.jsonwebtoken.lang.Strings
|
import io.jsonwebtoken.lang.Strings
|
||||||
|
import org.junit.Before
|
||||||
import org.junit.Test
|
import org.junit.Test
|
||||||
|
|
||||||
import static org.easymock.EasyMock.*
|
|
||||||
import static org.junit.Assert.*
|
import static org.junit.Assert.*
|
||||||
import static org.hamcrest.CoreMatchers.instanceOf
|
|
||||||
|
|
||||||
class JacksonDeserializerTest {
|
class JacksonDeserializerTest {
|
||||||
|
|
||||||
|
private JacksonDeserializer deserializer
|
||||||
|
|
||||||
|
@Before
|
||||||
|
void setUp() {
|
||||||
|
deserializer = new JacksonDeserializer()
|
||||||
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
void loadService() {
|
void loadService() {
|
||||||
def deserializer = ServiceLoader.load(Deserializer).iterator().next()
|
def deserializer = ServiceLoader.load(Deserializer).iterator().next()
|
||||||
assertThat(deserializer, instanceOf(JacksonDeserializer))
|
assertTrue deserializer instanceof JacksonDeserializer
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
void testDefaultConstructor() {
|
void testDefaultConstructor() {
|
||||||
def deserializer = new JacksonDeserializer()
|
assertSame JacksonSerializer.DEFAULT_OBJECT_MAPPER, deserializer.objectMapper
|
||||||
assertNotNull deserializer.objectMapper
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
void testObjectMapperConstructor() {
|
void testObjectMapperConstructor() {
|
||||||
def customOM = new ObjectMapper()
|
def customOM = new ObjectMapper()
|
||||||
def deserializer = new JacksonDeserializer(customOM)
|
deserializer = new JacksonDeserializer<>(customOM)
|
||||||
assertSame customOM, deserializer.objectMapper
|
assertSame customOM, deserializer.objectMapper
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -55,9 +62,9 @@ class JacksonDeserializerTest {
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
void testDeserialize() {
|
void testDeserialize() {
|
||||||
byte[] serialized = '{"hello":"世界"}'.getBytes(Strings.UTF_8)
|
byte[] data = Strings.utf8('{"hello":"世界"}')
|
||||||
def expected = [hello: '世界']
|
def expected = [hello: '世界']
|
||||||
def result = new JacksonDeserializer().deserialize(serialized)
|
def result = deserializer.deserialize(new ByteArrayInputStream(data))
|
||||||
assertEquals expected, result
|
assertEquals expected, result
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -66,7 +73,8 @@ class JacksonDeserializerTest {
|
||||||
|
|
||||||
long currentTime = System.currentTimeMillis()
|
long currentTime = System.currentTimeMillis()
|
||||||
|
|
||||||
byte[] serialized = """{
|
String json = """
|
||||||
|
{
|
||||||
"oneKey":"oneValue",
|
"oneKey":"oneValue",
|
||||||
"custom": {
|
"custom": {
|
||||||
"stringValue": "s-value",
|
"stringValue": "s-value",
|
||||||
|
@ -87,28 +95,31 @@ class JacksonDeserializerTest {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
""".getBytes(Strings.UTF_8)
|
"""
|
||||||
|
|
||||||
|
byte[] serialized = Strings.utf8(json)
|
||||||
|
|
||||||
CustomBean expectedCustomBean = new CustomBean()
|
CustomBean expectedCustomBean = new CustomBean()
|
||||||
.setByteArrayValue("bytes".getBytes("UTF-8"))
|
.setByteArrayValue("bytes".getBytes("UTF-8"))
|
||||||
.setByteValue(0xF as byte)
|
.setByteValue(0xF as byte)
|
||||||
.setDateValue(new Date(currentTime))
|
.setDateValue(new Date(currentTime))
|
||||||
.setIntValue(11)
|
.setIntValue(11)
|
||||||
.setShortValue(22 as short)
|
.setShortValue(22 as short)
|
||||||
.setLongValue(33L)
|
.setLongValue(33L)
|
||||||
.setStringValue("s-value")
|
.setStringValue("s-value")
|
||||||
.setNestedValue(new CustomBean()
|
.setNestedValue(new CustomBean()
|
||||||
.setByteArrayValue("bytes2".getBytes("UTF-8"))
|
.setByteArrayValue("bytes2".getBytes("UTF-8"))
|
||||||
.setByteValue(0xA as byte)
|
.setByteValue(0xA as byte)
|
||||||
.setDateValue(new Date(currentTime+1))
|
.setDateValue(new Date(currentTime + 1))
|
||||||
.setIntValue(111)
|
.setIntValue(111)
|
||||||
.setShortValue(222 as short)
|
.setShortValue(222 as short)
|
||||||
.setLongValue(333L)
|
.setLongValue(333L)
|
||||||
.setStringValue("nested-value")
|
.setStringValue("nested-value")
|
||||||
)
|
)
|
||||||
|
|
||||||
def expected = [oneKey: "oneValue", custom: expectedCustomBean]
|
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
|
assertEquals expected, result
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -143,7 +154,8 @@ class JacksonDeserializerTest {
|
||||||
typeMap.put("custom", CustomBean)
|
typeMap.put("custom", CustomBean)
|
||||||
|
|
||||||
def deserializer = new JacksonDeserializer(typeMap)
|
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)
|
assertEquals(["alg": "HS256"], result)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -153,33 +165,27 @@ class JacksonDeserializerTest {
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
void testDeserializeFailsWithJsonProcessingException() {
|
void testDeserializeFailsWithException() {
|
||||||
|
|
||||||
def ex = createMock(java.io.IOException)
|
def ex = new IOException('foo')
|
||||||
|
|
||||||
expect(ex.getMessage()).andReturn('foo')
|
deserializer = new JacksonDeserializer() {
|
||||||
|
|
||||||
def deserializer = new JacksonDeserializer() {
|
|
||||||
@Override
|
@Override
|
||||||
protected Object readValue(byte[] bytes) throws java.io.IOException {
|
protected Object doDeserialize(InputStream inputStream) throws Exception {
|
||||||
throw ex
|
throw ex
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
replay ex
|
|
||||||
|
|
||||||
try {
|
try {
|
||||||
deserializer.deserialize('{"hello":"世界"}'.getBytes(Strings.UTF_8))
|
deserializer.deserialize(new ByteArrayInputStream(Strings.utf8('{"hello":"世界"}')))
|
||||||
fail()
|
fail()
|
||||||
} catch (DeserializationException se) {
|
} 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()
|
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'))
|
return Encoders.BASE64.encode(input.getBytes('UTF-8'))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -15,36 +15,46 @@
|
||||||
*/
|
*/
|
||||||
package io.jsonwebtoken.jackson.io
|
package io.jsonwebtoken.jackson.io
|
||||||
|
|
||||||
import com.fasterxml.jackson.core.JsonProcessingException
|
|
||||||
import com.fasterxml.jackson.databind.ObjectMapper
|
import com.fasterxml.jackson.databind.ObjectMapper
|
||||||
import io.jsonwebtoken.io.SerializationException
|
|
||||||
import io.jsonwebtoken.io.Serializer
|
import io.jsonwebtoken.io.Serializer
|
||||||
import io.jsonwebtoken.lang.Strings
|
import io.jsonwebtoken.lang.Strings
|
||||||
|
import org.junit.Before
|
||||||
import org.junit.Test
|
import org.junit.Test
|
||||||
|
|
||||||
import static org.easymock.EasyMock.*
|
import static org.easymock.EasyMock.*
|
||||||
import static org.hamcrest.CoreMatchers.instanceOf
|
|
||||||
import static org.junit.Assert.*
|
import static org.junit.Assert.*
|
||||||
|
|
||||||
class JacksonSerializerTest {
|
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
|
@Test
|
||||||
void loadService() {
|
void loadService() {
|
||||||
def serializer = ServiceLoader.load(Serializer).iterator().next()
|
def serializer = ServiceLoader.load(Serializer).iterator().next()
|
||||||
assertThat(serializer, instanceOf(JacksonSerializer))
|
assertTrue serializer instanceof JacksonSerializer
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
void testDefaultConstructor() {
|
void testDefaultConstructor() {
|
||||||
def serializer = new JacksonSerializer()
|
assertSame JacksonSerializer.DEFAULT_OBJECT_MAPPER, ser.objectMapper
|
||||||
assertNotNull serializer.objectMapper
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
void testObjectMapperConstructor() {
|
void testObjectMapperConstructor() {
|
||||||
def customOM = new ObjectMapper()
|
ObjectMapper customOM = new ObjectMapper()
|
||||||
def serializer = new JacksonSerializer<>(customOM)
|
ser = new JacksonSerializer(customOM)
|
||||||
assertSame customOM, serializer.objectMapper
|
assertSame customOM, ser.objectMapper
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test(expected = IllegalArgumentException)
|
@Test(expected = IllegalArgumentException)
|
||||||
|
@ -54,79 +64,58 @@ class JacksonSerializerTest {
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
void testObjectMapperConstructorAutoRegistersModule() {
|
void testObjectMapperConstructorAutoRegistersModule() {
|
||||||
def om = createMock(ObjectMapper)
|
ObjectMapper om = createMock(ObjectMapper)
|
||||||
expect(om.registerModule(same(JacksonSerializer.MODULE))).andReturn(om)
|
expect(om.registerModule(same(JacksonSerializer.MODULE))).andReturn(om)
|
||||||
replay om
|
replay om
|
||||||
def serializer = new JacksonSerializer<>(om)
|
//noinspection GroovyResultOfObjectAllocationIgnored
|
||||||
|
new JacksonSerializer<>(om)
|
||||||
verify 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
|
@Test
|
||||||
void testSerialize() {
|
void testSerialize() {
|
||||||
byte[] expected = '{"hello":"世界"}'.getBytes(Strings.UTF_8)
|
byte[] expected = '{"hello":"世界"}'.getBytes(Strings.UTF_8)
|
||||||
byte[] result = new JacksonSerializer().serialize([hello: '世界'])
|
byte[] result = ser.serialize([hello: '世界'])
|
||||||
assertTrue Arrays.equals(expected, result)
|
assertTrue Arrays.equals(expected, result)
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@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() {
|
@Test
|
||||||
@Override
|
void testChar() { //expect Base64 string by default:
|
||||||
protected byte[] writeValueAsBytes(Object o) throws JsonProcessingException {
|
byte[] result = serialize('h' as char)
|
||||||
throw ex
|
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 {
|
@Test
|
||||||
serializer.serialize([hello: 'world'])
|
void testWriteObject() {
|
||||||
fail()
|
byte[] expected = Strings.utf8('{"hello":"世界"}' as String)
|
||||||
} catch (SerializationException se) {
|
byte[] result = serialize([hello: '世界'])
|
||||||
assertEquals 'Unable to serialize object: foo', se.getMessage()
|
assertArrayEquals expected, result
|
||||||
assertSame ex, se.getCause()
|
|
||||||
}
|
|
||||||
|
|
||||||
verify ex
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -15,11 +15,10 @@
|
||||||
*/
|
*/
|
||||||
package io.jsonwebtoken.jackson.io
|
package io.jsonwebtoken.jackson.io
|
||||||
|
|
||||||
|
import io.jsonwebtoken.lang.Strings
|
||||||
import io.jsonwebtoken.lang.Supplier
|
import io.jsonwebtoken.lang.Supplier
|
||||||
import org.junit.Test
|
import org.junit.Test
|
||||||
|
|
||||||
import java.nio.charset.StandardCharsets
|
|
||||||
|
|
||||||
import static org.junit.Assert.assertEquals
|
import static org.junit.Assert.assertEquals
|
||||||
|
|
||||||
class JacksonSupplierSerializerTest {
|
class JacksonSupplierSerializerTest {
|
||||||
|
@ -33,7 +32,22 @@ class JacksonSupplierSerializerTest {
|
||||||
return null
|
return null
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
byte[] bytes = serializer.serialize(supplier)
|
ByteArrayOutputStream out = new ByteArrayOutputStream()
|
||||||
assertEquals 'null', new String(bytes, StandardCharsets.UTF_8)
|
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())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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;
|
||||||
|
}
|
||||||
|
}
|
|
@ -131,16 +131,16 @@ class CustomBean {
|
||||||
|
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public String toString() {
|
String toString() {
|
||||||
return "CustomBean{" +
|
return "CustomBean{" +
|
||||||
"stringValue='" + stringValue + '\'' +
|
"stringValue='" + stringValue + '\'' +
|
||||||
", intValue=" + intValue +
|
", intValue=" + intValue +
|
||||||
", dateValue=" + dateValue?.time+
|
", dateValue=" + dateValue?.time +
|
||||||
", shortValue=" + shortValue +
|
", shortValue=" + shortValue +
|
||||||
", longValue=" + longValue +
|
", longValue=" + longValue +
|
||||||
", byteValue=" + byteValue +
|
", byteValue=" + byteValue +
|
||||||
// ", byteArrayValue=" + Arrays.toString(byteArrayValue) +
|
// ", byteArrayValue=" + Arrays.toString(byteArrayValue) +
|
||||||
", nestedValue=" + nestedValue +
|
", nestedValue=" + nestedValue +
|
||||||
'}';
|
'}'
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -15,15 +15,16 @@
|
||||||
*/
|
*/
|
||||||
package io.jsonwebtoken.orgjson.io;
|
package io.jsonwebtoken.orgjson.io;
|
||||||
|
|
||||||
import io.jsonwebtoken.io.DeserializationException;
|
import io.jsonwebtoken.io.AbstractDeserializer;
|
||||||
import io.jsonwebtoken.io.Deserializer;
|
|
||||||
import io.jsonwebtoken.lang.Assert;
|
|
||||||
import io.jsonwebtoken.lang.Strings;
|
|
||||||
import org.json.JSONArray;
|
import org.json.JSONArray;
|
||||||
import org.json.JSONException;
|
import org.json.JSONException;
|
||||||
import org.json.JSONObject;
|
import org.json.JSONObject;
|
||||||
import org.json.JSONTokener;
|
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.ArrayList;
|
||||||
import java.util.Iterator;
|
import java.util.Iterator;
|
||||||
import java.util.LinkedHashMap;
|
import java.util.LinkedHashMap;
|
||||||
|
@ -33,29 +34,17 @@ import java.util.Map;
|
||||||
/**
|
/**
|
||||||
* @since 0.10.0
|
* @since 0.10.0
|
||||||
*/
|
*/
|
||||||
public class OrgJsonDeserializer implements Deserializer<Object> {
|
public class OrgJsonDeserializer extends AbstractDeserializer<Object> {
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public Object deserialize(byte[] bytes) throws DeserializationException {
|
protected Object doDeserialize(InputStream in) {
|
||||||
|
Reader reader = new InputStreamReader(in, StandardCharsets.UTF_8);
|
||||||
Assert.notNull(bytes, "JSON byte array cannot be null");
|
return parse(reader);
|
||||||
|
|
||||||
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);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
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
|
char c = tokener.nextClean(); //peak ahead
|
||||||
tokener.back(); //revert
|
tokener.back(); //revert
|
||||||
|
|
|
@ -15,9 +15,8 @@
|
||||||
*/
|
*/
|
||||||
package io.jsonwebtoken.orgjson.io;
|
package io.jsonwebtoken.orgjson.io;
|
||||||
|
|
||||||
|
import io.jsonwebtoken.io.AbstractSerializer;
|
||||||
import io.jsonwebtoken.io.Encoders;
|
import io.jsonwebtoken.io.Encoders;
|
||||||
import io.jsonwebtoken.io.SerializationException;
|
|
||||||
import io.jsonwebtoken.io.Serializer;
|
|
||||||
import io.jsonwebtoken.lang.Classes;
|
import io.jsonwebtoken.lang.Classes;
|
||||||
import io.jsonwebtoken.lang.Collections;
|
import io.jsonwebtoken.lang.Collections;
|
||||||
import io.jsonwebtoken.lang.DateFormats;
|
import io.jsonwebtoken.lang.DateFormats;
|
||||||
|
@ -27,6 +26,8 @@ import io.jsonwebtoken.lang.Supplier;
|
||||||
import org.json.JSONArray;
|
import org.json.JSONArray;
|
||||||
import org.json.JSONObject;
|
import org.json.JSONObject;
|
||||||
|
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.io.OutputStream;
|
||||||
import java.math.BigDecimal;
|
import java.math.BigDecimal;
|
||||||
import java.math.BigInteger;
|
import java.math.BigInteger;
|
||||||
import java.util.Calendar;
|
import java.util.Calendar;
|
||||||
|
@ -37,7 +38,7 @@ import java.util.Map;
|
||||||
/**
|
/**
|
||||||
* @since 0.10.0
|
* @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
|
// 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";
|
private static final String JSON_WRITER_CLASS_NAME = "org.json.JSONWriter";
|
||||||
|
@ -54,17 +55,11 @@ public class OrgJsonSerializer<T> implements Serializer<T> {
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public byte[] serialize(T t) throws SerializationException {
|
protected void doSerialize(T t, OutputStream out) throws Exception {
|
||||||
try {
|
Object o = toJSONInstance(t);
|
||||||
Object o = toJSONInstance(t);
|
String s = toString(o);
|
||||||
return toBytes(o);
|
byte[] bytes = Strings.utf8(s);
|
||||||
} catch (SerializationException se) {
|
out.write(bytes);
|
||||||
//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);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -77,7 +72,7 @@ public class OrgJsonSerializer<T> implements Serializer<T> {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
private Object toJSONInstance(Object object) {
|
private Object toJSONInstance(Object object) throws IOException {
|
||||||
|
|
||||||
if (object == null) {
|
if (object == null) {
|
||||||
return JSONObject.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
|
//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:
|
//directly without using a marshaller of some sort:
|
||||||
String msg = "Unable to serialize object of type " + object.getClass().getName() + " to JSON using known heuristics.";
|
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();
|
JSONObject obj = new JSONObject();
|
||||||
|
|
||||||
|
@ -151,7 +146,7 @@ public class OrgJsonSerializer<T> implements Serializer<T> {
|
||||||
return obj;
|
return obj;
|
||||||
}
|
}
|
||||||
|
|
||||||
private JSONArray toJSONArray(Collection<?> c) {
|
private JSONArray toJSONArray(Collection<?> c) throws IOException {
|
||||||
|
|
||||||
JSONArray array = new JSONArray();
|
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
|
* @param o the org.json instance to convert to a String
|
||||||
* @return the JSON byte array
|
* @return the JSON String
|
||||||
*/
|
*/
|
||||||
@SuppressWarnings("WeakerAccess") //for testing
|
protected String toString(Object o) {
|
||||||
protected byte[] toBytes(Object o) {
|
|
||||||
String s;
|
|
||||||
// https://github.com/jwtk/jjwt/issues/380 for Android compatibility (Android doesn't have org.json.JSONWriter):
|
// 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>
|
// 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
|
// 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
|
// 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.
|
// is within the context of JwtBuilder execution and not for application use beyond that.
|
||||||
if (o instanceof JSONObject) {
|
if (o instanceof JSONObject) {
|
||||||
s = o.toString();
|
return 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 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);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -13,98 +13,79 @@
|
||||||
* See the License for the specific language governing permissions and
|
* See the License for the specific language governing permissions and
|
||||||
* limitations under the License.
|
* limitations under the License.
|
||||||
*/
|
*/
|
||||||
|
//file:noinspection GrDeprecatedAPIUsage
|
||||||
package io.jsonwebtoken.orgjson.io
|
package io.jsonwebtoken.orgjson.io
|
||||||
|
|
||||||
import io.jsonwebtoken.io.DeserializationException
|
import io.jsonwebtoken.io.DeserializationException
|
||||||
import io.jsonwebtoken.io.Deserializer
|
import io.jsonwebtoken.io.Deserializer
|
||||||
import io.jsonwebtoken.lang.Strings
|
import io.jsonwebtoken.lang.Strings
|
||||||
|
import org.junit.Before
|
||||||
import org.junit.Test
|
import org.junit.Test
|
||||||
|
|
||||||
import static org.hamcrest.CoreMatchers.instanceOf
|
|
||||||
import static org.junit.Assert.*
|
import static org.junit.Assert.*
|
||||||
|
|
||||||
class OrgJsonDeserializerTest {
|
class OrgJsonDeserializerTest {
|
||||||
|
|
||||||
@Test
|
private OrgJsonDeserializer des
|
||||||
void loadService() {
|
|
||||||
def deserializer = ServiceLoader.load(Deserializer).iterator().next()
|
private Object fromBytes(byte[] data) {
|
||||||
assertThat(deserializer, instanceOf(OrgJsonDeserializer))
|
return des.deserialize(new ByteArrayInputStream(data))
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test(expected=IllegalArgumentException)
|
private Object read(String s) {
|
||||||
|
return fromBytes(Strings.utf8(s))
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test(expected = IllegalArgumentException)
|
||||||
void testNullArgument() {
|
void testNullArgument() {
|
||||||
def d = new OrgJsonDeserializer()
|
des.deserialize((InputStream) null)
|
||||||
d.deserialize(null)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test(expected = DeserializationException)
|
@Test(expected = DeserializationException)
|
||||||
void testEmptyByteArray() {
|
void testEmptyByteArray() {
|
||||||
def d = new OrgJsonDeserializer()
|
fromBytes(new byte[0])
|
||||||
d.deserialize(new byte[0])
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test(expected = DeserializationException)
|
@Test(expected = DeserializationException)
|
||||||
void testInvalidJson() {
|
void testInvalidJson() {
|
||||||
def d = new OrgJsonDeserializer()
|
read('"')
|
||||||
d.deserialize('"'.getBytes(Strings.UTF_8))
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
void testLiteralNull() {
|
void testLiteralNull() {
|
||||||
def d = new OrgJsonDeserializer();
|
assertNull read('null')
|
||||||
def b = 'null'.getBytes(Strings.UTF_8)
|
|
||||||
def value = d.deserialize(b)
|
|
||||||
assertNull value
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
void testLiteralTrue() {
|
void testLiteralTrue() {
|
||||||
def d = new OrgJsonDeserializer();
|
assertTrue read('true') as boolean
|
||||||
def b = 'true'.getBytes(Strings.UTF_8)
|
|
||||||
def value = d.deserialize(b)
|
|
||||||
assertEquals Boolean.TRUE, value
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
void testLiteralFalse() {
|
void testLiteralFalse() {
|
||||||
def d = new OrgJsonDeserializer();
|
assertFalse read('false') as boolean
|
||||||
def b = 'false'.getBytes(Strings.UTF_8)
|
|
||||||
def value = d.deserialize(b)
|
|
||||||
assertEquals Boolean.FALSE, value
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
void testLiteralInteger() {
|
void testLiteralInteger() {
|
||||||
def d = new OrgJsonDeserializer();
|
assertEquals 1 as Integer, read('1')
|
||||||
def b = '1'.getBytes(Strings.UTF_8)
|
|
||||||
def value = d.deserialize(b)
|
|
||||||
assert value instanceof Integer
|
|
||||||
assertEquals 1 as Integer, value
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
void testLiteralDecimal() {
|
void testLiteralDecimal() {
|
||||||
def d = new OrgJsonDeserializer();
|
assertEquals 3.14159 as Double, read('3.14159') as BigDecimal, 0d
|
||||||
def b = '3.14159'.getBytes(Strings.UTF_8)
|
|
||||||
def value = d.deserialize(b)
|
|
||||||
assert value instanceof BigDecimal
|
|
||||||
assertEquals 3.14159 as Double, value, 0d
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
void testEmptyArray() {
|
void testEmptyArray() {
|
||||||
def d = new OrgJsonDeserializer();
|
def value = read('[]')
|
||||||
def b = '[]'.getBytes(Strings.UTF_8)
|
|
||||||
def value = d.deserialize(b)
|
|
||||||
assert value instanceof List
|
assert value instanceof List
|
||||||
assertEquals 0, value.size()
|
assertEquals 0, value.size()
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
void testSimpleArray() {
|
void testSimpleArray() {
|
||||||
def d = new OrgJsonDeserializer();
|
def value = read('[1, 2]')
|
||||||
def b = '[1, 2]'.getBytes(Strings.UTF_8)
|
|
||||||
def value = d.deserialize(b)
|
|
||||||
assert value instanceof List
|
assert value instanceof List
|
||||||
def expected = [1, 2]
|
def expected = [1, 2]
|
||||||
assertEquals expected, value
|
assertEquals expected, value
|
||||||
|
@ -112,9 +93,7 @@ class OrgJsonDeserializerTest {
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
void testArrayWithNullElements() {
|
void testArrayWithNullElements() {
|
||||||
def d = new OrgJsonDeserializer();
|
def value = read('[1, null, 3]')
|
||||||
def b = '[1, null, 3]'.getBytes(Strings.UTF_8)
|
|
||||||
def value = d.deserialize(b)
|
|
||||||
assert value instanceof List
|
assert value instanceof List
|
||||||
def expected = [1, null, 3]
|
def expected = [1, null, 3]
|
||||||
assertEquals expected, value
|
assertEquals expected, value
|
||||||
|
@ -122,18 +101,14 @@ class OrgJsonDeserializerTest {
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
void testEmptyObject() {
|
void testEmptyObject() {
|
||||||
def d = new OrgJsonDeserializer();
|
def value = read('{}')
|
||||||
def b = '{}'.getBytes(Strings.UTF_8)
|
|
||||||
def value = d.deserialize(b)
|
|
||||||
assert value instanceof Map
|
assert value instanceof Map
|
||||||
assertEquals 0, value.size()
|
assertEquals 0, value.size()
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
void testSimpleObject() {
|
void testSimpleObject() {
|
||||||
def d = new OrgJsonDeserializer();
|
def value = read('{"hello": "世界"}')
|
||||||
def b = '{"hello": "世界"}'.getBytes(Strings.UTF_8)
|
|
||||||
def value = d.deserialize(b)
|
|
||||||
assert value instanceof Map
|
assert value instanceof Map
|
||||||
def expected = [hello: '世界']
|
def expected = [hello: '世界']
|
||||||
assertEquals expected, value
|
assertEquals expected, value
|
||||||
|
@ -141,9 +116,7 @@ class OrgJsonDeserializerTest {
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
void testObjectWithKeyHavingNullValue() {
|
void testObjectWithKeyHavingNullValue() {
|
||||||
def d = new OrgJsonDeserializer();
|
def value = read('{"hello": "世界", "test": null}')
|
||||||
def b = '{"hello": "世界", "test": null}'.getBytes(Strings.UTF_8)
|
|
||||||
def value = d.deserialize(b)
|
|
||||||
assert value instanceof Map
|
assert value instanceof Map
|
||||||
def expected = [hello: '世界', test: null]
|
def expected = [hello: '世界', test: null]
|
||||||
assertEquals expected, value
|
assertEquals expected, value
|
||||||
|
@ -151,9 +124,7 @@ class OrgJsonDeserializerTest {
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
void testObjectWithKeyHavingArrayValue() {
|
void testObjectWithKeyHavingArrayValue() {
|
||||||
def d = new OrgJsonDeserializer();
|
def value = read('{"hello": "世界", "test": [1, 2]}')
|
||||||
def b = '{"hello": "世界", "test": [1, 2]}'.getBytes(Strings.UTF_8)
|
|
||||||
def value = d.deserialize(b)
|
|
||||||
assert value instanceof Map
|
assert value instanceof Map
|
||||||
def expected = [hello: '世界', test: [1, 2]]
|
def expected = [hello: '世界', test: [1, 2]]
|
||||||
assertEquals expected, value
|
assertEquals expected, value
|
||||||
|
@ -161,11 +132,58 @@ class OrgJsonDeserializerTest {
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
void testObjectWithKeyHavingObjectValue() {
|
void testObjectWithKeyHavingObjectValue() {
|
||||||
def d = new OrgJsonDeserializer();
|
def value = read('{"hello": "世界", "test": {"foo": "bar"}}')
|
||||||
def b = '{"hello": "世界", "test": {"foo": "bar"}}'.getBytes(Strings.UTF_8)
|
|
||||||
def value = d.deserialize(b)
|
|
||||||
assert value instanceof Map
|
assert value instanceof Map
|
||||||
def expected = [hello: '世界', test: [foo: 'bar']]
|
def expected = [hello: '世界', test: [foo: 'bar']]
|
||||||
assertEquals expected, value
|
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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -13,6 +13,7 @@
|
||||||
* See the License for the specific language governing permissions and
|
* See the License for the specific language governing permissions and
|
||||||
* limitations under the License.
|
* limitations under the License.
|
||||||
*/
|
*/
|
||||||
|
//file:noinspection GrDeprecatedAPIUsage
|
||||||
package io.jsonwebtoken.orgjson.io
|
package io.jsonwebtoken.orgjson.io
|
||||||
|
|
||||||
import io.jsonwebtoken.SignatureAlgorithm
|
import io.jsonwebtoken.SignatureAlgorithm
|
||||||
|
@ -20,13 +21,13 @@ import io.jsonwebtoken.io.SerializationException
|
||||||
import io.jsonwebtoken.io.Serializer
|
import io.jsonwebtoken.io.Serializer
|
||||||
import io.jsonwebtoken.lang.DateFormats
|
import io.jsonwebtoken.lang.DateFormats
|
||||||
import io.jsonwebtoken.lang.Strings
|
import io.jsonwebtoken.lang.Strings
|
||||||
|
import io.jsonwebtoken.lang.Supplier
|
||||||
import org.json.JSONObject
|
import org.json.JSONObject
|
||||||
import org.json.JSONString
|
import org.json.JSONString
|
||||||
import org.junit.Before
|
import org.junit.Before
|
||||||
import org.junit.Test
|
import org.junit.Test
|
||||||
|
|
||||||
import static org.junit.Assert.*
|
import static org.junit.Assert.*
|
||||||
import static org.hamcrest.CoreMatchers.instanceOf
|
|
||||||
|
|
||||||
class OrgJsonSerializerTest {
|
class OrgJsonSerializerTest {
|
||||||
|
|
||||||
|
@ -38,38 +39,24 @@ class OrgJsonSerializerTest {
|
||||||
}
|
}
|
||||||
|
|
||||||
private String ser(Object o) {
|
private String ser(Object o) {
|
||||||
byte[] bytes = s.serialize(o)
|
return Strings.utf8(s.serialize(o))
|
||||||
return new String(bytes, Strings.UTF_8)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
void loadService() {
|
void loadService() {
|
||||||
def serializer = ServiceLoader.load(Serializer).iterator().next()
|
def serializer = ServiceLoader.load(Serializer).iterator().next()
|
||||||
assertThat(serializer, instanceOf(OrgJsonSerializer))
|
assertTrue serializer instanceof OrgJsonSerializer
|
||||||
}
|
|
||||||
|
|
||||||
@Test(expected = SerializationException)
|
|
||||||
void testInvalidArgument() {
|
|
||||||
s.serialize(new Object())
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
void testToBytesFailure() {
|
void testInvalidArgument() {
|
||||||
|
|
||||||
final IllegalArgumentException iae = new IllegalArgumentException("foo")
|
|
||||||
|
|
||||||
s = new OrgJsonSerializer() {
|
|
||||||
@Override
|
|
||||||
protected byte[] toBytes(Object o) {
|
|
||||||
throw iae
|
|
||||||
}
|
|
||||||
}
|
|
||||||
try {
|
try {
|
||||||
s.serialize("hello")
|
ser(new Object())
|
||||||
fail()
|
fail()
|
||||||
} catch (SerializationException se) {
|
} catch (SerializationException expected) {
|
||||||
assertTrue se.getMessage().endsWith(iae.getMessage())
|
String causeMsg = 'Unable to serialize object of type java.lang.Object to JSON using known heuristics.'
|
||||||
assertSame iae, se.getCause()
|
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)
|
assertEquals '"HS256"', ser(SignatureAlgorithm.HS256)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void testSupplier() {
|
||||||
|
def supplier = new Supplier() {
|
||||||
|
@Override
|
||||||
|
Object get() {
|
||||||
|
return 'test'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
assertEquals '"test"', ser(supplier)
|
||||||
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
void testEmptyString() {
|
void testEmptyString() {
|
||||||
assertEquals '""', ser('')
|
assertEquals '""', ser('')
|
||||||
|
@ -276,4 +274,28 @@ class OrgJsonSerializerTest {
|
||||||
void testListWithNestedObject() {
|
void testListWithNestedObject() {
|
||||||
assertEquals '[1,null,{"hello":"世界"}]', ser([1, null, [hello: '世界']])
|
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'))
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -62,7 +62,7 @@ public class DefaultClaims extends ParameterMap implements Claims {
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public String getName() {
|
public String getName() {
|
||||||
return "JWT Claim";
|
return "JWT Claims";
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
|
|
|
@ -21,12 +21,20 @@ import io.jsonwebtoken.JweHeader;
|
||||||
import io.jsonwebtoken.JwsHeader;
|
import io.jsonwebtoken.JwsHeader;
|
||||||
import io.jsonwebtoken.JwtBuilder;
|
import io.jsonwebtoken.JwtBuilder;
|
||||||
import io.jsonwebtoken.Jwts;
|
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.Bytes;
|
||||||
import io.jsonwebtoken.impl.lang.Function;
|
import io.jsonwebtoken.impl.lang.Function;
|
||||||
import io.jsonwebtoken.impl.lang.Functions;
|
import io.jsonwebtoken.impl.lang.Functions;
|
||||||
import io.jsonwebtoken.impl.lang.Parameter;
|
import io.jsonwebtoken.impl.lang.Parameter;
|
||||||
import io.jsonwebtoken.impl.lang.Services;
|
import io.jsonwebtoken.impl.lang.Services;
|
||||||
import io.jsonwebtoken.impl.security.DefaultAeadRequest;
|
import io.jsonwebtoken.impl.security.DefaultAeadRequest;
|
||||||
|
import io.jsonwebtoken.impl.security.DefaultAeadResult;
|
||||||
import io.jsonwebtoken.impl.security.DefaultKeyRequest;
|
import io.jsonwebtoken.impl.security.DefaultKeyRequest;
|
||||||
import io.jsonwebtoken.impl.security.DefaultSecureRequest;
|
import io.jsonwebtoken.impl.security.DefaultSecureRequest;
|
||||||
import io.jsonwebtoken.impl.security.Pbes2HsAkwAlgorithm;
|
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.CompressionAlgorithm;
|
||||||
import io.jsonwebtoken.io.Decoders;
|
import io.jsonwebtoken.io.Decoders;
|
||||||
import io.jsonwebtoken.io.Encoder;
|
import io.jsonwebtoken.io.Encoder;
|
||||||
import io.jsonwebtoken.io.Encoders;
|
|
||||||
import io.jsonwebtoken.io.SerializationException;
|
|
||||||
import io.jsonwebtoken.io.Serializer;
|
import io.jsonwebtoken.io.Serializer;
|
||||||
import io.jsonwebtoken.lang.Assert;
|
import io.jsonwebtoken.lang.Assert;
|
||||||
import io.jsonwebtoken.lang.Collections;
|
import io.jsonwebtoken.lang.Collections;
|
||||||
|
import io.jsonwebtoken.lang.Objects;
|
||||||
import io.jsonwebtoken.lang.Strings;
|
import io.jsonwebtoken.lang.Strings;
|
||||||
import io.jsonwebtoken.security.AeadAlgorithm;
|
import io.jsonwebtoken.security.AeadAlgorithm;
|
||||||
import io.jsonwebtoken.security.AeadRequest;
|
import io.jsonwebtoken.security.AeadRequest;
|
||||||
|
@ -57,7 +64,11 @@ import io.jsonwebtoken.security.UnsupportedKeyException;
|
||||||
|
|
||||||
import javax.crypto.SecretKey;
|
import javax.crypto.SecretKey;
|
||||||
import javax.crypto.spec.SecretKeySpec;
|
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.Key;
|
||||||
import java.security.PrivateKey;
|
import java.security.PrivateKey;
|
||||||
import java.security.Provider;
|
import java.security.Provider;
|
||||||
|
@ -71,10 +82,11 @@ import java.util.Set;
|
||||||
|
|
||||||
public class DefaultJwtBuilder implements JwtBuilder {
|
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 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 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 Provider provider;
|
||||||
protected SecureRandom secureRandom;
|
protected SecureRandom secureRandom;
|
||||||
|
@ -85,21 +97,18 @@ public class DefaultJwtBuilder implements JwtBuilder {
|
||||||
private Payload payload = Payload.EMPTY;
|
private Payload payload = Payload.EMPTY;
|
||||||
|
|
||||||
private SecureDigestAlgorithm<Key, ?> sigAlg = Jwts.SIG.NONE;
|
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 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 KeyAlgorithm<Key, ?> keyAlg;
|
||||||
private Function<KeyRequest<Key>, KeyResult> keyAlgFunction;
|
private Function<KeyRequest<Key>, KeyResult> keyAlgFunction;
|
||||||
|
|
||||||
private Key key;
|
private Key key;
|
||||||
|
|
||||||
protected Serializer<Map<String, ?>> serializer;
|
private Serializer<Map<String, ?>> serializer;
|
||||||
protected Function<Map<String, ?>, byte[]> headerSerializer;
|
|
||||||
protected Function<Map<String, ?>, byte[]> claimsSerializer;
|
|
||||||
|
|
||||||
protected Encoder<byte[], String> encoder = Encoders.BASE64URL;
|
protected Encoder<OutputStream, OutputStream> encoder = Base64UrlStreamEncoder.INSTANCE;
|
||||||
private boolean encodePayload = true;
|
private boolean encodePayload = true;
|
||||||
protected CompressionAlgorithm compressionAlgorithm;
|
protected CompressionAlgorithm compressionAlgorithm;
|
||||||
|
|
||||||
|
@ -130,46 +139,24 @@ public class DefaultJwtBuilder implements JwtBuilder {
|
||||||
return this;
|
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
|
@Override
|
||||||
public JwtBuilder serializeToJsonWith(final Serializer<Map<String, ?>> serializer) {
|
public JwtBuilder serializeToJsonWith(final Serializer<Map<String, ?>> serializer) {
|
||||||
return serializer(serializer);
|
return json(serializer);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public JwtBuilder serializer(Serializer<Map<String, ?>> serializer) {
|
public JwtBuilder json(Serializer<Map<String, ?>> serializer) {
|
||||||
Assert.notNull(serializer, "Serializer cannot be null.");
|
this.serializer = Assert.notNull(serializer, "JSON Serializer cannot be null.");
|
||||||
this.serializer = serializer;
|
|
||||||
this.headerSerializer = wrap(serializer, "header");
|
|
||||||
this.claimsSerializer = wrap(serializer, "claims");
|
|
||||||
return this;
|
return this;
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public JwtBuilder base64UrlEncodeWith(Encoder<byte[], String> encoder) {
|
public JwtBuilder base64UrlEncodeWith(Encoder<byte[], String> encoder) {
|
||||||
return encoder(encoder);
|
return b64Url(new ByteBase64UrlStreamEncoder(encoder));
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public JwtBuilder encoder(Encoder<byte[], String> encoder) {
|
public JwtBuilder b64Url(Encoder<OutputStream, OutputStream> encoder) {
|
||||||
Assert.notNull(encoder, "encoder cannot be null.");
|
Assert.notNull(encoder, "encoder cannot be null.");
|
||||||
this.encoder = encoder;
|
this.encoder = encoder;
|
||||||
return this;
|
return this;
|
||||||
|
@ -189,24 +176,28 @@ public class DefaultJwtBuilder implements JwtBuilder {
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public JwtBuilder setHeader(Map<String, ?> map) {
|
public JwtBuilder setHeader(Map<String, ?> map) {
|
||||||
return this.headerBuilder.empty().add(map).and();
|
return header().empty().add(map).and();
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public JwtBuilder setHeaderParams(Map<String, ?> params) {
|
public JwtBuilder setHeaderParams(Map<String, ?> params) {
|
||||||
return this.headerBuilder.add(params).and();
|
return header().add(params).and();
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public JwtBuilder setHeaderParam(String name, Object value) {
|
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) {
|
protected static <K extends Key> SecureDigestAlgorithm<K, ?> forSigningKey(K key) {
|
||||||
Assert.notNull(key, "Key cannot be null.");
|
Assert.notNull(key, "Key cannot be null.");
|
||||||
SecureDigestAlgorithm<K, ?> alg = StandardSecureDigestAlgorithms.findBySigningKey(key);
|
SecureDigestAlgorithm<K, ?> alg = StandardSecureDigestAlgorithms.findBySigningKey(key);
|
||||||
if (alg == null) {
|
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);
|
throw new UnsupportedKeyException(msg);
|
||||||
}
|
}
|
||||||
return alg;
|
return alg;
|
||||||
|
@ -220,7 +211,9 @@ public class DefaultJwtBuilder implements JwtBuilder {
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@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.");
|
Assert.notNull(key, "Key argument cannot be null.");
|
||||||
if (key instanceof PublicKey) { // it's always wrong/insecure to try to create signatures with PublicKeys:
|
if (key instanceof PublicKey) { // it's always wrong/insecure to try to create signatures with PublicKeys:
|
||||||
throw new IllegalArgumentException(PUB_KEY_SIGN_MSG);
|
throw new IllegalArgumentException(PUB_KEY_SIGN_MSG);
|
||||||
|
@ -248,9 +241,9 @@ public class DefaultJwtBuilder implements JwtBuilder {
|
||||||
this.key = key;
|
this.key = key;
|
||||||
//noinspection unchecked
|
//noinspection unchecked
|
||||||
this.sigAlg = (SecureDigestAlgorithm<Key, ?>) alg;
|
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
|
@Override
|
||||||
public byte[] apply(SecureRequest<byte[], Key> request) {
|
public byte[] apply(SecureRequest<InputStream, Key> request) {
|
||||||
return sigAlg.digest(request);
|
return sigAlg.digest(request);
|
||||||
}
|
}
|
||||||
}, SignatureException.class, "Unable to compute %s signature.", id);
|
}, 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 {
|
public JwtBuilder signWith(io.jsonwebtoken.SignatureAlgorithm alg, byte[] secretKeyBytes) throws InvalidKeyException {
|
||||||
Assert.notNull(alg, "SignatureAlgorithm cannot be null.");
|
Assert.notNull(alg, "SignatureAlgorithm cannot be null.");
|
||||||
Assert.notEmpty(secretKeyBytes, "secret key byte array cannot be null or empty.");
|
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());
|
SecretKey key = new SecretKeySpec(secretKeyBytes, alg.getJcaName());
|
||||||
return signWith(key, alg);
|
return signWith(key, alg);
|
||||||
}
|
}
|
||||||
|
@ -279,7 +273,8 @@ public class DefaultJwtBuilder implements JwtBuilder {
|
||||||
@Override
|
@Override
|
||||||
public JwtBuilder signWith(io.jsonwebtoken.SignatureAlgorithm alg, String base64EncodedSecretKey) throws InvalidKeyException {
|
public JwtBuilder signWith(io.jsonwebtoken.SignatureAlgorithm alg, String base64EncodedSecretKey) throws InvalidKeyException {
|
||||||
Assert.hasText(base64EncodedSecretKey, "base64-encoded secret key cannot be null or empty.");
|
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);
|
byte[] bytes = Decoders.BASE64.decode(base64EncodedSecretKey);
|
||||||
return signWith(alg, bytes);
|
return signWith(alg, bytes);
|
||||||
}
|
}
|
||||||
|
@ -301,13 +296,7 @@ public class DefaultJwtBuilder implements JwtBuilder {
|
||||||
@Override
|
@Override
|
||||||
public <K extends Key> JwtBuilder encryptWith(final K key, final KeyAlgorithm<? super K, ?> keyAlg, final AeadAlgorithm enc) {
|
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.");
|
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.");
|
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.notNull(key, "Encryption key cannot be null.");
|
Assert.notNull(key, "Encryption key cannot be null.");
|
||||||
if (key instanceof PrivateKey) {
|
if (key instanceof PrivateKey) {
|
||||||
|
@ -350,7 +339,7 @@ public class DefaultJwtBuilder implements JwtBuilder {
|
||||||
@Override
|
@Override
|
||||||
public JwtBuilder content(String content) {
|
public JwtBuilder content(String content) {
|
||||||
if (Strings.hasText(content)) {
|
if (Strings.hasText(content)) {
|
||||||
this.payload = new Payload(content, Bytes.EMPTY, null);
|
this.payload = new Payload(content, null);
|
||||||
}
|
}
|
||||||
return this;
|
return this;
|
||||||
}
|
}
|
||||||
|
@ -358,7 +347,15 @@ public class DefaultJwtBuilder implements JwtBuilder {
|
||||||
@Override
|
@Override
|
||||||
public JwtBuilder content(byte[] content) {
|
public JwtBuilder content(byte[] content) {
|
||||||
if (!Bytes.isEmpty(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;
|
return this;
|
||||||
}
|
}
|
||||||
|
@ -367,7 +364,25 @@ public class DefaultJwtBuilder implements JwtBuilder {
|
||||||
public JwtBuilder content(byte[] content, String cty) {
|
public JwtBuilder content(byte[] content, String cty) {
|
||||||
Assert.notEmpty(content, "content byte array cannot be null or empty.");
|
Assert.notEmpty(content, "content byte array cannot be null or empty.");
|
||||||
Assert.hasText(cty, "Content Type String 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()
|
// clear out any previous value - it will be set appropriately during compact()
|
||||||
return header().delete(DefaultHeader.CONTENT_TYPE.getId()).and();
|
return header().delete(DefaultHeader.CONTENT_TYPE.getId()).and();
|
||||||
}
|
}
|
||||||
|
@ -410,32 +425,29 @@ public class DefaultJwtBuilder implements JwtBuilder {
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public JwtBuilder subject(String sub) {
|
public JwtBuilder subject(String sub) {
|
||||||
this.claimsBuilder.subject(sub);
|
return claims().subject(sub).and();
|
||||||
return this;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public JwtBuilder setAudience(String aud) {
|
public JwtBuilder setAudience(String aud) {
|
||||||
this.claimsBuilder.setAudience(aud);
|
//noinspection deprecation
|
||||||
return this;
|
return claims().setAudience(aud).and();
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public JwtBuilder audienceSingle(String aud) {
|
public JwtBuilder audienceSingle(String aud) {
|
||||||
this.claimsBuilder.audienceSingle(aud);
|
//noinspection deprecation
|
||||||
return this;
|
return claims().audienceSingle(aud).and();
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public JwtBuilder audience(String aud) {
|
public JwtBuilder audience(String aud) {
|
||||||
this.claimsBuilder.audience(aud);
|
return claims().audience(aud).and();
|
||||||
return this;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public JwtBuilder audience(Collection<String> aud) {
|
public JwtBuilder audience(Collection<String> aud) {
|
||||||
this.claimsBuilder.audience(aud);
|
return claims().audience(aud).and();
|
||||||
return this;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
|
@ -445,8 +457,7 @@ public class DefaultJwtBuilder implements JwtBuilder {
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public JwtBuilder expiration(Date exp) {
|
public JwtBuilder expiration(Date exp) {
|
||||||
this.claimsBuilder.expiration(exp);
|
return claims().expiration(exp).and();
|
||||||
return this;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
|
@ -456,8 +467,7 @@ public class DefaultJwtBuilder implements JwtBuilder {
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public JwtBuilder notBefore(Date nbf) {
|
public JwtBuilder notBefore(Date nbf) {
|
||||||
this.claimsBuilder.notBefore(nbf);
|
return claims().notBefore(nbf).and();
|
||||||
return this;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
|
@ -467,8 +477,7 @@ public class DefaultJwtBuilder implements JwtBuilder {
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public JwtBuilder issuedAt(Date iat) {
|
public JwtBuilder issuedAt(Date iat) {
|
||||||
this.claimsBuilder.issuedAt(iat);
|
return claims().issuedAt(iat).and();
|
||||||
return this;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
|
@ -478,8 +487,7 @@ public class DefaultJwtBuilder implements JwtBuilder {
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public JwtBuilder id(String jti) {
|
public JwtBuilder id(String jti) {
|
||||||
this.claimsBuilder.id(jti);
|
return claims().id(jti).and();
|
||||||
return this;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private void assertPayloadEncoding(String type) {
|
private void assertPayloadEncoding(String type) {
|
||||||
|
@ -492,110 +500,165 @@ public class DefaultJwtBuilder implements JwtBuilder {
|
||||||
@Override
|
@Override
|
||||||
public String compact() {
|
public String compact() {
|
||||||
|
|
||||||
final boolean jwe = encFunction != null;
|
final boolean jwe = this.enc != null;
|
||||||
|
|
||||||
if (jwe && signFunction != null) {
|
if (jwe && signFunction != null) {
|
||||||
String msg = "Both 'signWith' and 'encryptWith' cannot be specified. Choose either one.";
|
String msg = "Both 'signWith' and 'encryptWith' cannot be specified. Choose either one.";
|
||||||
throw new IllegalStateException(msg);
|
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();
|
final Claims claims = this.claimsBuilder.build();
|
||||||
|
|
||||||
if (content.isEmpty() && Collections.isEmpty(claims)) {
|
if (jwe && payload.isEmpty() && Collections.isEmpty(claims)) { // JWE payload can never be empty:
|
||||||
if (jwe) { // JWE payload can never be empty:
|
String msg = "Encrypted JWTs must have either 'claims' or non-empty 'content'.";
|
||||||
String msg = "Encrypted JWTs must have either 'claims' or non-empty 'content'.";
|
throw new IllegalStateException(msg);
|
||||||
throw new IllegalStateException(msg);
|
} // otherwise JWS and Unprotected JWT payloads can be empty
|
||||||
} else { //JWS or Unprotected JWT payloads can be empty
|
|
||||||
content = new Payload(null, Bytes.EMPTY, content.getContentType());
|
if (!payload.isEmpty() && !Collections.isEmpty(claims)) {
|
||||||
}
|
|
||||||
}
|
|
||||||
if (!content.isEmpty() && !Collections.isEmpty(claims)) {
|
|
||||||
throw new IllegalStateException("Both 'content' and 'claims' cannot be specified. Choose either one.");
|
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
|
if (this.serializer == null) { // try to find one based on the services available
|
||||||
//noinspection unchecked
|
//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:
|
if (!Collections.isEmpty(claims)) { // normalize so we have one object to deal with:
|
||||||
byte[] serialized = claimsSerializer.apply(claims);
|
payload = new Payload(claims);
|
||||||
content = new Payload(null, serialized, null);
|
|
||||||
}
|
}
|
||||||
if (compressionAlgorithm != null && !content.isEmpty()) {
|
if (compressionAlgorithm != null && !payload.isEmpty()) {
|
||||||
byte[] data = content.toByteArray();
|
payload.setZip(compressionAlgorithm);
|
||||||
data = compressionAlgorithm.compress(data);
|
|
||||||
content = new Payload(null, data, content.getContentType());
|
|
||||||
this.headerBuilder.put(DefaultHeader.COMPRESSION_ALGORITHM.getId(), compressionAlgorithm.getId());
|
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
|
// We retain the value from the content* calls to prevent accidental removal from
|
||||||
// header().empty() or header().delete calls
|
// header().empty() or header().delete calls
|
||||||
this.headerBuilder.contentType(content.getContentType());
|
this.headerBuilder.contentType(payload.getContentType());
|
||||||
}
|
}
|
||||||
|
|
||||||
Provider keyProvider = ProviderKey.getProvider(this.key, this.provider);
|
Provider keyProvider = ProviderKey.getProvider(this.key, this.provider);
|
||||||
Key key = ProviderKey.getKey(this.key);
|
Key key = ProviderKey.getKey(this.key);
|
||||||
if (jwe) {
|
if (jwe) {
|
||||||
return encrypt(content, key, keyProvider);
|
return encrypt(payload, key, keyProvider);
|
||||||
} else if (key != null) {
|
} else if (key != null) {
|
||||||
return sign(content, key, keyProvider);
|
return sign(payload, key, keyProvider);
|
||||||
} else {
|
} 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(key, "Key is required."); // set by signWithWith*
|
||||||
Assert.stateNotNull(sigAlg, "SignatureAlgorithm is required."); // invariant
|
Assert.stateNotNull(sigAlg, "SignatureAlgorithm is required."); // invariant
|
||||||
Assert.stateNotNull(signFunction, "Signature Algorithm function cannot be null.");
|
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());
|
this.headerBuilder.add(DefaultHeader.ALGORITHM.getId(), sigAlg.getId());
|
||||||
|
|
||||||
if (!this.encodePayload) { // b64 extension:
|
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();
|
String id = DefaultJwsHeader.B64.getId();
|
||||||
this.headerBuilder.critical(id).add(id, false);
|
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: ");
|
// ----- separator -----
|
||||||
|
jws.write(DefaultJwtParser.SEPARATOR_CHAR);
|
||||||
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
|
|
||||||
|
|
||||||
|
// ----- payload -----
|
||||||
// Logic defined by table in https://datatracker.ietf.org/doc/html/rfc7797#section-3 :
|
// 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 (this.encodePayload) {
|
||||||
if (!content.isEmpty()) {
|
encodeAndWrite("JWS Payload", payload, jws);
|
||||||
payloadString = encoder.encode(content.toByteArray());
|
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;
|
if (!payload.isClaims()) {
|
||||||
signingInput = jwt.getBytes(StandardCharsets.US_ASCII);
|
payloadStream = new CountingInputStream(payloadStream); // we'll need to assert if it's empty later
|
||||||
} 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 (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;
|
||||||
byte[] signature = signFunction.apply(request);
|
try {
|
||||||
String base64UrlSignature = encoder.encode(signature);
|
SecureRequest<InputStream, Key> request = new DefaultSecureRequest<>(signingInput, provider, secureRandom, key);
|
||||||
jwt += DefaultJwtParser.SEPARATOR_CHAR + base64UrlSignature;
|
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) {
|
private String unprotected(final Payload content) {
|
||||||
|
@ -604,18 +667,34 @@ public class DefaultJwtBuilder implements JwtBuilder {
|
||||||
assertPayloadEncoding("unprotected JWT");
|
assertPayloadEncoding("unprotected JWT");
|
||||||
|
|
||||||
this.headerBuilder.add(DefaultHeader.ALGORITHM.getId(), Jwts.SIG.NONE.getId());
|
this.headerBuilder.add(DefaultHeader.ALGORITHM.getId(), Jwts.SIG.NONE.getId());
|
||||||
|
|
||||||
|
final ByteArrayOutputStream jwt = new ByteArrayOutputStream(512);
|
||||||
|
|
||||||
|
// ----- header -----
|
||||||
final Header header = this.headerBuilder.build();
|
final Header header = this.headerBuilder.build();
|
||||||
byte[] headerBytes = headerSerializer.apply(header);
|
encodeAndWrite("JWT Header", header, jwt);
|
||||||
|
|
||||||
String b64UrlHeader = encoder.encode(headerBytes);
|
// ----- separator -----
|
||||||
String b64UrlPayload = Strings.EMPTY;
|
jwt.write(DefaultJwtParser.SEPARATOR_CHAR);
|
||||||
if (!content.isEmpty()) {
|
|
||||||
b64UrlPayload = encoder.encode(content.toByteArray());
|
|
||||||
}
|
|
||||||
|
|
||||||
return b64UrlHeader + DefaultJwtParser.SEPARATOR_CHAR +
|
// ----- payload -----
|
||||||
// Must terminate with a period per https://www.rfc-editor.org/rfc/rfc7519#section-6.1 :
|
encodeAndWrite("JWT Payload", content, jwt);
|
||||||
b64UrlPayload + DefaultJwtParser.SEPARATOR_CHAR;
|
|
||||||
|
// ----- 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) {
|
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(content, "Payload argument cannot be null.");
|
||||||
Assert.stateNotNull(key, "Key is required."); // set by encryptWith*
|
Assert.stateNotNull(key, "Key is required."); // set by encryptWith*
|
||||||
Assert.stateNotNull(enc, "Encryption algorithm 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(keyAlg, "KeyAlgorithm is required."); //set by encryptWith*
|
||||||
Assert.stateNotNull(keyAlgFunction, "KeyAlgorithm function cannot be null.");
|
Assert.stateNotNull(keyAlgFunction, "KeyAlgorithm function cannot be null.");
|
||||||
assertPayloadEncoding("JWE");
|
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
|
//only expose (mutable) JweHeader functionality to KeyAlgorithm instances, not the full headerBuilder
|
||||||
// (which exposes this JwtBuilder and shouldn't be referenced by KeyAlgorithms):
|
// (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.");
|
Assert.stateNotNull(keyResult, "KeyAlgorithm must return a KeyResult.");
|
||||||
|
|
||||||
SecretKey cek = Assert.notNull(keyResult.getKey(), "KeyResult must return a content encryption key.");
|
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.add(DefaultHeader.ALGORITHM.getId(), keyAlg.getId());
|
||||||
this.headerBuilder.put(DefaultJweHeader.ENCRYPTION_ALGORITHM.getId(), enc.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);
|
// ----- header -----
|
||||||
final String base64UrlEncodedHeader = encoder.encode(headerBytes);
|
ByteArrayOutputStream jwe = new ByteArrayOutputStream(8192);
|
||||||
byte[] aad = base64UrlEncodedHeader.getBytes(StandardCharsets.US_ASCII);
|
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
|
// 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).
|
// because all JVMs support the standard AeadAlgorithms (especially with BouncyCastle in the classpath).
|
||||||
// As such, the provider here is intentionally omitted (null):
|
// As such, the provider here is intentionally omitted (null):
|
||||||
// TODO: add encProvider(Provider) builder method that applies to this request only?
|
// TODO: add encProvider(Provider) builder method that applies to this request only?
|
||||||
AeadRequest encRequest = new DefaultAeadRequest(payload, null, secureRandom, cek, aad);
|
ByteArrayOutputStream ciphertextOut = new ByteArrayOutputStream(8192);
|
||||||
AeadResult encResult = encFunction.apply(encRequest);
|
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[] iv = Assert.notEmpty(res.getIv(), "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(res.getDigest(), "Encryption result must have a non-empty authentication tag.");
|
||||||
byte[] tag = Assert.notEmpty(encResult.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);
|
jwe.write(DefaultJwtParser.SEPARATOR_CHAR);
|
||||||
String base64UrlEncodedIv = encoder.encode(iv);
|
encodeAndWrite("JWE Encrypted CEK", encryptedCek, jwe);
|
||||||
String base64UrlEncodedCiphertext = encoder.encode(ciphertext);
|
|
||||||
String base64UrlEncodedTag = encoder.encode(tag);
|
|
||||||
|
|
||||||
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 {
|
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");
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -39,35 +39,45 @@ import io.jsonwebtoken.PrematureJwtException;
|
||||||
import io.jsonwebtoken.ProtectedHeader;
|
import io.jsonwebtoken.ProtectedHeader;
|
||||||
import io.jsonwebtoken.SigningKeyResolver;
|
import io.jsonwebtoken.SigningKeyResolver;
|
||||||
import io.jsonwebtoken.UnsupportedJwtException;
|
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.Bytes;
|
||||||
import io.jsonwebtoken.impl.lang.Function;
|
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.DefaultDecryptionKeyRequest;
|
||||||
import io.jsonwebtoken.impl.security.DefaultVerifySecureDigestRequest;
|
import io.jsonwebtoken.impl.security.DefaultVerifySecureDigestRequest;
|
||||||
import io.jsonwebtoken.impl.security.LocatingKeyResolver;
|
import io.jsonwebtoken.impl.security.LocatingKeyResolver;
|
||||||
import io.jsonwebtoken.impl.security.ProviderKey;
|
import io.jsonwebtoken.impl.security.ProviderKey;
|
||||||
import io.jsonwebtoken.io.CompressionAlgorithm;
|
import io.jsonwebtoken.io.CompressionAlgorithm;
|
||||||
import io.jsonwebtoken.io.Decoder;
|
import io.jsonwebtoken.io.Decoder;
|
||||||
import io.jsonwebtoken.io.DecodingException;
|
|
||||||
import io.jsonwebtoken.io.DeserializationException;
|
import io.jsonwebtoken.io.DeserializationException;
|
||||||
import io.jsonwebtoken.io.Deserializer;
|
import io.jsonwebtoken.io.Deserializer;
|
||||||
import io.jsonwebtoken.lang.Arrays;
|
|
||||||
import io.jsonwebtoken.lang.Assert;
|
import io.jsonwebtoken.lang.Assert;
|
||||||
|
import io.jsonwebtoken.lang.Classes;
|
||||||
import io.jsonwebtoken.lang.Collections;
|
import io.jsonwebtoken.lang.Collections;
|
||||||
import io.jsonwebtoken.lang.DateFormats;
|
import io.jsonwebtoken.lang.DateFormats;
|
||||||
|
import io.jsonwebtoken.lang.Objects;
|
||||||
import io.jsonwebtoken.lang.Strings;
|
import io.jsonwebtoken.lang.Strings;
|
||||||
import io.jsonwebtoken.security.AeadAlgorithm;
|
import io.jsonwebtoken.security.AeadAlgorithm;
|
||||||
import io.jsonwebtoken.security.DecryptAeadRequest;
|
import io.jsonwebtoken.security.DecryptAeadRequest;
|
||||||
import io.jsonwebtoken.security.DecryptionKeyRequest;
|
import io.jsonwebtoken.security.DecryptionKeyRequest;
|
||||||
import io.jsonwebtoken.security.InvalidKeyException;
|
import io.jsonwebtoken.security.InvalidKeyException;
|
||||||
import io.jsonwebtoken.security.KeyAlgorithm;
|
import io.jsonwebtoken.security.KeyAlgorithm;
|
||||||
import io.jsonwebtoken.security.Message;
|
|
||||||
import io.jsonwebtoken.security.SecureDigestAlgorithm;
|
import io.jsonwebtoken.security.SecureDigestAlgorithm;
|
||||||
import io.jsonwebtoken.security.SignatureException;
|
import io.jsonwebtoken.security.SignatureException;
|
||||||
import io.jsonwebtoken.security.VerifySecureDigestRequest;
|
import io.jsonwebtoken.security.VerifySecureDigestRequest;
|
||||||
import io.jsonwebtoken.security.WeakKeyException;
|
import io.jsonwebtoken.security.WeakKeyException;
|
||||||
|
|
||||||
import javax.crypto.SecretKey;
|
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.nio.charset.StandardCharsets;
|
||||||
import java.security.Key;
|
import java.security.Key;
|
||||||
import java.security.PrivateKey;
|
import java.security.PrivateKey;
|
||||||
|
@ -75,6 +85,7 @@ import java.security.Provider;
|
||||||
import java.security.PublicKey;
|
import java.security.PublicKey;
|
||||||
import java.util.Collection;
|
import java.util.Collection;
|
||||||
import java.util.Date;
|
import java.util.Date;
|
||||||
|
import java.util.LinkedHashSet;
|
||||||
import java.util.Map;
|
import java.util.Map;
|
||||||
import java.util.Set;
|
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 " +
|
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 " +
|
"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 " +
|
"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[]) " +
|
"application, the overloaded JwtParser.parseContentJws or JwtParser.parseClaimsJws methods that " +
|
||||||
"methods must be used for these kinds of JWSs. Header: %s";
|
"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 " +
|
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 +
|
"'%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 Locator<? extends Key> keyLocator;
|
||||||
|
|
||||||
private final Decoder<String, byte[]> decoder;
|
private final Decoder<InputStream, InputStream> decoder;
|
||||||
|
|
||||||
private final Deserializer<Map<String, ?>> deserializer;
|
private final Deserializer<Map<String, ?>> deserializer;
|
||||||
|
|
||||||
|
@ -210,7 +221,7 @@ public class DefaultJwtParser implements JwtParser {
|
||||||
Set<String> critical,
|
Set<String> critical,
|
||||||
long allowedClockSkewMillis,
|
long allowedClockSkewMillis,
|
||||||
DefaultClaims expectedClaims,
|
DefaultClaims expectedClaims,
|
||||||
Decoder<String, byte[]> base64UrlDecoder,
|
Decoder<InputStream, InputStream> base64UrlDecoder,
|
||||||
Deserializer<Map<String, ?>> deserializer,
|
Deserializer<Map<String, ?>> deserializer,
|
||||||
CompressionCodecResolver compressionCodecResolver,
|
CompressionCodecResolver compressionCodecResolver,
|
||||||
Collection<CompressionAlgorithm> extraZipAlgs,
|
Collection<CompressionAlgorithm> extraZipAlgs,
|
||||||
|
@ -223,11 +234,11 @@ public class DefaultJwtParser implements JwtParser {
|
||||||
this.signingKeyResolver = signingKeyResolver;
|
this.signingKeyResolver = signingKeyResolver;
|
||||||
this.keyLocator = Assert.notNull(keyLocator, "Key Locator cannot be null.");
|
this.keyLocator = Assert.notNull(keyLocator, "Key Locator cannot be null.");
|
||||||
this.clock = Assert.notNull(clock, "Clock 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.allowedClockSkewMillis = allowedClockSkewMillis;
|
||||||
this.expectedClaims = Jwts.claims().add(expectedClaims);
|
this.expectedClaims = Jwts.claims().add(expectedClaims);
|
||||||
this.decoder = Assert.notNull(base64UrlDecoder, "base64UrlDecoder cannot be null.");
|
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.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);
|
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());
|
return header != null && Strings.hasText(header.getContentType());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Returns {@code true} IFF the specified payload starts with a <code>{</code> character and ends with a
|
|
||||||
* <code>}</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>{</code> character and ends with a
|
|
||||||
* <code>}</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,
|
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.");
|
Assert.notNull(resolver, "SigningKeyResolver instance cannot be null.");
|
||||||
|
|
||||||
|
@ -344,7 +284,7 @@ public class DefaultJwtParser implements JwtParser {
|
||||||
if (claims != null) {
|
if (claims != null) {
|
||||||
key = resolver.resolveSigningKey(jwsHeader, claims);
|
key = resolver.resolveSigningKey(jwsHeader, claims);
|
||||||
} else {
|
} else {
|
||||||
key = resolver.resolveSigningKey(jwsHeader, payload);
|
key = resolver.resolveSigningKey(jwsHeader, payload.getBytes());
|
||||||
}
|
}
|
||||||
if (key == null) {
|
if (key == null) {
|
||||||
String msg = "Cannot verify JWS signature: unable to locate signature verification key for JWS with header: " + jwsHeader;
|
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");
|
final byte[] signature = decode(tokenized.getDigest(), "JWS signature");
|
||||||
|
|
||||||
//re-create the jwt part without the signature. This is what is needed for signature verification:
|
//re-create the jwt part without the signature. This is what is needed for signature verification:
|
||||||
byte[] verificationInput;
|
InputStream payloadStream = null;
|
||||||
String jwtPrefix = tokenized.getProtected() + SEPARATOR_CHAR;
|
InputStream verificationInput;
|
||||||
if (jwsHeader.isPayloadEncoded()) {
|
if (jwsHeader.isPayloadEncoded()) {
|
||||||
jwtPrefix += tokenized.getPayload();
|
int len = tokenized.getProtected().length() + 1 + tokenized.getPayload().length();
|
||||||
verificationInput = jwtPrefix.getBytes(StandardCharsets.US_ASCII);
|
CharBuffer cb = CharBuffer.allocate(len);
|
||||||
} else {
|
cb.put(Strings.wrap(tokenized.getProtected()));
|
||||||
byte[] prefixBytes = jwtPrefix.getBytes(StandardCharsets.US_ASCII);
|
cb.put(SEPARATOR_CHAR);
|
||||||
verificationInput = Bytes.concat(prefixBytes, payload);
|
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 {
|
try {
|
||||||
|
@ -390,28 +350,34 @@ public class DefaultJwtParser implements JwtParser {
|
||||||
"trusted. Another possibility is that the parser was provided the incorrect " +
|
"trusted. Another possibility is that the parser was provided the incorrect " +
|
||||||
"signature verification key, but this cannot be assumed for security reasons.";
|
"signature verification key, but this cannot be assumed for security reasons.";
|
||||||
throw new UnsupportedJwtException(msg, e);
|
throw new UnsupportedJwtException(msg, e);
|
||||||
|
} finally {
|
||||||
|
Streams.reset(payloadStream);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public Jwt<?, ?> parse(String compact) {
|
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.hasText(compact, "JWT String cannot be null or empty.");
|
||||||
|
Assert.stateNotNull(unencodedPayload, "internal error: unencodedPayload is null.");
|
||||||
|
|
||||||
final TokenizedJwt tokenized = jwtTokenizer.tokenize(compact);
|
final TokenizedJwt tokenized = jwtTokenizer.tokenize(compact);
|
||||||
final String base64UrlHeader = tokenized.getProtected();
|
final CharSequence base64UrlHeader = tokenized.getProtected();
|
||||||
if (!Strings.hasText(base64UrlHeader)) {
|
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);
|
throw new MalformedJwtException(msg);
|
||||||
}
|
}
|
||||||
|
|
||||||
// =============== Header =================
|
// =============== Header =================
|
||||||
final byte[] headerBytes = decode(base64UrlHeader, "protected 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;
|
Header header;
|
||||||
try {
|
try {
|
||||||
header = tokenized.createHeader(m);
|
header = tokenized.createHeader(m);
|
||||||
|
@ -433,7 +399,7 @@ public class DefaultJwtParser implements JwtParser {
|
||||||
}
|
}
|
||||||
final boolean unsecured = Jwts.SIG.NONE.getId().equalsIgnoreCase(alg);
|
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);
|
final boolean hasDigest = Strings.hasText(base64UrlDigest);
|
||||||
if (unsecured) {
|
if (unsecured) {
|
||||||
if (tokenized instanceof TokenizedJwe) {
|
if (tokenized instanceof TokenizedJwe) {
|
||||||
|
@ -459,13 +425,23 @@ public class DefaultJwtParser implements JwtParser {
|
||||||
// ----- crit assertions -----
|
// ----- crit assertions -----
|
||||||
if (header instanceof ProtectedHeader) {
|
if (header instanceof ProtectedHeader) {
|
||||||
Set<String> crit = Collections.nullSafe(((ProtectedHeader) header).getCritical());
|
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:
|
// assert any values per https://www.rfc-editor.org/rfc/rfc7515.html#section-4.1.11:
|
||||||
for (String name : crit) {
|
for (String name : crit) {
|
||||||
if (!header.containsKey(name)) {
|
if (!header.containsKey(name)) {
|
||||||
String msg = String.format(CRIT_MISSING_MSG, name, name, header);
|
String msg = String.format(CRIT_MISSING_MSG, name, name, header);
|
||||||
throw new MalformedJwtException(msg);
|
throw new MalformedJwtException(msg);
|
||||||
}
|
}
|
||||||
if (!this.critical.contains(name)) {
|
if (!supportedCrit.contains(name)) {
|
||||||
String msg = String.format(CRIT_UNSUPPORTED_MSG, name, name, header);
|
String msg = String.format(CRIT_UNSUPPORTED_MSG, name, name, header);
|
||||||
throw new UnsupportedJwtException(msg);
|
throw new UnsupportedJwtException(msg);
|
||||||
}
|
}
|
||||||
|
@ -473,27 +449,27 @@ public class DefaultJwtParser implements JwtParser {
|
||||||
}
|
}
|
||||||
|
|
||||||
// =============== Payload =================
|
// =============== Payload =================
|
||||||
final String payloadToken = tokenized.getPayload();
|
final CharSequence payloadToken = tokenized.getPayload();
|
||||||
byte[] payload;
|
Payload payload;
|
||||||
boolean integrityVerified = false; // only true after successful signature verification or AEAD decryption
|
boolean integrityVerified = false; // only true after successful signature verification or AEAD decryption
|
||||||
|
|
||||||
// check if b64 extension enabled:
|
// check if b64 extension enabled:
|
||||||
final boolean payloadBase64UrlEncoded = !(header instanceof JwsHeader) || ((JwsHeader) header).isPayloadEncoded();
|
final boolean payloadBase64UrlEncoded = !(header instanceof JwsHeader) || ((JwsHeader) header).isPayloadEncoded();
|
||||||
if (payloadBase64UrlEncoded) {
|
if (payloadBase64UrlEncoded) {
|
||||||
// standard encoding, so decode it:
|
// standard encoding, so decode it:
|
||||||
payload = decode(tokenized.getPayload(), "payload");
|
byte[] data = decode(tokenized.getPayload(), "payload");
|
||||||
|
payload = new Payload(data, header.getContentType());
|
||||||
} else {
|
} else {
|
||||||
// The JWT uses the b64 extension, and we already know the parser supports that extension at this point
|
// 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
|
// in the code execution path because of the ----- crit ----- assertions section above as well as the
|
||||||
// (JwsHeader).isPayloadEncoded() check
|
// (JwsHeader).isPayloadEncoded() check
|
||||||
|
|
||||||
if (Strings.hasText(payloadToken)) {
|
if (Strings.hasText(payloadToken)) {
|
||||||
// we need to verify what was in the token, otherwise it'd be a security issue if we ignored it
|
// 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:
|
// and assumed the (likely safe) unencodedPayload value instead:
|
||||||
payload = Strings.utf8(payloadToken);
|
payload = new Payload(payloadToken, header.getContentType());
|
||||||
} else {
|
} else {
|
||||||
//no payload token (a detached payload), so we need to ensure that they've specified the payload value:
|
//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);
|
String msg = String.format(B64_MISSING_PAYLOAD, header);
|
||||||
throw new SignatureException(msg);
|
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
|
// 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).";
|
String msg = "Compact JWE strings MUST always contain a payload (ciphertext).";
|
||||||
throw new MalformedJwtException(msg);
|
throw new MalformedJwtException(msg);
|
||||||
|
@ -516,10 +492,10 @@ public class DefaultJwtParser implements JwtParser {
|
||||||
JweHeader jweHeader = Assert.stateIsInstance(JweHeader.class, header, "Not a JweHeader. ");
|
JweHeader jweHeader = Assert.stateIsInstance(JweHeader.class, header, "Not a JweHeader. ");
|
||||||
|
|
||||||
byte[] cekBytes = Bytes.EMPTY; //ignored unless using an encrypted key algorithm
|
byte[] cekBytes = Bytes.EMPTY; //ignored unless using an encrypted key algorithm
|
||||||
String base64Url = tokenizedJwe.getEncryptedKey();
|
CharSequence base64Url = tokenizedJwe.getEncryptedKey();
|
||||||
if (Strings.hasText(base64Url)) {
|
if (Strings.hasText(base64Url)) {
|
||||||
cekBytes = decode(base64Url, "JWE encrypted key");
|
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.";
|
String msg = "Compact JWE string represents an encrypted key, but the key is empty.";
|
||||||
throw new MalformedJwtException(msg);
|
throw new MalformedJwtException(msg);
|
||||||
}
|
}
|
||||||
|
@ -529,7 +505,7 @@ public class DefaultJwtParser implements JwtParser {
|
||||||
if (Strings.hasText(base64Url)) {
|
if (Strings.hasText(base64Url)) {
|
||||||
iv = decode(base64Url, "JWE Initialization Vector");
|
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.";
|
String msg = "Compact JWE strings must always contain an Initialization Vector.";
|
||||||
throw new MalformedJwtException(msg);
|
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
|
// 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
|
// 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.
|
// 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;
|
base64Url = base64UrlDigest;
|
||||||
//guaranteed to be non-empty via the `alg` + digest check above:
|
//guaranteed to be non-empty via the `alg` + digest check above:
|
||||||
Assert.hasText(base64Url, "JWE AAD Authentication Tag cannot be null or empty.");
|
Assert.hasText(base64Url, "JWE AAD Authentication Tag cannot be null or empty.");
|
||||||
tag = decode(base64Url, "JWE AAD Authentication Tag");
|
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.";
|
String msg = "Compact JWE strings must always contain an AAD Authentication Tag.";
|
||||||
throw new MalformedJwtException(msg);
|
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).
|
// because all JVMs support the standard AeadAlgorithms (especially with BouncyCastle in the classpath).
|
||||||
// As such, the provider here is intentionally omitted (null):
|
// As such, the provider here is intentionally omitted (null):
|
||||||
// TODO: add encProvider(Provider) builder method that applies to this request only?
|
// TODO: add encProvider(Provider) builder method that applies to this request only?
|
||||||
DecryptAeadRequest decryptRequest =
|
InputStream ciphertext = payload.toInputStream();
|
||||||
new DefaultAeadResult(null, null, payload, cek, aad, tag, iv);
|
ByteArrayOutputStream plaintext = new ByteArrayOutputStream(8192);
|
||||||
Message<byte[]> result = encAlg.decrypt(decryptRequest);
|
DecryptAeadRequest dreq = new DefaultDecryptAeadRequest(ciphertext, cek, aad, iv, tag);
|
||||||
payload = result.getPayload();
|
encAlg.decrypt(dreq, plaintext);
|
||||||
|
payload = new Payload(plaintext.toByteArray(), header.getContentType());
|
||||||
|
|
||||||
integrityVerified = true; // AEAD performs integrity verification, so no exception = verified
|
integrityVerified = true; // AEAD performs integrity verification, so no exception = verified
|
||||||
|
|
||||||
|
@ -595,7 +575,7 @@ public class DefaultJwtParser implements JwtParser {
|
||||||
// always safer:
|
// always safer:
|
||||||
JwsHeader jwsHeader = Assert.stateIsInstance(JwsHeader.class, header, "Not a JwsHeader. ");
|
JwsHeader jwsHeader = Assert.stateIsInstance(JwsHeader.class, header, "Not a JwsHeader. ");
|
||||||
verifySignature(tokenized, jwsHeader, alg, new LocatingKeyResolver(this.keyLocator), null, payload);
|
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);
|
final CompressionAlgorithm compressionAlgorithm = zipAlgFn.apply(header);
|
||||||
|
@ -609,34 +589,59 @@ public class DefaultJwtParser implements JwtParser {
|
||||||
throw new UnsupportedJwtException(msg);
|
throw new UnsupportedJwtException(msg);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
payload = compressionAlgorithm.decompress(payload);
|
payload = payload.decompress(compressionAlgorithm);
|
||||||
}
|
}
|
||||||
|
|
||||||
Claims claims = null;
|
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
|
// to convert the byte payload themselves based on this content type
|
||||||
// https://www.rfc-editor.org/rfc/rfc7515.html#section-4.1.10 :
|
// https://www.rfc-editor.org/rfc/rfc7515.html#section-4.1.10 :
|
||||||
//
|
//
|
||||||
// "This parameter is ignored by JWS implementations; any processing of this
|
// "This parameter is ignored by JWS implementations; any processing of this
|
||||||
// parameter is performed by the JWS application."
|
// parameter is performed by the JWS application."
|
||||||
//
|
//
|
||||||
isLikelyJson(payload)) { // likely to be json, try to deserialize it:
|
Map<String, ?> claimsMap = null;
|
||||||
Map<String, ?> claimsMap = deserialize(payload, "claims");
|
try {
|
||||||
try {
|
// if deserialization fails, we'll need to rewind to convert to a byte array. So if
|
||||||
claims = new DefaultClaims(claimsMap);
|
// mark/reset isn't possible, we'll need to buffer:
|
||||||
} catch (Exception e) {
|
if (!in.markSupported()) {
|
||||||
String msg = "Invalid claims: " + e.getMessage();
|
in = new BufferedInputStream(in);
|
||||||
throw new MalformedJwtException(msg, e);
|
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;
|
Jwt<?, ?> jwt;
|
||||||
Object body = claims != null ? claims : payload;
|
Object body = claims != null ? claims : payloadBytes;
|
||||||
if (header instanceof JweHeader) {
|
if (header instanceof JweHeader) {
|
||||||
jwt = new DefaultJwe<>((JweHeader) header, body, iv, tag);
|
jwt = new DefaultJwe<>((JweHeader) header, body, iv, tag);
|
||||||
} else if (hasDigest) {
|
} else if (hasDigest) {
|
||||||
JwsHeader jwsHeader = Assert.isInstanceOf(JwsHeader.class, header, "JwsHeader required.");
|
JwsHeader jwsHeader = Assert.isInstanceOf(JwsHeader.class, header, "JwsHeader required.");
|
||||||
jwt = new DefaultJws<>(jwsHeader, body, base64UrlDigest);
|
jwt = new DefaultJws<>(jwsHeader, body, base64UrlDigest.toString());
|
||||||
} else {
|
} else {
|
||||||
//noinspection rawtypes
|
//noinspection rawtypes
|
||||||
jwt = new DefaultJwt(header, body);
|
jwt = new DefaultJwt(header, body);
|
||||||
|
@ -763,10 +768,10 @@ public class DefaultJwtParser implements JwtParser {
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public <T> T parse(String compact, JwtHandler<T> handler) {
|
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 {
|
throws ExpiredJwtException, MalformedJwtException, SignatureException {
|
||||||
Assert.notNull(handler, "JwtHandler argument cannot be null.");
|
Assert.notNull(handler, "JwtHandler argument cannot be null.");
|
||||||
Assert.hasText(compact, "JWT String argument cannot be null or empty.");
|
Assert.hasText(compact, "JWT String argument cannot be null or empty.");
|
||||||
|
@ -839,9 +844,7 @@ public class DefaultJwtParser implements JwtParser {
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
private Jws<byte[]> parseContentJws(String jws, Payload unencodedPayload) {
|
||||||
public Jws<byte[]> parseContentJws(String jws, byte[] unencodedPayload) {
|
|
||||||
Assert.notEmpty(unencodedPayload, "unencodedPayload argument cannot be null or empty.");
|
|
||||||
return parse(jws, unencodedPayload, new JwtHandlerAdapter<Jws<byte[]>>() {
|
return parse(jws, unencodedPayload, new JwtHandlerAdapter<Jws<byte[]>>() {
|
||||||
@Override
|
@Override
|
||||||
public Jws<byte[]> onContentJws(Jws<byte[]> jws) {
|
public Jws<byte[]> onContentJws(Jws<byte[]> jws) {
|
||||||
|
@ -850,9 +853,8 @@ public class DefaultJwtParser implements JwtParser {
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
private Jws<Claims> parseClaimsJws(String jws, Payload unencodedPayload) {
|
||||||
public Jws<Claims> parseClaimsJws(String jws, byte[] unencodedPayload) {
|
unencodedPayload.setClaimsExpected(true);
|
||||||
Assert.notEmpty(unencodedPayload, "unencodedPayload argument cannot be null or empty.");
|
|
||||||
return parse(jws, unencodedPayload, new JwtHandlerAdapter<Jws<Claims>>() {
|
return parse(jws, unencodedPayload, new JwtHandlerAdapter<Jws<Claims>>() {
|
||||||
@Override
|
@Override
|
||||||
public Jws<Claims> onClaimsJws(Jws<Claims> jws) {
|
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
|
@Override
|
||||||
public Jwe<byte[]> parseContentJwe(String compact) throws JwtException {
|
public Jwe<byte[]> parseContentJwe(String compact) throws JwtException {
|
||||||
return parse(compact, new JwtHandlerAdapter<Jwe<byte[]>>() {
|
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 {
|
try {
|
||||||
return decoder.decode(base64UrlEncoded);
|
InputStream decoding = this.decoder.decode(new ByteArrayInputStream(Strings.utf8(base64UrlEncoded)));
|
||||||
} catch (DecodingException e) {
|
return Streams.bytes(decoding, "Unable to Base64Url-decode input.");
|
||||||
String msg = "Invalid Base64Url " + name + ": " + base64UrlEncoded;
|
} catch (Throwable t) {
|
||||||
throw new MalformedJwtException(msg, e);
|
// 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 {
|
try {
|
||||||
return deserializer.deserialize(bytes);
|
JsonObjectDeserializer deserializer = new JsonObjectDeserializer(this.deserializer, name);
|
||||||
} catch (MalformedJwtException | DeserializationException e) {
|
return deserializer.apply(in);
|
||||||
String s = new String(bytes, StandardCharsets.UTF_8);
|
} finally {
|
||||||
throw new MalformedJwtException("Unable to read " + name + " JSON: " + s, e);
|
Objects.nullSafeClose(in);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -23,6 +23,7 @@ import io.jsonwebtoken.JwtParserBuilder;
|
||||||
import io.jsonwebtoken.Jwts;
|
import io.jsonwebtoken.Jwts;
|
||||||
import io.jsonwebtoken.Locator;
|
import io.jsonwebtoken.Locator;
|
||||||
import io.jsonwebtoken.SigningKeyResolver;
|
import io.jsonwebtoken.SigningKeyResolver;
|
||||||
|
import io.jsonwebtoken.impl.io.DelegateStringDecoder;
|
||||||
import io.jsonwebtoken.impl.lang.Services;
|
import io.jsonwebtoken.impl.lang.Services;
|
||||||
import io.jsonwebtoken.impl.security.ConstantKeyLocator;
|
import io.jsonwebtoken.impl.security.ConstantKeyLocator;
|
||||||
import io.jsonwebtoken.io.CompressionAlgorithm;
|
import io.jsonwebtoken.io.CompressionAlgorithm;
|
||||||
|
@ -39,6 +40,7 @@ import io.jsonwebtoken.security.Keys;
|
||||||
import io.jsonwebtoken.security.SecureDigestAlgorithm;
|
import io.jsonwebtoken.security.SecureDigestAlgorithm;
|
||||||
|
|
||||||
import javax.crypto.SecretKey;
|
import javax.crypto.SecretKey;
|
||||||
|
import java.io.InputStream;
|
||||||
import java.security.Key;
|
import java.security.Key;
|
||||||
import java.security.PrivateKey;
|
import java.security.PrivateKey;
|
||||||
import java.security.Provider;
|
import java.security.Provider;
|
||||||
|
@ -88,7 +90,7 @@ public class DefaultJwtParserBuilder implements JwtParserBuilder {
|
||||||
@SuppressWarnings("deprecation")
|
@SuppressWarnings("deprecation")
|
||||||
private CompressionCodecResolver compressionCodecResolver;
|
private CompressionCodecResolver compressionCodecResolver;
|
||||||
|
|
||||||
private Decoder<String, byte[]> decoder = Decoders.BASE64URL;
|
private Decoder<InputStream, InputStream> decoder = new DelegateStringDecoder(Decoders.BASE64URL);
|
||||||
|
|
||||||
private Deserializer<Map<String, ?>> deserializer;
|
private Deserializer<Map<String, ?>> deserializer;
|
||||||
|
|
||||||
|
@ -123,23 +125,24 @@ public class DefaultJwtParserBuilder implements JwtParserBuilder {
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public JwtParserBuilder deserializeJsonWith(Deserializer<Map<String, ?>> deserializer) {
|
public JwtParserBuilder deserializeJsonWith(Deserializer<Map<String, ?>> deserializer) {
|
||||||
return deserializer(deserializer);
|
return json(deserializer);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public JwtParserBuilder deserializer(Deserializer<Map<String, ?>> deserializer) {
|
public JwtParserBuilder json(Deserializer<Map<String, ?>> reader) {
|
||||||
Assert.notNull(deserializer, "deserializer cannot be null.");
|
this.deserializer = Assert.notNull(reader, "JSON Deserializer cannot be null.");
|
||||||
this.deserializer = deserializer;
|
|
||||||
return this;
|
return this;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@SuppressWarnings("deprecation")
|
||||||
@Override
|
@Override
|
||||||
public JwtParserBuilder base64UrlDecodeWith(Decoder<String, byte[]> decoder) {
|
public JwtParserBuilder base64UrlDecodeWith(final Decoder<CharSequence, byte[]> decoder) {
|
||||||
return decoder(decoder);
|
Assert.notNull(decoder, "decoder cannot be null.");
|
||||||
|
return b64Url(new DelegateStringDecoder(decoder));
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public JwtParserBuilder decoder(Decoder<String, byte[]> decoder) {
|
public JwtParserBuilder b64Url(Decoder<InputStream, InputStream> decoder) {
|
||||||
Assert.notNull(decoder, "decoder cannot be null.");
|
Assert.notNull(decoder, "decoder cannot be null.");
|
||||||
this.decoder = decoder;
|
this.decoder = decoder;
|
||||||
return this;
|
return this;
|
||||||
|
@ -349,14 +352,10 @@ public class DefaultJwtParserBuilder implements JwtParserBuilder {
|
||||||
@Override
|
@Override
|
||||||
public JwtParser build() {
|
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) {
|
if (this.deserializer == null) {
|
||||||
// try to find one based on the services available:
|
|
||||||
//noinspection unchecked
|
//noinspection unchecked
|
||||||
this.deserializer = Services.loadFirst(Deserializer.class);
|
json(Services.loadFirst(Deserializer.class));
|
||||||
}
|
}
|
||||||
|
|
||||||
if (this.signingKeyResolver != null && this.signatureVerificationKey != null) {
|
if (this.signingKeyResolver != null && this.signatureVerificationKey != null) {
|
||||||
String msg = "Both a 'signingKeyResolver and a 'verifyWith' key cannot be configured. " +
|
String msg = "Both a 'signingKeyResolver and a 'verifyWith' key cannot be configured. " +
|
||||||
"Choose either, or prefer `keyLocator` when possible.";
|
"Choose either, or prefer `keyLocator` when possible.";
|
||||||
|
@ -408,7 +407,7 @@ public class DefaultJwtParserBuilder implements JwtParserBuilder {
|
||||||
allowedClockSkewMillis,
|
allowedClockSkewMillis,
|
||||||
expClaims,
|
expClaims,
|
||||||
decoder,
|
decoder,
|
||||||
new JwtDeserializer<>(deserializer),
|
deserializer,
|
||||||
compressionCodecResolver,
|
compressionCodecResolver,
|
||||||
extraZipAlgs,
|
extraZipAlgs,
|
||||||
extraSigAlgs,
|
extraSigAlgs,
|
||||||
|
|
|
@ -21,22 +21,23 @@ import java.util.Map;
|
||||||
|
|
||||||
class DefaultTokenizedJwe extends DefaultTokenizedJwt implements TokenizedJwe {
|
class DefaultTokenizedJwe extends DefaultTokenizedJwt implements TokenizedJwe {
|
||||||
|
|
||||||
private final String encryptedKey;
|
private final CharSequence encryptedKey;
|
||||||
private final String iv;
|
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);
|
super(protectedHeader, body, digest);
|
||||||
this.encryptedKey = encryptedKey;
|
this.encryptedKey = encryptedKey;
|
||||||
this.iv = iv;
|
this.iv = iv;
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public String getEncryptedKey() {
|
public CharSequence getEncryptedKey() {
|
||||||
return this.encryptedKey;
|
return this.encryptedKey;
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public String getIv() {
|
public CharSequence getIv() {
|
||||||
return this.iv;
|
return this.iv;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -23,28 +23,28 @@ import java.util.Map;
|
||||||
|
|
||||||
class DefaultTokenizedJwt implements TokenizedJwt {
|
class DefaultTokenizedJwt implements TokenizedJwt {
|
||||||
|
|
||||||
private final String protectedHeader;
|
private final CharSequence protectedHeader;
|
||||||
private final String payload;
|
private final CharSequence payload;
|
||||||
private final String digest;
|
private final CharSequence digest;
|
||||||
|
|
||||||
DefaultTokenizedJwt(String protectedHeader, String payload, String digest) {
|
DefaultTokenizedJwt(CharSequence protectedHeader, CharSequence payload, CharSequence digest) {
|
||||||
this.protectedHeader = protectedHeader;
|
this.protectedHeader = protectedHeader;
|
||||||
this.payload = payload;
|
this.payload = payload;
|
||||||
this.digest = digest;
|
this.digest = digest;
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public String getProtected() {
|
public CharSequence getProtected() {
|
||||||
return this.protectedHeader;
|
return this.protectedHeader;
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public String getPayload() {
|
public CharSequence getPayload() {
|
||||||
return this.payload;
|
return this.payload;
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public String getDigest() {
|
public CharSequence getDigest() {
|
||||||
return this.digest;
|
return this.digest;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -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);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -17,28 +17,28 @@ package io.jsonwebtoken.impl;
|
||||||
|
|
||||||
import io.jsonwebtoken.MalformedJwtException;
|
import io.jsonwebtoken.MalformedJwtException;
|
||||||
import io.jsonwebtoken.lang.Assert;
|
import io.jsonwebtoken.lang.Assert;
|
||||||
|
import io.jsonwebtoken.lang.Strings;
|
||||||
|
|
||||||
public class JwtTokenizer {
|
public class JwtTokenizer {
|
||||||
|
|
||||||
static final char DELIMITER = '.';
|
static final char DELIMITER = '.';
|
||||||
|
|
||||||
private static final String DELIM_ERR_MSG_PREFIX = "Invalid compact JWT string: Compact JWSs must contain " +
|
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")
|
@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.");
|
Assert.hasText(jwt, "Argument cannot be null or empty.");
|
||||||
|
|
||||||
String protectedHeader = ""; //Both JWS and JWE
|
CharSequence protectedHeader = Strings.EMPTY; //Both JWS and JWE
|
||||||
String body = ""; //JWS payload or JWE Ciphertext
|
CharSequence body = Strings.EMPTY; //JWS payload or JWE Ciphertext
|
||||||
String encryptedKey = ""; //JWE only
|
CharSequence encryptedKey = Strings.EMPTY; //JWE only
|
||||||
String iv = ""; //JWE only
|
CharSequence iv = Strings.EMPTY; //JWE only
|
||||||
String digest; //JWS Signature or JWE AAD Tag
|
CharSequence digest; //JWS Signature or JWE AAD Tag
|
||||||
|
|
||||||
int delimiterCount = 0;
|
int delimiterCount = 0;
|
||||||
|
int start = 0;
|
||||||
StringBuilder sb = new StringBuilder(128);
|
|
||||||
|
|
||||||
for (int i = 0; i < jwt.length(); i++) {
|
for (int i = 0; i < jwt.length(); i++) {
|
||||||
|
|
||||||
|
@ -51,7 +51,8 @@ public class JwtTokenizer {
|
||||||
|
|
||||||
if (c == DELIMITER) {
|
if (c == DELIMITER) {
|
||||||
|
|
||||||
String token = sb.toString();
|
CharSequence token = jwt.subSequence(start, i);
|
||||||
|
start = i + 1;
|
||||||
|
|
||||||
switch (delimiterCount) {
|
switch (delimiterCount) {
|
||||||
case 0:
|
case 0:
|
||||||
|
@ -62,7 +63,7 @@ public class JwtTokenizer {
|
||||||
encryptedKey = token; //for JWE
|
encryptedKey = token; //for JWE
|
||||||
break;
|
break;
|
||||||
case 2:
|
case 2:
|
||||||
body = ""; //clear out value set for JWS
|
body = Strings.EMPTY; //clear out value set for JWS
|
||||||
iv = token;
|
iv = token;
|
||||||
break;
|
break;
|
||||||
case 3:
|
case 3:
|
||||||
|
@ -70,10 +71,7 @@ public class JwtTokenizer {
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
|
||||||
sb.setLength(0);
|
|
||||||
delimiterCount++;
|
delimiterCount++;
|
||||||
} else {
|
|
||||||
sb.append(c);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -82,7 +80,7 @@ public class JwtTokenizer {
|
||||||
throw new MalformedJwtException(msg);
|
throw new MalformedJwtException(msg);
|
||||||
}
|
}
|
||||||
|
|
||||||
digest = sb.toString();
|
digest = jwt.subSequence(start, jwt.length());
|
||||||
|
|
||||||
if (delimiterCount == 2) {
|
if (delimiterCount == 2) {
|
||||||
return (T) new DefaultTokenizedJwt(protectedHeader, body, digest);
|
return (T) new DefaultTokenizedJwt(protectedHeader, body, digest);
|
||||||
|
|
|
@ -15,42 +15,138 @@
|
||||||
*/
|
*/
|
||||||
package io.jsonwebtoken.impl;
|
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.impl.lang.Bytes;
|
||||||
|
import io.jsonwebtoken.io.CompressionAlgorithm;
|
||||||
|
import io.jsonwebtoken.lang.Assert;
|
||||||
|
import io.jsonwebtoken.lang.Collections;
|
||||||
import io.jsonwebtoken.lang.Strings;
|
import io.jsonwebtoken.lang.Strings;
|
||||||
|
|
||||||
|
import java.io.ByteArrayInputStream;
|
||||||
|
import java.io.InputStream;
|
||||||
|
import java.io.OutputStream;
|
||||||
|
|
||||||
class Payload {
|
class Payload {
|
||||||
|
|
||||||
static final Payload EMPTY = new Payload(null, null, null);
|
static final Payload EMPTY = new Payload(Bytes.EMPTY, null);
|
||||||
|
|
||||||
private String string;
|
private final CharSequence string;
|
||||||
private byte[] bytes;
|
private final byte[] bytes;
|
||||||
private String contentType;
|
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) {
|
Payload(Claims claims) {
|
||||||
this.string = Strings.clean(string);
|
this(claims, null, null, null, null);
|
||||||
this.bytes = Bytes.nullSafe(bytes);
|
|
||||||
this.contentType = Strings.clean(contentType);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
String getString() {
|
Payload(CharSequence content, String contentType) {
|
||||||
return this.string;
|
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() {
|
String getContentType() {
|
||||||
return this.contentType;
|
return this.contentType;
|
||||||
}
|
}
|
||||||
|
|
||||||
boolean isEmpty() {
|
public void setZip(CompressionAlgorithm zip) {
|
||||||
return !Strings.hasText(this.string) && Bytes.isEmpty(this.bytes);
|
this.zip = zip;
|
||||||
}
|
}
|
||||||
|
|
||||||
byte[] toByteArray() {
|
boolean isCompressed() {
|
||||||
if (Bytes.isEmpty(this.bytes)) {
|
return this.zip != null;
|
||||||
if (!Strings.hasText(this.string)) {
|
}
|
||||||
throw new IllegalStateException("Content is empty.");
|
|
||||||
|
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;
|
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;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -17,7 +17,7 @@ package io.jsonwebtoken.impl;
|
||||||
|
|
||||||
public interface TokenizedJwe extends TokenizedJwt {
|
public interface TokenizedJwe extends TokenizedJwt {
|
||||||
|
|
||||||
String getEncryptedKey();
|
CharSequence getEncryptedKey();
|
||||||
|
|
||||||
String getIv();
|
CharSequence getIv();
|
||||||
}
|
}
|
||||||
|
|
|
@ -26,21 +26,21 @@ public interface TokenizedJwt {
|
||||||
*
|
*
|
||||||
* @return protected header.
|
* @return protected header.
|
||||||
*/
|
*/
|
||||||
String getProtected();
|
CharSequence getProtected();
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Returns the Payload for a JWS or Ciphertext for a JWE.
|
* Returns the Payload for a JWS or Ciphertext for a JWE.
|
||||||
*
|
*
|
||||||
* @return 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.
|
* Returns the Signature for JWS or AAD Tag for JWE.
|
||||||
*
|
*
|
||||||
* @return 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.
|
* Returns a new {@link Header} instance with the specified map state.
|
||||||
|
|
|
@ -17,11 +17,17 @@ package io.jsonwebtoken.impl.compression;
|
||||||
|
|
||||||
import io.jsonwebtoken.CompressionCodec;
|
import io.jsonwebtoken.CompressionCodec;
|
||||||
import io.jsonwebtoken.CompressionException;
|
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.io.CompressionAlgorithm;
|
||||||
import io.jsonwebtoken.lang.Assert;
|
import io.jsonwebtoken.lang.Assert;
|
||||||
import io.jsonwebtoken.lang.Objects;
|
import io.jsonwebtoken.lang.Objects;
|
||||||
import io.jsonwebtoken.lang.Strings;
|
import io.jsonwebtoken.lang.Strings;
|
||||||
|
|
||||||
|
import java.io.ByteArrayInputStream;
|
||||||
import java.io.ByteArrayOutputStream;
|
import java.io.ByteArrayOutputStream;
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
import java.io.InputStream;
|
import java.io.InputStream;
|
||||||
|
@ -32,12 +38,54 @@ import java.io.OutputStream;
|
||||||
*
|
*
|
||||||
* @since 0.6.0
|
* @since 0.6.0
|
||||||
*/
|
*/
|
||||||
|
@SuppressWarnings("deprecation")
|
||||||
public abstract class AbstractCompressionAlgorithm implements CompressionAlgorithm, CompressionCodec {
|
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 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) {
|
protected AbstractCompressionAlgorithm(String id) {
|
||||||
this.id = Assert.hasText(Strings.clean(id), "id argument cannot be null or empty.");
|
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
|
@Override
|
||||||
|
@ -50,70 +98,38 @@ public abstract class AbstractCompressionAlgorithm implements CompressionAlgorit
|
||||||
return getId();
|
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).
|
@Override
|
||||||
//TODO: make protected on a minor release
|
public final OutputStream compress(final OutputStream out) throws CompressionException {
|
||||||
interface StreamWrapper {
|
return OS_WRAP_FN.apply(out);
|
||||||
OutputStream wrap(OutputStream out) throws IOException;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
//package-protected for a point release. This can be made protected on a minor release (0.11.0, 0.12.0, 1.0, etc).
|
protected abstract OutputStream doCompress(OutputStream out) throws IOException;
|
||||||
//TODO: make protected on a minor release
|
|
||||||
byte[] readAndClose(InputStream input) throws IOException {
|
@Override
|
||||||
byte[] buffer = new byte[512];
|
public final InputStream decompress(InputStream is) throws CompressionException {
|
||||||
ByteArrayOutputStream out = new ByteArrayOutputStream(buffer.length);
|
return IS_WRAP_FN.apply(is);
|
||||||
int read;
|
}
|
||||||
|
|
||||||
|
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 {
|
try {
|
||||||
read = input.read(buffer); //assignment separate from loop invariant check for code coverage checks
|
compression.write(data);
|
||||||
while (read != -1) {
|
compression.flush();
|
||||||
out.write(buffer, 0, read);
|
|
||||||
read = input.read(buffer);
|
|
||||||
}
|
|
||||||
} finally {
|
} finally {
|
||||||
Objects.nullSafeClose(input);
|
Objects.nullSafeClose(compression);
|
||||||
}
|
}
|
||||||
return out.toByteArray();
|
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}
|
* Asserts the compressed bytes is not null and calls {@link #doDecompress(byte[]) doDecompress}
|
||||||
|
@ -124,13 +140,8 @@ public abstract class AbstractCompressionAlgorithm implements CompressionAlgorit
|
||||||
*/
|
*/
|
||||||
@Override
|
@Override
|
||||||
public final byte[] decompress(byte[] compressed) {
|
public final byte[] decompress(byte[] compressed) {
|
||||||
Assert.notNull(compressed, "compressed bytes cannot be null.");
|
if (Bytes.isEmpty(compressed)) return Bytes.EMPTY;
|
||||||
|
return this.DECOMPRESS_FN.apply(compressed);
|
||||||
try {
|
|
||||||
return doDecompress(compressed);
|
|
||||||
} catch (IOException e) {
|
|
||||||
throw new CompressionException("Unable to decompress bytes.", e);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -140,5 +151,20 @@ public abstract class AbstractCompressionAlgorithm implements CompressionAlgorit
|
||||||
* @return decompressed bytes
|
* @return decompressed bytes
|
||||||
* @throws IOException if the decompression runs into an IO problem
|
* @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();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -17,7 +17,6 @@ package io.jsonwebtoken.impl.compression;
|
||||||
|
|
||||||
import io.jsonwebtoken.lang.Objects;
|
import io.jsonwebtoken.lang.Objects;
|
||||||
|
|
||||||
import java.io.ByteArrayInputStream;
|
|
||||||
import java.io.ByteArrayOutputStream;
|
import java.io.ByteArrayOutputStream;
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
import java.io.InputStream;
|
import java.io.InputStream;
|
||||||
|
@ -35,26 +34,24 @@ public class DeflateCompressionAlgorithm extends AbstractCompressionAlgorithm {
|
||||||
|
|
||||||
private static final String ID = "DEF";
|
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() {
|
public DeflateCompressionAlgorithm() {
|
||||||
super(ID);
|
super(ID);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
protected byte[] doCompress(byte[] content) throws IOException {
|
protected OutputStream doCompress(OutputStream out) {
|
||||||
return writeAndClose(content, WRAPPER);
|
return new DeflaterOutputStream(out);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected InputStream doDecompress(InputStream is) {
|
||||||
|
return new InflaterInputStream(is);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
protected byte[] doDecompress(final byte[] compressed) throws IOException {
|
protected byte[] doDecompress(final byte[] compressed) throws IOException {
|
||||||
try {
|
try {
|
||||||
return readAndClose(new InflaterInputStream(new ByteArrayInputStream(compressed)));
|
return super.doDecompress(compressed);
|
||||||
} catch (IOException e1) {
|
} catch (IOException e1) {
|
||||||
try {
|
try {
|
||||||
return doDecompressBackCompat(compressed);
|
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
|
* 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
|
* @param compressed the compressed byte array
|
||||||
* @return decompressed bytes
|
* @return decompressed bytes
|
||||||
|
|
|
@ -15,10 +15,8 @@
|
||||||
*/
|
*/
|
||||||
package io.jsonwebtoken.impl.compression;
|
package io.jsonwebtoken.impl.compression;
|
||||||
|
|
||||||
import io.jsonwebtoken.CompressionCodec;
|
|
||||||
|
|
||||||
import java.io.ByteArrayInputStream;
|
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
|
import java.io.InputStream;
|
||||||
import java.io.OutputStream;
|
import java.io.OutputStream;
|
||||||
import java.util.zip.GZIPInputStream;
|
import java.util.zip.GZIPInputStream;
|
||||||
import java.util.zip.GZIPOutputStream;
|
import java.util.zip.GZIPOutputStream;
|
||||||
|
@ -28,28 +26,21 @@ import java.util.zip.GZIPOutputStream;
|
||||||
*
|
*
|
||||||
* @since 0.6.0
|
* @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 String ID = "GZIP";
|
||||||
|
|
||||||
private static final StreamWrapper WRAPPER = new StreamWrapper() {
|
|
||||||
@Override
|
|
||||||
public OutputStream wrap(OutputStream out) throws IOException {
|
|
||||||
return new GZIPOutputStream(out);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
public GzipCompressionAlgorithm() {
|
public GzipCompressionAlgorithm() {
|
||||||
super(ID);
|
super(ID);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
protected byte[] doCompress(byte[] content) throws IOException {
|
protected OutputStream doCompress(OutputStream out) throws IOException {
|
||||||
return writeAndClose(content, WRAPPER);
|
return new GZIPOutputStream(out);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
protected byte[] doDecompress(byte[] compressed) throws IOException {
|
protected InputStream doDecompress(InputStream is) throws IOException {
|
||||||
return readAndClose(new GZIPInputStream(new ByteArrayInputStream(compressed)));
|
return new GZIPInputStream(is);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -41,15 +41,14 @@ public abstract class AbstractParserBuilder<T, B extends ParserBuilder<T, B>> im
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public B deserializer(Deserializer<Map<String, ?>> deserializer) {
|
public B json(Deserializer<Map<String, ?>> reader) {
|
||||||
this.deserializer = deserializer;
|
this.deserializer = reader;
|
||||||
return self();
|
return self();
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public final Parser<T> build() {
|
public final Parser<T> build() {
|
||||||
if (this.deserializer == null) {
|
if (this.deserializer == null) {
|
||||||
// try to find one based on the services available:
|
|
||||||
//noinspection unchecked
|
//noinspection unchecked
|
||||||
this.deserializer = Services.loadFirst(Deserializer.class);
|
this.deserializer = Services.loadFirst(Deserializer.class);
|
||||||
}
|
}
|
||||||
|
|
|
@ -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 > 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 <= 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 <= 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 <= 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 <= 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.");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
|
@ -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 <= 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 <= 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 <= 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 <= 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);
|
||||||
|
// }
|
||||||
|
}
|
|
@ -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 <= 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 <= 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 <= 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 <= 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);
|
||||||
|
}
|
||||||
|
}
|
|
@ -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);
|
||||||
|
}
|
||||||
|
}
|
|
@ -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} > 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 > 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 > 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 > 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 > 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 > 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;
|
||||||
|
}
|
||||||
|
}
|
|
@ -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;
|
||||||
|
}
|
||||||
|
}
|
|
@ -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);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
|
@ -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();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -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;
|
||||||
|
}
|
||||||
|
}
|
|
@ -23,15 +23,15 @@ import io.jsonwebtoken.io.Encoder;
|
||||||
import io.jsonwebtoken.io.Encoders;
|
import io.jsonwebtoken.io.Encoders;
|
||||||
import io.jsonwebtoken.lang.Assert;
|
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 BASE64 = new Codec(Encoders.BASE64, Decoders.BASE64);
|
||||||
public static final Codec BASE64URL = new Codec(Encoders.BASE64URL, Decoders.BASE64URL);
|
public static final Codec BASE64URL = new Codec(Encoders.BASE64URL, Decoders.BASE64URL);
|
||||||
|
|
||||||
private final Encoder<byte[], String> encoder;
|
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.encoder = Assert.notNull(encoder, "Encoder cannot be null.");
|
||||||
this.decoder = Assert.notNull(decoder, "Decoder cannot be null.");
|
this.decoder = Assert.notNull(decoder, "Decoder cannot be null.");
|
||||||
}
|
}
|
||||||
|
@ -42,7 +42,7 @@ public class Codec implements Converter<byte[], String> {
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public byte[] applyFrom(String b) {
|
public byte[] applyFrom(CharSequence b) {
|
||||||
try {
|
try {
|
||||||
return this.decoder.decode(b);
|
return this.decoder.decode(b);
|
||||||
} catch (DecodingException e) {
|
} catch (DecodingException e) {
|
||||||
|
|
|
@ -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
|
||||||
|
}
|
|
@ -17,65 +17,33 @@ package io.jsonwebtoken.impl.io;
|
||||||
|
|
||||||
import io.jsonwebtoken.impl.lang.Converter;
|
import io.jsonwebtoken.impl.lang.Converter;
|
||||||
import io.jsonwebtoken.impl.lang.Function;
|
import io.jsonwebtoken.impl.lang.Function;
|
||||||
import io.jsonwebtoken.io.DeserializationException;
|
|
||||||
import io.jsonwebtoken.io.Deserializer;
|
|
||||||
import io.jsonwebtoken.io.Parser;
|
import io.jsonwebtoken.io.Parser;
|
||||||
import io.jsonwebtoken.lang.Assert;
|
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;
|
import java.util.Map;
|
||||||
|
|
||||||
public class ConvertingParser<T> implements Parser<T> {
|
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 Converter<T, Object> converter;
|
||||||
private final Function<Throwable, RuntimeException> exceptionHandler;
|
|
||||||
|
|
||||||
public ConvertingParser(Deserializer<Map<String, ?>> deserializer, Converter<T, Object> converter,
|
public ConvertingParser(Function<InputStream, Map<String, ?>> deserializer, Converter<T, Object> converter) {
|
||||||
Function<Throwable, RuntimeException> exceptionHandler) {
|
this.deserializer = Assert.notNull(deserializer, "Deserializer function cannot be null.");
|
||||||
this.deserializer = Assert.notNull(deserializer, "Deserializer cannot be null.");
|
this.converter = Assert.notNull(converter, "Converter 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;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
|
|
||||||
public final T parse(String input) {
|
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);
|
return this.converter.applyFrom(m);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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;
|
||||||
|
}
|
||||||
|
}
|
|
@ -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);
|
||||||
|
}
|
||||||
|
}
|
|
@ -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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -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);
|
||||||
|
}
|
||||||
|
}
|
|
@ -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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
|
@ -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);
|
||||||
|
}
|
||||||
|
}
|
|
@ -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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -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);
|
||||||
|
}
|
||||||
|
}
|
|
@ -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;
|
||||||
|
}
|
||||||
|
}
|
|
@ -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);
|
||||||
|
}
|
|
@ -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<byte[], Object> BASE64URL_BYTES = Converters.forEncoded(byte[].class, Codec.BASE64URL);
|
||||||
|
|
||||||
public static final Converter<X509Certificate, Object> X509_CERTIFICATE =
|
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, byte[]> BIGINT_UBYTES = new BigIntegerUBytesConverter();
|
||||||
public static final Converter<BigInteger, Object> BIGINT = Converters.forEncoded(BigInteger.class,
|
public static final Converter<BigInteger, Object> BIGINT = Converters.forEncoded(BigInteger.class,
|
||||||
compound(BIGINT_UBYTES, Codec.BASE64URL));
|
compound(BIGINT_UBYTES, Codec.BASE64URL));
|
||||||
|
|
||||||
//prevent instantiation
|
//prevent instantiation
|
||||||
private Converters() {
|
private Converters() {
|
||||||
|
@ -53,7 +53,7 @@ public final class Converters {
|
||||||
return CollectionConverter.forList(elementConverter);
|
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);
|
return new EncodedObjectConverter<>(elementType, elementConverter);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -20,9 +20,9 @@ import io.jsonwebtoken.lang.Assert;
|
||||||
public class EncodedObjectConverter<T> implements Converter<T, Object> {
|
public class EncodedObjectConverter<T> implements Converter<T, Object> {
|
||||||
|
|
||||||
private final Class<T> type;
|
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.type = Assert.notNull(type, "Value type cannot be null.");
|
||||||
this.converter = Assert.notNull(converter, "Value converter 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.");
|
Assert.notNull(value, "Value argument cannot be null.");
|
||||||
if (type.isInstance(value)) {
|
if (type.isInstance(value)) {
|
||||||
return type.cast(value);
|
return type.cast(value);
|
||||||
} else if (value instanceof String) {
|
} else if (value instanceof CharSequence) {
|
||||||
return converter.applyFrom((String) value);
|
return converter.applyFrom((CharSequence) value);
|
||||||
} else {
|
} else {
|
||||||
String msg = "Values must be either String or " + type.getName() +
|
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);
|
throw new IllegalArgumentException(msg);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -32,6 +32,10 @@ public class PropagatingExceptionFunction<T, R, E extends RuntimeException> impl
|
||||||
this(new DelegatingCheckedFunction<>(f), exceptionClass, new ConstantFunction<T, String>(msg));
|
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) {
|
public PropagatingExceptionFunction(CheckedFunction<T, R> fn, Class<E> exceptionClass, final Supplier<String> msgSupplier) {
|
||||||
this(fn, exceptionClass, new Function<T, String>() {
|
this(fn, exceptionClass, new Function<T, String>() {
|
||||||
@Override
|
@Override
|
||||||
|
|
|
@ -19,7 +19,7 @@ import io.jsonwebtoken.lang.Assert;
|
||||||
|
|
||||||
import java.net.URI;
|
import java.net.URI;
|
||||||
|
|
||||||
public class UriStringConverter implements Converter<URI, String> {
|
public class UriStringConverter implements Converter<URI, CharSequence> {
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public String applyTo(URI uri) {
|
public String applyTo(URI uri) {
|
||||||
|
@ -28,10 +28,10 @@ public class UriStringConverter implements Converter<URI, String> {
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public URI applyFrom(String s) {
|
public URI applyFrom(CharSequence s) {
|
||||||
Assert.hasText(s, "URI string cannot be null or empty.");
|
Assert.hasText(s, "URI string cannot be null or empty.");
|
||||||
try {
|
try {
|
||||||
return URI.create(s);
|
return URI.create(s.toString());
|
||||||
} catch (Exception e) {
|
} catch (Exception e) {
|
||||||
String msg = "Unable to convert String value '" + s + "' to URI instance: " + e.getMessage();
|
String msg = "Unable to convert String value '" + s + "' to URI instance: " + e.getMessage();
|
||||||
throw new IllegalArgumentException(msg, e);
|
throw new IllegalArgumentException(msg, e);
|
||||||
|
|
|
@ -31,6 +31,8 @@ import io.jsonwebtoken.security.JwkThumbprint;
|
||||||
import io.jsonwebtoken.security.Jwks;
|
import io.jsonwebtoken.security.Jwks;
|
||||||
import io.jsonwebtoken.security.KeyOperation;
|
import io.jsonwebtoken.security.KeyOperation;
|
||||||
|
|
||||||
|
import java.io.ByteArrayInputStream;
|
||||||
|
import java.io.InputStream;
|
||||||
import java.nio.charset.StandardCharsets;
|
import java.nio.charset.StandardCharsets;
|
||||||
import java.security.Key;
|
import java.security.Key;
|
||||||
import java.security.PrivateKey;
|
import java.security.PrivateKey;
|
||||||
|
@ -150,7 +152,8 @@ public abstract class AbstractJwk<K extends Key> implements Jwk<K>, ParameterRea
|
||||||
String json = toThumbprintJson();
|
String json = toThumbprintJson();
|
||||||
Assert.hasText(json, "Canonical JWK Thumbprint JSON cannot be null or empty.");
|
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[] 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);
|
return new DefaultJwkThumbprint(digest, alg);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -23,6 +23,7 @@ import io.jsonwebtoken.security.SecurityException;
|
||||||
import io.jsonwebtoken.security.SignatureException;
|
import io.jsonwebtoken.security.SignatureException;
|
||||||
import io.jsonwebtoken.security.VerifySecureDigestRequest;
|
import io.jsonwebtoken.security.VerifySecureDigestRequest;
|
||||||
|
|
||||||
|
import java.io.InputStream;
|
||||||
import java.security.Key;
|
import java.security.Key;
|
||||||
|
|
||||||
abstract class AbstractSecureDigestAlgorithm<S extends Key, V extends Key> extends CryptoAlgorithm implements SecureDigestAlgorithm<S, V> {
|
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);
|
protected abstract void validateKey(Key key, boolean signing);
|
||||||
|
|
||||||
@Override
|
@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.");
|
Assert.notNull(request, "Request cannot be null.");
|
||||||
final S key = Assert.notNull(request.getKey(), "Signing key 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 {
|
try {
|
||||||
validateKey(key, true);
|
validateKey(key, true);
|
||||||
return doDigest(request);
|
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
|
@Override
|
||||||
public final boolean verify(VerifySecureDigestRequest<V> request) throws SecurityException {
|
public final boolean verify(VerifySecureDigestRequest<V> request) throws SecurityException {
|
||||||
Assert.notNull(request, "Request cannot be null.");
|
Assert.notNull(request, "Request cannot be null.");
|
||||||
final V key = Assert.notNull(request.getKey(), "Verification key 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.");
|
Assert.notEmpty(request.getDigest(), "Request signature byte array cannot be null or empty.");
|
||||||
try {
|
try {
|
||||||
validateKey(key, false);
|
validateKey(key, false);
|
||||||
|
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue